github.com/verrazzano/verrazzano@v1.7.0/pkg/helm/helmcli.go (about) 1 // Copyright (c) 2020, 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 helm 5 6 import ( 7 "fmt" 8 "io" 9 "net/url" 10 "os" 11 "regexp" 12 "strings" 13 14 yaml2 "github.com/verrazzano/verrazzano/pkg/yaml" 15 "helm.sh/helm/v3/pkg/action" 16 "helm.sh/helm/v3/pkg/chart" 17 "helm.sh/helm/v3/pkg/chart/loader" 18 "helm.sh/helm/v3/pkg/cli" 19 "helm.sh/helm/v3/pkg/getter" 20 "helm.sh/helm/v3/pkg/release" 21 "helm.sh/helm/v3/pkg/strvals" 22 "sigs.k8s.io/yaml" 23 24 "github.com/verrazzano/verrazzano/pkg/log/vzlog" 25 "go.uber.org/zap" 26 ) 27 28 // Debug is set from a platform-operator arg and sets the helm --debug flag 29 var Debug bool 30 31 // Helm chart status values: unknown, deployed, uninstalled, superseded, failed, uninstalling, pending-install, pending-upgrade or pending-rollback 32 const ChartNotFound = "NotFound" 33 const ChartStatusDeployed = "deployed" 34 const ChartStatusPendingInstall = "pending-install" 35 const ChartStatusFailed = "failed" 36 37 // ChartStatusFnType - Package-level var and functions to allow overriding GetChartStatus for unit test purposes 38 type ChartStatusFnType func(releaseName string, namespace string) (string, error) 39 40 // HelmOverrides contains all of the overrides that gets passed to the helm cli runner 41 type HelmOverrides struct { 42 SetOverrides string // for --set 43 SetStringOverrides string // for --set-string 44 SetFileOverrides string // for --set-file 45 FileOverride string // for -f 46 } 47 48 type ActionConfigFnType func(log vzlog.VerrazzanoLogger, settings *cli.EnvSettings, namespace string) (*action.Configuration, error) 49 50 var actionConfigFn ActionConfigFnType = getActionConfig 51 52 func SetActionConfigFunction(f ActionConfigFnType) { 53 actionConfigFn = f 54 } 55 56 // SetDefaultActionConfigFunction Resets the action config function 57 func SetDefaultActionConfigFunction() { 58 actionConfigFn = getActionConfig 59 } 60 61 type LoadChartFnType func(chartDir string) (*chart.Chart, error) 62 63 var loadChartFn LoadChartFnType = loadChart 64 65 func SetLoadChartFunction(f LoadChartFnType) { 66 loadChartFn = f 67 } 68 69 func SetDefaultLoadChartFunction() { 70 loadChartFn = loadChart 71 } 72 73 // GetValuesMap will run 'helm get values' command and return the output from the command. 74 func GetValuesMap(log vzlog.VerrazzanoLogger, releaseName string, namespace string) (map[string]interface{}, error) { 75 settings := cli.New() 76 settings.SetNamespace(namespace) 77 actionConfig, err := actionConfigFn(log, settings, namespace) 78 if err != nil { 79 return nil, err 80 } 81 82 client := action.NewGetValues(actionConfig) 83 vals, err := client.Run(releaseName) 84 if err != nil { 85 return nil, err 86 } 87 88 return vals, nil 89 } 90 91 // GetValues will run 'helm get values' command and return the output from the command. 92 func GetValues(log vzlog.VerrazzanoLogger, releaseName string, namespace string) ([]byte, error) { 93 vals, err := GetValuesMap(log, releaseName, namespace) 94 if err != nil { 95 return nil, err 96 } 97 98 yamlValues, err := yaml.Marshal(vals) 99 if err != nil { 100 return nil, err 101 } 102 return yamlValues, nil 103 } 104 105 // Upgrade will upgrade a Helm helmRelease with the specified charts. The override files array 106 // are in order with the first files in the array have lower precedence than latter files. 107 func Upgrade(log vzlog.VerrazzanoLogger, releaseName string, namespace string, chartDir string, wait bool, dryRun bool, overrides []HelmOverrides) (*release.Release, error) { 108 settings := cli.New() 109 settings.SetNamespace(namespace) 110 actionConfig, err := actionConfigFn(log, settings, namespace) 111 if err != nil { 112 return nil, err 113 } 114 115 p := getter.All(settings) 116 vals, err := mergeValues(overrides, p) 117 if err != nil { 118 return nil, err 119 } 120 // load chart from the path 121 chart, err := loadChartFn(chartDir) 122 if err != nil { 123 return nil, err 124 } 125 installed, err := IsReleaseInstalled(releaseName, namespace) 126 if err != nil { 127 return nil, err 128 } 129 130 var rel *release.Release 131 if installed { 132 // upgrade it 133 log.Progressf("Starting Helm upgrade of release %s in namespace %s with overrides: %v", releaseName, namespace, overrides) 134 client := action.NewUpgrade(actionConfig) 135 client.Namespace = namespace 136 client.DryRun = dryRun 137 client.Wait = wait 138 client.MaxHistory = 1 139 140 rel, err = client.Run(releaseName, chart, vals) 141 if err != nil { 142 return nil, log.ErrorfThrottledNewErr("Failed running Helm command for release %s, error: %s", 143 releaseName, err.Error()) 144 } 145 } else { 146 log.Progressf("Starting Helm installation of release %s in namespace %s with overrides: %v", releaseName, namespace, overrides) 147 client := action.NewInstall(actionConfig) 148 client.Namespace = namespace 149 client.ReleaseName = releaseName 150 client.DryRun = dryRun 151 client.Replace = true 152 client.Wait = wait 153 154 rel, err = client.Run(chart, vals) 155 if err != nil { 156 log.ErrorfThrottled("Failed running Helm command for release %s: %v", 157 releaseName, err.Error()) 158 return nil, err 159 } 160 } 161 162 log.Progressf("Helm upgraded/installed %s in namespace %s", rel.Name, rel.Namespace) 163 164 return rel, nil 165 } 166 167 // Uninstall will uninstall the helmRelease in the specified namespace using helm uninstall 168 func Uninstall(log vzlog.VerrazzanoLogger, releaseName string, namespace string, dryRun bool) (err error) { 169 settings := cli.New() 170 settings.SetNamespace(namespace) 171 actionConfig, err := actionConfigFn(log, settings, namespace) 172 if err != nil { 173 return err 174 } 175 176 client := action.NewUninstall(actionConfig) 177 client.DryRun = dryRun 178 179 _, err = client.Run(releaseName) 180 if err != nil { 181 log.Errorf("Error uninstalling release %s: %s", releaseName, err.Error()) 182 return err 183 } 184 185 return nil 186 } 187 188 // maskSensitiveData replaces sensitive data in a string with mask characters. 189 func maskSensitiveData(str string) string { 190 const maskString = "*****" 191 re := regexp.MustCompile(`[Pp]assword=(.+?)(?:,|\z)`) 192 193 matches := re.FindAllStringSubmatch(str, -1) 194 for _, match := range matches { 195 if len(match) == 2 { 196 str = strings.Replace(str, match[1], maskString, 1) 197 } 198 } 199 200 return str 201 } 202 203 // IsReleaseFailed Returns true if the chart helmRelease state is marked 'failed' 204 func IsReleaseFailed(releaseName string, namespace string) (bool, error) { 205 log := zap.S() 206 releaseStatus, err := getReleaseState(releaseName, namespace) 207 if err != nil { 208 log.Errorf("Getting status for chart %s/%s failed", namespace, releaseName) 209 return false, err 210 } 211 return releaseStatus == ChartStatusFailed, nil 212 } 213 214 // IsReleaseDeployed returns true if the helmRelease is deployed 215 func IsReleaseDeployed(releaseName string, namespace string) (found bool, err error) { 216 log := zap.S() 217 releaseStatus, err := getChartStatus(releaseName, namespace) 218 if err != nil { 219 log.Errorf("Getting status for chart %s/%s failed with error: %v\n", namespace, releaseName, err) 220 return false, err 221 } 222 switch releaseStatus { 223 case ChartNotFound: 224 log.Debugf("releasename=%s/%s; status= %s", namespace, releaseName, releaseStatus) 225 case ChartStatusDeployed: 226 return true, nil 227 } 228 return false, nil 229 } 230 231 // GetReleaseStatus returns the helmRelease status 232 func GetReleaseStatus(log vzlog.VerrazzanoLogger, releaseName string, namespace string) (status string, err error) { 233 releaseStatus, err := getChartStatus(releaseName, namespace) 234 if err != nil { 235 log.ErrorfNewErr("Failed getting status for chart %s/%s with stderr: %v\n", namespace, releaseName, err) 236 return "", err 237 } 238 if releaseStatus == ChartNotFound { 239 log.Debugf("Chart %s/%s not found", namespace, releaseName) 240 } 241 return releaseStatus, nil 242 } 243 244 // IsReleaseInstalled returns true if the helmRelease is installed 245 func IsReleaseInstalled(releaseName string, namespace string) (found bool, err error) { 246 settings := cli.New() 247 settings.SetNamespace(namespace) 248 actionConfig, err := actionConfigFn(vzlog.DefaultLogger(), settings, namespace) 249 if err != nil { 250 return false, err 251 } 252 253 client := action.NewStatus(actionConfig) 254 helmRelease, err := client.Run(releaseName) 255 if err != nil { 256 if strings.Contains(err.Error(), "not found") { 257 return false, nil 258 } 259 return false, err 260 } 261 return release.StatusDeployed == helmRelease.Info.Status || release.StatusFailed == helmRelease.Info.Status, nil 262 } 263 264 // ReleaseExists returns true if the helm Release exists in the cluster in any state 265 func ReleaseExists(releaseName string, namespace string) (found bool, err error) { 266 settings := cli.New() 267 settings.SetNamespace(namespace) 268 actionConfig, err := actionConfigFn(vzlog.DefaultLogger(), settings, namespace) 269 if err != nil { 270 return false, err 271 } 272 273 client := action.NewStatus(actionConfig) 274 helmRelease, err := client.Run(releaseName) 275 if err != nil { 276 if strings.Contains(err.Error(), "not found") { 277 return false, nil 278 } 279 return false, err 280 } 281 return len(helmRelease.Info.Status) > 0, nil 282 } 283 284 // getChartStatus extracts the Helm deployment status of the specified chart from the JSON output as a string 285 func getChartStatus(releaseName string, namespace string) (string, error) { 286 settings := cli.New() 287 settings.SetNamespace(namespace) 288 actionConfig, err := actionConfigFn(vzlog.DefaultLogger(), settings, namespace) 289 if err != nil { 290 return "", err 291 } 292 293 client := action.NewStatus(actionConfig) 294 helmRelease, err := client.Run(releaseName) 295 if err != nil { 296 if strings.Contains(err.Error(), "not found") { 297 return ChartNotFound, nil 298 } 299 return "", err 300 } 301 302 return helmRelease.Info.Status.String(), nil 303 } 304 305 // getReleaseState extracts the helmRelease state from an "ls -o json" command for a specific helmRelease/namespace 306 func getReleaseState(releaseName string, namespace string) (string, error) { 307 releases, err := getReleases(namespace) 308 if err != nil { 309 if strings.Contains(err.Error(), "not found") { 310 return ChartNotFound, nil 311 } 312 return "", err 313 } 314 315 status := "" 316 for _, info := range releases { 317 release := info.Name 318 if release == releaseName { 319 status = info.Info.Status.String() 320 break 321 } 322 } 323 return strings.TrimSpace(status), nil 324 } 325 326 // GetReleaseAppVersion - public function to execute releaseAppVersionFn 327 func GetReleaseAppVersion(releaseName string, namespace string) (string, error) { 328 return getReleaseAppVersion(releaseName, namespace) 329 } 330 331 // GetReleaseStringValues - Returns a subset of Helm helmRelease values as a map of strings 332 func GetReleaseStringValues(log vzlog.VerrazzanoLogger, valueKeys []string, releaseName string, namespace string) (map[string]string, error) { 333 values, err := GetReleaseValues(log, valueKeys, releaseName, namespace) 334 if err != nil { 335 return map[string]string{}, err 336 } 337 returnVals := map[string]string{} 338 for key, val := range values { 339 returnVals[key] = fmt.Sprintf("%v", val) 340 } 341 return returnVals, err 342 } 343 344 // GetReleaseValues - Returns a subset of Helm helmRelease values as a map of objects 345 func GetReleaseValues(log vzlog.VerrazzanoLogger, valueKeys []string, releaseName string, namespace string) (map[string]interface{}, error) { 346 isDeployed, err := IsReleaseDeployed(releaseName, namespace) 347 if err != nil { 348 return map[string]interface{}{}, err 349 } 350 var values = map[string]interface{}{} 351 if isDeployed { 352 valuesMap, err := GetValuesMap(log, releaseName, namespace) 353 if err != nil { 354 return map[string]interface{}{}, err 355 } 356 for _, valueKey := range valueKeys { 357 if mapVal, ok := valuesMap[valueKey]; ok { 358 log.Debugf("Found value for %s: %v", valueKey, mapVal) 359 values[valueKey] = mapVal 360 } 361 } 362 } 363 return values, nil 364 } 365 366 // getReleaseAppVersion extracts the helmRelease app_version from a "ls -o json" command for a specific helmRelease/namespace 367 func getReleaseAppVersion(releaseName string, namespace string) (string, error) { 368 releases, err := getReleases(namespace) 369 if err != nil { 370 if err.Error() == ChartNotFound { 371 return ChartNotFound, nil 372 } 373 return "", err 374 } 375 376 var status string 377 for _, info := range releases { 378 release := info.Name 379 if release == releaseName { 380 status = info.Chart.AppVersion() 381 break 382 } 383 } 384 return strings.TrimSpace(status), nil 385 } 386 387 func getReleases(namespace string) ([]*release.Release, error) { 388 settings := cli.New() 389 settings.SetNamespace(namespace) 390 actionConfig, err := actionConfigFn(vzlog.DefaultLogger(), settings, namespace) 391 if err != nil { 392 return nil, err 393 } 394 395 client := action.NewList(actionConfig) 396 client.AllNamespaces = false 397 client.All = true 398 client.StateMask = action.ListAll 399 400 releases, err := client.Run() 401 if err != nil { 402 return nil, err 403 } 404 405 return releases, nil 406 } 407 408 func getActionConfig(log vzlog.VerrazzanoLogger, settings *cli.EnvSettings, namespace string) (*action.Configuration, error) { 409 actionConfig := new(action.Configuration) 410 if err := actionConfig.Init(settings.RESTClientGetter(), namespace, os.Getenv("HELM_DRIVER"), log.Debugf); err != nil { 411 return nil, err 412 } 413 actionConfig.Releases.MaxHistory = 1 414 return actionConfig, nil 415 } 416 417 func loadChart(chartDir string) (*chart.Chart, error) { 418 return loader.Load(chartDir) 419 } 420 421 // readFile load a file using a URI scheme provider 422 func readFile(filePath string, p getter.Providers) ([]byte, error) { 423 if strings.TrimSpace(filePath) == "-" { 424 return io.ReadAll(os.Stdin) 425 } 426 u, err := url.Parse(filePath) 427 if err != nil { 428 return nil, err 429 } 430 431 g, err := p.ByScheme(u.Scheme) 432 if err != nil { 433 return os.ReadFile(filePath) 434 } 435 data, err := g.Get(filePath, getter.WithURL(filePath)) 436 if err != nil { 437 return nil, err 438 } 439 return data.Bytes(), err 440 } 441 442 // mergeValues merges values from the specified overrides 443 func mergeValues(overrides []HelmOverrides, p getter.Providers) (map[string]interface{}, error) { 444 base := map[string]interface{}{} 445 446 // User specified a values files via -f/--values 447 for _, override := range overrides { 448 if len(override.FileOverride) > 0 { 449 currentMap := map[string]interface{}{} 450 451 bytes, err := readFile(override.FileOverride, p) 452 if err != nil { 453 return nil, err 454 } 455 456 if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { 457 return nil, err 458 } 459 // Merge with the previous map 460 yaml2.MergeMaps(base, currentMap) 461 } 462 463 // User specified a value via --set 464 if len(override.SetOverrides) > 0 { 465 if err := strvals.ParseInto(override.SetOverrides, base); err != nil { 466 return nil, err 467 } 468 } 469 470 // User specified a value via --set-string 471 if len(override.SetStringOverrides) > 0 { 472 if err := strvals.ParseIntoString(override.SetStringOverrides, base); err != nil { 473 return nil, err 474 } 475 } 476 477 // User specified a value via --set-file 478 if len(override.SetFileOverrides) > 0 { 479 reader := func(rs []rune) (interface{}, error) { 480 bytes, err := readFile(string(rs), p) 481 if err != nil { 482 return nil, err 483 } 484 return string(bytes), err 485 } 486 if err := strvals.ParseIntoFile(override.SetFileOverrides, base, reader); err != nil { 487 return nil, err 488 } 489 } 490 } 491 492 return base, nil 493 }