-
Notifications
You must be signed in to change notification settings - Fork 24
Add migration support to the SSA client #480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,14 @@ | |
| "context" | ||
| "fmt" | ||
| "reflect" | ||
| "strings" | ||
|
|
||
| apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
| "k8s.io/apimachinery/pkg/runtime" | ||
| "k8s.io/apimachinery/pkg/runtime/schema" | ||
| "k8s.io/apimachinery/pkg/util/sets" | ||
| "k8s.io/client-go/rest" | ||
| "k8s.io/client-go/util/csaupgrade" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| "sigs.k8s.io/controller-runtime/pkg/client/apiutil" | ||
| "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" | ||
|
|
@@ -24,21 +29,59 @@ | |
|
|
||
| // The field owner to use for SSA-applied objects. | ||
| FieldOwner string | ||
|
|
||
| // MigrateSSAByDefault specifies the default SSA migration behavior. | ||
| // | ||
| // When checking for the migration, there is an additional GET of the resource, followed by optional | ||
| // UPDATE (if the migration is needed) before the actual changes to the objects are applied. | ||
| // | ||
| // This field specifies the default behavior that can be overridden by supplying an explicit MigrateSSA() option | ||
| // to ApplyObject or Apply methods. | ||
| // | ||
| // The main advantage of using the SSA in our code is that ability of SSA to handle automatic deletion of fields | ||
| // that we no longer set in our templates. But this only works when the fields are owned by managers and applied | ||
| // using "Apply" operation. As long as there is an "Update" entry with given field (even if the owner is the same) | ||
| // the field WILL NOT be automatically deleted by Kubernetes. | ||
| // | ||
| // Therefore, we need to make sure that our manager uses ONLY the Apply operations. This maximizes the chance | ||
| // that the object will look the way we need. | ||
| MigrateSSAByDefault bool | ||
|
|
||
| // NonSSAFieldOwner should be set to the same value as the user agent used by the provided Kubernetes client | ||
| // or to the value of the explicit field owner that the calling code used to use with the normal CRUD operations | ||
| // (highly unlikely and not the case in our codebase). | ||
| // | ||
| // The user agent can be obtained from the REST config from which the client is constructed. | ||
| // | ||
| // The user agent in the REST config is usually empty, so there's no need to set it here either in that case. | ||
| NonSSAFieldOwner string | ||
| } | ||
|
|
||
| // NewSSAApplyClient creates a new SSAApplyClient from the provided parameters that will use the provided field owner | ||
| // for the patches. | ||
| // | ||
| // The returned client checks for the SSA migration by default. | ||
| func NewSSAApplyClient(cl client.Client, fieldOwner string) *SSAApplyClient { | ||
| return &SSAApplyClient{ | ||
| Client: cl, | ||
| FieldOwner: fieldOwner, | ||
| Client: cl, | ||
| FieldOwner: fieldOwner, | ||
| MigrateSSAByDefault: true, | ||
| } | ||
| } | ||
|
|
||
| type migrateSSA int | ||
|
|
||
| const ( | ||
| migrateSSANotSpecified migrateSSA = iota | ||
| migrateSSAYes | ||
| migrateSSANo | ||
| ) | ||
|
|
||
| type ssaApplyObjectConfiguration struct { | ||
| owner metav1.Object | ||
| newLabels map[string]string | ||
| skipIf func(client.Object) bool | ||
| owner metav1.Object | ||
| newLabels map[string]string | ||
| skipIf func(client.Object) bool | ||
| migrateSSA migrateSSA | ||
| } | ||
|
|
||
| func newSSAApplyObjectConfiguration(options ...SSAApplyObjectOption) ssaApplyObjectConfiguration { | ||
|
|
@@ -76,6 +119,19 @@ | |
| } | ||
| } | ||
|
|
||
| // MigrateSSA instructs the apply to do the SSA managed fields migration or not. | ||
| // If not used at all, the MigrateSSAByDefault field of the SSA client determines | ||
| // whether the fields will be migrated or not. | ||
| func MigrateSSA(value bool) SSAApplyObjectOption { | ||
| return func(config *ssaApplyObjectConfiguration) { | ||
| if value { | ||
| config.migrateSSA = migrateSSAYes | ||
| } else { | ||
| config.migrateSSA = migrateSSANo | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Configure sets the owner reference and merges the labels. Other options modify the logic | ||
| // of apply function and therefore need to be checked manually. | ||
| func (c *ssaApplyObjectConfiguration) Configure(obj client.Object, s *runtime.Scheme) error { | ||
|
|
@@ -100,6 +156,12 @@ | |
| return composeError(obj, fmt.Errorf("failed to prepare the object for SSA: %w", err)) | ||
| } | ||
|
|
||
| if config.migrateSSA == migrateSSAYes || (config.migrateSSA == migrateSSANotSpecified && c.MigrateSSAByDefault) { | ||
| if err := c.migrateSSA(ctx, obj); err != nil { | ||
| return composeError(obj, err) | ||
| } | ||
| } | ||
|
|
||
| if config.skipIf != nil && config.skipIf(obj) { | ||
| return nil | ||
| } | ||
|
|
@@ -111,6 +173,32 @@ | |
| return nil | ||
| } | ||
|
|
||
| func (c *SSAApplyClient) migrateSSA(ctx context.Context, obj client.Object) error { | ||
| orig := obj.DeepCopyObject().(client.Object) | ||
| if err := c.Client.Get(ctx, client.ObjectKeyFromObject(obj), orig); err != nil { | ||
| if !apierrors.IsNotFound(err) { | ||
| return fmt.Errorf("failed to get the object from the cluster while migrating managed fields: %w", err) | ||
| } | ||
| orig = nil | ||
| } | ||
|
|
||
| if orig != nil { | ||
| oldFieldOwner := c.NonSSAFieldOwner | ||
| if len(oldFieldOwner) == 0 { | ||
| // this is how the kubernetes api server determines the default owner from the user agent | ||
| // The default user agent has the form of "name-of-binary/version information etc.". | ||
| // The owner is the first part of the UA unless explicitly specified in the request URI. | ||
| oldFieldOwner = strings.Split(rest.DefaultKubernetesUserAgent(), "/")[0] | ||
| } | ||
|
Comment on lines
+187
to
+192
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. related to my previous comment, do you think that we would need to se the owner name explicitly? Couldn't we rely on the default behavior only?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could, but the default behavior makes assumptions that I was not 100% sure we satisfy everywhere in the codebase, so I wanted to have an explicit option available for the case we find a place where the defaults would not be applicable. The default behavior assumes that:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm not aware of any place that would change any of these explicitly, so everything should comply. Anyway, thanks for the explanation. It's fine to keep it there if not required to be set explicitly by default (only as a backup solution for some corner-cases). |
||
| if isSsaMigrationNeeded(orig, oldFieldOwner) { | ||
| if err := migrateToSSA(ctx, c.Client, orig, oldFieldOwner, c.FieldOwner); err != nil { | ||
| return fmt.Errorf("failed to migrate the managed fields: %w", err) | ||
| } | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func composeError(obj client.Object, err error) error { | ||
| message := "unable to patch '%s' called '%s' in namespace '%s': %w" | ||
| if !obj.GetObjectKind().GroupVersionKind().Empty() { | ||
|
|
@@ -157,3 +245,19 @@ | |
| } | ||
| return nil | ||
| } | ||
|
|
||
| func isSsaMigrationNeeded(obj client.Object, expectedOwner string) bool { | ||
| for _, mf := range obj.GetManagedFields() { | ||
| if mf.Manager == expectedOwner && mf.Operation != metav1.ManagedFieldsOperationApply { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func migrateToSSA(ctx context.Context, cl client.Client, obj client.Object, oldFieldOwner, newFieldOwner string) error { | ||
| if err := csaupgrade.UpgradeManagedFields(obj, sets.New(oldFieldOwner), newFieldOwner); err != nil { | ||
| return err | ||
| } | ||
| return cl.Update(ctx, obj) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in your code in the previous PR, you originally had a code that was able to construct the owner name we should migrate from - couldn't we use that?
https://github.com/metlos/toolchain-common/blob/6ff87dfbb853a19475dff63b22a0e5a2f51300d9/pkg/client/ssa_client.go#L50-L62
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We technically don't need this because no code in the current codebase sets the field owner explicitly in the CRUD operations and therefore all the code uses the default field owner derived from the user agent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to have this option available though in case I missed something and we for example set the user agent of the kubernetes client to an explicit value somewhere in the code.
In that case, we'd need to set this to an explicit value, too.