github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/test/testutils/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 testutils 21 22 import ( 23 "context" 24 "fmt" 25 "io" 26 "os" 27 "os/signal" 28 "strings" 29 "syscall" 30 "time" 31 32 "github.com/containers/common/pkg/retry" 33 "github.com/pkg/errors" 34 "gopkg.in/yaml.v2" 35 "helm.sh/helm/v3/pkg/action" 36 "helm.sh/helm/v3/pkg/chart/loader" 37 "helm.sh/helm/v3/pkg/chartutil" 38 "helm.sh/helm/v3/pkg/cli" 39 "helm.sh/helm/v3/pkg/cli/values" 40 "helm.sh/helm/v3/pkg/getter" 41 kubefake "helm.sh/helm/v3/pkg/kube/fake" 42 "helm.sh/helm/v3/pkg/registry" 43 "helm.sh/helm/v3/pkg/release" 44 "helm.sh/helm/v3/pkg/repo" 45 "helm.sh/helm/v3/pkg/storage" 46 "helm.sh/helm/v3/pkg/storage/driver" 47 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 48 "k8s.io/cli-runtime/pkg/genericclioptions" 49 "k8s.io/client-go/rest" 50 "k8s.io/klog/v2" 51 ) 52 53 const defaultTimeout = time.Second * 600 54 55 type Config struct { 56 namespace string 57 kubeConfig string 58 debug bool 59 kubeContext string 60 logFn action.DebugLog 61 fake bool 62 } 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 79 // for helm template 80 DryRun *bool 81 OutputDir string 82 IncludeCRD bool 83 } 84 85 // AddRepo adds a repo 86 func AddRepo(r *repo.Entry) error { 87 88 settings := cli.New() 89 repoFile := settings.RepositoryConfig 90 b, err := os.ReadFile(repoFile) 91 if err != nil && !os.IsNotExist(err) { 92 return err 93 } 94 95 var f repo.File 96 if err = yaml.Unmarshal(b, &f); err != nil { 97 return err 98 } 99 100 // Check if the repo Name is legal 101 if strings.Contains(r.Name, "/") { 102 return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", r.Name) 103 } 104 105 if f.Has(r.Name) { 106 existing := f.Get(r.Name) 107 if *r != *existing && r.Name != KubeBlocksChartName { 108 // The input Name is different from the existing one, return an error 109 return errors.Errorf("repository name (%s) already exists, please specify a different name", r.Name) 110 } 111 } 112 113 cp, err := repo.NewChartRepository(r, getter.All(settings)) 114 if err != nil { 115 return err 116 } 117 118 if _, err := cp.DownloadIndexFile(); err != nil { 119 return errors.Wrapf(err, "looks like %q is not a valid Chart repository or cannot be reached", r.URL) 120 } 121 122 f.Update(r) 123 124 if err = f.WriteFile(repoFile, 0644); err != nil { 125 return err 126 } 127 return nil 128 } 129 130 func statusDeployed(rl *release.Release) bool { 131 if rl == nil { 132 return false 133 } 134 return release.StatusDeployed == rl.Info.Status 135 } 136 137 // GetInstalled gets helm package release info if installed. 138 func (i *InstallOpts) GetInstalled(cfg *action.Configuration) (*release.Release, error) { 139 var ErrReleaseNotDeployed = fmt.Errorf("release: not in deployed status") 140 res, err := action.NewGet(cfg).Run(i.Name) 141 if err != nil { 142 return nil, err 143 } 144 if res == nil { 145 return nil, driver.ErrReleaseNotFound 146 } 147 if !statusDeployed(res) { 148 return nil, errors.Wrapf(ErrReleaseNotDeployed, "current version not in right status, try to fix it first, \n"+ 149 "uninstall and install kubeblocks could be a way to fix error") 150 } 151 return res, nil 152 } 153 154 // Install installs a Chart 155 func (i *InstallOpts) Install(cfg *Config) (*release.Release, error) { 156 ctx := context.Background() 157 opts := retry.Options{ 158 MaxRetry: 1 + i.TryTimes, 159 } 160 161 actionCfg, err := NewActionConfig(cfg) 162 if err != nil { 163 return nil, err 164 } 165 166 var rel *release.Release 167 if err = retry.IfNecessary(ctx, func() error { 168 release, err1 := i.tryInstall(actionCfg) 169 if err1 != nil { 170 return err1 171 } 172 rel = release 173 return nil 174 }, &opts); err != nil { 175 return nil, errors.Errorf("install chart %s error: %s", i.Name, err.Error()) 176 } 177 178 return rel, nil 179 } 180 181 func ReleaseNotFound(err error) bool { 182 if err == nil { 183 return false 184 } 185 return errors.Is(err, driver.ErrReleaseNotFound) || 186 strings.Contains(err.Error(), driver.ErrReleaseNotFound.Error()) 187 } 188 189 func (i *InstallOpts) tryInstall(cfg *action.Configuration) (*release.Release, error) { 190 if i.DryRun == nil || !*i.DryRun { 191 released, err := i.GetInstalled(cfg) 192 if released != nil { 193 return released, nil 194 } 195 if err != nil && !ReleaseNotFound(err) { 196 return nil, err 197 } 198 } 199 settings := cli.New() 200 201 // TODO: Does not work now 202 // If a release does not exist, install it. 203 histClient := action.NewHistory(cfg) 204 histClient.Max = 1 205 if _, err := histClient.Run(i.Name); err != nil && 206 !errors.Is(err, driver.ErrReleaseNotFound) { 207 return nil, err 208 } 209 210 client := action.NewInstall(cfg) 211 client.ReleaseName = i.Name 212 client.Namespace = i.Namespace 213 client.CreateNamespace = i.CreateNamespace 214 client.Wait = i.Wait 215 client.WaitForJobs = i.Wait 216 client.Timeout = i.Timeout 217 client.Version = i.Version 218 client.Atomic = i.Atomic 219 220 // for helm template 221 if i.DryRun != nil { 222 client.DryRun = *i.DryRun 223 client.OutputDir = i.OutputDir 224 client.IncludeCRDs = i.IncludeCRD 225 client.Replace = true 226 client.ClientOnly = true 227 } 228 229 if client.Timeout == 0 { 230 client.Timeout = defaultTimeout 231 } 232 233 cp, err := client.ChartPathOptions.LocateChart(i.Chart, settings) 234 if err != nil { 235 return nil, err 236 } 237 238 p := getter.All(settings) 239 vals, err := i.ValueOpts.MergeValues(p) 240 if err != nil { 241 return nil, err 242 } 243 244 // Check Chart dependencies to make sure all are present in /charts 245 chartRequested, err := loader.Load(cp) 246 if err != nil { 247 return nil, err 248 } 249 250 // Create context and prepare the handle of SIGTERM 251 ctx := context.Background() 252 _, cancel := context.WithCancel(ctx) 253 254 // Set up channel through which to send signal notifications. 255 // We must use a buffered channel or risk missing the signal 256 // if we're not ready to receive when the signal is sent. 257 cSignal := make(chan os.Signal, 2) 258 signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) 259 go func() { 260 <-cSignal 261 fmt.Println("Install has been cancelled") 262 cancel() 263 }() 264 265 released, err := client.RunWithContext(ctx, chartRequested, vals) 266 if err != nil { 267 return nil, err 268 } 269 return released, nil 270 } 271 272 // Uninstall uninstalls a Chart 273 func (i *InstallOpts) Uninstall(cfg *Config) error { 274 ctx := context.Background() 275 opts := retry.Options{ 276 MaxRetry: 1 + i.TryTimes, 277 } 278 if cfg.Namespace() == "" { 279 cfg.SetNamespace(i.Namespace) 280 } 281 282 actionCfg, err := NewActionConfig(cfg) 283 if err != nil { 284 return err 285 } 286 287 if err := retry.IfNecessary(ctx, func() error { 288 if err := i.tryUninstall(actionCfg); err != nil { 289 return err 290 } 291 return nil 292 }, &opts); err != nil { 293 return err 294 } 295 return nil 296 } 297 func (i *InstallOpts) tryUninstall(cfg *action.Configuration) error { 298 client := action.NewUninstall(cfg) 299 client.Wait = i.Wait 300 client.Timeout = defaultTimeout 301 client.DisableHooks = i.DisableHooks 302 303 // Create context and prepare the handle of SIGTERM 304 ctx := context.Background() 305 _, cancel := context.WithCancel(ctx) 306 307 // Set up channel through which to send signal notifications. 308 // We must use a buffered channel or risk missing the signal 309 // if we're not ready to receive when the signal is sent. 310 cSignal := make(chan os.Signal, 2) 311 signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) 312 go func() { 313 <-cSignal 314 fmt.Println("Install has been cancelled") 315 cancel() 316 }() 317 318 if _, err := client.Run(i.Name); err != nil { 319 if i.ForceUninstall { 320 // Remove secrets left over when uninstalling kubeblocks, when addon CRD is uninstalled before kubeblocks. 321 secretCount, errRemove := i.RemoveRemainSecrets(cfg) 322 if secretCount == 0 { 323 return err 324 } 325 if errRemove != nil { 326 errMsg := fmt.Sprintf("failed to remove remain secrets, please remove them manually, %v", errRemove) 327 return errors.Wrap(err, errMsg) 328 } 329 } else { 330 return err 331 } 332 } 333 return nil 334 } 335 336 func (i *InstallOpts) RemoveRemainSecrets(cfg *action.Configuration) (int, error) { 337 clientSet, err := cfg.KubernetesClientSet() 338 if err != nil { 339 return -1, err 340 } 341 342 labelSelector := metav1.LabelSelector{ 343 MatchExpressions: []metav1.LabelSelectorRequirement{ 344 { 345 Key: "name", 346 Operator: metav1.LabelSelectorOpIn, 347 Values: []string{i.Name}, 348 }, 349 { 350 Key: "owner", 351 Operator: metav1.LabelSelectorOpIn, 352 Values: []string{"helm"}, 353 }, 354 { 355 Key: "status", 356 Operator: metav1.LabelSelectorOpIn, 357 Values: []string{"uninstalling", "superseded"}, 358 }, 359 }, 360 } 361 362 selector, err := metav1.LabelSelectorAsSelector(&labelSelector) 363 if err != nil { 364 fmt.Printf("Failed to build label selector: %v\n", err) 365 return -1, err 366 } 367 options := metav1.ListOptions{ 368 LabelSelector: selector.String(), 369 } 370 371 secrets, err := clientSet.CoreV1().Secrets(i.Namespace).List(context.TODO(), options) 372 if err != nil { 373 return -1, err 374 } 375 secretCount := len(secrets.Items) 376 if secretCount == 0 { 377 return 0, nil 378 } 379 380 for _, secret := range secrets.Items { 381 err := clientSet.CoreV1().Secrets(i.Namespace).Delete(context.TODO(), secret.Name, metav1.DeleteOptions{}) 382 if err != nil { 383 klog.V(1).Info(err) 384 return -1, fmt.Errorf("failed to delete Secret %s: %v", secret.Name, err) 385 } 386 } 387 return secretCount, nil 388 } 389 390 func fakeActionConfig() *action.Configuration { 391 registryClient, err := registry.NewClient() 392 if err != nil { 393 return nil 394 } 395 396 res := &action.Configuration{ 397 Releases: storage.Init(driver.NewMemory()), 398 KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}}, 399 Capabilities: chartutil.DefaultCapabilities, 400 RegistryClient: registryClient, 401 Log: func(format string, v ...interface{}) {}, 402 } 403 // to template the kubeblocks manifest, dry-run install will check and valida the KubeVersion in Capabilities is bigger than 404 // the KubeVersion in Chart.yaml. 405 // in helm v3.11.1 the DefaultCapabilities KubeVersion is 1.20 which lower than the kubeblocks Chart claimed '>=1.22.0-0' 406 res.Capabilities.KubeVersion.Version = "v99.99.0" 407 return res 408 } 409 410 func NewActionConfig(cfg *Config) (*action.Configuration, error) { 411 if cfg.fake { 412 return fakeActionConfig(), nil 413 } 414 415 var err error 416 settings := cli.New() 417 actionCfg := new(action.Configuration) 418 settings.SetNamespace(cfg.namespace) 419 settings.KubeConfig = cfg.kubeConfig 420 if cfg.kubeContext != "" { 421 settings.KubeContext = cfg.kubeContext 422 } 423 settings.Debug = cfg.debug 424 425 if actionCfg.RegistryClient, err = registry.NewClient( 426 registry.ClientOptDebug(settings.Debug), 427 registry.ClientOptEnableCache(true), 428 registry.ClientOptWriter(io.Discard), 429 registry.ClientOptCredentialsFile(settings.RegistryConfig), 430 ); err != nil { 431 return nil, err 432 } 433 434 // do not output warnings 435 getter := settings.RESTClientGetter() 436 getter.(*genericclioptions.ConfigFlags).WrapConfigFn = func(c *rest.Config) *rest.Config { 437 c.WarningHandler = rest.NoWarnings{} 438 return c 439 } 440 441 if err = actionCfg.Init(settings.RESTClientGetter(), 442 settings.Namespace(), 443 os.Getenv("HELM_DRIVER"), 444 cfg.logFn); err != nil { 445 return nil, err 446 } 447 return actionCfg, nil 448 } 449 450 func NewConfig(namespace string, kubeConfig string, ctx string, debug bool) *Config { 451 cfg := &Config{ 452 namespace: namespace, 453 debug: debug, 454 kubeConfig: kubeConfig, 455 kubeContext: ctx, 456 } 457 458 if debug { 459 cfg.logFn = GetVerboseLog() 460 } else { 461 cfg.logFn = GetQuiteLog() 462 } 463 return cfg 464 } 465 func (o *Config) SetNamespace(namespace string) { 466 o.namespace = namespace 467 } 468 469 func (o *Config) Namespace() string { 470 return o.namespace 471 } 472 473 func GetQuiteLog() action.DebugLog { 474 return func(format string, v ...interface{}) {} 475 } 476 477 func GetVerboseLog() action.DebugLog { 478 return func(format string, v ...interface{}) { 479 klog.Infof(format+"\n", v...) 480 } 481 }