github.com/verrazzano/verrazzano@v1.7.0/tools/vz/cmd/helpers/command.go (about)

     1  // Copyright (c) 2022, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package helpers
     5  
     6  import (
     7  	"bufio"
     8  	"fmt"
     9  	"helm.sh/helm/v3/pkg/strvals"
    10  	"os"
    11  	"sigs.k8s.io/yaml"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/spf13/cobra"
    16  	"github.com/verrazzano/verrazzano/pkg/k8sutil"
    17  	"github.com/verrazzano/verrazzano/pkg/semver"
    18  	"github.com/verrazzano/verrazzano/tools/vz/pkg/constants"
    19  	"github.com/verrazzano/verrazzano/tools/vz/pkg/helpers"
    20  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    21  )
    22  
    23  // NewCommand - utility method to create cobra commands
    24  func NewCommand(vzHelper helpers.VZHelper, use string, short string, long string) *cobra.Command {
    25  	cmd := &cobra.Command{
    26  		Use:   use,
    27  		Short: short,
    28  		Long:  long,
    29  	}
    30  
    31  	// Configure the IO streams
    32  	cmd.SetOut(vzHelper.GetOutputStream())
    33  	cmd.SetErr(vzHelper.GetErrorStream())
    34  	cmd.SetIn(vzHelper.GetInputStream())
    35  
    36  	// Disable usage output on errors
    37  	cmd.SilenceUsage = true
    38  	return cmd
    39  }
    40  
    41  // GetWaitTimeout returns the time to wait for a command to complete
    42  func GetWaitTimeout(cmd *cobra.Command, timeoutFlag string) (time.Duration, error) {
    43  	// Get the wait value from the command line
    44  	wait, err := cmd.PersistentFlags().GetBool(constants.WaitFlag)
    45  	if err != nil {
    46  		return time.Duration(0), err
    47  	}
    48  	if wait {
    49  		timeout, err := cmd.PersistentFlags().GetDuration(timeoutFlag)
    50  		if err != nil {
    51  			return time.Duration(0), err
    52  		}
    53  		return timeout, nil
    54  	}
    55  
    56  	// Return duration of zero since --wait=false was specified
    57  	return time.Duration(0), nil
    58  }
    59  
    60  // GetLogFormat returns the format type for streaming log output
    61  func GetLogFormat(cmd *cobra.Command) (LogFormat, error) {
    62  	// Get the log format value from the command line
    63  	logFormat := cmd.PersistentFlags().Lookup(constants.LogFormatFlag)
    64  	if logFormat == nil {
    65  		return LogFormatSimple, nil
    66  	}
    67  
    68  	return LogFormat(logFormat.Value.String()), nil
    69  }
    70  
    71  // GetVersion returns the version of Verrazzano to install/upgrade
    72  func GetVersion(cmd *cobra.Command, vzHelper helpers.VZHelper) (string, error) {
    73  	// Get the version from the command line
    74  	version, err := cmd.PersistentFlags().GetString(constants.VersionFlag)
    75  	if err != nil {
    76  		return "", err
    77  	}
    78  
    79  	// If the user has provided an operator YAML, attempt to get the version from the VPO deployment
    80  	if ManifestsFlagChanged(cmd) {
    81  		manifestsVersion, err := getVersionFromOperatorYAML(cmd, vzHelper)
    82  		if err != nil {
    83  			return "", err
    84  		}
    85  
    86  		if manifestsVersion != "" {
    87  			// If the user has explicitly passed a version, make sure it matches the version in the manifests
    88  			if cmd.PersistentFlags().Changed(constants.VersionFlag) {
    89  				match, err := versionsMatch(manifestsVersion, version)
    90  				if err != nil {
    91  					return "", err
    92  				}
    93  				if match {
    94  					// Return version and not manifestsVersion because version may have prerelease and build values that are
    95  					// not present in the manifests version, and make sure it has a "v" prefix
    96  					if !strings.HasPrefix(version, "v") {
    97  						version = "v" + version
    98  					}
    99  					return version, nil
   100  				}
   101  				return "", fmt.Errorf("Requested version '%s' does not match manifests version '%s'", version, manifestsVersion)
   102  			}
   103  
   104  			return manifestsVersion, nil
   105  		}
   106  	}
   107  
   108  	if version == constants.VersionFlagDefault {
   109  		// Find the latest release version of Verrazzano
   110  		version, err = helpers.GetLatestReleaseVersion(vzHelper.GetHTTPClient())
   111  		if err != nil {
   112  			return "", err
   113  		}
   114  	} else {
   115  		// Validate the version string
   116  		installVersion, err := semver.NewSemVersion(version)
   117  		if err != nil {
   118  			return "", err
   119  		}
   120  		version = fmt.Sprintf("v%s", installVersion.ToString())
   121  	}
   122  	return version, nil
   123  }
   124  
   125  // getVersionFromOperatorYAML attempts to parse the user-provided operator YAML and returns the
   126  // Verrazzano version from a label on the verrazzano-platform-operator deployment.
   127  func getVersionFromOperatorYAML(cmd *cobra.Command, vzHelper helpers.VZHelper) (string, error) {
   128  	localOperatorFilename, userVisibleFilename, isTempFile, err := getOrDownloadOperatorYAML(cmd, "", vzHelper)
   129  	if err != nil {
   130  		return "", err
   131  	}
   132  	if isTempFile {
   133  		// the operator YAML is a temporary file that must be deleted after applying it
   134  		defer os.Remove(localOperatorFilename)
   135  	}
   136  
   137  	fileObj, err := os.Open(localOperatorFilename)
   138  	defer func() { fileObj.Close() }()
   139  	if err != nil {
   140  		return "", err
   141  	}
   142  	objectsInYAML, err := k8sutil.Unmarshall(bufio.NewReader(fileObj))
   143  	if err != nil {
   144  		return "", err
   145  	}
   146  	vpoDeployIdx, _ := findVPODeploymentIndices(objectsInYAML)
   147  	if vpoDeployIdx == -1 {
   148  		return "", fmt.Errorf("Unable to find verrazzano-platform-operator deployment in operator file: %s", userVisibleFilename)
   149  	}
   150  
   151  	vpoDeploy := &objectsInYAML[vpoDeployIdx]
   152  	version, found, err := unstructured.NestedString(vpoDeploy.Object, "metadata", "labels", "app.kubernetes.io/version")
   153  	if err != nil || !found {
   154  		return "", err
   155  	}
   156  
   157  	// versions we return are expected to start with "v"
   158  	if !strings.HasPrefix(version, "v") {
   159  		version = "v" + version
   160  	}
   161  	return version, err
   162  }
   163  
   164  // versionsMatch returns true if the versions are semantically equivalent. Only the major, minor, and patch fields are considered.
   165  func versionsMatch(left, right string) (bool, error) {
   166  	leftVersion, err := semver.NewSemVersion(left)
   167  	if err != nil {
   168  		return false, err
   169  	}
   170  	rightVersion, err := semver.NewSemVersion(right)
   171  	if err != nil {
   172  		return false, err
   173  	}
   174  
   175  	// When comparing the versions, ignore the prerelease and build versions. This is needed to support development scenarios
   176  	// where the version to upgrade to looks like x.y.z-nnnn+hash but the VPO version label is x.y.z.
   177  	leftVersion.Prerelease = ""
   178  	leftVersion.Build = ""
   179  	rightVersion.Prerelease = ""
   180  	rightVersion.Build = ""
   181  
   182  	return leftVersion.IsEqualTo(rightVersion), nil
   183  }
   184  
   185  // ConfirmWithUser asks the user a yes/no question and returns true if the user answered yes, false
   186  // otherwise.
   187  func ConfirmWithUser(vzHelper helpers.VZHelper, questionText string, skipQuestion bool) (bool, error) {
   188  	if skipQuestion {
   189  		return true, nil
   190  	}
   191  	var response string
   192  	scanner := bufio.NewScanner(vzHelper.GetInputStream())
   193  	fmt.Fprintf(vzHelper.GetOutputStream(), "%s [y/N]: ", questionText)
   194  	if scanner.Scan() {
   195  		response = scanner.Text()
   196  	}
   197  	if err := scanner.Err(); err != nil {
   198  		return false, err
   199  	}
   200  	if response == "y" || response == "Y" {
   201  		return true, nil
   202  	}
   203  	return false, nil
   204  }
   205  
   206  // getOperatorFileFromFlag returns the value for the manifests (or the alias operator-file) option
   207  func getOperatorFileFromFlag(cmd *cobra.Command) (string, error) {
   208  	// Get the value from the command line
   209  	operatorFile, err := getManifestsFile(cmd)
   210  	if err != nil {
   211  		return "", fmt.Errorf("Failed to parse the command line option %s: %s", constants.ManifestsFlag, err.Error())
   212  	}
   213  	return operatorFile, nil
   214  }
   215  
   216  // getManifestsFile returns the manifests file, which could come from the manifests flag or the
   217  // deprecated operator-file flag
   218  func getManifestsFile(cmd *cobra.Command) (string, error) {
   219  	// if manifests flag has been explicitly provided, use that. Else if operator-file flag is
   220  	// explicitly provided, use that. If neither is explicitly provided, use the default for the
   221  	// manifests flag
   222  	if cmd.PersistentFlags().Changed(constants.ManifestsFlag) {
   223  		return cmd.PersistentFlags().GetString(constants.ManifestsFlag)
   224  	}
   225  	if cmd.PersistentFlags().Changed(constants.OperatorFileFlag) {
   226  		return cmd.PersistentFlags().GetString(constants.OperatorFileFlag)
   227  	}
   228  	// neither is explicitly specified, use the default value of manifests flag
   229  	return cmd.PersistentFlags().GetString(constants.ManifestsFlag)
   230  }
   231  
   232  // ManifestsFlagChanged returns whether the manifests flag (or deprecated operator-file flag) is specified.
   233  func ManifestsFlagChanged(cmd *cobra.Command) bool {
   234  	return cmd.PersistentFlags().Changed(constants.ManifestsFlag) || cmd.PersistentFlags().Changed(constants.OperatorFileFlag)
   235  }
   236  
   237  // AddManifestsFlags adds flags related to providing manifests (including the deprecated
   238  // operator-file flag as an alias for the manifests flag)
   239  func AddManifestsFlags(cmd *cobra.Command) {
   240  	cmd.PersistentFlags().StringP(constants.ManifestsFlag, constants.ManifestsShorthand, "", constants.ManifestsFlagHelp)
   241  	// The operator-file flag is left in as an alias for the manifests flag
   242  	cmd.PersistentFlags().String(constants.OperatorFileFlag, "", constants.ManifestsFlagHelp)
   243  	cmd.PersistentFlags().MarkDeprecated(constants.OperatorFileFlag, constants.OperatorFileDeprecateMsg)
   244  }
   245  
   246  // GetSetArguments gets all the set arguments and returns a map of property/value
   247  func GetSetArguments(cmd *cobra.Command, vzHelper helpers.VZHelper) (map[string]string, error) {
   248  	setMap := make(map[string]string)
   249  	setFlags, err := cmd.PersistentFlags().GetStringArray(constants.SetFlag)
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  
   254  	invalidFlag := false
   255  	for _, setFlag := range setFlags {
   256  		pv := strings.Split(setFlag, "=")
   257  		if len(pv) != 2 {
   258  			fmt.Fprintf(vzHelper.GetErrorStream(), fmt.Sprintf("Invalid set flag \"%s\" specified. Flag must be specified in the format path=value\n", setFlag))
   259  			invalidFlag = true
   260  			continue
   261  		}
   262  		if !invalidFlag {
   263  			path, value := strings.TrimSpace(pv[0]), strings.TrimSpace(pv[1])
   264  			if !strings.HasPrefix(path, "spec.") {
   265  				path = "spec." + path
   266  			}
   267  			setMap[path] = value
   268  		}
   269  	}
   270  
   271  	if invalidFlag {
   272  		return nil, fmt.Errorf("Invalid set flag(s) specified")
   273  	}
   274  
   275  	return setMap, nil
   276  }
   277  
   278  // GenerateYAMLForSetFlags creates a YAML string from a map of property value pairs representing --set flags
   279  // specified on the install command
   280  func GenerateYAMLForSetFlags(pvs map[string]string) (string, error) {
   281  	yamlObject := map[string]interface{}{}
   282  	for path, value := range pvs {
   283  		// replace unwanted characters in the value to avoid splitting
   284  		ignoreChars := ",[.{}"
   285  		for _, char := range ignoreChars {
   286  			value = strings.Replace(value, string(char), "\\"+string(char), -1)
   287  		}
   288  
   289  		composedStr := fmt.Sprintf("%s=%s", path, value)
   290  		err := strvals.ParseInto(composedStr, yamlObject)
   291  		if err != nil {
   292  			return "", err
   293  		}
   294  	}
   295  
   296  	yamlFile, err := yaml.Marshal(yamlObject)
   297  	if err != nil {
   298  		return "", err
   299  	}
   300  
   301  	yamlString := string(yamlFile)
   302  
   303  	// Replace any double-quoted strings that are surrounded by single quotes.
   304  	// These type of strings are problematic for helm.
   305  	yamlString = strings.ReplaceAll(yamlString, "'\"", "\"")
   306  	yamlString = strings.ReplaceAll(yamlString, "\"'", "\"")
   307  
   308  	return yamlString, nil
   309  }