...

Source file src/sigs.k8s.io/cli-utils/pkg/config/initoptions.go

Documentation: sigs.k8s.io/cli-utils/pkg/config

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package config
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/google/uuid"
    15  	"k8s.io/cli-runtime/pkg/genericclioptions"
    16  	"k8s.io/klog/v2"
    17  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    18  	"sigs.k8s.io/cli-utils/pkg/common"
    19  	"sigs.k8s.io/cli-utils/pkg/inventory/configmap"
    20  	"sigs.k8s.io/kustomize/kyaml/kio"
    21  	"sigs.k8s.io/kustomize/kyaml/kio/filters"
    22  	"sigs.k8s.io/kustomize/kyaml/openapi"
    23  )
    24  
    25  const (
    26  	manifestFilename = "inventory-template.yaml"
    27  )
    28  
    29  // InitOptions contains the fields necessary to generate a
    30  // inventory object template ConfigMap.
    31  type InitOptions struct {
    32  	factory cmdutil.Factory
    33  
    34  	ioStreams genericclioptions.IOStreams
    35  	// Template string; must be a valid k8s resource.
    36  	Template string
    37  	// Package directory argument; must be valid directory.
    38  	Dir string
    39  	// Namespace for inventory object; can not be empty.
    40  	Namespace string
    41  	// Inventory object label value; must be a valid k8s label value.
    42  	InventoryID string
    43  }
    44  
    45  func NewInitOptions(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *InitOptions {
    46  	return &InitOptions{
    47  		factory:   f,
    48  		ioStreams: ioStreams,
    49  		Template:  configmap.ConfigMapTemplate,
    50  	}
    51  }
    52  
    53  // Complete fills in the InitOptions fields.
    54  // TODO(seans3): Look into changing this kubectl-inspired way of organizing
    55  // the InitOptions (e.g. Complete and Run methods).
    56  func (i *InitOptions) Complete(args []string) error {
    57  	if len(args) != 1 {
    58  		return fmt.Errorf("need one 'directory' arg; have %d", len(args))
    59  	}
    60  	dir, err := NormalizeDir(args[0])
    61  	if err != nil {
    62  		return err
    63  	}
    64  	i.Dir = dir
    65  	klog.V(4).Infof("init directory: %s", i.Dir)
    66  
    67  	ns, err := FindNamespace(i.factory.ToRawKubeConfigLoader(), i.Dir)
    68  	if err != nil {
    69  		return err
    70  	}
    71  	i.Namespace = ns
    72  
    73  	// Set the default inventory label if one does not exist.
    74  	if len(i.InventoryID) == 0 {
    75  		inventoryID, err := i.defaultInventoryID()
    76  		if err != nil {
    77  			return err
    78  		}
    79  		i.InventoryID = inventoryID
    80  	}
    81  	if !validateInventoryID(i.InventoryID) {
    82  		return fmt.Errorf("invalid group name: %s", i.InventoryID)
    83  	}
    84  	// Output the calculated namespace used for inventory object.
    85  	fmt.Fprintf(i.ioStreams.Out, "namespace: %s is used for inventory object\n", i.Namespace)
    86  	return nil
    87  }
    88  
    89  type namespaceLoader interface {
    90  	Namespace() (string, bool, error)
    91  }
    92  
    93  // FindNamespace looks up the namespace that should be used for the
    94  // inventory template of the package. If the namespace is specified with
    95  // the --namespace flag, it will be used no matter what. If not, this
    96  // will look at all the resource, and if all belong in the same namespace,
    97  // it will return that namespace. Otherwise, it will return the namespace
    98  // set in the context.
    99  func FindNamespace(loader namespaceLoader, dir string) (string, error) {
   100  	namespace, enforceNamespace, err := loader.Namespace()
   101  	if err != nil {
   102  		return "", err
   103  	}
   104  	if enforceNamespace {
   105  		klog.V(6).Infof("enforcing namespace: %s", namespace)
   106  		return namespace, nil
   107  	}
   108  
   109  	ns, allInSameNs, err := allInSameNamespace(dir)
   110  	if err != nil {
   111  		return "", err
   112  	}
   113  	if allInSameNs {
   114  		klog.V(6).Infof("all in same namespace: %s", ns)
   115  		return ns, nil
   116  	}
   117  	klog.V(6).Infof("returning namespace: %s", namespace)
   118  	return namespace, nil
   119  }
   120  
   121  // NormalizeDir returns full absolute directory path of the
   122  // passed directory or an error. This function cleans up paths
   123  // such as current directory (.), relative directories (..), or
   124  // multiple separators.
   125  func NormalizeDir(dirPath string) (string, error) {
   126  	if !common.IsDir(dirPath) {
   127  		return "", fmt.Errorf("invalid directory argument: %s", dirPath)
   128  	}
   129  	return filepath.Abs(dirPath)
   130  }
   131  
   132  // allInSameNamespace goes through all resources in the package and
   133  // checks the namespace for all of them. If they all have the namespace
   134  // set and they all have the same value, this will return that namespace
   135  // and the second return value will be true. Otherwise, it will not return
   136  // a namespace and the second return value will be false.
   137  func allInSameNamespace(packageDir string) (string, bool, error) {
   138  	r := kio.LocalPackageReader{PackagePath: packageDir}
   139  	nodes, err := r.Read()
   140  	if err != nil {
   141  		return "", false, err
   142  	}
   143  
   144  	// Filter out any resources with the LocalConfig annotation
   145  	nodes, err = (&filters.IsLocalConfig{}).Filter(nodes)
   146  	if err != nil {
   147  		return "", false, err
   148  	}
   149  
   150  	var ns string
   151  	for _, node := range nodes {
   152  		rm, err := node.GetMeta()
   153  		if err != nil {
   154  			return "", false, err
   155  		}
   156  		// Skip found cluster-scoped resources. If not found, just assume namespaced.
   157  		namespaced, found := openapi.IsNamespaceScoped(rm.TypeMeta)
   158  		if found && !namespaced {
   159  			klog.V(6).Infof("cluster-scoped resource %s--skip namespace calc", rm.TypeMeta)
   160  			continue
   161  		}
   162  		if rm.Namespace == "" {
   163  			klog.V(6).Infof("one resource missing namespace (%s): return empty namespace", rm.Name)
   164  			return "", false, nil
   165  		}
   166  		if ns == "" {
   167  			ns = rm.Namespace
   168  		} else if rm.Namespace != ns {
   169  			klog.V(6).Infof("two namespaces not same: %s versus %s", rm.Namespace, ns)
   170  			return "", false, nil
   171  		}
   172  	}
   173  	if ns != "" {
   174  		klog.V(6).Infof("returning empty namespace")
   175  		return ns, true, nil
   176  	}
   177  	return "", false, nil
   178  }
   179  
   180  // defaultInventoryID returns a UUID string as a default unique
   181  // identifier for a inventory object label.
   182  func (i *InitOptions) defaultInventoryID() (string, error) {
   183  	u, err := uuid.NewRandom()
   184  	if err != nil {
   185  		return "", err
   186  	}
   187  	return u.String(), nil
   188  }
   189  
   190  // Must begin and end with an alphanumeric character ([a-z0-9A-Z])
   191  // with dashes (-), underscores (_), dots (.), and alphanumerics
   192  // between.
   193  const inventoryIDRegexp = `^[a-zA-Z0-9][a-zA-Z0-9\-\_\.]+[a-zA-Z0-9]$`
   194  
   195  // validateInventoryID returns true of the passed group name is a
   196  // valid label value; false otherwise. The valid label values
   197  // are [a-z0-9A-Z] "-", "_", and "." The inventoryID must not
   198  // be empty, but it can not be more than 63 characters.
   199  func validateInventoryID(inventoryID string) bool {
   200  	if len(inventoryID) == 0 || len(inventoryID) > 63 {
   201  		return false
   202  	}
   203  	re := regexp.MustCompile(inventoryIDRegexp)
   204  	return re.MatchString(inventoryID)
   205  }
   206  
   207  // fileExists returns true if a file at path already exists;
   208  // false otherwise.
   209  func fileExists(path string) bool {
   210  	f, err := os.Stat(path)
   211  	if os.IsNotExist(err) {
   212  		return false
   213  	}
   214  	return !f.IsDir()
   215  }
   216  
   217  // fillInValues returns a string of the inventory object template
   218  // ConfigMap with values filled in (eg. namespace, inventoryID).
   219  // TODO(seans3): Look into text/template package.
   220  func (i *InitOptions) fillInValues() string {
   221  	now := time.Now()
   222  	nowStr := now.Format("2006-01-02 15:04:05 MST")
   223  	randomSuffix := common.RandomStr()
   224  	manifestStr := i.Template
   225  	klog.V(4).Infof("namespace/inventory-id: %s/%s", i.Namespace, i.InventoryID)
   226  	manifestStr = strings.ReplaceAll(manifestStr, "<DATETIME>", nowStr)
   227  	manifestStr = strings.ReplaceAll(manifestStr, "<NAMESPACE>", i.Namespace)
   228  	manifestStr = strings.ReplaceAll(manifestStr, "<RANDOMSUFFIX>", randomSuffix)
   229  	manifestStr = strings.ReplaceAll(manifestStr, "<INVENTORYID>", i.InventoryID)
   230  	return manifestStr
   231  }
   232  
   233  func (i *InitOptions) Run() error {
   234  	manifestFilePath := filepath.Join(i.Dir, manifestFilename)
   235  	if fileExists(manifestFilePath) {
   236  		return fmt.Errorf("inventory object template file already exists: %s", manifestFilePath)
   237  	}
   238  	klog.V(4).Infof("creating manifest filename: %s", manifestFilePath)
   239  	f, err := os.Create(manifestFilePath)
   240  	if err != nil {
   241  		return fmt.Errorf("unable to create inventory object template file: %s", err)
   242  	}
   243  	defer f.Close()
   244  	_, err = f.WriteString(i.fillInValues())
   245  	if err != nil {
   246  		return fmt.Errorf("unable to write inventory object template file: %s", manifestFilePath)
   247  	}
   248  	fmt.Fprintf(i.ioStreams.Out, "Initialized: %s\n", manifestFilePath)
   249  	return nil
   250  }
   251  

View as plain text