diff --git a/README.md b/README.md index 563995304..10f2f7a88 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,10 @@ helm upgrade --install fairwinds charts/fairwinds/ --namespace fairwinds --recre kubectl port-forward --namespace fairwinds svc/fairwinds-fairwinds-dashboard 8080:80 & open http://localhost:8080 ``` + +## Run tests +``` +go list ./ | grep -v vendor | xargs golint -set_exit_status +go list ./ | grep -v vendor | xargs go vet +go test ./pkg/... -v -coverprofile cover.out +``` diff --git a/main.go b/main.go index ac08edea2..882c3e60f 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,8 @@ package main import ( + "bytes" + "encoding/json" "flag" "fmt" "io/ioutil" @@ -26,7 +28,9 @@ import ( conf "github.com/reactiveops/fairwinds/pkg/config" "github.com/reactiveops/fairwinds/pkg/dashboard" "github.com/reactiveops/fairwinds/pkg/kube" + "github.com/reactiveops/fairwinds/pkg/validator" fwebhook "github.com/reactiveops/fairwinds/pkg/webhook" + "gopkg.in/yaml.v2" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apitypes "k8s.io/apimachinery/pkg/types" @@ -43,8 +47,10 @@ var log = logf.Log.WithName("fairwinds") func main() { dashboard := flag.Bool("dashboard", false, "Runs the webserver for Fairwinds dashboard.") webhook := flag.Bool("webhook", false, "Runs the webhook webserver.") + audit := flag.Bool("audit", false, "Runs a one-time audit.") dashboardPort := flag.Int("dashboard-port", 8080, "Port for the dashboard webserver") webhookPort := flag.Int("webhook-port", 9876, "Port for the webhook webserver") + auditDestination := flag.String("audit-destination", "", "Destination URL to send audit results (prints to stdout if unspecified)") var disableWebhookConfigInstaller bool flag.BoolVar(&disableWebhookConfigInstaller, "disable-webhook-config-installer", false, @@ -58,17 +64,16 @@ func main() { os.Exit(1) } - if *webhook { - startWebhookServer(c, disableWebhookConfigInstaller, *webhookPort) + if !*dashboard && !*webhook && !*audit { + *audit = true } - if *dashboard { + if *webhook { + startWebhookServer(c, disableWebhookConfigInstaller, *webhookPort) + } else if *dashboard { startDashboardServer(c, *dashboardPort) - } - - if !*dashboard && !*webhook { - glog.Println("Must specify either -webhook, -dashboard, or both") - os.Exit(1) + } else if *audit { + runAudit(c, *auditDestination) } } @@ -154,3 +159,37 @@ func startWebhookServer(c conf.Configuration, disableWebhookConfigInstaller bool os.Exit(1) } } + +func runAudit(c conf.Configuration, destination string) { + k, _ := kube.CreateKubeAPI() + auditData, err := validator.RunAudit(c, k) + if err != nil { + panic(err) + } + + if destination != "" { + jsonData, err := json.Marshal(auditData) + if err != nil { + panic(err) + } + + req, err := http.NewRequest("POST", destination, bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + glog.Println(string(body)) + } else { + y, err := yaml.Marshal(auditData) + if err != nil { + panic(err) + } + fmt.Println(string(y)) + } +} diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go index 3ac43a2de..0bcf82299 100644 --- a/pkg/dashboard/dashboard.go +++ b/pkg/dashboard/dashboard.go @@ -18,15 +18,9 @@ const ( TemplateFile = "pkg/dashboard/templates/" + TemplateName ) -// TemplateData represents data in a format that's template friendly. -type TemplateData struct { - ClusterSummary *validator.ResultSummary - NamespacedResults validator.NamespacedResults -} - // MainHandler gets template data and renders the dashboard with it. func MainHandler(w http.ResponseWriter, r *http.Request, c conf.Configuration, kubeAPI *kube.API) { - templateData, err := getTemplateData(c, kubeAPI) + templateData, err := validator.RunAudit(c, kubeAPI) if err != nil { http.Error(w, "Error Fetching Deployments", 500) return @@ -62,7 +56,7 @@ func MainHandler(w http.ResponseWriter, r *http.Request, c conf.Configuration, k // EndpointHandler gets template data and renders json with it. func EndpointHandler(w http.ResponseWriter, r *http.Request, c conf.Configuration, kubeAPI *kube.API) { - templateData, err := getTemplateData(c, kubeAPI) + templateData, err := validator.RunAudit(c, kubeAPI) if err != nil { http.Error(w, "Error Fetching Deployments", 500) return @@ -72,36 +66,3 @@ func EndpointHandler(w http.ResponseWriter, r *http.Request, c conf.Configuratio w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(templateData) } - -func getTemplateData(config conf.Configuration, kubeAPI *kube.API) (TemplateData, error) { - - // TODO: Once we are validating more than deployments, - // we will need to merge the namespaceResults that get returned - // from each validation. - nsResults, err := validator.ValidateDeploys(config, kubeAPI) - if err != nil { - return TemplateData{}, err - } - - var clusterSuccesses, clusterErrors, clusterWarnings uint - - // Aggregate all summary counts to get a clusterwide count. - for _, nsRes := range nsResults { - for _, rr := range nsRes.Results { - clusterErrors += rr.Summary.Errors - clusterWarnings += rr.Summary.Warnings - clusterSuccesses += rr.Summary.Successes - } - } - - templateData := TemplateData{ - ClusterSummary: &validator.ResultSummary{ - Errors: clusterErrors, - Warnings: clusterWarnings, - Successes: clusterSuccesses, - }, - NamespacedResults: nsResults, - } - - return templateData, nil -} diff --git a/pkg/dashboard/dashboard_test.go b/pkg/dashboard/dashboard_test.go deleted file mode 100644 index 294db6a16..000000000 --- a/pkg/dashboard/dashboard_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package dashboard - -import ( - "testing" - - conf "github.com/reactiveops/fairwinds/pkg/config" - "github.com/reactiveops/fairwinds/pkg/validator" - "github.com/reactiveops/fairwinds/test" - "github.com/stretchr/testify/assert" -) - -func TestGetTemplateData(t *testing.T) { - k8s := test.SetupTestAPI() - k8s = test.SetupAddDeploys(k8s, "test") - - c := conf.Configuration{ - HealthChecks: conf.HealthChecks{ - ReadinessProbeMissing: conf.SeverityError, - LivenessProbeMissing: conf.SeverityWarning, - }, - } - - sum := &validator.ResultSummary{ - Successes: uint(4), - Warnings: uint(1), - Errors: uint(1), - } - - actualTmplData, _ := getTemplateData(c, k8s) - - assert.EqualValues(t, actualTmplData.ClusterSummary, sum) - assert.Equal(t, len(actualTmplData.NamespacedResults["test"].Results), 1, "should be equal") - assert.Equal(t, len(actualTmplData.NamespacedResults["test"].Results[0].PodResults), 1, "should be equal") - assert.Equal(t, len(actualTmplData.NamespacedResults["test"].Results[0].PodResults[0].ContainerResults), 1, "should be equal") - assert.Equal(t, len(actualTmplData.NamespacedResults["test"].Results[0].PodResults[0].ContainerResults[0].Messages), 6, "should be equal") -} diff --git a/pkg/validator/fullaudit.go b/pkg/validator/fullaudit.go new file mode 100644 index 000000000..9677ffd00 --- /dev/null +++ b/pkg/validator/fullaudit.go @@ -0,0 +1,47 @@ +package validator + +import ( + conf "github.com/reactiveops/fairwinds/pkg/config" + "github.com/reactiveops/fairwinds/pkg/kube" +) + +// AuditData contains all the data from a full Fairwinds audit +type AuditData struct { + ClusterSummary ResultSummary + NamespacedResults NamespacedResults +} + +// RunAudit runs a full Fairwinds audit and returns an AuditData object +func RunAudit(config conf.Configuration, kubeAPI *kube.API) (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 := ValidateDeploys(config, kubeAPI) + if err != nil { + return AuditData{}, err + } + + var clusterSuccesses, clusterErrors, clusterWarnings uint + + // Aggregate all summary counts to get a clusterwide count. + for _, nsRes := range nsResults { + for _, rr := range nsRes.Results { + clusterErrors += rr.Summary.Errors + clusterWarnings += rr.Summary.Warnings + clusterSuccesses += rr.Summary.Successes + } + } + + auditData := AuditData{ + ClusterSummary: ResultSummary{ + Errors: clusterErrors, + Warnings: clusterWarnings, + Successes: clusterSuccesses, + }, + NamespacedResults: nsResults, + } + return auditData, nil +} diff --git a/pkg/validator/fullaudit_test.go b/pkg/validator/fullaudit_test.go new file mode 100644 index 000000000..ca4618e6b --- /dev/null +++ b/pkg/validator/fullaudit_test.go @@ -0,0 +1,36 @@ +package validator + +import ( + "testing" + + conf "github.com/reactiveops/fairwinds/pkg/config" + "github.com/reactiveops/fairwinds/test" + "github.com/stretchr/testify/assert" +) + +func TestGetTemplateData(t *testing.T) { + k8s := test.SetupTestAPI() + k8s = test.SetupAddDeploys(k8s, "test") + + c := conf.Configuration{ + HealthChecks: conf.HealthChecks{ + ReadinessProbeMissing: conf.SeverityError, + LivenessProbeMissing: conf.SeverityWarning, + }, + } + + sum := ResultSummary{ + Successes: uint(4), + Warnings: uint(1), + Errors: uint(1), + } + + actualAudit, err := RunAudit(c, k8s) + assert.Equal(t, err, nil, "error should be nil") + + assert.EqualValues(t, actualAudit.ClusterSummary, sum) + assert.Equal(t, len(actualAudit.NamespacedResults["test"].Results), 1, "should be equal") + assert.Equal(t, len(actualAudit.NamespacedResults["test"].Results[0].PodResults), 1, "should be equal") + assert.Equal(t, len(actualAudit.NamespacedResults["test"].Results[0].PodResults[0].ContainerResults), 1, "should be equal") + assert.Equal(t, len(actualAudit.NamespacedResults["test"].Results[0].PodResults[0].ContainerResults[0].Messages), 6, "should be equal") +}