diff --git a/internal/cmdliveinit/cmdliveinit.go b/internal/cmdliveinit/cmdliveinit.go index a313dad390..75b61779f3 100644 --- a/internal/cmdliveinit/cmdliveinit.go +++ b/internal/cmdliveinit/cmdliveinit.go @@ -6,8 +6,11 @@ package cmdliveinit import ( "context" "crypto/sha1" + goerrors "errors" "fmt" + "io/ioutil" "os" + "path/filepath" "strconv" "strings" "time" @@ -16,9 +19,11 @@ import ( "github.com/GoogleContainerTools/kpt/internal/errors" "github.com/GoogleContainerTools/kpt/internal/pkg" "github.com/GoogleContainerTools/kpt/internal/printer" + "github.com/GoogleContainerTools/kpt/internal/types" "github.com/GoogleContainerTools/kpt/internal/util/attribution" "github.com/GoogleContainerTools/kpt/internal/util/pathutil" kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" "github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -26,6 +31,7 @@ import ( "sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/cli-utils/pkg/config" "sigs.k8s.io/kustomize/kyaml/filesys" + "sigs.k8s.io/kustomize/kyaml/yaml" ) const defaultInventoryName = "inventory" @@ -38,6 +44,24 @@ func (i *InvExistsError) Error() string { return "inventory information already set for package" } +// InvInRGExistsError defines new error when the inventory +// values have already been set on the ResourceGroup file and we will warn +// the user to migrate rather than init. This is part of kpt live STDIN work. +type InvInRGExistsError struct{} + +func (i *InvInRGExistsError) Error() string { + return "inventory information already set for package" +} + +// InvInKfExistsError defines new error when the inventory +// values have already been set on the Kptfile and we will warn +// the user to migrate rather than init. This is part of kpt live STDIN work. +type InvInKfExistsError struct{} + +func (i *InvInKfExistsError) Error() string { + return "inventory information already set within Kptfile for package" +} + func NewRunner(ctx context.Context, factory k8scmdutil.Factory, ioStreams genericclioptions.IOStreams) *Runner { r := &Runner{ @@ -76,6 +100,7 @@ type Runner struct { Force bool // Set inventory values even if already set in Kptfile Name string // Inventory object name namespace string // Inventory object namespace + RGFile string // resourcegroup object filepath InventoryID string // Inventory object unique identifier label Quiet bool // Output message during initialization } @@ -112,6 +137,7 @@ func (r *Runner) runE(_ *cobra.Command, args []string) error { Quiet: r.Quiet, Name: r.Name, InventoryID: r.InventoryID, + RGFileName: r.RGFile, Force: r.Force, }).Run(r.ctx) if err != nil { @@ -129,12 +155,22 @@ type ConfigureInventoryInfo struct { Name string InventoryID string + RGFileName string Force bool } // Run updates the inventory info in the package given by the Path. func (c *ConfigureInventoryInfo) Run(ctx context.Context) 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 c.RGFileName != "" { + return c.runLiveInitWithRGFile(ctx) + } + const op errors.Op = "cmdliveinit.Run" pr := printer.FromContextOrDie(ctx) @@ -189,6 +225,119 @@ func (c *ConfigureInventoryInfo) Run(ctx context.Context) error { return nil } +// func runLiveInitWithRGFile is a modified version of ConfigureInventoryInfo.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 (c *ConfigureInventoryInfo) runLiveInitWithRGFile(ctx context.Context) error { + const op errors.Op = "cmdliveinit.runLiveInitWithRGFile" + pr := printer.FromContextOrDie(ctx) + + namespace, err := config.FindNamespace(c.Factory.ToRawKubeConfigLoader(), c.Pkg.UniquePath.String()) + if err != nil { + return errors.E(op, c.Pkg.UniquePath, err) + } + namespace = strings.TrimSpace(namespace) + if !c.Quiet { + pr.Printf("initializing ResourceGroup inventory info (namespace: %s)...", namespace) + } + + // Autogenerate the name if it is not provided through the flag. + if c.Name == "" { + randomSuffix := common.RandomStr() + c.Name = fmt.Sprintf("%s-%s", defaultInventoryName, randomSuffix) + } + + // Finally, create a ResourceGroup containing the inventory information. + err = createRGFile(c.Pkg, &kptfilev1.Inventory{ + Namespace: namespace, + Name: c.Name, + InventoryID: c.InventoryID, + }, c.RGFileName, c.Force) + if !c.Quiet { + if err == nil { + pr.Printf("success\n") + } else { + pr.Printf("failed\n") + } + } + if err != nil { + return errors.E(op, c.Pkg.UniquePath, err) + } + // add metrics annotation to package resources to track the usage as the resources + // will be applied using kpt live group + at := attribution.Attributor{PackagePaths: []string{c.Pkg.UniquePath.String()}, CmdGroup: "live"} + at.Process() + return nil +} + +// createRGFile fills in the inventory object values into the resourcegroup object and writes to file storage. +func createRGFile(p *pkg.Pkg, inv *kptfilev1.Inventory, filename string, force bool) error { + const op errors.Op = "cmdliveinit.createRGFile" + // Read the resourcegroup object io io.dir + rg, err := p.ReadRGFile(filename) + if err != nil && !goerrors.Is(err, os.ErrNotExist) { + return errors.E(op, p.UniquePath, err) + } + + // Read the Kptfile to ensure that inventory information is not in Kptfile either. + kf, err := p.Kptfile() + if err != nil { + return errors.E(op, p.UniquePath, err) + } + // Validate the inventory values don't exist in Kptfile. + isEmpty := kptfileInventoryEmpty(kf.Inventory) + if !isEmpty && !force { + return errors.E(op, p.UniquePath, &InvInKfExistsError{}) + } + // Set the Kptfile inventory to be nil if we force write to resourcegroup instead. + kf.Inventory = nil + + // Validate the inventory values don't already exist in Resourcegroup. + if rg != nil && !force { + return errors.E(op, p.UniquePath, &InvExistsError{}) + } + // Initialize new resourcegroup object, as rg should have been nil. + rg = &rgfilev1alpha1.ResourceGroup{ResourceMeta: rgfilev1alpha1.DefaultMeta} + // // Finally, set the inventory parameters in the ResourceGroup object and write it. + rg.Name = inv.Name + rg.Namespace = inv.Namespace + if inv.InventoryID != "" { + rg.Labels = map[string]string{rgfilev1alpha1.RGInventoryIDLabel: inv.InventoryID} + } + if err := writeRGFile(p.UniquePath.String(), rg, filename); err != nil { + return errors.E(op, p.UniquePath, err) + } + + // Rewrite Kptfile without inventory existing Kptfile contains inventory info. This + // is required when a user appends the force flag. + if !isEmpty { + if err := kptfileutil.WriteFile(p.UniquePath.String(), kf); err != nil { + return errors.E(op, p.UniquePath, err) + } + } + + return nil +} + +// writeRGFile writes a ResourceGroup inventory to local disk. +func writeRGFile(dir string, rg *rgfilev1alpha1.ResourceGroup, filename string) error { + const op errors.Op = "cmdliveinit.writeRGFile" + b, err := yaml.MarshalWithOptions(rg, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle}) + if err != nil { + return err + } + if _, err := os.Stat(filepath.Join(dir, filename)); err != nil && !goerrors.Is(err, os.ErrNotExist) { + return errors.E(op, errors.IO, types.UniquePath(dir), err) + } + + // fyi: perm is ignored if the file already exists + err = ioutil.WriteFile(filepath.Join(dir, filename), b, 0600) + if err != nil { + return errors.E(op, errors.IO, types.UniquePath(dir), err) + } + return nil +} + // Run fills in the inventory object values into the Kptfile. func updateKptfile(p *pkg.Pkg, inv *kptfilev1.Inventory, force bool) error { const op errors.Op = "cmdliveinit.updateKptfile" diff --git a/internal/cmdliveinit/cmdliveinit_test.go b/internal/cmdliveinit/cmdliveinit_test.go index 043ed1172d..c909e04d87 100644 --- a/internal/cmdliveinit/cmdliveinit_test.go +++ b/internal/cmdliveinit/cmdliveinit_test.go @@ -14,6 +14,7 @@ import ( "github.com/GoogleContainerTools/kpt/internal/printer/fake" "github.com/GoogleContainerTools/kpt/internal/testutil" kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" "github.com/stretchr/testify/assert" "k8s.io/cli-runtime/pkg/genericclioptions" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" @@ -59,6 +60,14 @@ inventory: var testTime = time.Unix(5555555, 66666666) +var resourceGroupInventory = ` +apiVersion: kpt.dev/v1alpha1 +kind: ResourceGroup +metadata: + name: foo + namespace: test-namespace +` + func TestCmd_generateID(t *testing.T) { testCases := map[string]struct { namespace string @@ -131,6 +140,8 @@ func TestCmd_Run_NoKptfile(t *testing.T) { func TestCmd_Run(t *testing.T) { testCases := map[string]struct { kptfile string + resourcegroup string + rgfilename string name string namespace string inventoryID string @@ -162,15 +173,47 @@ func TestCmd_Run(t *testing.T) { InventoryID: "my-inv-id", }, }, + "Provided values are used with custom resourcegroup filename": { + kptfile: kptFile, + rgfilename: "custom-rg.yaml", + name: "my-pkg", + namespace: "my-ns", + inventoryID: "my-inv-id", + expectedInventory: kptfilev1.Inventory{ + Namespace: "my-ns", + Name: "my-pkg", + InventoryID: "my-inv-id", + }, + }, "Kptfile with inventory already set is error": { kptfile: kptFileWithInventory, name: inventoryName, namespace: inventoryNamespace, inventoryID: inventoryID, force: false, + expectedErrorMsg: "inventory information already set", + }, + "ResourceGroup with inventory already set is error": { + kptfile: kptFile, + resourcegroup: resourceGroupInventory, + rgfilename: "resourcegroup.yaml", + name: inventoryName, + namespace: inventoryNamespace, + inventoryID: inventoryID, + force: false, expectedErrorMsg: "inventory information already set for package", }, - "The force flag allows changing inventory information even if already set": { + "ResourceGroup with inventory and Kptfile with inventory already set is error": { + kptfile: kptFileWithInventory, + resourcegroup: resourceGroupInventory, + rgfilename: "resourcegroup.yaml", + name: inventoryName, + namespace: inventoryNamespace, + inventoryID: inventoryID, + force: false, + expectedErrorMsg: "inventory information already set", + }, + "The force flag allows changing inventory information even if already set in Kptfile": { kptfile: kptFileWithInventory, name: inventoryName, namespace: inventoryNamespace, @@ -182,6 +225,20 @@ func TestCmd_Run(t *testing.T) { InventoryID: inventoryID, }, }, + "The force flag allows changing inventory information even if already set in ResourceGroup": { + kptfile: kptFile, + resourcegroup: resourceGroupInventory, + rgfilename: "resourcegroup.yaml", + name: inventoryName, + namespace: inventoryNamespace, + inventoryID: inventoryID, + force: true, + expectedInventory: kptfilev1.Inventory{ + Namespace: inventoryNamespace, + Name: inventoryName, + InventoryID: inventoryID, + }, + }, } for tn, tc := range testCases { @@ -199,11 +256,21 @@ func TestCmd_Run(t *testing.T) { t.FailNow() } + // Create ResourceGroup file if testing the STDIN feature. + if tc.resourcegroup != "" && tc.rgfilename != "" { + err := ioutil.WriteFile(filepath.Join(w.WorkspaceDirectory, tc.rgfilename), + []byte(tc.resourcegroup), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + } + revert := testutil.Chdir(t, w.WorkspaceDirectory) defer revert() runner := NewRunner(fake.CtxWithDefaultPrinter(), tf, ioStreams) runner.namespace = tc.namespace + runner.RGFile = tc.rgfilename args := []string{ "--name", tc.name, "--inventory-id", tc.inventoryID, @@ -224,17 +291,39 @@ func TestCmd_Run(t *testing.T) { return } - // Otherwise, validate the kptfile values + // Otherwise, validate the kptfile values and/or resourcegroup values. + var actualInv kptfilev1.Inventory assert.NoError(t, err) kf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, w.WorkspaceDirectory) assert.NoError(t, err) - if !assert.NotNil(t, kf.Inventory) { - t.FailNow() + + switch tc.rgfilename { + case "": + if !assert.NotNil(t, kf.Inventory) { + t.FailNow() + } + actualInv = *kf.Inventory + default: + // Check resourcegroup file if testing the STDIN feature. + rg, err := pkg.ReadRGFile(w.WorkspaceDirectory, tc.rgfilename) + assert.NoError(t, err) + if !assert.NotNil(t, rg) { + t.FailNow() + } + + // Convert resourcegroup inventory back to Kptfile structure so we can share assertion + // logic for Kptfile inventory and ResourceGroup inventory structure. + actualInv = kptfilev1.Inventory{ + Name: rg.Name, + Namespace: rg.Namespace, + InventoryID: rg.Labels[rgfilev1alpha1.RGInventoryIDLabel], + } } - actualInv := *kf.Inventory + expectedInv := tc.expectedInventory assertInventoryName(t, expectedInv.Name, actualInv.Name) assert.Equal(t, expectedInv.Namespace, actualInv.Namespace) + if tc.expectAutoGenID { assertGenInvID(t, actualInv.Name, actualInv.Namespace, actualInv.InventoryID) } else { diff --git a/internal/errors/resolver/live.go b/internal/errors/resolver/live.go index d01c6dbddd..3e9ad18c93 100644 --- a/internal/errors/resolver/live.go +++ b/internal/errors/resolver/live.go @@ -60,6 +60,16 @@ Error: The ResourceGroup CRD was not found in the cluster. Please install it eit //nolint:lll invInfoAlreadyExistsMsg = ` Error: Inventory information has already been added to the package Kptfile. Changing it after a package has been applied to the cluster can lead to undesired results. Use the --force flag to suppress this error. +` + + //nolint:lll + invInfoInRGAlreadyExistsMsg = ` +Error: Inventory information has already been added to the package ResourceGroup object. Changing it after a package has been applied to the cluster can lead to undesired results. Use the --force flag to suppress this error. +` + + //nolint:lll + invInfoInKfAlreadyExistsMsg = ` +Error: Inventory information has already been added to the package Kptfile object. Please consider migrating to a standalone resourcegroup object using the 'kpt live migrate' command. ` multipleInvInfoMsg = ` @@ -136,6 +146,24 @@ func (*liveErrorResolver) Resolve(err error) (ResolvedResult, bool) { }, true } + var invInfoInRGAlreadyExistsError *cmdliveinit.InvInRGExistsError + if errors.As(err, &invInfoInRGAlreadyExistsError) { + return ResolvedResult{ + Message: ExecuteTemplate(invInfoInRGAlreadyExistsMsg, map[string]interface{}{ + "err": *invInfoInRGAlreadyExistsError, + }), + }, true + } + + var invInKfExistsError *cmdliveinit.InvInKfExistsError + if errors.As(err, &invInKfExistsError) { + return ResolvedResult{ + Message: ExecuteTemplate(invInfoInKfAlreadyExistsMsg, map[string]interface{}{ + "err": *invInKfExistsError, + }), + }, true + } + var multipleInvInfoError *live.MultipleInventoryInfoError if errors.As(err, &multipleInvInfoError) { return ResolvedResult{ diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go index de785e58b0..1903ae3c42 100644 --- a/internal/pkg/pkg.go +++ b/internal/pkg/pkg.go @@ -29,6 +29,7 @@ import ( "github.com/GoogleContainerTools/kpt/internal/util/git" "github.com/GoogleContainerTools/kpt/internal/util/pathutil" kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/kubectl/pkg/util/slice" "sigs.k8s.io/kustomize/kyaml/filesys" @@ -101,6 +102,21 @@ func (e *UnknownKptfileResourceError) Error() string { return fmt.Sprintf("unknown resource type %q found in Kptfile", e.GVK.String()) } +// RGError is an implementation of the error interface that is returned whenever +// kpt encounters errors reading a resourcegroup object file. +type RGError struct { + Path types.UniquePath + Err error +} + +func (rg *RGError) Error() string { + return fmt.Sprintf("error reading ResourceGroup file at %q: %s", rg.Path.String(), rg.Err.Error()) +} + +func (rg *RGError) Unwrap() error { + return rg.Err +} + // Pkg represents a kpt package with a one-to-one mapping to a directory on the local filesystem. type Pkg struct { // fsys represents the FileSystem of the package, it may or may not be FileSystem on disk @@ -124,6 +140,9 @@ type Pkg struct { // A package can contain zero or one Kptfile meta resource. // A nil value represents an implicit package. kptfile *kptfilev1.KptFile + + // A package can contain zero or one ResourceGroup object. + rgFile *rgfilev1alpha1.ResourceGroup } // New returns a pkg given an absolute OS-defined path. @@ -687,3 +706,50 @@ func SetPkgPathAnnotation(rn *yaml.RNode, pkgPath types.UniquePath) error { func RemovePkgPathAnnotation(rn *yaml.RNode) error { return rn.PipeE(yaml.ClearAnnotation(pkgPathAnnotation)) } + +// ReadRGFile returns the resourcegroup object by lazy loading it from the filesytem. +func (p *Pkg) ReadRGFile(filename string) (*rgfilev1alpha1.ResourceGroup, error) { + if p.rgFile == nil { + rg, err := ReadRGFile(p.UniquePath.String(), filename) + if err != nil { + return nil, err + } + p.rgFile = rg + } + return p.rgFile, nil +} + +// TODO(rquitales): Consolidate both Kptfile and ResourceGroup file reading functions to use +// shared logic/function. + +// ReadRGFile reads the KptFile in the given pkg. +func ReadRGFile(path, filename string) (*rgfilev1alpha1.ResourceGroup, error) { + f, err := os.Open(filepath.Join(path, filename)) + if err != nil { + return nil, &RGError{ + Path: types.UniquePath(path), + Err: err, + } + } + defer f.Close() + + rg := &rgfilev1alpha1.ResourceGroup{} + c, err := io.ReadAll(f) + if err != nil { + return nil, &RGError{ + Path: types.UniquePath(path), + Err: 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, nil + +} diff --git a/pkg/api/resourcegroup/v1alpha1/types.go b/pkg/api/resourcegroup/v1alpha1/types.go index f3ea6ccf5d..0d9741ffdd 100644 --- a/pkg/api/resourcegroup/v1alpha1/types.go +++ b/pkg/api/resourcegroup/v1alpha1/types.go @@ -18,6 +18,7 @@ package v1alpha1 import ( + "sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -27,6 +28,8 @@ const ( RGFileGroup = "kpt.dev" RGFileVersion = "v1alpha1" RGFileAPIVersion = RGFileGroup + "/" + RGFileVersion + // RGInventoryIDLabel is the label name used for storing an inventory ID. + RGInventoryIDLabel = common.InventoryLabel ) // DefaultMeta is the ResourceMeta for ResourceGroup instances. diff --git a/scripts/generate_site_sidebar/sidebar_template.md.tmpl b/scripts/generate_site_sidebar/sidebar_template.md.tmpl index 344febf67d..89cd5e9791 100644 --- a/scripts/generate_site_sidebar/sidebar_template.md.tmpl +++ b/scripts/generate_site_sidebar/sidebar_template.md.tmpl @@ -2,6 +2,10 @@ - [Book](book/) {{bookLayout}} - [Reference](reference/) + - [Annotations](reference/annotations/) + - [apply-time mutation](reference/annotations/apply-time-mutation/) + - [depends-on](reference/annotations/depends-on/) + - [local-config](reference/annotations/local-config/) - [CLI](reference/cli/) - [pkg](reference/cli/pkg/) - [diff](reference/cli/pkg/diff/) @@ -26,6 +30,7 @@ - [FunctionResultList](reference/schema/function-result-list/) - [ResourceList](reference/schema/resource-list/) - [CRD Status Convention](reference/schema/crd-status-convention/) + - [Config Connector Status Convention](reference/schema/config-connector-status-convention/) - [Functions Catalog](https://catalog.kpt.dev/ ":target=_self") - [Curated](https://catalog.kpt.dev/ ":target=_self") - [Contrib](https://catalog.kpt.dev/contrib/ ":target=_self") diff --git a/site/book/06-deploying-packages/00.md b/site/book/06-deploying-packages/00.md index 81a6ad94f2..eada6587da 100644 --- a/site/book/06-deploying-packages/00.md +++ b/site/book/06-deploying-packages/00.md @@ -1,4 +1,4 @@ -In the last chapter of this book, we are going to cover how you deploy a kpt +In this chapter of this book, we are going to cover how you deploy a kpt package to a Kubernetes cluster and how the cluster state is managed as the package evolves over time. @@ -56,14 +56,15 @@ number of pods have been created and become available. For core kubernetes types, reconcile status is computed using hardcoded rules. For CRDs, the status computation is based on recommended [convention for status fields] that needs to be followed by custom resource publishers. If CRDs follow -these conventions, `live apply` will be able to correctly compute status. +these conventions, `live apply` will be able to correctly compute status. `kpt` also +has special rules for computing status for [Config Connector resources]. -Once you know the reconcile status of a single resource, then you really want to -answer the following question: - -- Did all the resources in the local package reconcile successfully? - -This is referred to as _aggregated status_. +Usually multiple resources are being applied together, and we want to know +when all of those resources have been successfully reconciled. `live apply` computes +the aggregate status and will wait until either they are all reconciled, the timeout +expires, or all the remaining unreconciled resources have reached a state where they +are unlikely to successfully reconcile. An example of the latter for `Deployment` +resources is when the progress deadline is exceeded. ## Dependency ordering @@ -76,3 +77,4 @@ any resources that depend on it are applied. [convention for status fields]: /reference/schema/crd-status-convention/ +[Config Connector resources]: /reference/schema/config-connector-status-convention/ diff --git a/site/book/07-effective-customizations/00.md b/site/book/07-effective-customizations/00.md new file mode 100644 index 0000000000..3c699224db --- /dev/null +++ b/site/book/07-effective-customizations/00.md @@ -0,0 +1,19 @@ +Kubernetes configuration packages and customizations go hand in hand, all the +packaging tools enable package customization, since every package needs to be adapted to each specific use. In this chapter we cover effective +customizations techniques that kpt rendering and packaging enables. We show how +providing customization through parameters has some [pitfalls] and recommend +alternatives where the contents of the package are not hidden behind a facade. +Some of these alternatives are only possible because kpt has made an investment +into bulk editing with [KRM functions] and upstream merging. + +### Prerequisites + +Before reading this chapter you should familiarize yourself with [chapter 4] +which talks about using functions as well as [updating a package page] in +[chapter 3]. + +[chapter 4]: /book/04-using-functions/ +[chapter 3]: /book/03-packages/ +[pitfalls]: https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/declarative-application-management.md#parameterization-pitfalls +[KRM functions]: https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md +[updating a package page]: /book/03-packages/05-updating-a-package.md \ No newline at end of file diff --git a/site/book/07-effective-customizations/01-single-value-replacement.md b/site/book/07-effective-customizations/01-single-value-replacement.md new file mode 100644 index 0000000000..1239d5697a --- /dev/null +++ b/site/book/07-effective-customizations/01-single-value-replacement.md @@ -0,0 +1,137 @@ +## Scenario + +I have a single value replacement in my package. I don’t want package consumers +to look through all the yaml files to find the value I want them to set. It +seems easier to just create a parameter for this value and have the user look +at Kptfile for inputs. + +Example storage bucket: + +```yaml +apiVersion: storage.cnrm.cloud.google.com/v1beta1 +kind: StorageBucket +metadata: + name: my-bucket # kpt-set: ${project-id}-${name} + namespace: ns-test # kpt-set: ${namespace} + annotations: + cnrm.cloud.google.com/force-destroy: "false" + cnrm.cloud.google.com/project-id: my-project # kpt-set: ${project-id} +spec: + storageClass: standard # kpt-set: ${storage-class} + uniformBucketLevelAccess: true + versioning: + enabled: false +``` + +The corresponding Kptfile: + +```yaml +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: bucket +info: + description: A Google Cloud Storage bucket +pipeline: + mutators: + - image: gcr.io/kpt-fn/apply-setters:v0.2 + configMap: + name: todo-bucket-name + namespace: todo-namespace + project-id: todo-project-id + storage-class: standard +``` + + +## Problems + +1. With package popularity the single values inevitably expand to provide a +facade to a large portion of the data. That defeats the purpose of minimizing +the cognitive load. With this small example almost half of the StorageBucket configuration is now covered with parameters. +1. Some values like resource names are used as references so setting them in +one place needs to trigger updates in all the places where they are referenced. +1. If additional resources that have similar values are added to the package +new string replacements need to be added. In this case everything will need +to also be marked up with project ID and namespace. +1. If a package is used as a sub-package the string replacement parameters need +to be surfaced to the parent package and if the parent package already expects +some values to be set and the parameters do not exist, the sub-package needs to +be updated. + +## Solutions: + +1. kpt allows the user to edit a particular value directly in the configuration +data and will handle upstream merge. When [editing the yaml] directly the +consumers are not confined to the parameters that the package author has +provided. [kpt pkg update] merges the local edits made by consumer with the +changes in the upstream package made by publisher. In this case `storageClass` +can be set directly by the user. +1. Attributes like resource names which are often updated by consumers to add +prefix or suffix (e.g. *-dev, *-stage, *-prod, na1-*, eu1-*) are best handled +by the [ensure-name-substring] function that will handle dependency updates as +well as capture all the resources in the package. +1. Instead of setting a particular value on a resource a bulk operation can be +applied to all the resources that fit a particular interface. This can be done +by a custom function or by [set-namespace], [search-and-replace] , [set-labels] +and [set-annotations] functions. + +New bucket configuration: + +```yaml +apiVersion: storage.cnrm.cloud.google.com/v1beta1 +kind: StorageBucket +metadata: + name: bucket + annotations: + cnrm.cloud.google.com/force-destroy: "false" +spec: + storageClass: standard + uniformBucketLevelAccess: true + versioning: + enabled: false +``` + +The suggested customizations are now in the Kptfile: + +```yaml +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: bucket +info: + description: A Google Cloud Storage bucket +pipeline: + mutators: + - image: gcr.io/kpt-fn/set-namespace:v0.2.0 + configMap: + namespace: example-ns + - image: gcr.io/kpt-fn/ensure-name-substring:v0.1.1 + configMap: + prepend: project111- + - image: gcr.io/kpt-fn/set-annotations:v0.1.4 + configMap: + cnrm.cloud.google.com/project-id: project111 +``` + +The resource configuration YAML doesn't need to be marked up with where the +namespace value needs to go. The [set-namespace] function is smart enough to +find all the appropriate resources that need the namespace. + +We have put in the starter name `bucket` and have an [ensure-name-substring] +that shows the package consumer that the project ID prefix is what we suggest. +However if they have a different naming convention they can alter the name +prefix or suffix on all the resources in the pacakge. + +Since we are trying to set the annotation to the project ID we can use the +[set-annotations] function one time and the annotation are going to be set on +all the resources in the package. If we add additional resources or whole +sub packages we will get the consistent annotations across all resources +without having to find all the places where annotations can go. + +[editing the yaml]: /book/03-packages/03-editing-a-package +[kpt pkg update]: /book/03-packages/05-updating-a-package +[ensure-name-substring]: https://catalog.kpt.dev/ensure-name-substring/v0.1/ +[search-and-replace]: https://catalog.kpt.dev/search-replace/v0.2/ +[set-labels]: https://catalog.kpt.dev/set-labels/v0.1/ +[set-annotations]: https://catalog.kpt.dev/set-annotations/v0.1/ +[set-namespace]: https://catalog.kpt.dev/set-namespace/v0.2/ \ No newline at end of file diff --git a/site/book/07-effective-customizations/02-limiting-package-changes.md b/site/book/07-effective-customizations/02-limiting-package-changes.md new file mode 100644 index 0000000000..c7aa4bb0e3 --- /dev/null +++ b/site/book/07-effective-customizations/02-limiting-package-changes.md @@ -0,0 +1,113 @@ +## Scenario: + +I’d like to limit what my package consumers can do with my package and it feels +safer to just provide a string replacement in one place so they know not to +alter the configuration outside of the few places that I designated as OK +places to change. + +Example deployment: +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: # kpt-merge: /nginx-deploy + name: nginx-deploy +spec: + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: backend + image: nginx:1.16.1 # kpt-set: nginx:${tag} +``` + +kpt configuration that uses a setter: +```yaml +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: dont-change-much +pipeline: + mutators: + - image: gcr.io/kpt-fn/apply-setters:v0.2.0 + configMap: + tag: 1.21 +``` + +## Problems: + +1. The limitation by parameters does not guarantee that consumers are in fact +going to limit their changes to the parameters. A popular pattern is using +kustomize to change output of other tools no matter what parameters had. In +this particular case I am able to fork or patch this package and add: + +```yaml +securityContext: + runAsNonRoot: false +``` + +2. String replacements rarely describe the intent of the package author. +When additional resources are added I need additional places where parameters +need to be applied. I can easily add other containers to this deployment and +the package author's rules are not clear and not easily validated. + +## Solutions: + +1. General ways to describe policy already exist. kpt has a [gatekeeper] +function that allows the author to describe intended limitations for a class +of resources or the entire package giving the consumer the freedom to customize +and get an error or a warning when the policy is violated. + +In the sample provided by the function we see how to provide a policy that will +clearly describe the intent using rego: + +```yaml +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: # kpt-merge: /disallowroot + name: disallowroot +spec: + crd: + spec: + names: + kind: DisallowRoot + targets: + - target: admission.k8s.gatekeeper.sh + rego: |- + package disallowroot + violation[{"msg": msg}] { + not input.review.object.spec.template.spec.securityContext.runAsNonRoot + msg := "Containers must not run as root" + } +--- +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: DisallowRoot +metadata: # kpt-merge: /disallowroot + name: disallowroot +spec: + match: + kinds: + - apiGroups: + - 'apps' + kinds: + - Deployment +``` + +The Kptfile can enforce that resources comply with this policy every time +`kpt fn render` is used: + +```yaml +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: gatekeeper-disallow-root-user +pipeline: + validators: + - image: gcr.io/kpt-fn/gatekeeper:v0.2 +``` + +[gatekeeper]: https://catalog.kpt.dev/gatekeeper/v0.2/ diff --git a/site/book/07-effective-customizations/03-generation.md b/site/book/07-effective-customizations/03-generation.md new file mode 100644 index 0000000000..2e56329942 --- /dev/null +++ b/site/book/07-effective-customizations/03-generation.md @@ -0,0 +1,29 @@ +## Scenario: + +When using template languages I am able to provide conditional statements based +on parameter values. This allows me to ask the user for a little bit of +information and generate a lot of boilerplate configuration. Some template +languages like [Jinja] are very robust and feature rich. + +## Problems: + +1. Increased usage and additional edge cases make a template a piece of code +that required tets and debugging. +1. The interplay between different conditionals and loops is interleaved in the template making it hard to understand what exactly is configuration and what is +the logic that alters the configuration. The consumer is left with one choice +supply different parameter values, execute the template rendering code and see +what happens. +1. Templates are generally monolithic, when a change is introduced the package consumers need to either pay the cost of updating or the new consumers pay the +cost of having to decipher more optional parameters. + +## Solutions: + +1. When the generated configuration is simple consider just using a sub-package +and running customizations using [single value replacement] techniques. +1. When a complex configuration needs to be generated the package author can +create a generator function using turing complete languages and debugging tools. Example of such a function is [folder generation]. The output of the function +is plain old KRM. + +[folder generation]: https://catalog.kpt.dev/generate-folders/v0.1/ +[Jinja]: https://palletsprojects.com/p/jinja/ +[single value replacement]: /book/07-effective-customizations/01-single-value-replacement.md \ No newline at end of file diff --git a/site/book/README.md b/site/book/README.md index a77e3880d3..005dd90c80 100644 --- a/site/book/README.md +++ b/site/book/README.md @@ -13,6 +13,7 @@ This book is organized as follows: - [Chapter 4] covers how to use kpt functions to automate configuration changes. - [Chapter 5] guides you through developing custom functions. - [Chapter 6] covers how to deploy a package to a Kubernetes cluster. +- [Chapter 7] covers effective customizations techniques. Let's get started! @@ -22,3 +23,4 @@ Let's get started! [chapter 4]: /book/04-using-functions/ [chapter 5]: /book/05-developing-functions/ [chapter 6]: /book/06-deploying-packages/ +[chapter 7]: /book/07-effective-customizations/ diff --git a/site/reference/schema/config-connector-status-convention/README.md b/site/reference/schema/config-connector-status-convention/README.md new file mode 100644 index 0000000000..aa501eb5e9 --- /dev/null +++ b/site/reference/schema/config-connector-status-convention/README.md @@ -0,0 +1,36 @@ +# Config Connector Status Convention + +`kpt` includes custom rules for [Config Connector] resources to make them easier to work +with. This document describes how kpt uses fields and conditions on Config Connector +resources to compute [reconcile status]. + +Config Connector resources expose the `observedGeneration` field in the status +object, and `kpt` will always report a resource as being `InProgress` if the +`observedGeneration` doesn't match the value of `metadata.generation`. + +If the `Ready` condition is `True`, a Config Connector resource will be reported +as `Current`, i.e it has been successfully reconciled. + +If the `Ready` condition is `False`, `kpt` will look at the `Reason` field on the +condition object to determine whether the resource is making progress towards +reconciliation. The possible values mirrors those used by [Config Connector events]. +If the value is one of the following, the resource is considered to have failed +reconciliation: +- `ManagementConflict` +- `UpdateFailed` +- `DeleteFailed` +- `DependencyInvalid` + +Note that this doesn't necessarily mean it could never successfully reconcile. +The Config Connector controller will keep retrying. But it does mean that the +resource is in a state where an external change is most likely needed to resolve +the issue. Typical examples would be missing permissions or an API that has not +been enabled. + +Similar to all other resources, a Config Connector resource will be in the `Terminating` +state if the `metadata.deletionTimestamp` is set, and considered fully deleted when +the resource no longer exists in the cluster. + +[reconcile status]: /book/06-deploying-packages/?id=reconcile-status +[Config Connector]: https://cloud.google.com/config-connector/docs/overview +[Config Connector events]: https://cloud.google.com/config-connector/docs/how-to/monitoring-your-resources diff --git a/site/sidebar.md b/site/sidebar.md index 5d0601e459..ab783df6f9 100644 --- a/site/sidebar.md +++ b/site/sidebar.md @@ -31,7 +31,15 @@ - [6.1 Initializing a Package for Apply](book/06-deploying-packages/01-initializing-a-package-for-apply.md) - [6.2 Applying a Package](book/06-deploying-packages/02-applying-a-package.md) - [6.3 Handling Dependencies](book/06-deploying-packages/03-handling-dependencies.md) + - [7 Effective Customizations](book/07-effective-customizations/) + - [7.1 Single Value Replacement](book/07-effective-customizations/01-single-value-replacement.md) + - [7.2 Limiting Package Changes](book/07-effective-customizations/02-limiting-package-changes.md) + - [7.3 Generation](book/07-effective-customizations/03-generation.md) - [Reference](reference/) + - [Annotations](reference/annotations/) + - [apply-time mutation](reference/annotations/apply-time-mutation/) + - [depends-on](reference/annotations/depends-on/) + - [local-config](reference/annotations/local-config/) - [CLI](reference/cli/) - [pkg](reference/cli/pkg/) - [diff](reference/cli/pkg/diff/) @@ -58,6 +66,7 @@ - [FunctionResultList](reference/schema/function-result-list/) - [ResourceList](reference/schema/resource-list/) - [CRD Status Convention](reference/schema/crd-status-convention/) + - [Config Connector Status Convention](reference/schema/config-connector-status-convention/) - [Annotations Reference](reference/annotations/) - [apply-time-mutation](reference/annotations/apply-time-mutation/) - [depends-on](reference/annotations/depends-on/)