Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 21 additions & 35 deletions cmd/stackpack/stackpack_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ package stackpack

import (
"archive/zip"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/gurkankaymak/hocon"
"github.com/spf13/cobra"
"github.com/stackvista/stackstate-cli/internal/common"
"github.com/stackvista/stackstate-cli/internal/di"
"gopkg.in/yaml.v3"
)

const (
Expand All @@ -36,53 +36,39 @@ type StackpackConfigParser interface {
Parse(filePath string) (*StackpackInfo, error)
}

// HoconParser implements StackpackConfigParser for HOCON format
type HoconParser struct{}
// YamlParser implements StackpackConfigParser for YAML format (future)
type YamlParser struct{}

func (h *HoconParser) Parse(filePath string) (*StackpackInfo, error) {
// Read the file content
func (y *YamlParser) Parse(filePath string) (*StackpackInfo, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}

// Parse stackpack.conf content
conf, err := hocon.ParseString(string(content))
if err != nil {
return nil, fmt.Errorf("failed to parse stackpack.conf file: %w", err)
}

name := strings.Trim(conf.GetString("name"), `"`)
version := strings.Trim(conf.GetString("version"), `"`)
dec := yaml.NewDecoder(bytes.NewBuffer(content))
cfg := &StackpackInfo{}

if name == "" {
return nil, fmt.Errorf("name not found in stackpack.conf")
if err := dec.Decode(&cfg); err != nil {
return nil, fmt.Errorf("failed to parse stackpack.yaml file: %w", err)
}

if version == "" {
return nil, fmt.Errorf("version not found in stackpack.conf")
if cfg.Name == "" {
return nil, fmt.Errorf("name not found in stackpack.yaml")
}

return &StackpackInfo{
Name: name,
Version: version,
}, nil
}

// YamlParser implements StackpackConfigParser for YAML format (future)
type YamlParser struct{}
if cfg.Version == "" {
return nil, fmt.Errorf("version not found in stackpack.yaml")
}

func (y *YamlParser) Parse(filePath string) (*StackpackInfo, error) {
// TODO: Implement YAML parsing when format changes
return nil, fmt.Errorf("YAML format not yet implemented")
return cfg, nil
}

// Required files and directories for a valid stackpack
var requiredStackpackItems = []string{
"provisioning",
"README.md",
"resources",
"stackpack.conf",
"stackpack.yaml",
}

// StackpackPackageCommand creates the package subcommand
Expand All @@ -97,10 +83,10 @@ Creates a zip file containing all required stackpack files and directories:
- provisioning/ (directory)
- README.md (file)
- resources/ (directory)
- stackpack.conf (file)
- stackpack.yaml (file)

The zip file is named <stackpack_name>-<version>.zip where the name and
version are extracted from stackpack.conf and created in the current directory.`,
version are extracted from stackpack.yaml and created in the current directory.`,
Example: `# Package stackpack in current directory
sts stackpack package

Expand Down Expand Up @@ -142,10 +128,10 @@ func RunStackpackPackageCommand(args *PackageArgs) func(cli *di.Deps, cmd *cobra
args.StackpackDir = absStackpackDir

// Parse stackpack.conf using HOCON parser to get name and version
parser := &HoconParser{}
stackpackInfo, err := parser.Parse(filepath.Join(args.StackpackDir, "stackpack.conf"))
parser := &YamlParser{}
stackpackInfo, err := parser.Parse(filepath.Join(args.StackpackDir, "stackpack.yaml"))
if err != nil {
return common.NewRuntimeError(fmt.Errorf("failed to parse stackpack.conf: %w", err))
return common.NewRuntimeError(fmt.Errorf("failed to parse stackpack.yaml: %w", err))
}

// Set default archive file path if not specified
Expand Down
109 changes: 50 additions & 59 deletions cmd/stackpack/stackpack_package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ func createTestStackpack(t *testing.T, dir string, name string, version string)

// Create stackpack.conf
stackpackConf := fmt.Sprintf(`# schemaVersion -- Stackpack specification version.
schemaVersion = "2.0"
schemaVersion: "2.0"
# name -- Name of the StackPack.
name = "%s"
name: "%s"
# displayName -- Display name of the StackPack.
displayName = "Test %s"
displayName: "Test %s"
# version -- Semantic version of the StackPack.
version = "%s"
version: "%s"
`, name, name, version)
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.conf"), []byte(stackpackConf), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.yaml"), []byte(stackpackConf), 0644))

// Create README.md
readme := fmt.Sprintf("# %s\n\nThis is a test stackpack.", name)
Expand Down Expand Up @@ -196,7 +196,7 @@ func TestStackpackPackageCommand_MissingRequiredFiles(t *testing.T) {
setupFunc: func(dir string) {
require.NoError(t, os.MkdirAll(filepath.Join(dir, "resources"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"), []byte("readme"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.conf"), []byte("name = \"test\"\nversion = \"1.0.0\""), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.yaml"), []byte("name: \"test\"\nversion: \"1.0.0\""), 0644))
},
expectedError: "required stackpack item not found: provisioning",
},
Expand All @@ -205,7 +205,7 @@ func TestStackpackPackageCommand_MissingRequiredFiles(t *testing.T) {
setupFunc: func(dir string) {
require.NoError(t, os.MkdirAll(filepath.Join(dir, "provisioning"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "resources"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.conf"), []byte("name = \"test\"\nversion = \"1.0.0\""), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.yaml"), []byte("name: \"test\"\nversion: \"1.0.0\""), 0644))
},
expectedError: "required stackpack item not found: README.md",
},
Expand All @@ -214,18 +214,18 @@ func TestStackpackPackageCommand_MissingRequiredFiles(t *testing.T) {
setupFunc: func(dir string) {
require.NoError(t, os.MkdirAll(filepath.Join(dir, "provisioning"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"), []byte("readme"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.conf"), []byte("name = \"test\"\nversion = \"1.0.0\""), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.yaml"), []byte("name: \"test\"\nversion: \"1.0.0\""), 0644))
},
expectedError: "required stackpack item not found: resources",
},
{
name: "missing stackpack.conf file",
name: "missing stackpack.yaml file",
setupFunc: func(dir string) {
require.NoError(t, os.MkdirAll(filepath.Join(dir, "provisioning"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "resources"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"), []byte("readme"), 0644))
},
expectedError: "failed to parse stackpack.conf",
expectedError: "failed to parse stackpack.yaml",
},
}

Expand Down Expand Up @@ -255,37 +255,37 @@ func TestStackpackPackageCommand_InvalidStackpackConf(t *testing.T) {
expectedError string
}{
{
name: "invalid HOCON syntax",
name: "invalid YAML syntax",
confContent: `name = "test" invalid syntax {`,
expectedError: "failed to parse stackpack.conf file",
expectedError: "failed to parse stackpack.yaml file",
},
{
name: "missing name field",
confContent: `version = "1.0.0"`,
expectedError: "name not found in stackpack.conf",
confContent: `version: "1.0.0"`,
expectedError: "name not found in stackpack.yaml",
},
{
name: "missing version field",
confContent: `name = "test"`,
expectedError: "version not found in stackpack.conf",
confContent: `name: "test"`,
expectedError: "version not found in stackpack.yaml",
},
{
name: "empty name field",
confContent: `name = ""
version = "1.0.0"`,
expectedError: "name not found in stackpack.conf",
confContent: `name: ""
version: "1.0.0"`,
expectedError: "name not found in stackpack.yaml",
},
{
name: "empty version field",
confContent: `name = "test"
version = ""`,
expectedError: "version not found in stackpack.conf",
confContent: `name: "test"
version: ""`,
expectedError: "version not found in stackpack.yaml",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "stackpack-package-hocon-test-*")
tempDir, err := os.MkdirTemp("", "stackpack-package-yaml-test-*")
require.NoError(t, err)
defer os.RemoveAll(tempDir)

Expand All @@ -297,8 +297,8 @@ version = ""`,
require.NoError(t, os.MkdirAll(filepath.Join(stackpackDir, "resources"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(stackpackDir, "README.md"), []byte("readme"), 0644))

// Create invalid stackpack.conf
require.NoError(t, os.WriteFile(filepath.Join(stackpackDir, "stackpack.conf"), []byte(tt.confContent), 0644))
// Create invalid stackpack.yaml
require.NoError(t, os.WriteFile(filepath.Join(stackpackDir, "stackpack.yaml"), []byte(tt.confContent), 0644))

cli, cmd := setupStackPackPackageCmd(t)

Expand All @@ -314,7 +314,7 @@ func TestStackpackPackageCommand_NonExistentDirectory(t *testing.T) {

_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "-d", "/non/existent/directory")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse stackpack.conf")
assert.Contains(t, err.Error(), "failed to parse stackpack.yaml")
assert.Contains(t, err.Error(), "no such file or directory")
}

Expand Down Expand Up @@ -345,7 +345,7 @@ func TestStackpackPackageCommand_CreateOutputDirectory(t *testing.T) {
assert.NoError(t, err, "Zip file should be created in nested directory")
}

func TestHoconParser_Parse(t *testing.T) {
func TestYamlParser_Parse(t *testing.T) {
tests := []struct {
name string
content string
Expand All @@ -355,48 +355,48 @@ func TestHoconParser_Parse(t *testing.T) {
errorContains string
}{
{
name: "valid HOCON with quotes",
content: `name = "my-stackpack"
version = "1.2.3"`,
name: "valid YAML with quotes",
content: `name: "my-stackpack"
version: "1.2.3"`,
expectedName: "my-stackpack",
expectedVer: "1.2.3",
expectError: false,
},
{
name: "valid HOCON without quotes",
content: `name = my-stackpack
version = "1.2.3"`,
name: "valid YAML without quotes",
content: `name: my-stackpack
version: "1.2.3"`,
expectedName: "my-stackpack",
expectedVer: "1.2.3",
expectError: false,
},
{
name: "HOCON with comments",
name: "YAML with comments",
content: `# This is a comment
name = "test-app"
name: "test-app"
# Another comment
version = "2.0.0"`,
version: "2.0.0"`,
expectedName: "test-app",
expectedVer: "2.0.0",
expectError: false,
},
{
name: "missing name",
content: `version = "1.0.0"`,
content: `version: "1.0.0"`,
expectError: true,
errorContains: "name not found in stackpack.conf",
errorContains: "name not found in stackpack.yaml",
},
{
name: "missing version",
content: `name = "test"`,
content: `name: "test"`,
expectError: true,
errorContains: "version not found in stackpack.conf",
errorContains: "version not found in stackpack.yaml",
},
{
name: "invalid HOCON syntax",
content: `name = "test" { invalid`,
name: "invalid YAML syntax",
content: `name: "test" { invalid`,
expectError: true,
errorContains: "failed to parse stackpack.conf file",
errorContains: "failed to parse stackpack.yaml file",
},
}

Expand All @@ -406,10 +406,10 @@ version = "2.0.0"`,
require.NoError(t, err)
defer os.RemoveAll(tempDir)

confPath := filepath.Join(tempDir, "test.conf")
confPath := filepath.Join(tempDir, "test.yaml")
require.NoError(t, os.WriteFile(confPath, []byte(tt.content), 0644))

parser := &HoconParser{}
parser := &YamlParser{}
result, err := parser.Parse(confPath)

if tt.expectError {
Expand All @@ -428,21 +428,12 @@ version = "2.0.0"`,
}
}

func TestHoconParser_ParseNonExistentFile(t *testing.T) {
parser := &HoconParser{}
result, err := parser.Parse("/non/existent/file.conf")

require.Error(t, err)
assert.Contains(t, err.Error(), "failed to read file")
assert.Nil(t, result)
}

func TestYamlParser_Parse(t *testing.T) {
func TestYamlParser_ParseNonExistentFile(t *testing.T) {
parser := &YamlParser{}
result, err := parser.Parse("any-path")
result, err := parser.Parse("/non/existent/file.yaml")

require.Error(t, err)
assert.Contains(t, err.Error(), "YAML format not yet implemented")
assert.Contains(t, err.Error(), "failed to read file")
assert.Nil(t, result)
}

Expand All @@ -459,7 +450,7 @@ func TestValidateStackpackDirectory(t *testing.T) {
require.NoError(t, os.MkdirAll(filepath.Join(dir, "provisioning"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "resources"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"), []byte("readme"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.conf"), []byte("conf"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.yaml"), []byte("yaml"), 0644))
},
expectError: false,
},
Expand All @@ -468,7 +459,7 @@ func TestValidateStackpackDirectory(t *testing.T) {
setupFunc: func(dir string) {
require.NoError(t, os.MkdirAll(filepath.Join(dir, "resources"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"), []byte("readme"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.conf"), []byte("conf"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "stackpack.yaml"), []byte("yaml"), 0644))
},
expectError: true,
errorContains: "required stackpack item not found: provisioning",
Expand Down
Loading