Skip to content

Commit

Permalink
Merge pull request #209 from fluxcd/override-kubectl-managed-fields
Browse files Browse the repository at this point in the history
Take ownership of kubectl managed fields
  • Loading branch information
stefanprodan authored Jan 13, 2022
2 parents eda0924 + 22005fa commit e693be5
Show file tree
Hide file tree
Showing 10 changed files with 390 additions and 279 deletions.
24 changes: 20 additions & 4 deletions ssa/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,56 @@ go 1.17

require (
github.com/google/go-cmp v0.5.6
k8s.io/api v0.23.0
k8s.io/apimachinery v0.23.0
sigs.k8s.io/cli-utils v0.26.1
k8s.io/api v0.23.1
k8s.io/apimachinery v0.23.1
sigs.k8s.io/cli-utils v0.27.0
sigs.k8s.io/controller-runtime v0.11.0
sigs.k8s.io/yaml v1.3.0
)

require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday v1.5.2 // indirect
github.com/spf13/cobra v1.2.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.7.0 // indirect
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
Expand All @@ -53,8 +67,10 @@ require (
k8s.io/apiextensions-apiserver v0.23.0 // indirect
k8s.io/cli-runtime v0.23.0 // indirect
k8s.io/client-go v0.23.0 // indirect
k8s.io/component-base v0.23.0 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/kubectl v0.22.2 // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
sigs.k8s.io/kustomize/api v0.10.1 // indirect
Expand Down
297 changes: 39 additions & 258 deletions ssa/go.sum

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ssa/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestMain(m *testing.M) {
panic(err)
}

poller := polling.NewStatusPoller(kubeClient, restMapper)
poller := polling.NewStatusPoller(kubeClient, restMapper, nil)

manager = &ResourceManager{
client: kubeClient,
Expand Down
2 changes: 1 addition & 1 deletion ssa/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (m *ResourceManager) GetOwnerLabels(name, namespace string) map[string]stri

func (m *ResourceManager) changeSetEntry(o *unstructured.Unstructured, action Action) *ChangeSetEntry {
return &ChangeSetEntry{
ObjMetadata: object.UnstructuredToObjMetaOrDie(o),
ObjMetadata: object.UnstructuredToObjMetadata(o),
GroupVersion: o.GroupVersionKind().Version,
Subject: FmtUnstructured(o),
Action: string(action),
Expand Down
75 changes: 69 additions & 6 deletions ssa/manager_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,45 @@ package ssa

import (
"context"
"encoding/json"
"fmt"
"sort"
"time"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// ApplyOptions contains options for server-side apply requests.
type ApplyOptions struct {
// Force configures the engine to recreate objects that contain immutable field changes.
Force bool
Force bool `json:"force"`

// Exclusions determines which in-cluster objects are skipped from apply
// based on the specified key-value pairs.
// A nil Exclusions map means all objects are applied
// irregardless of their metadata labels and annotations.
Exclusions map[string]string
// regardless of their metadata labels and annotations.
Exclusions map[string]string `json:"exclusions"`

// WaitTimeout defines after which interval should the engine give up on waiting for
// cluster scoped resources to become ready.
WaitTimeout time.Duration
WaitTimeout time.Duration `json:"waitTimeout"`

// Cleanup defines which in-cluster metadata entries are to be removed before applying objects.
Cleanup ApplyCleanupOptions `json:"cleanup"`
}

// ApplyCleanupOptions defines which metadata entries are to be removed before applying objects.
type ApplyCleanupOptions struct {
// Annotations defines which 'metadata.annotations' keys should be removed from in-cluster objects.
Annotations []string `json:"annotations,omitempty"`

// Labels defines which 'metadata.labels' keys should be removed from in-cluster objects.
Labels []string `json:"labels,omitempty"`

// FieldManagers defines which `metadata.managedFields` managers should be removed from in-cluster objects.
FieldManagers []FiledManager `json:"fieldManagers,omitempty"`
}

// DefaultApplyOptions returns the default apply options where force apply is disabled.
Expand Down Expand Up @@ -76,8 +93,14 @@ func (m *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstru
return nil, m.validationError(dryRunObject, err)
}

patched, err := m.cleanupMetadata(ctx, existingObject, opts.Cleanup)
if err != nil {
return nil, fmt.Errorf("%s metadata.managedFields cleanup failed, error: %w",
FmtUnstructured(existingObject), err)
}

// do not apply objects that have not drifted to avoid bumping the resource version
if !m.hasDrifted(existingObject, dryRunObject) {
if !patched && !m.hasDrifted(existingObject, dryRunObject) {
return m.changeSetEntry(object, UnchangedAction), nil
}

Expand Down Expand Up @@ -121,7 +144,13 @@ func (m *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.
return nil, m.validationError(dryRunObject, err)
}

if m.hasDrifted(existingObject, dryRunObject) {
patched, err := m.cleanupMetadata(ctx, existingObject, opts.Cleanup)
if err != nil {
return nil, fmt.Errorf("%s metadata.managedFields cleanup failed, error: %w",
FmtUnstructured(existingObject), err)
}

if patched || m.hasDrifted(existingObject, dryRunObject) {
toApply = append(toApply, object)
if dryRunObject.GetResourceVersion() == "" {
changeSet.Add(*m.changeSetEntry(dryRunObject, CreatedAction))
Expand Down Expand Up @@ -201,3 +230,37 @@ func (m *ResourceManager) apply(ctx context.Context, object *unstructured.Unstru
}
return m.client.Patch(ctx, object, client.Apply, opts...)
}

// cleanupMetadata performs an HTTP PATCH request to remove entries from metadata annotations, labels and managedFields.
func (m *ResourceManager) cleanupMetadata(ctx context.Context, object *unstructured.Unstructured, opts ApplyCleanupOptions) (bool, error) {
if object == nil {
return false, nil
}
existingObject := object.DeepCopy()
var patches []jsonPatch

if len(opts.Annotations) > 0 {
patches = append(patches, patchRemoveAnnotations(existingObject, opts.Annotations)...)
}

if len(opts.Labels) > 0 {
patches = append(patches, patchRemoveLabels(existingObject, opts.Labels)...)
}

if len(opts.FieldManagers) > 0 {
patches = append(patches, patchRemoveFieldsManagers(existingObject, opts.FieldManagers)...)
}

// no patching is needed exit early
if len(patches) == 0 {
return false, nil
}

rawPatch, err := json.Marshal(patches)
if err != nil {
return false, err
}
patch := client.RawPatch(types.JSONPatchType, rawPatch)

return true, m.client.Patch(ctx, existingObject, patch, client.FieldOwner(m.owner.Field))
}
128 changes: 128 additions & 0 deletions ssa/manager_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
"time"

"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/client"
)
Expand Down Expand Up @@ -410,3 +412,129 @@ func TestApply_Exclusions(t *testing.T) {
}
})
}

func TestApply_Cleanup(t *testing.T) {
timeout := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

applyOpts := DefaultApplyOptions()
applyOpts.Cleanup = ApplyCleanupOptions{
Annotations: []string{corev1.LastAppliedConfigAnnotation},
FieldManagers: []FiledManager{
{
Name: "kubectl",
OperationType: metav1.ManagedFieldsOperationApply,
},
{
Name: "kubectl",
OperationType: metav1.ManagedFieldsOperationUpdate,
},
{
Name: "before-first-apply",
OperationType: metav1.ManagedFieldsOperationUpdate,
},
},
}

id := generateName("cleanup")
objects, err := readManifest("testdata/test2.yaml", id)
if err != nil {
t.Fatal(err)
}
manager.SetOwnerLabels(objects, "app1", "default")

_, deployObject := getFirstObject(objects, "Deployment", id)

if err := SetNativeKindsDefaults(objects); err != nil {
t.Fatal(err)
}

t.Run("creates objects as kubectl", func(t *testing.T) {
for _, object := range objects {
obj := object.DeepCopy()
obj.SetAnnotations(map[string]string{corev1.LastAppliedConfigAnnotation: "test"})
labels := obj.GetLabels()
labels[corev1.LastAppliedConfigAnnotation] = "test"
obj.SetLabels(labels)
if err := manager.client.Create(ctx, obj, client.FieldOwner("kubectl-client-side-apply")); err != nil {
t.Fatal(err)
}
}
})

t.Run("removes kubectl client-side-apply manager and annotation", func(t *testing.T) {
applyOpts.Cleanup.Labels = []string{corev1.LastAppliedConfigAnnotation}
changeSet, err := manager.ApplyAllStaged(ctx, objects, applyOpts)
if err != nil {
t.Fatal(err)
}

for _, entry := range changeSet.Entries {
if diff := cmp.Diff(string(ConfiguredAction), entry.Action); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
}

deploy := deployObject.DeepCopy()
err = manager.Client().Get(ctx, client.ObjectKeyFromObject(deploy), deploy)
if err != nil {
t.Fatal(err)
}

if _, ok := deploy.GetAnnotations()[corev1.LastAppliedConfigAnnotation]; ok {
t.Errorf("%s annotation not removed", corev1.LastAppliedConfigAnnotation)
}

if _, ok := deploy.GetLabels()[corev1.LastAppliedConfigAnnotation]; ok {
t.Errorf("%s label not removed", corev1.LastAppliedConfigAnnotation)
}

expectedManagers := []string{"before-first-apply", manager.owner.Field}
for _, entry := range deploy.GetManagedFields() {
if !containsItemString(expectedManagers, entry.Manager) {
t.Log(entry)
t.Errorf("Mismatch from expected values, want %v got %s", expectedManagers, entry.Manager)
}
}
})

t.Run("removes kubectl server-side-apply manager", func(t *testing.T) {
for _, object := range objects {
obj := object.DeepCopy()
if err := manager.client.Patch(ctx, obj, client.Apply, client.FieldOwner("kubectl")); err != nil {
t.Fatal(err)
}
}

deploy := deployObject.DeepCopy()
err = manager.Client().Get(ctx, client.ObjectKeyFromObject(deploy), deploy)
if err != nil {
t.Fatal(err)
}

changeSet, err := manager.ApplyAll(ctx, objects, applyOpts)
if err != nil {
t.Fatal(err)
}

for _, entry := range changeSet.Entries {
if diff := cmp.Diff(string(ConfiguredAction), entry.Action); diff != "" {
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
}

deploy = deployObject.DeepCopy()
err = manager.Client().Get(ctx, client.ObjectKeyFromObject(deploy), deploy)
if err != nil {
t.Fatal(err)
}

for _, entry := range deploy.GetManagedFields() {
if diff := cmp.Diff(manager.owner.Field, entry.Manager); diff != "" {
t.Log(entry)
t.Errorf("Mismatch from expected value (-want +got):\n%s", diff)
}
}
})
}
6 changes: 1 addition & 5 deletions ssa/manager_wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,7 @@ func DefaultWaitOptions() WaitOptions {

// Wait checks if the given set of objects has been fully reconciled.
func (m *ResourceManager) Wait(objects []*unstructured.Unstructured, opts WaitOptions) error {
objectsMeta, err := object.UnstructuredsToObjMetas(objects)
if err != nil {
return err
}

objectsMeta := object.UnstructuredSetToObjMetadataSet(objects)
if len(objectsMeta) == 0 {
return nil
}
Expand Down
Loading

0 comments on commit e693be5

Please sign in to comment.