Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 534d0c9

Browse files
committed
feat: add coderd_template resource
1 parent c0065c3 commit 534d0c9

File tree

7 files changed

+336
-0
lines changed

7 files changed

+336
-0
lines changed

integration/integration_test.go

+5
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, true)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a

integration/template-test/main.tf

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
version {
13+
name = "v1"
14+
directory = "./example-template"
15+
}
16+
version {
17+
name = "v2"
18+
directory = "./example-template-2"
19+
}
20+
}

internal/provider/provider.go

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ func (p *CoderdProvider) Resources(ctx context.Context) []func() resource.Resour
123123
return []func() resource.Resource{
124124
NewUserResource,
125125
NewGroupResource,
126+
NewTemplateResource,
126127
}
127128
}
128129

+238
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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+
Version []TemplateVersion `tfsdk:"version"`
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+
Active types.Bool `tfsdk:"active"`
46+
}
47+
48+
func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
49+
resp.TypeName = req.ProviderTypeName + "_template"
50+
}
51+
52+
func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
53+
resp.Schema = schema.Schema{
54+
MarkdownDescription: "A Coder template",
55+
56+
Blocks: map[string]schema.Block{
57+
"version": schema.ListNestedBlock{
58+
NestedObject: schema.NestedBlockObject{
59+
Attributes: map[string]schema.Attribute{
60+
"name": schema.StringAttribute{
61+
MarkdownDescription: "The name of the template version. Automatically generated if not provided.",
62+
Optional: true,
63+
},
64+
"message": schema.StringAttribute{
65+
MarkdownDescription: "A message describing the changes in this version of the template. Messages longer than 72 characters will be truncated..",
66+
Optional: true,
67+
},
68+
"directory": schema.StringAttribute{
69+
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.",
70+
Required: true,
71+
},
72+
"directory_hash": schema.StringAttribute{
73+
Computed: true,
74+
PlanModifiers: []planmodifier.String{
75+
NewDirectoryHashPlanModifier(),
76+
},
77+
},
78+
"active": schema.BoolAttribute{
79+
MarkdownDescription: "Whether this version is the active version of the template. Only one version can be active at a time.",
80+
Optional: true,
81+
},
82+
},
83+
},
84+
},
85+
},
86+
87+
Attributes: map[string]schema.Attribute{
88+
"id": schema.StringAttribute{
89+
MarkdownDescription: "The ID of the template.",
90+
Computed: true,
91+
},
92+
"name": schema.StringAttribute{
93+
MarkdownDescription: "The name of the template.",
94+
Required: true,
95+
},
96+
"display_name": schema.StringAttribute{
97+
MarkdownDescription: "The display name of the template. Defaults to the template name.",
98+
Optional: true,
99+
},
100+
"description": schema.StringAttribute{
101+
MarkdownDescription: "A description of the template.",
102+
Optional: true,
103+
},
104+
// TODO: Rest of the fields
105+
"organization_id": schema.StringAttribute{
106+
MarkdownDescription: "The ID of the organization. Defaults to the provider's default organization",
107+
Optional: true,
108+
},
109+
},
110+
}
111+
}
112+
113+
func (r *TemplateResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
114+
// Prevent panic if the provider has not been configured.
115+
if req.ProviderData == nil {
116+
return
117+
}
118+
119+
data, ok := req.ProviderData.(*CoderdProviderData)
120+
121+
if !ok {
122+
resp.Diagnostics.AddError(
123+
"Unexpected Resource Configure Type",
124+
fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
125+
)
126+
127+
return
128+
}
129+
130+
r.data = data
131+
}
132+
133+
func (r *TemplateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
134+
var data TemplateResourceModel
135+
136+
// Read Terraform plan data into the model
137+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
138+
if resp.Diagnostics.HasError() {
139+
return
140+
}
141+
142+
// TODO: Placeholder
143+
data.ID = types.StringValue(uuid.New().String())
144+
// client := r.data.Client
145+
// orgID, err := uuid.Parse(data.OrganizationID.ValueString())
146+
// if err != nil {
147+
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied organization ID as UUID, got error: %s", err))
148+
// return
149+
// }
150+
151+
// Save data into Terraform state
152+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
153+
}
154+
155+
func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
156+
var data TemplateResourceModel
157+
158+
// Read Terraform prior state data into the model
159+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
160+
161+
if resp.Diagnostics.HasError() {
162+
return
163+
}
164+
165+
// client := r.data.Client
166+
167+
// Save updated data into Terraform state
168+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
169+
}
170+
171+
func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
172+
var data TemplateResourceModel
173+
174+
// Read Terraform plan data into the model
175+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
176+
177+
if resp.Diagnostics.HasError() {
178+
return
179+
}
180+
181+
// client := r.data.Client
182+
183+
// Save updated data into Terraform state
184+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
185+
}
186+
187+
func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
188+
var data TemplateResourceModel
189+
190+
// Read Terraform prior state data into the model
191+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
192+
193+
if resp.Diagnostics.HasError() {
194+
return
195+
}
196+
197+
// client := r.data.Client
198+
}
199+
200+
func (r *TemplateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
201+
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
202+
}
203+
204+
type directoryHashPlanModifier struct{}
205+
206+
func NewDirectoryHashPlanModifier() planmodifier.String {
207+
return &directoryHashPlanModifier{}
208+
}
209+
210+
// Description implements planmodifier.String.
211+
func (m *directoryHashPlanModifier) Description(context.Context) string {
212+
return "Recomputes the directory hash if the directory has changed."
213+
}
214+
215+
// MarkdownDescription implements planmodifier.String.
216+
func (m *directoryHashPlanModifier) MarkdownDescription(ctx context.Context) string {
217+
return m.Description(ctx)
218+
}
219+
220+
// PlanModifyString implements planmodifier.String.
221+
func (m *directoryHashPlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
222+
var directory types.String
223+
diags := req.Config.GetAttribute(ctx, req.Path.ParentPath().AtName("directory"), &directory)
224+
resp.Diagnostics.Append(diags...)
225+
if resp.Diagnostics.HasError() {
226+
return
227+
}
228+
229+
hash, err := computeDirectoryHash(directory.ValueString())
230+
if err != nil {
231+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to compute directory hash: %s", err))
232+
return
233+
}
234+
235+
resp.PlanValue = types.StringValue(hash)
236+
}
237+
238+
var _ planmodifier.String = &directoryHashPlanModifier{}
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

+30
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
)
610

711
func PtrTo[T any](v T) *T {
@@ -46,3 +50,29 @@ func PrintOrNull(v any) string {
4650
panic(fmt.Errorf("unknown type in template: %T", value))
4751
}
4852
}
53+
54+
func computeDirectoryHash(directory string) (string, error) {
55+
var files []string
56+
err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
57+
if err != nil {
58+
return err
59+
}
60+
if !info.IsDir() {
61+
files = append(files, path)
62+
}
63+
return nil
64+
})
65+
if err != nil {
66+
return "", err
67+
}
68+
69+
hash := sha256.New()
70+
for _, file := range files {
71+
data, err := os.ReadFile(file)
72+
if err != nil {
73+
return "", err
74+
}
75+
hash.Write(data)
76+
}
77+
return hex.EncodeToString(hash.Sum(nil)), nil
78+
}

0 commit comments

Comments
 (0)