1
16
17 package action
18
19 import (
20 "bytes"
21 "context"
22 "fmt"
23 "io"
24 "net/url"
25 "os"
26 "path"
27 "path/filepath"
28 "strings"
29 "sync"
30 "text/template"
31 "time"
32
33 "github.com/Masterminds/sprig/v3"
34 "github.com/pkg/errors"
35 v1 "k8s.io/api/core/v1"
36 apierrors "k8s.io/apimachinery/pkg/api/errors"
37 "k8s.io/apimachinery/pkg/api/meta"
38 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
39 "k8s.io/cli-runtime/pkg/resource"
40 "sigs.k8s.io/yaml"
41
42 "helm.sh/helm/v3/pkg/chart"
43 "helm.sh/helm/v3/pkg/chartutil"
44 "helm.sh/helm/v3/pkg/cli"
45 "helm.sh/helm/v3/pkg/downloader"
46 "helm.sh/helm/v3/pkg/getter"
47 "helm.sh/helm/v3/pkg/kube"
48 kubefake "helm.sh/helm/v3/pkg/kube/fake"
49 "helm.sh/helm/v3/pkg/postrender"
50 "helm.sh/helm/v3/pkg/registry"
51 "helm.sh/helm/v3/pkg/release"
52 "helm.sh/helm/v3/pkg/releaseutil"
53 "helm.sh/helm/v3/pkg/repo"
54 "helm.sh/helm/v3/pkg/storage"
55 "helm.sh/helm/v3/pkg/storage/driver"
56 )
57
58
59
60
61
62 const notesFileSuffix = "NOTES.txt"
63
64 const defaultDirectoryPermission = 0755
65
66
67 type Install struct {
68 cfg *Configuration
69
70 ChartPathOptions
71
72 ClientOnly bool
73 Force bool
74 CreateNamespace bool
75 DryRun bool
76 DryRunOption string
77
78
79 HideSecret bool
80 DisableHooks bool
81 Replace bool
82 Wait bool
83 WaitForJobs bool
84 Devel bool
85 DependencyUpdate bool
86 Timeout time.Duration
87 Namespace string
88 ReleaseName string
89 GenerateName bool
90 NameTemplate string
91 Description string
92 OutputDir string
93 Atomic bool
94 SkipCRDs bool
95 SubNotes bool
96 DisableOpenAPIValidation bool
97 IncludeCRDs bool
98 Labels map[string]string
99
100
101
102 KubeVersion *chartutil.KubeVersion
103 APIVersions chartutil.VersionSet
104
105 IsUpgrade bool
106
107 EnableDNS bool
108
109
110 UseReleaseName bool
111 PostRenderer postrender.PostRenderer
112
113 Lock sync.Mutex
114 }
115
116
117 type ChartPathOptions struct {
118 CaFile string
119 CertFile string
120 KeyFile string
121 InsecureSkipTLSverify bool
122 PlainHTTP bool
123 Keyring string
124 Password string
125 PassCredentialsAll bool
126 RepoURL string
127 Username string
128 Verify bool
129 Version string
130
131
132
133 registryClient *registry.Client
134 }
135
136
137 func NewInstall(cfg *Configuration) *Install {
138 in := &Install{
139 cfg: cfg,
140 }
141 in.ChartPathOptions.registryClient = cfg.RegistryClient
142
143 return in
144 }
145
146
147 func (i *Install) SetRegistryClient(registryClient *registry.Client) {
148 i.ChartPathOptions.registryClient = registryClient
149 }
150
151
152 func (i *Install) GetRegistryClient() *registry.Client {
153 return i.ChartPathOptions.registryClient
154 }
155
156 func (i *Install) installCRDs(crds []chart.CRD) error {
157
158 totalItems := []*resource.Info{}
159 for _, obj := range crds {
160
161 res, err := i.cfg.KubeClient.Build(bytes.NewBuffer(obj.File.Data), false)
162 if err != nil {
163 return errors.Wrapf(err, "failed to install CRD %s", obj.Name)
164 }
165
166
167 if _, err := i.cfg.KubeClient.Create(res); err != nil {
168
169 if apierrors.IsAlreadyExists(err) {
170 crdName := res[0].Name
171 i.cfg.Log("CRD %s is already present. Skipping.", crdName)
172 continue
173 }
174 return errors.Wrapf(err, "failed to install CRD %s", obj.Name)
175 }
176 totalItems = append(totalItems, res...)
177 }
178 if len(totalItems) > 0 {
179
180 if err := i.cfg.KubeClient.Wait(totalItems, 60*time.Second); err != nil {
181 return err
182 }
183
184
185
186
187
188
189 if i.cfg.Capabilities != nil {
190 discoveryClient, err := i.cfg.RESTClientGetter.ToDiscoveryClient()
191 if err != nil {
192 return err
193 }
194
195 i.cfg.Log("Clearing discovery cache")
196 discoveryClient.Invalidate()
197
198 _, _ = discoveryClient.ServerGroups()
199 }
200
201
202
203 restMapper, err := i.cfg.RESTClientGetter.ToRESTMapper()
204 if err != nil {
205 return err
206 }
207 if resettable, ok := restMapper.(meta.ResettableRESTMapper); ok {
208 i.cfg.Log("Clearing REST mapper cache")
209 resettable.Reset()
210 }
211 }
212 return nil
213 }
214
215
216
217
218
219 func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
220 ctx := context.Background()
221 return i.RunWithContext(ctx, chrt, vals)
222 }
223
224
225
226
227
228 func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
229
230 if !i.ClientOnly {
231 if err := i.cfg.KubeClient.IsReachable(); err != nil {
232 return nil, err
233 }
234 }
235
236
237 if !i.isDryRun() && i.HideSecret {
238 return nil, errors.New("Hiding Kubernetes secrets requires a dry-run mode")
239 }
240
241 if err := i.availableName(); err != nil {
242 return nil, err
243 }
244
245 if err := chartutil.ProcessDependenciesWithMerge(chrt, vals); err != nil {
246 return nil, err
247 }
248
249 var interactWithRemote bool
250 if !i.isDryRun() || i.DryRunOption == "server" || i.DryRunOption == "none" || i.DryRunOption == "false" {
251 interactWithRemote = true
252 }
253
254
255
256 if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
257
258 if i.isDryRun() {
259 i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
260 } else if err := i.installCRDs(crds); err != nil {
261 return nil, err
262 }
263 }
264
265 if i.ClientOnly {
266
267
268 i.cfg.Capabilities = chartutil.DefaultCapabilities.Copy()
269 if i.KubeVersion != nil {
270 i.cfg.Capabilities.KubeVersion = *i.KubeVersion
271 }
272 i.cfg.Capabilities.APIVersions = append(i.cfg.Capabilities.APIVersions, i.APIVersions...)
273 i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard}
274
275 mem := driver.NewMemory()
276 mem.SetNamespace(i.Namespace)
277 i.cfg.Releases = storage.Init(mem)
278 } else if !i.ClientOnly && len(i.APIVersions) > 0 {
279 i.cfg.Log("API Version list given outside of client only mode, this list will be ignored")
280 }
281
282
283
284 i.Wait = i.Wait || i.Atomic
285
286 caps, err := i.cfg.getCapabilities()
287 if err != nil {
288 return nil, err
289 }
290
291
292 isUpgrade := i.IsUpgrade && i.isDryRun()
293 options := chartutil.ReleaseOptions{
294 Name: i.ReleaseName,
295 Namespace: i.Namespace,
296 Revision: 1,
297 IsInstall: !isUpgrade,
298 IsUpgrade: isUpgrade,
299 }
300 valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps)
301 if err != nil {
302 return nil, err
303 }
304
305 if driver.ContainsSystemLabels(i.Labels) {
306 return nil, fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels())
307 }
308
309 rel := i.createRelease(chrt, vals, i.Labels)
310
311 var manifestDoc *bytes.Buffer
312 rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS, i.HideSecret)
313
314 if manifestDoc != nil {
315 rel.Manifest = manifestDoc.String()
316 }
317
318 if err != nil {
319 rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error()))
320
321 return rel, err
322 }
323
324
325 rel.SetStatus(release.StatusPendingInstall, "Initial install underway")
326
327 var toBeAdopted kube.ResourceList
328 resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation)
329 if err != nil {
330 return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest")
331 }
332
333
334 err = resources.Visit(setMetadataVisitor(rel.Name, rel.Namespace, true))
335 if err != nil {
336 return nil, err
337 }
338
339
340
341
342
343
344
345 if !i.ClientOnly && !isUpgrade && len(resources) > 0 {
346 toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace)
347 if err != nil {
348 return nil, errors.Wrap(err, "Unable to continue with install")
349 }
350 }
351
352
353 if i.isDryRun() {
354 rel.Info.Description = "Dry run complete"
355 return rel, nil
356 }
357
358 if i.CreateNamespace {
359 ns := &v1.Namespace{
360 TypeMeta: metav1.TypeMeta{
361 APIVersion: "v1",
362 Kind: "Namespace",
363 },
364 ObjectMeta: metav1.ObjectMeta{
365 Name: i.Namespace,
366 Labels: map[string]string{
367 "name": i.Namespace,
368 },
369 },
370 }
371 buf, err := yaml.Marshal(ns)
372 if err != nil {
373 return nil, err
374 }
375 resourceList, err := i.cfg.KubeClient.Build(bytes.NewBuffer(buf), true)
376 if err != nil {
377 return nil, err
378 }
379 if _, err := i.cfg.KubeClient.Create(resourceList); err != nil && !apierrors.IsAlreadyExists(err) {
380 return nil, err
381 }
382 }
383
384
385 if i.Replace {
386 if err := i.replaceRelease(rel); err != nil {
387 return nil, err
388 }
389 }
390
391
392
393 if err := i.cfg.Releases.Create(rel); err != nil {
394
395
396
397 return rel, err
398 }
399
400 rel, err = i.performInstallCtx(ctx, rel, toBeAdopted, resources)
401 if err != nil {
402 rel, err = i.failRelease(rel, err)
403 }
404 return rel, err
405 }
406
407 func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) {
408 type Msg struct {
409 r *release.Release
410 e error
411 }
412 resultChan := make(chan Msg, 1)
413
414 go func() {
415 rel, err := i.performInstall(rel, toBeAdopted, resources)
416 resultChan <- Msg{rel, err}
417 }()
418 select {
419 case <-ctx.Done():
420 err := ctx.Err()
421 return rel, err
422 case msg := <-resultChan:
423 return msg.r, msg.e
424 }
425 }
426
427
428 func (i *Install) isDryRun() bool {
429 if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" {
430 return true
431 }
432 return false
433 }
434
435 func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) {
436 var err error
437
438 if !i.DisableHooks {
439 if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil {
440 return rel, fmt.Errorf("failed pre-install: %s", err)
441 }
442 }
443
444
445
446
447 if len(toBeAdopted) == 0 && len(resources) > 0 {
448 _, err = i.cfg.KubeClient.Create(resources)
449 } else if len(resources) > 0 {
450 _, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.Force)
451 }
452 if err != nil {
453 return rel, err
454 }
455
456 if i.Wait {
457 if i.WaitForJobs {
458 err = i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout)
459 } else {
460 err = i.cfg.KubeClient.Wait(resources, i.Timeout)
461 }
462 if err != nil {
463 return rel, err
464 }
465 }
466
467 if !i.DisableHooks {
468 if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil {
469 return rel, fmt.Errorf("failed post-install: %s", err)
470 }
471 }
472
473 if len(i.Description) > 0 {
474 rel.SetStatus(release.StatusDeployed, i.Description)
475 } else {
476 rel.SetStatus(release.StatusDeployed, "Install complete")
477 }
478
479
480
481
482
483
484
485
486 if err := i.recordRelease(rel); err != nil {
487 i.cfg.Log("failed to record the release: %s", err)
488 }
489
490 return rel, nil
491 }
492
493 func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
494 rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
495 if i.Atomic {
496 i.cfg.Log("Install failed and atomic is set, uninstalling release")
497 uninstall := NewUninstall(i.cfg)
498 uninstall.DisableHooks = i.DisableHooks
499 uninstall.KeepHistory = false
500 uninstall.Timeout = i.Timeout
501 if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil {
502 return rel, errors.Wrapf(uninstallErr, "an error occurred while uninstalling the release. original install error: %s", err)
503 }
504 return rel, errors.Wrapf(err, "release %s failed, and has been uninstalled due to atomic being set", i.ReleaseName)
505 }
506 i.recordRelease(rel)
507 return rel, err
508 }
509
510
511
512
513
514
515
516
517
518 func (i *Install) availableName() error {
519 start := i.ReleaseName
520
521 if err := chartutil.ValidateReleaseName(start); err != nil {
522 return errors.Wrapf(err, "release name %q", start)
523 }
524
525 if i.isDryRun() {
526 return nil
527 }
528
529 h, err := i.cfg.Releases.History(start)
530 if err != nil || len(h) < 1 {
531 return nil
532 }
533 releaseutil.Reverse(h, releaseutil.SortByRevision)
534 rel := h[0]
535
536 if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) {
537 return nil
538 }
539 return errors.New("cannot re-use a name that is still in use")
540 }
541
542
543 func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}, labels map[string]string) *release.Release {
544 ts := i.cfg.Now()
545 return &release.Release{
546 Name: i.ReleaseName,
547 Namespace: i.Namespace,
548 Chart: chrt,
549 Config: rawVals,
550 Info: &release.Info{
551 FirstDeployed: ts,
552 LastDeployed: ts,
553 Status: release.StatusUnknown,
554 },
555 Version: 1,
556 Labels: labels,
557 }
558 }
559
560
561 func (i *Install) recordRelease(r *release.Release) error {
562
563
564 return i.cfg.Releases.Update(r)
565 }
566
567
568
569
570 func (i *Install) replaceRelease(rel *release.Release) error {
571 hist, err := i.cfg.Releases.History(rel.Name)
572 if err != nil || len(hist) == 0 {
573
574 return nil
575 }
576
577 releaseutil.Reverse(hist, releaseutil.SortByRevision)
578 last := hist[0]
579
580
581 rel.Version = last.Version + 1
582
583
584 if last.Info.Status == release.StatusFailed {
585 return nil
586 }
587
588
589 last.SetStatus(release.StatusSuperseded, "superseded by new release")
590 return i.recordRelease(last)
591 }
592
593
594 func writeToFile(outputDir string, name string, data string, append bool) error {
595 outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator))
596
597 err := ensureDirectoryForFile(outfileName)
598 if err != nil {
599 return err
600 }
601
602 f, err := createOrOpenFile(outfileName, append)
603 if err != nil {
604 return err
605 }
606
607 defer f.Close()
608
609 _, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data))
610
611 if err != nil {
612 return err
613 }
614
615 fmt.Printf("wrote %s\n", outfileName)
616 return nil
617 }
618
619 func createOrOpenFile(filename string, append bool) (*os.File, error) {
620 if append {
621 return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600)
622 }
623 return os.Create(filename)
624 }
625
626
627 func ensureDirectoryForFile(file string) error {
628 baseDir := path.Dir(file)
629 _, err := os.Stat(baseDir)
630 if err != nil && !os.IsNotExist(err) {
631 return err
632 }
633
634 return os.MkdirAll(baseDir, defaultDirectoryPermission)
635 }
636
637
638
639
640 func (i *Install) NameAndChart(args []string) (string, string, error) {
641 flagsNotSet := func() error {
642 if i.GenerateName {
643 return errors.New("cannot set --generate-name and also specify a name")
644 }
645 if i.NameTemplate != "" {
646 return errors.New("cannot set --name-template and also specify a name")
647 }
648 return nil
649 }
650
651 if len(args) > 2 {
652 return args[0], args[1], errors.Errorf("expected at most two arguments, unexpected arguments: %v", strings.Join(args[2:], ", "))
653 }
654
655 if len(args) == 2 {
656 return args[0], args[1], flagsNotSet()
657 }
658
659 if i.NameTemplate != "" {
660 name, err := TemplateName(i.NameTemplate)
661 return name, args[0], err
662 }
663
664 if i.ReleaseName != "" {
665 return i.ReleaseName, args[0], nil
666 }
667
668 if !i.GenerateName {
669 return "", args[0], errors.New("must either provide a name or specify --generate-name")
670 }
671
672 base := filepath.Base(args[0])
673 if base == "." || base == "" {
674 base = "chart"
675 }
676
677 if idx := strings.Index(base, "."); idx != -1 {
678 base = base[0:idx]
679 }
680
681 return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil
682 }
683
684
685 func TemplateName(nameTemplate string) (string, error) {
686 if nameTemplate == "" {
687 return "", nil
688 }
689
690 t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
691 if err != nil {
692 return "", err
693 }
694 var b bytes.Buffer
695 if err := t.Execute(&b, nil); err != nil {
696 return "", err
697 }
698
699 return b.String(), nil
700 }
701
702
703 func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error {
704 var missing []string
705
706 OUTER:
707 for _, r := range reqs {
708 for _, d := range ch.Dependencies() {
709 if d.Name() == r.Name {
710 continue OUTER
711 }
712 }
713 missing = append(missing, r.Name)
714 }
715
716 if len(missing) > 0 {
717 return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", "))
718 }
719 return nil
720 }
721
722
723
724
725
726
727
728
729
730
731
732 func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (string, error) {
733 if registry.IsOCI(name) && c.registryClient == nil {
734 return "", fmt.Errorf("unable to lookup chart %q, missing registry client", name)
735 }
736
737 name = strings.TrimSpace(name)
738 version := strings.TrimSpace(c.Version)
739
740 if _, err := os.Stat(name); err == nil {
741 abs, err := filepath.Abs(name)
742 if err != nil {
743 return abs, err
744 }
745 if c.Verify {
746 if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil {
747 return "", err
748 }
749 }
750 return abs, nil
751 }
752 if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
753 return name, errors.Errorf("path %q not found", name)
754 }
755
756 dl := downloader.ChartDownloader{
757 Out: os.Stdout,
758 Keyring: c.Keyring,
759 Getters: getter.All(settings),
760 Options: []getter.Option{
761 getter.WithPassCredentialsAll(c.PassCredentialsAll),
762 getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile),
763 getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify),
764 getter.WithPlainHTTP(c.PlainHTTP),
765 },
766 RepositoryConfig: settings.RepositoryConfig,
767 RepositoryCache: settings.RepositoryCache,
768 RegistryClient: c.registryClient,
769 }
770
771 if registry.IsOCI(name) {
772 dl.Options = append(dl.Options, getter.WithRegistryClient(c.registryClient))
773 }
774
775 if c.Verify {
776 dl.Verify = downloader.VerifyAlways
777 }
778 if c.RepoURL != "" {
779 chartURL, err := repo.FindChartInAuthAndTLSAndPassRepoURL(c.RepoURL, c.Username, c.Password, name, version,
780 c.CertFile, c.KeyFile, c.CaFile, c.InsecureSkipTLSverify, c.PassCredentialsAll, getter.All(settings))
781 if err != nil {
782 return "", err
783 }
784 name = chartURL
785
786
787
788 u1, err := url.Parse(c.RepoURL)
789 if err != nil {
790 return "", err
791 }
792 u2, err := url.Parse(chartURL)
793 if err != nil {
794 return "", err
795 }
796
797
798
799
800 if c.PassCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) {
801 dl.Options = append(dl.Options, getter.WithBasicAuth(c.Username, c.Password))
802 } else {
803 dl.Options = append(dl.Options, getter.WithBasicAuth("", ""))
804 }
805 } else {
806 dl.Options = append(dl.Options, getter.WithBasicAuth(c.Username, c.Password))
807 }
808
809 if err := os.MkdirAll(settings.RepositoryCache, 0755); err != nil {
810 return "", err
811 }
812
813 filename, _, err := dl.DownloadTo(name, version, settings.RepositoryCache)
814 if err != nil {
815 return "", err
816 }
817
818 lname, err := filepath.Abs(filename)
819 if err != nil {
820 return filename, err
821 }
822 return lname, nil
823 }
824
View as plain text