diff --git a/README.md b/README.md index b3584c1f..f3c52dd8 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,10 @@ For example, a fully populated `app.json` file looks like this: }, "APP_SECRET": { "generator": "secret" + }, + "ORDERED_ENV": { + "description": "control the order env variables are prompted", + "order": 100 } }, "options": { @@ -115,6 +119,9 @@ Reference: - `required`, _(optional, default: `true`)_ indicates if they user must provide a value for this variable. - `generator`, _(optional)_ use a generator for the value, currently only support `secret` + - `order`, _(optional)_ if specified, used to indicate the order in which the + variable is prompted to the user. If some variables specify this and some + don't, then the unspecified ones are prompted last. - `options`: _(optional)_ Options when deploying the service - `allow-unauthenticated`: _(optional, default: `true`)_ allow unauthenticated requests - `memory`: _(optional)_ memory for each instance diff --git a/cmd/cloudshell_open/appfile.go b/cmd/cloudshell_open/appfile.go index 949d80ee..52d90de2 100644 --- a/cmd/cloudshell_open/appfile.go +++ b/cmd/cloudshell_open/appfile.go @@ -22,6 +22,7 @@ import ( "io" "os" "path/filepath" + "sort" "github.com/fatih/color" @@ -33,6 +34,7 @@ type env struct { Value string `json:"value"` Required *bool `json:"required"` Generator string `json:"generator"` + Order *int `json:"order"` } type options struct { @@ -191,7 +193,6 @@ func promptOrGenerateEnvs(list map[string]env) ([]string, error) { } func generateEnvs(keys []string) ([]string, error) { - for i, key := range keys { resp, err := rand64String() if err != nil { @@ -203,15 +204,50 @@ func generateEnvs(keys []string) ([]string, error) { return keys, nil } -func promptEnv(list map[string]env) ([]string, error) { - // TODO(ahmetb): remove these defers and make customizations at the - // individual prompt-level once survey lib allows non-global settings. +type envKeyValuePair struct { + k string + v env +} + +type envKeyValuePairs []envKeyValuePair + +func (e envKeyValuePairs) Len() int { return len(e) } + +func (e envKeyValuePairs) Swap(i, j int) { + e[i], e[j] = e[j], e[i] +} + +func (e envKeyValuePairs) Less(i, j int) bool { + // if env.Order is unspecified, it should appear less. + // otherwise, less values show earlier. + if e[i].v.Order == nil { + return false + } + if e[j].v.Order == nil { + return true + } + return *e[i].v.Order < *e[j].v.Order +} +func sortedEnvs(envs map[string]env) []string { + var v envKeyValuePairs + for key, value := range envs { + v = append(v, envKeyValuePair{key, value}) + } + sort.Sort(v) + var keys []string + for _, vv := range v { + keys = append(keys, vv.k) + } + return keys +} + +func promptEnv(list map[string]env) ([]string, error) { var out []string - // TODO(ahmetb): we should ideally use an ordered map structure for Env - // field and prompt the questions as they appear in the app.json file as - // opposed to random order we do here. - for k, e := range list { + sortedKeys := sortedEnvs(list) + + for _, k := range sortedKeys { + e := list[k] var resp string if err := survey.AskOne(&survey.Input{ diff --git a/cmd/cloudshell_open/appfile_test.go b/cmd/cloudshell_open/appfile_test.go index 0d7bdac8..afc5bcea 100644 --- a/cmd/cloudshell_open/appfile_test.go +++ b/cmd/cloudshell_open/appfile_test.go @@ -233,3 +233,25 @@ func TestGetAppFile(t *testing.T) { t.Fatalf("wrong parsed value: got=%#v, expected=%#v", v, expected) } } + +func Test_sortedEnvs(t *testing.T) { + envs := map[string]env{ + "NIL_ORDER": {}, + "ORDER_100": {Order: mkInt(100)}, + "ORDER_0": {Order: mkInt(0)}, + "ORDER_-10": {Order: mkInt(-10)}, + "ORDER_50": {Order: mkInt(50)}, + } + got := sortedEnvs(envs) + expected := []string{ + "ORDER_-10", "ORDER_0", "ORDER_50", "ORDER_100", "NIL_ORDER", + } + + if !reflect.DeepEqual(got, expected) { + t.Fatalf("sorted envs in wrong order: expected:%v\ngot=%v", expected, got) + } +} + +func mkInt(i int) *int { + return &i +}