Skip to content

Commit

Permalink
Merge pull request #9 from rusenask/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
rusenask authored Jun 15, 2017
2 parents a2d4447 + 24d0d84 commit 949d0e6
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 32 deletions.
5 changes: 2 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
FROM golang:1.8.1-alpine
FROM golang:1.8.3
COPY . /go/src/github.com/rusenask/keel
WORKDIR /go/src/github.com/rusenask/keel
RUN apk add --no-cache git && go get
RUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags -'w' -o keel .
RUN go get && make build

FROM alpine:latest
RUN apk --no-cache add ca-certificates
Expand Down
19 changes: 15 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
build:
CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags -'w' -o keel .
JOBDATE ?= $(shell date -u +%Y-%m-%dT%H%M%SZ)
GIT_REVISION = $(shell git rev-parse --short HEAD)
VERSION ?= $(GIT_REVISION)

LDFLAGS += -X github.com/rusenask/keel/version.Version=$(VERSION)
LDFLAGS += -X github.com/rusenask/keel/version.Revision=$(GIT_REVISION)
LDFLAGS += -X github.com/rusenask/keel/version.BuildDate=$(JOBDATE)

.PHONY: release

image: build
docker build -t karolisr/keel:0.1.2 -f Dockerfile .
test:
go test -v `go list ./... | egrep -v /vendor/`

build:
@echo "++ Building keel"
CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags "$(LDFLAGS)" -o keel .
11 changes: 11 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/rusenask/keel/trigger/http"
"github.com/rusenask/keel/trigger/pubsub"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/version"

log "github.com/Sirupsen/logrus"
)
Expand All @@ -32,6 +33,16 @@ const EnvDebug = "DEBUG"

func main() {

ver := version.GetKeelVersion()
log.WithFields(log.Fields{
"os": ver.OS,
"build_date": ver.BuildDate,
"revision": ver.Revision,
"version": ver.Version,
"go_version": ver.GoVersion,
"arch": ver.Arch,
}).Info("Keel starting..")

if os.Getenv(EnvDebug) != "" {
log.SetLevel(log.DebugLevel)
}
Expand Down
30 changes: 22 additions & 8 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@
Lightweight (uses ~10MB RAM when running) [Kubernetes](https://kubernetes.io/) controller for automating deployment updates when new image is available. Keel uses [semantic versioning](http://semver.org/) to determine whether deployment needs an update or not. Currently keel has several types of triggers:

* Google's pubsub integration with [Google Container Registry](https://cloud.google.com/container-registry/)
* [DockerHub Webhooks](https://docs.docker.com/docker-hub/webhooks/)
* Webhooks

Upcomming integrations:

* DockerHub webhooks


## Keel overview

* Stateless, runs as a single container in kube-system namespace
Expand Down Expand Up @@ -77,15 +73,33 @@ Available policy options:
* __minor__ - update only minor versions (ignores major)
* __patch__ - update only patch versions (ignores minor and major versions)

## Deployment
## Deployment and triggers

### Step 1: GCE Kubernetes + GCR pubsub configuration
### Step 1: Choosing triggers


#### GCE Kubernetes + GCR pubsub configuration (recommended option for deployments in Google Container Engine)

Since access to pubsub is required in GCE Kubernetes - your cluster node pools need to have permissions. If you are creating new cluster - just enable pubsub from the start. If you have existing cluster - currently the only way is create new node-pool through the gcloud CLI (more info in the [docs](https://cloud.google.com/sdk/gcloud/reference/container/node-pools/create?hl=en_US&_ga=1.2114551.650086469.1487625651):

```
gcloud container node-pools create new-pool --cluster CLUSTER_NAME --scopes https://www.googleapis.com/auth/pubsub
```
```

Make sure that in the Keel's deployment.yml you have set environment variables __PUBSUB=1__ and __PROJECT_ID=your-project-id__.

#### Webhook integration

Keel supports two types of webhooks:

* [DockerHub Webhooks](https://docs.docker.com/docker-hub/webhooks/) - go to your repository on
https://hub.docker.com/r/your-namespace/your-repository/~/settings/webhooks/ and point webhooks
to `http://your-keel-address.com/v1/webhooks/dockerhub`.
* Native webhooks (simplified version) - shoot webhooks at `http://your-keel-address.com/v1/webhooks/native` with a payload that has __name__ and __tag__ fields: `{"name": "gcr.io/v2-namespace/hello-world", "tag": "1.1.1"}`

If you don't want to expose your Keel service - I would recommend using [https://webhookrelay.com/](https://webhookrelay.com/) which can deliver webhooks to your internal Keel service through a sidecar container.



### Step 2: Kubernetes

Expand Down
102 changes: 102 additions & 0 deletions trigger/http/dockerhub_webhook_trigger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package http

import (
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/rusenask/keel/types"

log "github.com/Sirupsen/logrus"
)

// Example of dockerhub trigger
// {
// "push_data": {
// "pushed_at": 1497467660,
// "images": [],
// "tag": "0.1.7",
// "pusher": "karolisr"
// },
// "callback_url": "https://registry.hub.docker.com/u/karolisr/keel/hook/22hagb51h1gfb4eefc5f1g4j3abi0beg4/",
// "repository": {
// "status": "Active",
// "description": "",
// "is_trusted": false,
// "full_description": "# Keel - automated Kubernetes deployments for the rest of us\n\nLightweight (11MB image size, uses 12MB RAM when running) [Kubernetes](https://kubernetes.io/) controller for automating image updates for deployments. Keel uses [semantic versioning](http://semver.org/) to determine whether deployment needs an update or not. Currently keel has several types of triggers:\n\n* Google's pubsub integration with [Google Container Registry](https://cloud.google.com/container-registry/)\n* Webhooks\n\nUpcomming integrations:\n\n* DockerHub webhooks\n\n## Why?\n\nI have built Keel since I have a relatively small Golang project which doesn't use a lot of memory and introducing an antique, heavy weight CI solution with lots dependencies seemed like a terrible idea. \n\nYou should consider using Keel:\n* If you are not Netflix, Google, Amazon, {insert big company here} - you might not want to run something like Spinnaker that has heavy dependencies such as \"JDK8, Redis, Cassandra, Packer\". You probably need something lightweight, stateless, that you don't have to think about.\n* If you are not a bank that uses RedHat's OpenShift which embedded Jenkins that probably already does what Keel is doing.\n* You want automated Kubernetes deployment updates.\n\nHere is a list of Keel dependencies:\n\n1.\n\nYes, none.\n\n## Getting started\n\nKeel operates as a background service, you don't need to interact with it directly, just add labels to your deployments. \n\n### Example deployment\n\nHere is an example deployment which specifies that keel should always update image if a new version is available:\n\n```\n---\napiVersion: extensions/v1beta1\nkind: Deployment\nmetadata: \n name: wd\n namespace: default\n labels: \n name: \"wd\"\n keel.observer/policy: all\nspec:\n replicas: 1\n template:\n metadata:\n name: wd\n labels:\n app: wd \n\n spec:\n containers: \n - image: karolisr/webhook-demo:0.0.2\n imagePullPolicy: Always \n name: wd\n command: [\"/bin/webhook-demo\"]\n ports:\n - containerPort: 8090 \n livenessProbe:\n httpGet:\n path: /healthz\n port: 8090\n initialDelaySeconds: 30\n timeoutSeconds: 10\n securityContext:\n privileged: true \n```\n\nAvailable policy options:\n\n* all - update whenever there is a version bump\n* major - update major versions\n* minor - update only minor versions (ignores major)\n* patch - update only patch versions (ignores minor and major versions)\n\n## Deployment\n\n### Step 1: GCE Kubernetes + GCR pubsub configuration\n\nSince access to pubsub is required in GCE Kubernetes - your cluster node pools need to have permissions. If you are creating new cluster - just enable pubsub from the start. If you have existing cluster - currently the only way is create new node-pool through the gcloud CLI (more info in the [docs](https://cloud.google.com/sdk/gcloud/reference/container/node-pools/create?hl=en_US&_ga=1.2114551.650086469.1487625651):\n\n```\ngcloud container node-pools create new-pool --cluster CLUSTER_NAME --scopes https://www.googleapis.com/auth/pubsub\n``` \n\n### Step 2: Kubernetes\n\nSince keel will be updating deployments, let's create a new [service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) in `kube-system` namespace:\n\n```\nkubectl create serviceaccount keel --namespace=kube-system\n```\n\nNow, edit [deployment file](https://github.com/rusenask/keel/blob/master/hack/deployment.sample.yml) that is supplied with the repo (basically point to the newest keel release and set your PROJECT_ID to the actual project ID that you have):\n\n```\nkubectl create -f hack/deployment.yml\n```\n\nOnce Keel is deployed in your Kubernetes cluster - it occasionally scans your current deployments and looks for ones that have label _keel.observer/policy_. It then checks whether appropriate subscriptions and topics are set for GCR registries, if not - auto-creates them.\n\n",
// "repo_url": "https://hub.docker.com/r/karolisr/keel",
// "owner": "karolisr",
// "is_official": false,
// "is_private": false,
// "name": "keel",
// "namespace": "karolisr",
// "star_count": 0,
// "comment_count": 0,
// "date_created": 1497032538,
// "dockerfile": "FROM golang:1.8.1-alpine\nCOPY . /go/src/github.com/rusenask/keel\nWORKDIR /go/src/github.com/rusenask/keel\nRUN apk add --no-cache git && go get\nRUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags -'w' -o keel .\n\nFROM alpine:latest\nRUN apk --no-cache add ca-certificates\nCOPY --from=0 /go/src/github.com/rusenask/keel/keel /bin/keel\nENTRYPOINT [\"/bin/keel\"]\n\nEXPOSE 9300",
// "repo_name": "karolisr/keel"
// }
// }

type dockerHubWebhook struct {
PushData struct {
PushedAt int `json:"pushed_at"`
Images []interface{} `json:"images"`
Tag string `json:"tag"`
Pusher string `json:"pusher"`
} `json:"push_data"`
CallbackURL string `json:"callback_url"`
Repository struct {
Status string `json:"status"`
Description string `json:"description"`
IsTrusted bool `json:"is_trusted"`
FullDescription string `json:"full_description"`
RepoURL string `json:"repo_url"`
Owner string `json:"owner"`
IsOfficial bool `json:"is_official"`
IsPrivate bool `json:"is_private"`
Name string `json:"name"`
Namespace string `json:"namespace"`
StarCount int `json:"star_count"`
CommentCount int `json:"comment_count"`
DateCreated int `json:"date_created"`
Dockerfile string `json:"dockerfile"`
RepoName string `json:"repo_name"`
} `json:"repository"`
}

// dockerHubHandler - used to react to dockerhub webhooks
func (s *TriggerServer) dockerHubHandler(resp http.ResponseWriter, req *http.Request) {
dw := dockerHubWebhook{}
if err := json.NewDecoder(req.Body).Decode(&dw); err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("trigger.dockerHubHandler: failed to decode request")
resp.WriteHeader(http.StatusBadRequest)
return
}

if dw.Repository.RepoName == "" {
resp.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(resp, "repository name cannot be empty")
return
}

if dw.PushData.Tag == "" {
resp.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(resp, "repository tag cannot be empty")
return
}

event := types.Event{}
event.CreatedAt = time.Now()
event.TriggerName = "dockerhub"
event.Repository.Name = dw.Repository.RepoName
event.Repository.Tag = dw.PushData.Tag

s.trigger(event)

resp.WriteHeader(http.StatusOK)
return
}
76 changes: 76 additions & 0 deletions trigger/http/dockerhub_webhook_trigger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package http

import (
"bytes"
"net/http"

"github.com/rusenask/keel/provider"
// "github.com/rusenask/keel/types"

"net/http/httptest"
"testing"
)

var fakeRequest = `{
"push_data": {
"pushed_at": 1497467660,
"images": [],
"tag": "0.1.7",
"pusher": "karolisr"
},
"callback_url": "https://registry.hub.docker.com/u/karolisr/keel/hook/22hagb51h1gfb4eefc5f1g4j3abi0beg4/",
"repository": {
"status": "Active",
"description": "",
"is_trusted": false,
"full_description": "desc",
"repo_url": "https://hub.docker.com/r/karolisr/keel",
"owner": "karolisr",
"is_official": false,
"is_private": false,
"name": "keel",
"namespace": "karolisr",
"star_count": 0,
"comment_count": 0,
"date_created": 1497032538,
"dockerfile": "FROM golang:1.8.1-alpine\nCOPY . /go/src/github.com/rusenask/keel\nWORKDIR /go/src/github.com/rusenask/keel\nRUN apk add --no-cache git && go get\nRUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags -'w' -o keel .\n\nFROM alpine:latest\nRUN apk --no-cache add ca-certificates\nCOPY --from=0 /go/src/github.com/rusenask/keel/keel /bin/keel\nENTRYPOINT [\"/bin/keel\"]\n\nEXPOSE 9300",
"repo_name": "karolisr/keel"
}
}`

func TestDockerhubWebhookHandler(t *testing.T) {

fp := &fakeProvider{}
providers := map[string]provider.Provider{
fp.GetName(): fp,
}
srv := NewTriggerServer(&Opts{Providers: providers})
srv.registerRoutes(srv.router)

req, err := http.NewRequest("POST", "/v1/webhooks/dockerhub", bytes.NewBuffer([]byte(fakeRequest)))
if err != nil {
t.Fatalf("failed to create req: %s", err)
}

//The response recorder used to record HTTP responses
rec := httptest.NewRecorder()

srv.router.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Errorf("unexpected status code: %d", rec.Code)

t.Log(rec.Body.String())
}

if len(fp.submitted) != 1 {
t.Fatalf("unexpected number of events submitted: %d", len(fp.submitted))
}

if fp.submitted[0].Repository.Name != "karolisr/keel" {
t.Errorf("expected karolisr/keel but got %s", fp.submitted[0].Repository.Name)
}

if fp.submitted[0].Repository.Tag != "0.1.7" {
t.Errorf("expected 0.1.7 but got %s", fp.submitted[0].Repository.Tag)
}
}
39 changes: 38 additions & 1 deletion trigger/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package http

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
Expand All @@ -10,6 +11,8 @@ import (
"github.com/urfave/negroni"

"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/version"

log "github.com/Sirupsen/logrus"
)
Expand Down Expand Up @@ -69,10 +72,44 @@ func (s *TriggerServer) Stop() {
func (s *TriggerServer) registerRoutes(mux *mux.Router) {
// health endpoint for k8s to be happy
mux.HandleFunc("/healthz", s.healthHandler).Methods("GET", "OPTIONS")
// version handler
mux.HandleFunc("/version", s.versionHandler).Methods("GET", "OPTIONS")
// native webhooks handler
mux.HandleFunc("/v1/native", s.nativeHandler).Methods("POST", "OPTIONS")
mux.HandleFunc("/v1/webhooks/native", s.nativeHandler).Methods("POST", "OPTIONS")

// dockerhub webhooks handler
mux.HandleFunc("/v1/webhooks/dockerhub", s.dockerHubHandler).Methods("POST", "OPTIONS")
}

func (s *TriggerServer) healthHandler(resp http.ResponseWriter, req *http.Request) {
resp.WriteHeader(http.StatusOK)
}

func (s *TriggerServer) versionHandler(resp http.ResponseWriter, req *http.Request) {
v := version.GetKeelVersion()

encoded, err := json.Marshal(v)
if err != nil {
log.WithError(err).Error("trigger.http: failed to marshal version")
resp.WriteHeader(http.StatusInternalServerError)
return
}

resp.WriteHeader(http.StatusOK)
resp.Write(encoded)
}

func (s *TriggerServer) trigger(event types.Event) error {
for _, p := range s.providers {
err := p.Submit(event)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"provider": p.GetName(),
"trigger": event.TriggerName,
}).Error("trigger.trigger: got error while submitting event to provider")
}
}

return nil
}
Loading

0 comments on commit 949d0e6

Please sign in to comment.