github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/commands/live/init/cmdliveinit.go (about)

     1  // Copyright 2020 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package init
    16  
    17  import (
    18  	"context"
    19  	"crypto/sha1"
    20  	goerrors "errors"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"strconv"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/GoogleContainerTools/kpt/internal/docs/generated/livedocs"
    29  	"github.com/GoogleContainerTools/kpt/internal/errors"
    30  	"github.com/GoogleContainerTools/kpt/internal/pkg"
    31  	"github.com/GoogleContainerTools/kpt/internal/printer"
    32  	"github.com/GoogleContainerTools/kpt/internal/types"
    33  	"github.com/GoogleContainerTools/kpt/internal/util/attribution"
    34  	"github.com/GoogleContainerTools/kpt/internal/util/pathutil"
    35  	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
    36  	rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1"
    37  	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
    38  	"github.com/spf13/cobra"
    39  	"k8s.io/cli-runtime/pkg/genericclioptions"
    40  	k8scmdutil "k8s.io/kubectl/pkg/cmd/util"
    41  	"sigs.k8s.io/cli-utils/pkg/common"
    42  	"sigs.k8s.io/cli-utils/pkg/config"
    43  	"sigs.k8s.io/kustomize/kyaml/filesys"
    44  	"sigs.k8s.io/kustomize/kyaml/yaml"
    45  )
    46  
    47  const defaultInventoryName = "inventory"
    48  
    49  // InvExistsError defines new error when the inventory
    50  // values have already been set on the Kptfile.
    51  type InvExistsError struct{}
    52  
    53  func (i *InvExistsError) Error() string {
    54  	return "inventory information already set for package"
    55  }
    56  
    57  // InvInRGExistsError defines new error when the inventory
    58  // values have already been set on the ResourceGroup file and we will warn
    59  // the user to migrate rather than init. This is part of kpt live STDIN work.
    60  type InvInRGExistsError struct{}
    61  
    62  func (i *InvInRGExistsError) Error() string {
    63  	return "inventory information already set for package"
    64  }
    65  
    66  // InvInKfExistsError defines new error when the inventory
    67  // values have already been set on the Kptfile and we will warn
    68  // the user to migrate rather than init. This is part of kpt live STDIN work.
    69  type InvInKfExistsError struct{}
    70  
    71  func (i *InvInKfExistsError) Error() string {
    72  	return "inventory information already set within Kptfile for package"
    73  }
    74  
    75  func NewRunner(ctx context.Context, factory k8scmdutil.Factory,
    76  	ioStreams genericclioptions.IOStreams) *Runner {
    77  	r := &Runner{
    78  		ctx:       ctx,
    79  		factory:   factory,
    80  		ioStreams: ioStreams,
    81  	}
    82  
    83  	cmd := &cobra.Command{
    84  		Use:     "init [PKG_PATH]",
    85  		PreRunE: r.preRunE,
    86  		RunE:    r.runE,
    87  		Short:   livedocs.InitShort,
    88  		Long:    livedocs.InitShort + "\n" + livedocs.InitLong,
    89  		Example: livedocs.InitExamples,
    90  	}
    91  	r.Command = cmd
    92  
    93  	cmd.Flags().StringVar(&r.Name, "name", "", "Inventory object name")
    94  	cmd.Flags().BoolVar(&r.Force, "force", false, "Set inventory values even if already set in Kptfile or ResourceGroup file")
    95  	cmd.Flags().BoolVar(&r.Quiet, "quiet", false, "If true, do not print output message for initialization")
    96  	cmd.Flags().StringVar(&r.InventoryID, "inventory-id", "", "Inventory id for the package")
    97  	cmd.Flags().StringVar(&r.RGFileName, "rg-file", rgfilev1alpha1.RGFileName, "Name of the file holding the ResourceGroup resource.")
    98  	return r
    99  }
   100  
   101  func NewCommand(ctx context.Context, f k8scmdutil.Factory,
   102  	ioStreams genericclioptions.IOStreams) *cobra.Command {
   103  	return NewRunner(ctx, f, ioStreams).Command
   104  }
   105  
   106  type Runner struct {
   107  	ctx     context.Context
   108  	Command *cobra.Command
   109  
   110  	factory     k8scmdutil.Factory
   111  	ioStreams   genericclioptions.IOStreams
   112  	Force       bool   // Set inventory values even if already set in Kptfile
   113  	Name        string // Inventory object name
   114  	RGFileName  string // resourcegroup object filename
   115  	InventoryID string // Inventory object unique identifier label
   116  	Quiet       bool   // Output message during initialization
   117  }
   118  
   119  func (r *Runner) preRunE(_ *cobra.Command, _ []string) error {
   120  	dir := filepath.Dir(filepath.Clean(r.RGFileName))
   121  	if dir != "." {
   122  		return fmt.Errorf("rg-file must be a valid filename")
   123  	}
   124  	return nil
   125  }
   126  
   127  func (r *Runner) runE(_ *cobra.Command, args []string) error {
   128  	const op errors.Op = "cmdliveinit.runE"
   129  	if len(args) == 0 {
   130  		// default to the current working directory
   131  		cwd, err := os.Getwd()
   132  		if err != nil {
   133  			return errors.E(op, err)
   134  		}
   135  		args = append(args, cwd)
   136  	}
   137  
   138  	dir, err := config.NormalizeDir(args[0])
   139  	if err != nil {
   140  		return errors.E(op, err)
   141  	}
   142  
   143  	absPath, _, err := pathutil.ResolveAbsAndRelPaths(dir)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	p, err := pkg.New(filesys.FileSystemOrOnDisk{}, absPath)
   149  	if err != nil {
   150  		return errors.E(op, err)
   151  	}
   152  
   153  	err = (&ConfigureInventoryInfo{
   154  		Pkg:         p,
   155  		Factory:     r.factory,
   156  		Quiet:       r.Quiet,
   157  		Name:        r.Name,
   158  		InventoryID: r.InventoryID,
   159  		RGFileName:  r.RGFileName,
   160  		Force:       r.Force,
   161  	}).Run(r.ctx)
   162  	if err != nil {
   163  		return errors.E(op, p.UniquePath, err)
   164  	}
   165  	return nil
   166  }
   167  
   168  // ConfigureInventoryInfo contains the functionality for adding and updating
   169  // the inventory information in the Kptfile.
   170  type ConfigureInventoryInfo struct {
   171  	Pkg     *pkg.Pkg
   172  	Factory k8scmdutil.Factory
   173  	Quiet   bool
   174  
   175  	Name        string
   176  	InventoryID string
   177  	RGFileName  string
   178  
   179  	Force bool
   180  }
   181  
   182  // Run updates the inventory info in the package given by the Path.
   183  func (c *ConfigureInventoryInfo) Run(ctx context.Context) error {
   184  	const op errors.Op = "cmdliveinit.Run"
   185  	pr := printer.FromContextOrDie(ctx)
   186  
   187  	namespace, err := config.FindNamespace(c.Factory.ToRawKubeConfigLoader(), c.Pkg.UniquePath.String())
   188  	if err != nil {
   189  		return errors.E(op, c.Pkg.UniquePath, err)
   190  	}
   191  	namespace = strings.TrimSpace(namespace)
   192  	if !c.Quiet {
   193  		pr.Printf("initializing %q data (namespace: %s)...", c.RGFileName, namespace)
   194  	}
   195  
   196  	// Autogenerate the name if it is not provided through the flag.
   197  	if c.Name == "" {
   198  		randomSuffix := common.RandomStr()
   199  		c.Name = fmt.Sprintf("%s-%s", defaultInventoryName, randomSuffix)
   200  	}
   201  
   202  	// Autogenerate the inventory ID if not provided through the flag.
   203  	if c.InventoryID == "" {
   204  		c.InventoryID, err = generateID(namespace, c.Name, time.Now())
   205  		if err != nil {
   206  			return errors.E(op, c.Pkg.UniquePath, err)
   207  		}
   208  	}
   209  
   210  	// Finally, create a ResourceGroup containing the inventory information.
   211  	err = createRGFile(c.Pkg, &kptfilev1.Inventory{
   212  		Namespace:   namespace,
   213  		Name:        c.Name,
   214  		InventoryID: c.InventoryID,
   215  	}, c.RGFileName, c.Force)
   216  	if !c.Quiet {
   217  		if err == nil {
   218  			pr.Printf("success\n")
   219  		} else {
   220  			pr.Printf("failed\n")
   221  		}
   222  	}
   223  	if err != nil {
   224  		return errors.E(op, c.Pkg.UniquePath, err)
   225  	}
   226  	// add metrics annotation to package resources to track the usage as the resources
   227  	// will be applied using kpt live group
   228  	at := attribution.Attributor{PackagePaths: []string{c.Pkg.UniquePath.String()}, CmdGroup: "live"}
   229  	at.Process()
   230  	return nil
   231  }
   232  
   233  // createRGFile fills in the inventory object values into the resourcegroup object and writes to file storage.
   234  func createRGFile(p *pkg.Pkg, inv *kptfilev1.Inventory, filename string, force bool) error {
   235  	const op errors.Op = "cmdliveinit.createRGFile"
   236  	// Read the resourcegroup object io io.dir
   237  	rg, err := p.ReadRGFile(filename)
   238  	if err != nil && !goerrors.Is(err, os.ErrNotExist) {
   239  		return errors.E(op, p.UniquePath, err)
   240  	}
   241  
   242  	// Read the Kptfile to ensure that inventory information is not in Kptfile either.
   243  	// Ignore error if Kptfile not found as we now support live init without a Kptfile since
   244  	// inventory information is stored in a ResourceGroup object.
   245  	kf, err := p.Kptfile()
   246  	if err != nil && !errors.Is(err, os.ErrNotExist) {
   247  		return errors.E(op, p.UniquePath, err)
   248  	}
   249  	// Validate the inventory values don't exist in Kptfile.
   250  	isEmpty := true
   251  	if kf != nil {
   252  		isEmpty = kptfileInventoryEmpty(kf.Inventory)
   253  		if !isEmpty && !force {
   254  			return errors.E(op, p.UniquePath, &InvInKfExistsError{})
   255  		}
   256  
   257  		// Set the Kptfile inventory to be nil if we force write to resourcegroup instead.
   258  		kf.Inventory = nil
   259  	}
   260  
   261  	// Validate the inventory values don't already exist in Resourcegroup.
   262  	if rg != nil && !force {
   263  		return errors.E(op, p.UniquePath, &InvInRGExistsError{})
   264  	}
   265  	// Initialize new resourcegroup object, as rg should have been nil.
   266  	rg = &rgfilev1alpha1.ResourceGroup{ResourceMeta: rgfilev1alpha1.DefaultMeta}
   267  	// // Finally, set the inventory parameters in the ResourceGroup object and write it.
   268  	rg.Name = inv.Name
   269  	rg.Namespace = inv.Namespace
   270  	rg.Labels = map[string]string{rgfilev1alpha1.RGInventoryIDLabel: inv.InventoryID}
   271  	if err := writeRGFile(p.UniquePath.String(), rg, filename); err != nil {
   272  		return errors.E(op, p.UniquePath, err)
   273  	}
   274  
   275  	// Rewrite Kptfile without inventory existing Kptfile contains inventory info. This
   276  	// is required when a user appends the force flag.
   277  	if !isEmpty {
   278  		if err := kptfileutil.WriteFile(p.UniquePath.String(), kf); err != nil {
   279  			return errors.E(op, p.UniquePath, err)
   280  		}
   281  	}
   282  
   283  	return nil
   284  }
   285  
   286  // writeRGFile writes a ResourceGroup inventory to local disk.
   287  func writeRGFile(dir string, rg *rgfilev1alpha1.ResourceGroup, filename string) error {
   288  	const op errors.Op = "cmdliveinit.writeRGFile"
   289  	b, err := yaml.MarshalWithOptions(rg, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle})
   290  	if err != nil {
   291  		return err
   292  	}
   293  	if _, err := os.Stat(filepath.Join(dir, filename)); err != nil && !goerrors.Is(err, os.ErrNotExist) {
   294  		return errors.E(op, errors.IO, types.UniquePath(dir), err)
   295  	}
   296  
   297  	// fyi: perm is ignored if the file already exists
   298  	err = os.WriteFile(filepath.Join(dir, filename), b, 0600)
   299  	if err != nil {
   300  		return errors.E(op, errors.IO, types.UniquePath(dir), err)
   301  	}
   302  	return nil
   303  }
   304  
   305  // generateID returns the string which is a SHA1 hash of the passed namespace
   306  // and name, with the unix timestamp string concatenated. Returns an error
   307  // if either the namespace or name are empty.
   308  func generateID(namespace string, name string, t time.Time) (string, error) {
   309  	const op errors.Op = "cmdliveinit.generateID"
   310  	hashStr, err := generateHash(namespace, name)
   311  	if err != nil {
   312  		return "", errors.E(op, err)
   313  	}
   314  	timeStr := strconv.FormatInt(t.UTC().UnixNano(), 10)
   315  	return fmt.Sprintf("%s-%s", hashStr, timeStr), nil
   316  }
   317  
   318  // generateHash returns the SHA1 hash of the concatenated "namespace:name" string,
   319  // or an error if either namespace or name is empty.
   320  func generateHash(namespace string, name string) (string, error) {
   321  	const op errors.Op = "cmdliveinit.generateHash"
   322  	if len(namespace) == 0 || len(name) == 0 {
   323  		return "", errors.E(op,
   324  			fmt.Errorf("can not generate hash with empty namespace or name"))
   325  	}
   326  	str := fmt.Sprintf("%s:%s", namespace, name)
   327  	h := sha1.New()
   328  	if _, err := h.Write([]byte(str)); err != nil {
   329  		return "", errors.E(op, err)
   330  	}
   331  	return fmt.Sprintf("%x", (h.Sum(nil))), nil
   332  }
   333  
   334  // kptfileInventoryEmpty returns true if the Inventory structure
   335  // in the Kptfile is empty; false otherwise.
   336  func kptfileInventoryEmpty(inv *kptfilev1.Inventory) bool {
   337  	return inv == nil
   338  }