Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit8c6fd63

Browse files
committed
feat: load terraform modules when using dynamic parameters
1 parent170f41a commit8c6fd63

File tree

12 files changed

+374
-32
lines changed

12 files changed

+374
-32
lines changed

‎.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ site/stats/
5050
*.tfplan
5151
*.lock.hcl
5252
.terraform/
53+
!coderd/testdata/parameters/modules/.terraform/
5354
!provisioner/terraform/testdata/modules-source-caching/.terraform/
5455

5556
**/.coderv2/*

‎coderd/files/overlay.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package files
2+
3+
import (
4+
"io/fs"
5+
"path"
6+
"strings"
7+
8+
"golang.org/x/xerrors"
9+
)
10+
11+
// overlayFS allows you to "join" together the template files tar file fs.FS
12+
// with the Terraform modules tar file fs.FS. We could potentially turn this
13+
// into something more parameterized/configurable, but the requirements here are
14+
// a _bit_ odd, because every file in the modulesFS includes the
15+
// .terraform/modules/ folder at the beginning of it's path.
16+
typeoverlayFSstruct {
17+
baseFS fs.FS
18+
overlays []Overlay
19+
}
20+
21+
typeOverlaystruct {
22+
Pathstring
23+
fs.FS
24+
}
25+
26+
funcNewOverlayFS(baseFS fs.FS,overlays []Overlay) (fs.FS,error) {
27+
iferr:=valid(baseFS);err!=nil {
28+
returnnil,xerrors.Errorf("baseFS: %w",err)
29+
}
30+
31+
for_,overlay:=rangeoverlays {
32+
iferr:=valid(overlay.FS);err!=nil {
33+
returnnil,xerrors.Errorf("overlayFS: %w",err)
34+
}
35+
}
36+
37+
returnoverlayFS{
38+
baseFS:baseFS,
39+
overlays:overlays,
40+
},nil
41+
}
42+
43+
func (foverlayFS)Open(pstring) (fs.File,error) {
44+
for_,overlay:=rangef.overlays {
45+
ifstrings.HasPrefix(path.Clean(p),overlay.Path) {
46+
returnoverlay.FS.Open(p)
47+
}
48+
}
49+
returnf.baseFS.Open(p)
50+
}
51+
52+
func (foverlayFS)ReadDir(pstring) ([]fs.DirEntry,error) {
53+
for_,overlay:=rangef.overlays {
54+
ifstrings.HasPrefix(path.Clean(p),overlay.Path) {
55+
//nolint:forcetypeassert
56+
returnoverlay.FS.(fs.ReadDirFS).ReadDir(p)
57+
}
58+
}
59+
//nolint:forcetypeassert
60+
returnf.baseFS.(fs.ReadDirFS).ReadDir(p)
61+
}
62+
63+
func (foverlayFS)ReadFile(pstring) ([]byte,error) {
64+
for_,overlay:=rangef.overlays {
65+
ifstrings.HasPrefix(path.Clean(p),overlay.Path) {
66+
//nolint:forcetypeassert
67+
returnoverlay.FS.(fs.ReadFileFS).ReadFile(p)
68+
}
69+
}
70+
//nolint:forcetypeassert
71+
returnf.baseFS.(fs.ReadFileFS).ReadFile(p)
72+
}
73+
74+
// valid checks that the fs.FS implements the required interfaces.
75+
// The fs.FS interface is not sufficient.
76+
funcvalid(fsys fs.FS)error {
77+
_,ok:=fsys.(fs.ReadDirFS)
78+
if!ok {
79+
returnxerrors.New("overlayFS does not implement ReadDirFS")
80+
}
81+
_,ok=fsys.(fs.ReadFileFS)
82+
if!ok {
83+
returnxerrors.New("overlayFS does not implement ReadFileFS")
84+
}
85+
returnnil
86+
}

‎coderd/files/overlay_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package files_test
2+
3+
import (
4+
"io/fs"
5+
"testing"
6+
7+
"github.com/spf13/afero"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/v2/coderd/files"
11+
)
12+
13+
funcTestOverlayFS(t*testing.T) {
14+
t.Parallel()
15+
16+
a:=afero.NewMemMapFs()
17+
afero.WriteFile(a,"main.tf", []byte("terraform {}"),0o644)
18+
afero.WriteFile(a,".terraform/modules/example_module/main.tf", []byte("inaccessible"),0o644)
19+
afero.WriteFile(a,".terraform/modules/other_module/main.tf", []byte("inaccessible"),0o644)
20+
b:=afero.NewMemMapFs()
21+
afero.WriteFile(b,".terraform/modules/modules.json", []byte("{}"),0o644)
22+
afero.WriteFile(b,".terraform/modules/example_module/main.tf", []byte("terraform {}"),0o644)
23+
24+
it,err:=files.NewOverlayFS(afero.NewIOFS(a), []files.Overlay{{
25+
Path:".terraform/modules",
26+
FS:afero.NewIOFS(b),
27+
}})
28+
require.NoError(t,err)
29+
30+
content,err:=fs.ReadFile(it,"main.tf")
31+
require.NoError(t,err)
32+
require.Equal(t,"terraform {}",string(content))
33+
34+
_,err=fs.ReadFile(it,".terraform/modules/other_module/main.tf")
35+
require.Error(t,err)
36+
37+
content,err=fs.ReadFile(it,".terraform/modules/modules.json")
38+
require.NoError(t,err)
39+
require.Equal(t,"{}",string(content))
40+
41+
content,err=fs.ReadFile(it,".terraform/modules/example_module/main.tf")
42+
require.NoError(t,err)
43+
require.Equal(t,"terraform {}",string(content))
44+
}

‎coderd/parameters.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/coder/coder/v2/coderd/database"
1515
"github.com/coder/coder/v2/coderd/database/dbauthz"
16+
"github.com/coder/coder/v2/coderd/files"
1617
"github.com/coder/coder/v2/coderd/httpapi"
1718
"github.com/coder/coder/v2/coderd/httpmw"
1819
"github.com/coder/coder/v2/codersdk"
@@ -68,7 +69,7 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
6869
return
6970
}
7071

71-
fs,err:=api.FileCache.Acquire(fileCtx,fileID)
72+
templateFS,err:=api.FileCache.Acquire(fileCtx,fileID)
7273
iferr!=nil {
7374
httpapi.Write(ctx,rw,http.StatusNotFound, codersdk.Response{
7475
Message:"Internal error fetching template version Terraform.",
@@ -85,6 +86,26 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
8586
tf,err:=api.Database.GetTemplateVersionTerraformValues(ctx,templateVersion.ID)
8687
iferr==nil {
8788
plan=tf.CachedPlan
89+
90+
iftf.CachedModuleFiles.Valid {
91+
moduleFilesFS,err:=api.FileCache.Acquire(fileCtx,tf.CachedModuleFiles.UUID)
92+
iferr!=nil {
93+
httpapi.Write(ctx,rw,http.StatusNotFound, codersdk.Response{
94+
Message:"Internal error fetching Terraform modules.",
95+
Detail:err.Error(),
96+
})
97+
return
98+
}
99+
deferapi.FileCache.Release(tf.CachedModuleFiles.UUID)
100+
templateFS,err=files.NewOverlayFS(templateFS, []files.Overlay{{Path:".terraform/modules",FS:moduleFilesFS}})
101+
iferr!=nil {
102+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
103+
Message:"Internal error creating overlay filesystem.",
104+
Detail:err.Error(),
105+
})
106+
return
107+
}
108+
}
88109
}elseif!xerrors.Is(err,sql.ErrNoRows) {
89110
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
90111
Message:"Failed to retrieve Terraform values for template version",
@@ -124,7 +145,7 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
124145
)
125146

126147
// Send an initial form state, computed without any user input.
127-
result,diagnostics:=preview.Preview(ctx,input,fs)
148+
result,diagnostics:=preview.Preview(ctx,input,templateFS)
128149
response:= codersdk.DynamicParametersResponse{
129150
ID:-1,
130151
Diagnostics:previewtypes.Diagnostics(diagnostics),
@@ -152,7 +173,7 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
152173
return
153174
}
154175
input.ParameterValues=update.Inputs
155-
result,diagnostics:=preview.Preview(ctx,input,fs)
176+
result,diagnostics:=preview.Preview(ctx,input,templateFS)
156177
response:= codersdk.DynamicParametersResponse{
157178
ID:update.ID,
158179
Diagnostics:previewtypes.Diagnostics(diagnostics),

‎coderd/parameters_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/coder/coder/v2/coderd/rbac"
1111
"github.com/coder/coder/v2/codersdk"
1212
"github.com/coder/coder/v2/provisioner/echo"
13+
"github.com/coder/coder/v2/provisioner/terraform"
1314
"github.com/coder/coder/v2/provisionersdk/proto"
1415
"github.com/coder/coder/v2/testutil"
1516
"github.com/coder/websocket"
@@ -132,3 +133,51 @@ func TestDynamicParametersOwnerSSHPublicKey(t *testing.T) {
132133
require.True(t,preview.Parameters[0].Value.Valid())
133134
require.Equal(t,sshKey.PublicKey,preview.Parameters[0].Value.Value.AsString())
134135
}
136+
137+
funcTestDynamicParametersWithTerraformModules(t*testing.T) {
138+
t.Parallel()
139+
140+
cfg:=coderdtest.DeploymentValues(t)
141+
cfg.Experiments= []string{string(codersdk.ExperimentDynamicParameters)}
142+
ownerClient:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerDaemon:true,DeploymentValues:cfg})
143+
owner:=coderdtest.CreateFirstUser(t,ownerClient)
144+
templateAdmin,templateAdminUser:=coderdtest.CreateAnotherUser(t,ownerClient,owner.OrganizationID,rbac.RoleTemplateAdmin())
145+
146+
dynamicParametersTerraformSource,err:=os.ReadFile("testdata/parameters/modules/main.tf")
147+
require.NoError(t,err)
148+
modulesArchive,err:=terraform.GetModulesArchive(os.DirFS("testdata/parameters/modules"))
149+
require.NoError(t,err)
150+
151+
files:=echo.WithExtraFiles(map[string][]byte{
152+
"main.tf":dynamicParametersTerraformSource,
153+
})
154+
files.ProvisionPlan= []*proto.Response{{
155+
Type:&proto.Response_Plan{
156+
Plan:&proto.PlanComplete{
157+
Plan: []byte("{}"),
158+
ModuleFiles:modulesArchive,
159+
},
160+
},
161+
}}
162+
163+
version:=coderdtest.CreateTemplateVersion(t,templateAdmin,owner.OrganizationID,files)
164+
coderdtest.AwaitTemplateVersionJobCompleted(t,templateAdmin,version.ID)
165+
_=coderdtest.CreateTemplate(t,templateAdmin,owner.OrganizationID,version.ID)
166+
167+
ctx:=testutil.Context(t,testutil.WaitShort)
168+
stream,err:=templateAdmin.TemplateVersionDynamicParameters(ctx,templateAdminUser.ID,version.ID)
169+
require.NoError(t,err)
170+
deferstream.Close(websocket.StatusGoingAway)
171+
172+
previews:=stream.Chan()
173+
174+
// Should see the output of the module represented
175+
preview:=testutil.RequireReceive(ctx,t,previews)
176+
require.Equal(t,-1,preview.ID)
177+
require.Empty(t,preview.Diagnostics)
178+
179+
require.Len(t,preview.Parameters,1)
180+
require.Equal(t,"jetbrains_ide",preview.Parameters[0].Name)
181+
require.True(t,preview.Parameters[0].Value.Valid())
182+
require.Equal(t,"CL",preview.Parameters[0].Value.AsString())
183+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
terraform {
2+
required_version=">= 1.0"
3+
4+
required_providers {
5+
coder={
6+
source="coder/coder"
7+
version=">= 0.17"
8+
}
9+
}
10+
}
11+
12+
locals {
13+
jetbrains_ides={
14+
"GO"= {
15+
icon="/icon/goland.svg",
16+
name="GoLand",
17+
identifier="GO",
18+
},
19+
"WS"= {
20+
icon="/icon/webstorm.svg",
21+
name="WebStorm",
22+
identifier="WS",
23+
},
24+
"IU"= {
25+
icon="/icon/intellij.svg",
26+
name="IntelliJ IDEA Ultimate",
27+
identifier="IU",
28+
},
29+
"PY"= {
30+
icon="/icon/pycharm.svg",
31+
name="PyCharm Professional",
32+
identifier="PY",
33+
},
34+
"CL"= {
35+
icon="/icon/clion.svg",
36+
name="CLion",
37+
identifier="CL",
38+
},
39+
"PS"= {
40+
icon="/icon/phpstorm.svg",
41+
name="PhpStorm",
42+
identifier="PS",
43+
},
44+
"RM"= {
45+
icon="/icon/rubymine.svg",
46+
name="RubyMine",
47+
identifier="RM",
48+
},
49+
"RD"= {
50+
icon="/icon/rider.svg",
51+
name="Rider",
52+
identifier="RD",
53+
},
54+
"RR"= {
55+
icon="/icon/rustrover.svg",
56+
name="RustRover",
57+
identifier="RR"
58+
}
59+
}
60+
61+
icon=local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].icon
62+
display_name=local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].name
63+
identifier=data.coder_parameter.jetbrains_ide.value
64+
}
65+
66+
data"coder_parameter""jetbrains_ide" {
67+
type="string"
68+
name="jetbrains_ide"
69+
display_name="JetBrains IDE"
70+
icon="/icon/gateway.svg"
71+
mutable=true
72+
default=sort(keys(local.jetbrains_ides))[0]
73+
74+
dynamic"option" {
75+
for_each=local.jetbrains_ides
76+
content {
77+
icon=option.value.icon
78+
name=option.value.name
79+
value=option.key
80+
}
81+
}
82+
}
83+
84+
output"identifier" {
85+
value=local.identifier
86+
}
87+
88+
output"display_name" {
89+
value=local.display_name
90+
}
91+
92+
output"icon" {
93+
value=local.icon
94+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"jetbrains_gateway","Source":"jetbrains_gateway","Dir":".terraform/modules/jetbrains_gateway"}]}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
terraform {}
2+
3+
module"jetbrains_gateway" {
4+
source="jetbrains_gateway"
5+
}

‎provisioner/terraform/executor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
309309

310310
graphTimings.ingest(createGraphTimingsEvent(timingGraphComplete))
311311

312-
moduleFiles,err:=getModulesArchive(os.DirFS(e.workdir))
312+
moduleFiles,err:=GetModulesArchive(os.DirFS(e.workdir))
313313
iferr!=nil {
314314
// TODO: we probably want to persist this error or make it louder eventually
315315
e.logger.Warn(ctx,"failed to archive terraform modules",slog.Error(err))

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp