Merge pull request #1823 from briansonnenberg/main

Add `unmanagedMetadata` to canary service specification
This commit is contained in:
Stefan Prodan
2025-10-15 09:52:08 +03:00
committed by GitHub
8 changed files with 260 additions and 4 deletions

View File

@@ -909,6 +909,18 @@ spec:
type: object
additionalProperties:
type: string
unmanagedMetadata:
description: UnmanagedMetadata is a list of metadata keys that should be ignored by Flagger.
type: object
properties:
annotations:
type: array
items:
type: string
labels:
type: array
items:
type: string
skipAnalysis:
description: Skip analysis and promote canary
type: boolean

View File

@@ -909,6 +909,18 @@ spec:
type: object
additionalProperties:
type: string
unmanagedMetadata:
description: UnmanagedMetadata is a list of metadata keys that should be ignored by Flagger.
type: object
properties:
annotations:
type: array
items:
type: string
labels:
type: array
items:
type: string
skipAnalysis:
description: Skip analysis and promote canary
type: boolean

View File

@@ -206,6 +206,10 @@ Note that the `apex` annotations are added to both the generated Kubernetes Serv
generated service mesh/ingress object. This allows using external-dns with Istio `VirtualServices`
and `TraefikServices`. Beware of configuration conflicts [here](../faq.md#ExternalDNS).
Note that if any annotations or labels are added that are not specified here,
Flagger will remove them during reconciliation. To specify metadata
that should be ignored by Flagger, configure `unmanagedMetadata`.
If you want for the generated Kubernetes ClusterIP services to be [headless](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services),
then set `service.headless` to true.

View File

@@ -909,6 +909,18 @@ spec:
type: object
additionalProperties:
type: string
unmanagedMetadata:
description: UnmanagedMetadata is a list of metadata keys that should be ignored by Flagger.
type: object
properties:
annotations:
type: array
items:
type: string
labels:
type: array
items:
type: string
skipAnalysis:
description: Skip analysis and promote canary
type: boolean

View File

@@ -230,6 +230,17 @@ type CanaryService struct {
// Canary is the metadata to add to the canary service
// +optional
Canary *CustomMetadata `json:"canary,omitempty"`
// UnmanagedMetadata is a list of metadata keys that should be ignored by Flagger.
// Flagger will not add, remove or change the value of these annotations.
// +optional
UnmanagedMetadata *UnmanagedMetadata `json:"unmanagedMetadata,omitempty"`
}
// UnmanagedMetadata is a list of metadata keys that should be ignored by Flagger.
type UnmanagedMetadata struct {
Annotations []string `json:"annotations,omitempty"`
Labels []string `json:"labels,omitempty"`
}
// CanaryAnalysis is used to describe how the analysis should be done

View File

@@ -452,6 +452,11 @@ func (in *CanaryService) DeepCopyInto(out *CanaryService) {
*out = new(CustomMetadata)
(*in).DeepCopyInto(*out)
}
if in.UnmanagedMetadata != nil {
in, out := &in.UnmanagedMetadata, &out.UnmanagedMetadata
*out = new(UnmanagedMetadata)
(*in).DeepCopyInto(*out)
}
return
}
@@ -926,3 +931,29 @@ func (in *SessionAffinity) DeepCopy() *SessionAffinity {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UnmanagedMetadata) DeepCopyInto(out *UnmanagedMetadata) {
*out = *in
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UnmanagedMetadata.
func (in *UnmanagedMetadata) DeepCopy() *UnmanagedMetadata {
if in == nil {
return nil
}
out := new(UnmanagedMetadata)
in.DeepCopyInto(out)
return out
}

View File

@@ -213,12 +213,42 @@ func (c *KubernetesDefaultRouter) reconcileService(canary *flaggerv1.Canary, nam
if svc.ObjectMeta.Annotations == nil {
svc.ObjectMeta.Annotations = make(map[string]string)
}
if diff := cmp.Diff(filterMetadata(metadata.Annotations), svc.ObjectMeta.Annotations); diff != "" {
svcClone.ObjectMeta.Annotations = filterMetadata(metadata.Annotations)
// Preserve unmanaged metadata
unmanagedAnnotations := make(map[string]string)
if canary.Spec.Service.UnmanagedMetadata != nil {
for _, key := range canary.Spec.Service.UnmanagedMetadata.Annotations {
if value, ok := svc.ObjectMeta.Annotations[key]; ok {
unmanagedAnnotations[key] = value
}
}
}
unmanagedLabels := make(map[string]string)
if canary.Spec.Service.UnmanagedMetadata != nil {
for _, key := range canary.Spec.Service.UnmanagedMetadata.Labels {
if value, ok := svc.ObjectMeta.Labels[key]; ok {
unmanagedLabels[key] = value
}
}
}
newAnnotations := filterMetadata(metadata.Annotations)
for k, v := range unmanagedAnnotations {
newAnnotations[k] = v
}
newLabels := metadata.Labels
for k, v := range unmanagedLabels {
newLabels[k] = v
}
if diff := cmp.Diff(newAnnotations, svc.ObjectMeta.Annotations); diff != "" {
svcClone.ObjectMeta.Annotations = newAnnotations
updateService = true
}
if diff := cmp.Diff(metadata.Labels, svc.ObjectMeta.Labels); diff != "" {
svcClone.ObjectMeta.Labels = metadata.Labels
if diff := cmp.Diff(newLabels, svc.ObjectMeta.Labels); diff != "" {
svcClone.ObjectMeta.Labels = newLabels
updateService = true
}
}

View File

@@ -451,3 +451,147 @@ func TestServiceRouter_ReconcileMetadata(t *testing.T) {
assert.Equal(t, "test1", apexSvc.Labels["test"])
assert.Equal(t, "podinfo", apexSvc.Labels["app"])
}
func TestServiceRouter_UnmanagedAnnotations(t *testing.T) {
mocks := newFixture(nil)
router := &KubernetesDefaultRouter{
kubeClient: mocks.kubeClient,
flaggerClient: mocks.flaggerClient,
logger: mocks.logger,
labelSelector: "app",
}
mocks.canary.Spec.Service.Apex = &flaggerv1.CustomMetadata{
Annotations: map[string]string{"test": "expectedvalue"},
}
mocks.canary.Spec.Service.UnmanagedMetadata = &flaggerv1.UnmanagedMetadata{
Annotations: []string{"unmanaged"},
}
err := router.Initialize(mocks.canary)
require.NoError(t, err)
err = router.Reconcile(mocks.canary)
require.NoError(t, err)
apexSvc, err := mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
clone := apexSvc.DeepCopy()
clone.Annotations["unmanaged"] = "true"
clone.Annotations["test"] = "newvalue"
clone.Annotations["removable"] = "true"
_, err = mocks.kubeClient.CoreV1().Services("default").Update(context.TODO(), clone, metav1.UpdateOptions{})
require.NoError(t, err)
err = router.Reconcile(mocks.canary)
require.NoError(t, err)
apexSvc, err = mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, "expectedvalue", apexSvc.Annotations["test"])
assert.Equal(t, "true", apexSvc.Annotations["unmanaged"])
_, ok := apexSvc.Annotations["removable"]
assert.False(t, ok)
}
func TestServiceRouter_UnmanagedLabels(t *testing.T) {
mocks := newFixture(nil)
router := &KubernetesDefaultRouter{
kubeClient: mocks.kubeClient,
flaggerClient: mocks.flaggerClient,
logger: mocks.logger,
labelSelector: "app",
}
mocks.canary.Spec.Service.Apex = &flaggerv1.CustomMetadata{
Labels: map[string]string{"test": "expectedvalue"},
}
mocks.canary.Spec.Service.UnmanagedMetadata = &flaggerv1.UnmanagedMetadata{
Labels: []string{"unmanaged"},
}
err := router.Initialize(mocks.canary)
require.NoError(t, err)
err = router.Reconcile(mocks.canary)
require.NoError(t, err)
apexSvc, err := mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
clone := apexSvc.DeepCopy()
clone.Labels["unmanaged"] = "true"
clone.Labels["test"] = "newvalue"
clone.Labels["removable"] = "true"
_, err = mocks.kubeClient.CoreV1().Services("default").Update(context.TODO(), clone, metav1.UpdateOptions{})
require.NoError(t, err)
err = router.Reconcile(mocks.canary)
require.NoError(t, err)
apexSvc, err = mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, "expectedvalue", apexSvc.Labels["test"])
assert.Equal(t, "true", apexSvc.Labels["unmanaged"])
_, ok := apexSvc.Labels["removable"]
assert.False(t, ok)
}
func TestServiceRouter_UnmanagedMetadata_AnnotationsAndLabels(t *testing.T) {
mocks := newFixture(nil)
router := &KubernetesDefaultRouter{
kubeClient: mocks.kubeClient,
flaggerClient: mocks.flaggerClient,
logger: mocks.logger,
labelSelector: "app",
}
mocks.canary.Spec.Service.Apex = &flaggerv1.CustomMetadata{
Annotations: map[string]string{"test": "expectedvalue"},
Labels: map[string]string{"test": "expectedvalue"},
}
mocks.canary.Spec.Service.UnmanagedMetadata = &flaggerv1.UnmanagedMetadata{
Annotations: []string{"unmanaged"},
Labels: []string{"unmanaged"},
}
err := router.Initialize(mocks.canary)
require.NoError(t, err)
err = router.Reconcile(mocks.canary)
require.NoError(t, err)
apexSvc, err := mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
clone := apexSvc.DeepCopy()
clone.Annotations["unmanaged"] = "true"
clone.Annotations["test"] = "newvalue"
clone.Annotations["removable"] = "true"
clone.Labels["unmanaged"] = "true"
clone.Labels["test"] = "newvalue"
clone.Labels["removable"] = "true"
_, err = mocks.kubeClient.CoreV1().Services("default").Update(context.TODO(), clone, metav1.UpdateOptions{})
require.NoError(t, err)
err = router.Reconcile(mocks.canary)
require.NoError(t, err)
apexSvc, err = mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
// The result should be that the canary spec annotations should be changed back to configured canary value,
// and the unmanaged annotation should remain unchanged.
assert.Equal(t, "expectedvalue", apexSvc.Annotations["test"])
assert.Equal(t, "true", apexSvc.Annotations["unmanaged"])
_, ok := apexSvc.Annotations["removable"]
assert.False(t, ok)
assert.Equal(t, "expectedvalue", apexSvc.Labels["test"])
assert.Equal(t, "true", apexSvc.Labels["unmanaged"])
_, ok = apexSvc.Labels["removable"]
assert.False(t, ok)
}