From 397feb8484bd6a1fad5a67f83345ea0979e1c4b9 Mon Sep 17 00:00:00 2001 From: Hanife Tastemel Zeray Date: Tue, 18 Jun 2024 16:01:52 +0200 Subject: [PATCH 1/3] Add new template and objectkind configuration --- pkg/templates/all/all.go | 1 + .../internal/params/gen-params.go | 72 +++++++++++++++++++ .../internal/params/params.go | 8 +++ .../volumeclaimtemplates/template.go | 57 +++++++++++++++ .../volumeclaimtemplates/template_test.go | 0 5 files changed, 138 insertions(+) create mode 100644 pkg/templates/volumeclaimtemplates/internal/params/gen-params.go create mode 100644 pkg/templates/volumeclaimtemplates/internal/params/params.go create mode 100644 pkg/templates/volumeclaimtemplates/template.go create mode 100644 pkg/templates/volumeclaimtemplates/template_test.go diff --git a/pkg/templates/all/all.go b/pkg/templates/all/all.go index 01f8db3e7..a62c1ea59 100644 --- a/pkg/templates/all/all.go +++ b/pkg/templates/all/all.go @@ -56,6 +56,7 @@ import ( _ "golang.stackrox.io/kube-linter/pkg/templates/targetport" _ "golang.stackrox.io/kube-linter/pkg/templates/unsafeprocmount" _ "golang.stackrox.io/kube-linter/pkg/templates/updateconfig" + _ "golang.stackrox.io/kube-linter/pkg/templates/volumeclaimtemplates" _ "golang.stackrox.io/kube-linter/pkg/templates/wildcardinrules" _ "golang.stackrox.io/kube-linter/pkg/templates/writablehostmount" ) diff --git a/pkg/templates/volumeclaimtemplates/internal/params/gen-params.go b/pkg/templates/volumeclaimtemplates/internal/params/gen-params.go new file mode 100644 index 000000000..d3f80383f --- /dev/null +++ b/pkg/templates/volumeclaimtemplates/internal/params/gen-params.go @@ -0,0 +1,72 @@ +// Code generated by kube-linter template codegen. DO NOT EDIT. +// +build !templatecodegen + +package params + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/pkg/check" + "golang.stackrox.io/kube-linter/pkg/templates/util" +) + +var ( + // Use some imports in case they don't get used otherwise. + _ = util.MustParseParameterDesc + _ = fmt.Sprintf + + volumeClaimTemplateParamDesc = util.MustParseParameterDesc(`{ + "Name": "Annotation", + "Type": "string", + "Description": " ", + "Examples": null, + "Enum": null, + "SubParameters": null, + "ArrayElemType": "", + "Required": true, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "Annotation", + "XXXIsPointer": false +} +`) + + ParamDescs = []check.ParameterDesc{ + volumeClaimTemplateParamDesc, + } +) + +func (p *Params) Validate() error { + var validationErrors []string + if p.Annotation == "" { + validationErrors = append(validationErrors, "required param annotation not found") + } + if len(validationErrors) > 0 { + return errors.Errorf("invalid parameters: %s", strings.Join(validationErrors, ", ")) + } + return nil +} + +// ParseAndValidate instantiates a Params object out of the passed map[string]interface{}, +// validates it, and returns it. +// The return type is interface{} to satisfy the type in the Template struct. +func ParseAndValidate(m map[string]interface{}) (interface{}, error) { + var p Params + if err := util.DecodeMapStructure(m, &p); err != nil { + return nil, err + } + if err := p.Validate(); err != nil { + return nil, err + } + return p, nil +} + +// WrapInstantiateFunc is a convenience wrapper that wraps an untyped instantiate function +// into a typed one. +func WrapInstantiateFunc(f func(p Params) (check.Func, error)) func (interface{}) (check.Func, error) { + return func(paramsInt interface{}) (check.Func, error) { + return f(paramsInt.(Params)) + } +} diff --git a/pkg/templates/volumeclaimtemplates/internal/params/params.go b/pkg/templates/volumeclaimtemplates/internal/params/params.go new file mode 100644 index 000000000..8fa802de9 --- /dev/null +++ b/pkg/templates/volumeclaimtemplates/internal/params/params.go @@ -0,0 +1,8 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { + // Annotation specifies the required annotation to match. + // +required + Annotation string +} diff --git a/pkg/templates/volumeclaimtemplates/template.go b/pkg/templates/volumeclaimtemplates/template.go new file mode 100644 index 000000000..c10c34d69 --- /dev/null +++ b/pkg/templates/volumeclaimtemplates/template.go @@ -0,0 +1,57 @@ +package volumeclaimtemplates + +import ( + "fmt" + + "golang.stackrox.io/kube-linter/pkg/check" + "golang.stackrox.io/kube-linter/pkg/config" + "golang.stackrox.io/kube-linter/pkg/diagnostic" + "golang.stackrox.io/kube-linter/pkg/lintcontext" + "golang.stackrox.io/kube-linter/pkg/objectkinds" + "golang.stackrox.io/kube-linter/pkg/templates" + "golang.stackrox.io/kube-linter/pkg/templates/volumeclaimtemplates/internal/params" + v1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +const ( + templateKey = "statefulset-volumeclaimtemplate-annotation" +) + +func init() { + templates.Register(check.Template{ + HumanName: "StatefulSet VolumeClaimTemplate Annotation", + Key: templateKey, + Description: "Check if StatefulSet's VolumeClaimTemplate contains a specific annotation", + SupportedObjectKinds: config.ObjectKindsDesc{ + ObjectKinds: []string{objectkinds.DeploymentLike}, + }, + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) { + return func(_ lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { + k8sObj, ok := object.K8sObject.(*unstructured.Unstructured) + if !ok { + return nil + } + if k8sObj.GetKind() != "StatefulSet" { + return nil + } + var statefulSet v1.StatefulSet + err := runtime.DefaultUnstructuredConverter.FromUnstructured(k8sObj.UnstructuredContent(), &statefulSet) + if err != nil { + return nil + } + for _, volumeClaimTemplate := range statefulSet.Spec.VolumeClaimTemplates { + if annotationValue, found := volumeClaimTemplate.Annotations[p.Annotation]; found { + return []diagnostic.Diagnostic{{ + Message: fmt.Sprintf("found annotation %q with value %q in VolumeClaimTemplate", p.Annotation, annotationValue), + }} + } + } + return nil + }, nil + } ), + }) +} diff --git a/pkg/templates/volumeclaimtemplates/template_test.go b/pkg/templates/volumeclaimtemplates/template_test.go new file mode 100644 index 000000000..e69de29bb From aaddccda11785428927abf39c886d1340b9f64f0 Mon Sep 17 00:00:00 2001 From: Hanife Tastemel Zeray Date: Tue, 18 Jun 2024 16:02:13 +0200 Subject: [PATCH 2/3] Add new template and objectkind configuration --- .../yamls/volumeclaimtemplates.yaml | 7 ++++++ pkg/objectkinds/pvc.go | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 pkg/builtinchecks/yamls/volumeclaimtemplates.yaml create mode 100644 pkg/objectkinds/pvc.go diff --git a/pkg/builtinchecks/yamls/volumeclaimtemplates.yaml b/pkg/builtinchecks/yamls/volumeclaimtemplates.yaml new file mode 100644 index 000000000..0ea411619 --- /dev/null +++ b/pkg/builtinchecks/yamls/volumeclaimtemplates.yaml @@ -0,0 +1,7 @@ +name: "volumeclaimtemplates" +description: "Checks StatefulSets for the presence of a specified annotation in their VolumeClaimTemplate." +remediation: "Add an specified annotation to volumeClaimTemplate." +scope: + objectKinds: + - DeploymentLike +template: "volumeclaimtemplates" diff --git a/pkg/objectkinds/pvc.go b/pkg/objectkinds/pvc.go new file mode 100644 index 000000000..4c8b47ba6 --- /dev/null +++ b/pkg/objectkinds/pvc.go @@ -0,0 +1,24 @@ +package objectkinds + +import ( + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + PersistentVolumeClaim = "PersistentVolumeClaim" +) + +var ( + persistentvolumeclaimGVK = v1.SchemeGroupVersion.WithKind("PersistentVolumeClaim") +) + +func init() { + RegisterObjectKind(PersistentVolumeClaim, MatcherFunc(func(gvk schema.GroupVersionKind) bool { + return gvk == persistentvolumeclaimGVK + })) +} + +func GetPersistentVolumeClaimAPIVersion() string { + return persistentvolumeclaimGVK.GroupVersion().String() +} \ No newline at end of file From 99a2adbc195efae42bd8b4ad42000a151aeca0cc Mon Sep 17 00:00:00 2001 From: Hanife Tastemel Zeray Date: Wed, 17 Jul 2024 09:14:07 +0200 Subject: [PATCH 3/3] Update new template and add extract file --- docs/generated/templates.md | 20 ++++++ .../yamls/volumeclaimtemplates.yaml | 7 -- pkg/extract/sts_spec.go | 32 +++++++++ pkg/lintcontext/mocks/context.go | 5 ++ .../internal/params/gen-params.go | 8 +-- .../volumeclaimtemplates/template.go | 71 +++++++++---------- .../volumeclaimtemplates/template_test.go | 66 +++++++++++++++++ 7 files changed, 160 insertions(+), 49 deletions(-) delete mode 100644 pkg/builtinchecks/yamls/volumeclaimtemplates.yaml create mode 100644 pkg/extract/sts_spec.go diff --git a/docs/generated/templates.md b/docs/generated/templates.md index 5bb23a908..0a03b1bf7 100644 --- a/docs/generated/templates.md +++ b/docs/generated/templates.md @@ -759,6 +759,26 @@ KubeLinter supports the following templates: **Supported Objects**: DeploymentLike +## StatefulSet VolumeClaimTemplate Annotation + +**Key**: `statefulset-volumeclaimtemplate-annotation` + +**Description**: Check if StatefulSet's VolumeClaimTemplate contains a specific annotation + +**Supported Objects**: DeploymentLike + + +**Parameters**: + +```yaml +- description: Annotation specifies the required annotation to match. + name: annotation + negationAllowed: true + regexAllowed: true + required: true + type: string +``` + ## Target Port **Key**: `target-port` diff --git a/pkg/builtinchecks/yamls/volumeclaimtemplates.yaml b/pkg/builtinchecks/yamls/volumeclaimtemplates.yaml deleted file mode 100644 index 0ea411619..000000000 --- a/pkg/builtinchecks/yamls/volumeclaimtemplates.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: "volumeclaimtemplates" -description: "Checks StatefulSets for the presence of a specified annotation in their VolumeClaimTemplate." -remediation: "Add an specified annotation to volumeClaimTemplate." -scope: - objectKinds: - - DeploymentLike -template: "volumeclaimtemplates" diff --git a/pkg/extract/sts_spec.go b/pkg/extract/sts_spec.go new file mode 100644 index 000000000..933d218b5 --- /dev/null +++ b/pkg/extract/sts_spec.go @@ -0,0 +1,32 @@ +package extract + +import ( + "reflect" + + "golang.stackrox.io/kube-linter/pkg/k8sutil" + appsV1 "k8s.io/api/apps/v1" +) + +func StatefulSetSpec(obj k8sutil.Object) (appsV1.StatefulSetSpec, bool) { + switch obj := obj.(type) { + case *appsV1.StatefulSet: + return obj.Spec, true + default: + + kind := obj.GetObjectKind().GroupVersionKind().Kind + if kind != "StatefulSet" { + return appsV1.StatefulSetSpec{}, false + } + + objValue := reflect.Indirect(reflect.ValueOf(obj)) + spec := objValue.FieldByName("Spec") + if !spec.IsValid() { + return appsV1.StatefulSetSpec{}, false + } + statefulSetSpec, ok := spec.Interface().(appsV1.StatefulSetSpec) + if ok { + return statefulSetSpec, true + } + return appsV1.StatefulSetSpec{}, false + } +} diff --git a/pkg/lintcontext/mocks/context.go b/pkg/lintcontext/mocks/context.go index 7a3b8eb87..ee6644db5 100644 --- a/pkg/lintcontext/mocks/context.go +++ b/pkg/lintcontext/mocks/context.go @@ -28,3 +28,8 @@ func (l *MockLintContext) InvalidObjects() []lintcontext.InvalidObject { func NewMockContext() *MockLintContext { return &MockLintContext{objects: make(map[string]k8sutil.Object)} } + +// AddObject adds an object to the MockLintContext +func (l *MockLintContext) AddObject(key string, obj k8sutil.Object) { + l.objects[key] = obj +} diff --git a/pkg/templates/volumeclaimtemplates/internal/params/gen-params.go b/pkg/templates/volumeclaimtemplates/internal/params/gen-params.go index d3f80383f..82683ceaa 100644 --- a/pkg/templates/volumeclaimtemplates/internal/params/gen-params.go +++ b/pkg/templates/volumeclaimtemplates/internal/params/gen-params.go @@ -17,10 +17,10 @@ var ( _ = util.MustParseParameterDesc _ = fmt.Sprintf - volumeClaimTemplateParamDesc = util.MustParseParameterDesc(`{ - "Name": "Annotation", + annotationParamDesc = util.MustParseParameterDesc(`{ + "Name": "annotation", "Type": "string", - "Description": " ", + "Description": "Annotation specifies the required annotation to match.", "Examples": null, "Enum": null, "SubParameters": null, @@ -34,7 +34,7 @@ var ( `) ParamDescs = []check.ParameterDesc{ - volumeClaimTemplateParamDesc, + annotationParamDesc, } ) diff --git a/pkg/templates/volumeclaimtemplates/template.go b/pkg/templates/volumeclaimtemplates/template.go index c10c34d69..c17a9ca5d 100644 --- a/pkg/templates/volumeclaimtemplates/template.go +++ b/pkg/templates/volumeclaimtemplates/template.go @@ -6,52 +6,47 @@ import ( "golang.stackrox.io/kube-linter/pkg/check" "golang.stackrox.io/kube-linter/pkg/config" "golang.stackrox.io/kube-linter/pkg/diagnostic" + "golang.stackrox.io/kube-linter/pkg/extract" "golang.stackrox.io/kube-linter/pkg/lintcontext" "golang.stackrox.io/kube-linter/pkg/objectkinds" "golang.stackrox.io/kube-linter/pkg/templates" "golang.stackrox.io/kube-linter/pkg/templates/volumeclaimtemplates/internal/params" - v1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" ) const ( - templateKey = "statefulset-volumeclaimtemplate-annotation" + templateKey = "statefulset-volumeclaimtemplate-annotation" ) func init() { - templates.Register(check.Template{ - HumanName: "StatefulSet VolumeClaimTemplate Annotation", - Key: templateKey, - Description: "Check if StatefulSet's VolumeClaimTemplate contains a specific annotation", - SupportedObjectKinds: config.ObjectKindsDesc{ - ObjectKinds: []string{objectkinds.DeploymentLike}, - }, - Parameters: params.ParamDescs, - ParseAndValidateParams: params.ParseAndValidate, - Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) { - return func(_ lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { - k8sObj, ok := object.K8sObject.(*unstructured.Unstructured) - if !ok { - return nil - } - if k8sObj.GetKind() != "StatefulSet" { - return nil - } - var statefulSet v1.StatefulSet - err := runtime.DefaultUnstructuredConverter.FromUnstructured(k8sObj.UnstructuredContent(), &statefulSet) - if err != nil { - return nil - } - for _, volumeClaimTemplate := range statefulSet.Spec.VolumeClaimTemplates { - if annotationValue, found := volumeClaimTemplate.Annotations[p.Annotation]; found { - return []diagnostic.Diagnostic{{ - Message: fmt.Sprintf("found annotation %q with value %q in VolumeClaimTemplate", p.Annotation, annotationValue), - }} - } - } - return nil - }, nil - } ), - }) + templates.Register(check.Template{ + HumanName: "StatefulSet VolumeClaimTemplate Annotation", + Key: templateKey, + Description: "Check if StatefulSet's VolumeClaimTemplate contains a specific annotation", + SupportedObjectKinds: config.ObjectKindsDesc{ + ObjectKinds: []string{objectkinds.DeploymentLike}, + }, + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) { + return func(_ lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { + sts, ok := extract.StatefulSetSpec(object.K8sObject) + if !ok { + fmt.Println("failed to extract StatefulSet spec") + return nil + } + + var diagnostics []diagnostic.Diagnostic + + for _, vct := range sts.VolumeClaimTemplates { + if vct.Annotations == nil || vct.Annotations[p.Annotation] == "" { + diagnostics = append(diagnostics, diagnostic.Diagnostic{ + Message: fmt.Sprintf("StatefulSet's VolumeClaimTemplate is missing required annotation: %s", p.Annotation), + }) + } + } + + return diagnostics + }, nil + }), + }) } diff --git a/pkg/templates/volumeclaimtemplates/template_test.go b/pkg/templates/volumeclaimtemplates/template_test.go index e69de29bb..347b54eef 100644 --- a/pkg/templates/volumeclaimtemplates/template_test.go +++ b/pkg/templates/volumeclaimtemplates/template_test.go @@ -0,0 +1,66 @@ +package volumeclaimtemplates + +import ( + "testing" + + "golang.stackrox.io/kube-linter/pkg/lintcontext/mocks" + "golang.stackrox.io/kube-linter/pkg/templates" + "golang.stackrox.io/kube-linter/pkg/templates/volumeclaimtemplates/internal/params" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestStatefulSetVolumeClaimTemplateAnnotation(t *testing.T) { + tests := []struct { + name string + annotation string // Adjusted to match the parameter name in Params + wantDiags int + }{ + {"WithAnnotation", "value", 0}, + {"WithoutAnnotation", "", 1}, // Empty string or any value for negative case + } + + + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sts := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{Name: "statefulset"}, + Spec: appsv1.StatefulSetSpec{ + VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ + {ObjectMeta: metav1.ObjectMeta{Annotations: tt.annotations}}, + }, + }, + } + + // Creating mock lint context + mockLintCtx := mocks.NewMockContext() + mockLintCtx.AddObject("statefulset", sts) + + // Fetching template + template, found := templates.Get("statefulset-volumeclaimtemplate-annotation") + if !found { + t.Fatalf("failed to get template") + } + + // Parsing and validating parameters + params, err := params.ParseAndValidate(map[string]interface{}{}) + if err != nil { + t.Fatalf("failed to parse and validate params: %v", err) + } + + // Instantiating check function + checkFunc, err := template.Instantiate(params) + if err != nil { + t.Fatalf("failed to instantiate check function: %v", err) + } + + // Running the check function + diags := checkFunc(mockLintCtx, mockLintCtx.Objects()[0]) + if len(diags) != tt.wantDiags { + t.Errorf("got %d diagnostics, want %d", len(diags), tt.wantDiags) + } + }) + } +}