Skip to content

Commit

Permalink
feat: add sink interface, Slack sink (#84)
Browse files Browse the repository at this point in the history
* feat: add Slack sink

Signed-off-by: Tyler Gillson <tyler.gillson@gmail.com>

* docs: add sink section to README

Signed-off-by: Tyler Gillson <tyler.gillson@gmail.com>

* docs: add images to README.md

Signed-off-by: Tyler Gillson <tyler.gillson@gmail.com>

---------

Signed-off-by: Tyler Gillson <tyler.gillson@gmail.com>
  • Loading branch information
TylerGillson authored Nov 9, 2023
1 parent 3b5168e commit dac2c3a
Show file tree
Hide file tree
Showing 28 changed files with 529 additions and 48 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build the manager binary
FROM golang:alpine3.17 AS builder
FROM --platform=$TARGETPLATFORM golang:alpine3.17 AS builder
ARG TARGETOS
ARG TARGETARCH

Expand Down Expand Up @@ -32,7 +32,7 @@ RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o ma

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot AS production
FROM --platform=$TARGETPLATFORM gcr.io/distroless/static:nonroot AS production
WORKDIR /
COPY --from=builder /workspace/manager .
COPY --from=builder /workspace/helm .
Expand Down
9 changes: 1 addition & 8 deletions Dockerfile.devspace
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build the manager binary
FROM golang:alpine3.17 AS builder
FROM --platform=$TARGETPLATFORM golang:alpine3.17 AS builder
ARG TARGETOS
ARG TARGETARCH

Expand All @@ -24,10 +24,3 @@ COPY pkg/ pkg/

RUN curl -s https://get.helm.sh/helm-v3.10.1-linux-amd64.tar.gz | tar -xzf - && \
mv linux-amd64/helm . && rm -rf linux-amd64

# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# Image URL to use all building/pushing image targets
IMG ?= quay.io/spectrocloud-labs/validator:latest

GOARCH ?= $(shell go env GOARCH)

# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
Expand Down Expand Up @@ -77,7 +79,7 @@ run: manifests generate fmt vet ## Run a controller from your host.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
.PHONY: docker-build
docker-build: test ## Build docker image with the manager.
$(CONTAINER_TOOL) build -t ${IMG} .
$(CONTAINER_TOOL) build -t ${IMG} . --platform linux/$(GOARCH)

.PHONY: docker-push
docker-push: ## Push docker image with the manager.
Expand Down Expand Up @@ -141,7 +143,7 @@ CHART_VERSION=v0.0.1 # x-release-please-version
CONTROLLER_TOOLS_VERSION ?= v0.12.0
ENVTEST_K8S_VERSION = 1.27.1
HELM_VERSION=v3.10.1
KUSTOMIZE_VERSION ?= v5.0.1
KUSTOMIZE_VERSION ?= v5.2.1

.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading.
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ helm repo update
helm install validator validator/validator -n validator --create-namespace
```

## Sinks
Validator can be configured to emit updates to various event sinks whenever a `ValidationResult` is created or updated. See configuration details below for each supported sink.

### Slack
1. Go to https://api.slack.com/apps and click **Create New App**, then select **From scratch**. Pick an App Name and Slack Workspace, then click **Create App**.

<img src="https://github.com/spectrocloud-labs/validator/assets/1795270/58cbb5a0-12a4-4a83-a0dd-20ae87a8105d" width="500">

2. Go to `OAuth & Permissions` and copy the `Bot User OAuth Token` under the `OAuth Tokens for Your Workspace` section. Save it somewhere for later. Scroll down to `Scopes` and click **Add an OAuth Scope**. Enable the `chat:write` scope for your bot.

<img src="https://github.com/spectrocloud-labs/validator/assets/1795270/7b4d80be-5799-497a-9a4b-480793b26d59" width="500">

3. Find and/or create a channel in Slack and note its Channel ID (at the very bottom of the model when you view channel details). Add the bot you just created to the channel via `View channel details > Integrations > Apps > Add apps`.

<img src="https://github.com/spectrocloud-labs/validator/assets/1795270/a78c852c-7aeb-41a4-aa76-6afbe9b2ec81" width="500">

4. Install validator and/or upgrade your validator Helm release, configuring `values.sink` accordingly.

## Getting Started
You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster.
**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows).
Expand Down
51 changes: 51 additions & 0 deletions api/v1alpha1/validationresult_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package v1alpha1

import (
"reflect"
"testing"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestHash(t *testing.T) {
cs := []struct {
name string
validationResult ValidationResult
expectedHash string
}{
{
name: "Pass",
validationResult: ValidationResult{
ObjectMeta: metav1.ObjectMeta{
UID: "1",
},
Spec: ValidationResultSpec{
Plugin: "AWS",
},
Status: ValidationResultStatus{
State: ValidationSucceeded,
SinkState: SinkEmitSucceeded,
Conditions: []ValidationCondition{
{
ValidationType: "foo",
ValidationRule: "bar",
Message: "baz",
Details: []string{"detail"},
Failures: []string{"failure"},
Status: corev1.ConditionTrue,
LastValidationTime: metav1.Now(),
},
},
},
},
expectedHash: "mCyJwAeP5yOG82mDw8Yy1Q==",
},
}
for _, c := range cs {
hash := c.validationResult.Hash()
if !reflect.DeepEqual(hash, c.expectedHash) {
t.Errorf("expected (%s), got (%s)", c.expectedHash, hash)
}
}
}
35 changes: 32 additions & 3 deletions api/v1alpha1/validationresult_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ limitations under the License.
package v1alpha1

import (
"time"
"crypto"
"encoding/base64"
"fmt"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -36,9 +38,18 @@ const (
ValidationSucceeded ValidationState = "Succeeded"
)

type SinkState string

const (
SinkEmitNone SinkState = "N/A"
SinkEmitFailed SinkState = "Failed"
SinkEmitSucceeded SinkState = "Succeeded"
)

// ValidationResultStatus defines the observed state of ValidationResult
type ValidationResultStatus struct {
State ValidationState `json:"state"`
State ValidationState `json:"state"`
SinkState SinkState `json:"sinkState,omitempty"`

// +optional
// +patchMergeKey=type
Expand Down Expand Up @@ -68,7 +79,7 @@ func DefaultValidationCondition() ValidationCondition {
return ValidationCondition{
Details: make([]string, 0),
Status: corev1.ConditionTrue,
LastValidationTime: metav1.Time{Time: time.Now()},
LastValidationTime: metav1.Now(),
}
}

Expand All @@ -87,6 +98,24 @@ type ValidationResult struct {
Status ValidationResultStatus `json:"status,omitempty"`
}

func (r *ValidationResult) Hash() string {
digester := crypto.MD5.New()

fmt.Fprint(digester, r.ObjectMeta.UID)
fmt.Fprint(digester, r.Spec)
fmt.Fprint(digester, r.Status.State)
fmt.Fprint(digester, r.Status.SinkState)

if len(r.Status.Conditions) > 0 {
c := r.Status.Conditions[0].DeepCopy()
c.LastValidationTime = metav1.Time{}
fmt.Fprint(digester, c)
}

hash := digester.Sum(nil)
return base64.StdEncoding.EncodeToString(hash)
}

//+kubebuilder:object:root=true

// ValidationResultList contains a list of ValidationResult
Expand Down
8 changes: 8 additions & 0 deletions api/v1alpha1/validatorconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ import (
// ValidatorConfigSpec defines the desired state of ValidatorConfig
type ValidatorConfigSpec struct {
Plugins []HelmRelease `json:"plugins,omitempty"`
Sink *Sink `json:"sink,omitempty"`
}

type Sink struct {
// +kubebuilder:validation:Enum=slack
Type string `json:"type"`
// Name of a K8s secret containing configuration details for the sink
SecretName string `json:"secretName"`
}

type HelmRelease struct {
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
Expand Down Expand Up @@ -97,6 +98,8 @@ spec:
- validationType
type: object
type: array
sinkState:
type: string
state:
type: string
required:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
Expand Down Expand Up @@ -56,6 +57,20 @@ spec:
- values
type: object
type: array
sink:
properties:
secretName:
description: Name of a K8s secret containing configuration details
for the sink
type: string
type:
enum:
- slack
type: string
required:
- secretName
- type
type: object
type: object
status:
description: ValidatorConfigStatus defines the observed state of ValidatorConfig
Expand Down
6 changes: 6 additions & 0 deletions chart/validator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ spec:
env:
- name: KUBERNETES_CLUSTER_DOMAIN
value: {{ quote .Values.kubernetesClusterDomain }}
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: SINK_WEBHOOK_TIMEOUT_SECONDS
value: {{ quote .Values.controllerManager.manager.sinkWebhookTimeout }}
image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag
| default .Chart.AppVersion }}
livenessProbe:
Expand Down
11 changes: 11 additions & 0 deletions chart/validator/templates/sink-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{- if and .Values.sink .Values.sink.createSecret }}
apiVersion: v1
kind: Secret
metadata:
name: {{ required ".Values.sink.secretName is required!" .Values.sink.secretName }}
stringData:
{{- if eq .Values.sink.type "slack" }}
apiToken: {{ required ".Values.sink.apiToken is required!" .Values.sink.apiToken }}
channelId: {{ required ".Values.sink.channelId is required!" .Values.sink.channelId }}
{{- end }}
{{- end }}
7 changes: 0 additions & 7 deletions chart/validator/templates/valid8or-config.yaml

This file was deleted.

12 changes: 12 additions & 0 deletions chart/validator/templates/validator-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: validation.spectrocloud.labs/v1alpha1
kind: ValidatorConfig
metadata:
name: validator-config
spec:
plugins:
{{ toYaml .Values.plugins | indent 2 }}
{{- if .Values.sink }}
sink:
type: {{ required ".Values.sink.type is required!" .Values.sink.type }}
secretName: {{ required ".Values.sink.secretName is required!" .Values.sink.secretName }}
{{- end }}
13 changes: 13 additions & 0 deletions chart/validator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ controllerManager:
requests:
cpu: 10m
memory: 64Mi
sinkWebhookTimeout: 30s
replicas: 1
serviceAccount:
annotations: {}
Expand All @@ -50,6 +51,18 @@ metricsService:
protocol: TCP
targetPort: https
type: ClusterIP

# Optional sink configuration
sink: {}
# type: slack
# secretName: "slack-secret"
# apiToken: ""
# channelId: ""
# By default, a secret will be created. Leave the above fields blank and specify 'createSecret: false' to use an existing secret.
# WARNING: the existing secret must match the format used in sink-secret.yaml
# createSecret: true

# Validation plugin charts
plugins:
- chart:
name: validator-plugin-aws
Expand Down
Loading

0 comments on commit dac2c3a

Please sign in to comment.