Skip to content

Commit f332bab

Browse files
committed
refactor and move dynamic param rendering
1 parent fec51fc commit f332bab

File tree

3 files changed

+360
-371
lines changed

3 files changed

+360
-371
lines changed

coderd/dynamicparameters/render.go

Lines changed: 196 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,37 @@ package dynamicparameters
22

33
import (
44
"context"
5+
"encoding/json"
6+
"io/fs"
7+
"sync"
58

69
"github.com/google/uuid"
10+
"golang.org/x/sync/errgroup"
711
"golang.org/x/xerrors"
812

913
"github.com/coder/coder/v2/apiversion"
1014
"github.com/coder/coder/v2/coderd/database"
1115
"github.com/coder/coder/v2/coderd/database/dbauthz"
1216
"github.com/coder/coder/v2/coderd/files"
1317
"github.com/coder/preview"
18+
previewtypes "github.com/coder/preview/types"
1419

1520
"github.com/hashicorp/hcl/v2"
1621
)
1722

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.
1836
type Loader struct {
1937
templateVersionID uuid.UUID
2038

@@ -24,7 +42,7 @@ type Loader struct {
2442
terraformValues *database.TemplateVersionTerraformValue
2543
}
2644

27-
func New(ctx context.Context, versionID uuid.UUID) *Loader {
45+
func New(versionID uuid.UUID) *Loader {
2846
return &Loader{
2947
templateVersionID: versionID,
3048
}
@@ -70,7 +88,7 @@ func (r *Loader) Load(ctx context.Context, db database.Store) error {
7088
}
7189

7290
if !r.job.CompletedAt.Valid {
73-
return xerrors.Errorf("job has not completed")
91+
return ErrorTemplateVersionNotReady
7492
}
7593

7694
if r.terraformValues == nil {
@@ -88,23 +106,196 @@ func (r *Loader) loaded() bool {
88106
return r.templateVersion != nil && r.job != nil && r.terraformValues != nil
89107
}
90108

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) {
92110
if !r.loaded() {
93111
return nil, xerrors.New("Load() must be called before Renderer()")
94112
}
95113

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) {
96124
// If they can read the template version, then they can read the file.
97125
fileCtx := dbauthz.AsFileReader(ctx)
98126
templateFS, err := cache.Acquire(fileCtx, r.job.FileID)
99127
if err != nil {
100128
return nil, xerrors.Errorf("acquire template file: %w", err)
101129
}
102130

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
103158
}
104159

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+
}
106296

107-
return nil, nil
297+
func (r *DynamicRenderer) Close() {
298+
r.once.Do(r.close)
108299
}
109300

110301
func ProvisionerVersionSupportsDynamicParameters(version string) bool {

coderd/dynamicparameters/static.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package dynamicparameters
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
"github.com/google/uuid"
8+
"github.com/hashicorp/hcl/v2"
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/v2/coderd/database"
12+
"github.com/coder/coder/v2/coderd/util/ptr"
13+
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
14+
"github.com/coder/preview"
15+
previewtypes "github.com/coder/preview/types"
16+
"github.com/coder/terraform-provider-coder/v2/provider"
17+
)
18+
19+
type StaticRender struct {
20+
staticParams []previewtypes.Parameter
21+
}
22+
23+
func (r *Loader) staticRender(ctx context.Context, db database.Store) (*StaticRender, error) {
24+
dbTemplateVersionParameters, err := db.GetTemplateVersionParameters(ctx, r.templateVersionID)
25+
if err != nil {
26+
return nil, xerrors.Errorf("template version parameters: %w", err)
27+
}
28+
29+
params := make([]previewtypes.Parameter, 0, len(dbTemplateVersionParameters))
30+
for _, it := range dbTemplateVersionParameters {
31+
param := previewtypes.Parameter{
32+
ParameterData: previewtypes.ParameterData{
33+
Name: it.Name,
34+
DisplayName: it.DisplayName,
35+
Description: it.Description,
36+
Type: previewtypes.ParameterType(it.Type),
37+
FormType: provider.ParameterFormType(it.FormType),
38+
Styling: previewtypes.ParameterStyling{},
39+
Mutable: it.Mutable,
40+
DefaultValue: previewtypes.StringLiteral(it.DefaultValue),
41+
Icon: it.Icon,
42+
Options: make([]*previewtypes.ParameterOption, 0),
43+
Validations: make([]*previewtypes.ParameterValidation, 0),
44+
Required: it.Required,
45+
Order: int64(it.DisplayOrder),
46+
Ephemeral: it.Ephemeral,
47+
Source: nil,
48+
},
49+
// Always use the default, since we used to assume the empty string
50+
Value: previewtypes.StringLiteral(it.DefaultValue),
51+
Diagnostics: nil,
52+
}
53+
54+
if it.ValidationError != "" || it.ValidationRegex != "" || it.ValidationMonotonic != "" {
55+
var reg *string
56+
if it.ValidationRegex != "" {
57+
reg = ptr.Ref(it.ValidationRegex)
58+
}
59+
60+
var vMin *int64
61+
if it.ValidationMin.Valid {
62+
vMin = ptr.Ref(int64(it.ValidationMin.Int32))
63+
}
64+
65+
var vMax *int64
66+
if it.ValidationMax.Valid {
67+
vMin = ptr.Ref(int64(it.ValidationMax.Int32))
68+
}
69+
70+
var monotonic *string
71+
if it.ValidationMonotonic != "" {
72+
monotonic = ptr.Ref(it.ValidationMonotonic)
73+
}
74+
75+
param.Validations = append(param.Validations, &previewtypes.ParameterValidation{
76+
Error: it.ValidationError,
77+
Regex: reg,
78+
Min: vMin,
79+
Max: vMax,
80+
Monotonic: monotonic,
81+
})
82+
}
83+
84+
var protoOptions []*sdkproto.RichParameterOption
85+
_ = json.Unmarshal(it.Options, &protoOptions) // Not going to make this fatal
86+
for _, opt := range protoOptions {
87+
param.Options = append(param.Options, &previewtypes.ParameterOption{
88+
Name: opt.Name,
89+
Description: opt.Description,
90+
Value: previewtypes.StringLiteral(opt.Value),
91+
Icon: opt.Icon,
92+
})
93+
}
94+
95+
// Take the form type from the ValidateFormType function. This is a bit
96+
// unfortunate we have to do this, but it will return the default form_type
97+
// for a given set of conditions.
98+
_, param.FormType, _ = provider.ValidateFormType(provider.OptionType(param.Type), len(param.Options), param.FormType)
99+
100+
param.Diagnostics = previewtypes.Diagnostics(param.Valid(param.Value))
101+
params = append(params, param)
102+
}
103+
104+
return &StaticRender{
105+
staticParams: params,
106+
}, nil
107+
}
108+
109+
func (r *StaticRender) Render(_ context.Context, _ uuid.UUID, values map[string]string) (*preview.Output, hcl.Diagnostics) {
110+
params := r.staticParams
111+
for i := range params {
112+
param := &params[i]
113+
paramValue, ok := values[param.Name]
114+
if ok {
115+
param.Value = previewtypes.StringLiteral(paramValue)
116+
} else {
117+
param.Value = param.DefaultValue
118+
}
119+
param.Diagnostics = previewtypes.Diagnostics(param.Valid(param.Value))
120+
}
121+
122+
return &preview.Output{
123+
Parameters: params,
124+
}, hcl.Diagnostics{
125+
{
126+
// Only a warning because the form does still work.
127+
Severity: hcl.DiagWarning,
128+
Summary: "This template version is missing required metadata to support dynamic parameters.",
129+
Detail: "To restore full functionality, please re-import the terraform as a new template version.",
130+
},
131+
}
132+
}
133+
134+
func (r *StaticRender) Close() {}

0 commit comments

Comments
 (0)