Skip to content

Commit 9d925c0

Browse files
committed
feat: add coderd_template resource
1 parent 40c3e02 commit 9d925c0

File tree

6 files changed

+323
-0
lines changed

6 files changed

+323
-0
lines changed

integration/integration_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ func TestIntegration(t *testing.T) {
101101
assert.Equal(t, group.QuotaAllowance, 100)
102102
},
103103
},
104+
{
105+
name: "template-test",
106+
preF: func(t testing.TB, c *codersdk.Client) {},
107+
assertF: func(t testing.TB, c *codersdk.Client) {},
108+
},
104109
} {
105110
t.Run(tt.name, func(t *testing.T) {
106111
client := StartCoder(ctx, t, tt.name)

integration/template-test/main.tf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
terraform {
2+
required_providers {
3+
coderd = {
4+
source = "coder/coderd"
5+
version = ">=0.0.0"
6+
}
7+
}
8+
}
9+
10+
resource "coderd_template" "sample" {
11+
name = "example-template"
12+
latest = {
13+
directory = "./example-template"
14+
}
15+
}

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ func (p *CoderdProvider) Resources(ctx context.Context) []func() resource.Resour
105105
return []func() resource.Resource{
106106
NewUserResource,
107107
NewGroupResource,
108+
NewTemplateResource,
108109
}
109110
}
110111

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/google/uuid"
8+
"github.com/hashicorp/terraform-plugin-framework/path"
9+
"github.com/hashicorp/terraform-plugin-framework/resource"
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
11+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
12+
"github.com/hashicorp/terraform-plugin-framework/types"
13+
)
14+
15+
// Ensure provider defined types fully satisfy framework interfaces.
16+
var _ resource.Resource = &TemplateResource{}
17+
var _ resource.ResourceWithImportState = &TemplateResource{}
18+
19+
func NewTemplateResource() resource.Resource {
20+
return &TemplateResource{}
21+
}
22+
23+
// TemplateResource defines the resource implementation.
24+
type TemplateResource struct {
25+
data *CoderdProviderData
26+
}
27+
28+
// TemplateResourceModel describes the resource data model.
29+
type TemplateResourceModel struct {
30+
ID types.String `tfsdk:"id"`
31+
32+
Name types.String `tfsdk:"name"`
33+
DisplayName types.String `tfsdk:"display_name"`
34+
Description types.String `tfsdk:"description"`
35+
OrganizationID types.String `tfsdk:"organization_id"`
36+
37+
Latest *TemplateVersion `tfsdk:"latest"`
38+
}
39+
40+
type TemplateVersion struct {
41+
Name types.String `tfsdk:"name"`
42+
Message types.String `tfsdk:"message"`
43+
Directory types.String `tfsdk:"directory"`
44+
DirectoryHash types.String `tfsdk:"directory_hash"`
45+
}
46+
47+
func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
48+
resp.TypeName = req.ProviderTypeName + "_template"
49+
}
50+
51+
func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
52+
resp.Schema = schema.Schema{
53+
MarkdownDescription: "A Coder template",
54+
55+
Attributes: map[string]schema.Attribute{
56+
"id": schema.StringAttribute{
57+
MarkdownDescription: "The ID of the template.",
58+
Computed: true,
59+
},
60+
"name": schema.StringAttribute{
61+
MarkdownDescription: "The name of the template.",
62+
Required: true,
63+
},
64+
"display_name": schema.StringAttribute{
65+
MarkdownDescription: "The display name of the template. Defaults to the template name.",
66+
Optional: true,
67+
},
68+
"description": schema.StringAttribute{
69+
MarkdownDescription: "A description of the template.",
70+
Optional: true,
71+
},
72+
// TODO: Rest of the fields
73+
"organization_id": schema.StringAttribute{
74+
MarkdownDescription: "The ID of the organization. Defaults to the provider's default organization",
75+
Optional: true,
76+
},
77+
"latest": schema.SingleNestedAttribute{
78+
MarkdownDescription: "The latest version of the template.",
79+
Required: true,
80+
Attributes: map[string]schema.Attribute{
81+
"name": schema.StringAttribute{
82+
MarkdownDescription: "The name of the template version. Automatically generated if not provided.",
83+
Optional: true,
84+
},
85+
"message": schema.StringAttribute{
86+
MarkdownDescription: "A message describing the changes in this version of the template. Messages longer than 72 characters will be truncated..",
87+
Optional: true,
88+
},
89+
"directory": schema.StringAttribute{
90+
MarkdownDescription: "A path to the directory to create the template version from. Changes in the directory contents will trigger the creation of a new template version.",
91+
Required: true,
92+
},
93+
"directory_hash": schema.StringAttribute{
94+
Computed: true,
95+
PlanModifiers: []planmodifier.String{
96+
NewDirectoryHashPlanModifier(),
97+
},
98+
},
99+
// TODO: Rest of the fields
100+
},
101+
},
102+
},
103+
}
104+
}
105+
106+
func (r *TemplateResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
107+
// Prevent panic if the provider has not been configured.
108+
if req.ProviderData == nil {
109+
return
110+
}
111+
112+
data, ok := req.ProviderData.(*CoderdProviderData)
113+
114+
if !ok {
115+
resp.Diagnostics.AddError(
116+
"Unexpected Resource Configure Type",
117+
fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
118+
)
119+
120+
return
121+
}
122+
123+
r.data = data
124+
}
125+
126+
func (r *TemplateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
127+
var data TemplateResourceModel
128+
129+
// Read Terraform plan data into the model
130+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
131+
if resp.Diagnostics.HasError() {
132+
return
133+
}
134+
135+
// TODO: Placeholder
136+
data.ID = types.StringValue(uuid.New().String())
137+
// client := r.data.Client
138+
// orgID, err := uuid.Parse(data.OrganizationID.ValueString())
139+
// if err != nil {
140+
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied organization ID as UUID, got error: %s", err))
141+
// return
142+
// }
143+
144+
// Save data into Terraform state
145+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
146+
}
147+
148+
func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
149+
var data TemplateResourceModel
150+
151+
// Read Terraform prior state data into the model
152+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
153+
154+
if resp.Diagnostics.HasError() {
155+
return
156+
}
157+
158+
// client := r.data.Client
159+
160+
// Save updated data into Terraform state
161+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
162+
}
163+
164+
func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
165+
var data TemplateResourceModel
166+
167+
// Read Terraform plan data into the model
168+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
169+
170+
if resp.Diagnostics.HasError() {
171+
return
172+
}
173+
174+
// client := r.data.Client
175+
176+
// Save updated data into Terraform state
177+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
178+
}
179+
180+
func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
181+
var data TemplateResourceModel
182+
183+
// Read Terraform prior state data into the model
184+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
185+
186+
if resp.Diagnostics.HasError() {
187+
return
188+
}
189+
190+
// client := r.data.Client
191+
}
192+
193+
func (r *TemplateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
194+
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
195+
}
196+
197+
type directoryHashPlanModifier struct{}
198+
199+
func NewDirectoryHashPlanModifier() planmodifier.String {
200+
return &directoryHashPlanModifier{}
201+
}
202+
203+
// Description implements planmodifier.String.
204+
func (m *directoryHashPlanModifier) Description(context.Context) string {
205+
return "Recomputes the directory hash if the directory has changed."
206+
}
207+
208+
// MarkdownDescription implements planmodifier.String.
209+
func (m *directoryHashPlanModifier) MarkdownDescription(ctx context.Context) string {
210+
return m.Description(ctx)
211+
}
212+
213+
// PlanModifyString implements planmodifier.String.
214+
func (m *directoryHashPlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
215+
var data TemplateResourceModel
216+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
217+
218+
if resp.Diagnostics.HasError() {
219+
return
220+
}
221+
222+
hash, err := computeDirectoryHash(data.Latest.Directory.ValueString())
223+
if err != nil {
224+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to compute directory hash: %s", err))
225+
return
226+
}
227+
228+
resp.PlanValue = types.StringValue(hash)
229+
}
230+
231+
var _ planmodifier.String = &directoryHashPlanModifier{}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package provider
2+
3+
import (
4+
"strings"
5+
"testing"
6+
"text/template"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestAccTemplateVersionResource(t *testing.T) {}
12+
13+
type testAccTemplateVersionResourceConfig struct {
14+
URL string
15+
Token string
16+
}
17+
18+
func (c testAccTemplateVersionResourceConfig) String(t *testing.T) string {
19+
t.Helper()
20+
tpl := `
21+
provider coderd {
22+
url = "{{.URL}}"
23+
token = "{{.Token}}"
24+
}
25+
26+
resource "coderd_template_version" "test" {}
27+
`
28+
29+
funcMap := template.FuncMap{
30+
"orNull": PrintOrNull(t),
31+
}
32+
33+
buf := strings.Builder{}
34+
tmpl, err := template.New("test").Funcs(funcMap).Parse(tpl)
35+
require.NoError(t, err)
36+
37+
err = tmpl.Execute(&buf, c)
38+
require.NoError(t, err)
39+
40+
return buf.String()
41+
}

internal/provider/util.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package provider
22

33
import (
4+
"crypto/sha256"
5+
"encoding/hex"
46
"fmt"
7+
"os"
8+
"path/filepath"
59
"testing"
610

711
"github.com/stretchr/testify/require"
@@ -53,3 +57,29 @@ func PrintOrNull(t *testing.T) func(v any) string {
5357
}
5458
}
5559
}
60+
61+
func computeDirectoryHash(directory string) (string, error) {
62+
var files []string
63+
err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
64+
if err != nil {
65+
return err
66+
}
67+
if !info.IsDir() {
68+
files = append(files, path)
69+
}
70+
return nil
71+
})
72+
if err != nil {
73+
return "", err
74+
}
75+
76+
hash := sha256.New()
77+
for _, file := range files {
78+
data, err := os.ReadFile(file)
79+
if err != nil {
80+
return "", err
81+
}
82+
hash.Write(data)
83+
}
84+
return hex.EncodeToString(hash.Sum(nil)), nil
85+
}

0 commit comments

Comments
 (0)