Skip to content

Commit

Permalink
feat: cluster endpoint validator (#19)
Browse files Browse the repository at this point in the history
* Add endpoint validator

Signed-off-by: Eytan Avisror <[email protected]>

* Update README.md

Signed-off-by: Eytan Avisror <[email protected]>

* Update validator_test.go

Signed-off-by: Eytan Avisror <[email protected]>
  • Loading branch information
Eytan Avisror authored Mar 7, 2022
1 parent 25fe93e commit 183ffc7
Show file tree
Hide file tree
Showing 11 changed files with 659 additions and 164 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ spec:
successThreshold: 5
failureThreshold: 10

# Endpoints to validate
endpoints:
# You can validate cluster URIs
cluster:
- name: Component Validation
uri: "/readyz?include=etcd&verbose"
required: true
```
More examples [here](docs/examples).
Expand Down
39 changes: 8 additions & 31 deletions cmd/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ import (

"github.com/keikoproj/cluster-validator/pkg/client"

"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"

"github.com/spf13/cobra"
)

Expand All @@ -39,21 +35,26 @@ var validateCmd = &cobra.Command{
log.Fatalf("failed to parse validation spec from file: %v", err)
}

c, err := GetKubernetesDynamicClient()
c, err := client.GetKubernetesDynamicClient()
if err != nil {
log.Fatalf("failed to create dynamic client: %v", err)
}

r, err := client.GetRESTClient()
if err != nil {
log.Fatalf("failed to create REST client: %v", err)
}

if logLevel > 0 && logLevel <= 6 {
log.SetLevel(log.Level(logLevel))
} else {
log.SetLevel(log.Level(defaultLoggingLevel))
}

v := client.NewValidator(c, spec)
v := client.NewValidator(c, spec, r)
err = v.Validate()
if err != nil {
log.Fatalf("validation failed: %v", err)
log.Fatalf("validation failed: %v", client.ToValidationError(err).Message)
}
},
}
Expand All @@ -68,27 +69,3 @@ func init() {
validateCmd.Flags().StringVar(&specFile, "filename", "", "Path to cluster validation manifest file (yaml)")
validateCmd.Flags().Uint32Var(&logLevel, "verbosity", defaultLoggingLevel, "Logging verbosity 1-6")
}

func GetKubernetesConfig() (*rest.Config, error) {
var config *rest.Config
config, err := rest.InClusterConfig()
if err != nil {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
clientCfg := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
return clientCfg.ClientConfig()
}
return config, nil
}

func GetKubernetesDynamicClient() (dynamic.Interface, error) {
var config *rest.Config
config, err := GetKubernetesConfig()
if err != nil {
return nil, err
}
client, err := dynamic.NewForConfig(config)
if err != nil {
return nil, err
}
return client, nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ require (
k8s.io/api v0.21.3
k8s.io/apimachinery v0.21.3
k8s.io/client-go v0.21.3
k8s.io/kubectl v0.21.3
)
149 changes: 149 additions & 0 deletions go.sum

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions pkg/api/v1alpha1/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ func (c *ClusterValidation) GetConfiguration() ValidationConfiguration {

type ClusterValidationSpec struct {
Resources []ClusterResource `json:"resources"`
Endpoints EndpointsSpec `json:"endpoints"`
Configuration ValidationConfiguration `json:"configuration"`
}

type EndpointsSpec struct {
Cluster []ClusterEndpoint `json:"cluster"`
HTTP []HTTPEndpoint `json:"http"`
}

type ValidationConfiguration struct {
SuccessThreshold int `json:"successThreshold"`
FailureThreshold int `json:"failureThreshold"`
Expand Down
158 changes: 154 additions & 4 deletions pkg/api/v1alpha1/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,31 @@ package v1alpha1

import (
"strings"
"time"

log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
)

type ClusterResource struct {
Name string `json:"name"`
APIVersion string `json:"apiVersion"`
Required bool `json:"required"`
type ClusterEndpoint struct {
Name string `json:"name"`
Required bool `json:"required"`
Configuration ValidationConfiguration `json:"configuration,omitempty"`
URI string `json:"uri,omitempty"`
}

type HTTPEndpoint struct {
Name string `json:"name"`
Required bool `json:"required"`
Configuration ValidationConfiguration `json:"configuration,omitempty"`
URL string `json:"url,omitempty"`
Codes []int `json:"codes,omitempty"`
}

type ClusterResource struct {
Name string `json:"name"`
APIVersion string `json:"apiVersion"`
Required bool `json:"required"`
Configuration ValidationConfiguration `json:"configuration,omitempty"`
Namespaces *SelectionScope `json:"namespaces,omitempty"`
Names *SelectionScope `json:"names,omitempty"`
Expand All @@ -31,10 +47,144 @@ type ClusterResource struct {
Conditions []ResourceCondition `json:"conditions,omitempty"`
}

func (r *ClusterResource) SuccessThreshold(globalCfg ValidationConfiguration) int {
var (
resourceCfg = r.GetConfiguration()
)
if resourceCfg.SuccessThreshold > 0 {
return resourceCfg.SuccessThreshold
}
return globalCfg.SuccessThreshold
}

func (r *ClusterResource) FailureThreshold(globalCfg ValidationConfiguration) int {
var (
resourceCfg = r.GetConfiguration()
)
if resourceCfg.FailureThreshold > 0 {
return resourceCfg.FailureThreshold
}
return globalCfg.FailureThreshold
}

func (r *HTTPEndpoint) SuccessThreshold(globalCfg ValidationConfiguration) int {
var (
resourceCfg = r.GetConfiguration()
)
if resourceCfg.SuccessThreshold > 0 {
return resourceCfg.SuccessThreshold
}
return globalCfg.SuccessThreshold
}

func (r *HTTPEndpoint) FailureThreshold(globalCfg ValidationConfiguration) int {
var (
resourceCfg = r.GetConfiguration()
)
if resourceCfg.FailureThreshold > 0 {
return resourceCfg.FailureThreshold
}
return globalCfg.FailureThreshold
}

func (r *ClusterEndpoint) SuccessThreshold(globalCfg ValidationConfiguration) int {
var (
resourceCfg = r.GetConfiguration()
)
if resourceCfg.SuccessThreshold > 0 {
return resourceCfg.SuccessThreshold
}
return globalCfg.SuccessThreshold
}

func (r *ClusterEndpoint) FailureThreshold(globalCfg ValidationConfiguration) int {
var (
resourceCfg = r.GetConfiguration()
)
if resourceCfg.FailureThreshold > 0 {
return resourceCfg.FailureThreshold
}
return globalCfg.FailureThreshold
}

func (c *ClusterResource) GetConfiguration() ValidationConfiguration {
return c.Configuration
}

func (c *HTTPEndpoint) GetConfiguration() ValidationConfiguration {
return c.Configuration
}

func (c *ClusterEndpoint) GetConfiguration() ValidationConfiguration {
return c.Configuration
}

func (r *ClusterResource) Interval(globalCfg ValidationConfiguration) time.Duration {
var (
resourceCfg = r.GetConfiguration()
)

if resourceCfg.Interval != "" {
d, err := time.ParseDuration(resourceCfg.Interval)
if err != nil {
log.Warnf("failed to parse duration '%v', using default of 1s", resourceCfg.Interval)
return time.Second * 1
}
return d
} else {
d, err := time.ParseDuration(globalCfg.Interval)
if err != nil {
log.Warnf("failed to parse duration '%v', using default of 1s", globalCfg.Interval)
return time.Second * 1
}
return d
}
}

func (r *ClusterEndpoint) Interval(globalCfg ValidationConfiguration) time.Duration {
var (
resourceCfg = r.GetConfiguration()
)

if resourceCfg.Interval != "" {
d, err := time.ParseDuration(resourceCfg.Interval)
if err != nil {
log.Warnf("failed to parse duration '%v', using default of 1s", resourceCfg.Interval)
return time.Second * 1
}
return d
} else {
d, err := time.ParseDuration(globalCfg.Interval)
if err != nil {
log.Warnf("failed to parse duration '%v', using default of 1s", globalCfg.Interval)
return time.Second * 1
}
return d
}
}

func (r *HTTPEndpoint) Interval(globalCfg ValidationConfiguration) time.Duration {
var (
resourceCfg = r.GetConfiguration()
)

if resourceCfg.Interval != "" {
d, err := time.ParseDuration(resourceCfg.Interval)
if err != nil {
log.Warnf("failed to parse duration '%v', using default of 1s", resourceCfg.Interval)
return time.Second * 1
}
return d
} else {
d, err := time.ParseDuration(globalCfg.Interval)
if err != nil {
log.Warnf("failed to parse duration '%v', using default of 1s", globalCfg.Interval)
return time.Second * 1
}
return d
}
}

type FieldSelector struct {
Path string `json:"path"`
Values []string `json:"values"`
Expand Down
65 changes: 65 additions & 0 deletions pkg/client/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ package client

import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"

"github.com/gobwas/glob"
"github.com/keikoproj/cluster-validator/pkg/api/v1alpha1"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/jsonpath"
"k8s.io/kubectl/pkg/scheme"
)

func groupVersionResource(groupVersion, resource string) schema.GroupVersionResource {
Expand Down Expand Up @@ -131,3 +137,62 @@ func namespacedName(r unstructured.Unstructured) string {
}
return fmt.Sprintf("%v/%v", r.GetNamespace(), r.GetName())
}

func rawGet(restClient *rest.RESTClient, uri string) (*bytes.Buffer, error) {
r := restClient.Get().RequestURI(uri)
stream, err := r.Stream(context.TODO())
if err != nil {
return nil, errors.Wrap(err, "failed to stream call")
}
defer stream.Close()

buf := new(bytes.Buffer)
_, err = buf.ReadFrom(stream)
if err != nil {
return nil, errors.Wrap(err, "failed to read stream")
}
return buf, nil
}

func GetRESTClient() (*rest.RESTClient, error) {
config, err := GetKubernetesConfig()
if err != nil {
return nil, err
}

config.ContentConfig.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"}
config.APIPath = "/apis"
config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
config.UserAgent = rest.DefaultKubernetesUserAgent()

client, err := rest.RESTClientFor(config)
if err != nil {
return nil, err
}

return client, nil
}

func GetKubernetesConfig() (*rest.Config, error) {
var config *rest.Config
config, err := rest.InClusterConfig()
if err != nil {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
clientCfg := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
return clientCfg.ClientConfig()
}
return config, nil
}

func GetKubernetesDynamicClient() (dynamic.Interface, error) {
var config *rest.Config
config, err := GetKubernetesConfig()
if err != nil {
return nil, err
}
client, err := dynamic.NewForConfig(config)
if err != nil {
return nil, err
}
return client, nil
}
14 changes: 14 additions & 0 deletions pkg/client/test-files/cluster_endpoint_validation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: v1alpha1
kind: ClusterValidator
metadata:
name: field-validation
spec:
configuration:
successThreshold: 3
failureThreshold: 3
interval: 1ms
endpoints:
cluster:
- name: ETCD Validation
uri: "/readyz?include=etcd&verbose"
required: true
Loading

0 comments on commit 183ffc7

Please sign in to comment.