11package operator
22
33import (
4+ "bytes"
45 "encoding/json"
56 "fmt"
7+ "log"
68 "os"
9+ "os/exec"
710 "regexp"
811 "slices"
912 "strconv"
1013 "testing"
1114
1215 fuzz "github.com/google/gofuzz"
1316 "github.com/redpanda-data/helm-charts/pkg/helm"
17+ "github.com/redpanda-data/helm-charts/pkg/kube"
1418 "github.com/redpanda-data/helm-charts/pkg/testutil"
1519 "github.com/santhosh-tekuri/jsonschema/v5"
20+ "github.com/stretchr/testify/assert"
1621 "github.com/stretchr/testify/require"
1722 "golang.org/x/tools/txtar"
1823 corev1 "k8s.io/api/core/v1"
24+ rbacv1 "k8s.io/api/rbac/v1"
25+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1926 "k8s.io/apimachinery/pkg/api/resource"
2027 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2128 "k8s.io/apimachinery/pkg/util/intstr"
2229 "k8s.io/utils/ptr"
30+ "sigs.k8s.io/kustomize/api/konfig"
31+ "sigs.k8s.io/kustomize/kustomize/v5/commands/build"
32+ "sigs.k8s.io/kustomize/kyaml/filesys"
2333 "sigs.k8s.io/yaml"
2434)
2535
36+ func TestMain (m * testing.M ) {
37+ // Chart deps are kept within ./charts as a tgz archive, which is git
38+ // ignored. Helm dep build will ensure that ./charts is in sync with
39+ // Chart.lock, which is tracked by git.
40+ // This is performed in TestMain as there may be many tests that run the
41+ // redpanda helm chart.
42+ out , err := exec .Command ("helm" , "repo" , "add" , "prometheus" , "https://prometheus-community.github.io/helm-charts" ).CombinedOutput ()
43+ if err != nil {
44+ log .Fatalf ("failed to run helm repo add: %s" , out )
45+ }
46+
47+ out , err = exec .Command ("helm" , "dep" , "build" , "." ).CombinedOutput ()
48+ if err != nil {
49+ log .Fatalf ("failed to run helm dep build: %s" , out )
50+ }
51+
52+ os .Exit (m .Run ())
53+ }
54+
55+ func TestHelmKustomizeEquivalence (t * testing.T ) {
56+ ctx := testutil .Context (t )
57+ client , err := helm .New (helm.Options {ConfigHome : testutil .TempDir (t )})
58+ require .NoError (t , err )
59+
60+ kustomization , err := os .ReadFile ("testdata/kustomization.yaml" )
61+ require .NoError (t , err )
62+ require .Containsf (t , string (kustomization ), ChartMeta ().AppVersion , "kustomization.yaml should reference the current appVersion: %s" , chartMeta .AppVersion )
63+
64+ values := PartialValues {FullnameOverride : ptr .To ("redpanda" ), RBAC : & PartialRBAC {CreateAdditionalControllerCRs : ptr .To (true )}}
65+
66+ rendered , err := client .Template (ctx , "." , helm.TemplateOptions {
67+ Name : "redpanda" ,
68+ Namespace : "" ,
69+ Values : values ,
70+ })
71+ require .NoError (t , err )
72+
73+ fSys := filesys .MakeFsOnDisk ()
74+ buffy := new (bytes.Buffer )
75+ cmd := build .NewCmdBuild (
76+ fSys , build .MakeHelp (konfig .ProgramName , "build" ), buffy )
77+ require .NoError (t , cmd .RunE (cmd , []string {"testdata" }))
78+
79+ helmObjs , err := kube .DecodeYAML (rendered , Scheme )
80+ require .NoError (t , err )
81+
82+ require .NoError (t , apiextensionsv1 .AddToScheme (Scheme ))
83+ kustomizeObjs , err := kube .DecodeYAML (buffy .Bytes (), Scheme )
84+ require .NoError (t , err )
85+
86+ helmClusterRoleRules , helmRoleRules := ExtractRules (helmObjs )
87+ kClusterRoleRules , kRoleRules := ExtractRules (kustomizeObjs )
88+
89+ assert .JSONEq (t , jsonStr (helmRoleRules ), jsonStr (kRoleRules ), "difference in Roles\n --- Helm / Missing from Kustomize\n +++ Kustomize / Missing from Helm" )
90+ assert .JSONEq (t , jsonStr (helmClusterRoleRules ), jsonStr (kClusterRoleRules ), "difference in ClusterRoles\n --- Helm / Missing from Kustomize\n +++ Kustomize / Missing from Helm" )
91+ }
92+
93+ func jsonStr (in any ) string {
94+ out , err := json .Marshal (in )
95+ if err != nil {
96+ panic (err )
97+ }
98+ return string (out )
99+ }
100+
26101// TestValues asserts that the chart's values.yaml file can be losslessly
27102// loaded into our type [Values] struct.
28103// NB: values.yaml should round trip through [Values], not [PartialValues], as
@@ -48,12 +123,6 @@ func TestTemplate(t *testing.T) {
48123 client , err := helm .New (helm.Options {ConfigHome : testutil .TempDir (t )})
49124 require .NoError (t , err )
50125
51- // Chart deps are kept within ./charts as a tgz archive, which is git
52- // ignored. Helm dep build will ensure that ./charts is in sync with
53- // Chart.lock, which is tracked by git.
54- require .NoError (t , client .RepoAdd (ctx , "prometheus" , "https://prometheus-community.github.io/helm-charts" ))
55- require .NoError (t , client .DependencyBuild (ctx , "." ), "failed to refresh helm dependencies" )
56-
57126 casesArchive , err := txtar .ParseFile ("testdata/template-cases.txtar" )
58127 require .NoError (t , err )
59128
@@ -222,3 +291,37 @@ func makeSureTagIsNotEmptyString(values PartialValues, fuzzer *fuzz.Fuzzer) {
222291 }
223292 }
224293}
294+
295+ func CalculateRoleRules (rules []rbacv1.PolicyRule ) map [string ]map [string ]struct {} {
296+ flattened := map [string ]map [string ]struct {}{}
297+ for _ , rule := range rules {
298+ for _ , api := range rule .APIGroups {
299+ for _ , res := range rule .Resources {
300+ key := fmt .Sprintf ("%s#%s" , api , res )
301+
302+ if _ , ok := flattened [key ]; ! ok {
303+ flattened [key ] = map [string ]struct {}{}
304+ }
305+
306+ for _ , verb := range rule .Verbs {
307+ flattened [key ][verb ] = struct {}{}
308+ }
309+ }
310+ }
311+ }
312+ return flattened
313+ }
314+
315+ func ExtractRules (objs []kube.Object ) (map [string ]map [string ]struct {}, map [string ]map [string ]struct {}) {
316+ var rules []rbacv1.PolicyRule
317+ var clusterRules []rbacv1.PolicyRule
318+ for _ , o := range objs {
319+ switch obj := o .(type ) {
320+ case * rbacv1.Role :
321+ rules = append (rules , obj .Rules ... )
322+ case * rbacv1.ClusterRole :
323+ clusterRules = append (clusterRules , obj .Rules ... )
324+ }
325+ }
326+ return CalculateRoleRules (clusterRules ), CalculateRoleRules (rules )
327+ }
0 commit comments