diff --git a/pkg/bundle/prompt.go b/pkg/bundle/prompt.go index 84dbfb7..0b3ad2f 100644 --- a/pkg/bundle/prompt.go +++ b/pkg/bundle/prompt.go @@ -111,11 +111,28 @@ var ignoredTemplateDirs = map[string]bool{"alpha": true} func getTemplate(t *templates.TemplateData) error { templateList, err := templates.List() if err != nil { + if errors.Is(err, templates.ErrNotConfigured) { + fmt.Println() + fmt.Println("💡 Did you know? You can set up bundle templates for faster development!") + fmt.Println(" Set MASSDRIVER_TEMPLATES_PATH or templates_path in your config profile.") + fmt.Println(" See: https://docs.massdriver.cloud/guides/bundle-templates") + fmt.Println() + fmt.Println(" Continuing without a template - we'll generate a basic massdriver.yaml for you.") + fmt.Println() + t.TemplateName = "" + return nil + } return err } filteredTemplates := removeIgnoredTemplateDirectories(templateList) + if len(filteredTemplates) == 0 { + fmt.Println("No templates found in templates path. Generating a basic massdriver.yaml.") + t.TemplateName = "" + return nil + } + prompt := promptui.Select{ Label: "Template", Items: filteredTemplates, diff --git a/pkg/commands/bundle/new.go b/pkg/commands/bundle/new.go index 4832090..ef58c36 100644 --- a/pkg/commands/bundle/new.go +++ b/pkg/commands/bundle/new.go @@ -2,7 +2,9 @@ package bundle import ( "fmt" + "os" "path" + "path/filepath" "github.com/massdriver-cloud/mass/pkg/bundle" "github.com/massdriver-cloud/mass/pkg/provisioners" @@ -10,6 +12,10 @@ import ( ) func RunNew(data *templates.TemplateData) error { + if data.TemplateName == "" { + return generateBasicBundle(data) + } + if err := templates.Render(data); err != nil { return fmt.Errorf("failed to render template: %w", err) } @@ -31,3 +37,66 @@ func RunNew(data *templates.TemplateData) error { return nil } + +func generateBasicBundle(data *templates.TemplateData) error { + if err := os.MkdirAll(data.OutputDir, 0750); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + content := generateMassdriverYAML(data) + + outputPath := filepath.Join(data.OutputDir, "massdriver.yaml") + if err := os.WriteFile(outputPath, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write massdriver.yaml: %w", err) + } + + return nil +} + +func generateMassdriverYAML(data *templates.TemplateData) string { + yaml := fmt.Sprintf(`# Massdriver Bundle Specification +# https://docs.massdriver.cloud/guides/bundle-yaml-spec + +schema: draft-07 +name: %q +description: %q +source_url: "" + +# steps: +# - path: src +# provisioner: opentofu +# See provisioners: https://docs.massdriver.cloud/provisioners/overview + +params: + required: [] + properties: {} + +`, data.Name, data.Description) + + // Add connections + yaml += "connections:\n" + if len(data.Connections) == 0 { + yaml += " required: []\n properties: {}\n" + } else { + yaml += " required:\n" + for _, conn := range data.Connections { + yaml += fmt.Sprintf(" - %s\n", conn.Name) + } + yaml += " properties:\n" + for _, conn := range data.Connections { + yaml += fmt.Sprintf(" %s:\n $ref: %s\n", conn.Name, conn.ArtifactDefinition) + } + } + + yaml += ` +artifacts: + required: [] + properties: {} + +ui: + ui:order: + - "*" +` + + return yaml +} diff --git a/pkg/commands/bundle/new_test.go b/pkg/commands/bundle/new_test.go index 66ba458..4ad4b79 100644 --- a/pkg/commands/bundle/new_test.go +++ b/pkg/commands/bundle/new_test.go @@ -101,6 +101,90 @@ func TestTemplateRender(t *testing.T) { } } +func TestGenerateBasicBundleWithoutTemplate(t *testing.T) { + testDir := t.TempDir() + writePath := path.Join(testDir, "my-bundle") + + data := &templates.TemplateData{ + OutputDir: writePath, + Name: "my-test-bundle", + Description: "A test bundle without a template", + TemplateName: "", // No template + Connections: []templates.Connection{ + {Name: "vpc", ArtifactDefinition: "massdriver/aws-vpc"}, + {Name: "database", ArtifactDefinition: "massdriver/aws-rds-postgres"}, + }, + } + + err := cmdbundle.RunNew(data) + checkErr(err, t) + + // Verify massdriver.yaml was created + content, err := os.ReadFile(path.Join(writePath, "massdriver.yaml")) + checkErr(err, t) + + got := &bundle.Bundle{} + err = yaml.Unmarshal(content, got) + checkErr(err, t) + + if got.Name != "my-test-bundle" { + t.Errorf("Expected name to be 'my-test-bundle' but got %q", got.Name) + } + + if got.Description != "A test bundle without a template" { + t.Errorf("Expected description to be 'A test bundle without a template' but got %q", got.Description) + } + + // Check connections + wantConnections := map[string]any{ + "required": []any{"vpc", "database"}, + "properties": map[string]any{ + "vpc": map[string]any{"$ref": "massdriver/aws-vpc"}, + "database": map[string]any{"$ref": "massdriver/aws-rds-postgres"}, + }, + } + + if !reflect.DeepEqual(got.Connections, wantConnections) { + t.Errorf("Expected connections to be %v but got %v", wantConnections, got.Connections) + } +} + +func TestGenerateBasicBundleNoConnections(t *testing.T) { + testDir := t.TempDir() + writePath := path.Join(testDir, "simple-bundle") + + data := &templates.TemplateData{ + OutputDir: writePath, + Name: "simple-bundle", + Description: "A simple bundle", + TemplateName: "", // No template + Connections: []templates.Connection{}, + } + + err := cmdbundle.RunNew(data) + checkErr(err, t) + + content, err := os.ReadFile(path.Join(writePath, "massdriver.yaml")) + checkErr(err, t) + + got := &bundle.Bundle{} + err = yaml.Unmarshal(content, got) + checkErr(err, t) + + if got.Name != "simple-bundle" { + t.Errorf("Expected name to be 'simple-bundle' but got %q", got.Name) + } + + wantConnections := map[string]any{ + "required": []any{}, + "properties": map[string]any{}, + } + + if !reflect.DeepEqual(got.Connections, wantConnections) { + t.Errorf("Expected connections to be %v but got %v", wantConnections, got.Connections) + } +} + func mockTemplateData(writePath string) *templates.TemplateData { return &templates.TemplateData{ OutputDir: writePath,