Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
da1279f
feature: zalando.org/forward-backend annotation support to enable mig…
szuecs Oct 14, 2025
96701d5
feature: externalIngress needs to be notified that we do a cluster mi…
szuecs Oct 24, 2025
303f906
refactor: move resource patching into the stack
szuecs Oct 24, 2025
77c2e0c
doc: add context how the migration will work so we will understand in…
szuecs Oct 24, 2025
ae8b458
reafctor: change to pathc all resource creations
szuecs Nov 6, 2025
c10449f
doc: move the migration docs
szuecs Nov 6, 2025
ce65958
revert pkg/core/types.go
szuecs Nov 6, 2025
ac011d7
refactor: simplify test check
szuecs Nov 7, 2025
6a92b86
external ingress controllers should read stack.Annotations
szuecs Nov 7, 2025
2647d92
deployment test
szuecs Nov 7, 2025
c4a3a44
doc: link to ingress annotation
szuecs Nov 7, 2025
97032a1
add tests
szuecs Nov 7, 2025
dc42acc
fix: panic if stacktemplate annotations are nil
szuecs Nov 7, 2025
1d7afaf
fix: we do not need a deployment nor hpa if we migrate
szuecs Nov 7, 2025
dc95403
fix panic
szuecs Nov 7, 2025
d1f7183
fix: override backends was using no ptr override
szuecs Nov 7, 2025
0a93d0a
fix routegroup test
szuecs Nov 7, 2025
e7691b7
fix: deployment nil
szuecs Nov 17, 2025
7980179
deployment is required for CI/CD pipeline checks
szuecs Nov 19, 2025
18076ec
fix: test case should test for deisred 1 and not nil deployment
szuecs Nov 19, 2025
140b4a5
cleanup routegroup generation test
szuecs Nov 20, 2025
d1ed608
test: add case that routegroup segment will be created correctly for …
szuecs Nov 20, 2025
1b1153c
doc: fix comments to fit the current implementation
szuecs Nov 20, 2025
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
16 changes: 16 additions & 0 deletions docs/stack_crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,22 @@ spec:
- type: integer
- type: string
x-kubernetes-int-or-string: true
metadata:
description: |-
EmbeddedObjectMetaWithAnnotations defines the metadata which can be attached
to a resource. It's a slimmed down version of metav1.ObjectMeta only
containing annotations.
properties:
annotations:
additionalProperties:
type: string
description: |-
Annotations is an unstructured key value map stored with a resource that may be
set by external tools to store and retrieve arbitrary metadata. They are not
queryable and should be preserved when modifying objects.
More info: http://kubernetes.io/docs/user-guide/annotations
type: object
type: object
required:
- backendPort
type: object
Expand Down
22 changes: 16 additions & 6 deletions docs/stackset_crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ spec:
- type: integer
- type: string
x-kubernetes-int-or-string: true
metadata:
description: |-
EmbeddedObjectMetaWithAnnotations defines the metadata which can be attached
to a resource. It's a slimmed down version of metav1.ObjectMeta only
containing annotations.
properties:
annotations:
additionalProperties:
type: string
description: |-
Annotations is an unstructured key value map stored with a resource that may be
set by external tools to store and retrieve arbitrary metadata. They are not
queryable and should be preserved when modifying objects.
More info: http://kubernetes.io/docs/user-guide/annotations
type: object
type: object
required:
- backendPort
type: object
Expand Down Expand Up @@ -3589,12 +3605,6 @@ spec:
Must be set if and only if type is "Localhost".
type: string
type:
description: |-
type indicates which kind of AppArmor profile will be applied.
Valid options are:
Localhost - a profile pre-loaded on the node.
RuntimeDefault - the container runtime's default profile.
Unconfined - no AppArmor enforcement.
type: string
required:
- type
Expand Down
3 changes: 2 additions & 1 deletion pkg/apis/zalando.org/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ func (s *StackSetIngressSpec) GetAnnotations() map[string]string {
// backendport for ingress managed outside of stackset.
// +k8s:deepcopy-gen=true
type StackSetExternalIngressSpec struct {
BackendPort intstr.IntOrString `json:"backendPort"`
EmbeddedObjectMetaWithAnnotations `json:"metadata,omitempty"`
BackendPort intstr.IntOrString `json:"backendPort"`
}

// RouteGroupSpec defines the specification for defining a RouteGroup attached
Expand Down
5 changes: 3 additions & 2 deletions pkg/apis/zalando.org/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions pkg/core/stack_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ func objectMetaInjectLabels(objectMeta metav1.ObjectMeta, labels map[string]stri
return objectMeta
}

// patchForwardBackend rewrites a RouteGroupSpec to send all traffic to another cluster chosen by the operator of skipper
func patchForwardBackend(rg *rgv1.RouteGroupSpec) {
rg.Backends = []rgv1.RouteGroupBackend{
{
Name: forwardBackendName,
Type: rgv1.ForwardRouteGroupBackend,
},
}
for _, route := range rg.Routes {
route.Backends = []rgv1.RouteGroupBackendReference{
{
BackendName: forwardBackendName,
},
}
}
}

func (sc *StackContainer) objectMeta(segment bool) metav1.ObjectMeta {
resourceLabels := mapCopy(sc.Stack.Labels)

Expand Down Expand Up @@ -169,6 +186,10 @@ func (sc *StackContainer) selector() map[string]string {
return limitLabels(sc.Stack.Labels, selectorLabels)
}

// GenerateDeployment generates a deployment as configured in the
// stack. On cluster migrations set by stackset annotation
// "zalando.org/forward-backend", the deployment will be set to
// replicas 1.
func (sc *StackContainer) GenerateDeployment() *appsv1.Deployment {
stack := sc.Stack

Expand Down Expand Up @@ -224,9 +245,19 @@ func (sc *StackContainer) GenerateDeployment() *appsv1.Deployment {
if strategy != nil {
deployment.Spec.Strategy = *strategy
}

if _, clusterMigration := sc.Stack.Annotations[forwardBackendAnnotation]; clusterMigration {
i := int32(1)
deployment.Spec.Replicas = &i
}

return deployment
}

// GenerateHPA generates a hpa as configured in the
// stack. On cluster migrations set by stackset annotation
// "zalando.org/forward-backend", the hpa will be set to
// minReplicas = maxReplicass = 1.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just skip generating an HPA in this case, this is similar to if sc.ScaleDown() case below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My fear about removing objects we don't need is that we create alerts for the users and they overreact.
It's also not checking for traffic as we do in ScaleDown()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We anyway have to document how this feature works and what to expect.

They could also "overreact" because of minReplicas/maxReplicas = 1 :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think now?
I dropped HPA and deployment to not waste money.

func (sc *StackContainer) GenerateHPA() (
*autoscaling.HorizontalPodAutoscaler,
error,
Expand Down Expand Up @@ -281,6 +312,12 @@ func (sc *StackContainer) GenerateHPA() (
result.Spec.MinReplicas = &pr
}

if _, clusterMigration := sc.Stack.Annotations[forwardBackendAnnotation]; clusterMigration {
i := int32(1)
result.Spec.MinReplicas = &i
result.Spec.MaxReplicas = i
}

return result, nil
}

Expand Down Expand Up @@ -379,6 +416,10 @@ func (sc *StackContainer) GenerateIngressSegment() (
return res, nil
}

// generateIngress generates an ingress as configured in the stack.
// On cluster migrations set by stackset annotation
// "zalando.org/forward-backend", the annotation will be copied to the
// ingress.
func (sc *StackContainer) generateIngress(segment bool) (
*networking.Ingress,
error,
Expand Down Expand Up @@ -430,6 +471,9 @@ func (sc *StackContainer) generateIngress(segment bool) (
Rules: rules,
},
}
if _, clusterMigration := sc.Stack.Annotations[forwardBackendAnnotation]; clusterMigration {
result.Annotations["zalando.org/skipper-backend"] = "forward"
}

// insert annotations
result.Annotations = mergeLabels(
Expand Down Expand Up @@ -470,6 +514,10 @@ func (sc *StackContainer) GenerateRouteGroupSegment() (
return res, nil
}

// generateRouteGroup generates an RouteGroup as configured in the
// stack. On cluster migrations set by stackset annotation
// "zalando.org/forward-backend", the RouteGroup will be patched by
// patchForwardBackend() to execute the migration.
func (sc *StackContainer) generateRouteGroup(segment bool) (
*rgv1.RouteGroup,
error,
Expand Down Expand Up @@ -529,6 +577,10 @@ func (sc *StackContainer) generateRouteGroup(segment bool) (
sc.routeGroupSpec.GetAnnotations(),
)

if _, clusterMigration := sc.Stack.Annotations[forwardBackendAnnotation]; clusterMigration {
patchForwardBackend(&result.Spec)
}

return result, nil
}

Expand Down
13 changes: 12 additions & 1 deletion pkg/core/stackset.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const (
StackVersionLabelKey = "stack-version"

ingressTrafficAuthoritativeAnnotation = "zalando.org/traffic-authoritative"
forwardBackendAnnotation = "zalando.org/forward-backend"
forwardBackendName = "fwd"
)

var (
Expand Down Expand Up @@ -52,6 +54,7 @@ func sanitizeServicePorts(service *zv1.StackServiceSpec) *zv1.StackServiceSpec {

// NewStack returns an (optional) stack that should be created
func (ssc *StackSetContainer) NewStack() (*StackContainer, string) {
_, forwardMigration := ssc.StackSet.ObjectMeta.Annotations[forwardBackendAnnotation]
observedStackVersion := ssc.StackSet.Status.ObservedStackVersion
stackVersion := currentStackVersion(ssc.StackSet)
stackName := generateStackName(ssc.StackSet, stackVersion)
Expand All @@ -74,12 +77,20 @@ func (ssc *StackSetContainer) NewStack() (*StackContainer, string) {

if ssc.StackSet.Spec.ExternalIngress != nil {
spec.ExternalIngress = ssc.StackSet.Spec.ExternalIngress.DeepCopy()
if forwardMigration {
spec.ExternalIngress.Annotations[forwardBackendAnnotation] = forwardBackendName
}
}

if ssc.StackSet.Spec.RouteGroup != nil {
spec.RouteGroup = ssc.StackSet.Spec.RouteGroup.DeepCopy()
}

stackAnnotations := ssc.StackSet.Spec.StackTemplate.Annotations
if forwardMigration {
stackAnnotations[forwardBackendAnnotation] = forwardBackendName
}

return &StackContainer{
Stack: &zv1.Stack{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -100,7 +111,7 @@ func (ssc *StackSetContainer) NewStack() (*StackContainer, string) {
ssc.StackSet.Labels,
map[string]string{StackVersionLabelKey: stackVersion},
),
Annotations: ssc.StackSet.Spec.StackTemplate.Annotations,
Annotations: stackAnnotations,
},
Spec: *spec,
},
Expand Down