@@ -2,19 +2,37 @@ package dynamicparameters
2
2
3
3
import (
4
4
"context"
5
+ "encoding/json"
6
+ "io/fs"
7
+ "sync"
5
8
6
9
"github.com/google/uuid"
10
+ "golang.org/x/sync/errgroup"
7
11
"golang.org/x/xerrors"
8
12
9
13
"github.com/coder/coder/v2/apiversion"
10
14
"github.com/coder/coder/v2/coderd/database"
11
15
"github.com/coder/coder/v2/coderd/database/dbauthz"
12
16
"github.com/coder/coder/v2/coderd/files"
13
17
"github.com/coder/preview"
18
+ previewtypes "github.com/coder/preview/types"
14
19
15
20
"github.com/hashicorp/hcl/v2"
16
21
)
17
22
23
+ type Renderer interface {
24
+ Render (ctx context.Context , ownerID uuid.UUID , values map [string ]string ) (* preview.Output , hcl.Diagnostics )
25
+ Close ()
26
+ }
27
+
28
+ var (
29
+ ErrorTemplateVersionNotReady error = xerrors .New ("template version job not finished" )
30
+ )
31
+
32
+ // Loader is used to load the necessary coder objects for rendering a template
33
+ // version's parameters. The output is a Renderer, which is the object that uses
34
+ // the cached objects to render the template version's parameters. Closing the
35
+ // Renderer will release the cached files.
18
36
type Loader struct {
19
37
templateVersionID uuid.UUID
20
38
@@ -24,7 +42,7 @@ type Loader struct {
24
42
terraformValues * database.TemplateVersionTerraformValue
25
43
}
26
44
27
- func New (ctx context. Context , versionID uuid.UUID ) * Loader {
45
+ func New (versionID uuid.UUID ) * Loader {
28
46
return & Loader {
29
47
templateVersionID : versionID ,
30
48
}
@@ -70,7 +88,7 @@ func (r *Loader) Load(ctx context.Context, db database.Store) error {
70
88
}
71
89
72
90
if ! r .job .CompletedAt .Valid {
73
- return xerrors . Errorf ( "job has not completed" )
91
+ return ErrorTemplateVersionNotReady
74
92
}
75
93
76
94
if r .terraformValues == nil {
@@ -88,23 +106,196 @@ func (r *Loader) loaded() bool {
88
106
return r .templateVersion != nil && r .job != nil && r .terraformValues != nil
89
107
}
90
108
91
- func (r * Loader ) Renderer (ctx context.Context , cache * files.Cache ) (any , error ) {
109
+ func (r * Loader ) Renderer (ctx context.Context , db database. Store , cache * files.Cache ) (Renderer , error ) {
92
110
if ! r .loaded () {
93
111
return nil , xerrors .New ("Load() must be called before Renderer()" )
94
112
}
95
113
114
+ if ! ProvisionerVersionSupportsDynamicParameters (r .terraformValues .ProvisionerdVersion ) {
115
+ return r .staticRender (ctx , db )
116
+ }
117
+
118
+ return r .dynamicRenderer (ctx , db , cache )
119
+ }
120
+
121
+ // Renderer caches all the necessary files when rendering a template version's
122
+ // parameters. It must be closed after use to release the cached files.
123
+ func (r * Loader ) dynamicRenderer (ctx context.Context , db database.Store , cache * files.Cache ) (* DynamicRenderer , error ) {
96
124
// If they can read the template version, then they can read the file.
97
125
fileCtx := dbauthz .AsFileReader (ctx )
98
126
templateFS , err := cache .Acquire (fileCtx , r .job .FileID )
99
127
if err != nil {
100
128
return nil , xerrors .Errorf ("acquire template file: %w" , err )
101
129
}
102
130
131
+ var moduleFilesFS fs.FS
132
+ if r .terraformValues .CachedModuleFiles .Valid {
133
+ moduleFilesFS , err = cache .Acquire (fileCtx , r .terraformValues .CachedModuleFiles .UUID )
134
+ if err != nil {
135
+ cache .Release (r .job .FileID )
136
+ return nil , xerrors .Errorf ("acquire module files: %w" , err )
137
+ }
138
+ templateFS = files .NewOverlayFS (templateFS , []files.Overlay {{Path : ".terraform/modules" , FS : moduleFilesFS }})
139
+ }
140
+
141
+ plan := json .RawMessage ("{}" )
142
+ if len (r .terraformValues .CachedPlan ) > 0 {
143
+ plan = r .terraformValues .CachedPlan
144
+ }
145
+
146
+ return & DynamicRenderer {
147
+ data : r ,
148
+ templateFS : templateFS ,
149
+ db : db ,
150
+ plan : plan ,
151
+ close : func () {
152
+ cache .Release (r .job .FileID )
153
+ if moduleFilesFS != nil {
154
+ cache .Release (r .terraformValues .CachedModuleFiles .UUID )
155
+ }
156
+ },
157
+ }, nil
103
158
}
104
159
105
- func (r * Loader ) Render (ctx context.Context , ownerID uuid.UUID , values map [string ]string ) (* preview.Output , hcl.Diagnostics ) {
160
+ type DynamicRenderer struct {
161
+ db database.Store
162
+ data * Loader
163
+ templateFS fs.FS
164
+ plan json.RawMessage
165
+
166
+ failedOwners map [uuid.UUID ]error
167
+ currentOwner * previewtypes.WorkspaceOwner
168
+
169
+ once sync.Once
170
+ close func ()
171
+ }
172
+
173
+ func (r * DynamicRenderer ) Render (ctx context.Context , ownerID uuid.UUID , values map [string ]string ) (* preview.Output , hcl.Diagnostics ) {
174
+ err := r .getWorkspaceOwnerData (ctx , ownerID )
175
+ if err != nil || r .currentOwner == nil {
176
+ return nil , hcl.Diagnostics {
177
+ {
178
+ Severity : hcl .DiagError ,
179
+ Summary : "Failed to fetch workspace owner" ,
180
+ Detail : "Please check your permissions or the user may not exist." ,
181
+ Extra : previewtypes.DiagnosticExtra {
182
+ Code : "owner_not_found" ,
183
+ },
184
+ },
185
+ }
186
+ }
187
+
188
+ input := preview.Input {
189
+ PlanJSON : r .data .terraformValues .CachedPlan ,
190
+ ParameterValues : map [string ]string {},
191
+ Owner : * r .currentOwner ,
192
+ }
193
+
194
+ return preview .Preview (ctx , input , r .templateFS )
195
+ }
196
+
197
+ func (r * DynamicRenderer ) getWorkspaceOwnerData (ctx context.Context , ownerID uuid.UUID ) error {
198
+ if r .currentOwner != nil && r .currentOwner .ID == ownerID .String () {
199
+ return nil // already fetched
200
+ }
201
+
202
+ if r .failedOwners [ownerID ] != nil {
203
+ // previously failed, do not try again
204
+ return r .failedOwners [ownerID ]
205
+ }
206
+
207
+ var g errgroup.Group
208
+
209
+ // TODO: @emyrk we should only need read access on the org member, not the
210
+ // site wide user object. Figure out a better way to handle this.
211
+ user , err := r .db .GetUserByID (ctx , ownerID )
212
+ if err != nil {
213
+ return xerrors .Errorf ("fetch user: %w" , err )
214
+ }
215
+
216
+ var ownerRoles []previewtypes.WorkspaceOwnerRBACRole
217
+ g .Go (func () error {
218
+ // nolint:gocritic // This is kind of the wrong query to use here, but it
219
+ // matches how the provisioner currently works. We should figure out
220
+ // something that needs less escalation but has the correct behavior.
221
+ row , err := r .db .GetAuthorizationUserRoles (dbauthz .AsSystemRestricted (ctx ), ownerID )
222
+ if err != nil {
223
+ return err
224
+ }
225
+ roles , err := row .RoleNames ()
226
+ if err != nil {
227
+ return err
228
+ }
229
+ ownerRoles = make ([]previewtypes.WorkspaceOwnerRBACRole , 0 , len (roles ))
230
+ for _ , it := range roles {
231
+ if it .OrganizationID != uuid .Nil && it .OrganizationID != r .data .templateVersion .OrganizationID {
232
+ continue
233
+ }
234
+ var orgID string
235
+ if it .OrganizationID != uuid .Nil {
236
+ orgID = it .OrganizationID .String ()
237
+ }
238
+ ownerRoles = append (ownerRoles , previewtypes.WorkspaceOwnerRBACRole {
239
+ Name : it .Name ,
240
+ OrgID : orgID ,
241
+ })
242
+ }
243
+ return nil
244
+ })
245
+
246
+ var publicKey string
247
+ g .Go (func () error {
248
+ // The correct public key has to be sent. This will not be leaked
249
+ // unless the template leaks it.
250
+ // nolint:gocritic
251
+ key , err := r .db .GetGitSSHKey (dbauthz .AsSystemRestricted (ctx ), ownerID )
252
+ if err != nil {
253
+ return err
254
+ }
255
+ publicKey = key .PublicKey
256
+ return nil
257
+ })
258
+
259
+ var groupNames []string
260
+ g .Go (func () error {
261
+ // The groups need to be sent to preview. These groups are not exposed to the
262
+ // user, unless the template does it through the parameters. Regardless, we need
263
+ // the correct groups, and a user might not have read access.
264
+ // nolint:gocritic
265
+ groups , err := r .db .GetGroups (dbauthz .AsSystemRestricted (ctx ), database.GetGroupsParams {
266
+ OrganizationID : r .data .templateVersion .OrganizationID ,
267
+ HasMemberID : ownerID ,
268
+ })
269
+ if err != nil {
270
+ return err
271
+ }
272
+ groupNames = make ([]string , 0 , len (groups ))
273
+ for _ , it := range groups {
274
+ groupNames = append (groupNames , it .Group .Name )
275
+ }
276
+ return nil
277
+ })
278
+
279
+ err = g .Wait ()
280
+ if err != nil {
281
+ return err
282
+ }
283
+
284
+ r .currentOwner = & previewtypes.WorkspaceOwner {
285
+ ID : user .ID .String (),
286
+ Name : user .Username ,
287
+ FullName : user .Name ,
288
+ Email : user .Email ,
289
+ LoginType : string (user .LoginType ),
290
+ RBACRoles : ownerRoles ,
291
+ SSHPublicKey : publicKey ,
292
+ Groups : groupNames ,
293
+ }
294
+ return nil
295
+ }
106
296
107
- return nil , nil
297
+ func (r * DynamicRenderer ) Close () {
298
+ r .once .Do (r .close )
108
299
}
109
300
110
301
func ProvisionerVersionSupportsDynamicParameters (version string ) bool {
0 commit comments