Merge pull request #109 from varshavaradarajan/dobs-check
doks - check if pod referencing dobs volumes are owned by a statefulsetsdas/webhooks-timeout-seconds
commit
ad20fc18c8
|
@ -145,7 +145,8 @@ func secretVolume() *kube.Objects {
|
|||
SecretName: "secret_foo",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
return objs
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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...)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue