diff --git a/pkg/report/report.go b/pkg/report/report.go new file mode 100644 index 00000000..776619c1 --- /dev/null +++ b/pkg/report/report.go @@ -0,0 +1,53 @@ +package report + +import ( + "fmt" + "strings" + + "github.com/reactiveops/fairwinds/pkg/types" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" +) + +var log = logf.Log.WithName("Fairwinds report") + +// Results contains the validation check results. +type Results struct { + Pass bool + FailMsg string + Containers []types.ContainerResults + InitContainers []types.ContainerResults +} + +// Format structures the validation results to return back to k8s API. +func (r *Results) Format() (bool, string) { + var sb strings.Builder + + for _, container := range r.Containers { + if len(container.Failures) == 0 { + r.Pass = true + } + + r.Pass = false + s := fmt.Sprintf("\nContainer: %s\n Failure/s:\n", container.Name) + sb.WriteString(s) + for _, failure := range container.Failures { + sb.WriteString(failure.Reason()) + } + } + + for _, container := range r.InitContainers { + if len(container.Failures) == 0 && r.Pass == true { + return r.Pass, r.FailMsg + } + + r.Pass = false + s := fmt.Sprintf("\nInitContainer: %s\n Failure/s:\n", container.Name) + sb.WriteString(s) + for _, failure := range container.Failures { + sb.WriteString(failure.Reason()) + } + } + + r.FailMsg = sb.String() + return r.Pass, r.FailMsg +} diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 00000000..15a9f422 --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,42 @@ +package types + +import ( + "fmt" +) + +// Failure contains information about the failing validation. +type Failure struct { + Name string + Expected string + Actual string +} + +// NewFailure is a factory function for a Failure. +func NewFailure(name, expected, actual string) *Failure { + return &Failure{ + Name: name, + Expected: expected, + Actual: actual, + } +} + +// Reason returns a string that describes the reason for a Failure. +func (f *Failure) Reason() string { + return fmt.Sprintf("- %s: Expected: %s, Actual: %s.\n", + f.Name, + f.Expected, + f.Actual, + ) +} + +// ContainerResults has the results of the validation checks for containers. +type ContainerResults struct { + Name string + Failures []Failure +} + +// AddFailure creates a new Failure and adds it to ContainerResults. +func (c *ContainerResults) AddFailure(name, expected, actual string) { + f := NewFailure(name, expected, actual) + c.Failures = append(c.Failures, *f) +} diff --git a/pkg/validator/container.go b/pkg/validator/container.go index fe50f697..5b6d0c2f 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -15,35 +15,26 @@ package validator import ( - "fmt" "strings" conf "github.com/reactiveops/fairwinds/pkg/config" + "github.com/reactiveops/fairwinds/pkg/types" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" ) -type containerResults struct { - Name string - Reason string -} - -func validateContainer(conf conf.Configuration, container corev1.Container) containerResults { - var sb strings.Builder - results := containerResults{ +func validateContainer(conf conf.Configuration, container corev1.Container) types.ContainerResults { + results := types.ContainerResults{ Name: container.Name, } + results = resources(conf.Resources, container, results) + results = probes(conf.Resources, container, results) + results = tag(conf.Resources, container, results) - sb.WriteString(resources(conf.Resources, container)) - sb.WriteString(probes(container)) - sb.WriteString(tag(container)) - - results.Reason = sb.String() return results } -func resources(conf conf.ResourceRequestsAndLimits, c corev1.Container) string { - var sb strings.Builder +func resources(conf conf.ResourceRequestsAndLimits, c corev1.Container, results types.ContainerResults) types.ContainerResults { confCPUmin, err := resource.ParseQuantity(conf.Requests["cpu"].Min) if err != nil { log.Error(err, "cpu min parse quan") @@ -53,53 +44,47 @@ func resources(conf conf.ResourceRequestsAndLimits, c corev1.Container) string { // log.Error(err, "cpu max parse quan") // } - ctrRequests := c.Resources.Requests.Cpu().MilliValue() - configMin := confCPUmin.MilliValue() - if ctrRequests < configMin { - s := fmt.Sprintf("- CPU requests are too low. Expected greater than: %d, Actual: %d.\n", configMin, ctrRequests) - sb.WriteString(s) + containerRequests := c.Resources.Requests.Cpu() + if containerRequests.MilliValue() < confCPUmin.MilliValue() { + results.AddFailure("CPU requests", confCPUmin.String(), containerRequests.String()) } if c.Resources.Requests.Memory().IsZero() { - sb.WriteString("- Memory requests are not set.\n") + results.AddFailure("Memory requests", "placeholder", "placeholder") } if c.Resources.Limits.Cpu().IsZero() { - sb.WriteString("- CPU limits are not set.\n") + results.AddFailure("CPU limits", "placeholder", "placeholder") } if c.Resources.Limits.Memory().IsZero() { - sb.WriteString("- Memory limits are not set.\n") + results.AddFailure("Memory limits", "placeholder", "placeholder") } - return sb.String() + return results } -func probes(c corev1.Container) string { - var sb strings.Builder +func probes(conf conf.ResourceRequestsAndLimits, c corev1.Container, results types.ContainerResults) types.ContainerResults { if c.ReadinessProbe == nil { - sb.WriteString("- Readiness Probe is not set.\n") + results.AddFailure("Readiness Probe", "placeholder", "placeholder") } if c.LivenessProbe == nil { - sb.WriteString("- Liveness Probe is not set.\n") + results.AddFailure("Liveness Probe", "placeholder", "placeholder") } - return sb.String() + return results } -func tag(c corev1.Container) string { - var sb strings.Builder +func tag(conf conf.ResourceRequestsAndLimits, c corev1.Container, results types.ContainerResults) types.ContainerResults { img := strings.Split(c.Image, ":") if len(img) == 1 || img[1] == "latest" { - sb.WriteString("- Image tag is latest.\n") + results.AddFailure("Image Tag", "not latest", "latest") } - - return sb.String() + return results } -func hostPort(c corev1.Container) string { - var sb strings.Builder +func hostPort(conf conf.ResourceRequestsAndLimits, c corev1.Container, results types.ContainerResults) types.ContainerResults { for _, port := range c.Ports { if port.HostPort != 0 { - sb.WriteString("- Host Port set.\n") + results.AddFailure("Host port", "placeholder", "placeholder") } } - return sb.String() + return results } diff --git a/pkg/validator/pod.go b/pkg/validator/pod.go index f2e7635d..23e67467 100644 --- a/pkg/validator/pod.go +++ b/pkg/validator/pod.go @@ -16,11 +16,10 @@ package validator import ( "context" - "fmt" "net/http" - "strings" conf "github.com/reactiveops/fairwinds/pkg/config" + "github.com/reactiveops/fairwinds/pkg/report" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/runtime/inject" @@ -50,31 +49,28 @@ func (v *PodValidator) Handle(ctx context.Context, req types.Request) types.Resp return admission.ErrorResponse(http.StatusBadRequest, err) } - allowed, reason := validatePods(v.Config, pod) + results := validatePods(v.Config, pod, report.Results{}) + allowed, reason := results.Format() return admission.ValidationResponse(allowed, reason) } -func validatePods(conf conf.Configuration, pod *corev1.Pod) (bool, string) { - var sb strings.Builder - allowed := true +func validatePods(conf conf.Configuration, pod *corev1.Pod, results report.Results) report.Results { for _, container := range pod.Spec.InitContainers { - c := validateContainer(conf, container) - if c.Reason != "" { - sb.WriteString(fmt.Sprintf("\nContainer Name: %s\n%s", c.Name, c.Reason)) - allowed = false - } + results.InitContainers = append( + results.InitContainers, + validateContainer(conf, container), + ) } for _, container := range pod.Spec.Containers { - c := validateContainer(conf, container) - if c.Reason != "" { - sb.WriteString(fmt.Sprintf("\nName: %s\n%s", c.Name, c.Reason)) - allowed = false - } + results.Containers = append( + results.Containers, + validateContainer(conf, container), + ) } - return allowed, sb.String() + return results } // PodValidator implements inject.Client.