Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8a1db7b

Browse files
AbhineetJainkylecarbs
authored andcommitted
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 a0e4212 commit 8a1db7b

7 files changed

+431
-47
lines changed

cli/create.go

+17-5
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

+111-33
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

+56
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

+79
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)