Skip to content

Commit

Permalink
Dual-delegating provider allows ConfigMap and ResourceGroup inventory
Browse files Browse the repository at this point in the history
  • Loading branch information
seans3 committed Nov 4, 2020
1 parent c04ebc7 commit 40516b0
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 60 deletions.
130 changes: 130 additions & 0 deletions pkg/live/dual-delegating-provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2020 Google LLC.
// SPDX-License-Identifier: Apache-2.0

package live

import (
"fmt"
"io"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/klog"
"k8s.io/kubectl/pkg/cmd/util"
"sigs.k8s.io/cli-utils/pkg/inventory"
"sigs.k8s.io/cli-utils/pkg/manifestreader"
"sigs.k8s.io/cli-utils/pkg/provider"
)

// DualDelegatingProvider encapsulates another Provider to which it
// delegates most functions, enabling a Provider which will return
// values based on the inventory object found from ManifestReader()
// call.
type DualDelegatingProvider struct {
// ResourceGroupProvider is the delegate.
rgProvider provider.Provider
// Default inventory function is for ConfigMap.
wrapInv inventory.InventoryFactoryFunc
// Boolean on whether we've calculated the inventory object type.
calcInventory bool
}

// NewDualDelagatingProvider returns a pointer to the DualDelegatingProvider,
// setting default values.
func NewDualDelegatingProvider(f util.Factory) *DualDelegatingProvider {
return &DualDelegatingProvider{
rgProvider: NewResourceGroupProvider(f),
wrapInv: inventory.WrapInventoryObj,
calcInventory: false,
}
}

// Factory returns the delegate factory.
func (cp *DualDelegatingProvider) Factory() util.Factory {
return cp.rgProvider.Factory()
}

// InventoryClient returns an InventoryClient that is created from the
// stored/calculated InventoryFactoryFunction. This must be called
// after ManifestReader().
func (cp *DualDelegatingProvider) InventoryClient() (inventory.InventoryClient, error) {
if !cp.calcInventory {
return nil, fmt.Errorf("must be called after ManifestReader()")
}
return inventory.NewInventoryClient(cp.Factory(), cp.wrapInv)
}

// ToRESTMapper returns the value from the delegate provider; or an error.
func (cp *DualDelegatingProvider) ToRESTMapper() (meta.RESTMapper, error) {
return cp.Factory().ToRESTMapper()
}

// ManifestReader retrieves the ManifestReader from the delegate ResourceGroup
// Provider, then calls Read() for this ManifestReader to retrieve the objects
// and to calculate the type of Inventory object is present. Returns a
// CachedManifestReader with the read objects, or an error. Can return a
// NoInventoryError or MultipleInventoryError.
func (cp *DualDelegatingProvider) ManifestReader(reader io.Reader, args []string) (manifestreader.ManifestReader, error) {
r, err := cp.rgProvider.ManifestReader(reader, args)
if err != nil {
return nil, err
}
objs, err := r.Read()
if err != nil {
return nil, err
}
klog.V(4).Infof("ManifestReader read %d objects", len(objs))
rgInv := findResourceGroupInv(objs)
// A ResourceGroup inventory object means we need an InventoryFactoryFunc
// which works for ResourceGroup (instead of ConfigMap, which is default).
if rgInv != nil {
cp.wrapInv = WrapInventoryObj
}
cmInv := findConfigMapInv(objs)
if rgInv == nil && cmInv == nil {
return nil, inventory.NoInventoryObjError{}
}
if rgInv != nil && cmInv != nil {
return nil, inventory.MultipleInventoryObjError{
InventoryObjectTemplates: []*unstructured.Unstructured{rgInv, cmInv},
}
}
cp.calcInventory = true
return &CachedManifestReader{objs: objs}, nil
}

// CachedManifestReader implements ManifestReader, storing objects to return.
type CachedManifestReader struct {
objs []*unstructured.Unstructured
}

// Read simply returns the stored objects.
func (r *CachedManifestReader) Read() ([]*unstructured.Unstructured, error) {
return r.objs, nil
}

// findResourceGroupInv returns the pointer to the ResourceGroup inventory object,
// or nil if it does not exist.
func findResourceGroupInv(objs []*unstructured.Unstructured) *unstructured.Unstructured {
for _, obj := range objs {
if inventory.IsInventoryObject(obj) {
if obj.GetKind() == "ResourceGroup" {
return obj
}
}
}
return nil
}

// findConfigMapInv returns the pointer to the ConfigMap inventory object,
// or nil if it does not exist.
func findConfigMapInv(objs []*unstructured.Unstructured) *unstructured.Unstructured {
for _, obj := range objs {
if inventory.IsInventoryObject(obj) {
if obj.GetKind() == "ConfigMap" {
return obj
}
}
}
return nil
}
155 changes: 155 additions & 0 deletions pkg/live/dual-delegating-provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright 2020 Google LLC.
// SPDX-License-Identifier: Apache-2.0

package live

import (
"io/ioutil"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
"sigs.k8s.io/cli-utils/pkg/inventory"
)

var configMapInv = `
apiVersion: v1
kind: ConfigMap
metadata:
namespace: test-ns
name: inventory-111111
labels:
cli-utils.sigs.k8s.io/inventory-id: XXXX-YYYY-ZZZZ
`

func TestDualDelegatingProvider_Read(t *testing.T) {
testCases := map[string]struct {
manifests map[string]string
numObjs int
invKind string
isError bool
}{
"Basic ResourceGroup inventory object created": {
manifests: map[string]string{
"Kptfile": kptFile,
"pod-a.yaml": podA,
},
numObjs: 2,
invKind: "ResourceGroup",
isError: false,
},
"Only ResourceGroup inventory object created": {
manifests: map[string]string{
"Kptfile": kptFile,
},
numObjs: 1,
invKind: "ResourceGroup",
isError: false,
},
"ResourceGroup inventory object with multiple objects": {
manifests: map[string]string{
"pod-a.yaml": podA,
"Kptfile": kptFile,
"deployment-a.yaml": deploymentA,
},
numObjs: 3,
invKind: "ResourceGroup",
isError: false,
},
"Basic ConfigMap inventory object created": {
manifests: map[string]string{
"inventory-template.yaml": configMapInv,
"deployment-a.yaml": deploymentA,
},
numObjs: 2,
invKind: "ConfigMap",
isError: false,
},
"Only ConfigMap inventory object created": {
manifests: map[string]string{
"inventory-template.yaml": configMapInv,
},
numObjs: 1,
invKind: "ConfigMap",
isError: false,
},
"ConfigMap inventory object with multiple objects": {
manifests: map[string]string{
"deployment-a.yaml": deploymentA,
"inventory-template.yaml": configMapInv,
"pod-a.yaml": podA,
},
numObjs: 3,
invKind: "ConfigMap",
isError: false,
},
"No inventory manifests is an error": {
manifests: map[string]string{
"pod-a.yaml": podA,
"deployment-a.yaml": deploymentA,
},
numObjs: 2,
isError: true,
},
"Multiple manifests is an error": {
manifests: map[string]string{
"inventory-template.yaml": configMapInv,
"Kptfile": kptFile,
"pod-a.yaml": podA,
},
numObjs: 3,
isError: true,
},
}

for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
// Create the fake factory
tf := cmdtesting.NewTestFactory().WithNamespace("test-ns")
defer tf.Cleanup()
// Set up the yaml manifests (including Kptfile) in temp dir.
dir, err := ioutil.TempDir("", "provider-test")
assert.NoError(t, err)
for filename, content := range tc.manifests {
p := filepath.Join(dir, filename)
err := ioutil.WriteFile(p, []byte(content), 0600)
assert.NoError(t, err)
}
// Create the DualDelegatingProvider
provider := NewDualDelegatingProvider(tf)
assert.Equal(t, tf, provider.Factory())
// Calling InventoryClient before ManifestReader is always an error.
_, err = provider.InventoryClient()
if err == nil {
t.Errorf("expecting error on InventoryClient, but received none.")
}
// Read objects using provider ManifestReader.
mr, err := provider.ManifestReader(nil, []string{dir})
if tc.isError {
if err == nil {
t.Errorf("expected error on ManifestReader, but received none.")
}
return
}
objs, err := mr.Read()
assert.NoError(t, err)
if tc.numObjs != len(objs) {
t.Errorf("expected to read (%d) objs, got (%d)", tc.numObjs, len(objs))
}
// Retrieve single inventory object and validate the kind.
inv := inventory.FindInventoryObj(objs)
if inv == nil {
t.Errorf("inventory object not found")
}
if tc.invKind != inv.GetKind() {
t.Errorf("expected inventory kind (%s), got (%s)", tc.invKind, inv.GetKind())
}
// Calling InventoryClient after ManifestReader is valid.
_, err = provider.InventoryClient()
if err != nil {
t.Errorf("unexpected error calling InventoryClient: %s", err)
}
})
}
}
31 changes: 18 additions & 13 deletions pkg/live/rgpath.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 The Kubernetes Authors.
// Copyright 2020 Google LLC.
// SPDX-License-Identifier: Apache-2.0

package live
Expand All @@ -23,27 +23,32 @@ type ResourceGroupPathManifestReader struct {

// Read reads the manifests and returns them as Info objects.
// Generates and adds a ResourceGroup inventory object from
// Kptfile data.
// Kptfile data. If unable to generate the ResourceGroup inventory
// object from the Kptfile, it is NOT an error.
func (p *ResourceGroupPathManifestReader) Read() ([]*unstructured.Unstructured, error) {
// Using the default path reader to generate the objects.
infos, err := p.pathReader.Read()
objs, err := p.pathReader.Read()
if err != nil {
return []*unstructured.Unstructured{}, err
}
// Read the Kptfile in the top directory to get the inventory
// parameters to create the ResourceGroup inventory object.
kf, err := kptfileutil.ReadFile(p.pathReader.Path)
if err != nil {
return []*unstructured.Unstructured{}, err
}
inv := kf.Inventory
klog.V(4).Infof("generating ResourceGroup inventory object %s/%s/%s", inv.Namespace, inv.Name, inv.InventoryID)
invInfo, err := generateInventoryObj(inv)
if err != nil {
return []*unstructured.Unstructured{}, err
if err == nil {
inv := kf.Inventory
klog.V(4).Infof("from Kptfile generating ResourceGroup inventory object %s/%s/%s",
inv.Namespace, inv.Name, inv.InventoryID)
invObj, err := generateInventoryObj(inv)
if err == nil {
objs = append(objs, invObj)
} else {
klog.V(4).Infof("unable to generate ResourceGroup inventory: %s", err)
}
} else {
klog.V(4).Infof("unable to parse Kpfile for ResourceGroup inventory: %s", err)
}
infos = append(infos, invInfo)
return infos, nil
klog.V(4).Infof("path Read() generated %d resources", len(objs))
return objs, nil
}

// generateInventoryObj returns the ResourceGroupInventory object using the
Expand Down
Loading

0 comments on commit 40516b0

Please sign in to comment.