Add more detail to error messages and support a non-force mode in metadata visitor

Signed-off-by: Jacob LeGrone <git@jacob.work>
pull/7649/head
Jacob LeGrone 4 years ago
parent d829343c15
commit 8d1de39fe3
No known key found for this signature in database
GPG Key ID: 5FD0852F235368C1

@ -249,7 +249,8 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest")
}
err = resources.Visit(setMetadataVisitor(rel.Name, rel.Namespace))
// It is safe to use "force" here because these are resources currently rendered by the chart.
err = resources.Visit(setMetadataVisitor(rel.Name, rel.Namespace, true))
if err != nil {
return nil, err
}

@ -203,7 +203,8 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest")
}
err = target.Visit(setMetadataVisitor(upgradedRelease.Name, upgradedRelease.Namespace))
// It is safe to use force only on target because these are resources currently rendered by the chart.
err = target.Visit(setMetadataVisitor(upgradedRelease.Name, upgradedRelease.Namespace, true))
if err != nil {
return upgradedRelease, err
}

@ -22,6 +22,7 @@ import (
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v3/pkg/kube"
@ -30,12 +31,14 @@ import (
var accessor = meta.NewAccessor()
const (
appManagedByLabel = "app.kubernetes.io/managed-by"
appManagedByHelm = "Helm"
helmReleaseNameAnnotation = "meta.helm.sh/release-name"
helmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace"
)
func existingResourceConflict(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, error) {
requireUpdate := kube.ResourceList{}
var requireUpdate kube.ResourceList
err := resources.Visit(func(info *resource.Info, err error) error {
if err != nil {
@ -48,39 +51,134 @@ func existingResourceConflict(resources kube.ResourceList, releaseName, releaseN
if apierrors.IsNotFound(err) {
return nil
}
return errors.Wrap(err, "could not get information about the resource")
}
// Allow adoption of the resource if it already has app.kubernetes.io/instance and app.kubernetes.io/managed-by labels.
anno, err := accessor.Annotations(existing)
if err == nil && anno != nil && anno[helmReleaseNameAnnotation] == releaseName && anno[helmReleaseNamespaceAnnotation] == releaseNamespace {
requireUpdate = append(requireUpdate, info)
return nil
// 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)
}
return fmt.Errorf(
"existing resource conflict: namespace: %s, name: %s, existing_kind: %s, new_kind: %s",
info.Namespace, info.Name, existing.GetObjectKind().GroupVersionKind(), info.Mapping.GroupVersionKind)
requireUpdate.Append(info)
return nil
})
return requireUpdate, err
}
func setMetadataVisitor(releaseName, releaseNamespace string) resource.VisitorFunc {
func checkOwnership(obj runtime.Object, releaseName, releaseNamespace string) error {
lbls, err := accessor.Labels(obj)
if err != nil {
return err
}
annos, err := accessor.Annotations(obj)
if err != nil {
return err
}
var errs []error
if err := requireValue(lbls, appManagedByLabel, appManagedByHelm); err != nil {
errs = append(errs, fmt.Errorf("label validation error: %s", err))
}
if err := requireValue(annos, helmReleaseNameAnnotation, releaseName); err != nil {
errs = append(errs, fmt.Errorf("annotation validation error: %s", err))
}
if err := requireValue(annos, helmReleaseNamespaceAnnotation, releaseNamespace); err != nil {
errs = append(errs, fmt.Errorf("annotation validation error: %s", err))
}
if len(errs) > 0 {
err := errors.New("invalid ownership metadata")
for _, e := range errs {
err = fmt.Errorf("%w; %s", err, e)
}
return err
}
return nil
}
func requireValue(meta map[string]string, k, v string) error {
actual, ok := meta[k]
if !ok {
return fmt.Errorf("missing key %q: must be set to %q", k, v)
}
if actual != v {
return fmt.Errorf("key %q must equal %q: current value is %q", k, v, actual)
}
return nil
}
// setMetadataVisitor adds release tracking metadata to all resources. If force is enabled, existing
// ownership metadata will be overwritten. Otherwise an error will be returned if any resource has an
// existing and conflicting value for the managed by label or Helm release/namespace annotations.
func setMetadataVisitor(releaseName, releaseNamespace string, force bool) resource.VisitorFunc {
return func(info *resource.Info, err error) error {
if err != nil {
return err
}
anno, err := accessor.Annotations(info.Object)
if err != nil {
return err
if !force {
if err := checkOwnership(info.Object, releaseName, releaseNamespace); err != nil {
return fmt.Errorf("%s cannot be owned: %s", resourceString(info), err)
}
}
if anno == nil {
anno = make(map[string]string)
if err := mergeLabels(info.Object, map[string]string{
appManagedByLabel: appManagedByHelm,
}); err != nil {
return fmt.Errorf(
"%s labels could not be updated: %s",
resourceString(info), err,
)
}
if err := mergeAnnotations(info.Object, map[string]string{
helmReleaseNameAnnotation: releaseName,
helmReleaseNamespaceAnnotation: releaseNamespace,
}); err != nil {
return fmt.Errorf(
"%s annotations could not be updated: %s",
resourceString(info), err,
)
}
anno[helmReleaseNameAnnotation] = releaseName
anno[helmReleaseNamespaceAnnotation] = releaseNamespace
return accessor.SetAnnotations(info.Object, anno)
return nil
}
}
func resourceString(info *resource.Info) string {
_, k := info.Mapping.GroupVersionKind.ToAPIVersionAndKind()
return fmt.Sprintf(
"%s %q in namespace %q",
k, info.Name, info.Namespace,
)
}
func mergeLabels(obj runtime.Object, labels map[string]string) error {
current, err := accessor.Labels(obj)
if err != nil {
return err
}
return accessor.SetLabels(obj, mergeStrStrMaps(current, labels))
}
func mergeAnnotations(obj runtime.Object, annotations map[string]string) error {
current, err := accessor.Annotations(obj)
if err != nil {
return err
}
return accessor.SetAnnotations(obj, mergeStrStrMaps(current, annotations))
}
// merge two maps, always taking the value on the right
func mergeStrStrMaps(current, desired map[string]string) map[string]string {
result := make(map[string]string)
for k, v := range current {
result[k] = v
}
for k, desiredVal := range desired {
result[k] = desiredVal
}
return result
}

Loading…
Cancel
Save