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
27 changes: 7 additions & 20 deletions api/v1alpha1/blueprint_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type Blueprint struct {
Metadata Metadata `yaml:"metadata"`

// Repository details the source repository of the blueprint.
Repository Repository `yaml:"repository"`
Repository Repository `yaml:"repository,omitempty"`

// Sources are external resources referenced by the blueprint.
Sources []Source `yaml:"sources"`
Expand Down Expand Up @@ -64,7 +64,7 @@ type Repository struct {
Ref Reference `yaml:"ref"`

// SecretName is the secret for repository access.
SecretName string `yaml:"secretName,omitempty"`
SecretName *string `yaml:"secretName,omitempty"`
}

// Source is an external resource referenced by a blueprint.
Expand Down Expand Up @@ -269,7 +269,10 @@ func (b *Blueprint) DeepCopy() *Blueprint {
Tag: b.Repository.Ref.Tag,
Branch: b.Repository.Ref.Branch,
},
SecretName: b.Repository.SecretName,
}
if b.Repository.SecretName != nil {
secretNameCopy := *b.Repository.SecretName
repositoryCopy.SecretName = &secretNameCopy
}

sourcesCopy := make([]Source, len(b.Sources))
Expand Down Expand Up @@ -342,23 +345,7 @@ func (b *Blueprint) StrategicMerge(overlays ...*Blueprint) error {
}

if overlay.Repository.Url != "" {
b.Repository.Url = overlay.Repository.Url
}

if overlay.Repository.Ref.Commit != "" {
b.Repository.Ref.Commit = overlay.Repository.Ref.Commit
} else if overlay.Repository.Ref.Name != "" {
b.Repository.Ref.Name = overlay.Repository.Ref.Name
} else if overlay.Repository.Ref.SemVer != "" {
b.Repository.Ref.SemVer = overlay.Repository.Ref.SemVer
} else if overlay.Repository.Ref.Tag != "" {
b.Repository.Ref.Tag = overlay.Repository.Ref.Tag
} else if overlay.Repository.Ref.Branch != "" {
b.Repository.Ref.Branch = overlay.Repository.Ref.Branch
}

if overlay.Repository.SecretName != "" {
b.Repository.SecretName = overlay.Repository.SecretName
b.Repository = overlay.Repository
}

sourceMap := make(map[string]Source)
Expand Down
90 changes: 85 additions & 5 deletions api/v1alpha1/blueprint_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,15 +343,18 @@ func TestBlueprint_StrategicMerge(t *testing.T) {
})

t.Run("MergesMetadataAndRepository", func(t *testing.T) {
secretName := "base-secret"
updatedSecretName := "updated-secret"
// Given a base blueprint
base := &Blueprint{
Metadata: Metadata{
Name: "base",
Description: "base description",
},
Repository: Repository{
Url: "base-url",
Ref: Reference{Branch: "main"},
Url: "base-url",
Ref: Reference{Branch: "main"},
SecretName: &secretName,
},
}

Expand All @@ -362,8 +365,9 @@ func TestBlueprint_StrategicMerge(t *testing.T) {
Description: "updated description",
},
Repository: Repository{
Url: "updated-url",
Ref: Reference{Tag: "v1.0.0"},
Url: "updated-url",
Ref: Reference{Tag: "v1.0.0"},
SecretName: &updatedSecretName,
},
}

Expand All @@ -378,13 +382,89 @@ func TestBlueprint_StrategicMerge(t *testing.T) {
t.Errorf("Expected description 'updated description', got '%s'", base.Metadata.Description)
}

// And repository should be updated
// And repository should be completely replaced (not merged field-by-field)
if base.Repository.Url != "updated-url" {
t.Errorf("Expected url 'updated-url', got '%s'", base.Repository.Url)
}
if base.Repository.Ref.Tag != "v1.0.0" {
t.Errorf("Expected tag 'v1.0.0', got '%s'", base.Repository.Ref.Tag)
}
if base.Repository.Ref.Branch != "" {
t.Errorf("Expected branch to be empty (replaced, not merged), got '%s'", base.Repository.Ref.Branch)
}
if base.Repository.SecretName == nil || *base.Repository.SecretName != "updated-secret" {
t.Errorf("Expected secretName to be 'updated-secret', got %v", base.Repository.SecretName)
}
})

t.Run("RepositoryReplacementWhenOverlayHasURL", func(t *testing.T) {
baseSecretName := "base-secret"
// Given a base blueprint with repository
base := &Blueprint{
Repository: Repository{
Url: "base-url",
Ref: Reference{Branch: "main", Commit: "abc123"},
SecretName: &baseSecretName,
},
}

// And an overlay with only URL set (no ref, no secretName)
overlay := &Blueprint{
Repository: Repository{
Url: "overlay-url",
},
}

// When strategic merging
base.StrategicMerge(overlay)

// Then repository should be completely replaced
if base.Repository.Url != "overlay-url" {
t.Errorf("Expected url 'overlay-url', got '%s'", base.Repository.Url)
}
if base.Repository.Ref.Branch != "" {
t.Errorf("Expected branch to be empty (replaced), got '%s'", base.Repository.Ref.Branch)
}
if base.Repository.Ref.Commit != "" {
t.Errorf("Expected commit to be empty (replaced), got '%s'", base.Repository.Ref.Commit)
}
if base.Repository.SecretName != nil {
t.Errorf("Expected secretName to be nil (replaced), got %v", base.Repository.SecretName)
}
})

t.Run("RepositoryNotReplacedWhenOverlayURLEmpty", func(t *testing.T) {
baseSecretName := "base-secret"
// Given a base blueprint with repository
base := &Blueprint{
Repository: Repository{
Url: "base-url",
Ref: Reference{Branch: "main"},
SecretName: &baseSecretName,
},
}

// And an overlay with empty URL
overlay := &Blueprint{
Repository: Repository{
Url: "",
Ref: Reference{Tag: "v1.0.0"},
},
}

// When strategic merging
base.StrategicMerge(overlay)

// Then repository should remain unchanged
if base.Repository.Url != "base-url" {
t.Errorf("Expected url 'base-url' to remain, got '%s'", base.Repository.Url)
}
if base.Repository.Ref.Branch != "main" {
t.Errorf("Expected branch 'main' to remain, got '%s'", base.Repository.Ref.Branch)
}
if base.Repository.SecretName == nil || *base.Repository.SecretName != "base-secret" {
t.Errorf("Expected secretName 'base-secret' to remain, got %v", base.Repository.SecretName)
}
})

t.Run("MergesSourcesUniquely", func(t *testing.T) {
Expand Down
25 changes: 10 additions & 15 deletions pkg/composer/blueprint/blueprint_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1720,31 +1720,26 @@ func (b *BaseBlueprintHandler) deepMergeMaps(base, overlay map[string]any) map[s
return result
}

// setRepositoryDefaults sets the blueprint repository URL if not already specified.
// Uses development URL if dev flag is enabled, otherwise falls back to git remote origin URL.
// In dev mode, always overrides the URL even if it's already set.
// setRepositoryDefaults sets or overrides the blueprint repository URL based on development mode and git configuration.
// If development mode is enabled, the development URL is always used. Otherwise, the git remote origin URL is used if the URL is unset.
// If a URL is set and the repository reference is empty, the branch is set to "main".
func (b *BaseBlueprintHandler) setRepositoryDefaults() error {
devMode := b.runtime.ConfigHandler.GetBool("dev")

if devMode {
url := b.getDevelopmentRepositoryURL()
if url != "" {
b.blueprint.Repository.Url = url
return nil
}
}

// Only set from git remote if URL is not already set
if b.blueprint.Repository.Url != "" {
return nil
if b.blueprint.Repository.Url == "" {
gitURL, err := b.runtime.Shell.ExecSilent("git", "config", "--get", "remote.origin.url")
if err == nil && gitURL != "" {
b.blueprint.Repository.Url = b.normalizeGitURL(strings.TrimSpace(gitURL))
}
}

gitURL, err := b.runtime.Shell.ExecSilent("git", "config", "--get", "remote.origin.url")
if err == nil && gitURL != "" {
b.blueprint.Repository.Url = b.normalizeGitURL(strings.TrimSpace(gitURL))
return nil
if b.blueprint.Repository.Url != "" && b.blueprint.Repository.Ref == (blueprintv1alpha1.Reference{}) {
b.blueprint.Repository.Ref = blueprintv1alpha1.Reference{Branch: "main"}
}

return nil
}

Expand Down
162 changes: 162 additions & 0 deletions pkg/composer/blueprint/blueprint_handler_private_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4016,6 +4016,168 @@ func TestBaseBlueprintHandler_setRepositoryDefaults(t *testing.T) {
}
})

t.Run("SetsDefaultBranchToMainWhenURLSetButRefEmpty", func(t *testing.T) {
handler := setup(t)
handler.blueprint.Repository.Url = "https://github.com/user/repo"
handler.blueprint.Repository.Ref = blueprintv1alpha1.Reference{}

mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool {
return false
}

err := handler.setRepositoryDefaults()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if handler.blueprint.Repository.Ref.Branch != "main" {
t.Errorf("Expected default branch to be 'main', got '%s'", handler.blueprint.Repository.Ref.Branch)
}
if handler.blueprint.Repository.Url != "https://github.com/user/repo" {
t.Errorf("Expected URL to remain unchanged, got %s", handler.blueprint.Repository.Url)
}
})

t.Run("SetsDefaultBranchToMainWhenURLSetFromDevMode", func(t *testing.T) {
handler := setup(t)

mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool {
if key == "dev" {
return true
}
return false
}
mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string {
if key == "dns.domain" {
return "test.com"
}
return ""
}

handler.runtime.ProjectRoot = "/path/to/project"
handler.shims.FilepathBase = func(path string) string {
return "project"
}

err := handler.setRepositoryDefaults()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
expectedURL := "http://git.test.com/git/project"
if handler.blueprint.Repository.Url != expectedURL {
t.Errorf("Expected URL to be %s, got %s", expectedURL, handler.blueprint.Repository.Url)
}
if handler.blueprint.Repository.Ref.Branch != "main" {
t.Errorf("Expected default branch to be 'main', got '%s'", handler.blueprint.Repository.Ref.Branch)
}
})

t.Run("SetsDefaultBranchToMainWhenURLSetFromGitRemote", func(t *testing.T) {
handler := setup(t)

mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool {
return false
}

mockShell := handler.runtime.Shell.(*shell.MockShell)
mockShell.ExecSilentFunc = func(command string, args ...string) (string, error) {
if command == "git" && len(args) == 3 && args[0] == "config" && args[2] == "remote.origin.url" {
return "https://github.com/user/repo.git\n", nil
}
return "", fmt.Errorf("command not found")
}

err := handler.setRepositoryDefaults()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if handler.blueprint.Repository.Url != "https://github.com/user/repo.git" {
t.Errorf("Expected URL to be set from git remote, got %s", handler.blueprint.Repository.Url)
}
if handler.blueprint.Repository.Ref.Branch != "main" {
t.Errorf("Expected default branch to be 'main', got '%s'", handler.blueprint.Repository.Ref.Branch)
}
})

t.Run("PreservesExistingRefWhenURLSet", func(t *testing.T) {
handler := setup(t)
handler.blueprint.Repository.Url = "https://github.com/user/repo"
handler.blueprint.Repository.Ref = blueprintv1alpha1.Reference{Branch: "develop"}

mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool {
return false
}

err := handler.setRepositoryDefaults()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if handler.blueprint.Repository.Ref.Branch != "develop" {
t.Errorf("Expected branch to remain 'develop', got '%s'", handler.blueprint.Repository.Ref.Branch)
}
if handler.blueprint.Repository.Ref.Tag != "" {
t.Errorf("Expected tag to remain empty, got '%s'", handler.blueprint.Repository.Ref.Tag)
}
})

t.Run("PreservesExistingRefWhenRefHasTag", func(t *testing.T) {
handler := setup(t)
handler.blueprint.Repository.Url = "https://github.com/user/repo"
handler.blueprint.Repository.Ref = blueprintv1alpha1.Reference{Tag: "v1.0.0"}

mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool {
return false
}

err := handler.setRepositoryDefaults()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if handler.blueprint.Repository.Ref.Tag != "v1.0.0" {
t.Errorf("Expected tag to remain 'v1.0.0', got '%s'", handler.blueprint.Repository.Ref.Tag)
}
if handler.blueprint.Repository.Ref.Branch != "" {
t.Errorf("Expected branch to remain empty, got '%s'", handler.blueprint.Repository.Ref.Branch)
}
})

t.Run("DoesNotSetBranchWhenURLEmpty", func(t *testing.T) {
handler := setup(t)
handler.blueprint.Repository.Url = ""
handler.blueprint.Repository.Ref = blueprintv1alpha1.Reference{}

mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool {
return false
}

mockShell := handler.runtime.Shell.(*shell.MockShell)
mockShell.ExecSilentFunc = func(command string, args ...string) (string, error) {
return "", fmt.Errorf("not a git repository")
}

err := handler.setRepositoryDefaults()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if handler.blueprint.Repository.Url != "" {
t.Errorf("Expected URL to remain empty, got %s", handler.blueprint.Repository.Url)
}
if handler.blueprint.Repository.Ref.Branch != "" {
t.Errorf("Expected branch to remain empty when URL is empty, got '%s'", handler.blueprint.Repository.Ref.Branch)
}
})

}

func TestBaseBlueprintHandler_normalizeGitURL(t *testing.T) {
Expand Down
Loading
Loading