diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index 9385b05..d579e35 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -94,10 +94,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("status poller init failed: %w", err) } - resMgr := manager.NewResourceManager(kubeClient, statusPoller, manager.Owner{ - Field: PROJECT, - Group: PROJECT + ".dev", - }) + resMgr := manager.NewResourceManager(kubeClient, statusPoller, inventoryOwner) resMgr.SetOwnerLabels(objects, applyArgs.inventoryName, applyArgs.inventoryNamespace) @@ -133,12 +130,12 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("mode not supported") } - staleObjects, err := inventoryMgr.GetStaleObjects(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) + staleObjects, err := resMgr.GetInventoryStaleObjects(ctx, newInventory) if err != nil { return fmt.Errorf("inventory query failed, error: %w", err) } - err = inventoryMgr.Store(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) + err = resMgr.ApplyInventory(ctx, newInventory) if err != nil { return fmt.Errorf("inventory apply failed, error: %w", err) } diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index e2f0554..6d8fd62 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -19,6 +19,7 @@ package main import ( "context" "fmt" + "github.com/stefanprodan/kustomizer/pkg/inventory" "os" "sort" "time" @@ -74,13 +75,10 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("status poller init failed: %w", err) } - resMgr := manager.NewResourceManager(kubeClient, statusPoller, manager.Owner{ - Field: PROJECT, - Group: PROJECT + ".dev", - }) + resMgr := manager.NewResourceManager(kubeClient, statusPoller, inventoryOwner) - inv, err := inventoryMgr.Retrieve(ctx, kubeClient, deleteArgs.inventoryName, deleteArgs.inventoryNamespace) - if err != nil { + inv := inventory.NewInventory(applyArgs.inventoryName, applyArgs.inventoryNamespace) + if err := resMgr.GetInventory(ctx, inv); err != nil { return err } @@ -106,8 +104,7 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { os.Exit(1) } - err = inventoryMgr.Remove(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) - if err != nil { + if err := resMgr.DeleteInventory(ctx, inv); err != nil { return err } diff --git a/cmd/kustomizer/diff.go b/cmd/kustomizer/diff.go index f0a1ecb..9a914a9 100644 --- a/cmd/kustomizer/diff.go +++ b/cmd/kustomizer/diff.go @@ -81,10 +81,7 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("status poller init failed: %w", err) } - resMgr := manager.NewResourceManager(kubeClient, statusPoller, manager.Owner{ - Field: PROJECT, - Group: PROJECT + ".dev", - }) + resMgr := manager.NewResourceManager(kubeClient, statusPoller, inventoryOwner) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() @@ -114,7 +111,7 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { } if diffArgs.inventoryName != "" { - staleObjects, err := inventoryMgr.GetStaleObjects(ctx, resMgr.KubeClient(), newInventory, diffArgs.inventoryName, diffArgs.inventoryNamespace) + staleObjects, err := resMgr.GetInventoryStaleObjects(ctx, newInventory) if err != nil { return fmt.Errorf("inventory query failed, error: %w", err) } diff --git a/cmd/kustomizer/main.go b/cmd/kustomizer/main.go index 6bb3883..1558fc9 100644 --- a/cmd/kustomizer/main.go +++ b/cmd/kustomizer/main.go @@ -17,14 +17,13 @@ limitations under the License. package main import ( + "github.com/stefanprodan/kustomizer/pkg/manager" "os" - "path/filepath" + "path" "time" "github.com/spf13/cobra" _ "k8s.io/client-go/plugin/pkg/client/auth" - - "github.com/stefanprodan/kustomizer/pkg/inventory" ) var VERSION = "1.0.0-dev.0" @@ -46,9 +45,12 @@ type rootFlags struct { } var ( - rootArgs = rootFlags{} - logger = stderrLogger{stderr: os.Stderr} - inventoryMgr *inventory.InventoryManager + rootArgs = rootFlags{} + logger = stderrLogger{stderr: os.Stderr} + inventoryOwner = manager.Owner{ + Field: "kustomizer", + Group: "inventory.kustomizer.dev", + } ) func init() { @@ -65,12 +67,6 @@ func init() { func main() { configureKubeconfig() - if im, err := inventory.NewInventoryManager(PROJECT, PROJECT+".dev"); err != nil { - panic(err) - } else { - inventoryMgr = im - } - if err := rootCmd.Execute(); err != nil { logger.Println(`✗`, err) os.Exit(1) @@ -84,7 +80,7 @@ func configureKubeconfig() { rootArgs.kubeconfig = os.Getenv("KUBECONFIG") default: if home := homeDir(); len(home) > 0 { - rootArgs.kubeconfig = filepath.Join(home, ".kube", "config") + rootArgs.kubeconfig = path.Join(home, ".kube", "config") } } } diff --git a/pkg/inventory/doc.go b/pkg/inventory/doc.go index c83ee0a..fc01cfd 100644 --- a/pkg/inventory/doc.go +++ b/pkg/inventory/doc.go @@ -16,9 +16,4 @@ limitations under the License. */ // Package inventory contains utilities for keeping a record of Kubernetes objects applied on a cluster. -// -// The InventoryManager performs the following actions: -// - records the Kubernetes objects metadata in a compacted format -// - stores the inventory in a Kubernetes ConfigMap -// - determines which objects are subject to garbage collection package inventory diff --git a/pkg/inventory/manager.go b/pkg/inventory/manager.go deleted file mode 100644 index 5d34fd5..0000000 --- a/pkg/inventory/manager.go +++ /dev/null @@ -1,146 +0,0 @@ -/* -Copyright 2021 Stefan Prodan -Copyright 2021 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package inventory - -import ( - "context" - "encoding/json" - "fmt" - "time" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// InventoryManager records the Kubernetes objects that are applied on the cluster. -type InventoryManager struct { - fieldOwner string - group string -} - -// NewInventoryManager returns an InventoryManager. -func NewInventoryManager(fieldOwner, group string) (*InventoryManager, error) { - if fieldOwner == "" { - return nil, fmt.Errorf("fieldOwner is required") - } - if group == "" { - return nil, fmt.Errorf("group is required") - } - - return &InventoryManager{ - fieldOwner: fieldOwner, - group: group, - }, nil -} - -// Store applies the Inventory object on the server. -func (im *InventoryManager) Store(ctx context.Context, kubeClient client.Client, inv *Inventory, name, namespace string) error { - data, err := json.Marshal(inv.Entries) - if err != nil { - return err - } - - cm := im.newConfigMap(name, namespace) - cm.Annotations = map[string]string{ - im.group + "/last-applied-time": time.Now().UTC().Format(time.RFC3339), - } - cm.Data = map[string]string{ - "inventory": string(data), - } - - opts := []client.PatchOption{ - client.ForceOwnership, - client.FieldOwner(im.fieldOwner), - } - return kubeClient.Patch(ctx, cm, client.Apply, opts...) -} - -// Retrieve fetches the Inventory object from the server. -func (im *InventoryManager) Retrieve(ctx context.Context, kubeClient client.Client, name, namespace string) (*Inventory, error) { - cm := im.newConfigMap(name, namespace) - - cmKey := client.ObjectKeyFromObject(cm) - err := kubeClient.Get(ctx, cmKey, cm) - if err != nil { - return nil, err - } - - if _, ok := cm.Data["inventory"]; !ok { - return nil, fmt.Errorf("inventory data not found in ConfigMap/%s", cmKey) - } - - var entries []Entry - err = json.Unmarshal([]byte(cm.Data["inventory"]), &entries) - if err != nil { - return nil, err - } - - return &Inventory{Entries: entries}, nil -} - -// GetStaleObjects returns the list of objects subject to pruning. -func (im *InventoryManager) GetStaleObjects(ctx context.Context, kubeClient client.Client, inv *Inventory, name, namespace string) ([]*unstructured.Unstructured, error) { - objects := make([]*unstructured.Unstructured, 0) - exInv, err := im.Retrieve(ctx, kubeClient, name, namespace) - if err != nil { - if apierrors.IsNotFound(err) { - return objects, nil - } - return nil, err - } - - objects, err = exInv.Diff(inv) - if err != nil { - return nil, err - } - - return objects, nil -} - -// Remove deletes the Inventory object from the server. -func (im *InventoryManager) Remove(ctx context.Context, kubeClient client.Client, name, namespace string) error { - cm := im.newConfigMap(name, namespace) - - cmKey := client.ObjectKeyFromObject(cm) - err := kubeClient.Delete(ctx, cm) - if err != nil && !apierrors.IsNotFound(err) { - return fmt.Errorf("failed to delete ConfigMap/%s, error: %w", cmKey, err) - } - return nil -} - -func (im *InventoryManager) newConfigMap(name, namespace string) *corev1.ConfigMap { - return &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: map[string]string{ - "app.kubernetes.io/name": name, - "app.kubernetes.io/component": "inventory", - "app.kubernetes.io/created-by": im.fieldOwner, - }, - }, - } -} diff --git a/pkg/manager/doc.go b/pkg/manager/doc.go index 8fdbac3..2b580f8 100644 --- a/pkg/manager/doc.go +++ b/pkg/manager/doc.go @@ -25,4 +25,6 @@ limitations under the License. // - waits for the objects to be fully reconciled by looking up their readiness status // - deletes objects that are subject to garbage collection // - waits for the deleted objects to be terminated +// - maintains an inventory of objects applied on the cluster +// - performs garbage collection of stale objects based on the inventory entries package manager diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index a5b350a..9c50dd3 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -32,7 +32,7 @@ type ResourceManager struct { owner Owner } -// NewResourceManager creates a ResourceManager for the given Kubernetes client config and context. +// NewResourceManager creates a ResourceManager for the given Kubernetes client. func NewResourceManager(client client.Client, poller *polling.StatusPoller, owner Owner) *ResourceManager { return &ResourceManager{ client: client, @@ -41,15 +41,11 @@ func NewResourceManager(client client.Client, poller *polling.StatusPoller, owne } } -// KubeClient returns the underlying controller-runtime client. -func (m *ResourceManager) KubeClient() client.Client { +// Client returns the underlying controller-runtime client. +func (m *ResourceManager) Client() client.Client { return m.client } -func (m *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action) *ChangeSetEntry { - return &ChangeSetEntry{Subject: objectutil.FmtUnstructured(object), Action: string(action)} -} - // SetOwnerLabels adds the ownership labels to the given objects. // The ownership labels are in the format: // /name: @@ -62,3 +58,7 @@ func (m *ResourceManager) SetOwnerLabels(objects []*unstructured.Unstructured, n }) } } + +func (m *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action) *ChangeSetEntry { + return &ChangeSetEntry{Subject: objectutil.FmtUnstructured(object), Action: string(action)} +} diff --git a/pkg/manager/manager_inventory.go b/pkg/manager/manager_inventory.go new file mode 100644 index 0000000..c700e52 --- /dev/null +++ b/pkg/manager/manager_inventory.go @@ -0,0 +1,128 @@ +/* +Copyright 2021 Stefan Prodan +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package manager + +import ( + "context" + "encoding/json" + "fmt" + "github.com/stefanprodan/kustomizer/pkg/inventory" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + "time" +) + +const inventoryKindName = "inventory" + +// ApplyInventory creates or updates the ConfigMap object for the given inventory. +func (m *ResourceManager) ApplyInventory(ctx context.Context, i *inventory.Inventory) error { + data, err := json.Marshal(i.Entries) + if err != nil { + return err + } + + cm := m.newConfigMap(i.Name, i.Namespace) + cm.Annotations = map[string]string{ + m.owner.Group + "/last-applied-time": time.Now().UTC().Format(time.RFC3339), + } + cm.Data = map[string]string{ + inventoryKindName: string(data), + } + + opts := []client.PatchOption{ + client.ForceOwnership, + client.FieldOwner(m.owner.Field), + } + return m.client.Patch(ctx, cm, client.Apply, opts...) +} + +// GetInventory retrieves the entries from the ConfigMap for the given inventory name and namespace. +func (m *ResourceManager) GetInventory(ctx context.Context, i *inventory.Inventory) error { + cm := m.newConfigMap(i.Name, i.Namespace) + + cmKey := client.ObjectKeyFromObject(cm) + err := m.client.Get(ctx, cmKey, cm) + if err != nil { + return err + } + + if _, ok := cm.Data[inventoryKindName]; !ok { + return fmt.Errorf("inventory data not found in ConfigMap/%s", cmKey) + } + + var entries []inventory.Entry + err = json.Unmarshal([]byte(cm.Data[inventoryKindName]), &entries) + if err != nil { + return err + } + + i.Entries = entries + return nil +} + +// DeleteInventory removes the ConfigMap for the given inventory name and namespace. +func (m *ResourceManager) DeleteInventory(ctx context.Context, i *inventory.Inventory) error { + cm := m.newConfigMap(i.Name, i.Namespace) + + cmKey := client.ObjectKeyFromObject(cm) + err := m.client.Delete(ctx, cm) + if err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to delete ConfigMap/%s, error: %w", cmKey, err) + } + return nil +} + +// GetInventoryStaleObjects returns the list of objects subject to pruning. +func (m *ResourceManager) GetInventoryStaleObjects(ctx context.Context, i *inventory.Inventory) ([]*unstructured.Unstructured, error) { + objects := make([]*unstructured.Unstructured, 0) + existingInventory := inventory.NewInventory(i.Name, i.Namespace) + if err := m.GetInventory(ctx, existingInventory); err != nil { + if apierrors.IsNotFound(err) { + return objects, nil + } + return nil, err + } + + objects, err := existingInventory.Diff(i) + if err != nil { + return nil, err + } + + return objects, nil +} + +func (m *ResourceManager) newConfigMap(name, namespace string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{ + "app.kubernetes.io/name": name, + "app.kubernetes.io/component": inventoryKindName, + "app.kubernetes.io/created-by": m.owner.Field, + }, + }, + } +}