diff --git a/go.mod b/go.mod index b1642ae786..4a920bae50 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c gotest.tools v2.2.0+incompatible k8s.io/api v0.18.10 + k8s.io/apiextensions-apiserver v0.18.10 k8s.io/apimachinery v0.18.10 k8s.io/cli-runtime v0.18.10 k8s.io/client-go v0.18.10 @@ -23,5 +24,7 @@ require ( k8s.io/kubectl v0.18.10 sigs.k8s.io/cli-utils v0.22.5-0.20210127192708-27cfaa675296 sigs.k8s.io/kustomize/cmd/config v0.8.7-0.20201211170716-cc43a2d732d1 - sigs.k8s.io/kustomize/kyaml v0.10.6 + sigs.k8s.io/kustomize/kyaml v0.10.9 ) + +replace sigs.k8s.io/cli-utils v0.22.5-0.20210127192708-27cfaa675296 => ../../sigs.k8s.io/cli-utils diff --git a/go.sum b/go.sum index 12aafe69d8..b240542802 100644 --- a/go.sum +++ b/go.sum @@ -697,6 +697,10 @@ sigs.k8s.io/kustomize/kyaml v0.10.3 h1:ARSJUMN/c3k31DYxRfZ+vp/UepUQjg9zCwny7Oj90 sigs.k8s.io/kustomize/kyaml v0.10.3/go.mod h1:RA+iCHA2wPCOfv6uG6TfXXWhYsHpgErq/AljxWKuxtg= sigs.k8s.io/kustomize/kyaml v0.10.6 h1:xUJxc/k8JoWqHUahaB8DTqY0KwEPxTbTGStvW8TOcDc= sigs.k8s.io/kustomize/kyaml v0.10.6/go.mod h1:K9yg1k/HB/6xNOf5VH3LhTo1DK9/5ykSZO5uIv+Y/1k= +sigs.k8s.io/kustomize/kyaml v0.10.7 h1:r0r8UEL0bL7X56HKUmhJZ+TP+nvRNGrDHHSLO7izlcQ= +sigs.k8s.io/kustomize/kyaml v0.10.7/go.mod h1:K9yg1k/HB/6xNOf5VH3LhTo1DK9/5ykSZO5uIv+Y/1k= +sigs.k8s.io/kustomize/kyaml v0.10.9 h1:n3WNdvPPReRNDxW+XXd2JlyZ8EII721I21D1DBpBVBE= +sigs.k8s.io/kustomize/kyaml v0.10.9/go.mod h1:K9yg1k/HB/6xNOf5VH3LhTo1DK9/5ykSZO5uIv+Y/1k= sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= diff --git a/pkg/kptfile/kptfileutil/util.go b/pkg/kptfile/kptfileutil/util.go index d8be5d31ad..f9ef4906eb 100644 --- a/pkg/kptfile/kptfileutil/util.go +++ b/pkg/kptfile/kptfileutil/util.go @@ -23,6 +23,7 @@ import ( "github.com/GoogleContainerTools/kpt/pkg/kptfile" "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/kio/kioutil" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -48,6 +49,12 @@ func ReadFile(dir string) (kptfile.KptFile, error) { if err = d.Decode(&kpgfile); err != nil { return kptfile.KptFile{}, errors.Errorf("unable to parse %q: %v", kptfile.KptFileName, err) } + annotations := kpgfile.Annotations + if annotations == nil { + annotations = make(map[string]string) + } + annotations[kioutil.PathAnnotation] = dir + kpgfile.Annotations = annotations return kpgfile, nil } diff --git a/pkg/live/inventoryrg.go b/pkg/live/inventoryrg.go index 97361e7624..5e0aa32bc9 100644 --- a/pkg/live/inventoryrg.go +++ b/pkg/live/inventoryrg.go @@ -30,6 +30,8 @@ import ( "sigs.k8s.io/kustomize/kyaml/yaml" ) +var AllGroups []string + // ResourceGroupGVK is the group/version/kind of the custom // resource used to store inventory. var ResourceGroupGVK = schema.GroupVersionKind{ @@ -92,6 +94,19 @@ func (icm *InventoryResourceGroup) ID() string { return "" } +func (icm *InventoryResourceGroup) Match(id string) bool { + invID := icm.ID() + if invID == id { + return true + } + for _, g := range AllGroups { + if g == id { + return true + } + } + return false +} + // Load is an Inventory interface function returning the set of // object metadata from the wrapped ResourceGroup, or an error. func (icm *InventoryResourceGroup) Load() ([]object.ObjMetadata, error) { @@ -187,6 +202,116 @@ func (icm *InventoryResourceGroup) GetObject() (*unstructured.Unstructured, erro return invCopy, nil } +func (icm *InventoryResourceGroup) LoadSubgroups() ([]object.ObjMetadata, error) { + objs := []object.ObjMetadata{} + if icm.inv == nil { + return objs, fmt.Errorf("inventory info is nil") + } + klog.V(4).Infof("loading inventory...") + items, exists, err := unstructured.NestedSlice(icm.inv.Object, "spec", "subgroups") + if err != nil { + err := fmt.Errorf("error retrieving object metadata from inventory object") + return objs, err + } + if !exists { + klog.V(4).Infof("Inventory (spec.resources) does not exist") + return objs, nil + } + klog.V(4).Infof("loading %d inventory items", len(items)) + for _, itemUncast := range items { + item := itemUncast.(map[string]interface{}) + namespace, _, err := unstructured.NestedString(item, "namespace") + if err != nil { + return []object.ObjMetadata{}, err + } + name, _, err := unstructured.NestedString(item, "name") + if err != nil { + return []object.ObjMetadata{}, err + } + group, _, err := unstructured.NestedString(item, "group") + if err != nil { + return []object.ObjMetadata{}, err + } + kind, _, err := unstructured.NestedString(item, "kind") + if err != nil { + return []object.ObjMetadata{}, err + } + groupKind := schema.GroupKind{ + Group: strings.TrimSpace(group), + Kind: strings.TrimSpace(kind), + } + klog.V(4).Infof(`creating obj metadata: "%s/%s/%s"`, namespace, name, groupKind) + objMeta, err := object.CreateObjMetadata(namespace, name, groupKind) + if err != nil { + return []object.ObjMetadata{}, err + } + objs = append(objs, objMeta) + } + return objs, nil + +} + +func (icm *InventoryResourceGroup) StoreSubgroups(objMetas []object.ObjMetadata) (*unstructured.Unstructured, error){ + if icm.inv == nil { + return nil, fmt.Errorf("inventory info is nil") + } + klog.V(4).Infof("getting inventory resource group") + // Create a slice of Resources as empty Interface + klog.V(4).Infof("Creating list of %d resources", len(objMetas)) + var objs []interface{} + for _, objMeta := range objMetas { + klog.V(4).Infof(`storing inventory obj reference: "%s/%s"`, objMeta.Namespace, objMeta.Name) + objs = append(objs, map[string]interface{}{ + "group": objMeta.GroupKind.Group, + "kind": objMeta.GroupKind.Kind, + "namespace": objMeta.Namespace, + "name": objMeta.Name, + }) + } + // Create the inventory object by copying the template. + // Adds or clears the inventory ObjMetadata to the ResourceGroup "spec.resources" section + if len(objs) == 0 { + klog.V(4).Infoln("clearing inventory resources") + unstructured.RemoveNestedField(icm.inv.UnstructuredContent(), + "spec", "subgroups") + } else { + klog.V(4).Infof("storing inventory (%d) resources", len(objs)) + err := unstructured.SetNestedSlice(icm.inv.UnstructuredContent(), + objs, "spec", "subgroups") + if err != nil { + return nil, err + } + } + + if len(icm.objMetas) > 0 { + var objs []interface{} + for _, objMeta := range icm.objMetas { + klog.V(4).Infof(`storing inventory obj reference: "%s/%s"`, objMeta.Namespace, objMeta.Name) + objs = append(objs, map[string]interface{}{ + "group": objMeta.GroupKind.Group, + "kind": objMeta.GroupKind.Kind, + "namespace": objMeta.Namespace, + "name": objMeta.Name, + }) + } + // Adds or clears the inventory ObjMetadata to the ResourceGroup "spec.resources" section + if len(objs) == 0 { + klog.V(4).Infoln("clearing inventory resources") + unstructured.RemoveNestedField(icm.inv.UnstructuredContent(), + "spec", "resources") + } else { + klog.V(4).Infof("storing inventory (%d) resources", len(objs)) + err := unstructured.SetNestedSlice(icm.inv.UnstructuredContent(), + objs, "spec", "resources") + if err != nil { + return nil, err + } + } + } + + return icm.inv, nil +} + // IsResourceGroupInventory returns true if the passed object is // a ResourceGroup inventory object; false otherwise. If an error // occurs, then false is returned and the error. diff --git a/pkg/live/preprocess/process_test.go b/pkg/live/preprocess/process_test.go index 58f52b31b2..c2bf65e942 100644 --- a/pkg/live/preprocess/process_test.go +++ b/pkg/live/preprocess/process_test.go @@ -129,3 +129,7 @@ func (f *fakeInventoryClient) UpdateLabels(inv inventory.InventoryInfo, labels m f.inventory.SetLabels(labels) return nil } + +func (f *fakeInventoryClient) ApplyInventoryObj(u *unstructured.Unstructured) error { + return nil +} \ No newline at end of file diff --git a/pkg/live/rgloader.go b/pkg/live/rgloader.go index b15f9beaa1..28904a069d 100644 --- a/pkg/live/rgloader.go +++ b/pkg/live/rgloader.go @@ -19,6 +19,7 @@ var _ manifestreader.ManifestLoader = &ResourceGroupManifestLoader{} // ResourceGroup versions of some kpt live apply structures. type ResourceGroupManifestLoader struct { factory util.Factory + nested bool } // NewResourceGroupProvider encapsulates the passed values, and returns a pointer to an ResourceGroupProvider. @@ -28,6 +29,13 @@ func NewResourceGroupManifestLoader(f util.Factory) *ResourceGroupManifestLoader } } +func NewResourceGroupManifestLoaderNested(f util.Factory) *ResourceGroupManifestLoader { + return &ResourceGroupManifestLoader{ + factory: f, + nested: true, + } +} + // Factory returns the kubectl factory. func (f *ResourceGroupManifestLoader) InventoryInfo(objs []*unstructured.Unstructured) (inventory.InventoryInfo, []*unstructured.Unstructured, error) { objs, invObj := findResourceGroupInv(objs) @@ -79,6 +87,7 @@ func (f *ResourceGroupManifestLoader) ManifestReader(reader io.Reader, args []st Path: args[0], ReaderOptions: readerOptions, }, + nested: f.nested, } } return rgReader, nil diff --git a/pkg/live/rgpath.go b/pkg/live/rgpath.go index 3d587ad9e5..1fcf83a552 100644 --- a/pkg/live/rgpath.go +++ b/pkg/live/rgpath.go @@ -5,6 +5,9 @@ package live import ( "fmt" + "os" + "path/filepath" + "strings" "github.com/GoogleContainerTools/kpt/pkg/kptfile" "github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" @@ -12,12 +15,14 @@ import ( "k8s.io/klog" "sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/cli-utils/pkg/manifestreader" + "sigs.k8s.io/kustomize/kyaml/kio/kioutil" ) // ResourceGroupPathManifestReader encapsulates the default path // manifest reader. type ResourceGroupPathManifestReader struct { pathReader *manifestreader.PathManifestReader + nested bool } // Read reads the manifests and returns them as Info objects. @@ -43,10 +48,33 @@ func (p *ResourceGroupPathManifestReader) Read() ([]*unstructured.Unstructured, if err == nil { klog.V(4).Infof(`from Kptfile generating ResourceGroup inventory object "%s/%s/%s"`, inv.Namespace, inv.Name, inv.InventoryID) + if p.nested { + annotations := invObj.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + for k, v := range kf.Annotations { + if k == kioutil.PathAnnotation { + v = strings.TrimPrefix(v, p.pathReader.Path) + } + annotations[k] = v + } + invObj.SetAnnotations(annotations) + } objs = append(objs, invObj) } else { klog.V(4).Infof("unable to generate ResourceGroup inventory: %s", err) } + + // Read Kptfile from the subdirectories + if p.nested { + rgs, err := getSubDirResourceGroups(p.pathReader.Path) + if err != nil { + klog.V(4).Infof("unable to read the sub package level ResourceGroup: %s", err) + } + objs = append(objs, rgs...) + } + return objs, nil } @@ -89,3 +117,45 @@ func ResourceGroupUnstructured(name, namespace, id string) *unstructured.Unstruc } return inventoryObj } + +func getSubDirResourceGroups(dir string) ([]*unstructured.Unstructured, error) { + objs := []*unstructured.Unstructured{} + + err := filepath.Walk(dir, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if path == dir { + return nil + } + if !info.IsDir() { + return nil + } + kf, err := kptfileutil.ReadFile(path) + if err != nil { + klog.V(4).Infof("unable to parse Kptfile for ResourceGroup inventory: %s", err) + return err + } + inv := kf.Inventory + invObj, err := generateInventoryObj(inv) + if err == nil { + klog.V(4).Infof(`from Kptfile generating ResourceGroup inventory object "%s/%s/%s"`, + inv.Namespace, inv.Name, inv.InventoryID) + annotations := invObj.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + for k, v := range kf.Annotations { + if k == kioutil.PathAnnotation { + v = strings.TrimPrefix(strings.TrimPrefix(v, dir), string(filepath.Separator)) + } + annotations[k] = v + } + invObj.SetAnnotations(annotations) + objs = append(objs, invObj) + } + return nil + }) + return objs, err +} diff --git a/pkg/nested/apply.go b/pkg/nested/apply.go new file mode 100644 index 0000000000..175b054d2a --- /dev/null +++ b/pkg/nested/apply.go @@ -0,0 +1,283 @@ +// Copyright 2021 Google LLC. +// SPDX-License-Identifier: Apache-2.0 + +package nested + +import ( + "context" + "github.com/GoogleContainerTools/kpt/pkg/live" + "github.com/go-errors/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" + "k8s.io/klog" + "sigs.k8s.io/cli-utils/pkg/apply" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/cli-utils/pkg/provider" +) + +type applier struct { + provider provider.Provider + invclient inventory.InventoryClient + applier *apply.Applier + destroyer *apply.Destroyer +} + +func NewApplier(p provider.Provider) (*applier, error) { + invClient, err := p.InventoryClient() + if err != nil { + return nil, err + } + ap := apply.NewApplier(p) + err = ap.Initialize() + if err != nil { + return nil, err + } + ds := apply.NewDestroyer(p) + err = ds.Initialize() + if err != nil { + return nil, err + } + + return &applier{ + provider: p, + invclient: invClient, + applier: ap, + destroyer: ds, + }, nil +} + +func (a *applier) Apply(ctx context.Context, ninv *NestedInventory, option apply.Options) <-chan event.Event { + eventChannel := make(chan event.Event) + go func() { + defer close(eventChannel) + + // first step: apply inventory resourcegroups with updating the subgroup inventory list as the union + klog.V(6).Infof("starting union all the inventory lists under the root: %v", ninv.Resourcegroup) + if err := a.unionInventoryList(ctx, ninv); err != nil { + klog.V(6).Infof("received an error for union all the inventory lists: %v", err) + eventChannel <- event.Event{ + Type: event.ErrorType, + ErrorEvent: event.ErrorEvent{ + Err: errors.WrapPrefix(err, "failed to union inventory lists", 1), + }, + } + return + } + klog.V(6).Infof("finished union all the inventory lists under the root: %s", ninv.Resourcegroup.ID()) + + // second step: prune the inventory objects that have been removed in the new apply + klog.V(6).Infof("starting pruning the obsolete inventory objects under the root: %s", ninv.Resourcegroup.ID()) + a.pruneSubpackage(ctx, ninv) + klog.V(6).Infof("finished pruning the obsolete inventory objects under the root: %s", ninv.Resourcegroup.ID()) + + // third step: apply each inventory object by triggering the cli-utils applier + klog.V(6).Infof("starting applying individual package under the root: %s", ninv.Resourcegroup.ID()) + a.applySubpackage(ctx, eventChannel, ninv) + klog.V(6).Infof("finished applying individual package under the root: %s", ninv.Resourcegroup.ID()) + + // four step: update the final inventory resourcegroups for the subgroup inventory list. + klog.V(6).Infof("starting updating final inventory lists under the root: %s", ninv.Resourcegroup.ID()) + a.finalUpdateInventoryList(ctx, eventChannel, ninv) + klog.V(6).Infof("finished updating final inventory lists under the root: %s", ninv.Resourcegroup.ID()) + }() + + return eventChannel +} + +func (a *applier) unionInventoryList(ctx context.Context, ninv *NestedInventory) error { + if ninv == nil { + return nil + } + + // update live.AllGroups so that the resources being moved between packages + // can be handled correctly. + live.AllGroups = append(live.AllGroups, ninv.Resourcegroup.ID()) + + // update the top inventory object + clusterObject, err := a.invclient.GetClusterInventoryInfo(ninv.Resourcegroup) + if err != nil && apierrors.IsNotFound(err) || clusterObject == nil { + klog.V(6).Infof("Inventory object for inventory %s doesn't exist in the cluster", ninv.Resourcegroup.ID()) + obj, err := ninv.Resourcegroup.StoreSubgroups(ninv.newChildren) + if err != nil { + klog.V(6).Infof("failed to update the subgroup list for inventory %s", ninv.Resourcegroup.ID()) + return err + } + klog.V(6).Infof("Updating the subgroup list for inventory %s", ninv.Resourcegroup.ID()) + err = a.invclient.ApplyInventoryObj(obj) + if err != nil { + klog.V(6).Infof("failed to apply the updated inventory object %s", ninv.Resourcegroup.ID()) + return err + } + klog.V(6).Infof("Applied the updated inventory object %s", ninv.Resourcegroup.ID()) + } else { + klog.V(6).Infof("Inventory object for inventory %s exists in the cluster", ninv.Resourcegroup.ID()) + curr := live.WrapInventoryResourceGroup(clusterObject) + children, err := curr.LoadSubgroups() + if err != nil { + return err + } + klog.V(6).Infof("Loaded the old children from the cluster object for %s", ninv.Resourcegroup.ID()) + ninv.oldChildren = children + m := map[object.ObjMetadata]bool{} + for _, ch := range children { + m[ch] = true + } + for _, ch := range ninv.newChildren { + m[ch] = true + } + children = []object.ObjMetadata{} + for ch := range m { + children = append(children, ch) + } + + klog.V(6).Infof("Updating the subgroup list for inventory %s", ninv.Resourcegroup.ID()) + obj, err := curr.StoreSubgroups(children) + if err != nil { + klog.V(6).Infof("failed to update the subgroup list for inventory %s", ninv.Resourcegroup.ID()) + return err + } + klog.V(6).Infof("Merged the old children and new children for %s", ninv.Resourcegroup.ID()) + err = a.invclient.ApplyInventoryObj(obj) + if err != nil { + klog.V(6).Infof("failed to apply the updated inventory object %s", ninv.Resourcegroup.ID()) + return err + } + klog.V(6).Infof("Applied the updated inventory object %s", ninv.Resourcegroup.ID()) + } + // update the child inventory objects + for _, ch := range ninv.Children { + klog.V(6).Infof("union the inventory subgroup for child inventory object %s", ch.Resourcegroup.ID()) + if err := a.unionInventoryList(ctx, ch); err != nil { + return err + } + } + return nil +} + +func (a *applier) applySubpackage(ctx context.Context, eventChannel chan event.Event, ninv *NestedInventory) { + if ninv == nil { + return + } + + events := a.applier.Run(ctx, ninv.Resourcegroup, ninv.Resources, apply.Options{ + ServerSideOptions: common.ServerSideOptions{ + ServerSideApply: false, + }, + DryRunStrategy: common.DryRunNone, + InventoryPolicy: inventory.AdoptIfNoInventory, + }) + for e := range events { + eventChannel <- e + } + for _, ch := range ninv.Children { + a.applySubpackage(ctx, eventChannel, ch) + } +} + +func (a *applier) pruneSubpackage(ctx context.Context, ninv *NestedInventory) { + if ninv == nil { + return + } + for _, ch := range ninv.Children { + a.pruneSubpackage(ctx, ch) + } + + m := map[object.ObjMetadata]bool{} + for _, ch := range ninv.newChildren { + m[ch] = true + } + + var failedToDestroy []object.ObjMetadata + for _, ch := range ninv.oldChildren { + if !m[ch] { + client, err := namespacedClient(a.provider, ch) + obj, err := client.Get(ctx, ch.Name, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + continue + } + failedToDestroy = append(failedToDestroy, ch) + continue + } + events := a.destroyer.Run(live.WrapInventoryResourceGroup(obj), + &apply.DestroyerOption{InventoryPolicy: inventory.InventoryPolicyMustMatch}) + for e := range events { + if e.Type == event.ErrorType || e.DeleteEvent.Error != nil { + failedToDestroy = append(failedToDestroy, ch) + break + } + } + } + } + + ninv.newChildren = append(ninv.newChildren, failedToDestroy...) +} + +func (a *applier) finalUpdateInventoryList(ctx context.Context, eventChannel chan event.Event, ninv *NestedInventory) { + if ninv == nil { + return + } + + // update the child inventory objects + for _, ch := range ninv.Children { + a.finalUpdateInventoryList(ctx, eventChannel, ch) + } + // update the top inventory object + objs, err := a.invclient.GetClusterObjs(ninv.Resourcegroup) + if err != nil { + eventChannel <- event.Event{ + Type: event.ErrorType, + ErrorEvent: event.ErrorEvent{ + Err: errors.WrapPrefix(err, "error getting the resources", 1), + }, + } + } + err = ninv.Resourcegroup.Store(objs) + if err != nil { + eventChannel <- event.Event{ + Type: event.ErrorType, + ErrorEvent: event.ErrorEvent{ + Err: errors.WrapPrefix(err, "error saving the resources", 1), + }, + } + } + + obj, err := ninv.Resourcegroup.StoreSubgroups(ninv.newChildren) + if err != nil { + eventChannel <- event.Event{ + Type: event.ErrorType, + ErrorEvent: event.ErrorEvent{ + Err: errors.WrapPrefix(err, "error storing the sub groups", 1), + }, + } + } + + if err = a.invclient.ApplyInventoryObj(obj); err != nil { + eventChannel <- event.Event{ + Type: event.ErrorType, + ErrorEvent: event.ErrorEvent{ + Err: errors.WrapPrefix(err, "error final updating the sub groups", 1), + }, + } + } +} + +func namespacedClient(p provider.Provider, obj object.ObjMetadata) (dynamic.ResourceInterface, error) { + mapper, err := p.Factory().ToRESTMapper() + if err != nil { + return nil, err + } + mapping, err := mapper.RESTMapping(obj.GroupKind) + if err != nil { + return nil, err + } + dy, err := p.Factory().DynamicClient() + if err != nil { + return nil, err + } + return dy.Resource(mapping.Resource).Namespace(obj.Namespace), nil +} diff --git a/pkg/nested/apply_test.go b/pkg/nested/apply_test.go new file mode 100644 index 0000000000..e9152f4a97 --- /dev/null +++ b/pkg/nested/apply_test.go @@ -0,0 +1,114 @@ +// Copyright 2021 Google LLC. +// SPDX-License-Identifier: Apache-2.0 + +package nested + +import ( + "context" + "github.com/GoogleContainerTools/kpt/pkg/live" + "github.com/stretchr/testify/assert" + "io/ioutil" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/cmd/util" + "os" + "path/filepath" + "sigs.k8s.io/cli-utils/pkg/apply" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/provider" + "sigs.k8s.io/cli-utils/pkg/util/factory" + "testing" +) + +func TestApplier(t *testing.T) { + testCases := map[string]struct { + manifests map[string]string + subpackageManifests map[string]string + namespace string + enforceNamespace bool + validate bool + + infosCount int + namespaces []string + }{ + "multiple manifests with subpackages": { + manifests: map[string]string{ + "dep.yaml": depManifest, + "cm.yaml": cmManifest, + "Kptfile": kptFile, + }, + subpackageManifests: map[string]string{ + "cm.yaml": subpackageManifest, + "Kptfile": subpackageKptfile, + }, + namespace: "default", + enforceNamespace: true, + + infosCount: 4, + namespaces: []string{"default", "default", "default", "default"}, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + pr := newProvider() + + dir, err := ioutil.TempDir("", "nested-package-test") + assert.NoError(t, err) + if tc.subpackageManifests != nil { + err = os.Mkdir(filepath.Join(dir, "subpackage"), 0700) + 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) + } + for filename, content := range tc.subpackageManifests { + p := filepath.Join(filepath.Join(dir), "subpackage", filename) + err := ioutil.WriteFile(p, []byte(content), 0600) + assert.NoError(t, err) + } + + loader := NewLoader(pr.Factory()) + ninv, err := loader.Read(nil, []string{dir}) + assert.NoError(t, err) + + applier, err := NewApplier(pr) + assert.NoError(t, err) + + events := applier.Apply(context.TODO(), ninv, apply.Options{}) + for e := range events { + if e.Type == event.ErrorType { + t.Errorf("unexpected error %v", e.ErrorEvent.Err) + } + } + }) + } +} + +func TestApplierWithTestData(t *testing.T) { + pr := newProvider() + loader := NewLoader(pr.Factory()) + ninv, err := loader.Read(nil, []string{"testdata/d"}) + assert.NoError(t, err) + + applier, err := NewApplier(pr) + assert.NoError(t, err) + + events := applier.Apply(context.TODO(), ninv, apply.Options{}) + for e := range events { + if e.Type == event.ErrorType { + t.Errorf("unexpected error %v", e.ErrorEvent.Err) + } + } +} + +func newProvider() provider.Provider { + kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() + matchVersionKubeConfigFlags := util.NewMatchVersionFlags(&factory.CachingRESTClientGetter{ + Delegate: kubeConfigFlags, + }) + f := util.NewFactory(matchVersionKubeConfigFlags) + p := live.NewResourceGroupProvider(f) + return p +} diff --git a/pkg/nested/interface.go b/pkg/nested/interface.go new file mode 100644 index 0000000000..a1ce32e3aa --- /dev/null +++ b/pkg/nested/interface.go @@ -0,0 +1,33 @@ +// Copyright 2021 Google LLC. +// SPDX-License-Identifier: Apache-2.0 + +package nested + +import ( + "github.com/GoogleContainerTools/kpt/pkg/live" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/object" +) + +// NestedInventory represents a package with nested inventory that is read +// from the package manifest. +type NestedInventory struct { + Path string + Resourcegroup *live.InventoryResourceGroup + Resources []*unstructured.Unstructured + Children []*NestedInventory + + oldChildren []object.ObjMetadata + newChildren []object.ObjMetadata +} + +func (n *NestedInventory) AddChildInventory(u *unstructured.Unstructured, id string) { + annotations := u.GetAnnotations() + if len(annotations) == 0 { + annotations = make(map[string]string) + } + annotations["config.k8s.io/owning-inventory"] = id + u.SetAnnotations(annotations) + meta := object.UnstructuredToObjMeta(u) + n.newChildren = append(n.newChildren, meta) +} diff --git a/pkg/nested/loader.go b/pkg/nested/loader.go new file mode 100644 index 0000000000..138a3b15da --- /dev/null +++ b/pkg/nested/loader.go @@ -0,0 +1,151 @@ +// Copyright 2021 Google LLC. +// SPDX-License-Identifier: Apache-2.0 + +package nested + +import ( + "io" + "path/filepath" + "strings" + + "github.com/GoogleContainerTools/kpt/pkg/kptfile" + "github.com/GoogleContainerTools/kpt/pkg/live" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/cmd/util" + "sigs.k8s.io/kustomize/kyaml/kio/kioutil" +) + +// ManifestLoader defines the interface to read the +// package manifests. The manifest can be from either +// the local filesystem or from the stdin. +type ManifestLoader interface { + Read(io.Reader, []string) (*NestedInventory, error) +} + +var _ ManifestLoader = &loader{} + +func NewLoader(f util.Factory) *loader { + return &loader{ + factory: f, + } +} + +type loader struct { + factory util.Factory +} + +func (f *loader) Read(reader io.Reader, args []string) (*NestedInventory, error) { + l := live.NewResourceGroupManifestLoaderNested(f.factory) + r, err := l.ManifestReader(reader, args) + if err != nil { + return nil, err + } + objs, err := r.Read() + if err != nil { + return nil, err + } + return f.toNestedInventory(objs) +} + +func (f *loader) toNestedInventory(objs []*unstructured.Unstructured) (*NestedInventory, error) { + rgs, nrgs := splitObjects(objs) + ninv, err := formNestedInventory(rgs) + if err != nil { + return nil, err + } + err = fillInResources(ninv, nrgs) + return ninv, err +} + +func splitObjects(objs []*unstructured.Unstructured) ([]*unstructured.Unstructured, []*unstructured.Unstructured) { + var rgs []*unstructured.Unstructured + var nonRgs []*unstructured.Unstructured + gvk := schema.GroupVersionKind{ + Group: kptfile.KptFileGroup, + Version: kptfile.KptFileVersion, + Kind: "ResourceGroup", + } + for _, obj := range objs { + if obj.GroupVersionKind() == gvk { + rgs = append(rgs, obj) + } else { + nonRgs = append(nonRgs, obj) + } + } + return rgs, nonRgs +} + +func formNestedInventory(invs []*unstructured.Unstructured) (*NestedInventory, error) { + if len(invs) == 0 { + return nil, nil + } + visited := make([]bool, len(invs)) + var ninv *NestedInventory + + for i, inv := range invs { + if inv.GetAnnotations()[kioutil.PathAnnotation] == "" { + ninv = &NestedInventory{ + Path: "", + Resourcegroup: live.WrapInventoryResourceGroup(inv), + Resources: nil, + Children: nil, + } + visited[i] = true + break + } + } + constructNestedInv(ninv, invs, visited) + return ninv, nil +} + +func constructNestedInv(root *NestedInventory, invs []*unstructured.Unstructured, visited []bool) { + for i, inv := range invs { + if visited[i] { + continue + } + path := inv.GetAnnotations()[kioutil.PathAnnotation] + dir, _ := filepath.Split(path) + if dir == root.Path { + node := &NestedInventory{ + Path: path, + Resourcegroup: live.WrapInventoryResourceGroup(inv), + Resources: nil, + Children: nil, + } + root.Children = append(root.Children, node) + root.AddChildInventory(inv, root.Resourcegroup.ID()) + visited[i] = true + constructNestedInv(node, invs, visited) + } + } + return +} + +func fillInResources(ninv *NestedInventory, objs []*unstructured.Unstructured) error { + if ninv == nil { + return nil + } + for _, obj := range objs { + if err := fillInSingleResource(ninv, obj); err != nil { + return err + } + } + return nil +} + +func fillInSingleResource(ninv *NestedInventory, obj *unstructured.Unstructured) error { + path := obj.GetAnnotations()[kioutil.PathAnnotation] + dir, _ := filepath.Split(path) + dir = strings.TrimSuffix(dir, string(filepath.Separator)) + if ninv.Path == dir { + ninv.Resources = append(ninv.Resources, obj) + } else { + for _, ch := range ninv.Children { + if err := fillInSingleResource(ch, obj); err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/nested/loader_test.go b/pkg/nested/loader_test.go new file mode 100644 index 0000000000..cae70b477a --- /dev/null +++ b/pkg/nested/loader_test.go @@ -0,0 +1,165 @@ +package nested + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +func TestPathManifestReader_Read(t *testing.T) { + testCases := map[string]struct { + manifests map[string]string + subpackageManifests map[string]string + namespace string + enforceNamespace bool + validate bool + + infosCount int + namespaces []string + }{ + "namespace should be set if not already present": { + manifests: map[string]string{ + "dep.yaml": depManifest, + }, + namespace: "foo", + enforceNamespace: true, + + infosCount: 1, + namespaces: []string{"foo"}, + }, + "multiple manifests": { + manifests: map[string]string{ + "dep.yaml": depManifest, + "cm.yaml": cmManifest, + }, + namespace: "default", + enforceNamespace: true, + + infosCount: 2, + namespaces: []string{"default", "default"}, + }, + "multiple manifests with Kptfile": { + manifests: map[string]string{ + "dep.yaml": depManifest, + "cm.yaml": cmManifest, + "Kptfile": kptFile, + }, + namespace: "default", + enforceNamespace: true, + + infosCount: 3, + namespaces: []string{"default", "default", "default"}, + }, + "multiple manifests with subpackages": { + manifests: map[string]string{ + "dep.yaml": depManifest, + "cm.yaml": cmManifest, + "Kptfile": kptFile, + }, + subpackageManifests: map[string]string{ + "cm.yaml": subpackageManifest, + "Kptfile": subpackageKptfile, + }, + namespace: "default", + enforceNamespace: true, + + infosCount: 4, + namespaces: []string{"default", "default", "default", "default"}, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + tf := cmdtesting.NewTestFactory().WithNamespace("test-ns") + defer tf.Cleanup() + + dir, err := ioutil.TempDir("", "path-reader-test") + assert.NoError(t, err) + if tc.subpackageManifests != nil { + err = os.Mkdir(filepath.Join(dir, "subpackage"), 0700) + 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) + } + for filename, content := range tc.subpackageManifests { + p := filepath.Join(filepath.Join(dir), "subpackage", filename) + err := ioutil.WriteFile(p, []byte(content), 0600) + assert.NoError(t, err) + } + + loader := NewLoader(tf) + _, err = loader.Read(nil, []string{dir}) + assert.NoError(t, err) + }) + } +} + +var ( + depManifest = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 +` + + cmManifest = ` +kind: ConfigMap +apiVersion: v1 +metadata: + name: cm3 +data: + foo: bar +` + kptFile = ` +apiVersion: kpt.dev/v1alpha1 +kind: Kptfile +metadata: + name: test +inventory: + namespace: default + name: inventory-test + inventoryID: inventory-test +` + subpackageManifest = ` +kind: ConfigMap +apiVersion: v1 +metadata: + name: cm1 +data: + foo: bar +` + subpackageKptfile = ` +apiVersion: kpt.dev/v1alpha1 +kind: Kptfile +metadata: + name: subpackage +inventory: + namespace: default + name: inventory-subpackage-test + inventoryID: inventory-subpackage-test +` +) diff --git a/pkg/nested/testdata/a/Kptfile b/pkg/nested/testdata/a/Kptfile new file mode 100644 index 0000000000..716a48b097 --- /dev/null +++ b/pkg/nested/testdata/a/Kptfile @@ -0,0 +1,8 @@ +apiVersion: kpt.dev/v1alpha1 +kind: Kptfile +metadata: + name: test +inventory: + namespace: default + name: inventory-test-root-pkg + inventoryID: inventory-test-root-pkg \ No newline at end of file diff --git a/pkg/nested/testdata/a/manifests.yaml b/pkg/nested/testdata/a/manifests.yaml new file mode 100644 index 0000000000..4cfca2de3b --- /dev/null +++ b/pkg/nested/testdata/a/manifests.yaml @@ -0,0 +1,28 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: cm +data: + foo: bar diff --git a/pkg/nested/testdata/a/subpackage/Kptfile b/pkg/nested/testdata/a/subpackage/Kptfile new file mode 100644 index 0000000000..a73b9c5542 --- /dev/null +++ b/pkg/nested/testdata/a/subpackage/Kptfile @@ -0,0 +1,8 @@ +apiVersion: kpt.dev/v1alpha1 +kind: Kptfile +metadata: + name: subpackage +inventory: + namespace: default + name: inventory-test-sub-package + inventoryID: inventory-test-sub-package \ No newline at end of file diff --git a/pkg/nested/testdata/a/subpackage/manifests.yaml b/pkg/nested/testdata/a/subpackage/manifests.yaml new file mode 100644 index 0000000000..e11ce6a2e8 --- /dev/null +++ b/pkg/nested/testdata/a/subpackage/manifests.yaml @@ -0,0 +1,6 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: subpackage-cm1 +data: + foo: bar diff --git a/pkg/nested/testdata/b/Kptfile b/pkg/nested/testdata/b/Kptfile new file mode 100644 index 0000000000..716a48b097 --- /dev/null +++ b/pkg/nested/testdata/b/Kptfile @@ -0,0 +1,8 @@ +apiVersion: kpt.dev/v1alpha1 +kind: Kptfile +metadata: + name: test +inventory: + namespace: default + name: inventory-test-root-pkg + inventoryID: inventory-test-root-pkg \ No newline at end of file diff --git a/pkg/nested/testdata/b/manifests.yaml b/pkg/nested/testdata/b/manifests.yaml new file mode 100644 index 0000000000..bdff48bd90 --- /dev/null +++ b/pkg/nested/testdata/b/manifests.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 +--- +# Rename the ConfigMap from cm to root-cm +kind: ConfigMap +apiVersion: v1 +metadata: + name: root-cm +data: + foo: bar diff --git a/pkg/nested/testdata/b/subpackage/Kptfile b/pkg/nested/testdata/b/subpackage/Kptfile new file mode 100644 index 0000000000..a73b9c5542 --- /dev/null +++ b/pkg/nested/testdata/b/subpackage/Kptfile @@ -0,0 +1,8 @@ +apiVersion: kpt.dev/v1alpha1 +kind: Kptfile +metadata: + name: subpackage +inventory: + namespace: default + name: inventory-test-sub-package + inventoryID: inventory-test-sub-package \ No newline at end of file diff --git a/pkg/nested/testdata/b/subpackage/manifests.yaml b/pkg/nested/testdata/b/subpackage/manifests.yaml new file mode 100644 index 0000000000..0363073de8 --- /dev/null +++ b/pkg/nested/testdata/b/subpackage/manifests.yaml @@ -0,0 +1,7 @@ +# Rename the ConfigMap from subpackage-cm1 to subpackage-cm2 +kind: ConfigMap +apiVersion: v1 +metadata: + name: subpackage-cm2 +data: + foo: bar diff --git a/pkg/nested/testdata/c/Kptfile b/pkg/nested/testdata/c/Kptfile new file mode 100644 index 0000000000..716a48b097 --- /dev/null +++ b/pkg/nested/testdata/c/Kptfile @@ -0,0 +1,8 @@ +apiVersion: kpt.dev/v1alpha1 +kind: Kptfile +metadata: + name: test +inventory: + namespace: default + name: inventory-test-root-pkg + inventoryID: inventory-test-root-pkg \ No newline at end of file diff --git a/pkg/nested/testdata/c/manifests.yaml b/pkg/nested/testdata/c/manifests.yaml new file mode 100644 index 0000000000..4cfca2de3b --- /dev/null +++ b/pkg/nested/testdata/c/manifests.yaml @@ -0,0 +1,28 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: cm +data: + foo: bar diff --git a/pkg/nested/testdata/c/subpackage/Kptfile b/pkg/nested/testdata/c/subpackage/Kptfile new file mode 100644 index 0000000000..659dab6605 --- /dev/null +++ b/pkg/nested/testdata/c/subpackage/Kptfile @@ -0,0 +1,11 @@ +apiVersion: kpt.dev/v1alpha1 +kind: Kptfile +metadata: + name: subpackage +# change the inventory information +# change name: inventory-test-sub-package-renamed +# change the inventory id: inventory-test-sub-package-renamed +inventory: + namespace: default + name: inventory-test-sub-package-renamed + inventoryID: inventory-test-sub-package-renamed \ No newline at end of file diff --git a/pkg/nested/testdata/c/subpackage/manifests.yaml b/pkg/nested/testdata/c/subpackage/manifests.yaml new file mode 100644 index 0000000000..e11ce6a2e8 --- /dev/null +++ b/pkg/nested/testdata/c/subpackage/manifests.yaml @@ -0,0 +1,6 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: subpackage-cm1 +data: + foo: bar diff --git a/pkg/nested/testdata/d/Kptfile b/pkg/nested/testdata/d/Kptfile new file mode 100644 index 0000000000..716a48b097 --- /dev/null +++ b/pkg/nested/testdata/d/Kptfile @@ -0,0 +1,8 @@ +apiVersion: kpt.dev/v1alpha1 +kind: Kptfile +metadata: + name: test +inventory: + namespace: default + name: inventory-test-root-pkg + inventoryID: inventory-test-root-pkg \ No newline at end of file diff --git a/pkg/nested/testdata/d/manifests.yaml b/pkg/nested/testdata/d/manifests.yaml new file mode 100644 index 0000000000..4cfca2de3b --- /dev/null +++ b/pkg/nested/testdata/d/manifests.yaml @@ -0,0 +1,28 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: cm +data: + foo: bar diff --git a/pkg/nested/testdata/d/subpackage-new/Kptfile b/pkg/nested/testdata/d/subpackage-new/Kptfile new file mode 100644 index 0000000000..66c7b1e1ec --- /dev/null +++ b/pkg/nested/testdata/d/subpackage-new/Kptfile @@ -0,0 +1,8 @@ +apiVersion: kpt.dev/v1alpha1 +kind: Kptfile +metadata: + name: subpackage +inventory: + namespace: default + name: inventory-test-sub-package-new + inventoryID: inventory-test-sub-package-new \ No newline at end of file diff --git a/pkg/nested/testdata/d/subpackage-new/manifests.yaml b/pkg/nested/testdata/d/subpackage-new/manifests.yaml new file mode 100644 index 0000000000..e11ce6a2e8 --- /dev/null +++ b/pkg/nested/testdata/d/subpackage-new/manifests.yaml @@ -0,0 +1,6 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: subpackage-cm1 +data: + foo: bar diff --git a/pkg/nested/testdata/d/subpackage/Kptfile b/pkg/nested/testdata/d/subpackage/Kptfile new file mode 100644 index 0000000000..a73b9c5542 --- /dev/null +++ b/pkg/nested/testdata/d/subpackage/Kptfile @@ -0,0 +1,8 @@ +apiVersion: kpt.dev/v1alpha1 +kind: Kptfile +metadata: + name: subpackage +inventory: + namespace: default + name: inventory-test-sub-package + inventoryID: inventory-test-sub-package \ No newline at end of file diff --git a/pkg/nested/testdata/d/subpackage/manifests.yaml b/pkg/nested/testdata/d/subpackage/manifests.yaml new file mode 100644 index 0000000000..ab33de201b --- /dev/null +++ b/pkg/nested/testdata/d/subpackage/manifests.yaml @@ -0,0 +1,6 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: subpackage-cm1-renamed +data: + foo: bar