diff --git a/docs/resources/workspace_proxy.md b/docs/resources/workspace_proxy.md new file mode 100644 index 0000000..6047558 --- /dev/null +++ b/docs/resources/workspace_proxy.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coderd_workspace_proxy Resource - coderd" +subcategory: "" +description: |- + A Workspace Proxy for the Coder deployment. +--- + +# coderd_workspace_proxy (Resource) + +A Workspace Proxy for the Coder deployment. + + + + +## Schema + +### Required + +- `icon` (String) Relative path or external URL that specifes an icon to be displayed in the dashboard. +- `name` (String) Name of the workspace proxy. + +### Optional + +- `display_name` (String) Display name of the workspace proxy. + +### Read-Only + +- `id` (String) Workspace Proxy ID +- `session_token` (String) Session token for the workspace proxy. diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 6f9c29b..8b5db9d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -124,6 +124,7 @@ func (p *CoderdProvider) Resources(ctx context.Context) []func() resource.Resour NewUserResource, NewGroupResource, NewTemplateResource, + NewWorkspaceProxyResource, } } diff --git a/internal/provider/workspace_proxy_resource.go b/internal/provider/workspace_proxy_resource.go new file mode 100644 index 0000000..461a7fc --- /dev/null +++ b/internal/provider/workspace_proxy_resource.go @@ -0,0 +1,201 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/coder/coder/v2/codersdk" + "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/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &WorkspaceProxyResource{} + +func NewWorkspaceProxyResource() resource.Resource { + return &WorkspaceProxyResource{} +} + +// WorkspaceProxyResource defines the resource implementation. +type WorkspaceProxyResource struct { + data *CoderdProviderData +} + +// WorkspaceProxyResourceModel describes the resource data model. +type WorkspaceProxyResourceModel struct { + ID UUID `tfsdk:"id"` + Name types.String `tfsdk:"name"` + DisplayName types.String `tfsdk:"display_name"` + Icon types.String `tfsdk:"icon"` + SessionToken types.String `tfsdk:"session_token"` +} + +func (r *WorkspaceProxyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_workspace_proxy" +} + +func (r *WorkspaceProxyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "A Workspace Proxy for the Coder deployment.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + CustomType: UUIDType, + Computed: true, + MarkdownDescription: "Workspace Proxy ID", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the workspace proxy.", + Required: true, + }, + "display_name": schema.StringAttribute{ + MarkdownDescription: "Display name of the workspace proxy.", + Optional: true, + Computed: true, + }, + "icon": schema.StringAttribute{ + MarkdownDescription: "Relative path or external URL that specifes an icon to be displayed in the dashboard.", + Required: true, + }, + "session_token": schema.StringAttribute{ + MarkdownDescription: "Session token for the workspace proxy.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *WorkspaceProxyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + data, ok := req.ProviderData.(*CoderdProviderData) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.data = data +} + +func (r *WorkspaceProxyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data WorkspaceProxyResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + client := r.data.Client + wsp, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{ + Name: data.Name.ValueString(), + DisplayName: data.DisplayName.ValueString(), + Icon: data.Icon.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to create workspace proxy: %v", err)) + return + } + + data.ID = UUIDValue(wsp.Proxy.ID) + data.Name = types.StringValue(wsp.Proxy.Name) + data.DisplayName = types.StringValue(wsp.Proxy.DisplayName) + data.Icon = types.StringValue(wsp.Proxy.IconURL) + data.SessionToken = types.StringValue(wsp.ProxyToken) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *WorkspaceProxyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WorkspaceProxyResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + client := r.data.Client + wsp, err := client.WorkspaceProxyByID(ctx, data.ID.ValueUUID()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to read workspace proxy: %v", err)) + return + } + + data.ID = UUIDValue(wsp.ID) + data.Name = types.StringValue(wsp.Name) + data.DisplayName = types.StringValue(wsp.DisplayName) + data.Icon = types.StringValue(wsp.IconURL) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *WorkspaceProxyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data WorkspaceProxyResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + client := r.data.Client + + wsp, err := client.PatchWorkspaceProxy(ctx, codersdk.PatchWorkspaceProxy{ + ID: data.ID.ValueUUID(), + Name: data.Name.ValueString(), + DisplayName: data.DisplayName.ValueString(), + Icon: data.Icon.ValueString(), + RegenerateToken: false, + }) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to update workspace proxy: %v", err)) + return + } + + data.Name = types.StringValue(wsp.Proxy.Name) + data.DisplayName = types.StringValue(wsp.Proxy.DisplayName) + data.Icon = types.StringValue(wsp.Proxy.IconURL) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *WorkspaceProxyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data WorkspaceProxyResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + client := r.data.Client + err := client.DeleteWorkspaceProxyByID(ctx, data.ID.ValueUUID()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to delete workspace proxy: %v", err)) + return + } +} diff --git a/internal/provider/workspace_proxy_resource_test.go b/internal/provider/workspace_proxy_resource_test.go new file mode 100644 index 0000000..a5a5eba --- /dev/null +++ b/internal/provider/workspace_proxy_resource_test.go @@ -0,0 +1,92 @@ +package provider + +import ( + "context" + "os" + "strings" + "testing" + "text/template" + + "github.com/coder/terraform-provider-coderd/integration" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/stretchr/testify/require" +) + +func TestAccWorkspaceProxyResource(t *testing.T) { + if os.Getenv("TF_ACC") == "" { + t.Skip("Acceptance tests are disabled.") + } + ctx := context.Background() + client := integration.StartCoder(ctx, t, "ws_proxy_acc", true) + + cfg1 := testAccWorkspaceProxyResourceConfig{ + URL: client.URL.String(), + Token: client.SessionToken(), + Name: PtrTo("example"), + DisplayName: PtrTo("Example WS Proxy"), + Icon: PtrTo("/emojis/1f407.png"), + } + + cfg2 := cfg1 + cfg2.Name = PtrTo("example-new") + cfg2.DisplayName = PtrTo("Example WS Proxy New") + + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: cfg1.String(t), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("coderd_workspace_proxy.test", "session_token"), + ), + }, + // Update and Read testing + { + Config: cfg2.String(t), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("coderd_workspace_proxy.test", "session_token")), + }, + }, + }) +} + +type testAccWorkspaceProxyResourceConfig struct { + URL string + Token string + + Name *string + DisplayName *string + Icon *string +} + +func (c testAccWorkspaceProxyResourceConfig) String(t *testing.T) string { + t.Helper() + tpl := ` +provider coderd { + url = "{{.URL}}" + token = "{{.Token}}" +} + +resource "coderd_workspace_proxy" "test" { + name = {{orNull .Name}} + display_name = {{orNull .DisplayName}} + icon = {{orNull .Icon}} +} +` + // Define template functions + funcMap := template.FuncMap{ + "orNull": PrintOrNull, + } + + buf := strings.Builder{} + tmpl, err := template.New("test").Funcs(funcMap).Parse(tpl) + require.NoError(t, err) + + err = tmpl.Execute(&buf, c) + require.NoError(t, err) + + return buf.String() +}