Merge pull request #11 from digitalocean/varsha/latest-tag
Latest tag check: Check if containers and initContainers in a pod use image with latest tag.varsha/versions
commit
3a3fc4f657
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -24,8 +24,10 @@ func TestNamespaceWarning(t *testing.T) {
|
||||||
|
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.name, func(t *testing.T) {
|
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.ElementsMatch(t, scenario.expected, w)
|
||||||
|
assert.Empty(t, e)
|
||||||
|
assert.Nil(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue