Skip to content

Commit 64b92ee

Browse files
authored
feat: Allow inheriting parameters from previous template_versions when updating a template (#2397)
* WIP: feat: Update templates also updates parameters * Insert params for template version update * Working implementation of inherited params * Add "--always-prompt" flag and logging info
1 parent 18973a6 commit 64b92ee

16 files changed

+551
-171
lines changed

cli/parameterslist.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,23 @@ func parameterList() *cobra.Command {
4545
return xerrors.Errorf("get workspace template: %w", err)
4646
}
4747
scopeID = template.ID
48-
49-
case codersdk.ParameterScopeImportJob, "template_version":
50-
scope = string(codersdk.ParameterScopeImportJob)
48+
case codersdk.ParameterImportJob, "template_version":
49+
scope = string(codersdk.ParameterImportJob)
5150
scopeID, err = uuid.Parse(name)
5251
if err != nil {
5352
return xerrors.Errorf("%q must be a uuid for this scope type", name)
5453
}
54+
55+
// Could be a template_version id or a job id. Check for the
56+
// version id.
57+
tv, err := client.TemplateVersion(cmd.Context(), scopeID)
58+
if err == nil {
59+
scopeID = tv.Job.ID
60+
}
61+
5562
default:
5663
return xerrors.Errorf("%q is an unsupported scope, use %v", scope, []codersdk.ParameterScope{
57-
codersdk.ParameterWorkspace, codersdk.ParameterTemplate, codersdk.ParameterScopeImportJob,
64+
codersdk.ParameterWorkspace, codersdk.ParameterTemplate, codersdk.ParameterImportJob,
5865
})
5966
}
6067

cli/templatecreate.go

Lines changed: 87 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,13 @@ func templateCreate() *cobra.Command {
8282
}
8383
spin.Stop()
8484

85-
job, parameters, err := createValidTemplateVersion(cmd, client, organization, database.ProvisionerType(provisioner), resp.Hash, parameterFile)
85+
job, _, err := createValidTemplateVersion(cmd, createValidTemplateVersionArgs{
86+
Client: client,
87+
Organization: organization,
88+
Provisioner: database.ProvisionerType(provisioner),
89+
FileHash: resp.Hash,
90+
ParameterFile: parameterFile,
91+
})
8692
if err != nil {
8793
return err
8894
}
@@ -98,7 +104,6 @@ func templateCreate() *cobra.Command {
98104
createReq := codersdk.CreateTemplateRequest{
99105
Name: templateName,
100106
VersionID: job.ID,
101-
ParameterValues: parameters,
102107
MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()),
103108
MinAutostartIntervalMillis: ptr.Ref(minAutostartInterval.Milliseconds()),
104109
}
@@ -133,14 +138,34 @@ func templateCreate() *cobra.Command {
133138
return cmd
134139
}
135140

136-
func createValidTemplateVersion(cmd *cobra.Command, client *codersdk.Client, organization codersdk.Organization, provisioner database.ProvisionerType, hash string, parameterFile string, parameters ...codersdk.CreateParameterRequest) (*codersdk.TemplateVersion, []codersdk.CreateParameterRequest, error) {
141+
type createValidTemplateVersionArgs struct {
142+
Client *codersdk.Client
143+
Organization codersdk.Organization
144+
Provisioner database.ProvisionerType
145+
FileHash string
146+
ParameterFile string
147+
// Template is only required if updating a template's active version.
148+
Template *codersdk.Template
149+
// ReuseParameters will attempt to reuse params from the Template field
150+
// before prompting the user. Set to false to always prompt for param
151+
// values.
152+
ReuseParameters bool
153+
}
154+
155+
func createValidTemplateVersion(cmd *cobra.Command, args createValidTemplateVersionArgs, parameters ...codersdk.CreateParameterRequest) (*codersdk.TemplateVersion, []codersdk.CreateParameterRequest, error) {
137156
before := time.Now()
138-
version, err := client.CreateTemplateVersion(cmd.Context(), organization.ID, codersdk.CreateTemplateVersionRequest{
157+
client := args.Client
158+
159+
req := codersdk.CreateTemplateVersionRequest{
139160
StorageMethod: codersdk.ProvisionerStorageMethodFile,
140-
StorageSource: hash,
141-
Provisioner: codersdk.ProvisionerType(provisioner),
161+
StorageSource: args.FileHash,
162+
Provisioner: codersdk.ProvisionerType(args.Provisioner),
142163
ParameterValues: parameters,
143-
})
164+
}
165+
if args.Template != nil {
166+
req.TemplateID = args.Template.ID
167+
}
168+
version, err := client.CreateTemplateVersion(cmd.Context(), args.Organization.ID, req)
144169
if err != nil {
145170
return nil, nil, err
146171
}
@@ -175,33 +200,77 @@ func createValidTemplateVersion(cmd *cobra.Command, client *codersdk.Client, org
175200
return nil, nil, err
176201
}
177202

203+
// lastParameterValues are pulled from the current active template version if
204+
// templateID is provided. This allows pulling params from the last
205+
// version instead of prompting if we are updating template versions.
206+
lastParameterValues := make(map[string]codersdk.Parameter)
207+
if args.ReuseParameters && args.Template != nil {
208+
activeVersion, err := client.TemplateVersion(cmd.Context(), args.Template.ActiveVersionID)
209+
if err != nil {
210+
return nil, nil, xerrors.Errorf("Fetch current active template version: %w", err)
211+
}
212+
213+
// We don't want to compute the params, we only want to copy from this scope
214+
values, err := client.Parameters(cmd.Context(), codersdk.ParameterImportJob, activeVersion.Job.ID)
215+
if err != nil {
216+
return nil, nil, xerrors.Errorf("Fetch previous version parameters: %w", err)
217+
}
218+
for _, value := range values {
219+
lastParameterValues[value.Name] = value
220+
}
221+
}
222+
178223
if provisionerd.IsMissingParameterError(version.Job.Error) {
179224
valuesBySchemaID := map[string]codersdk.TemplateVersionParameter{}
180225
for _, parameterValue := range parameterValues {
181226
valuesBySchemaID[parameterValue.SchemaID.String()] = parameterValue
182227
}
228+
183229
sort.Slice(parameterSchemas, func(i, j int) bool {
184230
return parameterSchemas[i].Name < parameterSchemas[j].Name
185231
})
232+
233+
// parameterMapFromFile can be nil if parameter file is not specified
234+
var parameterMapFromFile map[string]string
235+
if args.ParameterFile != "" {
236+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Attempting to read the variables from the parameter file.")+"\r\n")
237+
parameterMapFromFile, err = createParameterMapFromFile(args.ParameterFile)
238+
if err != nil {
239+
return nil, nil, err
240+
}
241+
}
242+
243+
// pulled params come from the last template version
244+
pulled := make([]string, 0)
186245
missingSchemas := make([]codersdk.ParameterSchema, 0)
187246
for _, parameterSchema := range parameterSchemas {
188247
_, ok := valuesBySchemaID[parameterSchema.ID.String()]
189248
if ok {
190249
continue
191250
}
192-
missingSchemas = append(missingSchemas, parameterSchema)
193-
}
194-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("This template has required variables! They are scoped to the template, and not viewable after being set.")+"\r\n")
195251

196-
// parameterMapFromFile can be nil if parameter file is not specified
197-
var parameterMapFromFile map[string]string
198-
if parameterFile != "" {
199-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Attempting to read the variables from the parameter file.")+"\r\n")
200-
parameterMapFromFile, err = createParameterMapFromFile(parameterFile)
201-
if err != nil {
202-
return nil, nil, err
252+
// The file values are handled below. So don't handle them here,
253+
// just check if a value is present in the file.
254+
_, fileOk := parameterMapFromFile[parameterSchema.Name]
255+
if inherit, ok := lastParameterValues[parameterSchema.Name]; ok && !fileOk {
256+
// If the value is not in the param file, and can be pulled from the last template version,
257+
// then don't mark it as missing.
258+
parameters = append(parameters, codersdk.CreateParameterRequest{
259+
CloneID: inherit.ID,
260+
})
261+
pulled = append(pulled, fmt.Sprintf("%q", parameterSchema.Name))
262+
continue
203263
}
264+
265+
missingSchemas = append(missingSchemas, parameterSchema)
204266
}
267+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("This template has required variables! They are scoped to the template, and not viewable after being set."))
268+
if len(pulled) > 0 {
269+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render(fmt.Sprintf("The following parameter values are being pulled from the latest template version: %s.", strings.Join(pulled, ", "))))
270+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Use \"--always-prompt\" flag to change the values."))
271+
}
272+
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\r\n")
273+
205274
for _, parameterSchema := range missingSchemas {
206275
parameterValue, err := getParameterValueFromMapOrInput(cmd, parameterMapFromFile, parameterSchema)
207276
if err != nil {
@@ -218,7 +287,7 @@ func createValidTemplateVersion(cmd *cobra.Command, client *codersdk.Client, org
218287

219288
// This recursion is only 1 level deep in practice.
220289
// The first pass populates the missing parameters, so it does not enter this `if` block again.
221-
return createValidTemplateVersion(cmd, client, organization, provisioner, hash, parameterFile, parameters...)
290+
return createValidTemplateVersion(cmd, args, parameters...)
222291
}
223292

224293
if version.Job.Status != codersdk.ProvisionerJobSucceeded {

cli/templateupdate.go

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ import (
1010
"golang.org/x/xerrors"
1111

1212
"github.com/coder/coder/cli/cliui"
13+
"github.com/coder/coder/coderd/database"
1314
"github.com/coder/coder/codersdk"
1415
"github.com/coder/coder/provisionersdk"
1516
)
1617

1718
func templateUpdate() *cobra.Command {
1819
var (
19-
directory string
20-
provisioner string
20+
directory string
21+
provisioner string
22+
parameterFile string
23+
alwaysPrompt bool
2124
)
2225

2326
cmd := &cobra.Command{
@@ -64,42 +67,30 @@ func templateUpdate() *cobra.Command {
6467
}
6568
spin.Stop()
6669

67-
before := time.Now()
68-
templateVersion, err := client.CreateTemplateVersion(cmd.Context(), organization.ID, codersdk.CreateTemplateVersionRequest{
69-
TemplateID: template.ID,
70-
StorageMethod: codersdk.ProvisionerStorageMethodFile,
71-
StorageSource: resp.Hash,
72-
Provisioner: codersdk.ProvisionerType(provisioner),
70+
job, _, err := createValidTemplateVersion(cmd, createValidTemplateVersionArgs{
71+
Client: client,
72+
Organization: organization,
73+
Provisioner: database.ProvisionerType(provisioner),
74+
FileHash: resp.Hash,
75+
ParameterFile: parameterFile,
76+
Template: &template,
77+
ReuseParameters: !alwaysPrompt,
7378
})
7479
if err != nil {
7580
return err
7681
}
77-
logs, err := client.TemplateVersionLogsAfter(cmd.Context(), templateVersion.ID, before)
78-
if err != nil {
79-
return err
80-
}
81-
for {
82-
log, ok := <-logs
83-
if !ok {
84-
break
85-
}
86-
_, _ = fmt.Printf("%s (%s): %s\n", provisioner, log.Level, log.Output)
87-
}
88-
templateVersion, err = client.TemplateVersion(cmd.Context(), templateVersion.ID)
89-
if err != nil {
90-
return err
91-
}
9282

93-
if templateVersion.Job.Status != codersdk.ProvisionerJobSucceeded {
94-
return xerrors.Errorf("job failed: %s", templateVersion.Job.Error)
83+
if job.Job.Status != codersdk.ProvisionerJobSucceeded {
84+
return xerrors.Errorf("job failed: %s", job.Job.Status)
9585
}
9686

9787
err = client.UpdateActiveTemplateVersion(cmd.Context(), template.ID, codersdk.UpdateActiveTemplateVersion{
98-
ID: templateVersion.ID,
88+
ID: job.ID,
9989
})
10090
if err != nil {
10191
return err
10292
}
93+
10394
_, _ = fmt.Printf("Updated version!\n")
10495
return nil
10596
},
@@ -108,6 +99,8 @@ func templateUpdate() *cobra.Command {
10899
currentDirectory, _ := os.Getwd()
109100
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")
110101
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
102+
cmd.Flags().StringVarP(&parameterFile, "parameter-file", "", "", "Specify a file path with parameter values.")
103+
cmd.Flags().BoolVar(&alwaysPrompt, "always-prompt", false, "Always prompt all parameters. Does not pull parameter values from active template version")
111104
cliui.AllowSkipPrompt(cmd)
112105
// This is for testing!
113106
err := cmd.Flags().MarkHidden("test.provisioner")

0 commit comments

Comments
 (0)