github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/util/helm/helm.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package helm 21 22 import ( 23 "bytes" 24 "context" 25 "fmt" 26 "io" 27 "os" 28 "os/signal" 29 "path/filepath" 30 "strings" 31 "syscall" 32 "time" 33 34 "github.com/Masterminds/semver/v3" 35 "github.com/containers/common/pkg/retry" 36 "github.com/ghodss/yaml" 37 "github.com/pkg/errors" 38 "github.com/spf13/pflag" 39 "helm.sh/helm/v3/pkg/action" 40 "helm.sh/helm/v3/pkg/chart/loader" 41 "helm.sh/helm/v3/pkg/chartutil" 42 "helm.sh/helm/v3/pkg/cli" 43 "helm.sh/helm/v3/pkg/cli/values" 44 "helm.sh/helm/v3/pkg/getter" 45 "helm.sh/helm/v3/pkg/helmpath" 46 kubefake "helm.sh/helm/v3/pkg/kube/fake" 47 "helm.sh/helm/v3/pkg/registry" 48 "helm.sh/helm/v3/pkg/release" 49 "helm.sh/helm/v3/pkg/repo" 50 "helm.sh/helm/v3/pkg/storage" 51 "helm.sh/helm/v3/pkg/storage/driver" 52 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 53 "k8s.io/cli-runtime/pkg/genericclioptions" 54 "k8s.io/client-go/rest" 55 "k8s.io/klog/v2" 56 57 "github.com/1aal/kubeblocks/pkg/cli/testing" 58 "github.com/1aal/kubeblocks/pkg/cli/types" 59 "github.com/1aal/kubeblocks/pkg/cli/util/breakingchange" 60 ) 61 62 const defaultTimeout = time.Second * 600 63 64 type InstallOpts struct { 65 Name string 66 Chart string 67 Namespace string 68 Wait bool 69 Version string 70 TryTimes int 71 Login bool 72 CreateNamespace bool 73 ValueOpts *values.Options 74 Timeout time.Duration 75 Atomic bool 76 DisableHooks bool 77 ForceUninstall bool 78 Upgrader breakingchange.Upgrader 79 80 // for helm template 81 DryRun *bool 82 OutputDir string 83 IncludeCRD bool 84 } 85 86 type Option func(*cli.EnvSettings) 87 88 // AddRepo adds a repo 89 func AddRepo(r *repo.Entry) error { 90 if r.Name == testing.KubeBlocksChartName { 91 return nil 92 } 93 settings := cli.New() 94 repoFile := settings.RepositoryConfig 95 b, err := os.ReadFile(repoFile) 96 if err != nil && !os.IsNotExist(err) { 97 return err 98 } 99 100 var f repo.File 101 if err = yaml.Unmarshal(b, &f); err != nil { 102 return err 103 } 104 105 // Check if the repo Name is legal 106 if strings.Contains(r.Name, "/") { 107 return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", r.Name) 108 } 109 110 if f.Has(r.Name) { 111 existing := f.Get(r.Name) 112 if *r != *existing && r.Name != types.KubeBlocksChartName { 113 // The input Name is different from the existing one, return an error 114 return errors.Errorf("repository name (%s) already exists, please specify a different name", r.Name) 115 } 116 } 117 118 cp, err := repo.NewChartRepository(r, getter.All(settings)) 119 if err != nil { 120 return err 121 } 122 123 if _, err := cp.DownloadIndexFile(); err != nil { 124 return errors.Wrapf(err, "looks like %q is not a valid Chart repository or cannot be reached", r.URL) 125 } 126 127 f.Update(r) 128 129 if err = f.WriteFile(repoFile, 0644); err != nil { 130 return err 131 } 132 return nil 133 } 134 135 // RemoveRepo removes a repo 136 func RemoveRepo(r *repo.Entry) error { 137 settings := cli.New() 138 repoFile := settings.RepositoryConfig 139 b, err := os.ReadFile(repoFile) 140 if err != nil && !os.IsNotExist(err) { 141 return err 142 } 143 144 var f repo.File 145 if err = yaml.Unmarshal(b, &f); err != nil { 146 return err 147 } 148 149 if f.Has(r.Name) { 150 f.Remove(r.Name) 151 if err = f.WriteFile(repoFile, 0644); err != nil { 152 return err 153 } 154 } 155 return nil 156 } 157 158 // GetInstalled gets helm package release info if installed. 159 func (i *InstallOpts) GetInstalled(cfg *action.Configuration) (*release.Release, error) { 160 res, err := action.NewGet(cfg).Run(i.Name) 161 if err != nil { 162 return nil, err 163 } 164 if res == nil { 165 return nil, driver.ErrReleaseNotFound 166 } 167 if !statusDeployed(res) { 168 return nil, errors.Wrapf(ErrReleaseNotDeployed, "current version not in right status, try to fix it first, \n"+ 169 "uninstall and install kubeblocks could be a way to fix error") 170 } 171 return res, nil 172 } 173 174 // Install installs a Chart 175 func (i *InstallOpts) Install(cfg *Config) (*release.Release, error) { 176 ctx := context.Background() 177 opts := retry.Options{ 178 MaxRetry: 1 + i.TryTimes, 179 } 180 181 actionCfg, err := NewActionConfig(cfg) 182 if err != nil { 183 return nil, err 184 } 185 186 var rel *release.Release 187 if err = retry.IfNecessary(ctx, func() error { 188 release, err1 := i.tryInstall(actionCfg) 189 if err1 != nil { 190 return err1 191 } 192 rel = release 193 return nil 194 }, &opts); err != nil { 195 return nil, errors.Errorf("install chart %s error: %s", i.Name, err.Error()) 196 } 197 198 return rel, nil 199 } 200 201 func (i *InstallOpts) tryInstall(cfg *action.Configuration) (*release.Release, error) { 202 if i.DryRun == nil || !*i.DryRun { 203 released, err := i.GetInstalled(cfg) 204 if released != nil { 205 return released, nil 206 } 207 if err != nil && !ReleaseNotFound(err) { 208 return nil, err 209 } 210 } 211 settings := cli.New() 212 213 // TODO: Does not work now 214 // If a release does not exist, install it. 215 histClient := action.NewHistory(cfg) 216 histClient.Max = 1 217 if _, err := histClient.Run(i.Name); err != nil && 218 !errors.Is(err, driver.ErrReleaseNotFound) { 219 return nil, err 220 } 221 222 client := action.NewInstall(cfg) 223 client.ReleaseName = i.Name 224 client.Namespace = i.Namespace 225 client.CreateNamespace = i.CreateNamespace 226 client.Wait = i.Wait 227 client.WaitForJobs = i.Wait 228 client.Timeout = i.Timeout 229 client.Version = i.Version 230 client.Atomic = i.Atomic 231 232 // for helm template 233 if i.DryRun != nil { 234 client.DryRun = *i.DryRun 235 client.OutputDir = i.OutputDir 236 client.IncludeCRDs = i.IncludeCRD 237 client.Replace = true 238 client.ClientOnly = true 239 } 240 241 if client.Timeout == 0 { 242 client.Timeout = defaultTimeout 243 } 244 245 cp, err := client.ChartPathOptions.LocateChart(i.Chart, settings) 246 if err != nil { 247 return nil, err 248 } 249 250 p := getter.All(settings) 251 vals, err := i.ValueOpts.MergeValues(p) 252 if err != nil { 253 return nil, err 254 } 255 256 // Check Chart dependencies to make sure all are present in /charts 257 chartRequested, err := loader.Load(cp) 258 if err != nil { 259 return nil, err 260 } 261 262 // Create context and prepare the handle of SIGTERM 263 ctx := context.Background() 264 _, cancel := context.WithCancel(ctx) 265 266 // Set up channel through which to send signal notifications. 267 // We must use a buffered channel or risk missing the signal 268 // if we're not ready to receive when the signal is sent. 269 cSignal := make(chan os.Signal, 2) 270 signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) 271 go func() { 272 <-cSignal 273 fmt.Println("Install has been cancelled") 274 cancel() 275 }() 276 277 released, err := client.RunWithContext(ctx, chartRequested, vals) 278 if err != nil { 279 return nil, err 280 } 281 return released, nil 282 } 283 284 // Uninstall uninstalls a Chart 285 func (i *InstallOpts) Uninstall(cfg *Config) error { 286 ctx := context.Background() 287 opts := retry.Options{ 288 MaxRetry: 1 + i.TryTimes, 289 } 290 if cfg.Namespace() == "" { 291 cfg.SetNamespace(i.Namespace) 292 } 293 294 actionCfg, err := NewActionConfig(cfg) 295 if err != nil { 296 return err 297 } 298 299 if err := retry.IfNecessary(ctx, func() error { 300 if err := i.tryUninstall(actionCfg); err != nil { 301 return err 302 } 303 return nil 304 }, &opts); err != nil { 305 return err 306 } 307 return nil 308 } 309 310 func (i *InstallOpts) tryUninstall(cfg *action.Configuration) error { 311 client := action.NewUninstall(cfg) 312 client.Wait = i.Wait 313 client.Timeout = defaultTimeout 314 client.DisableHooks = i.DisableHooks 315 316 // Create context and prepare the handle of SIGTERM 317 ctx := context.Background() 318 _, cancel := context.WithCancel(ctx) 319 320 // Set up channel through which to send signal notifications. 321 // We must use a buffered channel or risk missing the signal 322 // if we're not ready to receive when the signal is sent. 323 cSignal := make(chan os.Signal, 2) 324 signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) 325 go func() { 326 <-cSignal 327 fmt.Println("Install has been cancelled") 328 cancel() 329 }() 330 331 if _, err := client.Run(i.Name); err != nil { 332 if i.ForceUninstall { 333 // Remove secrets left over when uninstalling kubeblocks, when addon CRD is uninstalled before kubeblocks. 334 secretCount, errRemove := i.RemoveRemainSecrets(cfg) 335 if secretCount == 0 { 336 return err 337 } 338 if errRemove != nil { 339 errMsg := fmt.Sprintf("failed to remove remain secrets, please remove them manually, %v", errRemove) 340 return errors.Wrap(err, errMsg) 341 } 342 } else { 343 return err 344 } 345 } 346 return nil 347 } 348 349 func (i *InstallOpts) RemoveRemainSecrets(cfg *action.Configuration) (int, error) { 350 clientSet, err := cfg.KubernetesClientSet() 351 if err != nil { 352 return -1, err 353 } 354 355 labelSelector := metav1.LabelSelector{ 356 MatchExpressions: []metav1.LabelSelectorRequirement{ 357 { 358 Key: "name", 359 Operator: metav1.LabelSelectorOpIn, 360 Values: []string{i.Name}, 361 }, 362 { 363 Key: "owner", 364 Operator: metav1.LabelSelectorOpIn, 365 Values: []string{"helm"}, 366 }, 367 { 368 Key: "status", 369 Operator: metav1.LabelSelectorOpIn, 370 Values: []string{"uninstalling", "superseded"}, 371 }, 372 }, 373 } 374 375 selector, err := metav1.LabelSelectorAsSelector(&labelSelector) 376 if err != nil { 377 fmt.Printf("Failed to build label selector: %v\n", err) 378 return -1, err 379 } 380 options := metav1.ListOptions{ 381 LabelSelector: selector.String(), 382 } 383 384 secrets, err := clientSet.CoreV1().Secrets(i.Namespace).List(context.TODO(), options) 385 if err != nil { 386 return -1, err 387 } 388 secretCount := len(secrets.Items) 389 if secretCount == 0 { 390 return 0, nil 391 } 392 393 for _, secret := range secrets.Items { 394 err := clientSet.CoreV1().Secrets(i.Namespace).Delete(context.TODO(), secret.Name, metav1.DeleteOptions{}) 395 if err != nil { 396 klog.V(1).Info(err) 397 return -1, fmt.Errorf("failed to delete Secret %s: %v", secret.Name, err) 398 } 399 } 400 return secretCount, nil 401 } 402 403 func NewActionConfig(cfg *Config) (*action.Configuration, error) { 404 if cfg.fake { 405 return fakeActionConfig(), nil 406 } 407 408 var err error 409 settings := cli.New() 410 actionCfg := new(action.Configuration) 411 settings.SetNamespace(cfg.namespace) 412 settings.KubeConfig = cfg.kubeConfig 413 if cfg.kubeContext != "" { 414 settings.KubeContext = cfg.kubeContext 415 } 416 settings.Debug = cfg.debug 417 418 if actionCfg.RegistryClient, err = registry.NewClient( 419 registry.ClientOptDebug(settings.Debug), 420 registry.ClientOptEnableCache(true), 421 registry.ClientOptWriter(io.Discard), 422 registry.ClientOptCredentialsFile(settings.RegistryConfig), 423 ); err != nil { 424 return nil, err 425 } 426 427 // do not output warnings 428 getter := settings.RESTClientGetter() 429 getter.(*genericclioptions.ConfigFlags).WrapConfigFn = func(c *rest.Config) *rest.Config { 430 c.WarningHandler = rest.NoWarnings{} 431 return c 432 } 433 434 if err = actionCfg.Init(settings.RESTClientGetter(), 435 settings.Namespace(), 436 os.Getenv("HELM_DRIVER"), 437 cfg.logFn); err != nil { 438 return nil, err 439 } 440 return actionCfg, nil 441 } 442 443 func fakeActionConfig() *action.Configuration { 444 registryClient, err := registry.NewClient() 445 if err != nil { 446 return nil 447 } 448 449 res := &action.Configuration{ 450 Releases: storage.Init(driver.NewMemory()), 451 KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}}, 452 Capabilities: chartutil.DefaultCapabilities, 453 RegistryClient: registryClient, 454 Log: func(format string, v ...interface{}) {}, 455 } 456 // to template the kubeblocks manifest, dry-run install will check and valida the KubeVersion in Capabilities is bigger than 457 // the KubeVersion in Chart.yaml. 458 // in helm v3.11.1 the DefaultCapabilities KubeVersion is 1.20 which lower than the kubeblocks Chart claimed '>=1.22.0-0' 459 res.Capabilities.KubeVersion.Version = "v99.99.0" 460 return res 461 } 462 463 // Upgrade will upgrade a Chart 464 func (i *InstallOpts) Upgrade(cfg *Config) error { 465 if i.Name == testing.KubeBlocksChartName { 466 return nil 467 } 468 ctx := context.Background() 469 opts := retry.Options{ 470 MaxRetry: 1 + i.TryTimes, 471 } 472 473 actionCfg, err := NewActionConfig(cfg) 474 if err != nil { 475 return err 476 } 477 478 if err = retry.IfNecessary(ctx, func() error { 479 var err1 error 480 if _, err1 = i.tryUpgrade(actionCfg); err1 != nil { 481 return err1 482 } 483 return nil 484 }, &opts); err != nil { 485 return err 486 } 487 488 return nil 489 } 490 491 func (i *InstallOpts) tryUpgrade(cfg *action.Configuration) (*release.Release, error) { 492 installed, err := i.GetInstalled(cfg) 493 if err != nil { 494 return nil, err 495 } 496 497 settings := cli.New() 498 499 client := action.NewUpgrade(cfg) 500 client.Namespace = i.Namespace 501 client.Wait = i.Wait 502 client.WaitForJobs = i.Wait 503 client.Timeout = i.Timeout 504 if client.Timeout == 0 { 505 client.Timeout = defaultTimeout 506 } 507 508 if len(i.Version) > 0 { 509 client.Version = i.Version 510 } else { 511 client.Version = installed.Chart.AppVersion() 512 } 513 // do not use helm's ReuseValues, do it ourselves, helm's default upgrade also set it to false 514 // if ReuseValues set to true, helm will use old values instead of new ones, which will cause nil pointer error if new values added. 515 client.ReuseValues = false 516 517 cp, err := client.ChartPathOptions.LocateChart(i.Chart, settings) 518 if err != nil { 519 return nil, err 520 } 521 522 p := getter.All(settings) 523 vals, err := i.ValueOpts.MergeValues(p) 524 if err != nil { 525 return nil, err 526 } 527 // get coalesced values of current chart 528 currentValues, err := chartutil.CoalesceValues(installed.Chart, installed.Config) 529 if err != nil { 530 return nil, err 531 } 532 // merge current values into vals, so current release's user values can be kept 533 installed.Chart.Values = currentValues 534 vals, err = chartutil.CoalesceValues(installed.Chart, vals) 535 if err != nil { 536 return nil, err 537 } 538 539 // Check Chart dependencies to make sure all are present in /charts 540 chartRequested, err := loader.Load(cp) 541 if err != nil { 542 return nil, err 543 } 544 545 // Create context and prepare the handle of SIGTERM 546 ctx := context.Background() 547 _, cancel := context.WithCancel(ctx) 548 549 // Set up channel through which to send signal notifications. 550 // We must use a buffered channel or risk missing the signal 551 // if we're not ready to receive when the signal is sent. 552 cSignal := make(chan os.Signal, 2) 553 signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) 554 go func() { 555 <-cSignal 556 fmt.Println("Upgrade has been cancelled") 557 cancel() 558 }() 559 560 // save resources of old version 561 if err = i.Upgrader.SaveOldResources(); err != nil { 562 return nil, err 563 } 564 565 // update crds before helm upgrade 566 for _, obj := range chartRequested.CRDObjects() { 567 // Read in the resources 568 target, err := cfg.KubeClient.Build(bytes.NewBuffer(obj.File.Data), false) 569 if err != nil { 570 return nil, errors.Wrapf(err, "failed to update CRD %s", obj.Name) 571 } 572 573 // helm only use the original.Info part for looking up original CRD in Update interface 574 // so set original with target as they have same .Info part 575 original := target 576 if _, err := cfg.KubeClient.Update(original, target, false); err != nil { 577 return nil, errors.Wrapf(err, "failed to update CRD %s", obj.Name) 578 } 579 } 580 581 // transform old resources to new resources and clear the tmp dir which saved the old resources. 582 if err = i.Upgrader.TransformResourcesAndClear(); err != nil { 583 return nil, err 584 } 585 586 released, err := client.RunWithContext(ctx, i.Name, chartRequested, vals) 587 if err != nil { 588 return nil, err 589 } 590 return released, nil 591 } 592 593 func GetChartVersions(chartName string) ([]*semver.Version, error) { 594 if chartName == testing.KubeBlocksChartName { 595 return nil, nil 596 } 597 settings := cli.New() 598 rf, err := repo.LoadFile(settings.RepositoryConfig) 599 if err != nil { 600 if os.IsNotExist(errors.Cause(err)) { 601 return nil, nil 602 } else { 603 return nil, err 604 } 605 } 606 607 var ind *repo.IndexFile 608 for _, re := range rf.Repositories { 609 n := re.Name 610 if n != types.KubeBlocksRepoName { 611 continue 612 } 613 614 // load index file 615 f := filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(n)) 616 ind, err = repo.LoadIndexFile(f) 617 if err != nil { 618 return nil, err 619 } 620 break 621 } 622 623 // cannot find any index file 624 if ind == nil { 625 return nil, nil 626 } 627 628 var versions []*semver.Version 629 for chart, entry := range ind.Entries { 630 if len(entry) == 0 || chart != chartName { 631 continue 632 } 633 for _, v := range entry { 634 ver, err := semver.NewVersion(v.Version) 635 if err != nil { 636 return nil, err 637 } 638 versions = append(versions, ver) 639 } 640 } 641 return versions, nil 642 } 643 644 // AddValueOptionsFlags add helm value flags 645 func AddValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { 646 f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "Specify values in a YAML file or a URL (can specify multiple)") 647 f.StringArrayVar(&v.Values, "set", []string{}, "Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") 648 f.StringArrayVar(&v.StringValues, "set-string", []string{}, "Set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") 649 f.StringArrayVar(&v.FileValues, "set-file", []string{}, "Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)") 650 f.StringArrayVar(&v.JSONValues, "set-json", []string{}, "Set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)") 651 } 652 653 func ValueOptsIsEmpty(valueOpts *values.Options) bool { 654 if valueOpts == nil { 655 return true 656 } 657 return len(valueOpts.ValueFiles) == 0 && 658 len(valueOpts.StringValues) == 0 && 659 len(valueOpts.Values) == 0 && 660 len(valueOpts.FileValues) == 0 && 661 len(valueOpts.JSONValues) == 0 662 } 663 664 func GetQuiteLog() action.DebugLog { 665 return func(format string, v ...interface{}) {} 666 } 667 668 func GetVerboseLog() action.DebugLog { 669 return func(format string, v ...interface{}) { 670 klog.Infof(format+"\n", v...) 671 } 672 } 673 674 // GetValues gives an implementation of 'helm get values' for target release 675 func GetValues(release string, cfg *Config) (map[string]interface{}, error) { 676 actionConfig, err := NewActionConfig(cfg) 677 if err != nil { 678 return nil, err 679 } 680 client := action.NewGetValues(actionConfig) 681 client.AllValues = true 682 return client.Run(release) 683 } 684 685 // GetTemplateInstallOps build a helm InstallOpts with dryrun to implement helm template 686 func GetTemplateInstallOps(name, chart, version, namespace string) *InstallOpts { 687 dryrun := true 688 return &InstallOpts{ 689 Name: name, 690 Chart: chart, 691 Version: version, 692 Namespace: namespace, 693 TryTimes: 2, 694 Atomic: true, 695 IncludeCRD: true, 696 DryRun: &dryrun, 697 } 698 }