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 }