github.com/opendevstack/tailor@v1.3.5-0.20220119161809-cab064e60a67/pkg/cli/options.go (about)

     1  package cli
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"os/exec"
     9  	"strings"
    10  
    11  	"github.com/opendevstack/tailor/pkg/utils"
    12  )
    13  
    14  // GlobalOptions are app-wide.
    15  type GlobalOptions struct {
    16  	Verbose         bool
    17  	Debug           bool
    18  	NonInteractive  bool
    19  	OcBinary        string
    20  	File            string
    21  	Force           bool
    22  	IsLoggedIn      bool
    23  	ClusterRequired bool
    24  	fs              utils.FileStater
    25  }
    26  
    27  // NamespaceOptions define which namespace Tailor works against.
    28  type NamespaceOptions struct {
    29  	Namespace         string
    30  	CheckedNamespaces []string
    31  }
    32  
    33  // CompareOptions define how to compare desired and current state.
    34  type CompareOptions struct {
    35  	*GlobalOptions
    36  	*NamespaceOptions
    37  	Selector                string
    38  	Excludes                []string
    39  	TemplateDir             string
    40  	ParamDir                string
    41  	PrivateKey              string
    42  	Passphrase              string
    43  	Labels                  string
    44  	Params                  []string
    45  	ParamFiles              []string
    46  	PreservePaths           []string
    47  	PreserveImmutableFields bool
    48  	IgnoreUnknownParameters bool
    49  	UpsertOnly              bool
    50  	AllowRecreate           bool
    51  	RevealSecrets           bool
    52  	Verify                  bool
    53  	Resource                string
    54  }
    55  
    56  // ExportOptions define how the export should be done.
    57  type ExportOptions struct {
    58  	*GlobalOptions
    59  	*NamespaceOptions
    60  	Selector               string
    61  	Excludes               []string
    62  	TemplateDir            string
    63  	ParamDir               string
    64  	WithAnnotations        bool
    65  	WithHardcodedNamespace bool
    66  	TrimAnnotations        []string
    67  	Resource               string
    68  }
    69  
    70  // SecretsOptions define how to work with encrypted files.
    71  type SecretsOptions struct {
    72  	*GlobalOptions
    73  	ParamDir     string
    74  	PublicKeyDir string
    75  	PrivateKey   string
    76  	Passphrase   string
    77  }
    78  
    79  // InitGlobalOptions creates a new pointer to GlobalOptions with a given filesystem.
    80  func InitGlobalOptions(fs utils.FileStater) *GlobalOptions {
    81  	return &GlobalOptions{fs: fs}
    82  }
    83  
    84  // NewGlobalOptions returns new global options based on file/flags.
    85  // Those options are shared across all commands.
    86  func NewGlobalOptions(
    87  	clusterRequired bool,
    88  	fileFlag string,
    89  	verboseFlag bool,
    90  	debugFlag bool,
    91  	nonInteractiveFlag bool,
    92  	ocBinaryFlag string,
    93  	forceFlag bool) (*GlobalOptions, error) {
    94  	o := InitGlobalOptions(&utils.OsFS{})
    95  	o.ClusterRequired = clusterRequired
    96  
    97  	fileFlags, err := getFileFlags(fileFlag, verbose)
    98  	if err != nil {
    99  		return o, fmt.Errorf("Could not read %s: %s", fileFlag, err)
   100  	}
   101  
   102  	if verboseFlag {
   103  		o.Verbose = true
   104  	} else if fileFlags["verbose"] == "true" {
   105  		o.Verbose = true
   106  	}
   107  
   108  	if debugFlag {
   109  		o.Debug = true
   110  	} else if fileFlags["debug"] == "true" {
   111  		o.Debug = true
   112  	}
   113  
   114  	if nonInteractiveFlag {
   115  		o.NonInteractive = true
   116  	} else if fileFlags["non-interactive"] == "true" {
   117  		o.NonInteractive = true
   118  	}
   119  
   120  	if len(fileFlag) > 0 {
   121  		o.File = fileFlag
   122  	}
   123  
   124  	if len(ocBinaryFlag) > 0 {
   125  		o.OcBinary = ocBinaryFlag
   126  	} else if val, ok := fileFlags["oc-binary"]; ok {
   127  		o.OcBinary = val
   128  	}
   129  
   130  	if forceFlag {
   131  		o.Force = true
   132  	} else if fileFlags["force"] == "true" {
   133  		o.Force = true
   134  	}
   135  
   136  	verbose = o.Verbose || o.Debug
   137  	debug = o.Debug
   138  	ocBinary = o.OcBinary
   139  
   140  	DebugMsg(fmt.Sprintf("%#v", o))
   141  
   142  	return o, o.check(clusterRequired)
   143  }
   144  
   145  // NewCompareOptions returns new options for the diff/apply command based on file/flags.
   146  func NewCompareOptions(
   147  	globalOptions *GlobalOptions,
   148  	namespaceFlag string,
   149  	selectorFlag string,
   150  	excludeFlag []string,
   151  	templateDirFlag string,
   152  	paramDirFlag string,
   153  	publicKeyDirFlag string,
   154  	privateKeyFlag string,
   155  	passphraseFlag string,
   156  	labelsFlag string,
   157  	paramFlag []string,
   158  	paramFileFlag []string,
   159  	preserveFlag []string,
   160  	preserveImmutableFieldsFlag bool,
   161  	ignoreUnknownParametersFlag bool,
   162  	upsertOnlyFlag bool,
   163  	allowRecreateFlag bool,
   164  	revealSecretsFlag bool,
   165  	verifyFlag bool,
   166  	resourceArg string) (*CompareOptions, error) {
   167  	o := &CompareOptions{
   168  		GlobalOptions:    globalOptions,
   169  		NamespaceOptions: &NamespaceOptions{},
   170  	}
   171  	filename := o.resolvedFile(namespaceFlag)
   172  
   173  	fileFlags, err := getFileFlags(filename, verbose)
   174  	if err != nil {
   175  		return o, fmt.Errorf("Could not read '%s': %s", filename, err)
   176  	}
   177  
   178  	if len(namespaceFlag) > 0 {
   179  		o.Namespace = namespaceFlag
   180  	} else if val, ok := fileFlags["namespace"]; ok {
   181  		o.Namespace = val
   182  	}
   183  
   184  	if len(selectorFlag) > 0 {
   185  		o.Selector = selectorFlag
   186  	} else if val, ok := fileFlags["selector"]; ok {
   187  		o.Selector = val
   188  	}
   189  
   190  	o.Excludes = []string{}
   191  	if len(excludeFlag) > 0 {
   192  		for _, val := range excludeFlag {
   193  			o.Excludes = append(o.Excludes, strings.Split(val, ",")...)
   194  		}
   195  	} else if val, ok := fileFlags["exclude"]; ok {
   196  		o.Excludes = strings.Split(val, ",")
   197  	}
   198  
   199  	o.TemplateDir = "."
   200  	if templateDirFlag != "." {
   201  		o.TemplateDir = templateDirFlag
   202  	} else if val, ok := fileFlags["template-dir"]; ok {
   203  		o.TemplateDir = val
   204  	}
   205  
   206  	o.ParamDir = "."
   207  	if paramDirFlag != "." {
   208  		o.ParamDir = paramDirFlag
   209  	} else if val, ok := fileFlags["param-dir"]; ok {
   210  		o.ParamDir = val
   211  	}
   212  
   213  	o.PrivateKey = "private.key"
   214  	if privateKeyFlag != "private.key" {
   215  		o.PrivateKey = privateKeyFlag
   216  	} else if val, ok := fileFlags["private-key"]; ok {
   217  		o.PrivateKey = val
   218  	}
   219  
   220  	if len(passphraseFlag) > 0 {
   221  		o.Passphrase = passphraseFlag
   222  	} else if val, ok := fileFlags["passphrase"]; ok {
   223  		o.Passphrase = val
   224  	}
   225  
   226  	if len(labelsFlag) > 0 {
   227  		o.Labels = labelsFlag
   228  	} else if val, ok := fileFlags["labels"]; ok {
   229  		o.Labels = val
   230  	}
   231  
   232  	if val, ok := fileFlags["param"]; ok {
   233  		o.Params = strings.Split(val, ",")
   234  	}
   235  	if len(paramFlag) > 0 {
   236  		params := map[string]string{}
   237  		for _, setParam := range o.Params {
   238  			setPair := strings.SplitN(setParam, "=", 2)
   239  			key := setPair[0]
   240  			params[key] = setPair[1]
   241  			for _, newParam := range paramFlag {
   242  				newPair := strings.SplitN(newParam, "=", 2)
   243  				if key == newPair[0] {
   244  					params[key] = newPair[1]
   245  					break
   246  				}
   247  			}
   248  		}
   249  		o.Params = []string{}
   250  		for k, v := range params {
   251  			o.Params = append(o.Params, k+"="+v)
   252  		}
   253  		for _, v := range paramFlag {
   254  			pair := strings.SplitN(v, "=", 2)
   255  			if _, ok := params[pair[0]]; !ok {
   256  				o.Params = append(o.Params, v)
   257  			}
   258  		}
   259  	}
   260  
   261  	if len(paramFileFlag) > 0 {
   262  		o.ParamFiles = paramFileFlag
   263  	} else if val, ok := fileFlags["param-file"]; ok {
   264  		o.ParamFiles = strings.Split(val, ",")
   265  	}
   266  
   267  	if len(preserveFlag) > 0 {
   268  		o.PreservePaths = preserveFlag
   269  	} else if val, ok := fileFlags["ignore-path"]; ok {
   270  		o.PreservePaths = strings.Split(val, ",")
   271  	} else if val, ok := fileFlags["preserve"]; ok {
   272  		o.PreservePaths = strings.Split(val, ",")
   273  	}
   274  
   275  	if preserveImmutableFieldsFlag {
   276  		o.PreserveImmutableFields = true
   277  	} else if fileFlags["preserve-immutable-fields"] == "true" {
   278  		o.PreserveImmutableFields = true
   279  	}
   280  
   281  	if ignoreUnknownParametersFlag {
   282  		o.IgnoreUnknownParameters = true
   283  	} else if fileFlags["ignore-unknown-parameters"] == "true" {
   284  		o.IgnoreUnknownParameters = true
   285  	}
   286  
   287  	if upsertOnlyFlag {
   288  		o.UpsertOnly = true
   289  	} else if fileFlags["upsert-only"] == "true" {
   290  		o.UpsertOnly = true
   291  	}
   292  
   293  	if allowRecreateFlag {
   294  		o.AllowRecreate = true
   295  	} else if fileFlags["allow-recreate"] == "true" {
   296  		o.AllowRecreate = true
   297  	}
   298  
   299  	if revealSecretsFlag {
   300  		o.RevealSecrets = true
   301  	} else if fileFlags["reveal-secrets"] == "true" {
   302  		o.RevealSecrets = true
   303  	}
   304  
   305  	if verifyFlag {
   306  		o.Verify = true
   307  	} else if fileFlags["verify"] == "true" {
   308  		o.Verify = true
   309  	}
   310  
   311  	if len(resourceArg) > 0 {
   312  		o.Resource = resourceArg
   313  	} else if val, ok := fileFlags["resource"]; ok {
   314  		o.Resource = val
   315  	}
   316  
   317  	DebugMsg(fmt.Sprintf("%#v", o))
   318  
   319  	return o, o.check(o.ClusterRequired)
   320  }
   321  
   322  // NewExportOptions returns new options for the export command based on file/flags.
   323  func NewExportOptions(
   324  	globalOptions *GlobalOptions,
   325  	namespaceFlag string,
   326  	selectorFlag string,
   327  	excludeFlag []string,
   328  	templateDirFlag string,
   329  	paramDirFlag string,
   330  	withAnnotationsFlag bool,
   331  	withHardcodedNamespaceFlag bool,
   332  	trimAnnotationsFlag []string,
   333  	resourceArg string) (*ExportOptions, error) {
   334  	o := &ExportOptions{
   335  		GlobalOptions:    globalOptions,
   336  		NamespaceOptions: &NamespaceOptions{},
   337  	}
   338  	filename := o.resolvedFile(namespaceFlag)
   339  
   340  	fileFlags, err := getFileFlags(filename, verbose)
   341  	if err != nil {
   342  		return o, fmt.Errorf("Could not read %s: %s", filename, err)
   343  	}
   344  
   345  	if len(namespaceFlag) > 0 {
   346  		o.Namespace = namespaceFlag
   347  	} else if val, ok := fileFlags["namespace"]; ok {
   348  		o.Namespace = val
   349  	}
   350  
   351  	if len(selectorFlag) > 0 {
   352  		o.Selector = selectorFlag
   353  	} else if val, ok := fileFlags["selector"]; ok {
   354  		o.Selector = val
   355  	}
   356  
   357  	o.Excludes = []string{}
   358  	if len(excludeFlag) > 0 {
   359  		for _, val := range excludeFlag {
   360  			o.Excludes = append(o.Excludes, strings.Split(val, ",")...)
   361  		}
   362  	} else if val, ok := fileFlags["exclude"]; ok {
   363  		o.Excludes = strings.Split(val, ",")
   364  	}
   365  
   366  	o.TemplateDir = "."
   367  	if templateDirFlag != "." {
   368  		o.TemplateDir = templateDirFlag
   369  	} else if val, ok := fileFlags["template-dir"]; ok {
   370  		o.TemplateDir = val
   371  	}
   372  
   373  	o.ParamDir = "."
   374  	if paramDirFlag != "." {
   375  		o.ParamDir = paramDirFlag
   376  	} else if val, ok := fileFlags["param-dir"]; ok {
   377  		o.ParamDir = val
   378  	}
   379  
   380  	if withAnnotationsFlag {
   381  		o.WithAnnotations = true
   382  	} else if fileFlags["with-annotations"] == "true" {
   383  		o.WithAnnotations = true
   384  	}
   385  
   386  	if withHardcodedNamespaceFlag {
   387  		o.WithHardcodedNamespace = true
   388  	} else if fileFlags["with-hardcoded-namespace"] == "true" {
   389  		o.WithHardcodedNamespace = true
   390  	}
   391  
   392  	if len(trimAnnotationsFlag) > 0 {
   393  		o.TrimAnnotations = trimAnnotationsFlag
   394  	} else if val, ok := fileFlags["trim-annotation"]; ok {
   395  		o.TrimAnnotations = strings.Split(val, ",")
   396  	}
   397  
   398  	if len(resourceArg) > 0 {
   399  		o.Resource = resourceArg
   400  	} else if val, ok := fileFlags["resource"]; ok {
   401  		o.Resource = val
   402  	}
   403  
   404  	DebugMsg(fmt.Sprintf("%#v", o))
   405  
   406  	return o, o.check()
   407  }
   408  
   409  // NewSecretsOptions returns new options for the secrets subcommand based on file/flags.
   410  func NewSecretsOptions(
   411  	globalOptions *GlobalOptions,
   412  	paramDirFlag string,
   413  	publicKeyDirFlag string,
   414  	privateKeyFlag string,
   415  	passphraseFlag string) (*SecretsOptions, error) {
   416  	o := &SecretsOptions{
   417  		GlobalOptions: globalOptions,
   418  	}
   419  	namespaceFlag := "" // namespace does not make sense for secrets
   420  	filename := o.resolvedFile(namespaceFlag)
   421  
   422  	fileFlags, err := getFileFlags(filename, verbose)
   423  	if err != nil {
   424  		return o, fmt.Errorf("Could not read %s: %s", filename, err)
   425  	}
   426  
   427  	o.ParamDir = "."
   428  	if paramDirFlag != "." {
   429  		o.ParamDir = paramDirFlag
   430  	} else if val, ok := fileFlags["param-dir"]; ok {
   431  		o.ParamDir = val
   432  	}
   433  
   434  	o.PublicKeyDir = "."
   435  	if publicKeyDirFlag != "." {
   436  		o.PublicKeyDir = publicKeyDirFlag
   437  	} else if val, ok := fileFlags["public-key-dir"]; ok {
   438  		o.PublicKeyDir = val
   439  	}
   440  
   441  	o.PrivateKey = "private.key"
   442  	if privateKeyFlag != "private.key" {
   443  		o.PrivateKey = privateKeyFlag
   444  	} else if val, ok := fileFlags["private-key"]; ok {
   445  		o.PrivateKey = val
   446  	}
   447  
   448  	DebugMsg(fmt.Sprintf("%#v", o))
   449  
   450  	return o, o.check()
   451  }
   452  
   453  // resolvedFile returns either the user-supplied value, or, if the default is used
   454  // AND a namespaceFlag is given, "Tailorfile.${NAMESPACE}" (if it exists).
   455  func (o *GlobalOptions) resolvedFile(namespaceFlag string) string {
   456  	if o.File != "Tailorfile" {
   457  		return o.File
   458  	}
   459  	if len(namespaceFlag) == 0 {
   460  		return o.File
   461  	}
   462  	namespacedFile := fmt.Sprintf("%s.%s", o.File, namespaceFlag)
   463  	if _, err := o.fs.Stat(namespacedFile); os.IsNotExist(err) {
   464  		return o.File
   465  	}
   466  	return namespacedFile
   467  }
   468  
   469  // FileExists checks whether given file exists.
   470  func (o *GlobalOptions) FileExists(file string) bool {
   471  	_, err := o.fs.Stat(file)
   472  	return !os.IsNotExist(err)
   473  }
   474  
   475  func (o *GlobalOptions) check(clusterRequired bool) error {
   476  	if !o.checkOcBinary() {
   477  		return fmt.Errorf("No such oc binary: %s", o.OcBinary)
   478  	}
   479  	if clusterRequired {
   480  		if !o.checkLoggedIn() {
   481  			return errors.New("You need to login with 'oc login' first")
   482  		}
   483  		c := NewOcClient("")
   484  		if v := ocVersion(c); !v.ExactMatch() {
   485  			if v.Incomplete() {
   486  				VerboseMsg(fmt.Sprintf("Version information is incomplete: client (%s) and server (%s) detected. "+
   487  					"This is likely due to a local cluster setup. "+
   488  					"If not, this could lead to incorrect behaviour.", v.client, v.server))
   489  			} else {
   490  				errorMsg := fmt.Sprintf("Version mismatch between client (%s) and server (%s) detected. "+
   491  					"This can lead to incorrect behaviour. "+
   492  					"Update your oc binary or point to an alternative binary with --oc-binary.", v.client, v.server)
   493  				if !o.Force {
   494  					return fmt.Errorf("%s\n\nRefusing to continue without --force", errorMsg)
   495  				}
   496  			}
   497  		}
   498  	}
   499  	return nil
   500  }
   501  
   502  func (o *GlobalOptions) checkLoggedIn() bool {
   503  	if !o.IsLoggedIn {
   504  		c := NewOcClient("")
   505  		loggedIn, err := c.CheckLoggedIn()
   506  		if err != nil {
   507  			VerboseMsg(err.Error())
   508  		}
   509  		o.IsLoggedIn = loggedIn
   510  	}
   511  	return o.IsLoggedIn
   512  }
   513  
   514  func (o *GlobalOptions) checkOcBinary() bool {
   515  	if !strings.Contains(o.OcBinary, string(os.PathSeparator)) {
   516  		_, err := exec.LookPath(o.OcBinary)
   517  		return err == nil
   518  	}
   519  	_, err := os.Stat(o.OcBinary)
   520  	return !os.IsNotExist(err)
   521  }
   522  
   523  func (o *CompareOptions) check(clusterRequired bool) error {
   524  	// Check if template dir exists
   525  	if o.TemplateDir != "." {
   526  		td := o.TemplateDir
   527  		if _, err := os.Stat(td); os.IsNotExist(err) {
   528  			return fmt.Errorf("Template directory '%s' does not exist", td)
   529  		}
   530  	}
   531  	// Check if param dir exists
   532  	if o.ParamDir != "." {
   533  		pd := o.ParamDir
   534  		if _, err := os.Stat(pd); os.IsNotExist(err) {
   535  			return fmt.Errorf("Param directory '%s' does not exist", pd)
   536  		}
   537  	}
   538  
   539  	if strings.Contains(o.Resource, "/") && len(o.Selector) > 0 {
   540  		DebugMsg("Ignoring selector", o.Selector, "as resource is given")
   541  		o.Selector = ""
   542  	}
   543  
   544  	return o.setNamespace(clusterRequired)
   545  }
   546  
   547  func (o *CompareOptions) PathsToPreserve() []string {
   548  	pathsToPreserve := []string{}
   549  	if o.PreserveImmutableFields {
   550  		pathsToPreserve = append(
   551  			pathsToPreserve,
   552  			"pvc:/spec/accessModes",
   553  			"pvc:/spec/storageClassName",
   554  			"pvc:/spec/resources/requests/storage",
   555  			"route:/spec/host",
   556  			"secret:/type",
   557  		)
   558  	}
   559  	return append(pathsToPreserve, o.PreservePaths...)
   560  }
   561  
   562  func (o *ExportOptions) check() error {
   563  	if strings.Contains(o.Resource, "/") && len(o.Selector) > 0 {
   564  		DebugMsg("Ignoring selector", o.Selector, "as resource is given")
   565  		o.Selector = ""
   566  	}
   567  
   568  	return o.setNamespace(o.ClusterRequired)
   569  }
   570  
   571  func (o *SecretsOptions) check() error {
   572  	return nil
   573  }
   574  
   575  func (o *NamespaceOptions) setNamespace(clusterRequired bool) error {
   576  	if clusterRequired {
   577  		if len(o.Namespace) == 0 {
   578  			n, err := getOcNamespace()
   579  			if err != nil {
   580  				return err
   581  			}
   582  			o.Namespace = n
   583  		} else {
   584  			err := o.checkOcNamespace(o.Namespace)
   585  			if err != nil {
   586  				return fmt.Errorf("No such project: %s", o.Namespace)
   587  			}
   588  		}
   589  	}
   590  	return nil
   591  }
   592  
   593  func (o *NamespaceOptions) checkOcNamespace(n string) error {
   594  	if utils.Includes(o.CheckedNamespaces, n) {
   595  		return nil
   596  	}
   597  	c := NewOcClient("")
   598  	exists, err := c.CheckProjectExists(n)
   599  	if exists {
   600  		o.CheckedNamespaces = append(o.CheckedNamespaces, n)
   601  	}
   602  	return err
   603  }
   604  
   605  func getOcNamespace() (string, error) {
   606  	c := NewOcClient("")
   607  	return c.CurrentProject()
   608  }
   609  
   610  func getFileFlags(filename string, verbose bool) (map[string]string, error) {
   611  	fileFlags := make(map[string]string)
   612  	if _, err := os.Stat(filename); os.IsNotExist(err) {
   613  		if filename == "Tailorfile" {
   614  			if verbose {
   615  				PrintBluef("--> No file '%s' found.\n", filename)
   616  			}
   617  			return fileFlags, nil
   618  		}
   619  		return fileFlags, err
   620  	}
   621  
   622  	b, err := ioutil.ReadFile(filename)
   623  	if err != nil {
   624  		return fileFlags, err
   625  	}
   626  	content := string(b)
   627  	text := strings.TrimSuffix(content, "\n")
   628  	lines := strings.Split(text, "\n")
   629  
   630  	for _, untrimmedLine := range lines {
   631  		line := strings.TrimSpace(untrimmedLine)
   632  		if len(line) == 0 || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
   633  			continue
   634  		}
   635  		pair := strings.SplitN(line, " ", 2)
   636  		if len(pair) == 2 {
   637  			key := pair[0]
   638  			value := strings.TrimSpace(pair[1])
   639  			if val, ok := fileFlags[key]; ok {
   640  				value = val + "," + value
   641  			}
   642  			fileFlags[key] = value
   643  		} else {
   644  			fileFlags["resource"] = pair[0]
   645  		}
   646  	}
   647  	return fileFlags, nil
   648  }