diff --git a/go.mod b/go.mod index cc9e901..7e9074c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/ctfer-io/terraform-provider-ctfd go 1.23.2 require ( - github.com/ctfer-io/go-ctfd v0.10.0 + github.com/ctfer-io/go-ctfd v0.10.1 github.com/hashicorp/terraform-plugin-docs v0.20.1 github.com/hashicorp/terraform-plugin-framework v1.13.0 github.com/hashicorp/terraform-plugin-go v0.25.0 diff --git a/go.sum b/go.sum index 5a31d02..545dc82 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZ github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/ctfer-io/go-ctfd v0.10.0 h1:+lBLqde9XNdLB+TzXvJg9I9qkLRA8rYmf9bpjQ2CAUg= -github.com/ctfer-io/go-ctfd v0.10.0/go.mod h1:jNeeXui6iW1jsAfa4D5v6MaK0UF26CtjTRtCqS/PWH4= +github.com/ctfer-io/go-ctfd v0.10.1 h1:Bxr6/qo/h1c0FYFNZv/ej839Hzo5x1VMs0v+Io8ovs8= +github.com/ctfer-io/go-ctfd v0.10.1/go.mod h1:ebgSW8LdP/qtRCpglK4djBp+g6kU5YM98XBZKowiCeY= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -189,8 +189,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= diff --git a/provider/challenge_resource.go b/provider/challenge_resource.go index 2369ba0..48715a3 100644 --- a/provider/challenge_resource.go +++ b/provider/challenge_resource.go @@ -13,9 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" @@ -102,34 +100,23 @@ func (r *challengeResource) Schema(ctx context.Context, req resource.SchemaReque MarkdownDescription: "Decay function to define how the challenge value evolve through solves, either linear or logarithmic.", Optional: true, Computed: true, - Default: defaults.String(stringdefault.StaticString("linear")), - Validators: []validator.String{ - validators.NewStringEnumValidator([]basetypes.StringValue{ - FunctionLinear, - FunctionLogarithmic, - }), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), }, }, "value": schema.Int64Attribute{ MarkdownDescription: "The value (points) of the challenge once solved. Internally, the provider will handle what target is legitimate depending on the `.type` value, i.e. either `value` for \"standard\" or `initial` for \"dynamic\".", Required: true, }, - // XXX decay and minimum are only required if .type == "dynamic" "decay": schema.Int64Attribute{ MarkdownDescription: "The decay defines from each number of solves does the decay function triggers until reaching minimum. This function is defined by CTFd and could be configured through `.function`.", Optional: true, Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, }, "minimum": schema.Int64Attribute{ MarkdownDescription: "The minimum points for a dynamic-score challenge to reach with the decay function. Once there, no solve could have more value.", Optional: true, Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, }, "state": schema.StringAttribute{ MarkdownDescription: "State of the challenge, either hidden or visible.", @@ -228,6 +215,26 @@ func (r *challengeResource) Create(ctx context.Context, req resource.CreateReque return } + // Configuration checks + if data.Type.Equal(types.StringValue("dynamic")) { + if data.Decay.IsNull() { + resp.Diagnostics.AddError("Configuration error", "decay must be set for dynamic challenges") + } + if data.Minimum.IsNull() { + resp.Diagnostics.AddError("Configuration error", "minimum must be set for dynamic challenges") + } + if data.Function.IsNull() || data.Function.IsUnknown() { + data.Function = FunctionLogarithmic + } + } else { + if data.Function.IsNull() || data.Function.IsUnknown() { + data.Function = types.StringNull() + } + } + if resp.Diagnostics.HasError() { + return + } + // Create Challenge reqs := (*api.Requirements)(nil) if data.Requirements != nil { @@ -247,11 +254,11 @@ func (r *challengeResource) Create(ctx context.Context, req resource.CreateReque Description: data.Description.ValueString(), ConnectionInfo: data.ConnectionInfo.ValueStringPointer(), MaxAttempts: utils.ToInt(data.MaxAttempts), - Function: data.Function.ValueString(), + Function: data.Function.ValueStringPointer(), Value: int(data.Value.ValueInt64()), - Initial: utils.ToInt(data.Value), - Decay: utils.ToInt(data.Decay), - Minimum: utils.ToInt(data.Minimum), + Initial: utils.ToIntOnDynamic(data.Value, data.Type), + Decay: utils.ToIntOnDynamic(data.Decay, data.Type), + Minimum: utils.ToIntOnDynamic(data.Minimum, data.Type), State: data.State.ValueString(), Type: data.Type.ValueString(), NextID: utils.ToInt(data.Next), @@ -343,6 +350,22 @@ func (r *challengeResource) Update(ctx context.Context, req resource.UpdateReque var dataState challengeResourceModel req.State.Get(ctx, &dataState) + // Configuration checks + if data.Type.Equal(types.StringValue("dynamic")) { + if data.Decay.IsNull() { + resp.Diagnostics.AddError("Configuration error", "decay must be set for dynamic challenges") + } + if data.Minimum.IsNull() { + resp.Diagnostics.AddError("Configuration error", "minimum must be set for dynamic challenges") + } + if data.Function.IsNull() { + data.Function = FunctionLogarithmic + } + } + if resp.Diagnostics.HasError() { + return + } + // Patch direct attributes reqs := (*api.Requirements)(nil) if data.Requirements != nil { @@ -362,7 +385,7 @@ func (r *challengeResource) Update(ctx context.Context, req resource.UpdateReque Description: data.Description.ValueString(), ConnectionInfo: data.ConnectionInfo.ValueStringPointer(), MaxAttempts: utils.ToInt(data.MaxAttempts), - Function: data.Function.ValueString(), + Function: data.Function.ValueStringPointer(), Value: utils.ToInt(data.Value), Initial: utils.ToInt(data.Value), Decay: utils.ToInt(data.Decay), @@ -499,7 +522,7 @@ func (chall *challengeResourceModel) Read(ctx context.Context, client *api.Clien chall.Description = types.StringValue(res.Description) chall.ConnectionInfo = utils.ToTFString(res.ConnectionInfo) chall.MaxAttempts = utils.ToTFInt64(res.MaxAttempts) - chall.Function = types.StringValue(res.Function) + chall.Function = utils.ToTFString(res.Function) chall.Decay = utils.ToTFInt64(res.Decay) chall.Minimum = utils.ToTFInt64(res.Minimum) chall.State = types.StringValue(res.State) diff --git a/provider/challenge_resource_test.go b/provider/challenge_resource_test.go index 2433f98..3e1e923 100644 --- a/provider/challenge_resource_test.go +++ b/provider/challenge_resource_test.go @@ -87,6 +87,7 @@ resource "ctfd_challenge" "icmp" { - NicolasFgrx EOT value = 500 + type = "standard" requirements = { behavior = "anonymized" @@ -96,6 +97,7 @@ resource "ctfd_challenge" "icmp" { `, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("ctfd_challenge.icmp", "requirements.prerequisites.#", "1"), + resource.TestCheckNoResourceAttr("ctfd_challenge.icmp", "function"), ), }, // Delete testing automatically occurs in TestCase diff --git a/provider/utils/utils.go b/provider/utils/utils.go index b16bdc9..2b138a7 100644 --- a/provider/utils/utils.go +++ b/provider/utils/utils.go @@ -37,6 +37,15 @@ func ToInt(itf types.Int64) *int { return &i } +// ToIntOnDynamic returns the value of itf as an integer pointer iif +// the challType is dynamic. +func ToIntOnDynamic(itf types.Int64, challType types.String) *int { + if challType == types.StringValue("dynamic") { + return ToInt(itf) + } + return nil +} + func Ptr[T any](t T) *T { return &t }