Merge pull request #109 from varshavaradarajan/dobs-check

doks - check if pod referencing dobs volumes are owned by a statefulset
sdas/webhooks-timeout-seconds
Varsha Varadarajan 2021-01-05 12:42:57 -08:00 committed by GitHub
commit ad20fc18c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 514 additions and 1 deletions

View File

@ -145,7 +145,8 @@ func secretVolume() *kube.Objects {
SecretName: "secret_foo",
},
},
}},
},
},
}
return objs
}

View File

@ -0,0 +1,139 @@
/*
Copyright 2021 DigitalOcean
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package doks
import (
"github.com/digitalocean/clusterlint/checks"
"github.com/digitalocean/clusterlint/kube"
corev1 "k8s.io/api/core/v1"
st "k8s.io/api/storage/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
DOCSIDriver = "dobs.csi.digitalocean.com"
LegacyCSIDriver = "com.digitalocean.csi.dobs"
DOBlockStorageName = "do-block-storage"
)
func init() {
checks.Register(&dobsPodOwner{})
}
type dobsPodOwner struct{}
// Name returns a unique name for this check.
func (p *dobsPodOwner) Name() string {
return "dobs-pod-owner"
}
// Groups returns a list of group names this check should be part of.
func (p *dobsPodOwner) Groups() []string {
return []string{"doks"}
}
// Description returns a detailed human-readable description of what this check
// does.
func (p *dobsPodOwner) Description() string {
return "Checks if pods referencing dobs volumes are owned by a stateful set."
}
// 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 *dobsPodOwner) Run(objects *kube.Objects) ([]checks.Diagnostic, error) {
var diagnostics []checks.Diagnostic
var dobsPods []corev1.Pod
for _, pod := range objects.Pods.Items {
pod := pod
for _, volume := range pod.Spec.Volumes {
if isDOBSVolume(volume, pod.Namespace, objects) {
dobsPods = append(dobsPods, pod)
}
}
}
for _, pod := range dobsPods {
if pod.OwnerReferences != nil && ownedByStatefulSet(pod.OwnerReferences) {
continue
}
d := checks.Diagnostic{
Severity: checks.Warning,
Message: "Pod referencing DOBS volumes must be owned by StatefulSet",
Kind: checks.Pod,
Object: &pod.ObjectMeta,
Owners: pod.ObjectMeta.GetOwnerReferences(),
}
diagnostics = append(diagnostics, d)
}
return diagnostics, nil
}
func isDOBSVolume(volume corev1.Volume, namespace string, objects *kube.Objects) bool {
if volume.PersistentVolumeClaim != nil {
claim := getPVC(objects.PersistentVolumeClaims, volume.PersistentVolumeClaim.ClaimName, namespace)
if claim == nil {
return false
}
if claim.Spec.StorageClassName == nil && isDOCSI(objects.DefaultStorageClass.Provisioner) {
return true
}
sc := getStorageClass(objects.StorageClasses, claim.Spec.StorageClassName)
if sc != nil && isDOCSI(sc.Provisioner) {
return true
}
}
if volume.CSI != nil {
if isDOCSI(volume.CSI.Driver) {
return true
}
}
return false
}
func isDOCSI(referrer string) bool {
return referrer == DOCSIDriver || referrer == LegacyCSIDriver
}
func getStorageClass(classes *st.StorageClassList, name *string) *st.StorageClass {
for _, c := range classes.Items {
if c.Name == *name {
return &c
}
}
return nil
}
func getPVC(claims *corev1.PersistentVolumeClaimList, name string, namespace string) *corev1.PersistentVolumeClaim {
for _, c := range claims.Items {
if c.Name == name && c.Namespace == namespace {
return &c
}
}
return nil
}
func ownedByStatefulSet(references []metav1.OwnerReference) bool {
for _, ref := range references {
if ref.Kind == "StatefulSet" {
return true
}
}
return false
}

View File

@ -0,0 +1,356 @@
/*
Copyright 2021 DigitalOcean
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package doks
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"testing"
"github.com/digitalocean/clusterlint/checks"
"github.com/digitalocean/clusterlint/kube"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
st "k8s.io/api/storage/v1"
)
func TestDobsPodOwnerCheckMeta(t *testing.T) {
dobsPodOwner := dobsPodOwner{}
assert.Equal(t, "dobs-pod-owner", dobsPodOwner.Name())
assert.Equal(t, []string{"doks"}, dobsPodOwner.Groups())
assert.NotEmpty(t, dobsPodOwner.Description())
}
func TestDobsPodOwnerCheckRegistration(t *testing.T) {
dobsPodOwner := &dobsPodOwner{}
check, err := checks.Get("dobs-pod-owner")
assert.NoError(t, err)
assert.Equal(t, check, dobsPodOwner)
}
func TestDobsPodOwnerWarning(t *testing.T) {
dobsPodOwner := dobsPodOwner{}
tests := []struct {
name string
objs *kube.Objects
expected []checks.Diagnostic
}{
{
name: "no pods",
objs: &kube.Objects{Pods: &corev1.PodList{}},
expected: nil,
},
{
name: "no pods referencing dobs volumes",
objs: noDobs(),
expected: nil,
},
{
name: "bare dobs pod referenced by pvc",
objs: pvcDobs(DOBlockStorageName, DOCSIDriver),
expected: []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "Pod referencing DOBS volumes must be owned by StatefulSet",
Kind: checks.Pod,
Object: &metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault},
Owners: nil,
},
},
},
{
name: "bare dobs pod referenced by pvc -- with legacy driver",
objs: pvcDobs(DOBlockStorageName, LegacyCSIDriver),
expected: []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "Pod referencing DOBS volumes must be owned by StatefulSet",
Kind: checks.Pod,
Object: &metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault},
Owners: nil,
},
},
},
{
name: "bare dobs pod referenced by pvc with default storage class",
objs: pvcDobs("", DOCSIDriver),
expected: []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "Pod referencing DOBS volumes must be owned by StatefulSet",
Kind: checks.Pod,
Object: &metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault},
Owners: nil,
},
},
},
{
name: "bare dobs pod referenced by pvc with default storage class -- with legacy driver",
objs: pvcDobs("", LegacyCSIDriver),
expected: []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "Pod referencing DOBS volumes must be owned by StatefulSet",
Kind: checks.Pod,
Object: &metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault},
Owners: nil,
},
},
},
{
name: "bare dobs pod referenced by csi",
objs: csiDobs(DOCSIDriver),
expected: []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "Pod referencing DOBS volumes must be owned by StatefulSet",
Kind: checks.Pod,
Object: &metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault},
Owners: nil,
},
},
},
{
name: "bare dobs pod referenced by legacy csi driver",
objs: csiDobs(LegacyCSIDriver),
expected: []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "Pod referencing DOBS volumes must be owned by StatefulSet",
Kind: checks.Pod,
Object: &metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault},
Owners: nil,
},
},
},
{
name: "dobs pod owned by deployment",
objs: deployment(pvcDobs("", DOCSIDriver)),
expected: []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "Pod referencing DOBS volumes must be owned by StatefulSet",
Kind: checks.Pod,
Object: &metav1.ObjectMeta{
Name: "foo",
Namespace: metav1.NamespaceDefault,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "web-app",
},
},
},
Owners: []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "web-app",
},
},
},
},
},
{
name: "dobs pod owned by deployment -- with legacy driver",
objs: deployment(pvcDobs("", LegacyCSIDriver)),
expected: []checks.Diagnostic{
{
Severity: checks.Warning,
Message: "Pod referencing DOBS volumes must be owned by StatefulSet",
Kind: checks.Pod,
Object: &metav1.ObjectMeta{
Name: "foo",
Namespace: metav1.NamespaceDefault,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "web-app",
},
},
},
Owners: []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "web-app",
},
},
},
},
},
{
name: "dobs pod owned by statefulset",
objs: statefulSet(pvcDobs("", DOCSIDriver)),
expected: nil,
},
{
name: "dobs pod owned by statefulset -- with legacy driver",
objs: statefulSet(pvcDobs("", LegacyCSIDriver)),
expected: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
d, err := dobsPodOwner.Run(test.objs)
assert.NoError(t, err)
assert.ElementsMatch(t, test.expected, d)
})
}
}
func csiDobs(driver string) *kube.Objects {
objs := &kube.Objects{
Pods: &corev1.PodList{
Items: []corev1.Pod{
{
TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault},
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "csi-do",
VolumeSource: corev1.VolumeSource{
CSI: &corev1.CSIVolumeSource{
Driver: driver,
},
},
},
},
},
},
},
},
}
return objs
}
func statefulSet(objs *kube.Objects) *kube.Objects {
objs.Pods.Items[0].OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "StatefulSet",
Name: "woot",
},
}
return objs
}
func deployment(objs *kube.Objects) *kube.Objects {
objs.Pods.Items[0].OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "web-app",
},
}
return objs
}
func pvcDobs(storageClass, driver string) *kube.Objects {
var sc *string
if storageClass != "" {
sc = &storageClass
}
objs := &kube.Objects{
Pods: &corev1.PodList{
Items: []corev1.Pod{
{
TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault},
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "def-pvc-source",
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: "def-pvc",
},
},
},
},
},
},
},
},
PersistentVolumeClaims: &corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{
{
TypeMeta: metav1.TypeMeta{Kind: "PersistentVolumeClaim", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Name: "def-pvc", Namespace: metav1.NamespaceDefault},
Spec: corev1.PersistentVolumeClaimSpec{
VolumeName: "dobs-v1",
StorageClassName: sc,
},
},
},
},
StorageClasses: &st.StorageClassList{
Items: []st.StorageClass{
{
TypeMeta: metav1.TypeMeta{Kind: "StorageClass", APIVersion: "storage.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{Name: DOBlockStorageName, Namespace: metav1.NamespaceDefault},
Provisioner: driver,
},
},
},
DefaultStorageClass: &st.StorageClass{
TypeMeta: metav1.TypeMeta{Kind: "StorageClass", APIVersion: "storage.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{Name: DOBlockStorageName, Namespace: metav1.NamespaceDefault},
Provisioner: driver,
},
}
return objs
}
func noDobs() *kube.Objects {
objs := &kube.Objects{
Pods: &corev1.PodList{
Items: []corev1.Pod{
{
TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Name: "pod_foo", Namespace: "k8s"},
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "bar",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "secret_foo",
},
},
},
},
},
},
},
},
Secrets: &corev1.SecretList{
Items: []corev1.Secret{
{
TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Name: "secret_foo", Namespace: "k8s"},
},
},
},
}
return objs
}

View File

@ -53,6 +53,7 @@ func (nr *nonRootUserCheck) Run(objects *kube.Objects) ([]checks.Diagnostic, err
var diagnostics []checks.Diagnostic
for _, pod := range objects.Pods.Items {
pod := pod
var containers []corev1.Container
containers = append(containers, pod.Spec.Containers...)
containers = append(containers, pod.Spec.InitContainers...)

View File

@ -23,6 +23,7 @@ import (
ar "k8s.io/api/admissionregistration/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
corev1 "k8s.io/api/core/v1"
st "k8s.io/api/storage/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
@ -52,6 +53,8 @@ type Objects struct {
ServiceAccounts *corev1.ServiceAccountList
ResourceQuotas *corev1.ResourceQuotaList
LimitRanges *corev1.LimitRangeList
StorageClasses *st.StorageClassList
DefaultStorageClass *st.StorageClass
MutatingWebhookConfigurations *ar.MutatingWebhookConfigurationList
ValidatingWebhookConfigurations *ar.ValidatingWebhookConfigurationList
Namespaces *corev1.NamespaceList
@ -69,6 +72,7 @@ func (c *Client) FetchObjects(ctx context.Context, filter ObjectFilter) (*Object
client := c.KubeClient.CoreV1()
admissionControllerClient := c.KubeClient.AdmissionregistrationV1()
batchClient := c.KubeClient.BatchV1beta1()
storageClient := c.KubeClient.StorageV1()
opts := metav1.ListOptions{}
objects := &Objects{}
@ -77,6 +81,18 @@ func (c *Client) FetchObjects(ctx context.Context, filter ObjectFilter) (*Object
objects.Nodes, err = client.Nodes().List(gCtx, opts)
return
})
g.Go(func() (err error) {
objects.StorageClasses, err = storageClient.StorageClasses().List(gCtx, opts)
if err != nil {
return err
}
for _, s := range objects.StorageClasses.Items {
if v, _ := s.Annotations["storageclass.kubernetes.io/is-default-class"]; v == "true" {
objects.DefaultStorageClass = &s
}
}
return
})
g.Go(func() (err error) {
objects.PersistentVolumes, err = client.PersistentVolumes().List(gCtx, opts)
return