Skip to content

Commit

Permalink
Add clearer prompts and better importing logic for 'mass bundle new'
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisghill committed Nov 29, 2024
1 parent 47b81e9 commit d1cd8a3
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 73 additions & 12 deletions pkg/bundle/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"errors"
"fmt"
"maps"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {

Check failure on line 235 in pkg/bundle/prompt.go

View workflow job for this annotation

GitHub Actions / lint

cognitive complexity 40 of func `getExistingParamsPath` is high (> 20) (gocognit)
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()
Expand Down
12 changes: 7 additions & 5 deletions pkg/params/params.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
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"
"github.com/massdriver-cloud/airlock/pkg/schema"
"sigs.k8s.io/yaml"
)

func GetFromPath(templateName, path string) (string, error) {
if path == "" {
func GetFromPath(templateName, paramsPath string) (string, error) {
if paramsPath == "" {
return "", nil
}

Expand All @@ -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
}
Expand Down
57 changes: 57 additions & 0 deletions pkg/provisioners/helm.go
Original file line number Diff line number Diff line change
@@ -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 {

Check warning on line 14 in pkg/provisioners/helm.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'stepPath' seems to be unused, consider removing or renaming it as _ (revive)
// 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))
}
132 changes: 132 additions & 0 deletions pkg/provisioners/helm_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}
Empty file.
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions pkg/provisioners/testdata/helm/same.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
foo: bar
baz: qux
3 changes: 2 additions & 1 deletion pkg/provisioners/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 {
Expand Down

0 comments on commit d1cd8a3

Please sign in to comment.