From 49e21c8d54da88e909fea17599d704e4e2759081 Mon Sep 17 00:00:00 2001 From: Dmitry Volodin Date: Thu, 9 Jan 2025 23:49:18 +0300 Subject: [PATCH] Able to use project components as golang library --- cmd/helpers.go | 5 - cmd/release.go | 2 +- cmd/revision.go | 2 +- cmd/rollback.go | 2 +- cmd/upgrade.go | 231 +---------------------------------------- manifest/generate.go | 237 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 243 insertions(+), 236 deletions(-) create mode 100644 manifest/generate.go diff --git a/cmd/helpers.go b/cmd/helpers.go index a0740efc..6379a104 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -11,11 +11,6 @@ import ( "k8s.io/client-go/util/homedir" ) -const ( - helm2TestSuccessHook = "test-success" - helm3TestHook = "test" -) - var ( // DefaultHelmHome to hold default home path of .helm dir DefaultHelmHome = filepath.Join(homedir.HomeDir(), ".helm") diff --git a/cmd/release.go b/cmd/release.go index 30a50efe..f0286e58 100644 --- a/cmd/release.go +++ b/cmd/release.go @@ -71,7 +71,7 @@ func releaseCmd() *cobra.Command { } func (d *release) differentiateHelm3() error { - excludes := []string{helm3TestHook, helm2TestSuccessHook} + excludes := []string{manifest.Helm3TestHook, manifest.Helm2TestSuccessHook} if d.includeTests { excludes = []string{} } diff --git a/cmd/revision.go b/cmd/revision.go index 4448b8cf..8599789b 100644 --- a/cmd/revision.go +++ b/cmd/revision.go @@ -79,7 +79,7 @@ func revisionCmd() *cobra.Command { func (d *revision) differentiateHelm3() error { namespace := os.Getenv("HELM_NAMESPACE") - excludes := []string{helm3TestHook, helm2TestSuccessHook} + excludes := []string{manifest.Helm3TestHook, manifest.Helm2TestSuccessHook} if d.includeTests { excludes = []string{} } diff --git a/cmd/rollback.go b/cmd/rollback.go index 3e93b8f9..6f8d6d17 100644 --- a/cmd/rollback.go +++ b/cmd/rollback.go @@ -69,7 +69,7 @@ func rollbackCmd() *cobra.Command { func (d *rollback) backcastHelm3() error { namespace := os.Getenv("HELM_NAMESPACE") - excludes := []string{helm3TestHook, helm2TestSuccessHook} + excludes := []string{manifest.Helm3TestHook, manifest.Helm2TestSuccessHook} if d.includeTests { excludes = []string{} } diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 59344f56..d083701c 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -2,7 +2,6 @@ package cmd import ( "bytes" - "encoding/json" "errors" "fmt" "log" @@ -11,19 +10,9 @@ import ( "strconv" "strings" - jsonpatch "github.com/evanphx/json-patch/v5" - jsoniterator "github.com/json-iterator/go" "github.com/spf13/cobra" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/kube" - apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/strategicpatch" - "k8s.io/cli-runtime/pkg/resource" - "sigs.k8s.io/yaml" "github.com/databus23/helm-diff/v3/diff" "github.com/databus23/helm-diff/v3/manifest" @@ -117,7 +106,6 @@ perform. ` var envSettings = cli.New() -var yamlSeperator = []byte("\n---\n") func newChartCommand() *cobra.Command { diff := diffCmd{ @@ -316,7 +304,7 @@ func (d *diffCmd) runHelm3() error { if err != nil { return fmt.Errorf("unable to build kubernetes objects from new release manifest: %w", err) } - releaseManifest, installManifest, err = genManifest(original, target) + releaseManifest, installManifest, err = manifest.Generate(original, target) if err != nil { return fmt.Errorf("unable to generate manifests: %w", err) } @@ -334,14 +322,14 @@ func (d *diffCmd) runHelm3() error { if d.includeTests { currentSpecs = manifest.Parse(string(releaseManifest), d.namespace, d.normalizeManifests) } else { - currentSpecs = manifest.Parse(string(releaseManifest), d.namespace, d.normalizeManifests, helm3TestHook, helm2TestSuccessHook) + currentSpecs = manifest.Parse(string(releaseManifest), d.namespace, d.normalizeManifests, manifest.Helm3TestHook, manifest.Helm2TestSuccessHook) } } var newSpecs map[string]*manifest.MappingResult if d.includeTests { newSpecs = manifest.Parse(string(installManifest), d.namespace, d.normalizeManifests) } else { - newSpecs = manifest.Parse(string(installManifest), d.namespace, d.normalizeManifests, helm3TestHook, helm2TestSuccessHook) + newSpecs = manifest.Parse(string(installManifest), d.namespace, d.normalizeManifests, manifest.Helm3TestHook, manifest.Helm2TestSuccessHook) } seenAnyChanges := diff.Manifests(currentSpecs, newSpecs, &d.Options, os.Stdout) @@ -354,216 +342,3 @@ func (d *diffCmd) runHelm3() error { return nil } - -func genManifest(original, target kube.ResourceList) ([]byte, []byte, error) { - var err error - releaseManifest, installManifest := make([]byte, 0), make([]byte, 0) - - // to be deleted - targetResources := make(map[string]bool) - for _, r := range target { - targetResources[objectKey(r)] = true - } - for _, r := range original { - if !targetResources[objectKey(r)] { - out, _ := yaml.Marshal(r.Object) - releaseManifest = append(releaseManifest, yamlSeperator...) - releaseManifest = append(releaseManifest, out...) - } - } - - existingResources := make(map[string]bool) - for _, r := range original { - existingResources[objectKey(r)] = true - } - - var toBeCreated kube.ResourceList - for _, r := range target { - if !existingResources[objectKey(r)] { - toBeCreated = append(toBeCreated, r) - } - } - - toBeUpdated, err := existingResourceConflict(toBeCreated) - if err != nil { - return nil, nil, fmt.Errorf("rendered manifests contain a resource that already exists. Unable to continue with update: %w", err) - } - - _ = toBeUpdated.Visit(func(r *resource.Info, err error) error { - if err != nil { - return err - } - original.Append(r) - return nil - }) - - err = target.Visit(func(info *resource.Info, err error) error { - if err != nil { - return err - } - kind := info.Mapping.GroupVersionKind.Kind - - // Fetch the current object for the three way merge - helper := resource.NewHelper(info.Client, info.Mapping) - currentObj, err := helper.Get(info.Namespace, info.Name) - if err != nil { - if !apierrors.IsNotFound(err) { - return fmt.Errorf("could not get information about the resource: %w", err) - } - // to be created - out, _ := yaml.Marshal(info.Object) - installManifest = append(installManifest, yamlSeperator...) - installManifest = append(installManifest, out...) - return nil - } - // to be updated - out, _ := jsoniterator.ConfigCompatibleWithStandardLibrary.Marshal(currentObj) - pruneObj, err := deleteStatusAndTidyMetadata(out) - if err != nil { - return fmt.Errorf("prune current obj %q with kind %s: %w", info.Name, kind, err) - } - pruneOut, err := yaml.Marshal(pruneObj) - if err != nil { - return fmt.Errorf("prune current out %q with kind %s: %w", info.Name, kind, err) - } - releaseManifest = append(releaseManifest, yamlSeperator...) - releaseManifest = append(releaseManifest, pruneOut...) - - originalInfo := original.Get(info) - if originalInfo == nil { - return fmt.Errorf("could not find %q", info.Name) - } - - patch, patchType, err := createPatch(originalInfo.Object, currentObj, info) - if err != nil { - return err - } - - helper.ServerDryRun = true - targetObj, err := helper.Patch(info.Namespace, info.Name, patchType, patch, nil) - if err != nil { - return fmt.Errorf("cannot patch %q with kind %s: %w", info.Name, kind, err) - } - out, _ = jsoniterator.ConfigCompatibleWithStandardLibrary.Marshal(targetObj) - pruneObj, err = deleteStatusAndTidyMetadata(out) - if err != nil { - return fmt.Errorf("prune current obj %q with kind %s: %w", info.Name, kind, err) - } - pruneOut, err = yaml.Marshal(pruneObj) - if err != nil { - return fmt.Errorf("prune current out %q with kind %s: %w", info.Name, kind, err) - } - installManifest = append(installManifest, yamlSeperator...) - installManifest = append(installManifest, pruneOut...) - return nil - }) - - return releaseManifest, installManifest, err -} - -func createPatch(originalObj, currentObj runtime.Object, target *resource.Info) ([]byte, types.PatchType, error) { - oldData, err := json.Marshal(originalObj) - if err != nil { - return nil, types.StrategicMergePatchType, fmt.Errorf("serializing current configuration: %w", err) - } - newData, err := json.Marshal(target.Object) - if err != nil { - return nil, types.StrategicMergePatchType, fmt.Errorf("serializing target configuration: %w", err) - } - - // Even if currentObj is nil (because it was not found), it will marshal just fine - currentData, err := json.Marshal(currentObj) - if err != nil { - return nil, types.StrategicMergePatchType, fmt.Errorf("serializing live configuration: %w", err) - } - // kind := target.Mapping.GroupVersionKind.Kind - // if kind == "Deployment" { - // curr, _ := yaml.Marshal(currentObj) - // fmt.Println(string(curr)) - // } - - // Get a versioned object - versionedObject := kube.AsVersioned(target) - - // Unstructured objects, such as CRDs, may not have an not registered error - // returned from ConvertToVersion. Anything that's unstructured should - // use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported - // on objects like CRDs. - _, isUnstructured := versionedObject.(runtime.Unstructured) - - // On newer K8s versions, CRDs aren't unstructured but has this dedicated type - _, isCRD := versionedObject.(*apiextv1.CustomResourceDefinition) - - if isUnstructured || isCRD { - // fall back to generic JSON merge patch - patch, err := jsonpatch.CreateMergePatch(oldData, newData) - return patch, types.MergePatchType, err - } - - patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject) - if err != nil { - return nil, types.StrategicMergePatchType, fmt.Errorf("unable to create patch metadata from object: %w", err) - } - - patch, err := strategicpatch.CreateThreeWayMergePatch(oldData, newData, currentData, patchMeta, true) - return patch, types.StrategicMergePatchType, err -} - -func objectKey(r *resource.Info) string { - gvk := r.Object.GetObjectKind().GroupVersionKind() - return fmt.Sprintf("%s/%s/%s/%s", gvk.GroupVersion().String(), gvk.Kind, r.Namespace, r.Name) -} - -func existingResourceConflict(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 fmt.Errorf("could not get information about the resource: %w", err) - } - - requireUpdate.Append(info) - return nil - }) - - return requireUpdate, err -} - -func deleteStatusAndTidyMetadata(obj []byte) (map[string]interface{}, error) { - var objectMap map[string]interface{} - err := jsoniterator.Unmarshal(obj, &objectMap) - if err != nil { - return nil, fmt.Errorf("could not unmarshal byte sequence: %w", err) - } - - delete(objectMap, "status") - - metadata := objectMap["metadata"].(map[string]interface{}) - - delete(metadata, "managedFields") - delete(metadata, "generation") - - // See the below for the goal of this metadata tidy logic. - // https://github.com/databus23/helm-diff/issues/326#issuecomment-1008253274 - if a := metadata["annotations"]; a != nil { - annotations := a.(map[string]interface{}) - delete(annotations, "meta.helm.sh/release-name") - delete(annotations, "meta.helm.sh/release-namespace") - delete(annotations, "deployment.kubernetes.io/revision") - - if len(annotations) == 0 { - delete(metadata, "annotations") - } - } - - return objectMap, nil -} diff --git a/manifest/generate.go b/manifest/generate.go new file mode 100644 index 00000000..f749cf60 --- /dev/null +++ b/manifest/generate.go @@ -0,0 +1,237 @@ +package manifest + +import ( + "encoding/json" + "fmt" + + "github.com/evanphx/json-patch/v5" + "github.com/json-iterator/go" + "helm.sh/helm/v3/pkg/kube" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/strategicpatch" + "k8s.io/cli-runtime/pkg/resource" + "sigs.k8s.io/yaml" +) + +const ( + Helm2TestSuccessHook = "test-success" + Helm3TestHook = "test" +) + +var yamlSeperator = []byte("\n---\n") + +func Generate(original, target kube.ResourceList) ([]byte, []byte, error) { + var err error + releaseManifest, installManifest := make([]byte, 0), make([]byte, 0) + + // to be deleted + targetResources := make(map[string]bool) + for _, r := range target { + targetResources[objectKey(r)] = true + } + for _, r := range original { + if !targetResources[objectKey(r)] { + out, _ := yaml.Marshal(r.Object) + releaseManifest = append(releaseManifest, yamlSeperator...) + releaseManifest = append(releaseManifest, out...) + } + } + + existingResources := make(map[string]bool) + for _, r := range original { + existingResources[objectKey(r)] = true + } + + var toBeCreated kube.ResourceList + for _, r := range target { + if !existingResources[objectKey(r)] { + toBeCreated = append(toBeCreated, r) + } + } + + toBeUpdated, err := existingResourceConflict(toBeCreated) + if err != nil { + return nil, nil, fmt.Errorf("rendered manifests contain a resource that already exists. Unable to continue with update: %w", err) + } + + _ = toBeUpdated.Visit(func(r *resource.Info, err error) error { + if err != nil { + return err + } + original.Append(r) + return nil + }) + + err = target.Visit(func(info *resource.Info, err error) error { + if err != nil { + return err + } + kind := info.Mapping.GroupVersionKind.Kind + + // Fetch the current object for the three way merge + helper := resource.NewHelper(info.Client, info.Mapping) + currentObj, err := helper.Get(info.Namespace, info.Name) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("could not get information about the resource: %w", err) + } + // to be created + out, _ := yaml.Marshal(info.Object) + installManifest = append(installManifest, yamlSeperator...) + installManifest = append(installManifest, out...) + return nil + } + // to be updated + out, _ := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(currentObj) + pruneObj, err := deleteStatusAndTidyMetadata(out) + if err != nil { + return fmt.Errorf("prune current obj %q with kind %s: %w", info.Name, kind, err) + } + pruneOut, err := yaml.Marshal(pruneObj) + if err != nil { + return fmt.Errorf("prune current out %q with kind %s: %w", info.Name, kind, err) + } + releaseManifest = append(releaseManifest, yamlSeperator...) + releaseManifest = append(releaseManifest, pruneOut...) + + originalInfo := original.Get(info) + if originalInfo == nil { + return fmt.Errorf("could not find %q", info.Name) + } + + patch, patchType, err := createPatch(originalInfo.Object, currentObj, info) + if err != nil { + return err + } + + helper.ServerDryRun = true + targetObj, err := helper.Patch(info.Namespace, info.Name, patchType, patch, nil) + if err != nil { + return fmt.Errorf("cannot patch %q with kind %s: %w", info.Name, kind, err) + } + out, _ = jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(targetObj) + pruneObj, err = deleteStatusAndTidyMetadata(out) + if err != nil { + return fmt.Errorf("prune current obj %q with kind %s: %w", info.Name, kind, err) + } + pruneOut, err = yaml.Marshal(pruneObj) + if err != nil { + return fmt.Errorf("prune current out %q with kind %s: %w", info.Name, kind, err) + } + installManifest = append(installManifest, yamlSeperator...) + installManifest = append(installManifest, pruneOut...) + return nil + }) + + return releaseManifest, installManifest, err +} + +func createPatch(originalObj, currentObj runtime.Object, target *resource.Info) ([]byte, types.PatchType, error) { + oldData, err := json.Marshal(originalObj) + if err != nil { + return nil, types.StrategicMergePatchType, fmt.Errorf("serializing current configuration: %w", err) + } + newData, err := json.Marshal(target.Object) + if err != nil { + return nil, types.StrategicMergePatchType, fmt.Errorf("serializing target configuration: %w", err) + } + + // Even if currentObj is nil (because it was not found), it will marshal just fine + currentData, err := json.Marshal(currentObj) + if err != nil { + return nil, types.StrategicMergePatchType, fmt.Errorf("serializing live configuration: %w", err) + } + // kind := target.Mapping.GroupVersionKind.Kind + // if kind == "Deployment" { + // curr, _ := yaml.Marshal(currentObj) + // fmt.Println(string(curr)) + // } + + // Get a versioned object + versionedObject := kube.AsVersioned(target) + + // Unstructured objects, such as CRDs, may not have an not registered error + // returned from ConvertToVersion. Anything that's unstructured should + // use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported + // on objects like CRDs. + _, isUnstructured := versionedObject.(runtime.Unstructured) + + // On newer K8s versions, CRDs aren't unstructured but has this dedicated type + _, isCRD := versionedObject.(*v1.CustomResourceDefinition) + + if isUnstructured || isCRD { + // fall back to generic JSON merge patch + patch, err := jsonpatch.CreateMergePatch(oldData, newData) + return patch, types.MergePatchType, err + } + + patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject) + if err != nil { + return nil, types.StrategicMergePatchType, fmt.Errorf("unable to create patch metadata from object: %w", err) + } + + patch, err := strategicpatch.CreateThreeWayMergePatch(oldData, newData, currentData, patchMeta, true) + return patch, types.StrategicMergePatchType, err +} + +func objectKey(r *resource.Info) string { + gvk := r.Object.GetObjectKind().GroupVersionKind() + return fmt.Sprintf("%s/%s/%s/%s", gvk.GroupVersion().String(), gvk.Kind, r.Namespace, r.Name) +} + +func existingResourceConflict(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 errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("could not get information about the resource: %w", err) + } + + requireUpdate.Append(info) + return nil + }) + + return requireUpdate, err +} + +func deleteStatusAndTidyMetadata(obj []byte) (map[string]interface{}, error) { + var objectMap map[string]interface{} + err := jsoniter.Unmarshal(obj, &objectMap) + if err != nil { + return nil, fmt.Errorf("could not unmarshal byte sequence: %w", err) + } + + delete(objectMap, "status") + + metadata := objectMap["metadata"].(map[string]interface{}) + + delete(metadata, "managedFields") + delete(metadata, "generation") + + // See the below for the goal of this metadata tidy logic. + // https://github.com/databus23/helm-diff/issues/326#issuecomment-1008253274 + if a := metadata["annotations"]; a != nil { + annotations := a.(map[string]interface{}) + delete(annotations, "meta.helm.sh/release-name") + delete(annotations, "meta.helm.sh/release-namespace") + delete(annotations, "deployment.kubernetes.io/revision") + + if len(annotations) == 0 { + delete(metadata, "annotations") + } + } + + return objectMap, nil +}