Skip to content

Commit

Permalink
Able to use project components as golang library
Browse files Browse the repository at this point in the history
  • Loading branch information
dmvolod committed Jan 9, 2025
1 parent ac4634a commit 49e21c8
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 236 deletions.
5 changes: 0 additions & 5 deletions cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion cmd/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/revision.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/rollback.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
}
Expand Down
231 changes: 3 additions & 228 deletions cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cmd

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
Expand All @@ -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"
Expand Down Expand Up @@ -117,7 +106,6 @@ perform.
`

var envSettings = cli.New()
var yamlSeperator = []byte("\n---\n")

func newChartCommand() *cobra.Command {
diff := diffCmd{
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)

Expand All @@ -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
}
Loading

0 comments on commit 49e21c8

Please sign in to comment.