-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(flag): add support of flag as a whole resource
- Loading branch information
Showing
5 changed files
with
298 additions
and
144 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
package provider | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strconv" | ||
|
||
"github.com/ctfer-io/go-ctfd/api" | ||
"github.com/ctfer-io/terraform-provider-ctfd/provider/utils" | ||
"github.com/ctfer-io/terraform-provider-ctfd/provider/validators" | ||
"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/planmodifier" | ||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" | ||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" | ||
"github.com/hashicorp/terraform-plugin-framework/schema/validator" | ||
"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 = (*flagResource)(nil) | ||
_ resource.ResourceWithConfigure = (*flagResource)(nil) | ||
_ resource.ResourceWithImportState = (*flagResource)(nil) | ||
) | ||
|
||
func NewFlagResource() resource.Resource { | ||
return &flagResource{} | ||
} | ||
|
||
type flagResource struct { | ||
client *api.Client | ||
} | ||
|
||
type flagResourceModel struct { | ||
ID types.String `tfsdk:"id"` | ||
ChallengeID types.String `tfsdk:"challenge_id"` | ||
Content types.String `tfsdk:"content"` | ||
Data types.String `tfsdk:"data"` | ||
Type types.String `tfsdk:"type"` | ||
} | ||
|
||
func (r *flagResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { | ||
resp.TypeName = req.ProviderTypeName + "_flag" | ||
} | ||
|
||
func (r *flagResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { | ||
resp.Schema = schema.Schema{ | ||
MarkdownDescription: "", | ||
Attributes: map[string]schema.Attribute{ | ||
"id": schema.StringAttribute{ | ||
MarkdownDescription: "Identifier of the flag, used internally to handle the CTFd corresponding object.", | ||
Computed: true, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.UseStateForUnknown(), | ||
}, | ||
}, | ||
"challenge_id": schema.StringAttribute{ | ||
MarkdownDescription: "Challenge of the flag.", | ||
Required: true, | ||
}, | ||
"content": schema.StringAttribute{ | ||
MarkdownDescription: "The actual flag to match. Consider using the convention `MYCTF{value}` with `MYCTF` being the shortcode of your event's name and `value` depending on each challenge.", | ||
Required: true, | ||
Sensitive: true, | ||
}, | ||
"data": schema.StringAttribute{ | ||
MarkdownDescription: "The flag sensitivity information, either case_sensitive or case_insensitive", | ||
Optional: true, | ||
Computed: true, | ||
// default value is "" (empty string) according to Web UI | ||
Default: stringdefault.StaticString("case_sensitive"), | ||
Validators: []validator.String{ | ||
validators.NewStringEnumValidator([]basetypes.StringValue{ | ||
types.StringValue("case_sensitive"), | ||
types.StringValue("case_insensitive"), | ||
}), | ||
}, | ||
}, | ||
"type": schema.StringAttribute{ | ||
MarkdownDescription: "The type of the flag, could be either static or regex", | ||
Optional: true, | ||
Computed: true, | ||
// default value is "static" according to ctfcli | ||
Default: stringdefault.StaticString("static"), | ||
Validators: []validator.String{ | ||
validators.NewStringEnumValidator([]basetypes.StringValue{ | ||
types.StringValue("static"), | ||
types.StringValue("regex"), | ||
}), | ||
}, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.RequiresReplace(), | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func (r *flagResource) 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 *flagResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { | ||
var data flagResourceModel | ||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
// Create flag | ||
res, err := r.client.PostFlags(&api.PostFlagsParams{ | ||
Challenge: utils.Atoi(data.ChallengeID.ValueString()), | ||
Content: data.Content.ValueString(), | ||
Data: data.Data.ValueString(), | ||
Type: data.Type.ValueString(), | ||
}, api.WithContext(ctx)) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
"Client Error", | ||
fmt.Sprintf("Unable to create flag, got error: %s", err), | ||
) | ||
return | ||
} | ||
|
||
tflog.Trace(ctx, "created a flag") | ||
|
||
// 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 *flagResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { | ||
var data flagResourceModel | ||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
// Retrieve flag | ||
res, err := r.client.GetFlag(data.ID.ValueString(), api.WithContext(ctx)) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
"Client Error", | ||
fmt.Sprintf("Unable to read flag %s, got error: %s", data.ID.ValueString(), err), | ||
) | ||
return | ||
} | ||
|
||
// Upsert values | ||
data.ChallengeID = types.StringValue(strconv.Itoa(res.ChallengeID)) | ||
data.Content = types.StringValue(res.Content) | ||
data.Data = types.StringValue(res.Data) | ||
data.Type = types.StringValue(res.Type) | ||
|
||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) | ||
} | ||
|
||
func (r *flagResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { | ||
var data flagResourceModel | ||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
// Update flag | ||
if _, err := r.client.PatchFlag(data.ID.ValueString(), &api.PatchFlagParams{ | ||
ID: data.ID.ValueString(), | ||
Content: data.Content.ValueString(), | ||
Data: data.Data.ValueString(), | ||
Type: data.Type.ValueString(), | ||
}, api.WithContext(ctx)); err != nil { | ||
resp.Diagnostics.AddError( | ||
"Client Error", | ||
fmt.Sprintf("Unable to update flag %s, got error: %s", data.ID.ValueString(), err), | ||
) | ||
return | ||
} | ||
|
||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) | ||
} | ||
|
||
func (r *flagResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { | ||
var data flagResourceModel | ||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
if err := r.client.DeleteFlag(data.ID.ValueString(), api.WithContext(ctx)); err != nil { | ||
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete flag %s, got error: %s", data.ID.ValueString(), err)) | ||
return | ||
} | ||
} | ||
|
||
func (r *flagResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { | ||
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) | ||
|
||
// Automatically call r.Read | ||
} |
Oops, something went wrong.