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+ values := PartialValues {FullnameOverride : ptr .To ("redpanda" ), RBAC : & PartialRBAC {CreateAdditionalControllerCRs : ptr .To (true )}}
61+
62+ rendered , err := client .Template (ctx , "." , helm.TemplateOptions {
63+ Name : "redpanda" ,
64+ Namespace : "" ,
65+ Values : values ,
66+ })
67+ require .NoError (t , err )
68+
69+ fSys := filesys .MakeFsOnDisk ()
70+ buffy := new (bytes.Buffer )
71+ cmd := build .NewCmdBuild (
72+ fSys , build .MakeHelp (konfig .ProgramName , "build" ), buffy )
73+ require .NoError (t , cmd .RunE (cmd , []string {"testdata" }))
74+
75+ helmObjs , err := kube .DecodeYAML (rendered , Scheme )
76+ require .NoError (t , err )
77+
78+ require .NoError (t , apiextensionsv1 .AddToScheme (Scheme ))
79+ kustomizeObjs , err := kube .DecodeYAML (buffy .Bytes (), Scheme )
80+ require .NoError (t , err )
81+
82+ helmClusterRoleRules , helmRoleRules := ExtractRules (helmObjs )
83+ kClusterRoleRules , kRoleRules := ExtractRules (kustomizeObjs )
84+
85+ assert .JSONEq (t , jsonStr (helmRoleRules ), jsonStr (kRoleRules ), "difference in Roles\n --- Helm / Missing from Kustomize\n +++ Kustomize / Missing from Helm" )
86+ assert .JSONEq (t , jsonStr (helmClusterRoleRules ), jsonStr (kClusterRoleRules ), "difference in ClusterRoles\n --- Helm / Missing from Kustomize\n +++ Kustomize / Missing from Helm" )
87+ }
88+
89+ func jsonStr (in any ) string {
90+ out , err := json .Marshal (in )
91+ if err != nil {
92+ panic (err )
93+ }
94+ return string (out )
95+ }
96+
2697// TestValues asserts that the chart's values.yaml file can be losslessly
2798// loaded into our type [Values] struct.
2899// NB: values.yaml should round trip through [Values], not [PartialValues], as
@@ -48,12 +119,6 @@ func TestTemplate(t *testing.T) {
48119 client , err := helm .New (helm.Options {ConfigHome : testutil .TempDir (t )})
49120 require .NoError (t , err )
50121
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-
57122 casesArchive , err := txtar .ParseFile ("testdata/template-cases.txtar" )
58123 require .NoError (t , err )
59124
@@ -222,3 +287,37 @@ func makeSureTagIsNotEmptyString(values PartialValues, fuzzer *fuzz.Fuzzer) {
222287 }
223288 }
224289}
290+
291+ func CalculateRoleRules (rules []rbacv1.PolicyRule ) map [string ]map [string ]struct {} {
292+ flattened := map [string ]map [string ]struct {}{}
293+ for _ , rule := range rules {
294+ for _ , api := range rule .APIGroups {
295+ for _ , res := range rule .Resources {
296+ key := fmt .Sprintf ("%s#%s" , api , res )
297+
298+ if _ , ok := flattened [key ]; ! ok {
299+ flattened [key ] = map [string ]struct {}{}
300+ }
301+
302+ for _ , verb := range rule .Verbs {
303+ flattened [key ][verb ] = struct {}{}
304+ }
305+ }
306+ }
307+ }
308+ return flattened
309+ }
310+
311+ func ExtractRules (objs []kube.Object ) (map [string ]map [string ]struct {}, map [string ]map [string ]struct {}) {
312+ var rules []rbacv1.PolicyRule
313+ var clusterRules []rbacv1.PolicyRule
314+ for _ , o := range objs {
315+ switch obj := o .(type ) {
316+ case * rbacv1.Role :
317+ rules = append (rules , obj .Rules ... )
318+ case * rbacv1.ClusterRole :
319+ clusterRules = append (clusterRules , obj .Rules ... )
320+ }
321+ }
322+ return CalculateRoleRules (clusterRules ), CalculateRoleRules (rules )
323+ }
0 commit comments