Skip to content

Commit c29e1ab

Browse files
committed
Enable custom inventory storage logic
This commit introduces 2 new Storage interface methods to enable clients to implement their own logic for applying inventory objects to the live cluster.
1 parent b00a8f8 commit c29e1ab

File tree

5 files changed

+207
-143
lines changed

5 files changed

+207
-143
lines changed

pkg/inventory/inventory-client.go

Lines changed: 48 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ func (cic *ClusterClient) Merge(localInv Info, objs object.ObjMetadataSet, dryRu
103103
if err != nil {
104104
return pruneIds, err
105105
}
106+
107+
// Inventory does not exist on the cluster.
106108
if clusterInv == nil {
107109
// Wrap inventory object and store the inventory in it.
108110
var status []actuation.ObjectStatus
@@ -113,60 +115,41 @@ func (cic *ClusterClient) Merge(localInv Info, objs object.ObjMetadataSet, dryRu
113115
if err := inv.Store(objs, status); err != nil {
114116
return nil, err
115117
}
116-
invInfo, err := inv.GetObject()
117-
if err != nil {
118-
return nil, err
119-
}
120118
klog.V(4).Infof("creating initial inventory object with %d objects", len(objs))
121-
createdObj, err := cic.createInventoryObj(invInfo, dryRun)
122-
if err != nil {
123-
return nil, err
124-
}
125-
// Status update requires the latest ResourceVersion
126-
invInfo.SetResourceVersion(createdObj.GetResourceVersion())
127-
if err := cic.updateStatus(invInfo, dryRun); err != nil {
128-
return nil, err
129-
}
130-
} else {
131-
// Update existing cluster inventory with merged union of objects
132-
clusterObjs, err := cic.GetClusterObjs(localInv)
133-
if err != nil {
134-
return pruneIds, err
135-
}
136-
pruneIds = clusterObjs.Diff(objs)
137-
unionObjs := clusterObjs.Union(objs)
138-
var status []actuation.ObjectStatus
139-
if cic.statusPolicy == StatusPolicyAll {
140-
status = getObjStatus(pruneIds, unionObjs)
141-
}
142-
klog.V(4).Infof("num objects to prune: %d", len(pruneIds))
143-
klog.V(4).Infof("num merged objects to store in inventory: %d", len(unionObjs))
144-
wrappedInv := cic.InventoryFactoryFunc(clusterInv)
145-
if err = wrappedInv.Store(unionObjs, status); err != nil {
146-
return pruneIds, err
147-
}
148-
clusterInv, err = wrappedInv.GetObject()
149-
if err != nil {
150-
return pruneIds, err
151-
}
119+
152120
if dryRun.ClientOrServerDryRun() {
153-
return pruneIds, nil
154-
}
155-
if !objs.Equal(clusterObjs) {
156-
klog.V(4).Infof("update cluster inventory: %s/%s", clusterInv.GetNamespace(), clusterInv.GetName())
157-
appliedObj, err := cic.applyInventoryObj(clusterInv, dryRun)
158-
if err != nil {
159-
return pruneIds, err
160-
}
161-
// Status update requires the latest ResourceVersion
162-
clusterInv.SetResourceVersion(appliedObj.GetResourceVersion())
163-
}
164-
if err := cic.updateStatus(clusterInv, dryRun); err != nil {
165-
return pruneIds, err
121+
klog.V(4).Infof("dry-run create inventory object: not created")
122+
return nil, nil
166123
}
124+
125+
err = inv.Apply(cic.dc, cic.mapper, cic.statusPolicy)
126+
return nil, err
127+
}
128+
129+
// Update existing cluster inventory with merged union of objects
130+
clusterObjs, err := cic.GetClusterObjs(localInv)
131+
if err != nil {
132+
return pruneIds, err
133+
}
134+
pruneIds = clusterObjs.Diff(objs)
135+
unionObjs := clusterObjs.Union(objs)
136+
var status []actuation.ObjectStatus
137+
if cic.statusPolicy == StatusPolicyAll {
138+
status = getObjStatus(pruneIds, unionObjs)
139+
}
140+
klog.V(4).Infof("num objects to prune: %d", len(pruneIds))
141+
klog.V(4).Infof("num merged objects to store in inventory: %d", len(unionObjs))
142+
wrappedInv := cic.InventoryFactoryFunc(clusterInv)
143+
if err = wrappedInv.Store(unionObjs, status); err != nil {
144+
return pruneIds, err
167145
}
168146

169-
return pruneIds, nil
147+
if dryRun.ClientOrServerDryRun() {
148+
klog.V(4).Infof("dry-run create inventory object: not created")
149+
return pruneIds, nil
150+
}
151+
err = wrappedInv.Apply(cic.dc, cic.mapper, cic.statusPolicy)
152+
return pruneIds, err
170153
}
171154

172155
// Replace stores the passed objects in the cluster inventory object, or
@@ -178,49 +161,49 @@ func (cic *ClusterClient) Replace(localInv Info, objs object.ObjMetadataSet, sta
178161
klog.V(4).Infoln("dry-run replace inventory object: not applied")
179162
return nil
180163
}
181-
clusterObjs, err := cic.GetClusterObjs(localInv)
182-
if err != nil {
183-
return fmt.Errorf("failed to read inventory objects from cluster: %w", err)
184-
}
185164
clusterInv, err := cic.GetClusterInventoryInfo(localInv)
186165
if err != nil {
187166
return fmt.Errorf("failed to read inventory from cluster: %w", err)
188167
}
189-
clusterInv, err = cic.replaceInventory(clusterInv, objs, status)
168+
169+
clusterObjs, err := cic.GetClusterObjs(localInv)
170+
if err != nil {
171+
return fmt.Errorf("failed to read inventory objects from cluster: %w", err)
172+
}
173+
174+
clusterInv, wrappedInv, err := cic.replaceInventory(clusterInv, objs, status)
190175
if err != nil {
191176
return err
192177
}
178+
193179
if !objs.Equal(clusterObjs) {
194180
klog.V(4).Infof("replace cluster inventory: %s/%s", clusterInv.GetNamespace(), clusterInv.GetName())
195181
klog.V(4).Infof("replace cluster inventory %d objects", len(objs))
196-
appliedObj, err := cic.applyInventoryObj(clusterInv, dryRun)
197-
if err != nil {
182+
183+
if err := wrappedInv.ApplyWithPrune(cic.dc, cic.mapper, cic.statusPolicy, objs); err != nil {
198184
return fmt.Errorf("failed to write updated inventory to cluster: %w", err)
199185
}
200-
// Status update requires the latest ResourceVersion
201-
clusterInv.SetResourceVersion(appliedObj.GetResourceVersion())
202-
}
203-
if err := cic.updateStatus(clusterInv, dryRun); err != nil {
204-
return err
205186
}
187+
206188
return nil
207189
}
208190

209191
// replaceInventory stores the passed objects into the passed inventory object.
210192
func (cic *ClusterClient) replaceInventory(inv *unstructured.Unstructured, objs object.ObjMetadataSet,
211-
status []actuation.ObjectStatus) (*unstructured.Unstructured, error) {
193+
status []actuation.ObjectStatus) (*unstructured.Unstructured, Storage, error) {
212194
if cic.statusPolicy == StatusPolicyNone {
213195
status = nil
214196
}
215197
wrappedInv := cic.InventoryFactoryFunc(inv)
216198
if err := wrappedInv.Store(objs, status); err != nil {
217-
return nil, err
199+
return nil, nil, err
218200
}
219201
clusterInv, err := wrappedInv.GetObject()
220202
if err != nil {
221-
return nil, err
203+
return nil, nil, err
222204
}
223-
return clusterInv, nil
205+
206+
return clusterInv, wrappedInv, nil
224207
}
225208

226209
// DeleteInventoryObj deletes the inventory object from the cluster.
@@ -364,26 +347,6 @@ func (cic *ClusterClient) GetClusterInventoryObjs(inv Info) (object.Unstructured
364347
return clusterInvObjects, err
365348
}
366349

367-
// applyInventoryObj applies the passed inventory object to the APIServer.
368-
func (cic *ClusterClient) applyInventoryObj(obj *unstructured.Unstructured, dryRun common.DryRunStrategy) (*unstructured.Unstructured, error) {
369-
if dryRun.ClientOrServerDryRun() {
370-
klog.V(4).Infof("dry-run apply inventory object: not applied")
371-
return obj.DeepCopy(), nil
372-
}
373-
if obj == nil {
374-
return nil, fmt.Errorf("attempting apply a nil inventory object")
375-
}
376-
377-
mapping, err := cic.getMapping(obj)
378-
if err != nil {
379-
return nil, err
380-
}
381-
382-
klog.V(4).Infof("replacing inventory object: %s/%s", obj.GetNamespace(), obj.GetName())
383-
return cic.dc.Resource(mapping.Resource).Namespace(obj.GetNamespace()).
384-
Update(context.TODO(), obj, metav1.UpdateOptions{})
385-
}
386-
387350
// createInventoryObj creates the passed inventory object on the APIServer.
388351
func (cic *ClusterClient) createInventoryObj(obj *unstructured.Unstructured, dryRun common.DryRunStrategy) (*unstructured.Unstructured, error) {
389352
if dryRun.ClientOrServerDryRun() {
@@ -463,62 +426,6 @@ func (cic *ClusterClient) getMapping(obj *unstructured.Unstructured) (*meta.REST
463426
return cic.mapper.RESTMapping(obj.GroupVersionKind().GroupKind(), obj.GroupVersionKind().Version)
464427
}
465428

466-
func (cic *ClusterClient) updateStatus(obj *unstructured.Unstructured, dryRun common.DryRunStrategy) error {
467-
if cic.statusPolicy != StatusPolicyAll {
468-
klog.V(4).Infof("inventory status update skipped (StatusPolicy: %s)", cic.statusPolicy)
469-
return nil
470-
}
471-
if dryRun.ClientOrServerDryRun() {
472-
klog.V(4).Infof("dry-run update inventory status: not updated")
473-
return nil
474-
}
475-
status, found, _ := unstructured.NestedMap(obj.UnstructuredContent(), "status")
476-
if !found {
477-
return nil
478-
}
479-
mapping, err := cic.mapper.RESTMapping(obj.GroupVersionKind().GroupKind())
480-
if err != nil {
481-
return nil
482-
}
483-
hasStatus, err := cic.hasSubResource(obj.GetAPIVersion(), mapping.Resource.Resource, "status")
484-
if err != nil {
485-
return err
486-
}
487-
if !hasStatus {
488-
klog.V(4).Infof("skip updating inventory status")
489-
return nil
490-
}
491-
492-
klog.V(4).Infof("update inventory status")
493-
resource := cic.dc.Resource(mapping.Resource).Namespace(obj.GetNamespace())
494-
meta := metav1.TypeMeta{
495-
Kind: obj.GetKind(),
496-
APIVersion: obj.GetAPIVersion(),
497-
}
498-
if err = unstructured.SetNestedMap(obj.Object, status, "status"); err != nil {
499-
return err
500-
}
501-
if _, err = resource.UpdateStatus(context.TODO(), obj, metav1.UpdateOptions{TypeMeta: meta}); err != nil {
502-
return fmt.Errorf("failed to write updated inventory status to cluster: %w", err)
503-
}
504-
return nil
505-
}
506-
507-
// hasSubResource checks if a resource has the given subresource using the discovery client.
508-
func (cic *ClusterClient) hasSubResource(groupVersion, resource, subresource string) (bool, error) {
509-
resources, err := cic.discoveryClient.ServerResourcesForGroupVersion(groupVersion)
510-
if err != nil {
511-
return false, err
512-
}
513-
514-
for _, r := range resources.APIResources {
515-
if r.Name == fmt.Sprintf("%s/%s", resource, subresource) {
516-
return true, nil
517-
}
518-
}
519-
return false, nil
520-
}
521-
522429
// getObjStatus returns the list of object status
523430
// at the beginning of an apply process.
524431
func getObjStatus(pruneIds, unionIds []object.ObjMetadata) []actuation.ObjectStatus {

pkg/inventory/inventory-client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ func TestReplace(t *testing.T) {
393393
t.Fatalf("unexpected error storing inventory objects: %s", err)
394394
}
395395
// Call replaceInventory with the new set of "localObjs"
396-
inv, err = invClient.replaceInventory(inv, tc.localObjs, tc.objStatus)
396+
inv, _, err = invClient.replaceInventory(inv, tc.localObjs, tc.objStatus)
397397
if err != nil {
398398
t.Fatalf("unexpected error received: %s", err)
399399
}

pkg/inventory/inventorycm.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@
99
package inventory
1010

1111
import (
12+
"context"
1213
"encoding/json"
1314
"fmt"
1415

16+
apierrors "k8s.io/apimachinery/pkg/api/errors"
17+
"k8s.io/apimachinery/pkg/api/meta"
18+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1519
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
20+
"k8s.io/client-go/dynamic"
21+
"k8s.io/klog/v2"
1622
"sigs.k8s.io/cli-utils/pkg/apis/actuation"
1723
"sigs.k8s.io/cli-utils/pkg/common"
1824
"sigs.k8s.io/cli-utils/pkg/object"
@@ -119,6 +125,69 @@ func (icm *ConfigMap) GetObject() (*unstructured.Unstructured, error) {
119125
return invCopy, nil
120126
}
121127

128+
// Apply is an Storage interface function implemented to apply the inventory
129+
// object. StatusPolicy is not needed since ConfigMaps do not have a status subresource.
130+
func (icm *ConfigMap) Apply(dc dynamic.Interface, mapper meta.RESTMapper, _ StatusPolicy) error {
131+
invInfo, namespacedClient, err := icm.getNamespacedClient(dc, mapper)
132+
if err != nil {
133+
return err
134+
}
135+
136+
// Get cluster object, if exsists.
137+
clusterObj, err := namespacedClient.Get(context.TODO(), invInfo.GetName(), metav1.GetOptions{})
138+
if err != nil && !apierrors.IsNotFound(err) {
139+
return err
140+
}
141+
142+
// Create cluster inventory object, if it does not exist on cluster.
143+
if clusterObj == nil {
144+
klog.V(4).Infof("creating inventory object: %s/%s", invInfo.GetNamespace(), invInfo.GetName())
145+
_, err = namespacedClient.Create(context.TODO(), invInfo, metav1.CreateOptions{})
146+
return err
147+
}
148+
149+
// Update the cluster inventory object instead.
150+
klog.V(4).Infof("updating inventory object: %s/%s", invInfo.GetNamespace(), invInfo.GetName())
151+
_, err = namespacedClient.Update(context.TODO(), invInfo, metav1.UpdateOptions{})
152+
return err
153+
}
154+
155+
// ApplyWithPrune is a Storage interface function implemented to apply the inventory object with a list of objects
156+
// to be pruned. StatusPolicy is not needed since ConfigMaps do not have a status subresource.
157+
func (icm *ConfigMap) ApplyWithPrune(dc dynamic.Interface, mapper meta.RESTMapper, _ StatusPolicy, _ object.ObjMetadataSet) error {
158+
invInfo, namespacedClient, err := icm.getNamespacedClient(dc, mapper)
159+
if err != nil {
160+
return err
161+
}
162+
163+
// Update the cluster inventory object.
164+
klog.V(4).Infof("updating inventory object: %s/%s", invInfo.GetNamespace(), invInfo.GetName())
165+
_, err = namespacedClient.Update(context.TODO(), invInfo, metav1.UpdateOptions{})
166+
return err
167+
}
168+
169+
// getNamespacedClient is a helper function for Apply and ApplyWithPrune that creates a namespaced client for interacting with the live
170+
// cluster, as well as returning the ConfigMap object as a wrapped resource.Info object.
171+
func (icm *ConfigMap) getNamespacedClient(dc dynamic.Interface, mapper meta.RESTMapper) (*unstructured.Unstructured, dynamic.ResourceInterface, error) {
172+
invInfo, err := icm.GetObject()
173+
if err != nil {
174+
return nil, nil, err
175+
}
176+
if invInfo == nil {
177+
return nil, nil, fmt.Errorf("attempting to create a nil inventory object")
178+
}
179+
180+
mapping, err := mapper.RESTMapping(invInfo.GroupVersionKind().GroupKind(), invInfo.GroupVersionKind().Version)
181+
if err != nil {
182+
return nil, nil, err
183+
}
184+
185+
// Create client to interact with cluster.
186+
namespacedClient := dc.Resource(mapping.Resource).Namespace(invInfo.GetNamespace())
187+
188+
return invInfo, namespacedClient, nil
189+
}
190+
122191
func buildObjMap(objMetas object.ObjMetadataSet, objStatus []actuation.ObjectStatus) map[string]string {
123192
objMap := map[string]string{}
124193
objStatusMap := map[object.ObjMetadata]actuation.ObjectStatus{}

pkg/inventory/inventory.go renamed to pkg/inventory/storage.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import (
1515
"fmt"
1616
"strings"
1717

18+
"k8s.io/apimachinery/pkg/api/meta"
1819
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
20+
"k8s.io/client-go/dynamic"
1921
"k8s.io/klog/v2"
2022
"sigs.k8s.io/cli-utils/pkg/apis/actuation"
2123
"sigs.k8s.io/cli-utils/pkg/common"
@@ -31,10 +33,19 @@ const legacyInvName = "inventory"
3133
type Storage interface {
3234
// Load retrieves the set of object metadata from the inventory object
3335
Load() (object.ObjMetadataSet, error)
34-
// Store the set of object metadata in the inventory object
36+
// Store the set of object metadata in the inventory object. This will
37+
// replace the metadata, spec and status.
3538
Store(objs object.ObjMetadataSet, status []actuation.ObjectStatus) error
3639
// GetObject returns the object that stores the inventory
3740
GetObject() (*unstructured.Unstructured, error)
41+
// Apply applies the inventory object. This utility function is used
42+
// in InventoryClient.Merge and merges the metadata, spec and status.
43+
Apply(dynamic.Interface, meta.RESTMapper, StatusPolicy) error
44+
// ApplyWithPrune applies the inventory object with a set of pruneIDs of
45+
// objects to be pruned (object.ObjMetadataSet). This function is used in
46+
// InventoryClient.Replace. pruneIDs are required for enabling custom logic
47+
// handling of multiple ResourceGroup inventories.
48+
ApplyWithPrune(dynamic.Interface, meta.RESTMapper, StatusPolicy, object.ObjMetadataSet) error
3849
}
3950

4051
// StorageFactoryFunc creates the object which implements the Inventory

0 commit comments

Comments
 (0)