mirror of
https://github.com/helm/helm.git
synced 2026-06-30 19:57:48 +00:00
Add ability to adopt unmanaged resources
Allow the SDK actions to adopt existing resources. This allows install and update to overwrite resources. If TakeOwnership is not set, adoption is only possible if they existing resources have the right labels (managed-by) and annotations (release-name, ...). Signed-off-by: Mario Manno <mmanno@suse.com>
This commit is contained in:
@@ -105,7 +105,9 @@ type Install struct {
|
||||
// Used by helm template to add the release as part of OutputDir path
|
||||
// OutputDir/<ReleaseName>
|
||||
UseReleaseName bool
|
||||
PostRenderer postrender.PostRenderer
|
||||
// TakeOwnership will ignore the check for helm annotations and take ownership of the resources.
|
||||
TakeOwnership bool
|
||||
PostRenderer postrender.PostRenderer
|
||||
// Lock to control raceconditions when the process receives a SIGTERM
|
||||
Lock sync.Mutex
|
||||
}
|
||||
@@ -335,7 +337,11 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
|
||||
// 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)
|
||||
if i.TakeOwnership {
|
||||
toBeAdopted, err = requireAdoption(resources)
|
||||
} else {
|
||||
toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to continue with install")
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@ type Upgrade struct {
|
||||
Lock sync.Mutex
|
||||
// Enable DNS lookups when rendering templates
|
||||
EnableDNS bool
|
||||
// TakeOwnership will skip the check for helm annotations and adopt all existing resources.
|
||||
TakeOwnership bool
|
||||
}
|
||||
|
||||
type resultMessage struct {
|
||||
@@ -329,7 +331,12 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
|
||||
}
|
||||
}
|
||||
|
||||
toBeUpdated, err := existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace)
|
||||
var toBeUpdated kube.ResourceList
|
||||
if u.TakeOwnership {
|
||||
toBeUpdated, err = requireAdoption(toBeCreated)
|
||||
} else {
|
||||
toBeUpdated, err = existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to continue with update")
|
||||
}
|
||||
|
||||
@@ -37,6 +37,31 @@ const (
|
||||
helmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace"
|
||||
)
|
||||
|
||||
// requireAdoption returns the subset of resources that already exist in the cluster.
|
||||
func requireAdoption(resources kube.ResourceList) (kube.ResourceList, error) {
|
||||
var requireUpdate kube.ResourceList
|
||||
|
||||
err := resources.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
helper := resource.NewHelper(info.Client, info.Mapping)
|
||||
_, err = helper.Get(info.Namespace, info.Name)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrapf(err, "could not get information about the resource %s", resourceString(info))
|
||||
}
|
||||
|
||||
requireUpdate.Append(info)
|
||||
return nil
|
||||
})
|
||||
|
||||
return requireUpdate, err
|
||||
}
|
||||
|
||||
func existingResourceConflict(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, error) {
|
||||
var requireUpdate kube.ResourceList
|
||||
|
||||
|
||||
@@ -17,17 +17,23 @@ limitations under the License.
|
||||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"helm.sh/helm/v3/pkg/kube"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
)
|
||||
|
||||
func newDeploymentResource(name, namespace string) *resource.Info {
|
||||
@@ -46,6 +52,117 @@ func newDeploymentResource(name, namespace string) *resource.Info {
|
||||
}
|
||||
}
|
||||
|
||||
func newMissingDeployment(name, namespace string) *resource.Info {
|
||||
info := &resource.Info{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Mapping: &meta.RESTMapping{
|
||||
Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"},
|
||||
GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
|
||||
Scope: meta.RESTScopeNamespace,
|
||||
},
|
||||
Object: &appsv1.Deployment{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
},
|
||||
Client: fakeClientWith(http.StatusNotFound, appsV1GV, ""),
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func newDeploymentWithOwner(name, namespace string, labels map[string]string, annotations map[string]string) *resource.Info {
|
||||
obj := &appsv1.Deployment{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Labels: labels,
|
||||
Annotations: annotations,
|
||||
},
|
||||
}
|
||||
return &resource.Info{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Mapping: &meta.RESTMapping{
|
||||
Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"},
|
||||
GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
|
||||
Scope: meta.RESTScopeNamespace,
|
||||
},
|
||||
Object: obj,
|
||||
Client: fakeClientWith(http.StatusOK, appsV1GV, runtime.EncodeOrDie(appsv1Codec, obj)),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
appsV1GV = schema.GroupVersion{Group: "apps", Version: "v1"}
|
||||
appsv1Codec = scheme.Codecs.CodecForVersions(scheme.Codecs.LegacyCodec(appsV1GV), scheme.Codecs.UniversalDecoder(appsV1GV), appsV1GV, appsV1GV)
|
||||
)
|
||||
|
||||
func stringBody(body string) io.ReadCloser {
|
||||
return io.NopCloser(bytes.NewReader([]byte(body)))
|
||||
}
|
||||
|
||||
func fakeClientWith(code int, gv schema.GroupVersion, body string) *fake.RESTClient {
|
||||
return &fake.RESTClient{
|
||||
GroupVersion: gv,
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
header := http.Header{}
|
||||
header.Set("Content-Type", runtime.ContentTypeJSON)
|
||||
return &http.Response{
|
||||
StatusCode: code,
|
||||
Header: header,
|
||||
Body: stringBody(body),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdoption(t *testing.T) {
|
||||
var (
|
||||
missing = newMissingDeployment("missing", "ns-a")
|
||||
existing = newDeploymentWithOwner("existing", "ns-a", nil, nil)
|
||||
resources = kube.ResourceList{missing, existing}
|
||||
)
|
||||
|
||||
// Verify that a resource that lacks labels/annotations can be adopted
|
||||
found, err := requireAdoption(resources)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, found, 1)
|
||||
assert.Equal(t, found[0], existing)
|
||||
}
|
||||
|
||||
func TestExistingResourceConflict(t *testing.T) {
|
||||
var (
|
||||
releaseName = "rel-name"
|
||||
releaseNamespace = "rel-namespace"
|
||||
labels = map[string]string{
|
||||
appManagedByLabel: appManagedByHelm,
|
||||
}
|
||||
annotations = map[string]string{
|
||||
helmReleaseNameAnnotation: releaseName,
|
||||
helmReleaseNamespaceAnnotation: releaseNamespace,
|
||||
}
|
||||
missing = newMissingDeployment("missing", "ns-a")
|
||||
existing = newDeploymentWithOwner("existing", "ns-a", labels, annotations)
|
||||
conflict = newDeploymentWithOwner("conflict", "ns-a", nil, nil)
|
||||
resources = kube.ResourceList{missing, existing}
|
||||
)
|
||||
|
||||
// Verify only existing resources are returned
|
||||
found, err := existingResourceConflict(resources, releaseName, releaseNamespace)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, found, 1)
|
||||
assert.Equal(t, found[0], existing)
|
||||
|
||||
// Verify that an existing resource that lacks labels/annotations results in an error
|
||||
resources = append(resources, conflict)
|
||||
_, err = existingResourceConflict(resources, releaseName, releaseNamespace)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCheckOwnership(t *testing.T) {
|
||||
deployFoo := newDeploymentResource("foo", "ns-a")
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ func (r *ResourceList) Append(val *resource.Info) {
|
||||
*r = append(*r, val)
|
||||
}
|
||||
|
||||
// Visit implements resource.Visitor.
|
||||
// Visit implements resource.Visitor. The visitor stops if fn returns an error.
|
||||
func (r ResourceList) Visit(fn resource.VisitorFunc) error {
|
||||
for _, i := range r {
|
||||
if err := fn(i, nil); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user