├── .draftignore ├── Dockerfile ├── Makefile ├── README.md ├── brigade.js ├── chart └── helm-hello │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── ingress.yaml │ └── service.yaml │ └── values.yaml ├── draft.toml ├── index.js └── package.json /.draftignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.tmp 3 | *.temp 4 | .git* 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | 3 | ENV PORT 8080 4 | EXPOSE 8080 5 | 6 | COPY index.js index.js 7 | COPY package.json package.json 8 | 9 | RUN yarn install 10 | 11 | CMD yarn start 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION ?= latest 2 | REGISTRY ?= osseu.azurecr.io 3 | 4 | .PHONY: build 5 | build: docker-build 6 | build: docker-push 7 | 8 | .PHONY: docker-build 9 | docker-build: 10 | docker build -t $(REGISTRY)/hello-helm:$(VERSION) . 11 | 12 | .PHONY: docker-push 13 | docker-push: 14 | docker push $(REGISTRY)/hello-helm:$(VERSION) 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hello Helm 2 | 3 | This is a simple demo of an ACR webhook to automatically upgrade a Helm chart. The 4 | `brigade.js` is configured to upgrade a Helm release every time the specified 5 | Docker image is upgraded in an Azure Container Registry (ACR). 6 | 7 | _This is not production quailty. It is a demo._ 8 | 9 | To use this demo, you will need to point it to your own ACR repository. 10 | To do this, you will need to edit the registry in the following places: 11 | 12 | - Makefile 13 | - charts/values.yaml 14 | 15 | Then you will need to configure your ACR to trigger a webhook on each push 16 | event. 17 | 18 | **Note:** With a little bit of modification to the `brigade.js`, you can also 19 | use this for DockerHub webhooks. 20 | 21 | ## Prerequisites 22 | 23 | - Helm and Brigade must be installed on your cluster 24 | - Your cluster must have a routable IP attached to the `brigade-gw` service 25 | - You must have an ACR created with type `Managed` 26 | - You need a clone of this repository 27 | - You need to configure a Slack incomming webhook. See [Slack-Notify](https://github.com/technosophos/slack-notify) 28 | 29 | ## Configuring ACR 30 | 31 | You can either use the `az acr webhook` tool or use the `webhooks` panel in the 32 | Container Registry section of the Azure portal. You must configure the webhook 33 | to point to the Brigade gateway. 34 | 35 | To get the external IP of the gateway, do: 36 | 37 | ``` 38 | $ kubectl get svc | brigade-gw 39 | ``` 40 | 41 | Set up your webhook to use `http://:7744/events/dockerhub//.` 42 | 43 | - The IP is the gateway IP from the command above (or a DNS address, if you set one up) 44 | - Project name is the _name of your Brigade project_ (`foo/bar`) 45 | - Commit is the commit/branch/repo in the repo you want to use to fetch the brigade.js from, usually `master` 46 | 47 | So if project name is `technosophos/hello-helm`, and the host is `example.com`, then 48 | the URL would be something like: 49 | 50 | ``` 51 | http://example.com:7744/events/dockerhub/technosophos/hello-helm/master 52 | ``` 53 | 54 | ## Configuring your Brigade project 55 | 56 | If this is the first time you have used the Slack Notifier webhook, you need to add 57 | the Slack incomming webhook URL to your project. 58 | 59 | Typically, you will want to edit the project's `values.yaml` overrides. But here's how to do it 60 | from the Helm commandline: 61 | 62 | ```console 63 | $ helm upgrade myproject brigade/brigade-project --set secrets.SLACK_WEBHOOK=https://rest/of/url 64 | ``` 65 | 66 | ## How it works 67 | 68 | - `make docker-build docker-push` pushes a new image 69 | - ACR responds to the push by calling the webhook 70 | - The webhook triggers a brigade event 71 | - The brigade event handler causes Helm to upgrade the chart with the new label 72 | 73 | You can update the new build by running: 74 | 75 | ``` 76 | $ VERSION=0.1.2 make docker-build docker-push 77 | ``` 78 | 79 | The above will create a new tag for the image (`0.1.2`), which will trigger a 80 | new `helm upgrade`. 81 | -------------------------------------------------------------------------------- /brigade.js: -------------------------------------------------------------------------------- 1 | const {events, Job} = require("brigadier") 2 | 3 | // Set to 2.5.1 b/c of ACS requirements 4 | const helmTag = "v2.5.1" 5 | 6 | events.on("image_push", (e, p) => { 7 | var name = "example-hello" 8 | var docker = JSON.parse(e.payload) 9 | console.log(docker) 10 | 11 | if (docker.action != "push") { 12 | console.log(`ignoring action ${docker.action}`) 13 | return 14 | } 15 | 16 | var version = docker.target.tag || "latest" 17 | if (version == "latest") { 18 | console.log("ignoring 'latest'") 19 | return 20 | } 21 | 22 | var helm = new Job("helm", "lachlanevenson/k8s-helm:" + helmTag) 23 | helm.storage.enabled = false 24 | helm.tasks = [ 25 | "ls /src", 26 | "helm upgrade --reuse-values --set tag=" + version + " --install " + name + " /src/chart/helm-hello" 27 | ] 28 | 29 | var slack = new Job("slack-notify", "technosophos/slack-notify:latest", ["/slack-notify"]) 30 | 31 | helm.run().then( result => { 32 | slack.storage.enabled = false 33 | slack.env = { 34 | SLACK_WEBHOOK: p.secrets.SLACK_WEBHOOK, 35 | SLACK_USERNAME: "BrigadeBot", 36 | SLACK_TITLE: ":helm: upgraded " + name, 37 | SLACK_MESSAGE: result.toString(), 38 | SLACK_COLOR: "#0000ff" 39 | } 40 | slack.run() 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /chart/helm-hello/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /chart/helm-hello/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | description: A Helm chart for Kubernetes 3 | name: helm-hello 4 | version: 0.1.0 5 | -------------------------------------------------------------------------------- /chart/helm-hello/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.hostname }} 3 | http://{{- .Values.ingress.hostname }} 4 | {{- else if contains "NodePort" .Values.service.type }} 5 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "fullname" . }}) 6 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 7 | echo http://$NODE_IP:$NODE_PORT 8 | {{- else if contains "LoadBalancer" .Values.service.type }} 9 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 10 | You can watch the status of by running 'kubectl get svc -w {{ template "fullname" . }}' 11 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 12 | echo http://$SERVICE_IP:{{ .Values.service.externalPort }} 13 | {{- else if contains "ClusterIP" .Values.service.type }} 14 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 15 | echo "Visit http://127.0.0.1:8080 to use your application" 16 | kubectl port-forward $POD_NAME 8080:{{ .Values.service.externalPort }} 17 | {{- end }} 18 | -------------------------------------------------------------------------------- /chart/helm-hello/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "fullname" -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /chart/helm-hello/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | labels: 6 | app: {{ template "name" . }} 7 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | replicas: {{ .Values.replicaCount }} 12 | template: 13 | metadata: 14 | labels: 15 | app: {{ template "name" . }} 16 | release: {{ .Release.Name }} 17 | spec: 18 | containers: 19 | - name: {{ .Chart.Name }} 20 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 21 | imagePullPolicy: {{ .Values.image.pullPolicy }} 22 | ports: 23 | - containerPort: {{ .Values.service.internalPort }} 24 | livenessProbe: 25 | httpGet: 26 | path: / 27 | port: {{ .Values.service.internalPort }} 28 | readinessProbe: 29 | httpGet: 30 | path: / 31 | port: {{ .Values.service.internalPort }} 32 | resources: 33 | {{ toYaml .Values.resources | indent 12 }} 34 | {{- if .Values.nodeSelector }} 35 | nodeSelector: 36 | {{ toYaml .Values.nodeSelector | indent 8 }} 37 | {{- end }} 38 | -------------------------------------------------------------------------------- /chart/helm-hello/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $serviceName := include "fullname" . -}} 3 | {{- $servicePort := .Values.service.externalPort -}} 4 | apiVersion: extensions/v1beta1 5 | kind: Ingress 6 | metadata: 7 | name: {{ template "fullname" . }} 8 | labels: 9 | app: {{ template "name" . }} 10 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} 11 | release: {{ .Release.Name }} 12 | heritage: {{ .Release.Service }} 13 | annotations: 14 | {{- range $key, $value := .Values.ingress.annotations }} 15 | {{ $key }}: {{ $value | quote }} 16 | {{- end }} 17 | spec: 18 | rules: 19 | {{- range $host := .Values.ingress.hosts }} 20 | - host: {{ $host }} 21 | http: 22 | paths: 23 | - path: / 24 | backend: 25 | serviceName: {{ $serviceName }} 26 | servicePort: {{ $servicePort }} 27 | {{- end -}} 28 | {{- if .Values.ingress.tls }} 29 | tls: 30 | {{ toYaml .Values.ingress.tls | indent 4 }} 31 | {{- end -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /chart/helm-hello/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "fullname" . }} 5 | labels: 6 | app: {{ template "name" . }} 7 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | type: {{ .Values.service.type }} 12 | ports: 13 | - port: {{ .Values.service.externalPort }} 14 | targetPort: {{ .Values.service.internalPort }} 15 | protocol: TCP 16 | name: {{ .Values.service.name }} 17 | selector: 18 | app: {{ template "name" . }} 19 | release: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /chart/helm-hello/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for helm-hello. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | replicaCount: 1 5 | image: 6 | repository: osseu.azurecr.io/hello-helm 7 | tag: dev 8 | pullPolicy: IfNotPresent 9 | service: 10 | name: hello 11 | type: ClusterIP 12 | externalPort: 8080 13 | internalPort: 8080 14 | ingress: 15 | enabled: false 16 | # Used to create an Ingress record. 17 | hosts: 18 | - chart-example.local 19 | annotations: 20 | # kubernetes.io/ingress.class: nginx 21 | # kubernetes.io/tls-acme: "true" 22 | tls: 23 | # Secrets must be manually created in the namespace. 24 | # - secretName: chart-example-tls 25 | # hosts: 26 | # - chart-example.local 27 | resources: {} 28 | # We usually recommend not to specify default resources and to leave this as a conscious 29 | # choice for the user. This also increases chances charts run on environments with little 30 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 31 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 32 | # limits: 33 | # cpu: 100m 34 | # memory: 128Mi 35 | # requests: 36 | # cpu: 100m 37 | # memory: 128Mi 38 | -------------------------------------------------------------------------------- /draft.toml: -------------------------------------------------------------------------------- 1 | [environments] 2 | [environments.development] 3 | name = "sweet-stingeray" 4 | namespace = "default" 5 | wait = false 6 | watch = false 7 | watch_delay = 2 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const port = process.env.PORT || 8080; 3 | 4 | const requestHandler = (request, response) => { 5 | console.log(request.url); 6 | response.end("Hello Kubernetes World 4!"); 7 | } 8 | 9 | http.createServer(requestHandler).listen(port, err => { 10 | if (err) { 11 | return console.log(err); 12 | } 13 | 14 | console.log(`server is listening on ${port}`); 15 | }) 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-draft", 3 | "version": "1.0.0", 4 | "description": "Simple draft demo", 5 | "main": "index.js", 6 | "author": "technosophos", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "node ./index.js" 10 | } 11 | } 12 | --------------------------------------------------------------------------------