Skip to content

Commit

Permalink
Add unit tests framework (#111)
Browse files Browse the repository at this point in the history
* Changed LintContext from struct to interface

* Add mock context and some kube objects

* Added a test case to container capabilities check

* Found a bug and continue to strengthen test...

* Added more tests

* gofmt fixes

* Addressed comments

* Unexport New function
  • Loading branch information
kreamkorokke committed Dec 18, 2020
1 parent 86d6dfa commit 0177d11
Show file tree
Hide file tree
Showing 22 changed files with 412 additions and 41 deletions.
2 changes: 1 addition & 1 deletion internal/check/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
// A Func is a specific lint-check, which runs on a specific objects, and emits diagnostics if problems are found.
// Checks have access to the entire LintContext, with all the objects in it, but must only report problems for the
// object passed in the second argument.
type Func func(lintCtx *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic
type Func func(lintCtx lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic

// ObjectKindsDesc describes a list of supported object kinds for a check template.
type ObjectKindsDesc struct {
Expand Down
27 changes: 21 additions & 6 deletions internal/lintcontext/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,37 @@ type InvalidObject struct {
}

// A LintContext represents the context for a lint run.
type LintContext struct {
type LintContext interface {
Objects() []Object
InvalidObjects() []InvalidObject
}

type lintContextImpl struct {
objects []Object
invalidObjects []InvalidObject
}

// Objects returns the (valid) objects loaded from this LintContext.
func (l *LintContext) Objects() []Object {
func (l *lintContextImpl) Objects() []Object {
return l.objects
}

// addObject adds a valid object to this LintContext
func (l *lintContextImpl) addObjects(objs ...Object) {
l.objects = append(l.objects, objs...)
}

// InvalidObjects returns any objects that we attempted to load, but which were invalid.
func (l *LintContext) InvalidObjects() []InvalidObject {
func (l *lintContextImpl) InvalidObjects() []InvalidObject {
return l.invalidObjects
}

// New returns a ready-to-use, empty, lint context.
func New() *LintContext {
return &LintContext{}
// addInvalidObject adds an invalid object to this LintContext
func (l *lintContextImpl) addInvalidObjects(objs ...InvalidObject) {
l.invalidObjects = append(l.invalidObjects, objs...)
}

// new returns a ready-to-use, empty, lintContextImpl.
func new() *lintContextImpl {
return &lintContextImpl{}
}
12 changes: 6 additions & 6 deletions internal/lintcontext/create_contexts.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ var (
// Currently, each directory of Kube YAML files (or Helm charts) are treated as a separate context.
// TODO: Figure out if it's useful to allow people to specify that files spanning different directories
// should be treated as being in the same context.
func CreateContexts(filesOrDirs ...string) ([]*LintContext, error) {
contextsByDir := make(map[string]*LintContext)
func CreateContexts(filesOrDirs ...string) ([]LintContext, error) {
contextsByDir := make(map[string]*lintContextImpl)
for _, fileOrDir := range filesOrDirs {
// Stdin
if fileOrDir == "-" {
if _, alreadyExists := contextsByDir["-"]; alreadyExists {
continue
}
ctx := New()
ctx := new()
if err := ctx.loadObjectsFromReader("<standard input>", os.Stdin); err != nil {
return nil, err
}
Expand All @@ -46,7 +46,7 @@ func CreateContexts(filesOrDirs ...string) ([]*LintContext, error) {
if knownYAMLExtensions.Contains(strings.ToLower(filepath.Ext(currentPath))) || fileOrDir == currentPath {
ctx := contextsByDir[dirName]
if ctx == nil {
ctx = New()
ctx = new()
contextsByDir[dirName] = ctx
}
if err := ctx.loadObjectsFromYAMLFile(currentPath, info); err != nil {
Expand All @@ -60,7 +60,7 @@ func CreateContexts(filesOrDirs ...string) ([]*LintContext, error) {
if _, alreadyExists := contextsByDir[currentPath]; alreadyExists {
return nil
}
ctx := New()
ctx := new()
contextsByDir[currentPath] = ctx
if err := ctx.loadObjectsFromHelmChart(currentPath); err != nil {
return err
Expand All @@ -78,7 +78,7 @@ func CreateContexts(filesOrDirs ...string) ([]*LintContext, error) {
dirs = append(dirs, dir)
}
sort.Strings(dirs)
var contexts []*LintContext
var contexts []LintContext
for _, dir := range dirs {
contexts = append(contexts, contextsByDir[dir])
}
Expand Down
29 changes: 29 additions & 0 deletions internal/lintcontext/mocks/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package mocks

import (
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
)

// AddContainerToPod adds a mock container to the specified pod under context
func (l *MockLintContext) AddContainerToPod(
podName, containerName, image string,
ports []v1.ContainerPort,
env []v1.EnvVar,
sc *v1.SecurityContext,
) error {
pod, ok := l.pods[podName]
if !ok {
return errors.Errorf("pod with name %q is not found", podName)
}
// TODO: keep supporting other fields
pod.Spec.Containers = append(pod.Spec.Containers, v1.Container{
Name: containerName,
Image: image,
Ports: ports,
Env: env,
Resources: v1.ResourceRequirements{},
SecurityContext: sc,
})
return nil
}
30 changes: 30 additions & 0 deletions internal/lintcontext/mocks/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package mocks

import (
"golang.stackrox.io/kube-linter/internal/lintcontext"
v1 "k8s.io/api/core/v1"
)

// MockLintContext is mock implementation of the LintContext used in unit tests
type MockLintContext struct {
pods map[string]*v1.Pod
}

// Objects returns all the objects under this MockLintContext
func (l *MockLintContext) Objects() []lintcontext.Object {
result := make([]lintcontext.Object, 0, len(l.pods))
for _, p := range l.pods {
result = append(result, lintcontext.Object{Metadata: lintcontext.ObjectMetadata{}, K8sObject: p})
}
return result
}

// InvalidObjects is not implemented. For now we don't care about invalid objects for mock context.
func (l *MockLintContext) InvalidObjects() []lintcontext.InvalidObject {
return nil
}

// NewMockContext returns an empty mockLintContext
func NewMockContext() *MockLintContext {
return &MockLintContext{pods: make(map[string]*v1.Pod)}
}
53 changes: 53 additions & 0 deletions internal/lintcontext/mocks/pod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package mocks

import (
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// AddMockPod adds a mock Pod to LintContext
func (l *MockLintContext) AddMockPod(
podName, namespace, clusterName string,
labels, annotations map[string]string,
) {
l.pods[podName] =
&v1.Pod{
TypeMeta: metaV1.TypeMeta{},
ObjectMeta: metaV1.ObjectMeta{
Name: podName,
Namespace: namespace,
Labels: labels,
Annotations: annotations,
ClusterName: clusterName,
},
Spec: v1.PodSpec{},
Status: v1.PodStatus{},
}
}

// AddSecurityContextToPod adds a security context to the pod specified by name
func (l *MockLintContext) AddSecurityContextToPod(
podName string,
runAsUser *int64,
runAsNonRoot *bool,
) error {
pod, ok := l.pods[podName]
if !ok {
return errors.Errorf("pod with name %q is not found", podName)
}
// TODO: keep supporting other fields
pod.Spec.SecurityContext = &v1.PodSecurityContext{
SELinuxOptions: nil,
WindowsOptions: nil,
RunAsUser: runAsUser,
RunAsGroup: nil,
RunAsNonRoot: runAsNonRoot,
SupplementalGroups: nil,
FSGroup: nil,
Sysctls: nil,
FSGroupChangePolicy: nil,
SeccompProfile: nil,
}
return nil
}
16 changes: 8 additions & 8 deletions internal/lintcontext/parse_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (w nopWriter) Write(p []byte) (n int, err error) {
return len(p), nil
}

func (l *LintContext) renderHelmChart(dir string) (map[string]string, error) {
func (l *lintContextImpl) renderHelmChart(dir string) (map[string]string, error) {
// Helm doesn't have great logging behaviour, and can spam stderr, so silence their logging.
// TODO: capture these logs.
log.SetOutput(nopWriter{})
Expand Down Expand Up @@ -95,11 +95,11 @@ func (l *LintContext) renderHelmChart(dir string) (map[string]string, error) {
return rendered, nil
}

func (l *LintContext) loadObjectsFromHelmChart(dir string) error {
func (l *lintContextImpl) loadObjectsFromHelmChart(dir string) error {
metadata := ObjectMetadata{FilePath: dir}
renderedFiles, err := l.renderHelmChart(dir)
if err != nil {
l.invalidObjects = append(l.invalidObjects, InvalidObject{Metadata: metadata, LoadErr: err})
l.addInvalidObjects(InvalidObject{Metadata: metadata, LoadErr: err})
return nil
}
for path, contents := range renderedFiles {
Expand All @@ -113,7 +113,7 @@ func (l *LintContext) loadObjectsFromHelmChart(dir string) error {
return nil
}

func (l *LintContext) loadObjectFromYAMLReader(filePath string, r *yaml.YAMLReader) error {
func (l *lintContextImpl) loadObjectFromYAMLReader(filePath string, r *yaml.YAMLReader) error {
doc, err := r.Read()
if err != nil {
return err
Expand All @@ -130,22 +130,22 @@ func (l *LintContext) loadObjectFromYAMLReader(filePath string, r *yaml.YAMLRead

objs, err := parseObjects(doc)
if err != nil {
l.invalidObjects = append(l.invalidObjects, InvalidObject{
l.addInvalidObjects(InvalidObject{
Metadata: metadata,
LoadErr: err,
})
return nil
}
for _, obj := range objs {
l.objects = append(l.objects, Object{
l.addObjects(Object{
Metadata: metadata,
K8sObject: obj,
})
}
return nil
}

func (l *LintContext) loadObjectsFromYAMLFile(filePath string, info os.FileInfo) error {
func (l *lintContextImpl) loadObjectsFromYAMLFile(filePath string, info os.FileInfo) error {
if info.Size() > maxFileSizeBytes {
return nil
}
Expand All @@ -160,7 +160,7 @@ func (l *LintContext) loadObjectsFromYAMLFile(filePath string, info os.FileInfo)
return l.loadObjectsFromReader(filePath, file)
}

func (l *LintContext) loadObjectsFromReader(filePath string, reader io.Reader) error {
func (l *lintContextImpl) loadObjectsFromReader(filePath string, reader io.Reader) error {
yamlReader := yaml.NewYAMLReader(bufio.NewReader(reader))
for {
if err := l.loadObjectFromYAMLReader(filePath, yamlReader); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Result struct {
}

// Run runs the linter on the given context, with the given config.
func Run(lintCtxs []*lintcontext.LintContext, registry checkregistry.CheckRegistry, checks []string) (Result, error) {
func Run(lintCtxs []lintcontext.LintContext, registry checkregistry.CheckRegistry, checks []string) (Result, error) {
instantiatedChecks := make([]*instantiatedcheck.InstantiatedCheck, 0, len(checks))
for _, checkName := range checks {
instantiatedCheck := registry.Load(checkName)
Expand Down
2 changes: 1 addition & 1 deletion internal/templates/antiaffinity/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func init() {
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 {
return func(_ lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic {
replicas, found := extract.Replicas(object.K8sObject)
if !found {
return nil
Expand Down
22 changes: 14 additions & 8 deletions internal/templates/containercapabilities/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import (
)

const (
templateKey = "verify-container-capabilities"
reservedCapabilitiesAll = "all"
matchLiteralReservedCapabilitiesAll = "(?i)" + reservedCapabilitiesAll
matchLiteralReservedCapabilitiesAll = "^(?i)" + reservedCapabilitiesAll + "$"
)

var (
Expand All @@ -26,6 +27,12 @@ var (
utils.Must(err)
return m
}()

addListDiagMsgFmt = "container %q has ADD capability: %q, which matched with the forbidden capability for containers"
addListWithAllDiagMsgFmt = "container %q has ADD capability: %q, but no capabilities " +
"should be added at all and this capability is not included in the exceptions list"
dropListDiagMsgFmt = "container %q has DROP capabilities: %q, but does not drop capability %q which is required"
dropListWithAllDiagMsgFmt = "container %q has DROP capabilities: %q, but in fact all capabilities are required to be dropped"
)

func checkCapabilityDropList(
Expand All @@ -49,7 +56,7 @@ func checkCapabilityDropList(
*result,
diagnostic.Diagnostic{
Message: fmt.Sprintf(
"container %q has DROP capabilities: %q, but in fact all capabilities are required to be dropeed",
dropListWithAllDiagMsgFmt,
containerName,
scCaps.Drop),
})
Expand All @@ -71,8 +78,8 @@ func checkCapabilityDropList(
append(
*result,
diagnostic.Diagnostic{
Message: fmt.Sprintf("container %q has DROP capabilities: %q, but does not drop "+
"capability %q which is required",
Message: fmt.Sprintf(
dropListDiagMsgFmt,
containerName,
scCaps.Drop,
paramCap),
Expand Down Expand Up @@ -106,8 +113,7 @@ func checkCapabilityAddList(
*result,
diagnostic.Diagnostic{
Message: fmt.Sprintf(
"container %q has ADD capability: %q, but no capabilities should be added at all and"+
" this capabilty is not included in the exceptions list",
addListWithAllDiagMsgFmt,
containerName,
scCap),
})
Expand All @@ -128,7 +134,7 @@ func checkCapabilityAddList(
*result,
diagnostic.Diagnostic{
Message: fmt.Sprintf(
"container %q has ADD capability: %q, which matched with the forbidden capability for containers",
addListDiagMsgFmt,
containerName,
scCap),
})
Expand Down Expand Up @@ -181,7 +187,7 @@ func validateExceptionsList(forbidAll bool, exceptions []string) error {
func init() {
templates.Register(check.Template{
HumanName: "Verify container capabilities",
Key: "verify-container-capabilities",
Key: templateKey,
Description: "Flag containers that do not match capabilities requirements",
SupportedObjectKinds: check.ObjectKindsDesc{
ObjectKinds: []string{objectkinds.DeploymentLike},
Expand Down

0 comments on commit 0177d11

Please sign in to comment.