Use owner references to indicate the objects that refer to the problematic object.

* Change output format to json
varsha/versions
Varsha Varadarajan 2019-06-21 17:36:55 -04:00
parent 0320c5633a
commit 65ba22e8d8
18 changed files with 297 additions and 152 deletions

View File

@ -34,15 +34,13 @@ func (fq *fullyQualifiedImageCheck) Description() string {
// 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 (fq *fullyQualifiedImageCheck) Run(objects *kube.Objects) ([]kube.Diagnostic, error) {
var diagnostics []kube.Diagnostic
func (fq *fullyQualifiedImageCheck) Run(objects *kube.Objects) ([]checks.Diagnostic, error) {
var diagnostics []checks.Diagnostic
for _, pod := range objects.Pods.Items {
podName := pod.GetName()
namespace := pod.GetNamespace()
d := checkImage(pod.Spec.Containers, podName, namespace)
d := checkImage(pod.Spec.Containers, pod)
diagnostics = append(diagnostics, d...)
d = checkImage(pod.Spec.InitContainers, podName, namespace)
d = checkImage(pod.Spec.InitContainers, pod)
diagnostics = append(diagnostics, d...)
}
@ -51,17 +49,29 @@ func (fq *fullyQualifiedImageCheck) Run(objects *kube.Objects) ([]kube.Diagnosti
// checkImage checks if the image name is fully qualified
// Adds a warning if the container does not use a fully qualified image name
func checkImage(containers []corev1.Container, podName string, namespace string) []kube.Diagnostic {
var d []kube.Diagnostic
func checkImage(containers []corev1.Container, pod corev1.Pod) []checks.Diagnostic {
var diagnostics []checks.Diagnostic
for _, container := range containers {
value, err := reference.ParseAnyReference(container.Image)
if err != nil {
d = append(d, kube.Diagnostic{Category: "error", Message: fmt.Sprintf("Malformed image name for container '%s' in pod '%s' in namespace '%s'", container.Name, podName, namespace)})
d := checks.Diagnostic{
Severity: checks.Error,
Message: fmt.Sprintf("Malformed image name for container '%s' in pod '%s'", container.Name, pod.GetName()),
Object: kube.Object{TypeInfo: &pod.TypeMeta, ObjectInfo: &pod.ObjectMeta},
Owners: pod.ObjectMeta.GetOwnerReferences(),
}
diagnostics = append(diagnostics, d)
} else {
if value.String() != container.Image {
d = append(d, kube.Diagnostic{Category: "warning", Message: fmt.Sprintf("Use fully qualified image for container '%s' in pod '%s' in namespace '%s'", container.Name, podName, namespace)})
d := checks.Diagnostic{
Severity: checks.Warning,
Message: fmt.Sprintf("Use fully qualified image for container '%s' in pod '%s'", container.Name, pod.GetName()),
Object: kube.Object{TypeInfo: &pod.TypeMeta, ObjectInfo: &pod.ObjectMeta},
Owners: pod.ObjectMeta.GetOwnerReferences(),
}
diagnostics = append(diagnostics, d)
}
}
}
return d
return diagnostics
}

View File

@ -23,13 +23,13 @@ func TestFullyQualifiedImageCheckRegistration(t *testing.T) {
}
func TestFullyQualifiedImageWarning(t *testing.T) {
const message string = "Use fully qualified image for container 'bar' in pod 'pod_foo' in namespace 'k8s'"
const category string = "warning"
const message string = "Use fully qualified image for container 'bar' in pod 'pod_foo'"
const severity checks.Severity = checks.Warning
scenarios := []struct {
name string
arg *kube.Objects
expected []kube.Diagnostic
expected []checks.Diagnostic
}{
{
name: "no pods",
@ -44,7 +44,7 @@ func TestFullyQualifiedImageWarning(t *testing.T) {
{
name: "pod with container image - busybox:latest",
arg: container("busybox:latest"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - k8s.gcr.io/busybox",
@ -54,7 +54,7 @@ func TestFullyQualifiedImageWarning(t *testing.T) {
{
name: "pod with container image - busybox",
arg: container("busybox"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - test:5000/repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
@ -64,7 +64,7 @@ func TestFullyQualifiedImageWarning(t *testing.T) {
{
name: "pod with container image - repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
arg: container("repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - test:5000/repo/image:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
@ -74,7 +74,7 @@ func TestFullyQualifiedImageWarning(t *testing.T) {
{
name: "pod with container image - repo/image:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
arg: container("repo/image:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - k8s.gcr.io/busybox:latest",
@ -84,7 +84,7 @@ func TestFullyQualifiedImageWarning(t *testing.T) {
{
name: "pod with container image - busybox:latest",
arg: initContainer("busybox:latest"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - k8s.gcr.io/busybox",
@ -94,7 +94,7 @@ func TestFullyQualifiedImageWarning(t *testing.T) {
{
name: "pod with container image - busybox",
arg: initContainer("busybox"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - test:5000/repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
@ -104,7 +104,7 @@ func TestFullyQualifiedImageWarning(t *testing.T) {
{
name: "pod with container image - repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
arg: initContainer("repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - test:5000/repo/image:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
@ -114,7 +114,7 @@ func TestFullyQualifiedImageWarning(t *testing.T) {
{
name: "pod with container image - repo/image:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
arg: initContainer("repo/image:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
expected: issues(category, message),
expected: issues(severity, message),
},
}
@ -130,23 +130,22 @@ func TestFullyQualifiedImageWarning(t *testing.T) {
}
func TestMalformedImageError(t *testing.T) {
const message string = "Malformed image name for container 'bar' in pod 'pod_foo' in namespace 'k8s'"
const category string = "error"
const message string = "Malformed image name for container 'bar' in pod 'pod_foo'"
const severity checks.Severity = checks.Error
scenarios := []struct {
name string
arg *kube.Objects
expected []kube.Diagnostic
expected []checks.Diagnostic
}{
{
name: "container with image : test:5000/repo/image@sha256:digest",
arg: container("test:5000/repo/image@sha256:digest"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "init container with image : test:5000/repo/image@sha256:digest",
arg: initContainer("test:5000/repo/image@sha256:digest"),
expected: issues(category, message),
expected: issues(severity, message),
},
}
fullyQualifiedImageCheck := fullyQualifiedImageCheck{}

View File

@ -1,6 +1,7 @@
package basic
import (
"github.com/digitalocean/clusterlint/checks"
"github.com/digitalocean/clusterlint/kube"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -11,6 +12,7 @@ func initPod() *kube.Objects {
Pods: &corev1.PodList{
Items: []corev1.Pod{
{
TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Name: "pod_foo", Namespace: "k8s"},
},
},
@ -19,6 +21,21 @@ func initPod() *kube.Objects {
return objs
}
func GetTypeMeta() *metav1.TypeMeta {
objs := initPod()
return &objs.Pods.Items[0].TypeMeta
}
func GetObjectMeta() *metav1.ObjectMeta {
objs := initPod()
return &objs.Pods.Items[0].ObjectMeta
}
func GetOwners() []metav1.OwnerReference {
objs := initPod()
return objs.Pods.Items[0].ObjectMeta.GetOwnerReferences()
}
func container(image string) *kube.Objects {
objs := initPod()
objs.Pods.Items[0].Spec = corev1.PodSpec{
@ -43,9 +60,14 @@ func initContainer(image string) *kube.Objects {
return objs
}
func issues(category string, message string) []kube.Diagnostic {
d := []kube.Diagnostic{
{Category: category, Message: message},
func issues(severity checks.Severity, message string) []checks.Diagnostic {
d := []checks.Diagnostic{
{
Severity: severity,
Message: message,
Object: kube.Object{TypeInfo: GetTypeMeta(), ObjectInfo: GetObjectMeta()},
Owners: GetOwners(),
},
}
return d
}

View File

@ -35,13 +35,11 @@ func (l *latestTagCheck) Description() string {
// 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) ([]kube.Diagnostic, error) {
var diagnostics []kube.Diagnostic
func (l *latestTagCheck) Run(objects *kube.Objects) ([]checks.Diagnostic, error) {
var diagnostics []checks.Diagnostic
for _, pod := range objects.Pods.Items {
podName := pod.GetName()
namespace := pod.GetNamespace()
diagnostics = append(diagnostics, checkTags(pod.Spec.Containers, podName, namespace)...)
diagnostics = append(diagnostics, checkTags(pod.Spec.InitContainers, podName, namespace)...)
diagnostics = append(diagnostics, checkTags(pod.Spec.Containers, pod)...)
diagnostics = append(diagnostics, checkTags(pod.Spec.InitContainers, pod)...)
}
return diagnostics, nil
@ -49,14 +47,20 @@ func (l *latestTagCheck) Run(objects *kube.Objects) ([]kube.Diagnostic, error) {
// 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) []kube.Diagnostic {
var d []kube.Diagnostic
func checkTags(containers []corev1.Container, pod corev1.Pod) []checks.Diagnostic {
var diagnostics []checks.Diagnostic
for _, container := range containers {
namedRef, _ := reference.ParseNormalizedNamed(container.Image)
tagNameOnly := reference.TagNameOnly(namedRef)
if strings.HasSuffix(tagNameOnly.String(), ":latest") {
d = append(d, kube.Diagnostic{Category: "warning", Message: fmt.Sprintf("[Best Practice] Use specific tags instead of latest for container '%s' in pod '%s' in namespace '%s'", container.Name, podName, namespace)})
d := checks.Diagnostic{
Severity: checks.Warning,
Message: fmt.Sprintf("Avoid using latest tag for container '%s' in pod '%s'", container.Name, pod.GetName()),
Object: kube.Object{TypeInfo: &pod.TypeMeta, ObjectInfo: &pod.ObjectMeta},
Owners: pod.ObjectMeta.GetOwnerReferences(),
}
diagnostics = append(diagnostics, d)
}
}
return d
return diagnostics
}

View File

@ -23,12 +23,13 @@ func TestLatestTagCheckRegistration(t *testing.T) {
}
func TestLatestTagWarning(t *testing.T) {
const message string = "[Best Practice] Use specific tags instead of latest for container 'bar' in pod 'pod_foo' in namespace 'k8s'"
const category string = "warning"
const message string = "Avoid using latest tag for container 'bar' in pod 'pod_foo'"
const severity checks.Severity = checks.Warning
scenarios := []struct {
name string
arg *kube.Objects
expected []kube.Diagnostic
expected []checks.Diagnostic
}{
{
name: "no pods",
@ -38,46 +39,46 @@ func TestLatestTagWarning(t *testing.T) {
{
name: "pod with container image - k8s.gcr.io/busybox:latest",
arg: container("k8s.gcr.io/busybox:latest"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - busybox:latest",
arg: container("busybox:latest"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - k8s.gcr.io/busybox",
arg: container("k8s.gcr.io/busybox"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - busybox",
arg: container("busybox"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - private:5000/repo/busybox",
name: "pod with container image - private:5000/busybox",
arg: container("private:5000/repo/busybox"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - private:5000/repo/busybox:latest",
name: "pod with container image - private:5000/busybox:latest",
arg: container("private:5000/repo/busybox:latest"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - test:5000/repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
name: "pod with container image - test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
arg: container("test:5000/repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
expected: nil,
},
{
name: "pod with container image - repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
name: "pod with container image - repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
arg: container("repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
expected: nil,
},
{
name: "pod with container image - test:5000/repo/image:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
arg: container("test:5000/repo/image:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
name: "pod with container image - test:5000/repo:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
arg: container("test:5000/repo:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
expected: nil,
},
{
@ -89,45 +90,45 @@ func TestLatestTagWarning(t *testing.T) {
{
name: "pod with init container image - k8s.gcr.io/busybox:latest",
arg: initContainer("k8s.gcr.io/busybox:latest"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with init container image - busybox:latest",
arg: initContainer("busybox:latest"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with init container image - k8s.gcr.io/busybox",
arg: initContainer("k8s.gcr.io/busybox"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with init container image - busybox",
arg: initContainer("busybox"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - private:5000/repo/busybox",
name: "pod with container image - private:5000/busybox",
arg: container("private:5000/repo/busybox"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - private:5000/repo/busybox:latest",
name: "pod with container image - private:5000/busybox:latest",
arg: container("private:5000/repo/busybox:latest"),
expected: issues(category, message),
expected: issues(severity, message),
},
{
name: "pod with container image - test:5000/repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
name: "pod with container image - test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
arg: initContainer("test:5000/repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
expected: nil,
},
{
name: "pod with container image - test:5000/repo/image:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
name: "pod with container image - test:5000/repo:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
arg: initContainer("test:5000/repo/image:ignore-tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
expected: nil,
},
{
name: "pod with container image - repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
name: "pod with container image - repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
arg: initContainer("repo/image@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
expected: nil,
},

View File

@ -19,23 +19,28 @@ func init() {
type defaultNamespaceCheck struct{}
type alert struct {
diagnostics []kube.Diagnostic
diagnostics []checks.Diagnostic
mu sync.Mutex
}
// GetWarnings returns alert.warnings
func (alert *alert) GetDiagnostics() []kube.Diagnostic {
func (alert *alert) GetDiagnostics() []checks.Diagnostic {
return alert.diagnostics
}
// SetWarnings sets alert.warnings
func (alert *alert) SetDiagnostics(d []kube.Diagnostic) {
func (alert *alert) SetDiagnostics(d []checks.Diagnostic) {
alert.diagnostics = d
}
// warn adds warnings for k8s objects that should not be in the default namespace
func (alert *alert) warn(k8stype string, item metav1.ObjectMeta) {
d := kube.Diagnostic{Category: "warning", Message: fmt.Sprintf("%s '%s' is in the default namespace.", k8stype, item.GetName())}
func (alert *alert) warn(k8stype string, itemMeta metav1.ObjectMeta, itemType metav1.TypeMeta) {
d := checks.Diagnostic{
Severity: checks.Warning,
Message: fmt.Sprintf("Avoid using the default namespace for %s '%s'", k8stype, itemMeta.GetName()),
Object: kube.Object{TypeInfo: &itemType, ObjectInfo: &itemMeta},
Owners: itemMeta.GetOwnerReferences(),
}
alert.mu.Lock()
alert.diagnostics = append(alert.diagnostics, d)
alert.mu.Unlock()
@ -61,7 +66,7 @@ func (nc *defaultNamespaceCheck) Description() string {
func checkPods(items *corev1.PodList, alert *alert) {
for _, item := range items.Items {
if corev1.NamespaceDefault == item.GetNamespace() {
alert.warn("Pod", item.ObjectMeta)
alert.warn("pod", item.ObjectMeta, item.TypeMeta)
}
}
}
@ -70,7 +75,7 @@ func checkPods(items *corev1.PodList, alert *alert) {
func checkPodTemplates(items *corev1.PodTemplateList, alert *alert) {
for _, item := range items.Items {
if corev1.NamespaceDefault == item.GetNamespace() {
alert.warn("Pod template", item.ObjectMeta)
alert.warn("pod template", item.ObjectMeta, item.TypeMeta)
}
}
}
@ -79,7 +84,7 @@ func checkPodTemplates(items *corev1.PodTemplateList, alert *alert) {
func checkPVCs(items *corev1.PersistentVolumeClaimList, alert *alert) {
for _, item := range items.Items {
if corev1.NamespaceDefault == item.GetNamespace() {
alert.warn("Persistent Volume Claim", item.ObjectMeta)
alert.warn("persistent volume claim", item.ObjectMeta, item.TypeMeta)
}
}
}
@ -88,7 +93,7 @@ func checkPVCs(items *corev1.PersistentVolumeClaimList, alert *alert) {
func checkConfigMaps(items *corev1.ConfigMapList, alert *alert) {
for _, item := range items.Items {
if corev1.NamespaceDefault == item.GetNamespace() {
alert.warn("Config Map", item.ObjectMeta)
alert.warn("config map", item.ObjectMeta, item.TypeMeta)
}
}
}
@ -97,7 +102,7 @@ func checkConfigMaps(items *corev1.ConfigMapList, alert *alert) {
func checkServices(items *corev1.ServiceList, alert *alert) {
for _, item := range items.Items {
if corev1.NamespaceDefault == item.GetNamespace() && item.GetName() != "kubernetes" {
alert.warn("Service", item.ObjectMeta)
alert.warn("service", item.ObjectMeta, item.TypeMeta)
}
}
}
@ -106,7 +111,7 @@ func checkServices(items *corev1.ServiceList, alert *alert) {
func checkSecrets(items *corev1.SecretList, alert *alert) {
for _, item := range items.Items {
if corev1.NamespaceDefault == item.GetNamespace() && !strings.Contains(item.GetName(), "default-token-") {
alert.warn("Secret", item.ObjectMeta)
alert.warn("secret", item.ObjectMeta, item.TypeMeta)
}
}
}
@ -115,7 +120,7 @@ func checkSecrets(items *corev1.SecretList, alert *alert) {
func checkSA(items *corev1.ServiceAccountList, alert *alert) {
for _, item := range items.Items {
if corev1.NamespaceDefault == item.GetNamespace() && item.GetName() != "default" {
alert.warn("Service Account", item.ObjectMeta)
alert.warn("service account", item.ObjectMeta, item.TypeMeta)
}
}
}
@ -123,7 +128,7 @@ func checkSA(items *corev1.ServiceAccountList, alert *alert) {
// 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 (nc *defaultNamespaceCheck) Run(objects *kube.Objects) ([]kube.Diagnostic, error) {
func (nc *defaultNamespaceCheck) Run(objects *kube.Objects) ([]checks.Diagnostic, error) {
alert := &alert{}
var g errgroup.Group
g.Go(func() error {

View File

@ -1,7 +1,6 @@
package basic
import (
"fmt"
"testing"
"github.com/digitalocean/clusterlint/checks"
@ -29,7 +28,7 @@ func TestNamespaceWarning(t *testing.T) {
scenarios := []struct {
name string
arg *kube.Objects
expected []kube.Diagnostic
expected []checks.Diagnostic
}{
{"no objects in cluster", empty(), nil},
{"user created objects in default namespace", userCreatedObjects(), errors()},
@ -73,16 +72,59 @@ func userCreatedObjects() *kube.Objects {
return objs
}
func errors() []kube.Diagnostic {
const warning string = "warning"
d := []kube.Diagnostic{
{Category: warning, Message: fmt.Sprintf("Pod 'pod_foo' is in the default namespace.")},
{Category: warning, Message: fmt.Sprintf("Pod template 'template_foo' is in the default namespace.")},
{Category: warning, Message: fmt.Sprintf("Persistent Volume Claim 'pvc_foo' is in the default namespace.")},
{Category: warning, Message: fmt.Sprintf("Config Map 'cm_foo' is in the default namespace.")},
{Category: warning, Message: fmt.Sprintf("Service 'svc_foo' is in the default namespace.")},
{Category: warning, Message: fmt.Sprintf("Secret 'secret_foo' is in the default namespace.")},
{Category: warning, Message: fmt.Sprintf("Service Account 'sa_foo' is in the default namespace.")},
func errors() []checks.Diagnostic {
objs := userCreatedObjects()
pod := objs.Pods.Items[0]
template := objs.PodTemplates.Items[0]
pvc := objs.PersistentVolumeClaims.Items[0]
cm := objs.ConfigMaps.Items[0]
service := objs.Services.Items[0]
secret := objs.Secrets.Items[0]
sa := objs.ServiceAccounts.Items[0]
d := []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "Avoid using the default namespace for pod 'pod_foo'",
Object: kube.Object{TypeInfo: &pod.TypeMeta, ObjectInfo: &pod.ObjectMeta},
Owners: pod.ObjectMeta.GetOwnerReferences(),
},
{
Severity: checks.Warning,
Message: "Avoid using the default namespace for pod template 'template_foo'",
Object: kube.Object{TypeInfo: &template.TypeMeta, ObjectInfo: &template.ObjectMeta},
Owners: template.ObjectMeta.GetOwnerReferences(),
},
{
Severity: checks.Warning,
Message: "Avoid using the default namespace for persistent volume claim 'pvc_foo'",
Object: kube.Object{TypeInfo: &pvc.TypeMeta, ObjectInfo: &pvc.ObjectMeta},
Owners: pvc.ObjectMeta.GetOwnerReferences(),
},
{
Severity: checks.Warning,
Message: "Avoid using the default namespace for config map 'cm_foo'",
Object: kube.Object{TypeInfo: &cm.TypeMeta, ObjectInfo: &cm.ObjectMeta},
Owners: cm.ObjectMeta.GetOwnerReferences(),
},
{
Severity: checks.Warning,
Message: "Avoid using the default namespace for service 'svc_foo'",
Object: kube.Object{TypeInfo: &service.TypeMeta, ObjectInfo: &service.ObjectMeta},
Owners: service.ObjectMeta.GetOwnerReferences(),
},
{
Severity: checks.Warning,
Message: "Avoid using the default namespace for secret 'secret_foo'",
Object: kube.Object{TypeInfo: &secret.TypeMeta, ObjectInfo: &secret.ObjectMeta},
Owners: secret.ObjectMeta.GetOwnerReferences(),
},
{
Severity: checks.Warning,
Message: "Avoid using the default namespace for service account 'sa_foo'",
Object: kube.Object{TypeInfo: &sa.TypeMeta, ObjectInfo: &sa.ObjectMeta},
Owners: sa.ObjectMeta.GetOwnerReferences(),
},
}
return d
}

View File

@ -33,12 +33,18 @@ func (p *podStatusCheck) Description() string {
// 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 (p *podStatusCheck) Run(objects *kube.Objects) ([]kube.Diagnostic, error) {
var diagnostics []kube.Diagnostic
func (p *podStatusCheck) Run(objects *kube.Objects) ([]checks.Diagnostic, error) {
var diagnostics []checks.Diagnostic
for _, pod := range objects.Pods.Items {
if corev1.PodFailed == pod.Status.Phase || corev1.PodUnknown == pod.Status.Phase {
diagnostics = append(diagnostics, kube.Diagnostic{Category: "error", Message: fmt.Sprintf("Pod '%s' in namespace '%s' has state: %s. Pod state should be `Running`, `Pending` or `Succeeded`.", pod.GetName(), pod.GetNamespace(), pod.Status.Phase)})
d := checks.Diagnostic{
Severity: checks.Warning,
Message: fmt.Sprintf("Pod '%s' in namespace '%s' has state: %s. Pod state should be `Running`, `Pending` or `Succeeded`.", pod.GetName(), pod.GetNamespace(), pod.Status.Phase),
Object: kube.Object{TypeInfo: &pod.TypeMeta, ObjectInfo: &pod.ObjectMeta},
Owners: pod.ObjectMeta.GetOwnerReferences(),
}
diagnostics = append(diagnostics, d)
}
}

View File

@ -3,6 +3,7 @@ package basic
import (
"testing"
"github.com/digitalocean/clusterlint/checks"
"github.com/digitalocean/clusterlint/kube"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
@ -19,7 +20,7 @@ func TestPodStateError(t *testing.T) {
scenarios := []struct {
name string
arg *kube.Objects
expected []kube.Diagnostic
expected []checks.Diagnostic
}{
{
name: "no pods",
@ -44,15 +45,25 @@ func TestPodStateError(t *testing.T) {
{
name: "pod with failed status",
arg: status(corev1.PodFailed),
expected: []kube.Diagnostic{
{Category: "error", Message: "Pod 'pod_foo' in namespace 'k8s' has state: Failed. Pod state should be `Running`, `Pending` or `Succeeded`."},
expected: []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "Pod 'pod_foo' in namespace 'k8s' has state: Failed. Pod state should be `Running`, `Pending` or `Succeeded`.",
Object: kube.Object{TypeInfo: GetTypeMeta(), ObjectInfo: GetObjectMeta()},
Owners: GetOwners(),
},
},
},
{
name: "pod with unknown status",
arg: status(corev1.PodUnknown),
expected: []kube.Diagnostic{
{Category: "error", Message: "Pod 'pod_foo' in namespace 'k8s' has state: Unknown. Pod state should be `Running`, `Pending` or `Succeeded`."},
expected: []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "Pod 'pod_foo' in namespace 'k8s' has state: Unknown. Pod state should be `Running`, `Pending` or `Succeeded`.",
Object: kube.Object{TypeInfo: GetTypeMeta(), ObjectInfo: GetObjectMeta()},
Owners: GetOwners(),
},
},
},
}

View File

@ -16,5 +16,5 @@ type Check interface {
// 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.
Run(*kube.Objects) ([]kube.Diagnostic, error)
Run(*kube.Objects) ([]Diagnostic, error)
}

29
checks/diagnostic.go Normal file
View File

@ -0,0 +1,29 @@
package checks
import (
"fmt"
"github.com/digitalocean/clusterlint/kube"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Diagnostic encapsulates the information each check returns.
type Diagnostic struct {
Severity Severity
Message string
Object kube.Object
Owners []metav1.OwnerReference
}
func (d *Diagnostic) String() string {
return fmt.Sprintf("[%s] %s/%s/%s: %s", d.Severity, d.Object.ObjectInfo.Namespace,
d.Object.TypeInfo.Kind, d.Object.ObjectInfo.Name, d.Message)
}
type Severity string
const (
Error Severity = "error"
Warning Severity = "warning"
Suggestion Severity = "suggestion"
)

View File

@ -33,12 +33,18 @@ func (nc *podSelectorCheck) Description() string {
// 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 (nc *podSelectorCheck) Run(objects *kube.Objects) ([]kube.Diagnostic, error) {
var diagnostics []kube.Diagnostic
func (nc *podSelectorCheck) Run(objects *kube.Objects) ([]checks.Diagnostic, error) {
var diagnostics []checks.Diagnostic
for _, pod := range objects.Pods.Items {
nodeSelectorMap := pod.Spec.NodeSelector
if _, ok := nodeSelectorMap[corev1.LabelHostname]; ok {
diagnostics = append(diagnostics, kube.Diagnostic{Category: "error", Message: fmt.Sprintf("pod '%s' in namespace '%s' uses the node name for node selector", pod.GetName(), pod.GetNamespace())})
d := checks.Diagnostic{
Severity: checks.Error,
Message: fmt.Sprintf("Avoid node name label for node selector in pod: %s", pod.GetName()),
Object: kube.Object{TypeInfo: &pod.TypeMeta, ObjectInfo: &pod.ObjectMeta},
Owners: pod.ObjectMeta.GetOwnerReferences(),
}
diagnostics = append(diagnostics, d)
}
}
return diagnostics, nil

View File

@ -1,7 +1,6 @@
package doks
import (
"fmt"
"testing"
"github.com/digitalocean/clusterlint/checks"
@ -29,7 +28,7 @@ func TestNodeNameError(t *testing.T) {
scenarios := []struct {
name string
arg *kube.Objects
expected []kube.Diagnostic
expected []checks.Diagnostic
}{
{
name: "no node name selector",
@ -39,7 +38,7 @@ func TestNodeNameError(t *testing.T) {
{
name: "node name used in node selector",
arg: invalidPod(),
expected: errors(),
expected: errors(invalidPod()),
},
}
@ -74,9 +73,15 @@ func invalidPod() *kube.Objects {
return objs
}
func errors() []kube.Diagnostic {
diagnostics := []kube.Diagnostic{
{Category: "error", Message: fmt.Sprintf("pod 'pod_foo' in namespace 'k8s' uses the node name for node selector")},
func errors(objs *kube.Objects) []checks.Diagnostic {
pod := objs.Pods.Items[0]
diagnostics := []checks.Diagnostic{
{
Severity: checks.Error,
Message: "Avoid node name label for node selector in pod: pod_foo",
Object: kube.Object{TypeInfo: &pod.TypeMeta, ObjectInfo: &pod.ObjectMeta},
Owners: pod.ObjectMeta.GetOwnerReferences(),
},
}
return diagnostics
}

View File

@ -30,6 +30,6 @@ func (nc *check) Description() string {
// 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 (nc *check) Run(*kube.Objects) ([]kube.Diagnostic, error) {
func (nc *check) Run(*kube.Objects) ([]checks.Diagnostic, error) {
return nil, nil
}

View File

@ -33,14 +33,12 @@ func (pc *privilegedContainerCheck) Description() string {
// 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 (pc *privilegedContainerCheck) Run(objects *kube.Objects) ([]kube.Diagnostic, error) {
var diagnostics []kube.Diagnostic
func (pc *privilegedContainerCheck) Run(objects *kube.Objects) ([]checks.Diagnostic, error) {
var diagnostics []checks.Diagnostic
for _, pod := range objects.Pods.Items {
podName := pod.GetName()
namespace := pod.GetNamespace()
diagnostics = append(diagnostics, checkPrivileged(pod.Spec.Containers, podName, namespace)...)
diagnostics = append(diagnostics, checkPrivileged(pod.Spec.InitContainers, podName, namespace)...)
diagnostics = append(diagnostics, checkPrivileged(pod.Spec.Containers, pod)...)
diagnostics = append(diagnostics, checkPrivileged(pod.Spec.InitContainers, pod)...)
}
return diagnostics, nil
@ -48,12 +46,18 @@ func (pc *privilegedContainerCheck) Run(objects *kube.Objects) ([]kube.Diagnosti
// checkPrivileged checks if the container is running in privileged mode
// Adds a warning if it finds any privileged container
func checkPrivileged(containers []corev1.Container, podName string, namespace string) []kube.Diagnostic {
var d []kube.Diagnostic
func checkPrivileged(containers []corev1.Container, pod corev1.Pod) []checks.Diagnostic {
var diagnostics []checks.Diagnostic
for _, container := range containers {
if container.SecurityContext != nil && container.SecurityContext.Privileged != nil && *container.SecurityContext.Privileged {
d = append(d, kube.Diagnostic{Category: "warning", Message: fmt.Sprintf("[Best Practice] Privileged container '%s' found in pod '%s', namespace '%s'.", container.Name, podName, namespace)})
d := checks.Diagnostic{
Severity: checks.Warning,
Message: fmt.Sprintf("[Best Practice] Privileged container '%s' found in pod '%s'", container.Name, pod.GetName()),
Object: kube.Object{TypeInfo: &pod.TypeMeta, ObjectInfo: &pod.ObjectMeta},
Owners: pod.ObjectMeta.GetOwnerReferences(),
}
diagnostics = append(diagnostics, d)
}
}
return d
return diagnostics
}

View File

@ -1,7 +1,6 @@
package security
import (
"fmt"
"testing"
"github.com/digitalocean/clusterlint/checks"
@ -29,7 +28,7 @@ func TestPrivilegedContainerWarning(t *testing.T) {
scenarios := []struct {
name string
arg *kube.Objects
expected []kube.Diagnostic
expected []checks.Diagnostic
}{
{
name: "no pods",
@ -39,7 +38,7 @@ func TestPrivilegedContainerWarning(t *testing.T) {
{
name: "pod with container in privileged mode",
arg: container(true),
expected: warnings(),
expected: warnings(container(true)),
},
{
name: "pod with container.SecurityContext = nil",
@ -59,7 +58,7 @@ func TestPrivilegedContainerWarning(t *testing.T) {
{
name: "pod with init container in privileged mode",
arg: initContainer(true),
expected: warnings(),
expected: warnings(initContainer(true)),
},
{
name: "pod with initContainer.SecurityContext = nil",
@ -94,6 +93,7 @@ func initPod() *kube.Objects {
Pods: &corev1.PodList{
Items: []corev1.Pod{
{
TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Name: "pod_foo", Namespace: "k8s"},
},
},
@ -172,9 +172,15 @@ func initContainerPrivilegedNil() *kube.Objects {
return objs
}
func warnings() []kube.Diagnostic {
d := []kube.Diagnostic{
{Category: "warning", Message: fmt.Sprintf("[Best Practice] Privileged container 'bar' found in pod 'pod_foo', namespace 'k8s'.")},
func warnings(objs *kube.Objects) []checks.Diagnostic {
pod := objs.Pods.Items[0]
d := []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "[Best Practice] Privileged container 'bar' found in pod 'pod_foo'",
Object: kube.Object{TypeInfo: &pod.TypeMeta, ObjectInfo: &pod.ObjectMeta},
Owners: pod.ObjectMeta.GetOwnerReferences(),
},
}
return d
}

View File

@ -1,7 +1,7 @@
package main
import (
"fmt"
"encoding/json"
"log"
"os"
"path/filepath"
@ -61,7 +61,7 @@ func main() {
}
err := app.Run(os.Args)
if err != nil {
fmt.Printf("failed: %v", err)
log.Printf("failed: %v", err)
os.Exit(1)
}
}
@ -72,7 +72,7 @@ func listChecks(c *cli.Context) error {
group := c.String("group")
allChecks := getChecks(group)
for _, check := range allChecks {
fmt.Printf("%s : %s\n", check.Name(), check.Description())
log.Printf("%s : %s\n", check.Name(), check.Description())
}
return nil
@ -102,7 +102,7 @@ func runChecks(c *cli.Context) error {
// runs all checks in the registry if group is not specified
func runChecksForGroup(group string, objects *kube.Objects) error {
allChecks := getChecks(group)
var diagnostics []kube.Diagnostic
var diagnostics []checks.Diagnostic
var mu sync.Mutex
var g errgroup.Group
@ -136,22 +136,20 @@ func runCheck(name string, objects *kube.Objects) error {
log.Println("Running check: ", name)
diagnostics, err := check.Run(objects)
showDiagnostics(diagnostics)
return err
if err != nil {
return err
}
return showDiagnostics(diagnostics)
}
// showErrorsAndWarnings displays all the errors and warnings returned by checks
func showDiagnostics(diagnostics []kube.Diagnostic) {
for _, diagnostic := range diagnostics {
log.Printf("[%s] %s\n", diagnostic.Category, diagnostic.Message)
if len(diagnostic.Metadata) > 0 {
log.Println("Object Meta Information: ")
for key, value := range diagnostic.Metadata {
log.Printf("%s: %s\n", key, value)
}
}
func showDiagnostics(diagnostics []checks.Diagnostic) error {
resp, err := json.Marshal(diagnostics)
if err != nil {
return err
}
log.Println(string(resp))
return nil
}
// getChecks retrieves all checks within given group

View File

@ -25,19 +25,16 @@ type Objects struct {
LimitRanges *corev1.LimitRangeList
}
type Object struct {
TypeInfo *metav1.TypeMeta
ObjectInfo *metav1.ObjectMeta
}
// Client encapsulates a client for a Kubernetes cluster.
type Client struct {
kubeClient kubernetes.Interface
}
// Diagnostic encapsulates the information each check returns.
type Diagnostic struct {
Category string
Message string
Metadata map[string]string
}
// FetchObjects returns the objects from a Kubernetes cluster.
func (c *Client) FetchObjects() (*Objects, error) {
const all = ""