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
229 changes: 191 additions & 38 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/nginx/deployah.platform.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
apiVersion: platform/v1-alpha.2
apiVersion: platform/v1-alpha.3
environments:
local:
context: kind-deployah
Expand Down
2 changes: 1 addition & 1 deletion examples/nginx/deployah.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
apiVersion: v1-alpha.3
apiVersion: v1-alpha.4
project: nginx
components:
web:
Expand Down
2 changes: 1 addition & 1 deletion internal/action/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (m *mockSpecLoader) Spec(_ context.Context, _ string) (*spec.Spec, error) {
}

var testManifest = &spec.Spec{
APIVersion: "v1-alpha.3",
APIVersion: "v1-alpha.4",
Project: "my-app",
}

Expand Down
58 changes: 1 addition & 57 deletions internal/cmd/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func runDeploy(c *nabat.Context) error {
// Skip when Helm is idle and only CRDs will apply (CRDs are the APIs).
// Also skip group/versions that .deployah/crds/ will install in this deploy.
if !helmIdle && k8sErr == nil {
reqs := filterCoveredAPIs(requiredAPIs(manifest, opts.Environment, resolvedSpec), extras.GroupVersionsFromCRDs(bundle.CRDs))
reqs := filterCoveredAPIs(k8s.RequiredAPIs(manifest, opts.Environment, resolvedSpec), extras.GroupVersionsFromCRDs(bundle.CRDs))
if len(reqs) > 0 {
if capErr := k8s.CheckAPIRequirements(k8sClient, reqs); capErr != nil {
return capErr
Expand Down Expand Up @@ -614,62 +614,6 @@ func filterCoveredAPIs(reqs []k8s.APIRequirement, covered map[string]struct{}) [
return out
}

// requiredAPIs derives the Kubernetes API group/version requirements for
// components deployed in the target environment, including cert-manager
// (TLS mode) via the resolved spec.
func requiredAPIs(manifest *spec.Spec, environment string, resolved *spec.ResolvedSpec) []k8s.APIRequirement {
type entry struct {
groupVersions []string
components []string
}

entries := make(map[string]*entry) // keyed by canonical group/version string

add := func(groupVersions []string, componentName string) {
key := strings.Join(groupVersions, "|")
e := entries[key]
if e == nil {
e = &entry{groupVersions: groupVersions}
entries[key] = e
}
e.components = append(e.components, fmt.Sprintf("%q", componentName))
}

for name, component := range manifest.Components {
// Same matcher as spec.Resolve and chart generation, so wildcard
// deploys agree on the active component set.
if len(component.Environments) > 0 {
if _, ok := spec.MatchEnvKey(environment, component.Environments); !ok {
continue
}
}
if component.Autoscaling != nil && component.Autoscaling.Enabled {
add([]string{"autoscaling/v2", "autoscaling/v2beta2"}, name)
}
if component.Expose != nil {
add([]string{"networking.k8s.io/v1"}, name)
}
if resolved != nil {
if rc, ok := resolved.Components[name]; ok && rc.TLSMode == spec.TLSModeCertManager {
add([]string{"cert-manager.io/v1"}, name)
}
}
}

reqs := make([]k8s.APIRequirement, 0, len(entries))
for _, e := range entries {
noun := "component"
if len(e.components) > 1 {
noun = "components"
}
reqs = append(reqs, k8s.APIRequirement{
GroupVersions: e.groupVersions,
Reason: fmt.Sprintf("required by %s %s", noun, strings.Join(e.components, ", ")),
})
}
return reqs
}

// warnContextMismatch emits a warning when the --context flag overrides the
// platform-file context for the target environment. Silenced by setting
// DEPLOYAH_ALLOW_CONTEXT_MISMATCH=1.
Expand Down
12 changes: 11 additions & 1 deletion internal/cmd/deploy/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,22 @@ func TestRequiredAPIs(t *testing.T) {
environment: "staging",
wantEmpty: true,
},
{
name: "metrics enabled requires prometheus operator API",
manifest: &spec.Spec{
Components: map[string]spec.Component{
"api": {Metrics: &spec.ComponentMetrics{}},
},
},
environment: "production",
wantContains: []string{"monitoring.coreos.com/v1"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
reqs := requiredAPIs(tt.manifest, tt.environment, tt.resolved)
reqs := k8s.RequiredAPIs(tt.manifest, tt.environment, tt.resolved)
if tt.wantEmpty {
assert.Empty(t, reqs)
return
Expand Down
18 changes: 18 additions & 0 deletions internal/cmd/deploy/guards.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ func checkWorkloadGuards(
))
}

wantRole := string(component.Role)
if wantRole == "" {
wantRole = string(spec.ComponentRoleService)
}
// Missing or empty previous role means service (the only role before
// workers existed). Always compare so service -> worker upgrades are
// rejected even when the prior release omitted role from resolved values.
prevRole, hasRole := prev["role"].(string)
if !hasRole || prevRole == "" {
prevRole = string(spec.ComponentRoleService)
}
if prevRole != wantRole {
errors = append(errors, fmt.Sprintf(
" %s: role change %s -> %s is not supported; delete the release and redeploy",
name, prevRole, wantRole,
))
}

prevSize, hasPrevSize := prev["persistenceSize"].(string)
prevHadPersistence := hasPrevSize && prevSize != ""
nowHasPersistence := component.Persistence != nil
Expand Down
64 changes: 64 additions & 0 deletions internal/cmd/deploy/guards_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,67 @@ func TestCheckWorkloadGuards_SizeParseError(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "parse previous persistence.size")
}

func TestCheckWorkloadGuards_RoleChangeRejected(t *testing.T) {
t.Parallel()
prev := previousResolvedComponents(releaseWithResolved("api", map[string]any{
"workloadKind": "Deployment",
"role": "service",
}).Chart.Values)
manifest := &spec.Spec{
Project: "shop",
Components: map[string]spec.Component{
"api": {Role: spec.ComponentRoleWorker, Image: "worker:1"},
},
}
err := checkWorkloadGuards(manifest, "production", prev)
require.Error(t, err)
assert.Contains(t, err.Error(), "role change")
assert.Contains(t, err.Error(), "service -> worker")
}

func TestCheckWorkloadGuards_SameRoleAllowed(t *testing.T) {
t.Parallel()
prev := previousResolvedComponents(releaseWithResolved("api", map[string]any{
"workloadKind": "Deployment",
"role": "worker",
}).Chart.Values)
manifest := &spec.Spec{
Project: "shop",
Components: map[string]spec.Component{
"api": {Role: spec.ComponentRoleWorker, Image: "worker:1"},
},
}
assert.NoError(t, checkWorkloadGuards(manifest, "production", prev))
}

func TestCheckWorkloadGuards_MissingPrevRoleTreatedAsService(t *testing.T) {
t.Parallel()
prev := previousResolvedComponents(releaseWithResolved("api", map[string]any{
"workloadKind": "Deployment",
}).Chart.Values)
manifest := &spec.Spec{
Project: "shop",
Components: map[string]spec.Component{
"api": {Role: spec.ComponentRoleWorker, Image: "worker:1"},
},
}
err := checkWorkloadGuards(manifest, "production", prev)
require.Error(t, err)
assert.Contains(t, err.Error(), "role change")
assert.Contains(t, err.Error(), "service -> worker")
}

func TestCheckWorkloadGuards_MissingPrevRoleSameAsServiceAllowed(t *testing.T) {
t.Parallel()
prev := previousResolvedComponents(releaseWithResolved("api", map[string]any{
"workloadKind": "Deployment",
}).Chart.Values)
manifest := &spec.Spec{
Project: "shop",
Components: map[string]spec.Component{
"api": {Role: spec.ComponentRoleService, Image: "api:1", Port: 8080},
},
}
assert.NoError(t, checkWorkloadGuards(manifest, "production", prev))
}
Loading
Loading