Merge pull request #12876 from manno/sdk-ignore-missing-annotations

Allow install, update action to adopt existing resources (sdk only)
pull/13252/head
Matt Farina 4 months ago committed by GitHub
commit c261d0655c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -110,7 +110,9 @@ type Install struct {
// Used by helm template to add the release as part of OutputDir path // Used by helm template to add the release as part of OutputDir path
// OutputDir/<ReleaseName> // OutputDir/<ReleaseName>
UseReleaseName bool 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 to control raceconditions when the process receives a SIGTERM
Lock sync.Mutex Lock sync.Mutex
} }
@ -345,7 +347,11 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
// deleting the release because the manifest will be pointing at that // deleting the release because the manifest will be pointing at that
// resource // resource
if !i.ClientOnly && !isUpgrade && len(resources) > 0 { 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 { if err != nil {
return nil, errors.Wrap(err, "Unable to continue with install") return nil, errors.Wrap(err, "Unable to continue with install")
} }

@ -117,6 +117,8 @@ type Upgrade struct {
Lock sync.Mutex Lock sync.Mutex
// Enable DNS lookups when rendering templates // Enable DNS lookups when rendering templates
EnableDNS bool EnableDNS bool
// TakeOwnership will skip the check for helm annotations and adopt all existing resources.
TakeOwnership bool
} }
type resultMessage struct { type resultMessage struct {
@ -341,7 +343,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 { if err != nil {
return nil, errors.Wrap(err, "Unable to continue with update") return nil, errors.Wrap(err, "Unable to continue with update")
} }

@ -37,6 +37,31 @@ const (
helmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace" 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) { func existingResourceConflict(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, error) {
var requireUpdate kube.ResourceList var requireUpdate kube.ResourceList

@ -17,17 +17,23 @@ limitations under the License.
package action package action
import ( import (
"bytes"
"io"
"net/http"
"testing" "testing"
"helm.sh/helm/v3/pkg/kube" "helm.sh/helm/v3/pkg/kube"
appsv1 "k8s.io/api/apps/v1"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/resource" "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 { 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) { func TestCheckOwnership(t *testing.T) {
deployFoo := newDeploymentResource("foo", "ns-a") deployFoo := newDeploymentResource("foo", "ns-a")

@ -26,7 +26,7 @@ func (r *ResourceList) Append(val *resource.Info) {
*r = append(*r, val) *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 { func (r ResourceList) Visit(fn resource.VisitorFunc) error {
for _, i := range r { for _, i := range r {
if err := fn(i, nil); err != nil { if err := fn(i, nil); err != nil {

Loading…
Cancel
Save