Skip to content
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