Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 49e21c8

Browse files
committedJan 9, 2025
Able to use project components as golang library
1 parent ac4634a commit 49e21c8

File tree

6 files changed

+243
-236
lines changed

6 files changed

+243
-236
lines changed
 

‎cmd/helpers.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ import (
1111
"k8s.io/client-go/util/homedir"
1212
)
1313

14-
const (
15-
helm2TestSuccessHook = "test-success"
16-
helm3TestHook = "test"
17-
)
18-
1914
var (
2015
// DefaultHelmHome to hold default home path of .helm dir
2116
DefaultHelmHome = filepath.Join(homedir.HomeDir(), ".helm")

‎cmd/release.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func releaseCmd() *cobra.Command {
7171
}
7272

7373
func (d *release) differentiateHelm3() error {
74-
excludes := []string{helm3TestHook, helm2TestSuccessHook}
74+
excludes := []string{manifest.Helm3TestHook, manifest.Helm2TestSuccessHook}
7575
if d.includeTests {
7676
excludes = []string{}
7777
}

‎cmd/revision.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func revisionCmd() *cobra.Command {
7979

8080
func (d *revision) differentiateHelm3() error {
8181
namespace := os.Getenv("HELM_NAMESPACE")
82-
excludes := []string{helm3TestHook, helm2TestSuccessHook}
82+
excludes := []string{manifest.Helm3TestHook, manifest.Helm2TestSuccessHook}
8383
if d.includeTests {
8484
excludes = []string{}
8585
}

‎cmd/rollback.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func rollbackCmd() *cobra.Command {
6969

7070
func (d *rollback) backcastHelm3() error {
7171
namespace := os.Getenv("HELM_NAMESPACE")
72-
excludes := []string{helm3TestHook, helm2TestSuccessHook}
72+
excludes := []string{manifest.Helm3TestHook, manifest.Helm2TestSuccessHook}
7373
if d.includeTests {
7474
excludes = []string{}
7575
}

‎cmd/upgrade.go

Lines changed: 3 additions & 228 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cmd
22

33
import (
44
"bytes"
5-
"encoding/json"
65
"errors"
76
"fmt"
87
"log"
@@ -11,19 +10,9 @@ import (
1110
"strconv"
1211
"strings"
1312

14-
jsonpatch "github.com/evanphx/json-patch/v5"
15-
jsoniterator "github.com/json-iterator/go"
1613
"github.com/spf13/cobra"
1714
"helm.sh/helm/v3/pkg/action"
1815
"helm.sh/helm/v3/pkg/cli"
19-
"helm.sh/helm/v3/pkg/kube"
20-
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
21-
apierrors "k8s.io/apimachinery/pkg/api/errors"
22-
"k8s.io/apimachinery/pkg/runtime"
23-
"k8s.io/apimachinery/pkg/types"
24-
"k8s.io/apimachinery/pkg/util/strategicpatch"
25-
"k8s.io/cli-runtime/pkg/resource"
26-
"sigs.k8s.io/yaml"
2716

2817
"github.com/databus23/helm-diff/v3/diff"
2918
"github.com/databus23/helm-diff/v3/manifest"
@@ -117,7 +106,6 @@ perform.
117106
`
118107

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

122110
func newChartCommand() *cobra.Command {
123111
diff := diffCmd{
@@ -316,7 +304,7 @@ func (d *diffCmd) runHelm3() error {
316304
if err != nil {
317305
return fmt.Errorf("unable to build kubernetes objects from new release manifest: %w", err)
318306
}
319-
releaseManifest, installManifest, err = genManifest(original, target)
307+
releaseManifest, installManifest, err = manifest.Generate(original, target)
320308
if err != nil {
321309
return fmt.Errorf("unable to generate manifests: %w", err)
322310
}
@@ -334,14 +322,14 @@ func (d *diffCmd) runHelm3() error {
334322
if d.includeTests {
335323
currentSpecs = manifest.Parse(string(releaseManifest), d.namespace, d.normalizeManifests)
336324
} else {
337-
currentSpecs = manifest.Parse(string(releaseManifest), d.namespace, d.normalizeManifests, helm3TestHook, helm2TestSuccessHook)
325+
currentSpecs = manifest.Parse(string(releaseManifest), d.namespace, d.normalizeManifests, manifest.Helm3TestHook, manifest.Helm2TestSuccessHook)
338326
}
339327
}
340328
var newSpecs map[string]*manifest.MappingResult
341329
if d.includeTests {
342330
newSpecs = manifest.Parse(string(installManifest), d.namespace, d.normalizeManifests)
343331
} else {
344-
newSpecs = manifest.Parse(string(installManifest), d.namespace, d.normalizeManifests, helm3TestHook, helm2TestSuccessHook)
332+
newSpecs = manifest.Parse(string(installManifest), d.namespace, d.normalizeManifests, manifest.Helm3TestHook, manifest.Helm2TestSuccessHook)
345333
}
346334
seenAnyChanges := diff.Manifests(currentSpecs, newSpecs, &d.Options, os.Stdout)
347335

@@ -354,216 +342,3 @@ func (d *diffCmd) runHelm3() error {
354342

355343
return nil
356344
}
357-
358-
func genManifest(original, target kube.ResourceList) ([]byte, []byte, error) {
359-
var err error
360-
releaseManifest, installManifest := make([]byte, 0), make([]byte, 0)
361-
362-
// to be deleted
363-
targetResources := make(map[string]bool)
364-
for _, r := range target {
365-
targetResources[objectKey(r)] = true
366-
}
367-
for _, r := range original {
368-
if !targetResources[objectKey(r)] {
369-
out, _ := yaml.Marshal(r.Object)
370-
releaseManifest = append(releaseManifest, yamlSeperator...)
371-
releaseManifest = append(releaseManifest, out...)
372-
}
373-
}
374-
375-
existingResources := make(map[string]bool)
376-
for _, r := range original {
377-
existingResources[objectKey(r)] = true
378-
}
379-
380-
var toBeCreated kube.ResourceList
381-
for _, r := range target {
382-
if !existingResources[objectKey(r)] {
383-
toBeCreated = append(toBeCreated, r)
384-
}
385-
}
386-
387-
toBeUpdated, err := existingResourceConflict(toBeCreated)
388-
if err != nil {
389-
return nil, nil, fmt.Errorf("rendered manifests contain a resource that already exists. Unable to continue with update: %w", err)
390-
}
391-
392-
_ = toBeUpdated.Visit(func(r *resource.Info, err error) error {
393-
if err != nil {
394-
return err
395-
}
396-
original.Append(r)
397-
return nil
398-
})
399-
400-
err = target.Visit(func(info *resource.Info, err error) error {
401-
if err != nil {
402-
return err
403-
}
404-
kind := info.Mapping.GroupVersionKind.Kind
405-
406-
// Fetch the current object for the three way merge
407-
helper := resource.NewHelper(info.Client, info.Mapping)
408-
currentObj, err := helper.Get(info.Namespace, info.Name)
409-
if err != nil {
410-
if !apierrors.IsNotFound(err) {
411-
return fmt.Errorf("could not get information about the resource: %w", err)
412-
}
413-
// to be created
414-
out, _ := yaml.Marshal(info.Object)
415-
installManifest = append(installManifest, yamlSeperator...)
416-
installManifest = append(installManifest, out...)
417-
return nil
418-
}
419-
// to be updated
420-
out, _ := jsoniterator.ConfigCompatibleWithStandardLibrary.Marshal(currentObj)
421-
pruneObj, err := deleteStatusAndTidyMetadata(out)
422-
if err != nil {
423-
return fmt.Errorf("prune current obj %q with kind %s: %w", info.Name, kind, err)
424-
}
425-
pruneOut, err := yaml.Marshal(pruneObj)
426-
if err != nil {
427-
return fmt.Errorf("prune current out %q with kind %s: %w", info.Name, kind, err)
428-
}
429-
releaseManifest = append(releaseManifest, yamlSeperator...)
430-
releaseManifest = append(releaseManifest, pruneOut...)
431-
432-
originalInfo := original.Get(info)
433-
if originalInfo == nil {
434-
return fmt.Errorf("could not find %q", info.Name)
435-
}
436-
437-
patch, patchType, err := createPatch(originalInfo.Object, currentObj, info)
438-
if err != nil {
439-
return err
440-
}
441-
442-
helper.ServerDryRun = true
443-
targetObj, err := helper.Patch(info.Namespace, info.Name, patchType, patch, nil)
444-
if err != nil {
445-
return fmt.Errorf("cannot patch %q with kind %s: %w", info.Name, kind, err)
446-
}
447-
out, _ = jsoniterator.ConfigCompatibleWithStandardLibrary.Marshal(targetObj)
448-
pruneObj, err = deleteStatusAndTidyMetadata(out)
449-
if err != nil {
450-
return fmt.Errorf("prune current obj %q with kind %s: %w", info.Name, kind, err)
451-
}
452-
pruneOut, err = yaml.Marshal(pruneObj)
453-
if err != nil {
454-
return fmt.Errorf("prune current out %q with kind %s: %w", info.Name, kind, err)
455-
}
456-
installManifest = append(installManifest, yamlSeperator...)
457-
installManifest = append(installManifest, pruneOut...)
458-
return nil
459-
})
460-
461-
return releaseManifest, installManifest, err
462-
}
463-
464-
func createPatch(originalObj, currentObj runtime.Object, target *resource.Info) ([]byte, types.PatchType, error) {
465-
oldData, err := json.Marshal(originalObj)
466-
if err != nil {
467-
return nil, types.StrategicMergePatchType, fmt.Errorf("serializing current configuration: %w", err)
468-
}
469-
newData, err := json.Marshal(target.Object)
470-
if err != nil {
471-
return nil, types.StrategicMergePatchType, fmt.Errorf("serializing target configuration: %w", err)
472-
}
473-
474-
// Even if currentObj is nil (because it was not found), it will marshal just fine
475-
currentData, err := json.Marshal(currentObj)
476-
if err != nil {
477-
return nil, types.StrategicMergePatchType, fmt.Errorf("serializing live configuration: %w", err)
478-
}
479-
// kind := target.Mapping.GroupVersionKind.Kind
480-
// if kind == "Deployment" {
481-
// curr, _ := yaml.Marshal(currentObj)
482-
// fmt.Println(string(curr))
483-
// }
484-
485-
// Get a versioned object
486-
versionedObject := kube.AsVersioned(target)
487-
488-
// Unstructured objects, such as CRDs, may not have an not registered error
489-
// returned from ConvertToVersion. Anything that's unstructured should
490-
// use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported
491-
// on objects like CRDs.
492-
_, isUnstructured := versionedObject.(runtime.Unstructured)
493-
494-
// On newer K8s versions, CRDs aren't unstructured but has this dedicated type
495-
_, isCRD := versionedObject.(*apiextv1.CustomResourceDefinition)
496-
497-
if isUnstructured || isCRD {
498-
// fall back to generic JSON merge patch
499-
patch, err := jsonpatch.CreateMergePatch(oldData, newData)
500-
return patch, types.MergePatchType, err
501-
}
502-
503-
patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject)
504-
if err != nil {
505-
return nil, types.StrategicMergePatchType, fmt.Errorf("unable to create patch metadata from object: %w", err)
506-
}
507-
508-
patch, err := strategicpatch.CreateThreeWayMergePatch(oldData, newData, currentData, patchMeta, true)
509-
return patch, types.StrategicMergePatchType, err
510-
}
511-
512-
func objectKey(r *resource.Info) string {
513-
gvk := r.Object.GetObjectKind().GroupVersionKind()
514-
return fmt.Sprintf("%s/%s/%s/%s", gvk.GroupVersion().String(), gvk.Kind, r.Namespace, r.Name)
515-
}
516-
517-
func existingResourceConflict(resources kube.ResourceList) (kube.ResourceList, error) {
518-
var requireUpdate kube.ResourceList
519-
520-
err := resources.Visit(func(info *resource.Info, err error) error {
521-
if err != nil {
522-
return err
523-
}
524-
525-
helper := resource.NewHelper(info.Client, info.Mapping)
526-
_, err = helper.Get(info.Namespace, info.Name)
527-
if err != nil {
528-
if apierrors.IsNotFound(err) {
529-
return nil
530-
}
531-
return fmt.Errorf("could not get information about the resource: %w", err)
532-
}
533-
534-
requireUpdate.Append(info)
535-
return nil
536-
})
537-
538-
return requireUpdate, err
539-
}
540-
541-
func deleteStatusAndTidyMetadata(obj []byte) (map[string]interface{}, error) {
542-
var objectMap map[string]interface{}
543-
err := jsoniterator.Unmarshal(obj, &objectMap)
544-
if err != nil {
545-
return nil, fmt.Errorf("could not unmarshal byte sequence: %w", err)
546-
}
547-
548-
delete(objectMap, "status")
549-
550-
metadata := objectMap["metadata"].(map[string]interface{})
551-
552-
delete(metadata, "managedFields")
553-
delete(metadata, "generation")
554-
555-
// See the below for the goal of this metadata tidy logic.
556-
// https://github.com/databus23/helm-diff/issues/326#issuecomment-1008253274
557-
if a := metadata["annotations"]; a != nil {
558-
annotations := a.(map[string]interface{})
559-
delete(annotations, "meta.helm.sh/release-name")
560-
delete(annotations, "meta.helm.sh/release-namespace")
561-
delete(annotations, "deployment.kubernetes.io/revision")
562-
563-
if len(annotations) == 0 {
564-
delete(metadata, "annotations")
565-
}
566-
}
567-
568-
return objectMap, nil
569-
}

‎manifest/generate.go

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package manifest
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/evanphx/json-patch/v5"
8+
"github.com/json-iterator/go"
9+
"helm.sh/helm/v3/pkg/kube"
10+
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
11+
"k8s.io/apimachinery/pkg/api/errors"
12+
"k8s.io/apimachinery/pkg/runtime"
13+
"k8s.io/apimachinery/pkg/types"
14+
"k8s.io/apimachinery/pkg/util/strategicpatch"
15+
"k8s.io/cli-runtime/pkg/resource"
16+
"sigs.k8s.io/yaml"
17+
)
18+
19+
const (
20+
Helm2TestSuccessHook = "test-success"
21+
Helm3TestHook = "test"
22+
)
23+
24+
var yamlSeperator = []byte("\n---\n")
25+
26+
func Generate(original, target kube.ResourceList) ([]byte, []byte, error) {
27+
var err error
28+
releaseManifest, installManifest := make([]byte, 0), make([]byte, 0)
29+
30+
// to be deleted
31+
targetResources := make(map[string]bool)
32+
for _, r := range target {
33+
targetResources[objectKey(r)] = true
34+
}
35+
for _, r := range original {
36+
if !targetResources[objectKey(r)] {
37+
out, _ := yaml.Marshal(r.Object)
38+
releaseManifest = append(releaseManifest, yamlSeperator...)
39+
releaseManifest = append(releaseManifest, out...)
40+
}
41+
}
42+
43+
existingResources := make(map[string]bool)
44+
for _, r := range original {
45+
existingResources[objectKey(r)] = true
46+
}
47+
48+
var toBeCreated kube.ResourceList
49+
for _, r := range target {
50+
if !existingResources[objectKey(r)] {
51+
toBeCreated = append(toBeCreated, r)
52+
}
53+
}
54+
55+
toBeUpdated, err := existingResourceConflict(toBeCreated)
56+
if err != nil {
57+
return nil, nil, fmt.Errorf("rendered manifests contain a resource that already exists. Unable to continue with update: %w", err)
58+
}
59+
60+
_ = toBeUpdated.Visit(func(r *resource.Info, err error) error {
61+
if err != nil {
62+
return err
63+
}
64+
original.Append(r)
65+
return nil
66+
})
67+
68+
err = target.Visit(func(info *resource.Info, err error) error {
69+
if err != nil {
70+
return err
71+
}
72+
kind := info.Mapping.GroupVersionKind.Kind
73+
74+
// Fetch the current object for the three way merge
75+
helper := resource.NewHelper(info.Client, info.Mapping)
76+
currentObj, err := helper.Get(info.Namespace, info.Name)
77+
if err != nil {
78+
if !errors.IsNotFound(err) {
79+
return fmt.Errorf("could not get information about the resource: %w", err)
80+
}
81+
// to be created
82+
out, _ := yaml.Marshal(info.Object)
83+
installManifest = append(installManifest, yamlSeperator...)
84+
installManifest = append(installManifest, out...)
85+
return nil
86+
}
87+
// to be updated
88+
out, _ := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(currentObj)
89+
pruneObj, err := deleteStatusAndTidyMetadata(out)
90+
if err != nil {
91+
return fmt.Errorf("prune current obj %q with kind %s: %w", info.Name, kind, err)
92+
}
93+
pruneOut, err := yaml.Marshal(pruneObj)
94+
if err != nil {
95+
return fmt.Errorf("prune current out %q with kind %s: %w", info.Name, kind, err)
96+
}
97+
releaseManifest = append(releaseManifest, yamlSeperator...)
98+
releaseManifest = append(releaseManifest, pruneOut...)
99+
100+
originalInfo := original.Get(info)
101+
if originalInfo == nil {
102+
return fmt.Errorf("could not find %q", info.Name)
103+
}
104+
105+
patch, patchType, err := createPatch(originalInfo.Object, currentObj, info)
106+
if err != nil {
107+
return err
108+
}
109+
110+
helper.ServerDryRun = true
111+
targetObj, err := helper.Patch(info.Namespace, info.Name, patchType, patch, nil)
112+
if err != nil {
113+
return fmt.Errorf("cannot patch %q with kind %s: %w", info.Name, kind, err)
114+
}
115+
out, _ = jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(targetObj)
116+
pruneObj, err = deleteStatusAndTidyMetadata(out)
117+
if err != nil {
118+
return fmt.Errorf("prune current obj %q with kind %s: %w", info.Name, kind, err)
119+
}
120+
pruneOut, err = yaml.Marshal(pruneObj)
121+
if err != nil {
122+
return fmt.Errorf("prune current out %q with kind %s: %w", info.Name, kind, err)
123+
}
124+
installManifest = append(installManifest, yamlSeperator...)
125+
installManifest = append(installManifest, pruneOut...)
126+
return nil
127+
})
128+
129+
return releaseManifest, installManifest, err
130+
}
131+
132+
func createPatch(originalObj, currentObj runtime.Object, target *resource.Info) ([]byte, types.PatchType, error) {
133+
oldData, err := json.Marshal(originalObj)
134+
if err != nil {
135+
return nil, types.StrategicMergePatchType, fmt.Errorf("serializing current configuration: %w", err)
136+
}
137+
newData, err := json.Marshal(target.Object)
138+
if err != nil {
139+
return nil, types.StrategicMergePatchType, fmt.Errorf("serializing target configuration: %w", err)
140+
}
141+
142+
// Even if currentObj is nil (because it was not found), it will marshal just fine
143+
currentData, err := json.Marshal(currentObj)
144+
if err != nil {
145+
return nil, types.StrategicMergePatchType, fmt.Errorf("serializing live configuration: %w", err)
146+
}
147+
// kind := target.Mapping.GroupVersionKind.Kind
148+
// if kind == "Deployment" {
149+
// curr, _ := yaml.Marshal(currentObj)
150+
// fmt.Println(string(curr))
151+
// }
152+
153+
// Get a versioned object
154+
versionedObject := kube.AsVersioned(target)
155+
156+
// Unstructured objects, such as CRDs, may not have an not registered error
157+
// returned from ConvertToVersion. Anything that's unstructured should
158+
// use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported
159+
// on objects like CRDs.
160+
_, isUnstructured := versionedObject.(runtime.Unstructured)
161+
162+
// On newer K8s versions, CRDs aren't unstructured but has this dedicated type
163+
_, isCRD := versionedObject.(*v1.CustomResourceDefinition)
164+
165+
if isUnstructured || isCRD {
166+
// fall back to generic JSON merge patch
167+
patch, err := jsonpatch.CreateMergePatch(oldData, newData)
168+
return patch, types.MergePatchType, err
169+
}
170+
171+
patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject)
172+
if err != nil {
173+
return nil, types.StrategicMergePatchType, fmt.Errorf("unable to create patch metadata from object: %w", err)
174+
}
175+
176+
patch, err := strategicpatch.CreateThreeWayMergePatch(oldData, newData, currentData, patchMeta, true)
177+
return patch, types.StrategicMergePatchType, err
178+
}
179+
180+
func objectKey(r *resource.Info) string {
181+
gvk := r.Object.GetObjectKind().GroupVersionKind()
182+
return fmt.Sprintf("%s/%s/%s/%s", gvk.GroupVersion().String(), gvk.Kind, r.Namespace, r.Name)
183+
}
184+
185+
func existingResourceConflict(resources kube.ResourceList) (kube.ResourceList, error) {
186+
var requireUpdate kube.ResourceList
187+
188+
err := resources.Visit(func(info *resource.Info, err error) error {
189+
if err != nil {
190+
return err
191+
}
192+
193+
helper := resource.NewHelper(info.Client, info.Mapping)
194+
_, err = helper.Get(info.Namespace, info.Name)
195+
if err != nil {
196+
if errors.IsNotFound(err) {
197+
return nil
198+
}
199+
return fmt.Errorf("could not get information about the resource: %w", err)
200+
}
201+
202+
requireUpdate.Append(info)
203+
return nil
204+
})
205+
206+
return requireUpdate, err
207+
}
208+
209+
func deleteStatusAndTidyMetadata(obj []byte) (map[string]interface{}, error) {
210+
var objectMap map[string]interface{}
211+
err := jsoniter.Unmarshal(obj, &objectMap)
212+
if err != nil {
213+
return nil, fmt.Errorf("could not unmarshal byte sequence: %w", err)
214+
}
215+
216+
delete(objectMap, "status")
217+
218+
metadata := objectMap["metadata"].(map[string]interface{})
219+
220+
delete(metadata, "managedFields")
221+
delete(metadata, "generation")
222+
223+
// See the below for the goal of this metadata tidy logic.
224+
// https://github.com/databus23/helm-diff/issues/326#issuecomment-1008253274
225+
if a := metadata["annotations"]; a != nil {
226+
annotations := a.(map[string]interface{})
227+
delete(annotations, "meta.helm.sh/release-name")
228+
delete(annotations, "meta.helm.sh/release-namespace")
229+
delete(annotations, "deployment.kubernetes.io/revision")
230+
231+
if len(annotations) == 0 {
232+
delete(metadata, "annotations")
233+
}
234+
}
235+
236+
return objectMap, nil
237+
}

0 commit comments

Comments
 (0)
Please sign in to comment.