github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/util/helm/diff_test.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package helm
    21  
    22  import (
    23  	. "github.com/onsi/ginkgo/v2"
    24  	. "github.com/onsi/gomega"
    25  	"golang.org/x/exp/maps"
    26  
    27  	"go.uber.org/zap/buffer"
    28  	helm "helm.sh/helm/v3/pkg/release"
    29  	"sigs.k8s.io/kustomize/kyaml/yaml"
    30  )
    31  
    32  const (
    33  	resourceName = "kubeblocks-opsrequest-editor-role, ClusterRole (rbac.authorization.k8s.io)"
    34  	exceptRemove = `--- kubeblocks-opsrequest-editor-role, ClusterRole (rbac.authorization.k8s.io) 0.5.1-fake
    35  +++ 
    36  @@ -1,13 +1 @@
    37  -apiVersion: rbac.authorization.k8s.io/v1
    38  -kind: ClusterRole
    39  -metadata:
    40  -  labels:
    41  -    app.kubernetes.io/instance: kubeblocks
    42  -    app.kubernetes.io/managed-by: Helm
    43  -    app.kubernetes.io/name: kubeblocks
    44  -  name: kubeblocks-opsrequest-editor-role
    45  -rules:
    46  -  change: 1
    47  -  slice:
    48  -  - {}
    49   
    50  
    51  `
    52  	exceptAdd = `--- kubeblocks-opsrequest-editor-role, ClusterRole (rbac.authorization.k8s.io) 0.5.1-fake
    53  +++ kubeblocks-opsrequest-editor-role, ClusterRole (rbac.authorization.k8s.io) 0.5.2-fake
    54  @@ -9,2 +9,3 @@
    55   rules:
    56  +  apiGroups: apps
    57     change: 1
    58  
    59  `
    60  	exceptModify = `--- kubeblocks-opsrequest-editor-role, ClusterRole (rbac.authorization.k8s.io) 0.5.1-fake
    61  +++ kubeblocks-opsrequest-editor-role, ClusterRole (rbac.authorization.k8s.io) 0.5.2-fake
    62  @@ -9,3 +9,3 @@
    63   rules:
    64  -  apiGroups: appsv2
    65  +  apiGroups: apps
    66     change: 1
    67  
    68  `
    69  
    70  	crdContent = `apiVersion: apiextensions.k8s.io/v1
    71  kind: CustomResourceDefinition
    72  metadata:
    73    name: componentclassdefinitions.apps.kubeblocks.io
    74  spec:
    75    group: apps.kubeblocks.io
    76    names:
    77      categories:
    78      - kubeblocks
    79      kind: ComponentClassDefinition
    80    scope: Cluster
    81    versions:
    82    - name: v1alpha1
    83      schema:
    84        openAPIV3Schema:
    85          description: ComponentClassDefinition is the Schema for the componentclassdefinitions
    86            API
    87          properties:
    88            apiVersion:
    89              type: string
    90      served: true
    91      storage: true
    92      subresources:
    93        status: {}`
    94  )
    95  
    96  var _ = Describe("helm diff", func() {
    97  	var obj map[any]any
    98  	var crdObj map[any]any
    99  	var content string
   100  	var release *helm.Release
   101  	var out buffer.Buffer
   102  
   103  	buildRelease := func(obj map[any]any) *helm.Release {
   104  		var res helm.Release
   105  		marshal, _ := yaml.Marshal(obj)
   106  		manifest := `---
   107  # Source: kubeblocks/templates/rbac/apps_backuppolicytemplate_editor_role.yaml
   108  # permissions for end users to edit backuppolicytemplates.` + "\n" + string(marshal)
   109  		res.Manifest = manifest
   110  		return &res
   111  	}
   112  
   113  	exceptToMap := func(obj map[any]any, key string) map[any]any {
   114  		Expect(obj).ShouldNot(BeNil())
   115  		m, ok := obj[key].(map[any]any)
   116  		Expect(ok).Should(BeTrue())
   117  		return m
   118  	}
   119  	exceptToSlice := func(obj map[any]any, key string) []any {
   120  		Expect(obj).ShouldNot(BeNil())
   121  		m, ok := obj[key].([]any)
   122  		Expect(ok).Should(BeTrue())
   123  		return m
   124  	}
   125  
   126  	BeforeEach(func() {
   127  		obj = map[any]any{
   128  			"apiVersion": "rbac.authorization.k8s.io/v1",
   129  			"kind":       "ClusterRole",
   130  			"metadata": map[any]any{
   131  				"name": "kubeblocks-opsrequest-editor-role",
   132  				"labels": map[any]any{
   133  					"helm.sh/chart":                "kubeblocks-0.5.1",
   134  					"app.kubernetes.io/name":       "kubeblocks",
   135  					"app.kubernetes.io/instance":   "kubeblocks",
   136  					"app.kubernetes.io/version":    "0.5.1",
   137  					"app.kubernetes.io/managed-by": "Helm",
   138  				},
   139  			},
   140  			"rules": map[any]any{
   141  				"description": "should be delete",
   142  				"change":      1,
   143  				"slice": []any{
   144  					map[any]any{
   145  						"description": "should be delete too",
   146  					},
   147  				},
   148  			},
   149  		}
   150  		release = buildRelease(obj)
   151  		content = release.Manifest
   152  	})
   153  
   154  	It("test metadata String", func() {
   155  		m := []metadata{
   156  			{APIVersion: "rbac.authorization.k8s.io/v1",
   157  				Kind: "ClusterRole",
   158  				Metadata: struct {
   159  					Name   string            `yaml:"name"`
   160  					Labels map[string]string `yaml:"labels"`
   161  				}{
   162  					"kubeblocks-opsrequest-editor-role",
   163  					map[string]string{},
   164  				},
   165  			}, {APIVersion: "v1",
   166  				Kind: "Service",
   167  				Metadata: struct {
   168  					Name   string            `yaml:"name"`
   169  					Labels map[string]string `yaml:"labels"`
   170  				}{
   171  					"kubeblocks",
   172  					map[string]string{},
   173  				},
   174  			},
   175  		}
   176  		e := []string{
   177  			resourceName,
   178  			"kubeblocks, Service (v1)",
   179  		}
   180  		for i := range m {
   181  			Expect(m[i].String()).Should(Equal(e[i]))
   182  		}
   183  	})
   184  
   185  	It("test sortedKey", func() {
   186  		manifest := map[string]*MappingResult{
   187  			"b": nil,
   188  			"c": nil,
   189  			"a": nil,
   190  		}
   191  		Expect(sortedKeys(manifest)).Should(Equal([]string{"a", "b", "c"}))
   192  	})
   193  
   194  	It("test delete obj label", func() {
   195  		testObj := obj
   196  		metadata := exceptToMap(testObj, "metadata")
   197  		labels := exceptToMap(metadata, "labels")
   198  		Expect(labels["helm.sh/chart"]).ShouldNot(BeNil())
   199  		deleteLabel(&testObj, "helm.sh/chart")
   200  		Expect(labels["helm.sh/chart"]).Should(BeNil())
   201  	})
   202  
   203  	It("test delete obj field", func() {
   204  		testObj := obj
   205  		rules := exceptToMap(testObj, "rules")
   206  		slice := exceptToSlice(rules, "slice")
   207  		Expect(rules["description"]).ShouldNot(BeNil())
   208  		Expect(slice[0]).ShouldNot(BeEmpty())
   209  		deleteObjField(&testObj, "description")
   210  		Expect(rules["description"]).Should(BeNil())
   211  		Expect(slice[0]).Should(BeEmpty())
   212  	})
   213  
   214  	Context("test ParseContent", func() {
   215  
   216  		It("test ParseContent", func() {
   217  			parseContent, err := ParseContent(content)
   218  			Expect(err).ShouldNot(HaveOccurred())
   219  			Expect(parseContent.Name).Should(Equal(resourceName))
   220  			Expect(parseContent.Kind).Should(Equal("ClusterRole"))
   221  			unusefulContent := `---
   222  # Source: kubeblocks/templates/rbac/apps_backuppolicytemplate_editor_role.yaml
   223  # permissions for end users to edit backuppolicytemplates.
   224  `
   225  			parseContent, err = ParseContent(unusefulContent)
   226  			Expect(err).ShouldNot(HaveOccurred())
   227  			Expect(parseContent).Should(BeNil())
   228  			errorContent := `---
   229  			# Source: kubeblocks/tJ!@JDASD!bASD!@D!@
   230  			# permissions for end users to edit backuppolicytemplates.
   231  			ASDasdodh1*(!@#D!`
   232  			parseContent, err = ParseContent(errorContent)
   233  			Expect(err).Should(HaveOccurred())
   234  			Expect(parseContent).Should(BeNil())
   235  		})
   236  
   237  		It("test black list", func() {
   238  			blackListContent := `apiVersion: v1
   239  kind: ConfigMap
   240  metadata:
   241    name: grafana-chart-kubeblocks-values
   242  data:
   243    values-kubeblocks-override.yaml: |-
   244      adminPassword: kubeblocks
   245      adminUser: admin`
   246  			parseContent, err := ParseContent(blackListContent)
   247  			Expect(err).Should(Succeed())
   248  			Expect(parseContent).Should(BeNil())
   249  		})
   250  
   251  		It("test Parse CRD", func() {
   252  			parseContent, err := ParseContent(crdContent)
   253  			Expect(err).Should(Succeed())
   254  			Expect(parseContent).ShouldNot(BeNil())
   255  		})
   256  	})
   257  
   258  	It("test buildManifestMapByRelease", func() {
   259  		releaseMap, err := buildManifestMapByRelease(release)
   260  		Expect(err).ShouldNot(HaveOccurred())
   261  		Expect(releaseMap).Should(HaveKey(resourceName))
   262  	})
   263  
   264  	Context("test OutputDiff", func() {
   265  		BeforeEach(func() {
   266  			crdObj = map[any]any{
   267  				"apiVersion": "apiextensions.k8s.io/v1",
   268  				"kind":       "CustomResourceDefinition",
   269  				"metadata": map[any]any{
   270  					"name": "kubeblocks-opsrequest-editor-role",
   271  					"labels": map[any]any{
   272  						"helm.sh/chart": "kubeblocks-0.5.1",
   273  					},
   274  				},
   275  				"spec": map[any]any{
   276  					"versions": []any{
   277  						map[any]any{
   278  							"schema": map[any]any{
   279  								"openAPIV3Schema": map[any]any{
   280  									"type": object,
   281  									"properties": map[any]any{
   282  										"apiVersion": map[any]any{"type": "string"},
   283  										"kind":       map[any]any{"type": "string"},
   284  										"metadata":   map[any]any{"type": object},
   285  										"spec": map[any]any{
   286  											"properties": map[any]any{
   287  												"fake": map[any]any{"type": "string"},
   288  											},
   289  											"type": object,
   290  										},
   291  									},
   292  								},
   293  							},
   294  						},
   295  					},
   296  				},
   297  			}
   298  		})
   299  		It("test OutputDiff CRD", func() {
   300  			crdRelease := buildRelease(crdObj)
   301  			Expect(OutputDiff(crdRelease, nil, "0.5.1-fake", "", &out, false)).ShouldNot(HaveOccurred())
   302  			Expect(out.String()).Should(Equal(`CUSTOMRESOURCEDEFINITION            MODE      
   303  kubeblocks-opsrequest-editor-role   Removed   
   304  
   305  `))
   306  			out.Reset()
   307  			Expect(OutputDiff(nil, crdRelease, "", "0.5.1-fake", &out, false)).Should(Succeed())
   308  			Expect(out.String()).Should(Equal(`CUSTOMRESOURCEDEFINITION            MODE    
   309  kubeblocks-opsrequest-editor-role   Added   
   310  
   311  `))
   312  		})
   313  
   314  		It("test OutputDiff detail", func() {
   315  			out.Reset()
   316  			releaseA := release
   317  			Expect(OutputDiff(releaseA, nil, "0.5.1-fake", "", &out, true)).Should(Succeed())
   318  			Expect(out.String()).Should(Equal(exceptRemove))
   319  			out.Reset()
   320  			// add
   321  			otherObj := obj
   322  			rules := exceptToMap(otherObj, "rules")
   323  			rules["apiGroups"] = "apps"
   324  			releaseB := buildRelease(otherObj)
   325  			Expect(OutputDiff(releaseA, releaseB, "0.5.1-fake", "0.5.2-fake", &out, true)).Should(Succeed())
   326  			Expect(out.String()).Should(Equal(exceptAdd))
   327  			// modify
   328  			out.Reset()
   329  			rules["apiGroups"] = "appsv2"
   330  			releaseA = buildRelease(otherObj)
   331  			Expect(OutputDiff(releaseA, releaseB, "0.5.1-fake", "0.5.2-fake", &out, true)).Should(Succeed())
   332  			Expect(out.String()).Should(Equal(exceptModify))
   333  		})
   334  	})
   335  
   336  	Context("test outputCRDDiff", func() {
   337  		var apiA map[string]any
   338  		var properties = map[any]any{}
   339  
   340  		removeAPIAndAddAPI := func() map[string]any {
   341  			var newProperties = map[any]any{}
   342  			maps.Copy(newProperties, properties)
   343  			delete(newProperties, "namespace")
   344  			newProperties["newApi"] = map[any]any{TYPE: "boolean"}
   345  			temp := map[string]any{
   346  				"spec": map[any]any{
   347  					REQUIRED:   []any{"name"},
   348  					TYPE:       object,
   349  					PROPERTIES: newProperties,
   350  				},
   351  			}
   352  			return temp
   353  		}
   354  
   355  		modifyTheRequired := func() map[string]any {
   356  			temp := map[string]any{
   357  				"spec": map[any]any{
   358  					TYPE:       object,
   359  					PROPERTIES: properties,
   360  				},
   361  			}
   362  			return temp
   363  		}
   364  
   365  		modifyTheField := func() map[string]any {
   366  			var newProperties = map[any]any{}
   367  			maps.Copy(newProperties, properties)
   368  			newProperties["name"] = map[any]any{TYPE: "string", "maxLength": 63}
   369  			temp := map[string]any{
   370  				"spec": map[any]any{
   371  					REQUIRED:   []any{"name"},
   372  					TYPE:       object,
   373  					PROPERTIES: newProperties,
   374  				},
   375  			}
   376  			return temp
   377  		}
   378  		BeforeEach(func() {
   379  			properties = map[any]any{
   380  				"name":      map[any]any{TYPE: "string"},
   381  				"namespace": map[any]any{TYPE: "string"},
   382  				"defaultInstallValues": map[any]any{
   383  					TYPE: array,
   384  					"items": map[any]any{
   385  						TYPE: object,
   386  						PROPERTIES: map[any]any{
   387  							"container1": map[any]any{TYPE: "string"},
   388  							"container2": map[any]any{TYPE: "string"},
   389  						},
   390  					},
   391  				},
   392  			}
   393  			apiA = map[string]any{
   394  				"spec": map[any]any{
   395  					REQUIRED:   []any{"name"},
   396  					TYPE:       object,
   397  					PROPERTIES: properties,
   398  				},
   399  			}
   400  		})
   401  		It("test Added and Removed", func() {
   402  			out.Reset()
   403  			apiB := removeAPIAndAddAPI()
   404  			outputCRDDiff(apiA, apiB, "Fake CRD", &out)
   405  			Expect(out.String()).Should(Equal(`Fake CRD
   406  API              IS-REQUIRED   MODE      DETAILS             
   407  spec.newApi      false         Added                         
   408  spec.namespace   false         Removed   {"type":"string"}   
   409  
   410  `))
   411  		})
   412  		It("test Modified", func() {
   413  			out.Reset()
   414  			apiB := modifyTheField()
   415  			outputCRDDiff(apiA, apiB, "Fake CRD", &out)
   416  			Expect(out.String()).Should(Equal(`Fake CRD
   417  API         IS-REQUIRED   MODE       DETAILS                                                 
   418  spec.name   true          Modified   {"type":"string"} -> {"maxLength":63,"type":"string"}   
   419  
   420  `))
   421  			out.Reset()
   422  			apiB = modifyTheRequired()
   423  			outputCRDDiff(apiA, apiB, "Fake CRD", &out)
   424  			Expect(out.String()).Should(Equal(`Fake CRD
   425  API         IS-REQUIRED     MODE       DETAILS   
   426  spec.name   true -> false   Modified             
   427  
   428  `))
   429  		})
   430  
   431  	})
   432  })