diff --git a/checks/basic/latest_go_test.go b/checks/basic/latest_go_test.go new file mode 100644 index 0000000..97027d0 --- /dev/null +++ b/checks/basic/latest_go_test.go @@ -0,0 +1,192 @@ +package basic + +import ( + "fmt" + "testing" + + "github.com/digitalocean/clusterlint/checks" + "github.com/digitalocean/clusterlint/kube" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestMeta(t *testing.T) { + latestTagCheck := latestTagCheck{} + assert.Equal(t, "latest-tag", latestTagCheck.Name()) + assert.Equal(t, "Checks if there are pods with container images having latest tag", latestTagCheck.Description()) + assert.Equal(t, []string{"basic"}, latestTagCheck.Groups()) +} + +func TestRegistration(t *testing.T) { + latestTagCheck := &latestTagCheck{} + check, err := checks.Get("latest-tag") + assert.Equal(t, check, latestTagCheck) + assert.Nil(t, err) +} + +func TestLatestTagWarning(t *testing.T) { + scenarios := []struct { + name string + arg *kube.Objects + expected []error + }{ + { + name: "no pods", + arg: initPod(), + expected: nil, + }, + { + name: "pod with container image - k8s.gcr.io/busybox:latest", + arg: container("k8s.gcr.io/busybox:latest"), + expected: warnings(), + }, + { + name: "pod with container image - busybox:latest", + arg: container("busybox:latest"), + expected: warnings(), + }, + { + name: "pod with container image - k8s.gcr.io/busybox", + arg: container("k8s.gcr.io/busybox"), + expected: warnings(), + }, + { + name: "pod with container image - busybox", + arg: container("busybox"), + expected: warnings(), + }, + { + name: "pod with container image - http://private:5000/repo/busybox", + arg: container("http://private:5000/repo/busybox"), + expected: warnings(), + }, + { + name: "pod with container image - http://private:5000/repo/busybox:latest", + arg: container("http://private:5000/repo/busybox:latest"), + expected: warnings(), + }, + { + name: "pod with container image - test:5000/repo@sha256:digest", + arg: container("test:5000/repo@sha256:digest"), + expected: nil, + }, + { + name: "pod with container image - repo@sha256:digest", + arg: container("repo@sha256:digest"), + expected: nil, + }, + { + name: "pod with container image - test:5000/repo:ignore-tag@sha256:digest", + arg: container("test:5000/repo:ignore-tag@sha256:digest"), + expected: nil, + }, + { + name: "pod with container image - busybox:v1.2.3", + arg: container("busybox:v1.2.3"), + expected: nil, + }, + + { + name: "pod with init container image - k8s.gcr.io/busybox:latest", + arg: initContainer("k8s.gcr.io/busybox:latest"), + expected: warnings(), + }, + { + name: "pod with init container image - busybox:latest", + arg: initContainer("busybox:latest"), + expected: warnings(), + }, + { + name: "pod with init container image - k8s.gcr.io/busybox", + arg: initContainer("k8s.gcr.io/busybox"), + expected: warnings(), + }, + { + name: "pod with init container image - busybox", + arg: initContainer("busybox"), + expected: warnings(), + }, + { + name: "pod with container image - http://private:5000/repo/busybox", + arg: container("http://private:5000/repo/busybox"), + expected: warnings(), + }, + { + name: "pod with container image - http://private:5000/repo/busybox:latest", + arg: container("http://private:5000/repo/busybox:latest"), + expected: warnings(), + }, + { + name: "pod with container image - test:5000/repo@sha256:digest", + arg: initContainer("test:5000/repo@sha256:digest"), + expected: nil, + }, + { + name: "pod with container image - test:5000/repo:ignore-tag@sha256:digest", + arg: initContainer("test:5000/repo:ignore-tag@sha256:digest"), + expected: nil, + }, + { + name: "pod with container image - repo@sha256:digest", + arg: initContainer("repo@sha256:digest"), + expected: nil, + }, + { + name: "pod with init container image - busybox:v1.2.3", + arg: initContainer("busybox:v1.2.3"), + expected: nil, + }, + } + + latestTagCheck := latestTagCheck{} + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + w, e, err := latestTagCheck.Run(scenario.arg) + assert.ElementsMatch(t, scenario.expected, w) + assert.Empty(t, e) + assert.Nil(t, err) + }) + } +} + +func initPod() *kube.Objects { + objs := &kube.Objects{ + Pods: &corev1.PodList{}, + } + return objs +} + +func container(image string) *kube.Objects { + objs := initPod() + objs.Pods = &corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "pod_foo", Namespace: "k8s"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "bar", Image: image}}}, + }, + }, + } + return objs +} + +func initContainer(image string) *kube.Objects { + objs := initPod() + objs.Pods = &corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "pod_foo", Namespace: "k8s"}, + Spec: corev1.PodSpec{InitContainers: []corev1.Container{{Name: "bar", Image: image}}}, + }, + }, + } + return objs +} + +func warnings() []error { + w := []error{ + fmt.Errorf("[Best Practice] Use specific tags instead of latest for container 'bar' in pod 'pod_foo' in namespace 'k8s'"), + } + return w +} diff --git a/checks/basic/latest_tag.go b/checks/basic/latest_tag.go new file mode 100644 index 0000000..f886be9 --- /dev/null +++ b/checks/basic/latest_tag.go @@ -0,0 +1,60 @@ +package basic + +import ( + "fmt" + "strings" + + "github.com/digitalocean/clusterlint/checks" + "github.com/digitalocean/clusterlint/kube" + corev1 "k8s.io/api/core/v1" +) + +func init() { + checks.Register(&latestTagCheck{}) +} + +type latestTagCheck struct{} + +// Name returns a unique name for this check. +func (l *latestTagCheck) Name() string { + return "latest-tag" +} + +// Groups returns a list of group names this check should be part of. +func (l *latestTagCheck) Groups() []string { + return []string{"basic"} +} + +// Description returns a detailed human-readable description of what this check +// does. +func (l *latestTagCheck) Description() string { + return "Checks if there are pods with container images having latest tag" +} + +// Run runs this check on a set of Kubernetes objects. It can return warnings +// (low-priority problems) and errors (high-priority problems) as well as an +// error value indicating that the check failed to run. +func (l *latestTagCheck) Run(objects *kube.Objects) (warnings []error, errors []error, err error) { + var w []error + for _, pod := range objects.Pods.Items { + podName := pod.GetName() + namespace := pod.GetNamespace() + w = append(w, checkTags(pod.Spec.Containers, podName, namespace)...) + w = append(w, checkTags(pod.Spec.InitContainers, podName, namespace)...) + } + + return w, nil, nil +} + +// checkTags checks if the image name conforms to pattern `image:latest` or `image` +// Adds a warning if it finds any image that uses the latest tag +func checkTags(containers []corev1.Container, podName string, namespace string) []error { + var w []error + for _, container := range containers { + image := container.Image[strings.LastIndex(container.Image, "/")+1:] + if strings.Contains(image, ":latest") || !strings.Contains(image, ":") { + w = append(w, fmt.Errorf("[Best Practice] Use specific tags instead of latest for container '%s' in pod '%s' in namespace '%s'", container.Name, podName, namespace)) + } + } + return w +} diff --git a/checks/basic/namespace_test.go b/checks/basic/namespace_test.go index 7509249..330bd46 100644 --- a/checks/basic/namespace_test.go +++ b/checks/basic/namespace_test.go @@ -24,8 +24,10 @@ func TestNamespaceWarning(t *testing.T) { for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { - w, _, _ := namespace.Run(scenario.arg) + w, e, err := namespace.Run(scenario.arg) assert.ElementsMatch(t, scenario.expected, w) + assert.Empty(t, e) + assert.Nil(t, err) }) } }