Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

score: add test for deployment selector matching the pod template labels #385

Merged
merged 1 commit into from
Aug 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README_CHECKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@
| pod-networkpolicy | Pod | Makes sure that all Pods are targeted by a NetworkPolicy | default |
| networkpolicy-targets-pod | NetworkPolicy | Makes sure that all NetworkPolicies targets at least one Pod | default |
| pod-probes | Pod | Makes sure that all Pods have safe probe configurations | default |
| container-security-context | Pod | Makes sure that all pods have good securityContexts configured (*deprecated*, see [README_SECURITYCONTEXT.md](README_SECURITYCONTEXT.md) | default |
| container-security-context-user-group-id | Pod | Makes sure that user and group ID are set and > 10000 | optional |
| container-security-context-privileged | Pod | Makes sure that no Containers run in privileged mode | optional |
| container-security-context-readonlyrootfilesystem | Pod | Makes sure that all Containers have a read only root filesystem | optional |
| container-security-context | Pod | Makes sure that all pods have good securityContexts configured | default |
| container-security-context-user-group-id | Pod | Makes sure that all pods have a security context with valid UID and GID set | optional |
| container-security-context-privileged | Pod | Makes sure that all pods have a unprivileged security context set | optional |
| container-security-context-readonlyrootfilesystem | Pod | Makes sure that all pods have a security context with read only filesystem set | optional |
| container-seccomp-profile | Pod | Makes sure that all pods have at a seccomp policy configured. | optional |
| service-targets-pod | Service | Makes sure that all Services targets a Pod | default |
| service-type | Service | Makes sure that the Service type is not NodePort | default |
| stable-version | all | Checks if the object is using a deprecated apiVersion | default |
| deployment-has-host-podantiaffinity | Deployment | Makes sure that a podAntiAffinity has been set that prevents multiple pods from being scheduled on the same node. https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ | default |
| statefulset-has-host-podantiaffinity | StatefulSet | Makes sure that a podAntiAffinity has been set that prevents multiple pods from being scheduled on the same node. https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ | default |
| deployment-targeted-by-hpa-does-not-have-replicas-configured | Deployment | Makes sure that Deployments using a HorizontalPodAutoscaler doesn't have a statically configured replica count set | default |
| statefulset-has-servicename | StatefulSet | Makes sure that StatefulSets have a existing serviceName that is headless. | default |
| statefulset-has-servicename | StatefulSet | Makes sure that StatefulSets have a existing headless serviceName. | default |
| label-values | all | Validates label values | default |
| horizontalpodautoscaler-has-target | HorizontalPodAutoscaler | Makes sure that the HPA targets a valid object | default |
40 changes: 33 additions & 7 deletions score/apps/apps.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package apps

import (
"fmt"
"strings"

appsv1 "k8s.io/api/apps/v1"
Expand All @@ -16,9 +17,12 @@ import (
func Register(allChecks *checks.Checks, allHPAs []ks.HpaTargeter, allServices []ks.Service) {
allChecks.RegisterDeploymentCheck("Deployment has host PodAntiAffinity", "Makes sure that a podAntiAffinity has been set that prevents multiple pods from being scheduled on the same node. https://kubernetes.io/docs/concepts/configuration/assign-pod-node/", deploymentHasAntiAffinity)
allChecks.RegisterStatefulSetCheck("StatefulSet has host PodAntiAffinity", "Makes sure that a podAntiAffinity has been set that prevents multiple pods from being scheduled on the same node. https://kubernetes.io/docs/concepts/configuration/assign-pod-node/", statefulsetHasAntiAffinity)

allChecks.RegisterDeploymentCheck("Deployment targeted by HPA does not have replicas configured", "Makes sure that Deployments using a HorizontalPodAutoscaler doesn't have a statically configured replica count set", hpaDeploymentNoReplicas(allHPAs))
allChecks.RegisterStatefulSetCheck("StatefulSet has ServiceName", "Makes sure that StatefulSets have an existing headless serviceName.", statefulsetHasServiceName(allServices))
allChecks.RegisterStatefulSetCheck("StatefulSet Pod Selector labels match template metadata labels", "Ensure the StatefulSet selector labels match the template metadata labels.", statefulsetSelectorLabelsMatchTemplateMetadataLabels)

allChecks.RegisterDeploymentCheck("Deployment Pod Selector labels match template metadata labels", "Ensure the StatefulSet selector labels match the template metadata labels.", deploymentSelectorLabelsMatching)
allChecks.RegisterStatefulSetCheck("StatefulSet Pod Selector labels match template metadata labels", "Ensure the StatefulSet selector labels match the template metadata labels.", statefulSetSelectorLabelsMatching)
}

func hpaDeploymentNoReplicas(allHPAs []ks.HpaTargeter) func(deployment appsv1.Deployment) (scorecard.TestScore, error) {
Expand Down Expand Up @@ -161,16 +165,38 @@ func statefulsetHasServiceName(allServices []ks.Service) func(statefulset appsv1
}
}

func statefulsetSelectorLabelsMatchTemplateMetadataLabels(statefulset appsv1.StatefulSet) (score scorecard.TestScore, err error) {
if internal.LabelSelectorMatchesLabels(
statefulset.Spec.Selector.MatchLabels,
statefulset.Spec.Template.GetObjectMeta().GetLabels(),
) {
func statefulSetSelectorLabelsMatching(statefulset appsv1.StatefulSet) (score scorecard.TestScore, err error) {
selector, err := metav1.LabelSelectorAsSelector(statefulset.Spec.Selector)
if err != nil {
score.Grade = scorecard.GradeCritical
score.AddComment("", "StatefulSet selector labels are not matching template metadata labels", fmt.Sprintf("Invalid selector: %s", err))
return
}

if selector.Matches(internal.MapLables(statefulset.Spec.Template.GetObjectMeta().GetLabels())) {
score.Grade = scorecard.GradeAllOK
return
}

score.Grade = scorecard.GradeCritical
score.AddComment("", "StatefulSet selector labels not matching template metadata labels", "StatefulSets require `.spec.selector` to match `.spec.template.metadata.labels`. https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-selector")
return
}

func deploymentSelectorLabelsMatching(deployment appsv1.Deployment) (score scorecard.TestScore, err error) {
selector, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector)
if err != nil {
score.Grade = scorecard.GradeCritical
score.AddComment("", "Deployment selector labels are not matching template metadata labels", fmt.Sprintf("Invalid selector: %s", err))
return
}

if selector.Matches(internal.MapLables(deployment.Spec.Template.GetObjectMeta().GetLabels())) {
score.Grade = scorecard.GradeAllOK
return
}

score.Grade = scorecard.GradeCritical
score.AddComment("", "StatefulSet selector labels not matching template metadata labels", "StatefulSets require `.spec.selector.matchLabels` to match `.spec.template.metadata.labels`. https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-selector")
score.AddComment("", "Deployment selector labels not matching template metadata labels", "Deployment require `.spec.selector` to match `.spec.template.metadata.labels`. https://kubernetes.io/docs/concepts/workloads/controllers/deployment/")
return
}
178 changes: 177 additions & 1 deletion score/apps/apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,10 +705,186 @@ func TestStatefulSetSelectorLabels(t *testing.T) {
expectedErr: nil,
expectedGrade: scorecard.GradeCritical,
},

// Match (expression)
{
statefulset: appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{Kind: "StatefulSet"},
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Spec: appsv1.StatefulSetSpec{
Selector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "app",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"aaa", "bbb", "bar"},
},
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "bar",
},
},
},
},
},
expectedErr: nil,
expectedGrade: scorecard.GradeAllOK,
},

// No match (expression)
{
statefulset: appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{Kind: "StatefulSet"},
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Spec: appsv1.StatefulSetSpec{
Selector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "app",
Operator: metav1.LabelSelectorOpNotIn,
Values: []string{"aaa", "bbb", "bar"},
},
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "bar",
},
},
},
},
},
expectedErr: nil,
expectedGrade: scorecard.GradeCritical,
},
}

for _, tc := range testcases {
score, err := statefulSetSelectorLabelsMatching(tc.statefulset)
assert.Equal(t, tc.expectedErr, err)
assert.Equal(t, tc.expectedGrade, score.Grade)
}
}

func TestDeploymentSelectorLabels(t *testing.T) {
t.Parallel()

testcases := []struct {
statefulset appsv1.Deployment
expectedErr error
expectedGrade scorecard.Grade
}{
// Match
{
statefulset: appsv1.Deployment{
TypeMeta: metav1.TypeMeta{Kind: "Deployment"},
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "foo",
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "foo",
},
},
},
},
},
expectedErr: nil,
expectedGrade: scorecard.GradeAllOK,
},

// No match (labels differ)
{
statefulset: appsv1.Deployment{
TypeMeta: metav1.TypeMeta{Kind: "StatefulSet"},
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "foo",
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "bar",
},
},
},
},
},
expectedErr: nil,
expectedGrade: scorecard.GradeCritical,
},

// Match (expression)
{
statefulset: appsv1.Deployment{
TypeMeta: metav1.TypeMeta{Kind: "StatefulSet"},
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "app",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"aaa", "bbb", "bar"},
},
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "bar",
},
},
},
},
},
expectedErr: nil,
expectedGrade: scorecard.GradeAllOK,
},

// No match (expression)
{
statefulset: appsv1.Deployment{
TypeMeta: metav1.TypeMeta{Kind: "StatefulSet"},
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "app",
Operator: metav1.LabelSelectorOpNotIn,
Values: []string{"aaa", "bbb", "bar"},
},
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "bar",
},
},
},
},
},
expectedErr: nil,
expectedGrade: scorecard.GradeCritical,
},
}

for _, tc := range testcases {
score, err := statefulsetSelectorLabelsMatchTemplateMetadataLabels(tc.statefulset)
score, err := deploymentSelectorLabelsMatching(tc.statefulset)
assert.Equal(t, tc.expectedErr, err)
assert.Equal(t, tc.expectedGrade, score.Grade)
}
Expand Down
2 changes: 1 addition & 1 deletion score/internal/labelselector.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package internal

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

// Implements the Kubernetes Labels interface
// MapLables implements the Kubernetes Labels interface
type MapLables map[string]string

func (l MapLables) Has(key string) bool {
Expand Down