Skip to content

Commit

Permalink
feat(hint): add support of hint as a whole subresource
Browse files Browse the repository at this point in the history
  • Loading branch information
pandatix committed Jun 2, 2024
1 parent 7b33422 commit d098b1f
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 150 deletions.
24 changes: 0 additions & 24 deletions provider/challenge/hint_subdata_source.go

This file was deleted.

126 changes: 0 additions & 126 deletions provider/challenge/hint_subresource.go

This file was deleted.

243 changes: 243 additions & 0 deletions provider/hint_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package provider

import (
"context"
"fmt"
"strconv"

"github.com/ctfer-io/go-ctfd/api"
"github.com/ctfer-io/terraform-provider-ctfd/provider/utils"
"github.com/hashicorp/terraform-plugin-framework/attr"
"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/int64default"
"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/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

var (
_ resource.Resource = (*hintResource)(nil)
_ resource.ResourceWithConfigure = (*hintResource)(nil)
_ resource.ResourceWithImportState = (*hintResource)(nil)
)

func NewHintResource() resource.Resource {
return &hintResource{}
}

type hintResource struct {
client *api.Client
}

type hintResourceModel struct {
ID types.String `tfsdk:"id"`
ChallengeID types.String `tfsdk:"challenge_id"`
Content types.String `tfsdk:"content"`
Cost types.Int64 `tfsdk:"cost"`
Requirements []types.String `tfsdk:"requirements"`
}

func (r *hintResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hint"
}

func (r *hintResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "A hint for a challenge to help players solve it.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Identifier of the hint, used internally to handle the CTFd corresponding object.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"challenge_id": schema.StringAttribute{
MarkdownDescription: "Challenge of the hint.",
Required: true,
},
"content": schema.StringAttribute{
MarkdownDescription: "Content of the hint as displayed to the end-user.",
Required: true,
},
"cost": schema.Int64Attribute{
MarkdownDescription: "Cost of the hint, and if any specified, the end-user will consume its own (or team) points to get it.",
Computed: true,
Optional: true,
Default: int64default.StaticInt64(0),
},
"requirements": schema.ListAttribute{
MarkdownDescription: "List of the other hints it depends on.",
ElementType: types.StringType,
Computed: true,
Optional: true,
Default: listdefault.StaticValue(basetypes.NewListValueMust(types.StringType, []attr.Value{})),
},
},
}
}

func (r *hintResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*api.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *github.com/ctfer-io/go-ctfd/api.Client, got: %T. Please open an issue at https://github.com/ctfer-io/terraform-provider-ctfd", req.ProviderData),
)
return
}

r.client = client
}

func (r *hintResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data hintResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

// Create hint
reqs := make([]int, 0, len(data.Requirements))
for _, preq := range data.Requirements {
id, _ := strconv.Atoi(preq.ValueString())
reqs = append(reqs, id)
}
res, err := r.client.PostHints(&api.PostHintsParams{
ChallengeID: utils.Atoi(data.ChallengeID.ValueString()),
Content: data.Content.ValueString(),
Cost: int(data.Cost.ValueInt64()),
Requirements: api.Requirements{
Prerequisites: reqs,
},
}, api.WithContext(ctx))
if err != nil {
resp.Diagnostics.AddError(
"Client Error",
fmt.Sprintf("Unable to create hint, got error: %s", err),
)
return
}

tflog.Trace(ctx, "created a hint")

// Save computed attributes in state
data.ID = types.StringValue(strconv.Itoa(res.ID))

if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *hintResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data hintResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

// Retrieve hint
h, err := r.client.GetHint(data.ID.ValueString(), api.WithContext(ctx))
if err != nil {
resp.Diagnostics.AddError(
"Client Error",
fmt.Sprintf("Unable to update hint %s, got error: %s", data.ID.ValueString(), err),
)
return
}
// Forced to pass by all hints as CTFd does not return content for direct query
hints, err := r.client.GetChallengeHints(h.ChallengeID, api.WithContext(ctx))
hint := (*api.Hint)(nil)
for _, h := range hints {
if h.ID == utils.Atoi(data.ID.ValueString()) {
hint = h
break
}
}
if hint == nil {
resp.Diagnostics.AddError(
"CTFd Error",
fmt.Sprintf("Unable to get hint %s of challenge %s, got error: %s", data.ID.ValueString(), data.ChallengeID.ValueString(), err),
)
return
}

// Upsert values
data.ChallengeID = types.StringValue(strconv.Itoa(h.ChallengeID))
data.Content = types.StringValue(*hint.Content)
data.Cost = types.Int64Value(int64(hint.Cost))
reqs := make([]basetypes.StringValue, 0, len(hint.Requirements.Prerequisites))
for _, preq := range hint.Requirements.Prerequisites {
reqs = append(reqs, types.StringValue(strconv.Itoa(preq)))
}
data.Requirements = reqs

if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *hintResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data hintResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

// Update hint
preqs := make([]int, 0, len(data.Requirements))
for _, preq := range data.Requirements {
id, _ := strconv.Atoi(preq.ValueString())
preqs = append(preqs, id)
}
if _, err := r.client.PatchHint(data.ID.ValueString(), &api.PatchHintsParams{
ChallengeID: utils.Atoi(data.ChallengeID.ValueString()),
Content: data.Content.ValueString(),
Cost: int(data.Cost.ValueInt64()),
Requirements: api.Requirements{
Prerequisites: preqs,
},
}, api.WithContext(ctx)); err != nil {
resp.Diagnostics.AddError(
"Client Error",
fmt.Sprintf("Unable to update hint %s, got error: %s", data.ID.ValueString(), err),
)
return
}

if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *hintResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data hintResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

if err := r.client.DeleteHint(data.ID.ValueString(), api.WithContext(ctx)); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete hint %s, got error: %s", data.ID.ValueString(), err))
return
}
}

func (r *hintResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)

// Automatically call r.Read
}
Loading

0 comments on commit d098b1f

Please sign in to comment.