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
37 changes: 15 additions & 22 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,38 +35,33 @@ var initCmd = &cobra.Command{
Long: "Initialize the application environment with the specified context configuration",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// Get shared dependency injector from context
injector := cmd.Context().Value(injectorKey).(di.Injector)
ctx := cmd.Context()
if len(args) > 0 {
ctx = context.WithValue(ctx, "contextName", args[0])
}
ctx = context.WithValue(ctx, "reset", initReset)
if initBlueprint != "" {
ctx = context.WithValue(ctx, "blueprint", initBlueprint)
}

// First, run the env pipeline in quiet mode to set up environment variables
envPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "envPipeline")
ctx = context.WithValue(ctx, "quiet", true)
ctx = context.WithValue(ctx, "decrypt", true)
envPipeline, err := pipelines.WithPipeline(injector, ctx, "envPipeline")
if err != nil {
return fmt.Errorf("failed to set up env pipeline: %w", err)
}
envCtx := context.WithValue(cmd.Context(), "quiet", true)
envCtx = context.WithValue(envCtx, "decrypt", true)
if err := envPipeline.Execute(envCtx); err != nil {
if err := envPipeline.Execute(ctx); err != nil {
return fmt.Errorf("failed to set up environment: %w", err)
}

// Set up the init pipeline
initPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "initPipeline")
ctx = context.WithValue(ctx, "quiet", false)
ctx = context.WithValue(ctx, "decrypt", false)
initPipeline, err := pipelines.WithPipeline(injector, ctx, "initPipeline")
if err != nil {
return fmt.Errorf("failed to set up init pipeline: %w", err)
}

ctx := cmd.Context()

// Add context name and reset flag to context (these are needed during Initialize)
if len(args) > 0 {
ctx = context.WithValue(ctx, "contextName", args[0])
}
ctx = context.WithValue(ctx, "reset", initReset)
if initBlueprint != "" {
ctx = context.WithValue(ctx, "blueprint", initBlueprint)
}

// Set flag values in config handler before execution
configHandler := injector.Resolve("configHandler").(config.ConfigHandler)

if initBackend != "" {
Expand Down Expand Up @@ -125,7 +120,6 @@ var initCmd = &cobra.Command{
}
}

// Handle set flags
for _, setFlag := range initSetFlags {
parts := strings.SplitN(setFlag, "=", 2)
if len(parts) == 2 {
Expand All @@ -140,7 +134,6 @@ var initCmd = &cobra.Command{
}

func init() {

initCmd.Flags().BoolVar(&initReset, "reset", false, "Reset/overwrite existing files and clean .terraform directory")
initCmd.Flags().StringVar(&initBackend, "backend", "", "Specify the backend to use")
initCmd.Flags().StringVar(&initAwsProfile, "aws-profile", "", "Specify the AWS profile to use")
Expand Down
76 changes: 69 additions & 7 deletions pkg/artifact/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package artifact
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"maps"
Expand Down Expand Up @@ -60,6 +59,16 @@ type BuilderInfo struct {
Email string `json:"email"`
}

// OCIArtifactInfo contains information about the OCI artifact source for blueprint data
type OCIArtifactInfo struct {
// Name is the name of the OCI artifact
Name string
// URL is the full OCI URL of the artifact
URL string
// Tag is the tag/version of the OCI artifact
Tag string
}

// BlueprintMetadataInput represents the input metadata from contexts/_template/metadata.yaml
type BlueprintMetadataInput struct {
Name string `yaml:"name"`
Expand Down Expand Up @@ -360,12 +369,7 @@ func (a *ArtifactBuilder) GetTemplateData(ociRef string) (map[string][]byte, err
templateData := make(map[string][]byte)
templateData["ociUrl"] = []byte(ociRef)

gzipReader, err := gzip.NewReader(bytes.NewReader(artifactData))
if err != nil {
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gzipReader.Close()
tarReader := tar.NewReader(gzipReader)
tarReader := tar.NewReader(bytes.NewReader(artifactData))

var metadataName string
jsonnetFiles := make(map[string][]byte)
Expand Down Expand Up @@ -421,6 +425,64 @@ func (a *ArtifactBuilder) GetTemplateData(ociRef string) (map[string][]byte, err
return templateData, nil
}

// =============================================================================
// Package Functions
// =============================================================================

// ParseOCIReference parses a blueprint reference string in OCI URL or org/repo:tag format and returns an OCIArtifactInfo struct.
// Accepts full OCI URLs (e.g., oci://ghcr.io/org/repo:v1.0.0) and org/repo:v1.0.0 formats only.
// Returns nil if the reference is empty, missing a version, or not in a supported format.
func ParseOCIReference(ociRef string) (*OCIArtifactInfo, error) {
if ociRef == "" {
return nil, nil
}

var name, version, fullURL string

if strings.HasPrefix(ociRef, "oci://") {
fullURL = ociRef
remaining := strings.TrimPrefix(ociRef, "oci://")
if lastColon := strings.LastIndex(remaining, ":"); lastColon > 0 {
version = remaining[lastColon+1:]
pathPart := remaining[:lastColon]
if lastSlash := strings.LastIndex(pathPart, "/"); lastSlash >= 0 {
name = pathPart[lastSlash+1:]
} else {
return nil, fmt.Errorf("blueprint reference '%s' is missing a version (e.g., core:v1.0.0)", ociRef)
}
} else {
return nil, fmt.Errorf("blueprint reference '%s' is missing a version (e.g., core:v1.0.0)", ociRef)
}
} else {
if colonIdx := strings.LastIndex(ociRef, ":"); colonIdx > 0 {
pathPart := ociRef[:colonIdx]
version = ociRef[colonIdx+1:]
if strings.Count(pathPart, "/") >= 1 {
if lastSlash := strings.LastIndex(pathPart, "/"); lastSlash >= 0 {
name = pathPart[lastSlash+1:]
} else {
return nil, fmt.Errorf("blueprint reference '%s' is missing a version (e.g., core:v1.0.0)", ociRef)
}
fullURL = "oci://ghcr.io/" + ociRef
} else {
return nil, fmt.Errorf("blueprint reference '%s' is missing a version (e.g., core:v1.0.0)", ociRef)
}
} else {
return nil, fmt.Errorf("blueprint reference '%s' is missing a version (e.g., core:v1.0.0)", ociRef)
}
}

if version == "" || name == "" {
return nil, fmt.Errorf("blueprint reference '%s' is missing a version (e.g., core:v1.0.0)", ociRef)
}

return &OCIArtifactInfo{
Name: name,
URL: fullURL,
Tag: version,
}, nil
}

// =============================================================================
// Private Methods
// =============================================================================
Expand Down
106 changes: 102 additions & 4 deletions pkg/artifact/artifact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3305,13 +3305,12 @@ func TestArtifactBuilder_GetTemplateData(t *testing.T) {
})
}

// createTestTarGz creates a test tar.gz archive with the given files
// createTestTarGz creates a test tar archive with the given files
func createTestTarGz(t *testing.T, files map[string][]byte) []byte {
t.Helper()

var buf bytes.Buffer
gzipWriter := gzip.NewWriter(&buf)
tarWriter := tar.NewWriter(gzipWriter)
tarWriter := tar.NewWriter(&buf)

for path, content := range files {
header := &tar.Header{
Expand All @@ -3330,7 +3329,106 @@ func createTestTarGz(t *testing.T, files map[string][]byte) []byte {
}

tarWriter.Close()
gzipWriter.Close()

return buf.Bytes()
}

// =============================================================================
// Test Package Functions
// =============================================================================

func TestParseOCIReference(t *testing.T) {
testCases := []struct {
name string
input string
expected *OCIArtifactInfo
expectError bool
}{
{
name: "EmptyString",
input: "",
expected: nil,
expectError: false,
},
{
name: "FullOCIURL",
input: "oci://ghcr.io/windsorcli/core:v1.0.0",
expected: &OCIArtifactInfo{
Name: "core",
URL: "oci://ghcr.io/windsorcli/core:v1.0.0",
Tag: "v1.0.0",
},
expectError: false,
},
{
name: "ShortFormat",
input: "windsorcli/core:v1.0.0",
expected: &OCIArtifactInfo{
Name: "core",
URL: "oci://ghcr.io/windsorcli/core:v1.0.0",
Tag: "v1.0.0",
},
expectError: false,
},
{
name: "MissingVersion",
input: "windsorcli/core",
expected: nil,
expectError: true,
},
{
name: "InvalidFormat",
input: "core:v1.0.0",
expected: nil,
expectError: true,
},
{
name: "EmptyVersion",
input: "windsorcli/core:",
expected: nil,
expectError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := ParseOCIReference(tc.input)

if tc.expectError {
if err == nil {
t.Errorf("Expected error but got none")
}
return
}

if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}

if tc.expected == nil {
if result != nil {
t.Errorf("Expected nil result but got: %+v", result)
}
return
}

if result == nil {
t.Errorf("Expected result but got nil")
return
}

if result.Name != tc.expected.Name {
t.Errorf("Expected name %s but got %s", tc.expected.Name, result.Name)
}

if result.URL != tc.expected.URL {
t.Errorf("Expected URL %s but got %s", tc.expected.URL, result.URL)
}

if result.Tag != tc.expected.Tag {
t.Errorf("Expected tag %s but got %s", tc.expected.Tag, result.Tag)
}
})
}
}
Loading
Loading