Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 899e04e

Browse files
committedJul 22, 2024·
feat: add coderd_template resource
1 parent cae8c17 commit 899e04e

File tree

13 files changed

+1023
-3
lines changed

13 files changed

+1023
-3
lines changed
 

‎docs/resources/template.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "coderd_template Resource - coderd"
4+
subcategory: ""
5+
description: |-
6+
A Coder template
7+
---
8+
9+
# coderd_template (Resource)
10+
11+
A Coder template
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Required
19+
20+
- `name` (String) The name of the template.
21+
- `versions` (Attributes List) (see [below for nested schema](#nestedatt--versions))
22+
23+
### Optional
24+
25+
- `description` (String) A description of the template.
26+
- `display_name` (String) The display name of the template. Defaults to the template name.
27+
- `organization_id` (String) The ID of the organization. Defaults to the provider's default organization
28+
29+
### Read-Only
30+
31+
- `id` (String) The ID of the template.
32+
33+
<a id="nestedatt--versions"></a>
34+
### Nested Schema for `versions`
35+
36+
Required:
37+
38+
- `directory` (String) 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.
39+
40+
Optional:
41+
42+
- `active` (Boolean) Whether this version is the active version of the template. Only one version can be active at a time.
43+
- `message` (String) A message describing the changes in this version of the template. Messages longer than 72 characters will be truncated.
44+
- `name` (String) The name of the template version. Automatically generated if not provided.
45+
- `vars` (Attributes Set) (see [below for nested schema](#nestedatt--versions--vars))
46+
47+
Read-Only:
48+
49+
- `directory_hash` (String)
50+
- `id` (String)
51+
52+
<a id="nestedatt--versions--vars"></a>
53+
### Nested Schema for `versions.vars`
54+
55+
Required:
56+
57+
- `name` (String)
58+
- `value` (String)

‎go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ require (
120120
github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect
121121
github.com/shopspring/decimal v1.3.1 // indirect
122122
github.com/spaolacci/murmur3 v1.1.0 // indirect
123+
github.com/spf13/afero v1.11.0 // indirect
123124
github.com/spf13/cast v1.6.0 // indirect
124125
github.com/spf13/pflag v1.0.5 // indirect
125126
github.com/tinylib/msgp v1.1.8 // indirect

‎go.sum

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6 h1:KHblWIE/KHOwQ6lEbMZt6YpcGve2FEZ1sDtrW1Am5UI=
22
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ=
3-
cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o=
3+
cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
44
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
55
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
66
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
@@ -81,8 +81,6 @@ github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo
8181
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
8282
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
8383
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
84-
github.com/coder/coder/v2 v2.13.0 h1:MlkRGqQcCAdwIkLc9iV8sQfT4jB3EThHopG0jF3BuFE=
85-
github.com/coder/coder/v2 v2.13.0/go.mod h1:Gxc79InMB6b+sncuDUORtFLWi7aKshvis3QrMUhpq5Q=
8684
github.com/coder/coder/v2 v2.13.1 h1:tCd8ljqIAufbVcBr8ODS1QbsrjJbmOIvgDkvdd/JMXc=
8785
github.com/coder/coder/v2 v2.13.1/go.mod h1:Gxc79InMB6b+sncuDUORtFLWi7aKshvis3QrMUhpq5Q=
8886
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
@@ -134,6 +132,8 @@ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
134132
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
135133
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
136134
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
135+
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
136+
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
137137
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
138138
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
139139
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
@@ -390,6 +390,8 @@ github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L
390390
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
391391
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
392392
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
393+
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
394+
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
393395
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
394396
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
395397
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=

‎integration/integration_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ func TestIntegration(t *testing.T) {
105105
assert.Equal(t, group.QuotaAllowance, 100)
106106
},
107107
},
108+
{
109+
name: "template-test",
110+
preF: func(t testing.TB, c *codersdk.Client) {},
111+
assertF: func(t testing.TB, c *codersdk.Client) {},
112+
},
108113
} {
109114
t.Run(tt.name, func(t *testing.T) {
110115
client := StartCoder(ctx, t, tt.name, true)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
resource "local_file" "a" {
2+
filename = "${path.module}/a.txt"
3+
content = "hello world"
4+
}
5+
6+
output "a" {
7+
value = local_file.a.content
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
variable "name" {
2+
type = string
3+
}
4+
5+
resource "local_file" "a" {
6+
filename = "${path.module}/a.txt"
7+
content = "hello ${var.name}"
8+
}
9+
10+
output "a" {
11+
value = local_file.a.content
12+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name = "world"

‎integration/template-test/main.tf

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
versions = [
13+
{
14+
name = "v1"
15+
directory = "./example-template"
16+
active = true
17+
vars = [
18+
{
19+
name = "name"
20+
value = "world"
21+
},
22+
]
23+
}
24+
]
25+
}

‎internal/provider/logger.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
6+
"cdr.dev/slog"
7+
"github.com/hashicorp/terraform-plugin-log/tflog"
8+
)
9+
10+
var _ slog.Sink = &tfLogSink{}
11+
12+
type tfLogSink struct {
13+
tfCtx context.Context
14+
}
15+
16+
func newTFLogSink(tfCtx context.Context) *tfLogSink {
17+
return &tfLogSink{
18+
tfCtx: tfCtx,
19+
}
20+
}
21+
22+
func (s *tfLogSink) LogEntry(ctx context.Context, e slog.SinkEntry) {
23+
var logFn func(ctx context.Context, msg string, additionalFields ...map[string]interface{})
24+
switch e.Level {
25+
case slog.LevelDebug:
26+
logFn = tflog.Debug
27+
case slog.LevelInfo:
28+
logFn = tflog.Info
29+
case slog.LevelWarn:
30+
logFn = tflog.Warn
31+
default:
32+
logFn = tflog.Error
33+
}
34+
logFn(s.tfCtx, e.Message, mapToFields(e.Fields))
35+
}
36+
37+
func (s *tfLogSink) Sync() {}
38+
39+
func mapToFields(m slog.Map) map[string]interface{} {
40+
fields := make(map[string]interface{}, len(m))
41+
for _, v := range m {
42+
fields[v.Name] = v.Value
43+
}
44+
return fields
45+
}

‎internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
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

‎internal/provider/template_resource.go

Lines changed: 630 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"regexp"
6+
"strings"
7+
"testing"
8+
"text/template"
9+
10+
"github.com/coder/coder/v2/codersdk"
11+
"github.com/coder/terraform-provider-coderd/integration"
12+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestAccTemplateResource(t *testing.T) {
17+
ctx := context.Background()
18+
client := integration.StartCoder(ctx, t, "group_acc", false)
19+
firstUser, err := client.User(ctx, codersdk.Me)
20+
require.NoError(t, err)
21+
cfg1 := testAccTemplateResourceConfig{
22+
URL: client.URL.String(),
23+
Token: client.SessionToken(),
24+
Name: PtrTo("example-template"),
25+
Versions: []testAccTemplateVersionConfig{
26+
{
27+
Name: PtrTo("main"),
28+
Directory: PtrTo("../../integration/template-test/example-template/"),
29+
Active: PtrTo(true),
30+
Variables: []testAccTemplateVariableConfig{
31+
{
32+
Name: PtrTo("name"),
33+
Value: PtrTo("world"),
34+
},
35+
},
36+
},
37+
},
38+
}
39+
cfg2 := cfg1
40+
cfg2.Name = PtrTo("example-template-new")
41+
cfg2.Versions[0].Directory = PtrTo("../../integration/template-test/example-template-2/")
42+
cfg2.Versions[0].Name = PtrTo("main2")
43+
44+
cfg3 := cfg2
45+
cfg3.Versions = append(cfg3.Versions, testAccTemplateVersionConfig{
46+
Name: PtrTo("legacy-template"),
47+
Directory: PtrTo("../../integration/template-test/example-template/"),
48+
Active: PtrTo(false),
49+
Variables: []testAccTemplateVariableConfig{
50+
{
51+
Name: PtrTo("name"),
52+
Value: PtrTo("world"),
53+
},
54+
},
55+
})
56+
57+
cfg4 := cfg3
58+
cfg4.Versions[0].Active = PtrTo(false)
59+
cfg4.Versions[1].Active = PtrTo(true)
60+
61+
resource.Test(t, resource.TestCase{
62+
IsUnitTest: true,
63+
PreCheck: func() { testAccPreCheck(t) },
64+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
65+
Steps: []resource.TestStep{
66+
{
67+
Config: cfg1.String(t),
68+
Check: resource.ComposeTestCheckFunc(
69+
resource.TestCheckResourceAttrSet("coderd_template.test", "id"),
70+
resource.TestCheckResourceAttr("coderd_template.test", "display_name", "example-template"),
71+
resource.TestCheckResourceAttr("coderd_template.test", "description", ""),
72+
resource.TestCheckResourceAttr("coderd_template.test", "organization_id", firstUser.OrganizationIDs[0].String()),
73+
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "versions.*", map[string]*regexp.Regexp{
74+
"name": regexp.MustCompile("main"),
75+
"id": regexp.MustCompile(".*"),
76+
"directory_hash": regexp.MustCompile(".+"),
77+
"message": regexp.MustCompile(""),
78+
}),
79+
),
80+
},
81+
// Import
82+
{
83+
Config: cfg1.String(t),
84+
ResourceName: "coderd_template.test",
85+
ImportState: true,
86+
ImportStateVerify: true,
87+
// In the real world, `versions` needs to be added to the configuration after importing
88+
ImportStateVerifyIgnore: []string{"versions"},
89+
},
90+
// Update existing version
91+
{
92+
Config: cfg2.String(t),
93+
Check: resource.ComposeAggregateTestCheckFunc(
94+
resource.TestCheckResourceAttrSet("coderd_template.test", "id"),
95+
resource.TestCheckResourceAttr("coderd_template.test", "name", "example-template-new"),
96+
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "versions.*", map[string]*regexp.Regexp{
97+
"name": regexp.MustCompile("main2"),
98+
}),
99+
),
100+
},
101+
// Append version
102+
{
103+
Config: cfg3.String(t),
104+
Check: resource.ComposeAggregateTestCheckFunc(
105+
resource.TestCheckResourceAttr("coderd_template.test", "versions.#", "2"),
106+
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "versions.*", map[string]*regexp.Regexp{
107+
"name": regexp.MustCompile("main"),
108+
}),
109+
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "versions.*", map[string]*regexp.Regexp{
110+
"name": regexp.MustCompile("main2"),
111+
}),
112+
),
113+
},
114+
// Change active version
115+
{
116+
Config: cfg4.String(t),
117+
Check: resource.ComposeAggregateTestCheckFunc(
118+
resource.TestCheckResourceAttr("coderd_template.test", "versions.#", "2"),
119+
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "versions.*", map[string]*regexp.Regexp{
120+
"active": regexp.MustCompile("true"),
121+
}),
122+
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "versions.*", map[string]*regexp.Regexp{
123+
"active": regexp.MustCompile("false"),
124+
}),
125+
),
126+
},
127+
},
128+
})
129+
}
130+
131+
type testAccTemplateResourceConfig struct {
132+
URL string
133+
Token string
134+
135+
Name *string
136+
DisplayName *string
137+
Description *string
138+
OrganizationID *string
139+
Versions []testAccTemplateVersionConfig
140+
}
141+
142+
func (c testAccTemplateResourceConfig) String(t *testing.T) string {
143+
t.Helper()
144+
tpl := `
145+
provider coderd {
146+
url = "{{.URL}}"
147+
token = "{{.Token}}"
148+
}
149+
150+
resource "coderd_template" "test" {
151+
name = {{orNull .Name}}
152+
display_name = {{orNull .DisplayName}}
153+
description = {{orNull .Description}}
154+
organization_id = {{orNull .OrganizationID}}
155+
156+
versions = [
157+
{{- range .Versions }}
158+
{
159+
name = {{orNull .Name}}
160+
directory = {{orNull .Directory}}
161+
active = {{orNull .Active}}
162+
163+
vars = [
164+
{{- range .Variables }}
165+
{
166+
name = {{orNull .Name}}
167+
value = {{orNull .Value}}
168+
},
169+
{{- end}}
170+
]
171+
},
172+
{{- end}}
173+
]
174+
}
175+
`
176+
177+
funcMap := template.FuncMap{
178+
"orNull": PrintOrNull,
179+
}
180+
181+
buf := strings.Builder{}
182+
tmpl, err := template.New("test").Funcs(funcMap).Parse(tpl)
183+
require.NoError(t, err)
184+
185+
err = tmpl.Execute(&buf, c)
186+
require.NoError(t, err)
187+
188+
return buf.String()
189+
}
190+
191+
type testAccTemplateVersionConfig struct {
192+
Name *string
193+
Message *string
194+
Directory *string
195+
Active *bool
196+
Variables []testAccTemplateVariableConfig
197+
}
198+
199+
type testAccTemplateVariableConfig struct {
200+
Name *string
201+
Value *string
202+
}

‎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
)
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)
Please sign in to comment.