Skip to content

Add a New Check For Annotations on PersistentVolumeClaims #922

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
20 changes: 20 additions & 0 deletions docs/generated/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
35 changes: 35 additions & 0 deletions pkg/extract/sts_spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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) {
if obj == nil {
return appsV1.StatefulSetSpec{}, false
}

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
}

Check warning on line 28 in pkg/extract/sts_spec.go

View check run for this annotation

Codecov / codecov/patch

pkg/extract/sts_spec.go#L27-L28

Added lines #L27 - L28 were not covered by tests
statefulSetSpec, ok := spec.Interface().(appsV1.StatefulSetSpec)
if ok {
return statefulSetSpec, true
}
return appsV1.StatefulSetSpec{}, false

Check warning on line 33 in pkg/extract/sts_spec.go

View check run for this annotation

Codecov / codecov/patch

pkg/extract/sts_spec.go#L33

Added line #L33 was not covered by tests
}
}
93 changes: 93 additions & 0 deletions pkg/extract/sts_spec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package extract

import (
"testing"
"time"

appsV1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"

"github.com/stretchr/testify/assert"
)

type fakeStatefulSet struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec appsV1.StatefulSetSpec
}

func (f *fakeStatefulSet) GetObjectKind() schema.ObjectKind {
return &f.TypeMeta
}

func (f *fakeStatefulSet) DeepCopyObject() runtime.Object {
return &fakeStatefulSet{
TypeMeta: f.TypeMeta,
ObjectMeta: metav1.ObjectMeta{
Name: f.Name,
Namespace: f.Namespace,
},
Spec: f.Spec,
}
}

func (f *fakeStatefulSet) GetAnnotations() map[string]string {
return map[string]string{"key": "value"} // Example annotation
}

func (f *fakeStatefulSet) GetCreationTimestamp() metav1.Time {
return metav1.Time{Time: time.Now()}
}

func TestStatefulSetSpec(t *testing.T) {
t.Run("nil object", func(t *testing.T) {
spec, ok := StatefulSetSpec(nil)
assert.False(t, ok)
assert.Equal(t, appsV1.StatefulSetSpec{}, spec)
})

t.Run("typed StatefulSet", func(t *testing.T) {
sampleSpec := appsV1.StatefulSetSpec{
ServiceName: "my-service",
}
obj := &appsV1.StatefulSet{
Spec: sampleSpec,
}
spec, ok := StatefulSetSpec(obj)
assert.True(t, ok)
assert.Equal(t, sampleSpec, spec)
})

t.Run("fallback via reflection", func(t *testing.T) {
sampleSpec := appsV1.StatefulSetSpec{
ServiceName: "reflected-service",
}
obj := &fakeStatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "StatefulSet",
},
ObjectMeta: metav1.ObjectMeta{
Name: "fake-statefulset",
Namespace: "default",
},
Spec: sampleSpec,
}
spec, ok := StatefulSetSpec(obj)
assert.True(t, ok)
assert.Equal(t, sampleSpec, spec)
})

t.Run("wrong kind", func(t *testing.T) {
obj := &fakeStatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
},
Spec: appsV1.StatefulSetSpec{},
}
spec, ok := StatefulSetSpec(obj)
assert.False(t, ok)
assert.Equal(t, appsV1.StatefulSetSpec{}, spec)
})
}
5 changes: 5 additions & 0 deletions pkg/lintcontext/mocks/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@
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

Check warning on line 34 in pkg/lintcontext/mocks/context.go

View check run for this annotation

Codecov / codecov/patch

pkg/lintcontext/mocks/context.go#L33-L34

Added lines #L33 - L34 were not covered by tests
}
24 changes: 24 additions & 0 deletions pkg/objectkinds/pvc.go
Original file line number Diff line number Diff line change
@@ -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
}))

Check warning on line 19 in pkg/objectkinds/pvc.go

View check run for this annotation

Codecov / codecov/patch

pkg/objectkinds/pvc.go#L18-L19

Added lines #L18 - L19 were not covered by tests
}

func GetPersistentVolumeClaimAPIVersion() string {
return persistentvolumeclaimGVK.GroupVersion().String()
}
13 changes: 13 additions & 0 deletions pkg/objectkinds/pvc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package objectkinds_test

import (
"testing"

"github.com/stretchr/testify/assert"
"golang.stackrox.io/kube-linter/pkg/objectkinds"
)

func TestGetPersistentVolumeClaimAPIVersion(t *testing.T) {
apiVersion := objectkinds.GetPersistentVolumeClaimAPIVersion()
assert.NotEmpty(t, apiVersion)
}
1 change: 1 addition & 0 deletions pkg/templates/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package params

import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
"golang.stackrox.io/kube-linter/pkg/check"
"golang.stackrox.io/kube-linter/pkg/diagnostic"
"golang.stackrox.io/kube-linter/pkg/lintcontext"
)

func TestValidate(t *testing.T) {
tests := []struct {
name string
params Params
expectedError error
}{
{
name: "valid annotation",
params: Params{
Annotation: "some-annotation",
},
expectedError: nil,
},
{
name: "missing annotation",
params: Params{
Annotation: "",
},
expectedError: errors.New("invalid parameters: required param annotation not found"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.params.Validate()
if tt.expectedError == nil {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.expectedError.Error())
}
})
}
}

func TestParseAndValidate(t *testing.T) {
t.Run("valid map input", func(t *testing.T) {
m := map[string]interface{}{
"annotation": "required-annotation",
}
result, err := ParseAndValidate(m)
assert.NoError(t, err)

params, ok := result.(Params)
assert.True(t, ok)
assert.Equal(t, "required-annotation", params.Annotation)
})

t.Run("missing annotation in map", func(t *testing.T) {
m := map[string]interface{}{}
_, err := ParseAndValidate(m)
assert.Error(t, err)
assert.Contains(t, err.Error(), "required param annotation not found")
})
}

func TestWrapInstantiateFunc(t *testing.T) {
mockFunc := func(p Params) (check.Func, error) {
return func(ctx lintcontext.LintContext, obj lintcontext.Object) []diagnostic.Diagnostic {
return []diagnostic.Diagnostic{
{
Message: "mocked diagnostic",
},
}
}, nil
}

wrapped := WrapInstantiateFunc(mockFunc)

t.Run("wrapped function works", func(t *testing.T) {
params := Params{Annotation: "test-annotation"}
fn, err := wrapped(params)
assert.NoError(t, err)

var dummyCtx lintcontext.LintContext
var dummyObj lintcontext.Object

diagnostics := fn(dummyCtx, dummyObj)
assert.Len(t, diagnostics, 1)
assert.Equal(t, "mocked diagnostic", diagnostics[0].Message)
})
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading