Skip to content

Commit 7c3e1a5

Browse files
authored
feat: Read params from file for template/workspace creation (#1541)
* Read params from file for template/workspace creation * Use os.ReadFile * Refactor reading params into a separate module * Add comments and unit tests * Rename variable * Uncomment and fix unit test * Fix comment * Refactor tests * Fix unit tests for windows * Fix unit tests for Windows * Add comments for the hotfix
1 parent d0fd0d7 commit 7c3e1a5

File tree

7 files changed

+431
-47
lines changed

7 files changed

+431
-47
lines changed

cli/create.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func create() *cobra.Command {
1717
var (
1818
workspaceName string
1919
templateName string
20+
parameterFile string
2021
)
2122
cmd := &cobra.Command{
2223
Annotations: workspaceCommand,
@@ -116,23 +117,33 @@ func create() *cobra.Command {
116117
return err
117118
}
118119

119-
printed := false
120+
// parameterMapFromFile can be nil if parameter file is not specified
121+
var parameterMapFromFile map[string]string
122+
if parameterFile != "" {
123+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Attempting to read the variables from the parameter file.")+"\r\n")
124+
parameterMapFromFile, err = createParameterMapFromFile(parameterFile)
125+
if err != nil {
126+
return err
127+
}
128+
}
129+
130+
disclaimerPrinted := false
120131
parameters := make([]codersdk.CreateParameterRequest, 0)
121132
for _, parameterSchema := range parameterSchemas {
122133
if !parameterSchema.AllowOverrideSource {
123134
continue
124135
}
125-
if !printed {
136+
if !disclaimerPrinted {
126137
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss).")+"\r\n")
127-
printed = true
138+
disclaimerPrinted = true
128139
}
129-
value, err := cliui.ParameterSchema(cmd, parameterSchema)
140+
parameterValue, err := getParameterValueFromMapOrInput(cmd, parameterMapFromFile, parameterSchema)
130141
if err != nil {
131142
return err
132143
}
133144
parameters = append(parameters, codersdk.CreateParameterRequest{
134145
Name: parameterSchema.Name,
135-
SourceValue: value,
146+
SourceValue: parameterValue,
136147
SourceScheme: codersdk.ParameterSourceSchemeData,
137148
DestinationScheme: parameterSchema.DefaultDestinationScheme,
138149
})
@@ -194,5 +205,6 @@ func create() *cobra.Command {
194205
}
195206

196207
cliflag.StringVarP(cmd.Flags(), &templateName, "template", "t", "CODER_TEMPLATE_NAME", "", "Specify a template name.")
208+
cliflag.StringVarP(cmd.Flags(), &parameterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.")
197209
return cmd
198210
}

cli/create_test.go

Lines changed: 111 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli_test
22

33
import (
44
"fmt"
5+
"os"
56
"testing"
67

78
"github.com/stretchr/testify/require"
@@ -113,39 +114,7 @@ func TestCreate(t *testing.T) {
113114

114115
defaultValue := "something"
115116
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
116-
Parse: []*proto.Parse_Response{{
117-
Type: &proto.Parse_Response_Complete{
118-
Complete: &proto.Parse_Complete{
119-
ParameterSchemas: []*proto.ParameterSchema{
120-
{
121-
AllowOverrideSource: true,
122-
Name: "region",
123-
Description: "description 1",
124-
DefaultSource: &proto.ParameterSource{
125-
Scheme: proto.ParameterSource_DATA,
126-
Value: defaultValue,
127-
},
128-
DefaultDestination: &proto.ParameterDestination{
129-
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
130-
},
131-
},
132-
{
133-
AllowOverrideSource: true,
134-
Name: "username",
135-
Description: "description 2",
136-
DefaultSource: &proto.ParameterSource{
137-
Scheme: proto.ParameterSource_DATA,
138-
// No default value
139-
Value: "",
140-
},
141-
DefaultDestination: &proto.ParameterDestination{
142-
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
143-
},
144-
},
145-
},
146-
},
147-
},
148-
}},
117+
Parse: createTestParseResponseWithDefault(defaultValue),
149118
Provision: echo.ProvisionComplete,
150119
ProvisionDryRun: echo.ProvisionComplete,
151120
})
@@ -178,4 +147,113 @@ func TestCreate(t *testing.T) {
178147
}
179148
<-doneChan
180149
})
150+
151+
t.Run("WithParameterFileContainingTheValue", func(t *testing.T) {
152+
t.Parallel()
153+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
154+
user := coderdtest.CreateFirstUser(t, client)
155+
156+
defaultValue := "something"
157+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
158+
Parse: createTestParseResponseWithDefault(defaultValue),
159+
Provision: echo.ProvisionComplete,
160+
ProvisionDryRun: echo.ProvisionComplete,
161+
})
162+
163+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
164+
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
165+
tempDir := t.TempDir()
166+
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
167+
_, _ = parameterFile.WriteString("region: \"bingo\"\nusername: \"boingo\"")
168+
cmd, root := clitest.New(t, "create", "", "--parameter-file", parameterFile.Name())
169+
clitest.SetupConfig(t, client, root)
170+
doneChan := make(chan struct{})
171+
pty := ptytest.New(t)
172+
cmd.SetIn(pty.Input())
173+
cmd.SetOut(pty.Output())
174+
go func() {
175+
defer close(doneChan)
176+
err := cmd.Execute()
177+
require.NoError(t, err)
178+
}()
179+
180+
matches := []string{
181+
"Specify a name", "my-workspace",
182+
"Confirm create?", "yes",
183+
}
184+
for i := 0; i < len(matches); i += 2 {
185+
match := matches[i]
186+
value := matches[i+1]
187+
pty.ExpectMatch(match)
188+
pty.WriteLine(value)
189+
}
190+
<-doneChan
191+
removeTmpDirUntilSuccess(t, tempDir)
192+
})
193+
t.Run("WithParameterFileNotContainingTheValue", func(t *testing.T) {
194+
t.Parallel()
195+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
196+
user := coderdtest.CreateFirstUser(t, client)
197+
198+
defaultValue := "something"
199+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
200+
Parse: createTestParseResponseWithDefault(defaultValue),
201+
Provision: echo.ProvisionComplete,
202+
ProvisionDryRun: echo.ProvisionComplete,
203+
})
204+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
205+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
206+
tempDir := t.TempDir()
207+
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
208+
_, _ = parameterFile.WriteString("zone: \"bananas\"")
209+
cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--parameter-file", parameterFile.Name())
210+
clitest.SetupConfig(t, client, root)
211+
doneChan := make(chan struct{})
212+
pty := ptytest.New(t)
213+
cmd.SetIn(pty.Input())
214+
cmd.SetOut(pty.Output())
215+
go func() {
216+
defer close(doneChan)
217+
err := cmd.Execute()
218+
require.EqualError(t, err, "Parameter value absent in parameter file for \"region\"!")
219+
}()
220+
<-doneChan
221+
removeTmpDirUntilSuccess(t, tempDir)
222+
})
223+
}
224+
225+
func createTestParseResponseWithDefault(defaultValue string) []*proto.Parse_Response {
226+
return []*proto.Parse_Response{{
227+
Type: &proto.Parse_Response_Complete{
228+
Complete: &proto.Parse_Complete{
229+
ParameterSchemas: []*proto.ParameterSchema{
230+
{
231+
AllowOverrideSource: true,
232+
Name: "region",
233+
Description: "description 1",
234+
DefaultSource: &proto.ParameterSource{
235+
Scheme: proto.ParameterSource_DATA,
236+
Value: defaultValue,
237+
},
238+
DefaultDestination: &proto.ParameterDestination{
239+
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
240+
},
241+
},
242+
{
243+
AllowOverrideSource: true,
244+
Name: "username",
245+
Description: "description 2",
246+
DefaultSource: &proto.ParameterSource{
247+
Scheme: proto.ParameterSource_DATA,
248+
// No default value
249+
Value: "",
250+
},
251+
DefaultDestination: &proto.ParameterDestination{
252+
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
253+
},
254+
},
255+
},
256+
},
257+
},
258+
}}
181259
}

cli/parameter.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package cli
2+
3+
import (
4+
"os"
5+
6+
"golang.org/x/xerrors"
7+
"gopkg.in/yaml.v3"
8+
9+
"github.com/coder/coder/cli/cliui"
10+
"github.com/coder/coder/codersdk"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
// Reads a YAML file and populates a string -> string map.
15+
// Throws an error if the file name is empty.
16+
func createParameterMapFromFile(parameterFile string) (map[string]string, error) {
17+
if parameterFile != "" {
18+
parameterMap := make(map[string]string)
19+
20+
parameterFileContents, err := os.ReadFile(parameterFile)
21+
22+
if err != nil {
23+
return nil, err
24+
}
25+
26+
err = yaml.Unmarshal(parameterFileContents, &parameterMap)
27+
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
return parameterMap, nil
33+
}
34+
35+
return nil, xerrors.Errorf("Parameter file name is not specified")
36+
}
37+
38+
// Returns a parameter value from a given map, if the map exists, else takes input from the user.
39+
// Throws an error if the map exists but does not include a value for the parameter.
40+
func getParameterValueFromMapOrInput(cmd *cobra.Command, parameterMap map[string]string, parameterSchema codersdk.ParameterSchema) (string, error) {
41+
var parameterValue string
42+
if parameterMap != nil {
43+
var ok bool
44+
parameterValue, ok = parameterMap[parameterSchema.Name]
45+
if !ok {
46+
return "", xerrors.Errorf("Parameter value absent in parameter file for %q!", parameterSchema.Name)
47+
}
48+
} else {
49+
var err error
50+
parameterValue, err = cliui.ParameterSchema(cmd, parameterSchema)
51+
if err != nil {
52+
return "", err
53+
}
54+
}
55+
return parameterValue, nil
56+
}

cli/parameter_internal_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package cli
2+
3+
import (
4+
"os"
5+
"runtime"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestCreateParameterMapFromFile(t *testing.T) {
12+
t.Parallel()
13+
t.Run("CreateParameterMapFromFile", func(t *testing.T) {
14+
t.Parallel()
15+
tempDir := t.TempDir()
16+
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
17+
_, _ = parameterFile.WriteString("region: \"bananas\"\ndisk: \"20\"\n")
18+
19+
parameterMapFromFile, err := createParameterMapFromFile(parameterFile.Name())
20+
21+
expectedMap := map[string]string{
22+
"region": "bananas",
23+
"disk": "20",
24+
}
25+
26+
assert.Equal(t, expectedMap, parameterMapFromFile)
27+
assert.Nil(t, err)
28+
29+
removeTmpDirUntilSuccess(t, tempDir)
30+
})
31+
t.Run("WithEmptyFilename", func(t *testing.T) {
32+
t.Parallel()
33+
34+
parameterMapFromFile, err := createParameterMapFromFile("")
35+
36+
assert.Nil(t, parameterMapFromFile)
37+
assert.EqualError(t, err, "Parameter file name is not specified")
38+
})
39+
t.Run("WithInvalidFilename", func(t *testing.T) {
40+
t.Parallel()
41+
42+
parameterMapFromFile, err := createParameterMapFromFile("invalidFile.yaml")
43+
44+
assert.Nil(t, parameterMapFromFile)
45+
46+
// On Unix based systems, it is: `open invalidFile.yaml: no such file or directory`
47+
// On Windows, it is `open invalidFile.yaml: The system cannot find the file specified.`
48+
if runtime.GOOS == "windows" {
49+
assert.EqualError(t, err, "open invalidFile.yaml: The system cannot find the file specified.")
50+
} else {
51+
assert.EqualError(t, err, "open invalidFile.yaml: no such file or directory")
52+
}
53+
})
54+
t.Run("WithInvalidYAML", func(t *testing.T) {
55+
t.Parallel()
56+
tempDir := t.TempDir()
57+
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
58+
_, _ = parameterFile.WriteString("region = \"bananas\"\ndisk = \"20\"\n")
59+
60+
parameterMapFromFile, err := createParameterMapFromFile(parameterFile.Name())
61+
62+
assert.Nil(t, parameterMapFromFile)
63+
assert.EqualError(t, err, "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `region ...` into map[string]string")
64+
65+
removeTmpDirUntilSuccess(t, tempDir)
66+
})
67+
}
68+
69+
// Need this for Windows because of a known issue with Go:
70+
// https://github.com/golang/go/issues/52986
71+
func removeTmpDirUntilSuccess(t *testing.T, tempDir string) {
72+
t.Helper()
73+
t.Cleanup(func() {
74+
err := os.RemoveAll(tempDir)
75+
for err != nil {
76+
err = os.RemoveAll(tempDir)
77+
}
78+
})
79+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy