1- // Copyright 2021-2025 the Pinniped contributors. All Rights Reserved.
1+ // Copyright 2021-2026 the Pinniped contributors. All Rights Reserved.
22// SPDX-License-Identifier: Apache-2.0
33
44package impersonatorconfig
@@ -11,8 +11,9 @@ import (
1111 "encoding/json"
1212 "encoding/pem"
1313 "fmt"
14+ "maps"
1415 "net"
15- "sort "
16+ "slices "
1617 "strings"
1718 "time"
1819
@@ -56,6 +57,7 @@ const (
5657 caKeyKey = "ca.key"
5758 appLabelKey = "app"
5859 annotationKeysKey = "credentialissuer.pinniped.dev/annotation-keys"
60+ labelKeysKey = "credentialissuer.pinniped.dev/label-keys"
5961)
6062
6163type impersonatorConfigController struct {
@@ -626,6 +628,62 @@ func (c *impersonatorConfigController) ensureClusterIPServiceIsStopped(ctx conte
626628 return utilerrors .FilterOut (err , apierrors .IsNotFound )
627629}
628630
631+ // recordDesiredKeysAsBookkeepingAnnotation adds a bookkeeping annotation to the given service that records which
632+ // keys were explicitly desired by this controller. This allows future runs to detect which keys have been removed
633+ // from the desired state (so they can be cleaned up) vs. which keys belong to other actors (which should
634+ // be left alone). The bookkeepingKey is the annotation key under which the JSON-encoded list of desired keys will
635+ // be stored. If the desiredMap is empty, no bookkeeping entry is added.
636+ func recordDesiredKeysAsBookkeepingAnnotation (service * corev1.Service , desiredMap map [string ]string , bookkeepingKey string ) error {
637+ desiredKeys := slices .Sorted (maps .Keys (desiredMap ))
638+
639+ if len (desiredKeys ) > 0 {
640+ keysJSONArray , err := json .Marshal (desiredKeys )
641+ if err != nil {
642+ return err // This shouldn't really happen. We should always be able to marshal an array of strings.
643+ }
644+
645+ // Save the desired keys to a bookkeeping annotation on the service.
646+ if service .Annotations == nil {
647+ service .Annotations = map [string ]string {}
648+ }
649+ service .Annotations [bookkeepingKey ] = string (keysJSONArray )
650+ }
651+
652+ return nil
653+ }
654+
655+ func mergeMap (existingMap map [string ]string , desiredMap map [string ]string , existingBookkeepingSource map [string ]string , bookkeepingKey string ) map [string ]string {
656+ resultMap := maps .Clone (existingMap )
657+ if resultMap == nil {
658+ resultMap = map [string ]string {}
659+ }
660+
661+ // Merge desired into existing, with desired overwriting when there are conflicts.
662+ for k , v := range desiredMap {
663+ resultMap [k ] = v
664+ }
665+
666+ // Check if the existing Service contains a record of previous keys that were added by this controller.
667+ // Note that in an upgrade, older versions of Pinniped might have created the Service without this bookkeeping entry.
668+ oldDesiredKeysJSON , foundOldDesiredKeysJSON := existingBookkeepingSource [bookkeepingKey ]
669+ oldDesiredKeys := []string {}
670+ if foundOldDesiredKeysJSON {
671+ _ = json .Unmarshal ([]byte (oldDesiredKeysJSON ), & oldDesiredKeys )
672+ // In the unlikely event that we cannot parse the value of our bookkeeping entry, just act like it
673+ // wasn't present and update it to the new value that it should have based on the current desired state.
674+ }
675+
676+ // Check if any keys which were previously managed by this controller are now gone from the desired state,
677+ // which means that those now-missing keys should get deleted.
678+ for _ , oldKey := range oldDesiredKeys {
679+ if _ , existsInDesired := desiredMap [oldKey ]; ! existsInDesired {
680+ delete (resultMap , oldKey )
681+ }
682+ }
683+
684+ return resultMap
685+ }
686+
629687func (c * impersonatorConfigController ) createOrUpdateService (ctx context.Context , desiredService * corev1.Service ) error {
630688 log := c .log .WithValues ("serviceType" , desiredService .Spec .Type , "service" , klog .KObj (desiredService ))
631689
@@ -634,19 +692,15 @@ func (c *impersonatorConfigController) createOrUpdateService(ctx context.Context
634692 // to be able to detect that the missing key means that we should remove the key. This is needed to
635693 // differentiate it from a key that was added by another actor, which we should not remove.
636694 // But don't bother recording the requested annotations if there were no annotations requested.
637- desiredAnnotationKeys := make ([]string , 0 , len (desiredService .Annotations ))
638- for k := range desiredService .Annotations {
639- desiredAnnotationKeys = append (desiredAnnotationKeys , k )
640- }
641- if len (desiredAnnotationKeys ) > 0 {
642- // Sort them since they come out of the map in no particular order.
643- sort .Strings (desiredAnnotationKeys )
644- keysJSONArray , err := json .Marshal (desiredAnnotationKeys )
645- if err != nil {
646- return err // This shouldn't really happen. We should always be able to marshal an array of strings.
647- }
648- // Save the desired annotations to a bookkeeping annotation.
649- desiredService .Annotations [annotationKeysKey ] = string (keysJSONArray )
695+ err := recordDesiredKeysAsBookkeepingAnnotation (desiredService , desiredService .Annotations , annotationKeysKey )
696+ if err != nil {
697+ return err
698+ }
699+
700+ // Similarly, prepare to remember which label keys were added by this controller.
701+ err = recordDesiredKeysAsBookkeepingAnnotation (desiredService , desiredService .Labels , labelKeysKey )
702+ if err != nil {
703+ return err
650704 }
651705
652706 // Get the Service from the informer, and create it if it does not already exist.
@@ -662,49 +716,35 @@ func (c *impersonatorConfigController) createOrUpdateService(ctx context.Context
662716
663717 // The Service already exists, so update only the specific fields that are meaningfully part of our desired state.
664718 updatedService := existingService .DeepCopy ()
665- updatedService .Labels = desiredService .Labels
666719 updatedService .Spec .LoadBalancerIP = desiredService .Spec .LoadBalancerIP
667720 updatedService .Spec .Type = desiredService .Spec .Type
668721 updatedService .Spec .Selector = desiredService .Spec .Selector
669722
670723 // Do not simply overwrite the existing annotations with the desired annotations. Instead, merge-overwrite.
671724 // Another actor in the system, like a human user or a non-Pinniped controller, might have updated the
672- // existing Service's annotations. If they did, then we do not want to overwrite those keys expect for
725+ // existing Service's annotations. If they did, then we do not want to overwrite those keys except for
673726 // the specific keys that are from the CredentialIssuer's spec, because if we overwrite keys belonging
674727 // to another controller then we could end up infinitely flapping back and forth with the other controller,
675728 // both updating that annotation on the Service.
676- if updatedService .Annotations == nil {
677- updatedService .Annotations = map [string ]string {}
678- }
679- for k , v := range desiredService .Annotations {
680- updatedService .Annotations [k ] = v
681- }
682-
683- // Check if the the existing Service contains a record of previous annotations that were added by this controller.
684- // Note that in an upgrade, older versions of Pinniped might have created the Service without this bookkeeping annotation.
685- oldDesiredAnnotationKeysJSON , foundOldDesiredAnnotationKeysJSON := existingService .Annotations [annotationKeysKey ]
686- oldDesiredAnnotationKeys := []string {}
687- if foundOldDesiredAnnotationKeysJSON {
688- _ = json .Unmarshal ([]byte (oldDesiredAnnotationKeysJSON ), & oldDesiredAnnotationKeys )
689- // In the unlikely event that we cannot parse the value of our bookkeeping annotation, just act like it
690- // wasn't present and update it to the new value that it should have based on the current desired state.
691- }
692-
693- // Check if any annotations which were previously in the CredentialIssuer spec are now gone from the spec,
694- // which means that those now-missing annotations should get deleted.
695- for _ , oldKey := range oldDesiredAnnotationKeys {
696- if _ , existsInDesired := desiredService .Annotations [oldKey ]; ! existsInDesired {
697- delete (updatedService .Annotations , oldKey )
698- }
699- }
729+ updatedService .Annotations = mergeMap (existingService .Annotations , desiredService .Annotations , existingService .Annotations , annotationKeysKey )
700730
701731 // If no annotations were requested, then remove the special bookkeeping annotation which might be
702732 // leftover from a previous update. During the next update, non-existence will be taken to mean
703733 // that no annotations were previously requested by the CredentialIssuer spec.
704- if len (desiredAnnotationKeys ) == 0 {
734+ if len (desiredService . Annotations ) == 0 || mapHasExactlyOneKey ( desiredService . Annotations , labelKeysKey ) {
705735 delete (updatedService .Annotations , annotationKeysKey )
706736 }
707737
738+ // Same merge strategy as above, but for labels this time.
739+ updatedService .Labels = mergeMap (existingService .Labels , desiredService .Labels , existingService .Annotations , labelKeysKey )
740+
741+ // If no labels were requested, then remove the special bookkeeping annotation which might be
742+ // leftover from a previous update. During the next update, non-existence will be taken to mean
743+ // that no labels were previously requested by this controller.
744+ if len (desiredService .Labels ) == 0 {
745+ delete (updatedService .Annotations , labelKeysKey )
746+ }
747+
708748 // If our updates didn't change anything, we're done.
709749 if equality .Semantic .DeepEqual (existingService , updatedService ) {
710750 return nil
@@ -1222,3 +1262,11 @@ func validateCredentialIssuerSpec(spec *conciergeconfigv1alpha1.ImpersonationPro
12221262
12231263 return nil
12241264}
1265+
1266+ func mapHasExactlyOneKey (m map [string ]string , key string ) bool {
1267+ if len (m ) != 1 {
1268+ return false
1269+ }
1270+ _ , ok := m [key ]
1271+ return ok
1272+ }
0 commit comments