From 55c355913a1e7fe37dcb546926ca4e7fbb79e182 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Tue, 30 Jul 2024 05:11:30 +0000
Subject: [PATCH] feat: add coderd_workspace_proxy resource

---
 docs/resources/workspace_proxy.md             |  30 +++
 internal/provider/provider.go                 |   1 +
 internal/provider/workspace_proxy_resource.go | 201 ++++++++++++++++++
 .../provider/workspace_proxy_resource_test.go |  92 ++++++++
 4 files changed, 324 insertions(+)
 create mode 100644 docs/resources/workspace_proxy.md
 create mode 100644 internal/provider/workspace_proxy_resource.go
 create mode 100644 internal/provider/workspace_proxy_resource_test.go

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 generated by tfplugindocs -->
+## 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()
+}