Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Able to use project components as golang library #718

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ release/
.idea
docker-run-release-cache/
.vscode/
/cover.out
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
240 changes: 3 additions & 237 deletions cmd/upgrade.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package cmd

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
Expand All @@ -11,19 +9,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 +105,6 @@ perform.
`

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

func newChartCommand() *cobra.Command {
diff := diffCmd{
Expand Down Expand Up @@ -308,15 +295,7 @@ func (d *diffCmd) runHelm3() error {
if err := actionConfig.KubeClient.IsReachable(); err != nil {
return err
}
original, err := actionConfig.KubeClient.Build(bytes.NewBuffer(releaseManifest), false)
if err != nil {
return fmt.Errorf("unable to build kubernetes objects from original release manifest: %w", err)
}
target, err := actionConfig.KubeClient.Build(bytes.NewBuffer(installManifest), false)
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(actionConfig, releaseManifest, installManifest)
if err != nil {
return fmt.Errorf("unable to generate manifests: %w", err)
}
Expand All @@ -334,14 +313,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 +333,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
Loading