From fd87d1796e7610dd6c6a7d8662323cff4b0cb9d4 Mon Sep 17 00:00:00 2001 From: Ramon Quitales Date: Tue, 8 Feb 2022 09:16:23 -0800 Subject: [PATCH] Main feat: enable migrating from Kptfile & CM to resourcegroup inventories (#2705) This commit enables `kpt live migrate` to migrate inventory information from either a ConfigMap or Kptfile to a separate ResourceGroup file. This functionality is currently behind a feature gate and is not exposed to the user via any CLI flags. Enabling of this feature to users will be done later. make kpt binary optional in test harness (#2758) feat: enable STDIN apply and destroy using RG inventory (#2709) This commit enables actuation from STDIN using inventory information that is stored in a ResourceGroup file. This feature is currently behind a feature gate and will be exposed to users as a CLI flag in a future commit/PR. updated the version to 1.0.0-beta.13 (#2806) Merge main into porch Correct Apache License Text (#2675) LICENSE file is supposed to be an exact copy of https://www.apache.org/licenses/LICENSE-2.0.txt. --- Formula/kpt.rb | 4 +- LICENSE | 4 +- internal/cmdapply/cmdapply.go | 3 +- internal/cmddestroy/cmddestroy.go | 3 +- internal/cmdmigrate/migratecmd.go | 192 ++++++++++++++++++++- internal/cmdmigrate/migratecmd_test.go | 133 +++++++++++++++ internal/pkg/pkg.go | 19 ++- internal/testutil/pkgbuilder/builder.go | 52 ++++++ pkg/api/kptfile/v1/types.go | 7 + pkg/live/load.go | 146 +++++++++++++--- pkg/live/load_test.go | 202 +++++++++++++++++++++-- pkg/live/rgpath.go | 8 + pkg/live/rgstream.go | 5 + pkg/test/runner/runner.go | 6 +- site/installation/README.md | 8 +- thirdparty/cli-utils/status/cmdstatus.go | 3 +- 16 files changed, 735 insertions(+), 60 deletions(-) diff --git a/Formula/kpt.rb b/Formula/kpt.rb index 0de3e08023..241744dd8c 100644 --- a/Formula/kpt.rb +++ b/Formula/kpt.rb @@ -15,8 +15,8 @@ class Kpt < Formula desc "Toolkit to manage,and apply Kubernetes Resource config data files" homepage "https://googlecontainertools.github.io/kpt" - url "https://github.com/GoogleContainerTools/kpt/archive/v1.0.0-beta.7.tar.gz" - sha256 "e31e7ee63006150f730ed8a08e9063a2fb2faf9e617353e3649ef893bdf7c156" + url "https://github.com/GoogleContainerTools/kpt/archive/v1.0.0-beta.13.tar.gz" + sha256 "d98a95f1de38fc8c7dee861ec249874ea3ddf03f5a28ae7fc4a0364d3578667b" depends_on "go" => :build diff --git a/LICENSE b/LICENSE index 9441df3dcb..d645695673 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2019 Google LLC + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -199,4 +199,4 @@ 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. \ No newline at end of file + limitations under the License. diff --git a/internal/cmdapply/cmdapply.go b/internal/cmdapply/cmdapply.go index de0aedebc7..a5391fdc41 100644 --- a/internal/cmdapply/cmdapply.go +++ b/internal/cmdapply/cmdapply.go @@ -108,6 +108,7 @@ type Runner struct { pruneTimeout time.Duration inventoryPolicyString string dryRun bool + rgFile string printStatusEvents bool inventoryPolicy inventory.InventoryPolicy @@ -166,7 +167,7 @@ func (r *Runner) runE(c *cobra.Command, args []string) error { } } - objs, inv, err := live.Load(r.factory, path, c.InOrStdin()) + objs, inv, err := live.Load(r.factory, path, r.rgFile, c.InOrStdin()) if err != nil { return err } diff --git a/internal/cmddestroy/cmddestroy.go b/internal/cmddestroy/cmddestroy.go index 5d71c70758..7ddbddf677 100644 --- a/internal/cmddestroy/cmddestroy.go +++ b/internal/cmddestroy/cmddestroy.go @@ -83,6 +83,7 @@ type Runner struct { output string inventoryPolicyString string dryRun bool + rgFile string printStatusEvents bool inventoryPolicy inventory.InventoryPolicy @@ -128,7 +129,7 @@ func (r *Runner) runE(c *cobra.Command, args []string) error { } } - _, inv, err := live.Load(r.factory, path, c.InOrStdin()) + _, inv, err := live.Load(r.factory, path, r.rgFile, c.InOrStdin()) if err != nil { return err } diff --git a/internal/cmdmigrate/migratecmd.go b/internal/cmdmigrate/migratecmd.go index 1005f32e3e..23a44310af 100644 --- a/internal/cmdmigrate/migratecmd.go +++ b/internal/cmdmigrate/migratecmd.go @@ -6,17 +6,21 @@ package cmdmigrate import ( "bytes" "context" - "errors" + goerrors "errors" "fmt" "io" "io/ioutil" "os" + "path/filepath" "github.com/GoogleContainerTools/kpt/internal/cmdliveinit" "github.com/GoogleContainerTools/kpt/internal/docs/generated/livedocs" + "github.com/GoogleContainerTools/kpt/internal/errors" "github.com/GoogleContainerTools/kpt/internal/pkg" + "github.com/GoogleContainerTools/kpt/internal/types" "github.com/GoogleContainerTools/kpt/internal/util/argutil" "github.com/GoogleContainerTools/kpt/internal/util/pathutil" + "github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" "github.com/GoogleContainerTools/kpt/pkg/live" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -41,10 +45,12 @@ type MigrateRunner struct { dir string dryRun bool name string + rgFile string force bool rgInvClientFunc func(util.Factory) (inventory.InventoryClient, error) cmInvClientFunc func(util.Factory) (inventory.InventoryClient, error) cmLoader manifestreader.ManifestLoader + cmNotMigrated bool // flag to determine if migration from ConfigMap has occurred } // NewRunner returns a pointer to an initial MigrateRunner structure. @@ -98,6 +104,15 @@ func NewCommand(ctx context.Context, f util.Factory, cmLoader manifestreader.Man // Run executes the migration from the ConfigMap based inventory to the ResourceGroup // based inventory. func (mr *MigrateRunner) Run(reader io.Reader, args []string) error { + // Use ResourceGroup file for inventory logic if the resourcegroup file + // is set directly. For this feature gate, the resourcegroup must be directly set + // through our tests since we are not exposing this through the command surface as a + // flag, currently. When we promote this, the resourcegroup filename can be empty and + // the default filename value will be inferred/used. + if mr.rgFile != "" { + return mr.runLiveMigrateWithRGFile(reader, args) + } + // Validate the number of arguments. if len(args) > 1 { return fmt.Errorf("too many arguments; migrate requires one directory argument (or stdin)") @@ -292,7 +307,7 @@ func (mr *MigrateRunner) migrateObjs(rgInvClient inventory.InventoryClient, } } - _, inv, err := live.Load(mr.factory, path, reader) + _, inv, err := live.Load(mr.factory, path, mr.rgFile, reader) if err != nil { return err } @@ -390,3 +405,176 @@ func rgInvClient(factory util.Factory) (inventory.InventoryClient, error) { func cmInvClient(factory util.Factory) (inventory.InventoryClient, error) { return inventory.NewInventoryClient(factory, inventory.WrapInventoryObj, inventory.InvInfoToConfigMap) } + +// func runLiveMigrateWithRGFile is a modified version of MigrateRunner.Run that stores the +// package inventory information in a separate resourcegroup file. The logic for this is branched into +// a separate function to enable feature gating. +func (mr *MigrateRunner) runLiveMigrateWithRGFile(reader io.Reader, args []string) error { + // Validate the number of arguments. + if len(args) > 1 { + return fmt.Errorf("too many arguments; migrate requires one directory argument (or stdin)") + } + // Validate argument is a directory. + if len(args) == 1 { + var err error + mr.dir, err = config.NormalizeDir(args[0]) + if err != nil { + return err + } + } + // Store the stdin bytes if necessary so they can be used twice. + var stdinBytes []byte + var err error + if len(args) == 0 { + stdinBytes, err = ioutil.ReadAll(reader) + if err != nil { + return err + } + if len(stdinBytes) == 0 { + return fmt.Errorf("no arguments means stdin has data; missing bytes on stdin") + } + } + + // Apply the ResourceGroup CRD to the cluster, ignoring if it already exists. + if err := mr.applyCRD(); err != nil { + return err + } + + // Check if we need to migrate from ConfigMap to ResourceGroup. + if err := mr.migrateCMToRG(stdinBytes, args); err != nil { + return err + } + + // Migrate from Kptfile instead. + if mr.cmNotMigrated { + return mr.migrateKptfileToRG(args) + } + + return nil +} + +// migrateKptfileToRG extracts inventory information from a package's Kptfile +// into an external resourcegroup file. +func (mr *MigrateRunner) migrateKptfileToRG(args []string) error { + const op errors.Op = "migratecmd.migrateKptfileToRG" + klog.V(4).Infoln("attempting to migrate from Kptfile inventory") + fmt.Fprint(mr.ioStreams.Out, " reading existing Kptfile...") + if !mr.dryRun { + dir := args[0] + p, err := pkg.New(filesys.FileSystemOrOnDisk{}, dir) + if err != nil { + return err + } + kf, err := p.Kptfile() + if err != nil { + return err + } + + if _, err := kptfileutil.ValidateInventory(kf.Inventory); err != nil { + // Invalid Kptfile. + return err + } + + // Make sure resourcegroup file does not exist. + _, rgFileErr := os.Stat(filepath.Join(dir, mr.rgFile)) + switch { + case rgFileErr == nil: + return errors.E(op, errors.IO, types.UniquePath(dir), "the resourcegroup file already exists and inventory information cannot be migrated") + case err != nil && !goerrors.Is(err, os.ErrNotExist): + return errors.E(op, errors.IO, types.UniquePath(dir), err) + } + + err = (&cmdliveinit.ConfigureInventoryInfo{ + Pkg: p, + Factory: mr.factory, + Quiet: true, + Name: kf.Inventory.Name, + InventoryID: kf.Inventory.InventoryID, + RGFileName: mr.rgFile, + Force: true, + }).Run(mr.ctx) + + if err != nil { + return err + } + } + fmt.Fprint(mr.ioStreams.Out, "success\n") + return nil +} + +// migrateCMToRG migrates from ConfigMap to resourcegroup object. +func (mr *MigrateRunner) migrateCMToRG(stdinBytes []byte, args []string) error { + // Create the inventory clients for reading inventories based on RG and + // ConfigMap. + rgInvClient, err := mr.rgInvClientFunc(mr.factory) + if err != nil { + return err + } + cmInvClient, err := mr.cmInvClientFunc(mr.factory) + if err != nil { + return err + } + // Retrieve the current ConfigMap inventory objects. + cmInvObj, err := mr.retrieveConfigMapInv(bytes.NewReader(stdinBytes), args) + if err != nil { + if _, ok := err.(inventory.NoInventoryObjError); ok { + // No ConfigMap inventory means the migration has already run before. + klog.V(4).Infoln("swallowing no ConfigMap inventory error") + mr.cmNotMigrated = true + return nil + } + klog.V(4).Infof("error retrieving ConfigMap inventory object: %s", err) + return err + } + cmInventoryID := cmInvObj.ID() + klog.V(4).Infof("previous inventoryID: %s", cmInventoryID) + // Create ResourceGroup object file locallly (e.g. namespace, name, id). + if err := mr.createRGfile(mr.ctx, args, cmInventoryID); err != nil { + return err + } + cmObjs, err := mr.retrieveInvObjs(cmInvClient, cmInvObj) + if err != nil { + return err + } + if len(cmObjs) > 0 { + // Migrate the ConfigMap inventory objects to a ResourceGroup custom resource. + if err = mr.migrateObjs(rgInvClient, cmObjs, bytes.NewReader(stdinBytes), args); err != nil { + return err + } + // Delete the old ConfigMap inventory object. + if err = mr.deleteConfigMapInv(cmInvClient, cmInvObj); err != nil { + return err + } + } + return mr.deleteConfigMapFile() +} + +// createRGfile writes the inventory information into the resourcegroup object. +func (mr *MigrateRunner) createRGfile(ctx context.Context, args []string, prevID string) error { + fmt.Fprint(mr.ioStreams.Out, " creating ResourceGroup object file...") + if !mr.dryRun { + p, err := pkg.New(filesys.FileSystemOrOnDisk{}, args[0]) + if err != nil { + return err + } + err = (&cmdliveinit.ConfigureInventoryInfo{ + Pkg: p, + Factory: mr.factory, + Quiet: true, + InventoryID: prevID, + RGFileName: mr.rgFile, + Force: mr.force, + }).Run(ctx) + + if err != nil { + var invExistsError *cmdliveinit.InvExistsError + if errors.As(err, &invExistsError) { + fmt.Fprint(mr.ioStreams.Out, "values already exist...") + } else { + return err + } + } + } + fmt.Fprint(mr.ioStreams.Out, "success\n") + return nil +} diff --git a/internal/cmdmigrate/migratecmd_test.go b/internal/cmdmigrate/migratecmd_test.go index 21fc03e09f..82b6c9ff9e 100644 --- a/internal/cmdmigrate/migratecmd_test.go +++ b/internal/cmdmigrate/migratecmd_test.go @@ -11,6 +11,7 @@ import ( "github.com/GoogleContainerTools/kpt/internal/pkg" "github.com/GoogleContainerTools/kpt/internal/printer/fake" + rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -178,6 +179,118 @@ func TestKptMigrate_updateKptfile(t *testing.T) { } } +func TestKptMigrate_migrateKptfileToRG(t *testing.T) { + testCases := map[string]struct { + kptfile string + rgFilename string + resourcegroup string + dryRun bool + isError bool + }{ + "Missing Kptfile is an error": { + kptfile: "", + rgFilename: "resourcegroup.yaml", + dryRun: false, + isError: true, + }, + "Kptfile with existing inventory will create ResourceGroup": { + kptfile: kptFileWithInventory, + rgFilename: "resourcegroup.yaml", + dryRun: false, + isError: false, + }, + "ResopurceGroup file already exists will error": { + kptfile: kptFileWithInventory, + rgFilename: "resourcegroup.yaml", + resourcegroup: resourceGroupInventory, + dryRun: false, + isError: true, + }, + "Dry-run will not fill in inventory fields": { + kptfile: kptFile, + rgFilename: "resourcegroup.yaml", + dryRun: true, + isError: false, + }, + "Custom ResourceGroup file will be generated": { + kptfile: kptFileWithInventory, + rgFilename: "custom-rg.yaml", + dryRun: false, + isError: false, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + // Set up fake test factory + tf := cmdtesting.NewTestFactory().WithNamespace(inventoryNamespace) + defer tf.Cleanup() + ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled + + // Set up temp directory with Ktpfile + dir, err := ioutil.TempDir("", "kpt-migrate-test") + assert.NoError(t, err) + p := filepath.Join(dir, "Kptfile") + err = ioutil.WriteFile(p, []byte(tc.kptfile), 0600) + assert.NoError(t, err) + + if tc.resourcegroup != "" { + p := filepath.Join(dir, tc.rgFilename) + err = ioutil.WriteFile(p, []byte(tc.resourcegroup), 0600) + assert.NoError(t, err) + } + + ctx := fake.CtxWithDefaultPrinter() + // Create MigrateRunner and call "updateKptfile" + cmLoader := manifestreader.NewManifestLoader(tf) + migrateRunner := NewRunner(ctx, tf, cmLoader, ioStreams) + migrateRunner.dryRun = tc.dryRun + migrateRunner.rgFile = tc.rgFilename + migrateRunner.cmInvClientFunc = func(factory util.Factory) (inventory.InventoryClient, error) { + return inventory.NewFakeInventoryClient([]object.ObjMetadata{}), nil + } + err = migrateRunner.migrateKptfileToRG([]string{dir}) + // Check if there should be an error + if tc.isError { + if err == nil { + t.Fatalf("expected error but received none") + } + return + } + assert.NoError(t, err) + kf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, dir) + if !assert.NoError(t, err) { + t.FailNow() + } + + rg, err := pkg.ReadRGFile(dir, migrateRunner.rgFile) + if !tc.dryRun && !assert.NoError(t, err) { + t.FailNow() + } + + // Ensure the Kptfile does not contain inventory information. + if !assert.Nil(t, kf.Inventory) { + t.Errorf("inventory information should not be set in Kptfile") + } + + if !tc.dryRun { + if rg == nil { + t.Fatalf("unable to read ResourceGroup file") + } + assert.Equal(t, inventoryNamespace, rg.ObjectMeta.Namespace) + if len(rg.ObjectMeta.Name) == 0 { + t.Errorf("inventory name not set in Kptfile") + } + if rg.ObjectMeta.Labels[rgfilev1alpha1.RGInventoryIDLabel] != testInventoryID { + t.Errorf("inventory id not set correctly in ResourceGroup: %s", rg.ObjectMeta.Labels[rgfilev1alpha1.RGInventoryIDLabel]) + } + } else if rg != nil { + t.Errorf("inventory shouldn't be set during dryrun") + } + }) + } +} + func TestKptMigrate_retrieveConfigMapInv(t *testing.T) { testCases := map[string]struct { configMap string @@ -298,6 +411,16 @@ func TestKptMigrate_migrateObjs(t *testing.T) { }, isError: false, }, + "Kptfile does not have inventory is valid": { + invObj: kptFile, + objs: []object.ObjMetadata{}, + isError: false, + }, + "One migrate object is valid with inventory in Kptfile": { + invObj: kptFileWithInventory, + objs: []object.ObjMetadata{object.UnstructuredToObjMetadata(pod1)}, + isError: false, + }, } for tn, tc := range testCases { @@ -376,3 +499,13 @@ upstreamLock: ` var inventoryNamespace = "test-namespace" + +var resourceGroupInventory = ` +apiVersion: kpt.dev/v1alpha1 +kind: ResourceGroup +metadata: + name: foo + namespace: test-namespace + labels: + cli-utils.sigs.k8s.io/inventory-id: SSSSSSSSSS-RRRRR +` diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go index 1903ae3c42..aa759915b4 100644 --- a/internal/pkg/pkg.go +++ b/internal/pkg/pkg.go @@ -733,23 +733,28 @@ func ReadRGFile(path, filename string) (*rgfilev1alpha1.ResourceGroup, error) { } defer f.Close() - rg := &rgfilev1alpha1.ResourceGroup{} - c, err := io.ReadAll(f) + rg, err := DecodeRGFile(f) if err != nil { return nil, &RGError{ Path: types.UniquePath(path), Err: err, } } + return rg, nil +} + +// DecodeRGFile converts a string reader into structured a ResourceGroup object. +func DecodeRGFile(in io.Reader) (*rgfilev1alpha1.ResourceGroup, error) { + rg := &rgfilev1alpha1.ResourceGroup{} + c, err := io.ReadAll(in) + if err != nil { + return rg, err + } d := yaml.NewDecoder(bytes.NewBuffer(c)) d.KnownFields(true) if err := d.Decode(rg); err != nil { - return nil, &RGError{ - Path: types.UniquePath(path), - Err: err, - } + return rg, err } return rg, nil - } diff --git a/internal/testutil/pkgbuilder/builder.go b/internal/testutil/pkgbuilder/builder.go index 4e1bfabe3e..5c9bd112da 100644 --- a/internal/testutil/pkgbuilder/builder.go +++ b/internal/testutil/pkgbuilder/builder.go @@ -26,6 +26,7 @@ import ( "text/template" kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" "github.com/stretchr/testify/assert" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -92,6 +93,8 @@ var ( type pkg struct { Kptfile *Kptfile + RGFile *RGFile + resources []resourceInfoWithMutators files map[string]string @@ -99,6 +102,12 @@ type pkg struct { subPkgs []*SubPkg } +// WithRGFile configures the current package to have a resourcegroup file. +func (rp *RootPkg) WithRGFile(rg *RGFile) *RootPkg { + rp.pkg.RGFile = rg + return rp +} + // withKptfile configures the current package to have a Kptfile. Only // zero or one Kptfiles are accepted. func (p *pkg) withKptfile(kf ...*Kptfile) { @@ -300,6 +309,22 @@ func (sp *SubPkg) WithSubPackages(ps ...*SubPkg) *SubPkg { return sp } +// RGFile represents a minimal resourcegroup. +type RGFile struct { + Name, Namespace, ID string +} + +func NewRGFile() *RGFile { + return &RGFile{} +} + +func (rg *RGFile) WithInventory(inv Inventory) *RGFile { + rg.Name = inv.Name + rg.Namespace = inv.Namespace + rg.ID = inv.ID + return rg +} + // Kptfile represents the Kptfile of a package. type Kptfile struct { Upstream *Upstream @@ -476,6 +501,16 @@ func buildPkg(pkgPath string, pkg *pkg, pkgName string, reposInfo ReposInfo) err } } + if pkg.RGFile != nil { + content := buildRGFile(pkg) + + err := ioutil.WriteFile(filepath.Join(pkgPath, rgfilev1alpha1.RGFileName), + []byte(content), 0600) + if err != nil { + return err + } + } + for _, ri := range pkg.resources { m := ri.resourceInfo.manifest r := yaml.MustParse(m) @@ -510,6 +545,23 @@ func buildPkg(pkgPath string, pkg *pkg, pkgName string, reposInfo ReposInfo) err return nil } +// buildRGFile creates a ResourceGroup inventory file. +func buildRGFile(pkg *pkg) string { + tmp := rgfilev1alpha1.ResourceGroup{ResourceMeta: rgfilev1alpha1.DefaultMeta} + tmp.ObjectMeta.Name = pkg.RGFile.Name + tmp.ObjectMeta.Namespace = pkg.RGFile.Namespace + if pkg.RGFile.ID != "" { + tmp.ObjectMeta.Labels = map[string]string{rgfilev1alpha1.RGInventoryIDLabel: pkg.RGFile.ID} + } + + b, err := yaml.MarshalWithOptions(tmp, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle}) + if err != nil { + panic(err) + } + + return string(b) +} + // TODO: Consider using the Kptfile struct for this instead of a template. var kptfileTemplate = `apiVersion: kpt.dev/v1 kind: Kptfile diff --git a/pkg/api/kptfile/v1/types.go b/pkg/api/kptfile/v1/types.go index fd6be7f3bc..ed64e99cf4 100644 --- a/pkg/api/kptfile/v1/types.go +++ b/pkg/api/kptfile/v1/types.go @@ -373,3 +373,10 @@ type Inventory struct { Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` } + +func (i Inventory) IsValid() bool { + // Name and Namespace are required inventory fields, so we check these 2 fields. + // InventoryID is an optional field since we only store it locally if the user + // specifies one. + return i.Name != "" && i.Namespace != "" +} diff --git a/pkg/live/load.go b/pkg/live/load.go index dd6cd4647a..a3665be8ef 100644 --- a/pkg/live/load.go +++ b/pkg/live/load.go @@ -25,6 +25,7 @@ import ( "github.com/GoogleContainerTools/kpt/internal/util/pathutil" "github.com/GoogleContainerTools/kpt/internal/util/strings" kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/kubectl/pkg/cmd/util" "sigs.k8s.io/cli-utils/pkg/common" @@ -51,7 +52,15 @@ func (e *InventoryInfoValidationError) Error() string { type MultipleInventoryInfoError struct{} func (e *MultipleInventoryInfoError) Error() string { - return "multiple Kptfiles contains inventory information" + return "multiple inventory information found in package" +} + +// NoInvInfoError is the error returned if there are no inventory information +// provided in either a stream or locally. +type NoInvInfoError struct{} + +func (e *NoInvInfoError) Error() string { + return "no inventory information was provided within the stream or package" } // Load reads resources either from disk or from an input stream. It filters @@ -60,26 +69,58 @@ func (e *MultipleInventoryInfoError) Error() string { // for inventory information inside Kptfile resources. // It returns the resources in unstructured format and the inventory information. // If no inventory information is found, that is not considered an error here. -func Load(f util.Factory, path string, stdIn io.Reader) ([]*unstructured.Unstructured, kptfilev1.Inventory, error) { +func Load(f util.Factory, path, rgfile string, stdIn io.Reader) ([]*unstructured.Unstructured, kptfilev1.Inventory, error) { if path == "-" { - return loadFromStream(f, stdIn) + return loadFromStream(f, stdIn, rgfile) } - return loadFromDisk(f, path) + return loadFromDisk(f, path, rgfile) } // loadFromStream reads resources from the provided reader and returns the // filtered resources and any inventory information found in Kptfile resources. // If there is more than one Kptfile in the stream with inventory information, that // is considered an error. -func loadFromStream(f util.Factory, r io.Reader) ([]*unstructured.Unstructured, kptfilev1.Inventory, error) { +func loadFromStream(f util.Factory, r io.Reader, rgfile string) ([]*unstructured.Unstructured, kptfilev1.Inventory, error) { var stdInBuf bytes.Buffer tee := io.TeeReader(r, &stdInBuf) + // Check if stream contains inventory info. invInfo, err := readInvInfoFromStream(tee) if err != nil { return nil, kptfilev1.Inventory{}, err } + // Check resourcegroup file for inventory information if file is specified. + if rgfile != "" { + cwd, err := os.Getwd() + if err != nil { + return nil, kptfilev1.Inventory{}, err + } + + diskInv, err := readInvInfoFromDisk(cwd, rgfile) + if err != nil { + return nil, kptfilev1.Inventory{}, err + } + + if diskInv.IsValid() && invInfo.IsValid() { + return nil, kptfilev1.Inventory{}, &MultipleInventoryInfoError{} + } + + if !diskInv.IsValid() && !invInfo.IsValid() { + return nil, kptfilev1.Inventory{}, &NoInvInfoError{} + } + + if diskInv.IsValid() { + invInfo = diskInv + } + + } + + // Stream does not contain a valid inventory and no local inventory does not exist, or is not valid. + if !invInfo.IsValid() { + return nil, kptfilev1.Inventory{}, &NoInvInfoError{} + } + ro, err := toReaderOptions(f) if err != nil { return nil, kptfilev1.Inventory{}, err @@ -98,6 +139,7 @@ func loadFromStream(f util.Factory, r io.Reader) ([]*unstructured.Unstructured, func readInvInfoFromStream(in io.Reader) (kptfilev1.Inventory, error) { invFilter := &InventoryFilter{} + rgFilter := &RGFilter{} if err := (&kio.Pipeline{ Inputs: []kio.Reader{ &kio.ByteReader{ @@ -107,18 +149,31 @@ func readInvInfoFromStream(in io.Reader) (kptfilev1.Inventory, error) { }, Filters: []kio.Filter{ kio.FilterAll(invFilter), + kio.FilterAll(rgFilter), }, }).Execute(); err != nil { return kptfilev1.Inventory{}, err } - if len(invFilter.Inventories) > 1 { + if len(invFilter.Inventories) > 1 || + len(rgFilter.Inventories) > 1 || + (len(invFilter.Inventories) > 0 && len(rgFilter.Inventories) > 0) { return kptfilev1.Inventory{}, &MultipleInventoryInfoError{} } if len(invFilter.Inventories) == 1 { return *invFilter.Inventories[0], nil } + + if len(rgFilter.Inventories) == 1 { + invID := rgFilter.Inventories[0].Labels[rgfilev1alpha1.RGInventoryIDLabel] + return kptfilev1.Inventory{ + Name: rgFilter.Inventories[0].Name, + Namespace: rgFilter.Inventories[0].Namespace, + InventoryID: invID, + }, nil + } + return kptfilev1.Inventory{}, nil } @@ -126,8 +181,8 @@ func readInvInfoFromStream(in io.Reader) (kptfilev1.Inventory, error) { // It returns the filtered resources and any inventory information found in // Kptfile resources. // Only the Kptfile in the root directory will be checked for inventory information. -func loadFromDisk(f util.Factory, path string) ([]*unstructured.Unstructured, kptfilev1.Inventory, error) { - invInfo, err := readInvInfoFromDisk(path) +func loadFromDisk(f util.Factory, path, rgfile string) ([]*unstructured.Unstructured, kptfilev1.Inventory, error) { + invInfo, err := readInvInfoFromDisk(path, rgfile) if err != nil { return nil, kptfilev1.Inventory{}, err } @@ -148,7 +203,7 @@ func loadFromDisk(f util.Factory, path string) ([]*unstructured.Unstructured, kp return objs, invInfo, nil } -func readInvInfoFromDisk(path string) (kptfilev1.Inventory, error) { +func readInvInfoFromDisk(path, rgfile string) (kptfilev1.Inventory, error) { absPath, _, err := pathutil.ResolveAbsAndRelPaths(path) if err != nil { return kptfilev1.Inventory{}, err @@ -158,12 +213,45 @@ func readInvInfoFromDisk(path string) (kptfilev1.Inventory, error) { return kptfilev1.Inventory{}, err } + // Read Kptfile for inventory. We ignore errors if no local Kptfile as that could + // be provided via STDIN. kf, err := p.Kptfile() - if err != nil && errors.Is(err, os.ErrNotExist) { - return kptfilev1.Inventory{}, nil + if rgfile == "" { + if err != nil && errors.Is(err, os.ErrNotExist) { + return kptfilev1.Inventory{}, nil + } + if err != nil { + return kptfilev1.Inventory{}, err + } } - if err != nil { - return kptfilev1.Inventory{}, err + + // Check if resourcegroup exists and use inventory info from there if provided. + if rgfile != "" { + rg, err := p.ReadRGFile(rgfile) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return kptfilev1.Inventory{}, nil + } + + // Ensure we only have at most 1 instance of an inventory. + if kf != nil { + if kf.Inventory == nil && rg == nil { + return kptfilev1.Inventory{}, nil + } + + if kf.Inventory != nil && rg != nil { + return kptfilev1.Inventory{}, &MultipleInventoryInfoError{} + } + + if kf.Inventory != nil { + return *kf.Inventory, nil + } + } + + return kptfilev1.Inventory{ + Name: rg.ObjectMeta.Name, + Namespace: rg.ObjectMeta.Namespace, + InventoryID: rg.ObjectMeta.Labels[rgfilev1alpha1.RGInventoryIDLabel], + }, nil } if kf.Inventory == nil { @@ -199,6 +287,30 @@ func (i *InventoryFilter) Filter(object *yaml.RNode) (*yaml.RNode, error) { return object, nil } +// RGFilter is an implementation of the yaml.Filter interface +// that extracts inventory information from resourcegroup objects. +type RGFilter struct { + Inventories []*rgfilev1alpha1.ResourceGroup +} + +func (r *RGFilter) Filter(object *yaml.RNode) (*yaml.RNode, error) { + if object.GetApiVersion() != rgfilev1alpha1.RGFileAPIVersion || + object.GetKind() != rgfilev1alpha1.RGFileKind { + return object, nil + } + + s, err := object.String() + if err != nil { + return object, err + } + rg, err := pkg.DecodeRGFile(bytes.NewBufferString(s)) + if err != nil { + return nil, err + } + r.Inventories = append(r.Inventories, rg) + return object, nil +} + // toReaderOptions returns the readerOptions for a factory. func toReaderOptions(f util.Factory) (manifestreader.ReaderOptions, error) { namespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() @@ -245,14 +357,6 @@ func validateInventory(inventory kptfilev1.Inventory) error { Reason: "\"inventory.namespace\" must not be empty", }) } - if inventory.InventoryID == "" { - violations = append(violations, errors.Violation{ - Field: "inventoryID", - Value: inventory.InventoryID, - Type: errors.Missing, - Reason: "\"inventory.inventoryID\" must not be empty", - }) - } if len(violations) > 0 { return &InventoryInfoValidationError{ ValidationError: errors.ValidationError{ diff --git a/pkg/live/load_test.go b/pkg/live/load_test.go index 4562d10a08..f57be37f28 100644 --- a/pkg/live/load_test.go +++ b/pkg/live/load_test.go @@ -17,11 +17,15 @@ package live import ( "bytes" "os" + "path/filepath" "sort" + "strings" "testing" + "github.com/GoogleContainerTools/kpt/internal/testutil" "github.com/GoogleContainerTools/kpt/internal/testutil/pkgbuilder" kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/runtime/schema" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" @@ -37,6 +41,7 @@ func TestLoad_LocalDisk(t *testing.T) { expectedObjs object.ObjMetadataSet expectedInv kptfile.Inventory expectedErrMsg string + rgFile string }{ "no Kptfile in root package": { pkg: pkgbuilder.NewRootPkg(). @@ -160,6 +165,24 @@ func TestLoad_LocalDisk(t *testing.T) { InventoryID: "foo-bar", }, }, + "Inventory information taken from resourcegroup": { + pkg: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(), + ).WithRGFile(pkgbuilder.NewRGFile().WithInventory(pkgbuilder.Inventory{ + Name: "foo", + Namespace: "bar", + ID: "foo-bar"}, + )), + namespace: "foo", + expectedObjs: []object.ObjMetadata{}, + expectedInv: kptfile.Inventory{ + Name: "foo", + Namespace: "bar", + InventoryID: "foo-bar", + }, + rgFile: "resourcegroup.yaml", + }, } for tn, tc := range testCases { @@ -173,7 +196,7 @@ func TestLoad_LocalDisk(t *testing.T) { }() var buf bytes.Buffer - objs, inv, err := Load(tf, dir, &buf) + objs, inv, err := Load(tf, dir, tc.rgFile, &buf) if tc.expectedErrMsg != "" { if !assert.Error(t, err) { @@ -202,27 +225,27 @@ func TestLoad_StdIn(t *testing.T) { expectedObjs object.ObjMetadataSet expectedInv kptfile.Inventory expectedErrMsg string + rgFile string + rgInStream bool }{ - "no Kptfile among the resources": { + "no inventory among the resources": { pkg: pkgbuilder.NewRootPkg(). WithKptfile( pkgbuilder.NewKptfile(), ). WithFile("deployment.yaml", deploymentA), - namespace: "foo", - expectedObjs: []object.ObjMetadata{ - { - GroupKind: schema.GroupKind{ - Group: "apps", - Kind: "Deployment", - }, - Name: "test-deployment", - Namespace: "foo", - }, - }, + expectedErrMsg: "no inventory information was provided within the stream", }, "missing namespace for namespace scoped resources are defaulted": { pkg: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithInventory(pkgbuilder.Inventory{ + Name: "foo", + Namespace: "bar", + ID: "foo-bar", + }), + ). WithFile("cm.yaml", configMap), namespace: "foo", expectedObjs: []object.ObjMetadata{ @@ -234,6 +257,11 @@ func TestLoad_StdIn(t *testing.T) { Namespace: "foo", }, }, + expectedInv: kptfile.Inventory{ + Name: "foo", + Namespace: "bar", + InventoryID: "foo-bar", + }, }, "inventory info is taken from the Kptfile": { pkg: pkgbuilder.NewRootPkg(). @@ -297,7 +325,128 @@ func TestLoad_StdIn(t *testing.T) { ), ), namespace: "foo", - expectedErrMsg: "multiple Kptfiles contains inventory information", + expectedErrMsg: "multiple inventory information found in package", + }, + "Multiple inventories, stdin and local resourcegroup, is an error": { + pkg: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithInventory(pkgbuilder.Inventory{ + Name: "foo", + Namespace: "bar", + ID: "foo-bar", + }), + ). + WithRGFile(pkgbuilder.NewRGFile().WithInventory(pkgbuilder.Inventory{ + Name: "foo", + Namespace: "bar", + ID: "foo-bar", + }, + )), + expectedErrMsg: "multiple inventory information found in package", + rgFile: rgfilev1alpha1.RGFileName, + }, + "Inventory using local resourcegroup file": { + pkg: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(), + ). + WithFile("cm.yaml", configMap). + WithRGFile(pkgbuilder.NewRGFile().WithInventory(pkgbuilder.Inventory{ + Name: "foo", + Namespace: "bar", + ID: "foo-bar", + }, + )), + namespace: "foo", + expectedInv: kptfile.Inventory{ + Name: "foo", + Namespace: "bar", + InventoryID: "foo-bar", + }, + expectedObjs: []object.ObjMetadata{ + { + GroupKind: schema.GroupKind{ + Kind: "ConfigMap", + }, + Name: "cm", + Namespace: "foo", + }, + }, + rgFile: rgfilev1alpha1.RGFileName, + }, + "Inventory using STDIN resourcegroup file": { + pkg: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(), + ). + WithFile("cm.yaml", configMap). + WithRGFile(pkgbuilder.NewRGFile().WithInventory(pkgbuilder.Inventory{ + Name: "foo", + Namespace: "bar", + ID: "foo-bar", + }, + )), + namespace: "foo", + expectedInv: kptfile.Inventory{ + Name: "foo", + Namespace: "bar", + InventoryID: "foo-bar", + }, + expectedObjs: []object.ObjMetadata{ + { + GroupKind: schema.GroupKind{ + Kind: "ConfigMap", + }, + Name: "cm", + Namespace: "foo", + }, + }, + rgInStream: true, + }, + "Multiple inventories using STDIN resourcegroup and Kptfile is error": { + pkg: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithInventory(pkgbuilder.Inventory{ + Name: "foo", + Namespace: "bar", + ID: "foo-bar", + }), + ). + WithFile("cm.yaml", configMap). + WithRGFile(pkgbuilder.NewRGFile().WithInventory(pkgbuilder.Inventory{ + Name: "foo", + Namespace: "bar", + ID: "foo-bar", + }, + )), + expectedErrMsg: "multiple inventory information found in package", + rgInStream: true, + }, + "Non-valid inventory using STDIN Kptfile is error": { + pkg: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(). + WithInventory(pkgbuilder.Inventory{ + Name: "foo", + }), + ). + WithFile("cm.yaml", configMap), + expectedErrMsg: "no inventory information was provided within the stream", + }, + "Non-valid inventory in resourcegroup is error": { + pkg: pkgbuilder.NewRootPkg(). + WithKptfile( + pkgbuilder.NewKptfile(), + ). + WithFile("cm.yaml", configMap). + WithRGFile(pkgbuilder.NewRGFile().WithInventory(pkgbuilder.Inventory{ + Name: "foo", + }, + )), + expectedErrMsg: "no inventory information was provided within the stream or package", + rgFile: rgfilev1alpha1.RGFileName, }, } @@ -311,6 +460,9 @@ func TestLoad_StdIn(t *testing.T) { _ = os.RemoveAll(dir) }() + revert := testutil.Chdir(t, dir) + defer revert() + var buf bytes.Buffer err := (&kio.Pipeline{ Inputs: []kio.Reader{ @@ -320,6 +472,14 @@ func TestLoad_StdIn(t *testing.T) { MatchFilesGlob: append([]string{kptfile.KptFileName}, kio.DefaultMatch...), IncludeSubpackages: true, WrapBareSeqNode: true, + FileSkipFunc: func(rp string) bool { + // No skipping if we don't have a resourcegroup file, or we stream it in STDIN. + if tc.rgFile == "" || tc.rgInStream { + return false + } + + return strings.Contains(rp, tc.rgFile) + }, }, }, Outputs: []kio.Writer{ @@ -332,7 +492,15 @@ func TestLoad_StdIn(t *testing.T) { t.FailNow() } - objs, inv, err := Load(tf, "-", &buf) + if tc.rgFile != "" { + os.Remove(filepath.Join(dir, kptfile.KptFileName)) + } + + if tc.rgInStream { + os.Remove(filepath.Join(dir, tc.rgFile)) + } + + objs, inv, err := Load(tf, "-", tc.rgFile, &buf) if tc.expectedErrMsg != "" { if !assert.Error(t, err) { @@ -376,12 +544,12 @@ func TestValidateInventory(t *testing.T) { expectErr: true, expectedErrorFields: []string{"name"}, }, - "inventory without id or namespace doesn't validate": { + "inventory namespace doesn't validate": { inventory: kptfile.Inventory{ Name: "foo", }, expectErr: true, - expectedErrorFields: []string{"namespace", "inventoryID"}, + expectedErrorFields: []string{"namespace"}, }, } diff --git a/pkg/live/rgpath.go b/pkg/live/rgpath.go index e850bfade7..7e2c9576b0 100644 --- a/pkg/live/rgpath.go +++ b/pkg/live/rgpath.go @@ -8,6 +8,7 @@ import ( "github.com/GoogleContainerTools/kpt/internal/pkg" "github.com/GoogleContainerTools/kpt/internal/util/pathutil" + rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/cli-utils/pkg/manifestreader" "sigs.k8s.io/kustomize/kyaml/filesys" @@ -70,6 +71,13 @@ func (r *ResourceGroupPathManifestReader) Read() ([]*unstructured.Unstructured, if err != nil { return objs, err } + + // Skip if current file is a ResourceGroup resource. We do not want to apply/delete any ResourceGroup CRs when we + // run any `kpt live` commands on a package. Instead, we have specific logic in place for handling ResourceGroups in + // the live cluster. + if u.GetKind() == rgfilev1alpha1.RGFileKind && u.GetAPIVersion() == rgfilev1alpha1.DefaultMeta.APIVersion { + continue + } objs = append(objs, u) } diff --git a/pkg/live/rgstream.go b/pkg/live/rgstream.go index 2ea2596618..4dafb60d89 100644 --- a/pkg/live/rgstream.go +++ b/pkg/live/rgstream.go @@ -8,6 +8,7 @@ import ( "io" kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/cli-utils/pkg/manifestreader" @@ -23,6 +24,10 @@ var ( Group: kptfilev1.KptFileGroup, Kind: kptfilev1.KptFileKind, }, + { + Group: rgfilev1alpha1.RGFileGroup, + Kind: rgfilev1alpha1.RGFileKind, + }, } ) diff --git a/pkg/test/runner/runner.go b/pkg/test/runner/runner.go index e9d3a27bb0..5bccd10bdf 100644 --- a/pkg/test/runner/runner.go +++ b/pkg/test/runner/runner.go @@ -73,9 +73,11 @@ func NewRunner(t *testing.T, testCase TestCase, c string) (*Runner, error) { } kptBin, err := getKptBin() if err != nil { - return nil, fmt.Errorf("failed to find kpt binary: %w", err) + t.Logf("failed to find kpt binary: %v", err) + } + if kptBin != "" { + t.Logf("Using kpt binary: %s", kptBin) } - t.Logf("Using kpt binary: %s", kptBin) return &Runner{ pkgName: filepath.Base(testCase.Path), testCase: testCase, diff --git a/site/installation/README.md b/site/installation/README.md index ed410bf943..37be11f761 100644 --- a/site/installation/README.md +++ b/site/installation/README.md @@ -93,7 +93,7 @@ Use one of the kpt docker images. ### `kpt` ```shell -$ docker run gcr.io/kpt-dev/kpt:v1.0.0-beta.12 version +$ docker run gcr.io/kpt-dev/kpt:v1.0.0-beta.13 version ``` ### `kpt-gcloud` @@ -101,7 +101,7 @@ $ docker run gcr.io/kpt-dev/kpt:v1.0.0-beta.12 version An image which includes kpt based upon the Google [cloud-sdk] alpine image. ```shell -$ docker run gcr.io/kpt-dev/kpt-gcloud:v1.0.0-beta.12 version +$ docker run gcr.io/kpt-dev/kpt-gcloud:v1.0.0-beta.13 version ``` ## Source @@ -124,8 +124,8 @@ $ kpt version https://console.cloud.google.com/gcr/images/kpt-dev/GLOBAL/kpt-gcloud?gcrImageListsize=30 [cloud-sdk]: https://github.com/GoogleCloudPlatform/cloud-sdk-docker [linux]: - https://github.com/GoogleContainerTools/kpt/releases/download/v1.0.0-beta.12/kpt_linux_amd64 + https://github.com/GoogleContainerTools/kpt/releases/download/v1.0.0-beta.13/kpt_linux_amd64 [darwin]: - https://github.com/GoogleContainerTools/kpt/releases/download/v1.0.0-beta.12/kpt_darwin_amd64 + https://github.com/GoogleContainerTools/kpt/releases/download/v1.0.0-beta.13/kpt_darwin_amd64 [migration guide]: /installation/migration [bash-completion]: https://github.com/scop/bash-completion#installation diff --git a/thirdparty/cli-utils/status/cmdstatus.go b/thirdparty/cli-utils/status/cmdstatus.go index d1fa90ad1c..029ac5e63f 100644 --- a/thirdparty/cli-utils/status/cmdstatus.go +++ b/thirdparty/cli-utils/status/cmdstatus.go @@ -85,6 +85,7 @@ type Runner struct { pollUntil string timeout time.Duration output string + rgFile string pollerFactoryFunc func(util.Factory) (poller.Poller, error) } @@ -124,7 +125,7 @@ func (r *Runner) runE(c *cobra.Command, args []string) error { } } - _, inv, err := live.Load(r.factory, path, c.InOrStdin()) + _, inv, err := live.Load(r.factory, path, r.rgFile, c.InOrStdin()) if err != nil { return err }