From d1cd8a3ef0d6444bf0dd025301bd1deb90e7d9d8 Mon Sep 17 00:00:00 2001 From: chrisghill Date: Fri, 29 Nov 2024 16:20:54 -0700 Subject: [PATCH] Add clearer prompts and better importing logic for 'mass bundle new' --- .golangci.yaml | 2 +- pkg/bundle/prompt.go | 85 +++++++++-- pkg/params/params.go | 12 +- pkg/provisioners/helm.go | 57 ++++++++ pkg/provisioners/helm_test.go | 132 ++++++++++++++++++ .../testdata/helm/initializetest/Chart.yaml | 0 .../initializetest/templates/deployment.yaml | 0 .../testdata/helm/initializetest/values.yaml | 0 pkg/provisioners/testdata/helm/same.yaml | 2 + pkg/provisioners/types.go | 3 +- 10 files changed, 274 insertions(+), 19 deletions(-) create mode 100644 pkg/provisioners/helm.go create mode 100644 pkg/provisioners/helm_test.go create mode 100644 pkg/provisioners/testdata/helm/initializetest/Chart.yaml create mode 100644 pkg/provisioners/testdata/helm/initializetest/templates/deployment.yaml create mode 100644 pkg/provisioners/testdata/helm/initializetest/values.yaml create mode 100644 pkg/provisioners/testdata/helm/same.yaml diff --git a/.golangci.yaml b/.golangci.yaml index 7d81d6f..af4dbdc 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -162,7 +162,7 @@ linters: - nakedret # Finds naked returns in functions greater than a specified function length # - nestif # Reports deeply nested if statements - nilerr # Finds the code that returns nil even if it checks that the error is not nil. - - nilnil # Checks that there is no simultaneous return of nil error and an invalid value. + # - nilnil # Checks that there is no simultaneous return of nil error and an invalid value. - noctx # noctx finds sending http request without context.Context - nolintlint # Reports ill-formed or insufficient nolint directives # - nonamedreturns # Reports all named returns diff --git a/pkg/bundle/prompt.go b/pkg/bundle/prompt.go index ab59057..d333fed 100644 --- a/pkg/bundle/prompt.go +++ b/pkg/bundle/prompt.go @@ -4,6 +4,9 @@ import ( "errors" "fmt" "maps" + "os" + "path" + "path/filepath" "regexp" "sort" "strings" @@ -119,21 +122,18 @@ func getTemplate(t *templatecache.TemplateData) error { Items: filteredTemplates, } - _, result, err := prompt.Run() + _, templateName, err := prompt.Run() if err != nil { return err } - t.TemplateName = result + t.TemplateName = templateName - // "helm-chart" doesn't exist yet but seems like the right thing to call the template - if result == "terraform-module" || result == "opentofu-module" || result == "helm-chart" || result == "bicep-template" { - paramPath, paramsErr := getExistingParamsPath(result) - if paramsErr != nil { - return paramsErr - } - t.ExistingParamsPath = paramPath + paramsPath, paramsErr := getExistingParamsPath(templateName) + if paramsErr != nil { + return paramsErr } + t.ExistingParamsPath = paramsPath return nil } @@ -232,9 +232,70 @@ func getOutputDir(t *templatecache.TemplateData) error { return nil } -func getExistingParamsPath(in string) (string, error) { - prompt := promptui.Prompt{ - Label: fmt.Sprintf("Path to an existing %s to generate params from, leave blank to skip", in), +func getExistingParamsPath(templateName string) (string, error) { + prompt := promptui.Prompt{} + + switch templateName { + case "terraform-module", "opentofu-module": + prompt.Label = "Path to an existing Terraform/OpenTofu module to generate a bundle from, leave blank to skip" + prompt.Validate = func(input string) error { + if input == "" { + return nil + } + pathInfo, statErr := os.Stat(input) + if statErr != nil { + return statErr + } + if !pathInfo.IsDir() { + return errors.New("path must be a directory containing a Terraform/OpenTofu module") + } + matches, err := filepath.Glob(path.Join(input, "*.tf")) + if err != nil { + return errors.New("unable to read directory") + } + if len(matches) == 0 { + return errors.New("path does not contain any '.tf' files, and therefore isn't a valid Terraform/OpenTofu module") + } + return nil + } + case "helm-chart": + prompt.Label = "Path to an existing Helm chart to generate a bundle from, leave blank to skip" + prompt.Validate = func(input string) error { + if input == "" { + return nil + } + pathInfo, statErr := os.Stat(input) + if statErr != nil { + return statErr + } + if !pathInfo.IsDir() { + return errors.New("path must be a directory containing a helm chart") + } + if _, chartErr := os.Stat(path.Join(input, "Chart.yaml")); errors.Is(chartErr, os.ErrNotExist) { + return errors.New("path does not contain 'Chart.yaml' file, and therefore isn't a valid Helm chart") + } + if _, valuesErr := os.Stat(path.Join(input, "values.yaml")); errors.Is(valuesErr, os.ErrNotExist) { + return errors.New("path does not contain 'values.yaml' file, and therefore isn't a valid Helm chart") + } + return nil + } + case "bicep-template": + prompt.Label = "Path to an existing Bicep template file to generate a bundle from, leave blank to skip" + prompt.Validate = func(input string) error { + if input == "" { + return nil + } + pathInfo, statErr := os.Stat(input) + if statErr != nil { + return statErr + } + if pathInfo.IsDir() { + return errors.New("path must be a file containing a Bicep template") + } + return nil + } + default: + return "", nil } return prompt.Run() diff --git a/pkg/params/params.go b/pkg/params/params.go index a6f62cf..4080a6d 100644 --- a/pkg/params/params.go +++ b/pkg/params/params.go @@ -1,6 +1,8 @@ package params import ( + "path" + "github.com/massdriver-cloud/airlock/pkg/bicep" "github.com/massdriver-cloud/airlock/pkg/helm" "github.com/massdriver-cloud/airlock/pkg/opentofu" @@ -8,8 +10,8 @@ import ( "sigs.k8s.io/yaml" ) -func GetFromPath(templateName, path string) (string, error) { - if path == "" { +func GetFromPath(templateName, paramsPath string) (string, error) { + if paramsPath == "" { return "", nil } @@ -20,17 +22,17 @@ func GetFromPath(templateName, path string) (string, error) { switch templateName { case "terraform-module", "opentofu-module": - paramSchema, err = opentofu.TofuToSchema(path) + paramSchema, err = opentofu.TofuToSchema(paramsPath) if err != nil { return "", err } case "helm-chart": - paramSchema, err = helm.HelmToSchema(path) + paramSchema, err = helm.HelmToSchema(path.Join(paramsPath, "values.yaml")) if err != nil { return "", err } case "bicep-template": - paramSchema, err = bicep.BicepToSchema(path) + paramSchema, err = bicep.BicepToSchema(paramsPath) if err != nil { return "", err } diff --git a/pkg/provisioners/helm.go b/pkg/provisioners/helm.go new file mode 100644 index 0000000..3bfe716 --- /dev/null +++ b/pkg/provisioners/helm.go @@ -0,0 +1,57 @@ +package provisioners + +import ( + "encoding/json" + "errors" + "os" + "path" + + "github.com/massdriver-cloud/airlock/pkg/helm" +) + +type HelmProvisioner struct{} + +func (p *HelmProvisioner) ExportMassdriverInputs(stepPath string, variables map[string]interface{}) error { + // Nothing to do here. Helm doesn't required variables to be declared before use, nor does it require types to be specified + + return nil +} + +func (p *HelmProvisioner) ReadProvisionerInputs(stepPath string) (map[string]interface{}, error) { + helmParamsSchema, err := helm.HelmToSchema(path.Join(stepPath, "values.yaml")) + if err != nil { + return nil, err + } + + schemaBytes, marshallErr := json.Marshal(helmParamsSchema) + if marshallErr != nil { + return nil, marshallErr + } + + variables := map[string]interface{}{} + err = json.Unmarshal(schemaBytes, &variables) + if err != nil { + return nil, err + } + + return variables, nil +} + +func (p *HelmProvisioner) InitializeStep(stepPath string, sourcePath string) error { + pathInfo, statErr := os.Stat(sourcePath) + if statErr != nil { + return statErr + } + if !pathInfo.IsDir() { + return errors.New("path is not a directory containing a helm chart") + } + + if _, chartErr := os.Stat(path.Join(sourcePath, "Chart.yaml")); errors.Is(chartErr, os.ErrNotExist) { + return errors.New("path does not contain 'Chart.yaml' file, and therefore isn't a valid Helm chart") + } + if _, valuesErr := os.Stat(path.Join(sourcePath, "values.yaml")); errors.Is(valuesErr, os.ErrNotExist) { + return errors.New("path does not contain 'values.yaml' file, and therefore isn't a valid Helm chart") + } + + return os.CopyFS(stepPath, os.DirFS(sourcePath)) +} diff --git a/pkg/provisioners/helm_test.go b/pkg/provisioners/helm_test.go new file mode 100644 index 0000000..7a752f9 --- /dev/null +++ b/pkg/provisioners/helm_test.go @@ -0,0 +1,132 @@ +package provisioners_test + +import ( + "fmt" + "os" + "path" + "path/filepath" + "reflect" + "slices" + "testing" + + "github.com/massdriver-cloud/mass/pkg/provisioners" +) + +func TestHelmReadProvisionerInputs(t *testing.T) { + type test struct { + name string + want map[string]interface{} + } + tests := []test{ + { + name: "same", + want: map[string]interface{}{ + "required": []interface{}{"foo", "baz"}, + "properties": map[string]interface{}{ + "foo": map[string]interface{}{ + "title": "foo", + "type": "string", + "default": "bar", + }, + "baz": map[string]interface{}{ + "title": "baz", + "type": "string", + "default": "qux", + }, + }, + "type": "object", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + testDir := t.TempDir() + + content, err := os.ReadFile(path.Join("testdata", "helm", fmt.Sprintf("%s.yaml", tc.name))) + if err != nil { + t.Fatalf("%d, unexpected error", err) + } + + testFile := path.Join(testDir, "values.yaml") + err = os.WriteFile(testFile, content, 0644) + if err != nil { + t.Fatalf("%d, unexpected error", err) + } + + prov := provisioners.HelmProvisioner{} + got, err := prov.ReadProvisionerInputs(testDir) + if err != nil { + t.Errorf("Error during validation: %s", err) + } + + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("want %v got %v", got, tc.want) + } + }) + } +} + +func TestHelmInitializeStep(t *testing.T) { + type test struct { + name string + chartPath string + } + tests := []test{ + { + name: "same", + chartPath: "testdata/helm/initializetest", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + testDir := t.TempDir() + + prov := provisioners.HelmProvisioner{} + initErr := prov.InitializeStep(testDir, tc.chartPath) + if initErr != nil { + t.Fatalf("unexpected error: %s", initErr) + } + + want := []string{} + wanttErr := filepath.Walk(tc.chartPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if tc.chartPath == path { + return nil + } + want = append(want, info.Name()) + return nil + }) + if wanttErr != nil { + t.Fatalf("unexpected error: %s", wanttErr) + } + + got := []string{} + gotErr := filepath.Walk(testDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if testDir == path { + return nil + } + got = append(got, info.Name()) + return nil + }) + if gotErr != nil { + t.Fatalf("unexpected error: %s", gotErr) + } + + if len(got) != len(want) { + t.Errorf("want %v got %v", got, want) + } + for _, curr := range got { + if !slices.Contains(want, curr) { + t.Errorf("%v doesn't exist in %v", curr, want) + } + } + }) + } +} diff --git a/pkg/provisioners/testdata/helm/initializetest/Chart.yaml b/pkg/provisioners/testdata/helm/initializetest/Chart.yaml new file mode 100644 index 0000000..e69de29 diff --git a/pkg/provisioners/testdata/helm/initializetest/templates/deployment.yaml b/pkg/provisioners/testdata/helm/initializetest/templates/deployment.yaml new file mode 100644 index 0000000..e69de29 diff --git a/pkg/provisioners/testdata/helm/initializetest/values.yaml b/pkg/provisioners/testdata/helm/initializetest/values.yaml new file mode 100644 index 0000000..e69de29 diff --git a/pkg/provisioners/testdata/helm/same.yaml b/pkg/provisioners/testdata/helm/same.yaml new file mode 100644 index 0000000..d84e708 --- /dev/null +++ b/pkg/provisioners/testdata/helm/same.yaml @@ -0,0 +1,2 @@ +foo: bar +baz: qux diff --git a/pkg/provisioners/types.go b/pkg/provisioners/types.go index c58b958..d15bae9 100644 --- a/pkg/provisioners/types.go +++ b/pkg/provisioners/types.go @@ -10,6 +10,8 @@ func NewProvisioner(provisionerType string) Provisioner { switch provisionerType { case "opentofu", "terraform": return new(OpentofuProvisioner) + case "helm": + return new(HelmProvisioner) case "bicep": return new(BicepProvisioner) default: @@ -23,7 +25,6 @@ func (p *NoopProvisioner) ExportMassdriverInputs(string, map[string]interface{}) return nil } func (p *NoopProvisioner) ReadProvisionerInputs(string) (map[string]interface{}, error) { - //nolint:nilnil return nil, nil } func (p *NoopProvisioner) InitializeStep(string, string) error {