Skip to content

Commit

Permalink
Add support for stateful sets (#145)
Browse files Browse the repository at this point in the history
  • Loading branch information
rbren authored Jun 13, 2019
1 parent e030b61 commit ebfb4ea
Show file tree
Hide file tree
Showing 15 changed files with 301 additions and 168 deletions.
5 changes: 5 additions & 0 deletions pkg/dashboard/assets/css/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 9 additions & 8 deletions pkg/dashboard/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
7 changes: 7 additions & 0 deletions pkg/dashboard/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
6 changes: 4 additions & 2 deletions pkg/dashboard/templates/dashboard.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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">
Expand Down
90 changes: 49 additions & 41 deletions pkg/kube/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type ResourceProvider struct {
SourceType string
Nodes []corev1.Node
Deployments []appsv1.Deployment
StatefulSets []appsv1.StatefulSet
Namespaces []corev1.Namespace
Pods []corev1.Pod
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}
6 changes: 5 additions & 1 deletion pkg/kube/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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")

Expand All @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions pkg/kube/test_files/test_1/stateful_set.yaml
Original file line number Diff line number Diff line change
@@ -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 changes: 96 additions & 0 deletions pkg/validator/controller.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
Loading

0 comments on commit ebfb4ea

Please sign in to comment.