diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 21a41b9f9..a689302f1 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -136,6 +136,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) { + f.BoolVar(&client.Adopt, "adopt", false, "if set, adopt the resources already exist and aren't owned by any other releases.") f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present") f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install") diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index c263d32e7..3b1161368 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -87,6 +87,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", args[0]) } instClient := action.NewInstall(cfg) + instClient.Adopt = client.Adopt instClient.CreateNamespace = createNamespace instClient.ChartPathOptions = client.ChartPathOptions instClient.DryRun = client.DryRun @@ -166,6 +167,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { }) f := cmd.Flags() + f.BoolVar(&client.Adopt, "adopt", false, "if set, adopt the resources already exist and aren't owned by any other releases.") f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present") f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install") f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") diff --git a/pkg/action/install.go b/pkg/action/install.go index 351e0928c..7bcb72583 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -72,6 +72,7 @@ type Install struct { ChartPathOptions ClientOnly bool + Adopt bool CreateNamespace bool DryRun bool DisableHooks bool @@ -269,7 +270,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. // deleting the release because the manifest will be pointing at that // resource if !i.ClientOnly && !isUpgrade && len(resources) > 0 { - toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace) + toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace, i.Adopt) if err != nil { return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with install") } @@ -281,6 +282,12 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. return rel, nil } + if i.Adopt { + if err := adoptExistingResource(toBeAdopted, rel.Name, rel.Namespace); err != nil { + return nil, err + } + } + if i.CreateNamespace { ns := &v1.Namespace{ TypeMeta: metav1.TypeMeta{ diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index fc289dbab..0a6e482c0 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -96,6 +96,8 @@ type Upgrade struct { PostRenderer postrender.PostRenderer // DisableOpenAPIValidation controls whether OpenAPI validation is enforced. DisableOpenAPIValidation bool + // Adopt, if true, adopt the resources already exist and aren't owned by any other releases. + Adopt bool } // NewUpgrade creates a new Upgrade object with the given configuration. @@ -282,7 +284,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea } } - toBeUpdated, err := existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace) + toBeUpdated, err := existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace, u.Adopt) if err != nil { return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with update") } @@ -305,6 +307,12 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea return upgradedRelease, nil } + if u.Adopt { + if err := adoptExistingResource(toBeUpdated, upgradedRelease.Name, upgradedRelease.Namespace); err != nil { + return nil, err + } + } + u.cfg.Log("creating upgraded release for %s", upgradedRelease.Name) if err := u.cfg.Releases.Create(upgradedRelease); err != nil { return nil, err diff --git a/pkg/action/validate.go b/pkg/action/validate.go index 0c40a9c3c..315aa874b 100644 --- a/pkg/action/validate.go +++ b/pkg/action/validate.go @@ -23,6 +23,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/resource" "helm.sh/helm/v3/pkg/kube" @@ -37,7 +38,36 @@ const ( helmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace" ) -func existingResourceConflict(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, error) { +func adoptExistingResource(resources kube.ResourceList, releaseName, releaseNamespace string) error { + err := resources.Visit(func(info *resource.Info, err error) error { + if err != nil { + return err + } + + helper := resource.NewHelper(info.Client, info.Mapping) + patchData := fmt.Sprintf(`{ + "metadata": { + "labels": { + "%s":"%s" + }, + "annotations": { + "%s":"%s", + "%s":"%s" + } + } +}`, appManagedByLabel, appManagedByHelm, helmReleaseNameAnnotation, releaseName, + helmReleaseNamespaceAnnotation, releaseNamespace) + + if _, err := helper.Patch(info.Namespace, info.Name, types.StrategicMergePatchType, ([]byte)(patchData), nil); err != nil { + return errors.Wrap(err, "could not patch the resource") + } + return nil + }) + + return err +} + +func existingResourceConflict(resources kube.ResourceList, releaseName, releaseNamespace string, adopt bool) (kube.ResourceList, error) { var requireUpdate kube.ResourceList err := resources.Visit(func(info *resource.Info, err error) error { @@ -54,9 +84,15 @@ func existingResourceConflict(resources kube.ResourceList, releaseName, releaseN return errors.Wrap(err, "could not get information about the resource") } - // Allow adoption of the resource if it is managed by Helm and is annotated with correct release name and namespace. - if err := checkOwnership(existing, releaseName, releaseNamespace); err != nil { - return fmt.Errorf("%s exists and cannot be imported into the current release: %s", resourceString(info), err) + if adopt { + if err := checkAssignedRelease(existing, releaseName); err != nil { + return fmt.Errorf("%s is already used in other release: %s", resourceString(info), err) + } + } else { + // Allow adoption of the resource if it is managed by Helm and is annotated with correct release name and namespace. + if err := checkOwnership(existing, releaseName, releaseNamespace); err != nil { + return fmt.Errorf("%s exists and cannot be imported into the current release: %s", resourceString(info), err) + } } requireUpdate.Append(info) @@ -98,6 +134,20 @@ func checkOwnership(obj runtime.Object, releaseName, releaseNamespace string) er return nil } +func checkAssignedRelease(obj runtime.Object, releaseName string) error { + annos, err := accessor.Annotations(obj) + if err != nil { + return err + } + actual, ok := annos[helmReleaseNameAnnotation] + if ok { + if actual != releaseName { + return fmt.Errorf("adoption error: %q is the owner", actual) + } + } + return nil +} + func requireValue(meta map[string]string, k, v string) error { actual, ok := meta[k] if !ok { diff --git a/pkg/action/validate_test.go b/pkg/action/validate_test.go index a9c1cb49c..d903b5874 100644 --- a/pkg/action/validate_test.go +++ b/pkg/action/validate_test.go @@ -46,6 +46,25 @@ func newDeploymentResource(name, namespace string) *resource.Info { } } +func TestCheckAssignedRelease(t *testing.T) { + deployFoo := newDeploymentResource("foo", "ns-a") + + // Don't set annotations the resource and verify no errors + err := checkAssignedRelease(deployFoo.Object, "rel-a") + assert.NoError(t, err) + + // Set other release name annotation and verify annotation error message + _ = accessor.SetAnnotations(deployFoo.Object, map[string]string{ + helmReleaseNameAnnotation: "rel-a", + }) + err = checkAssignedRelease(deployFoo.Object, "rel-b") + assert.EqualError(t, err, `adoption error: "rel-a" is the owner`) + + // Set same release name annotation andverify no errors + err = checkAssignedRelease(deployFoo.Object, "rel-a") + assert.NoError(t, err) +} + func TestCheckOwnership(t *testing.T) { deployFoo := newDeploymentResource("foo", "ns-a")