Add support for stateful sets (#145)

This commit is contained in:
Bobby Brennan
2019-06-13 07:59:01 -06:00
committed by GitHub
parent e030b61705
commit ebfb4ea9a1
15 changed files with 301 additions and 168 deletions
+5
View File
@@ -265,6 +265,11 @@ ul.message-list li i.message-icon {
color: #a11f4c;
}
.controller-type {
display: inline-block;
min-width: 115px;
}
a.more-info {
color: #bbb;
font-size: 12px;
+9 -8
View File
@@ -87,14 +87,15 @@ type templateData struct {
// GetBaseTemplate puts together the dashboard template. Individual pieces can be overridden before rendering.
func GetBaseTemplate(name string) (*template.Template, error) {
tmpl := template.New(name).Funcs(template.FuncMap{
"getWarningWidth": getWarningWidth,
"getSuccessWidth": getSuccessWidth,
"getWeatherIcon": getWeatherIcon,
"getWeatherText": getWeatherText,
"getGrade": getGrade,
"getIcon": getIcon,
"getCategoryLink": getCategoryLink,
"getCategoryInfo": getCategoryInfo,
"getWarningWidth": getWarningWidth,
"getSuccessWidth": getSuccessWidth,
"getWeatherIcon": getWeatherIcon,
"getWeatherText": getWeatherText,
"getGrade": getGrade,
"getIcon": getIcon,
"getCategoryLink": getCategoryLink,
"getCategoryInfo": getCategoryInfo,
"getAllControllerResults": getAllControllerResults,
})
templateFileNames := []string{
+7
View File
@@ -20,6 +20,13 @@ import (
"strings"
)
func getAllControllerResults(nr validator.NamespaceResult) []validator.ControllerResult {
results := []validator.ControllerResult{}
results = append(results, nr.DeploymentResults...)
results = append(results, nr.StatefulSetResults...)
return results
}
func getWarningWidth(counts validator.CountSummary, fullWidth int) uint {
return uint(float64(counts.Successes+counts.Warnings) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth))
}
+4 -2
View File
@@ -78,7 +78,7 @@
<div class="card namespace">
<h3>Namespace: <strong>{{ $namespace }}</strong></h3>
<div class="expandable-table">
{{ range .DeploymentResults }}
{{ range getAllControllerResults $nsResult }}
<div class="resource-info">
<div class="status-bar">
<div class="status">
@@ -90,7 +90,9 @@
</div>
</div>
<div class="name"><span class="caret-expander"></span>Deployment: <strong>{{ .Name }}</strong></div>
<div class="name"><span class="caret-expander"></span>
<span class="controller-type">{{ .Type }}:</span>
<strong>{{ .Name }}</strong></div>
<div class="result-messages expandable-content">
<h4>Pod Spec:</h4>
<ul class="message-list">
+49 -41
View File
@@ -27,6 +27,7 @@ type ResourceProvider struct {
SourceType string
Nodes []corev1.Node
Deployments []appsv1.Deployment
StatefulSets []appsv1.StatefulSet
Namespaces []corev1.Namespace
Pods []corev1.Pod
}
@@ -51,47 +52,13 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
SourceName: directory,
Nodes: []corev1.Node{},
Deployments: []appsv1.Deployment{},
StatefulSets: []appsv1.StatefulSet{},
Namespaces: []corev1.Namespace{},
Pods: []corev1.Pod{},
}
addYaml := func(contents string) error {
contentBytes := []byte(contents)
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
resource := k8sResource{}
err := decoder.Decode(&resource)
if err != nil {
// TODO: should we panic if the YAML is bad?
logrus.Errorf("Invalid YAML: %s", string(contents))
return nil
}
decoder = k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
if resource.Kind == "Deployment" {
dep := appsv1.Deployment{}
err = decoder.Decode(&dep)
if err != nil {
logrus.Errorf("Error parsing deployment %v", err)
return err
}
resources.Deployments = append(resources.Deployments, dep)
} else if resource.Kind == "Namespace" {
ns := corev1.Namespace{}
err = decoder.Decode(&ns)
if err != nil {
logrus.Errorf("Error parsing namespace %v", err)
return err
}
resources.Namespaces = append(resources.Namespaces, ns)
} else if resource.Kind == "Pod" {
pod := corev1.Pod{}
err = decoder.Decode(&pod)
if err != nil {
logrus.Errorf("Error parsing pod %v", err)
return err
}
resources.Pods = append(resources.Pods, pod)
}
return nil
return addResourceFromString(contents, &resources)
}
visitFile := func(path string, f os.FileInfo, err error) error {
@@ -144,27 +111,32 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
listOpts := metav1.ListOptions{}
serverVersion, err := kube.Discovery().ServerVersion()
if err != nil {
logrus.Errorf("Error fetching Kubernetes API version %v", err)
logrus.Errorf("Error fetching Cluster API version %v", err)
return nil, err
}
deploys, err := kube.AppsV1().Deployments("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Kubernetes Deployments %v", err)
logrus.Errorf("Error fetching Deployments %v", err)
return nil, err
}
statefulSets, err := kube.AppsV1().StatefulSets("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching StatefulSets%v", err)
return nil, err
}
nodes, err := kube.CoreV1().Nodes().List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Kubernetes Nodes %v", err)
logrus.Errorf("Error fetching Nodes %v", err)
return nil, err
}
namespaces, err := kube.CoreV1().Namespaces().List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Kubernetes Namespaces %v", err)
logrus.Errorf("Error fetching Namespaces %v", err)
return nil, err
}
pods, err := kube.CoreV1().Pods("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Kubernetes Pods %v", err)
logrus.Errorf("Error fetching Pods %v", err)
return nil, err
}
@@ -174,9 +146,45 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
SourceName: clusterName,
CreationTime: time.Now(),
Deployments: deploys.Items,
StatefulSets: statefulSets.Items,
Nodes: nodes.Items,
Namespaces: namespaces.Items,
Pods: pods.Items,
}
return &api, nil
}
func addResourceFromString(contents string, resources *ResourceProvider) error {
contentBytes := []byte(contents)
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
resource := k8sResource{}
err := decoder.Decode(&resource)
if err != nil {
// TODO: should we panic if the YAML is bad?
logrus.Errorf("Invalid YAML: %s", string(contents))
return nil
}
decoder = k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
if resource.Kind == "Deployment" {
dep := appsv1.Deployment{}
err = decoder.Decode(&dep)
resources.Deployments = append(resources.Deployments, dep)
} else if resource.Kind == "StatefulSet" {
dep := appsv1.StatefulSet{}
err = decoder.Decode(&dep)
resources.StatefulSets = append(resources.StatefulSets, dep)
} else if resource.Kind == "Namespace" {
ns := corev1.Namespace{}
err = decoder.Decode(&ns)
resources.Namespaces = append(resources.Namespaces, ns)
} else if resource.Kind == "Pod" {
pod := corev1.Pod{}
err = decoder.Decode(&pod)
resources.Pods = append(resources.Pods, pod)
}
if err != nil {
logrus.Errorf("Error parsing %s: %v", resource.Kind, err)
return err
}
return nil
}
+5 -1
View File
@@ -22,6 +22,9 @@ func TestGetResourcesFromPath(t *testing.T) {
assert.Equal(t, 1, len(resources.Deployments), "Should have a deployment")
assert.Equal(t, "ubuntu", resources.Deployments[0].Spec.Template.Spec.Containers[0].Name)
assert.Equal(t, 1, len(resources.StatefulSets), "Should have a stateful set")
assert.Equal(t, "nginx", resources.StatefulSets[0].Spec.Template.Spec.Containers[0].Name)
assert.Equal(t, 1, len(resources.Namespaces), "Should have a namespace")
assert.Equal(t, "two", resources.Namespaces[0].ObjectMeta.Name)
@@ -52,7 +55,7 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
func TestGetResourceFromAPI(t *testing.T) {
k8s := test.SetupTestAPI()
k8s = test.SetupAddDeploys(k8s, "test")
k8s = test.SetupAddControllers(k8s, "test")
resources, err := CreateResourceProviderFromAPI(k8s, "test")
assert.Equal(t, nil, err, "Error should be nil")
@@ -62,6 +65,7 @@ func TestGetResourceFromAPI(t *testing.T) {
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Deployments), "Should have a deployment")
assert.Equal(t, 1, len(resources.StatefulSets), "Should have a stateful set")
assert.Equal(t, 0, len(resources.Pods), "Should have a pod")
assert.Equal(t, "", resources.Deployments[0].ObjectMeta.Name)
@@ -0,0 +1,35 @@
apiVersion: apps/v1 # for k8s versions before 1.9.0 use apps/v1beta2 and before 1.8.0 use extensions/v1beta1
kind: StatefulSet
metadata:
name: web
labels:
app: nginx
spec:
serviceName: "nginx"
selector:
matchLabels:
app: nginx
replicas: 14
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: k8s.gcr.io/nginx-slim:0.8
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
storageClassName: thin-disk
+96
View File
@@ -0,0 +1,96 @@
// Copyright 2019 ReactiveOps
//
// 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 validator
import (
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/pkg/kube"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
)
// ControllerSpec is a generic type for k8s controller specs
type ControllerSpec struct {
Template corev1.PodTemplateSpec
}
// Controller is a generic type for k8s controllers (e.g. Deployments and StatefulSets)
type Controller struct {
Type string
Name string
Namespace string
Spec ControllerSpec
}
// ValidateController validates a single controller, returns a ControllerResult.
func ValidateController(conf conf.Configuration, controller Controller) ControllerResult {
pod := controller.Spec.Template.Spec
podResult := ValidatePod(conf, &pod)
return ControllerResult{
Type: controller.Type,
Name: controller.Name,
PodResult: podResult,
}
}
// ValidateControllers validates that each deployment conforms to the Polaris config,
// returns a list of ResourceResults organized by namespace.
func ValidateControllers(config conf.Configuration, kubeResources *kube.ResourceProvider, nsResults *NamespacedResults) {
controllers := []Controller{}
for _, deploy := range kubeResources.Deployments {
controllers = append(controllers, ControllerFromDeployment(deploy))
}
for _, deploy := range kubeResources.StatefulSets {
controllers = append(controllers, ControllerFromStatefulSet(deploy))
}
for _, controller := range controllers {
controllerResult := ValidateController(config, controller)
nsResult := nsResults.getNamespaceResult(controller.Namespace)
nsResult.Summary.appendResults(*controllerResult.PodResult.Summary)
if controller.Type == "Deployment" {
nsResult.DeploymentResults = append(nsResult.DeploymentResults, controllerResult)
} else if controller.Type == "StatefulSet" {
nsResult.StatefulSetResults = append(nsResult.StatefulSetResults, controllerResult)
}
}
}
// ControllerFrom* functions are 100% boilerplate
// ControllerFromDeployment creates a controller
func ControllerFromDeployment(c appsv1.Deployment) Controller {
spec := ControllerSpec{
Template: c.Spec.Template,
}
return Controller{
Type: "Deployment",
Name: c.Name,
Namespace: c.Namespace,
Spec: spec,
}
}
// ControllerFromStatefulSet creates a controller
func ControllerFromStatefulSet(c appsv1.StatefulSet) Controller {
spec := ControllerSpec{
Template: c.Spec.Template,
}
return Controller{
Type: "StatefulSet",
Name: c.Name,
Namespace: c.Namespace,
Spec: spec,
}
}
-67
View File
@@ -1,67 +0,0 @@
// Copyright 2019 ReactiveOps
//
// 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 validator
import (
conf "github.com/reactiveops/polaris/pkg/config"
"github.com/reactiveops/polaris/pkg/kube"
appsv1 "k8s.io/api/apps/v1"
)
// ValidateDeployment validates a single deployment, returns a PodResult.
func ValidateDeployment(conf conf.Configuration, deploy *appsv1.Deployment) ControllerResult {
pod := deploy.Spec.Template.Spec
podResult := ValidatePod(conf, &pod)
return ControllerResult{
Name: deploy.Name,
Type: "Deployment",
PodResult: podResult,
}
}
// ValidateDeployments validates that each deployment conforms to the Polaris config,
// returns a list of ResourceResults organized by namespace.
func ValidateDeployments(config conf.Configuration, kubeResources *kube.ResourceProvider) (NamespacedResults, error) {
nsResults := NamespacedResults{}
for _, deploy := range kubeResources.Deployments {
deploymentResult := ValidateDeployment(config, &deploy)
nsResults = addResult(deploymentResult, nsResults, deploy.Namespace)
}
return nsResults, nil
}
func addResult(deploymentResult ControllerResult, nsResults NamespacedResults, nsName string) NamespacedResults {
nsResult := &NamespaceResult{}
// If there is already data stored for this namespace name,
// then append to the ResourceResults to the existing data.
switch nsResults[nsName] {
case nil:
nsResult = &NamespaceResult{
Summary: &ResultSummary{},
DeploymentResults: []ControllerResult{},
}
nsResults[nsName] = nsResult
default:
nsResult = nsResults[nsName]
}
nsResult.DeploymentResults = append(nsResult.DeploymentResults, deploymentResult)
nsResult.Summary.appendResults(*deploymentResult.PodResult.Summary)
return nsResults
}
+21 -24
View File
@@ -14,13 +14,14 @@ const (
// ClusterSummary contains Polaris results as well as some high-level stats
type ClusterSummary struct {
Results ResultSummary
Version string
Nodes int
Pods int
Namespaces int
Deployments int
Score uint
Results ResultSummary
Version string
Nodes int
Pods int
Namespaces int
Deployments int
StatefulSets int
Score uint
}
// AuditData contains all the data from a full Polaris audit
@@ -36,16 +37,8 @@ type AuditData struct {
// RunAudit runs a full Polaris audit and returns an AuditData object
func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
// TODO: Validate StatefulSets, DaemonSets, Cron jobs
// in addition to deployments
// TODO: Once we are validating more than deployments,
// we will need to merge the namespaceResults that get returned
// from each validation.
nsResults, err := ValidateDeployments(config, kubeResources)
if err != nil {
return AuditData{}, err
}
nsResults := NamespacedResults{}
ValidateControllers(config, kubeResources, &nsResults)
clusterResults := ResultSummary{}
@@ -54,6 +47,9 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
for _, dr := range nsRes.DeploymentResults {
clusterResults.appendResults(*dr.PodResult.Summary)
}
for _, dr := range nsRes.StatefulSetResults {
clusterResults.appendResults(*dr.PodResult.Summary)
}
}
displayName := config.DisplayName
@@ -68,13 +64,14 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
SourceName: kubeResources.SourceName,
DisplayName: displayName,
ClusterSummary: ClusterSummary{
Version: kubeResources.ServerVersion,
Nodes: len(kubeResources.Nodes),
Pods: len(kubeResources.Pods),
Namespaces: len(kubeResources.Namespaces),
Deployments: len(kubeResources.Deployments),
Results: clusterResults,
Score: clusterResults.Totals.GetScore(),
Version: kubeResources.ServerVersion,
Nodes: len(kubeResources.Nodes),
Pods: len(kubeResources.Pods),
Namespaces: len(kubeResources.Namespaces),
Deployments: len(kubeResources.Deployments),
StatefulSets: len(kubeResources.StatefulSets),
Results: clusterResults,
Score: clusterResults.Totals.GetScore(),
},
NamespacedResults: nsResults,
}
+13 -7
View File
@@ -11,7 +11,7 @@ import (
func TestGetTemplateData(t *testing.T) {
k8s := test.SetupTestAPI()
k8s = test.SetupAddDeploys(k8s, "test")
k8s = test.SetupAddControllers(k8s, "test")
resources, err := kube.CreateResourceProviderFromAPI(k8s, "test")
assert.Equal(t, err, nil, "error should be nil")
@@ -24,19 +24,19 @@ func TestGetTemplateData(t *testing.T) {
sum := ResultSummary{
Totals: CountSummary{
Successes: uint(4),
Warnings: uint(1),
Errors: uint(1),
Successes: uint(8),
Warnings: uint(2),
Errors: uint(2),
},
ByCategory: CategorySummary{},
}
sum.ByCategory["Health Checks"] = &CountSummary{
Successes: uint(0),
Warnings: uint(1),
Errors: uint(1),
Warnings: uint(2),
Errors: uint(2),
}
sum.ByCategory["Resources"] = &CountSummary{
Successes: uint(4),
Successes: uint(8),
Warnings: uint(0),
Errors: uint(0),
}
@@ -47,8 +47,14 @@ func TestGetTemplateData(t *testing.T) {
assert.EqualValues(t, sum, actualAudit.ClusterSummary.Results)
assert.Equal(t, actualAudit.SourceType, "Cluster", "should be from a cluster")
assert.Equal(t, actualAudit.SourceName, "test", "should be from a cluster")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults), "should be equal")
assert.Equal(t, 6, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults), "should be equal")
assert.Equal(t, 6, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
}
+1 -1
View File
@@ -35,7 +35,7 @@ func TestValidatePod(t *testing.T) {
}
k8s := test.SetupTestAPI()
k8s = test.SetupAddDeploys(k8s, "test")
k8s = test.SetupAddControllers(k8s, "test")
pod := test.MockPod()
expectedSum := ResultSummary{
+22 -5
View File
@@ -28,14 +28,31 @@ const (
MessageTypeError MessageType = "error"
)
// NamespaceResult groups container results by parent resource.
type NamespaceResult struct {
Name string
Summary *ResultSummary
DeploymentResults []ControllerResult
StatefulSetResults []ControllerResult
}
// NamespacedResults is a mapping of namespace name to the validation results.
type NamespacedResults map[string]*NamespaceResult
// NamespaceResult groups container results by parent resource.
type NamespaceResult struct {
Name string
Summary *ResultSummary
DeploymentResults []ControllerResult
func (nsResults NamespacedResults) getNamespaceResult(nsName string) *NamespaceResult {
nsResult := &NamespaceResult{}
switch nsResults[nsName] {
case nil:
nsResult = &NamespaceResult{
Summary: &ResultSummary{},
DeploymentResults: []ControllerResult{},
StatefulSetResults: []ControllerResult{},
}
nsResults[nsName] = nsResult
default:
nsResult = nsResults[nsName]
}
return nsResult
}
// CountSummary provides a high level overview of success, warnings, and errors.
+17 -10
View File
@@ -87,19 +87,24 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
var err error
var podResult validator.PodResult
allowed := true
reason := ""
switch req.AdmissionRequest.Kind.Kind {
case "Deployment":
deploy := appsv1.Deployment{}
err = v.decoder.Decode(req, &deploy)
deployResult := validator.ValidateDeployment(v.Config, &deploy)
podResult = deployResult.PodResult
case "Pod":
if req.AdmissionRequest.Kind.Kind == "Pod" {
pod := corev1.Pod{}
err = v.decoder.Decode(req, &pod)
podResult = validator.ValidatePod(v.Config, &pod.Spec)
} else {
var controller validator.Controller
switch req.AdmissionRequest.Kind.Kind {
case "Deployment":
deploy := appsv1.Deployment{}
err = v.decoder.Decode(req, &deploy)
controller = validator.ControllerFromDeployment(deploy)
case "StatefulSet":
statefulSet := appsv1.StatefulSet{}
err = v.decoder.Decode(req, &statefulSet)
controller = validator.ControllerFromStatefulSet(statefulSet)
}
controllerResult := validator.ValidateController(v.Config, controller)
podResult = controllerResult.PodResult
}
if err != nil {
@@ -107,6 +112,8 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
return admission.ErrorResponse(http.StatusBadRequest, err)
}
allowed := true
reason := ""
if podResult.Summary.Totals.Errors > 0 {
allowed = false
reason = getFailureReason(podResult)
+17 -2
View File
@@ -39,17 +39,32 @@ func mockDeploy() appsv1.Deployment {
return d
}
func mockStatefulSet() appsv1.StatefulSet {
p := MockPod()
s := appsv1.StatefulSet{
Spec: appsv1.StatefulSetSpec{
Template: p,
},
}
return s
}
// SetupTestAPI creates a test kube API struct.
func SetupTestAPI() kubernetes.Interface {
return fake.NewSimpleClientset()
}
// SetupAddDeploys creates a mock deployment and adds it to the test clientset.
func SetupAddDeploys(k kubernetes.Interface, namespace string) kubernetes.Interface {
// SetupAddControllers creates mock controllers and adds them to the test clientset.
func SetupAddControllers(k kubernetes.Interface, namespace string) kubernetes.Interface {
d1 := mockDeploy()
_, err := k.AppsV1().Deployments(namespace).Create(&d1)
if err != nil {
fmt.Println(err)
}
s1 := mockStatefulSet()
_, err = k.AppsV1().StatefulSets(namespace).Create(&s1)
if err != nil {
fmt.Println(err)
}
return k
}