mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-23 22:16:45 +00:00
Merge pull request #1 from stakater/initial-implementation
[STK-322] Initial implementation
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
*.swp
|
||||||
|
_dist/
|
||||||
|
.idea
|
||||||
|
golib
|
||||||
|
release
|
||||||
|
out/
|
||||||
|
_gopath/
|
||||||
|
.DS_Store
|
||||||
|
vendor
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/groovy
|
||||||
|
@Library('github.com/stakater/fabric8-pipeline-library@v2.4.0')
|
||||||
|
|
||||||
|
def dummy
|
||||||
|
|
||||||
|
goBuildAndRelease {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# note: call scripts from /scripts
|
||||||
|
|
||||||
|
.PHONY: default build builder-image binary-image test stop clean-images clean push apply deploy
|
||||||
|
|
||||||
|
BUILDER ?= reloader-builder
|
||||||
|
BINARY ?= Reloader
|
||||||
|
DOCKER_IMAGE ?= stakater/reloader
|
||||||
|
# Default value "dev"
|
||||||
|
DOCKER_TAG ?= 1.0.0
|
||||||
|
REPOSITORY = ${DOCKER_IMAGE}:${DOCKER_TAG}
|
||||||
|
|
||||||
|
VERSION=$(shell cat .version)
|
||||||
|
BUILD=
|
||||||
|
|
||||||
|
GOCMD = go
|
||||||
|
GLIDECMD = glide
|
||||||
|
GOFLAGS ?= $(GOFLAGS:)
|
||||||
|
LDFLAGS =
|
||||||
|
|
||||||
|
default: build test
|
||||||
|
|
||||||
|
install:
|
||||||
|
"$(GLIDECMD)" install
|
||||||
|
|
||||||
|
build:
|
||||||
|
"$(GOCMD)" build ${GOFLAGS} ${LDFLAGS} -o "${BINARY}"
|
||||||
|
|
||||||
|
builder-image:
|
||||||
|
@docker build --network host -t "${BUILDER}" -f build/package/Dockerfile.build .
|
||||||
|
|
||||||
|
binary-image: builder-image
|
||||||
|
@docker run --network host --rm "${BUILDER}" | docker build --network host -t "${REPOSITORY}" -f Dockerfile.run -
|
||||||
|
|
||||||
|
test:
|
||||||
|
"$(GOCMD)" test -v ./...
|
||||||
|
|
||||||
|
stop:
|
||||||
|
@docker stop "${BINARY}"
|
||||||
|
|
||||||
|
clean-images: stop
|
||||||
|
@docker rmi "${BUILDER}" "${BINARY}"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
"$(GOCMD)" clean -i
|
||||||
|
|
||||||
|
push: ## push the latest Docker image to DockerHub
|
||||||
|
docker push $(REPOSITORY)
|
||||||
|
|
||||||
|
apply:
|
||||||
|
kubectl apply -f deployments/manifests/ -n temp-reloader
|
||||||
|
|
||||||
|
deploy: binary-image push apply
|
||||||
@@ -1 +1,96 @@
|
|||||||
# Reloader
|
# RELOADER
|
||||||
|
|
||||||
|
## WHY NAME RELOADER
|
||||||
|
|
||||||
|
In english language, Reloader is a thing/tool that can reload certain stuff. So refereig to that meaning relaoder can reload
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
We would like to watch if some change happens in `ConfigMap` and `Secret` objects and then perform certain upgrade on relavent `Deployment`, `Deamonset` and `Statefulset`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Reloader can watch any changes in `ConfigMap` and `Secret` objects and update or recreate Pods for their associated `Deployments`, `Deamonsets` and `Statefulsets`. In this way Pods can get the latest changes in `ConfigMap` or `Secret` objects.
|
||||||
|
|
||||||
|
**NOTE:** This controller has been inspired from [configmapController](https://github.com/fabric8io/configmapcontroller)
|
||||||
|
|
||||||
|
## How to use Reloader
|
||||||
|
|
||||||
|
For a `Deployment` called `foo` have a `ConfigMap` called `foo`. Then add this annotation to your `Deployment`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
reloader.stakater.com/update-on-change: "foo"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, providing `Reloader` is running, whenever you edit the `ConfigMap` called `foo` the Reloader will update the `Deployment` by adding the environment variable:
|
||||||
|
|
||||||
|
```
|
||||||
|
STAKATER_FOO_REVISION=${reloaderRevision}
|
||||||
|
```
|
||||||
|
|
||||||
|
This then triggers a rolling upgrade of your deployment's pods to use the new configuration.
|
||||||
|
|
||||||
|
Same procedure can be followed to perform rolling upgrade on `Deamonsets` and `Statefulsets` as well.
|
||||||
|
|
||||||
|
## Deploying to Kubernetes
|
||||||
|
|
||||||
|
You can deploy Reloader by running the following kubectl commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f rbac.yaml -n <namespace>
|
||||||
|
kubectl apply -f deployment.yaml -n <namespace>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helm Charts
|
||||||
|
|
||||||
|
Or alternatively if you configured `helm` on your cluster, you can deploy Reloader via helm chart located under `deployments/kubernetes/chart/reloader` folder.
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
**Got a question?**
|
||||||
|
File a GitHub [issue](https://github.com/stakater/Reloader/issues), or send us an [email](mailto:stakater@gmail.com).
|
||||||
|
|
||||||
|
### Talk to us on Slack
|
||||||
|
|
||||||
|
Join and talk to us on the #tools-imc channel for discussing Reloader
|
||||||
|
|
||||||
|
[](https://stakater-slack.herokuapp.com/)
|
||||||
|
[](https://stakater.slack.com/messages/CAN960CTG/)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### Bug Reports & Feature Requests
|
||||||
|
|
||||||
|
Please use the [issue tracker](https://github.com/stakater/Reloader/issues) to report any bugs or file feature requests.
|
||||||
|
|
||||||
|
### Developing
|
||||||
|
|
||||||
|
PRs are welcome. In general, we follow the "fork-and-pull" Git workflow.
|
||||||
|
|
||||||
|
1. **Fork** the repo on GitHub
|
||||||
|
2. **Clone** the project to your own machine
|
||||||
|
3. **Commit** changes to your own branch
|
||||||
|
4. **Push** your work back up to your fork
|
||||||
|
5. Submit a **Pull request** so that we can review your changes
|
||||||
|
|
||||||
|
NOTE: Be sure to merge the latest from "upstream" before making a pull request!
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
View our closed [Pull Requests](https://github.com/stakater/Reloader/pulls?q=is%3Apr+is%3Aclosed).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache2 © [Stakater](http://stakater.com)
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
`Reloader` is maintained by [Stakater][website]. Like it? Please let us know at <hello@stakater.com>
|
||||||
|
|
||||||
|
See [our other projects][community]
|
||||||
|
or contact us in case of professional services and queries on <hello@stakater.com>
|
||||||
|
|
||||||
|
[website]: http://stakater.com/
|
||||||
|
[community]: https://github.com/stakater/
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
FROM stakater/go-glide:1.9.3
|
||||||
|
MAINTAINER "Stakater Team"
|
||||||
|
|
||||||
|
RUN apk update
|
||||||
|
|
||||||
|
RUN apk -v --update \
|
||||||
|
add git build-base && \
|
||||||
|
rm -rf /var/cache/apk/* && \
|
||||||
|
mkdir -p "$GOPATH/src/github.com/stakater/Reloader"
|
||||||
|
|
||||||
|
ADD . "$GOPATH/src/github.com/stakater/Reloader"
|
||||||
|
|
||||||
|
RUN cd "$GOPATH/src/github.com/stakater/Reloader" && \
|
||||||
|
glide update && \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a --installsuffix cgo --ldflags="-s" -o /Reloader
|
||||||
|
|
||||||
|
COPY build/package/Dockerfile.run /
|
||||||
|
|
||||||
|
# Running this image produces a tarball suitable to be piped into another
|
||||||
|
# Docker build command.
|
||||||
|
CMD tar -cf - -C / Dockerfile.run Reloader
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
FROM alpine:3.4
|
||||||
|
MAINTAINER "Stakater Team"
|
||||||
|
|
||||||
|
RUN apk add --update ca-certificates
|
||||||
|
|
||||||
|
COPY Reloader /bin/Reloader
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/Reloader"]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Generated from deployments/kubernetes/templates/chart/Chart.yaml.tmpl
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
name: reloader
|
||||||
|
description: Reloader chart that runs on kubernetes
|
||||||
|
version: 0.0.1
|
||||||
|
keywords:
|
||||||
|
- Reloader
|
||||||
|
- kubernetes
|
||||||
|
home: https://github.com/stakater/Reloader
|
||||||
|
maintainers:
|
||||||
|
- name: Stakater
|
||||||
|
email: hello@stakater.com
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{{/* vim: set filetype=mustache: */}}
|
||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "reloader-name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" | lower -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
*/}}
|
||||||
|
{{- define "reloader-fullname" -}}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "reloader-labels.selector" -}}
|
||||||
|
app: {{ template "reloader-name" . }}
|
||||||
|
group: {{ .Values.reloader.labels.group }}
|
||||||
|
provider: {{ .Values.reloader.labels.provider }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "reloader-labels.stakater" -}}
|
||||||
|
{{ template "reloader-labels.selector" . }}
|
||||||
|
version: {{ .Values.reloader.labels.version }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "reloader-labels.chart" -}}
|
||||||
|
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||||
|
release: {{ .Release.Name | quote }}
|
||||||
|
heritage: {{ .Release.Service | quote }}
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{ include "reloader-labels.stakater" . | indent 4 }}
|
||||||
|
{{ include "reloader-labels.chart" . | indent 4 }}
|
||||||
|
name: {{ template "reloader-name" . }}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
revisionHistoryLimit: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{ include "reloader-labels.selector" . | indent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{ include "reloader-labels.selector" . | indent 8 }}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- env:
|
||||||
|
- name: KUBERNETES_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
image: "{{ .Values.reloader.image.name }}:{{ .Values.reloader.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.reloader.image.pullPolicy }}
|
||||||
|
name: {{ template "reloader-name" . }}
|
||||||
|
serviceAccountName: {{ template "reloader-name" . }}
|
||||||
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{ include "reloader-labels.stakater" . | indent 4 }}
|
||||||
|
{{ include "reloader-labels.chart" . | indent 4 }}
|
||||||
|
name: {{ template "reloader-name" . }}
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{ include "reloader-labels.stakater" . | indent 4 }}
|
||||||
|
{{ include "reloader-labels.chart" . | indent 4 }}
|
||||||
|
name: {{ template "reloader-name" . }}-role
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- secrets
|
||||||
|
- configmaps
|
||||||
|
verbs:
|
||||||
|
- list
|
||||||
|
- get
|
||||||
|
- watch
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{ include "reloader-labels.stakater" . | indent 4 }}
|
||||||
|
{{ include "reloader-labels.chart" . | indent 4 }}
|
||||||
|
name: {{ template "reloader-name" . }}-role-binding
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: {{ template "reloader-name" . }}-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: {{ template "reloader-name" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated from deployments/kubernetes/templates/chart/values.yaml.tmpl
|
||||||
|
|
||||||
|
kubernetes:
|
||||||
|
host: https://kubernetes.default
|
||||||
|
|
||||||
|
reloader:
|
||||||
|
labels:
|
||||||
|
provider: stakater
|
||||||
|
group: com.stakater.platform
|
||||||
|
version: 0.0.1
|
||||||
|
image:
|
||||||
|
name: stakater/reloader
|
||||||
|
tag: "0.0.1"
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Generated from deployments/kubernetes/templates/chart/Chart.yaml.tmpl
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
name: reloader
|
||||||
|
description: Reloader chart that runs on kubernetes
|
||||||
|
version: {{ getenv "VERSION" }}
|
||||||
|
keywords:
|
||||||
|
- Reloader
|
||||||
|
- kubernetes
|
||||||
|
home: https://github.com/stakater/Reloader
|
||||||
|
maintainers:
|
||||||
|
- name: Stakater
|
||||||
|
email: hello@stakater.com
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated from deployments/kubernetes/templates/chart/values.yaml.tmpl
|
||||||
|
|
||||||
|
kubernetes:
|
||||||
|
host: https://kubernetes.default
|
||||||
|
|
||||||
|
reloader:
|
||||||
|
labels:
|
||||||
|
provider: stakater
|
||||||
|
group: com.stakater.platform
|
||||||
|
version: {{ getenv "VERSION" }}
|
||||||
|
image:
|
||||||
|
name: {{ getenv "DOCKER_IMAGE" }}
|
||||||
|
tag: "{{ getenv "VERSION" }}"
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package: github.com/stakater/Reloader
|
||||||
|
import:
|
||||||
|
- package: k8s.io/api
|
||||||
|
version: kubernetes-1.8.0
|
||||||
|
- package: k8s.io/apimachinery
|
||||||
|
version: kubernetes-1.8.0
|
||||||
|
- package: k8s.io/client-go
|
||||||
|
version: 5.0.0
|
||||||
|
- package: github.com/spf13/cobra
|
||||||
|
version: 0.0.3
|
||||||
|
- package: github.com/spf13/pflag
|
||||||
|
version: 1.0.1
|
||||||
|
- package: github.com/sirupsen/logrus
|
||||||
|
version: 1.0.5
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import "github.com/stakater/Reloader/internal/pkg/cmd"
|
||||||
|
|
||||||
|
// Run runs the command
|
||||||
|
func Run() error {
|
||||||
|
cmd := cmd.NewReloaderCommand()
|
||||||
|
return cmd.Execute()
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stakater/Reloader/internal/pkg/controller"
|
||||||
|
"github.com/stakater/Reloader/pkg/kube"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewReloaderCommand starts the reloader controller
|
||||||
|
func NewReloaderCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "reloader",
|
||||||
|
Short: "A watcher for your Kubernetes cluster",
|
||||||
|
Run: startReloader,
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func startReloader(cmd *cobra.Command, args []string) {
|
||||||
|
logrus.Info("Starting Reloader")
|
||||||
|
currentNamespace := os.Getenv("KUBERNETES_NAMESPACE")
|
||||||
|
if len(currentNamespace) == 0 {
|
||||||
|
currentNamespace = v1.NamespaceAll
|
||||||
|
logrus.Warnf("KUBERNETES_NAMESPACE is unset, will detect changes in all namespaces.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the clientset
|
||||||
|
clientset, err := kube.GetClient()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range kube.ResourceMap {
|
||||||
|
c, err := controller.NewController(clientset, k, currentNamespace)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now let's start the controller
|
||||||
|
stop := make(chan struct{})
|
||||||
|
defer close(stop)
|
||||||
|
|
||||||
|
go c.Run(1, stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait forever
|
||||||
|
select {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stakater/Reloader/internal/pkg/handler"
|
||||||
|
"github.com/stakater/Reloader/pkg/kube"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
errorHandler "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
"k8s.io/client-go/util/workqueue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller for checking events
|
||||||
|
type Controller struct {
|
||||||
|
client kubernetes.Interface
|
||||||
|
indexer cache.Indexer
|
||||||
|
queue workqueue.RateLimitingInterface
|
||||||
|
informer cache.Controller
|
||||||
|
namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewController for initializing a Controller
|
||||||
|
func NewController(
|
||||||
|
client kubernetes.Interface, resource string, namespace string) (*Controller, error) {
|
||||||
|
|
||||||
|
c := Controller{
|
||||||
|
client: client,
|
||||||
|
namespace: namespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
|
||||||
|
listWatcher := cache.NewListWatchFromClient(client.CoreV1().RESTClient(), resource, namespace, fields.Everything())
|
||||||
|
|
||||||
|
indexer, informer := cache.NewIndexerInformer(listWatcher, kube.ResourceMap[resource], 0, cache.ResourceEventHandlerFuncs{
|
||||||
|
AddFunc: c.Add,
|
||||||
|
UpdateFunc: c.Update,
|
||||||
|
DeleteFunc: c.Delete,
|
||||||
|
}, cache.Indexers{})
|
||||||
|
c.indexer = indexer
|
||||||
|
c.informer = informer
|
||||||
|
c.queue = queue
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add function to add a new object to the queue in case of creating a resource
|
||||||
|
func (c *Controller) Add(obj interface{}) {
|
||||||
|
c.queue.Add(handler.ResourceCreatedHandler{
|
||||||
|
Resource: obj,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update function to add an old object and a new object to the queue in case of updating a resource
|
||||||
|
func (c *Controller) Update(old interface{}, new interface{}) {
|
||||||
|
c.queue.Add(handler.ResourceUpdatedHandler{
|
||||||
|
Resource: new,
|
||||||
|
OldResource: old,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete function to add an object to the queue in case of deleting a resource
|
||||||
|
func (c *Controller) Delete(old interface{}) {
|
||||||
|
// TODO Added this function for future usecase
|
||||||
|
logrus.Infof("Deleted resource has been detected but no further implementation found to take action")
|
||||||
|
}
|
||||||
|
|
||||||
|
//Run function for controller which handles the queue
|
||||||
|
func (c *Controller) Run(threadiness int, stopCh chan struct{}) {
|
||||||
|
|
||||||
|
logrus.Infof("Starting Controller")
|
||||||
|
defer errorHandler.HandleCrash()
|
||||||
|
|
||||||
|
// Let the workers stop when we are done
|
||||||
|
defer c.queue.ShutDown()
|
||||||
|
|
||||||
|
go c.informer.Run(stopCh)
|
||||||
|
|
||||||
|
// Wait for all involved caches to be synced, before processing items from the queue is started
|
||||||
|
if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {
|
||||||
|
errorHandler.HandleError(fmt.Errorf("Timed out waiting for caches to sync"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < threadiness; i++ {
|
||||||
|
go wait.Until(c.runWorker, time.Second, stopCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-stopCh
|
||||||
|
logrus.Infof("Stopping Controller")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) runWorker() {
|
||||||
|
for c.processNextItem() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) processNextItem() bool {
|
||||||
|
// Wait until there is a new item in the working queue
|
||||||
|
resourceHandler, quit := c.queue.Get()
|
||||||
|
if quit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Tell the queue that we are done with processing this key. This unblocks the key for other workers
|
||||||
|
// This allows safe parallel processing because two events with the same key are never processed in
|
||||||
|
// parallel.
|
||||||
|
defer c.queue.Done(resourceHandler)
|
||||||
|
|
||||||
|
// Invoke the method containing the business logic
|
||||||
|
err := resourceHandler.(handler.ResourceHandler).Handle()
|
||||||
|
// Handle the error if something went wrong during the execution of the business logic
|
||||||
|
c.handleErr(err, resourceHandler)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleErr checks if an error happened and makes sure we will retry later.
|
||||||
|
func (c *Controller) handleErr(err error, key interface{}) {
|
||||||
|
if err == nil {
|
||||||
|
// Forget about the #AddRateLimited history of the key on every successful synchronization.
|
||||||
|
// This ensures that future processing of updates for this key is not delayed because of
|
||||||
|
// an outdated error history.
|
||||||
|
c.queue.Forget(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// This controller retries 5 times if something goes wrong. After that, it stops trying.
|
||||||
|
if c.queue.NumRequeues(key) < 5 {
|
||||||
|
logrus.Errorf("Error syncing events %v: %v", key, err)
|
||||||
|
|
||||||
|
// Re-enqueue the key rate limited. Based on the rate limiter on the
|
||||||
|
// queue and the re-enqueue history, the key will be processed later again.
|
||||||
|
c.queue.AddRateLimited(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.queue.Forget(key)
|
||||||
|
// Report to an external entity that, even after several retries, we could not successfully process this key
|
||||||
|
runtime.HandleError(err)
|
||||||
|
logrus.Infof("Dropping the key %q out of the queue: %v", key, err)
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stakater/Reloader/pkg/kube"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
"k8s.io/api/extensions/v1beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configmapNamePrefix = "testconfigmap-reloader"
|
||||||
|
secretNamePrefix = "testsecret-reloader"
|
||||||
|
letters = []rune("abcdefghijklmnopqrstuvwxyz")
|
||||||
|
)
|
||||||
|
|
||||||
|
func randSeq(n int) string {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
b := make([]rune, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letters[rand.Intn(len(letters))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creating a Controller to do a rolling upgrade upon updating the configmap or secret
|
||||||
|
func TestControllerForUpdatingConfigmapShouldUpdateDeployment(t *testing.T) {
|
||||||
|
client, err := kube.GetClient()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Unable to create Kubernetes client error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
namespace := "test-reloader"
|
||||||
|
createNamespace(t, namespace, client)
|
||||||
|
defer deleteNamespace(t, namespace, client)
|
||||||
|
|
||||||
|
controller, err := NewController(client, "configMaps", namespace)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Unable to create NewController error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stop := make(chan struct{})
|
||||||
|
defer close(stop)
|
||||||
|
go controller.Run(1, stop)
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
configmapName := configmapNamePrefix + "-update-" + randSeq(5)
|
||||||
|
configmapClient := client.CoreV1().ConfigMaps(namespace)
|
||||||
|
_, err = configmapClient.Create(initConfigmap(namespace, configmapName))
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("Fatal error in configmap creation: %v", err)
|
||||||
|
}
|
||||||
|
logrus.Infof("Created Configmap %q.\n", configmapName)
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
deployment := createDeployement(configmapName, namespace, client)
|
||||||
|
|
||||||
|
logrus.Infof("Updating Configmap %q.\n", configmapName)
|
||||||
|
_, err = configmapClient.Get(configmapName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Error while getting configmap %v", err)
|
||||||
|
}
|
||||||
|
_, updateErr := configmapClient.Update(updateConfigmap(namespace, configmapName))
|
||||||
|
|
||||||
|
// TODO: Add functionality to verify reloader functionality here
|
||||||
|
|
||||||
|
if updateErr != nil {
|
||||||
|
err = controller.client.CoreV1().ConfigMaps(namespace).Delete(configmapName, &metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Error while deleting the configmap %v", err)
|
||||||
|
}
|
||||||
|
logrus.Fatalf("Fatal error in configmap update: %v", updateErr)
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
logrus.Infof("Deleting Deployment %q.\n", deployment.GetObjectMeta().GetName())
|
||||||
|
deploymentError := controller.client.ExtensionsV1beta1().Deployments(namespace).Delete(configmapName, &metav1.DeleteOptions{})
|
||||||
|
if deploymentError != nil {
|
||||||
|
logrus.Fatalf("Error while deleting the configmap %v", deploymentError)
|
||||||
|
}
|
||||||
|
logrus.Infof("Deleting Configmap %q.\n", configmapName)
|
||||||
|
err = controller.client.CoreV1().ConfigMaps(namespace).Delete(configmapName, &metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Error while deleting the configmap %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDeployement(deploymentName string, namespace string, client kubernetes.Interface) *v1beta1.Deployment {
|
||||||
|
deploymentClient := client.ExtensionsV1beta1().Deployments(namespace)
|
||||||
|
deployment := initDeployment(namespace, deploymentName)
|
||||||
|
deployment, err := deploymentClient.Create(deployment)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("Fatal error in deployment creation: %v", err)
|
||||||
|
}
|
||||||
|
logrus.Infof("Created Deployment %q.\n", deployment.GetObjectMeta().GetName())
|
||||||
|
return deployment
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestControllerForUpdatingSecretShouldUpdateDeployment(t *testing.T) {
|
||||||
|
client, err := kube.GetClient()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Unable to create Kubernetes client error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
namespace := "test-reloader-secrets"
|
||||||
|
createNamespace(t, namespace, client)
|
||||||
|
defer deleteNamespace(t, namespace, client)
|
||||||
|
|
||||||
|
controller, err := NewController(client, "secrets", namespace)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Unable to create NewController error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stop := make(chan struct{})
|
||||||
|
defer close(stop)
|
||||||
|
go controller.Run(1, stop)
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
secretName := secretNamePrefix + "-update-" + randSeq(5)
|
||||||
|
secretClient := client.CoreV1().Secrets(namespace)
|
||||||
|
_, err = secretClient.Create(initSecret(namespace, secretName))
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("Fatal error in secret creation: %v", err)
|
||||||
|
}
|
||||||
|
logrus.Infof("Created Secret %q.\n", secretName)
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
logrus.Infof("Updating Secret %q.\n", secretName)
|
||||||
|
_, err = secretClient.Get(secretName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Error while getting secret %v", err)
|
||||||
|
}
|
||||||
|
_, updateErr := secretClient.Update(updateSecret(namespace, secretName))
|
||||||
|
|
||||||
|
// TODO: Add functionality to verify reloader functionality here
|
||||||
|
|
||||||
|
if updateErr != nil {
|
||||||
|
err := controller.client.CoreV1().Secrets(namespace).Delete(secretName, &metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Error while deleting the secret %v", err)
|
||||||
|
}
|
||||||
|
logrus.Errorf("Error while updating the secret %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
logrus.Infof("Deleting Secret %q.\n", secretName)
|
||||||
|
err = controller.client.CoreV1().Secrets(namespace).Delete(secretName, &metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Error while deleting the secret %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initConfigmap(namespace string, configmapName string) *v1.ConfigMap {
|
||||||
|
return &v1.ConfigMap{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: configmapName,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: map[string]string{"firstLabel": "temp"},
|
||||||
|
},
|
||||||
|
Data: map[string]string{"test.url": "www.google.com"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDeployment(namespace string, deploymentName string) *v1beta1.Deployment {
|
||||||
|
replicaset := int32(1)
|
||||||
|
return &v1beta1.Deployment{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: deploymentName,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: map[string]string{"firstLabel": "temp"},
|
||||||
|
Annotations: map[string]string{"reloader.stakater.com/update-on-change": deploymentName},
|
||||||
|
},
|
||||||
|
Spec: v1beta1.DeploymentSpec{
|
||||||
|
Replicas: &replicaset,
|
||||||
|
Strategy: v1beta1.DeploymentStrategy{
|
||||||
|
Type: v1beta1.RollingUpdateDeploymentStrategyType,
|
||||||
|
},
|
||||||
|
Template: v1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: map[string]string{"secondLabel": "temp"},
|
||||||
|
},
|
||||||
|
Spec: v1.PodSpec{
|
||||||
|
Containers: []v1.Container{
|
||||||
|
{
|
||||||
|
Image: "tutum/hello-world",
|
||||||
|
Name: deploymentName,
|
||||||
|
Env: []v1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "BUCKET_NAME",
|
||||||
|
Value: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSecret(namespace string, secretName string) *v1.Secret {
|
||||||
|
return &v1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: secretName,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: map[string]string{"firstLabel": "temp"},
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{"test.url": []byte("dGVzdFNlY3JldEVuY29kaW5nRm9yUmVsb2FkZXI=")},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNamespace(t *testing.T, namespace string, client kubernetes.Interface) {
|
||||||
|
_, err := client.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Failed to create namespace for testing", err)
|
||||||
|
} else {
|
||||||
|
logrus.Infof("Creating namespace for testing = %s", namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteNamespace(t *testing.T, namespace string, client kubernetes.Interface) {
|
||||||
|
err := client.CoreV1().Namespaces().Delete(namespace, &metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Failed to delete namespace that was created for testing", err)
|
||||||
|
} else {
|
||||||
|
logrus.Infof("Deleting namespace for testing = %s", namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateConfigmap(namespace string, configmapName string) *v1.ConfigMap {
|
||||||
|
return &v1.ConfigMap{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: configmapName,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: map[string]string{"firstLabel": "temp"},
|
||||||
|
},
|
||||||
|
Data: map[string]string{"test.url": "www.stakater.com"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSecret(namespace string, secretName string) *v1.Secret {
|
||||||
|
return &v1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: secretName,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: map[string]string{"firstLabel": "temp"},
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{"test.url": []byte("dGVzdFVwZGF0ZWRTZWNyZXRFbmNvZGluZ0ZvclJlbG9hZGVy")},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceCreatedHandler contains new objects
|
||||||
|
type ResourceCreatedHandler struct {
|
||||||
|
Resource interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle processes the newly created resource
|
||||||
|
func (r ResourceCreatedHandler) Handle() error {
|
||||||
|
if r.Resource == nil {
|
||||||
|
logrus.Errorf("Error in Handler")
|
||||||
|
} else {
|
||||||
|
logrus.Infof("Detected changes in object %s", r.Resource)
|
||||||
|
// process resource based on its type
|
||||||
|
if _, ok := r.Resource.(*v1.ConfigMap); ok {
|
||||||
|
logrus.Infof("Performing 'Added' action for resource of type 'configmap'")
|
||||||
|
} else if _, ok := r.Resource.(*v1.Secret); ok {
|
||||||
|
logrus.Infof("Performing 'Added' action for resource of type 'secret'")
|
||||||
|
} else {
|
||||||
|
logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found %v", r.Resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
// ResourceHandler handles the creation and update of resources
|
||||||
|
type ResourceHandler interface {
|
||||||
|
Handle() error
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceUpdatedHandler contains updated objects
|
||||||
|
type ResourceUpdatedHandler struct {
|
||||||
|
Resource interface{}
|
||||||
|
OldResource interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle processes the updated resource
|
||||||
|
func (r ResourceUpdatedHandler) Handle() error {
|
||||||
|
if r.Resource == nil || r.OldResource == nil {
|
||||||
|
logrus.Errorf("Error in Handler")
|
||||||
|
} else {
|
||||||
|
logrus.Infof("Detected changes in object %s", r.Resource)
|
||||||
|
// process resource based on its type
|
||||||
|
if _, ok := r.Resource.(*v1.ConfigMap); ok {
|
||||||
|
logrus.Infof("Performing 'Updated' action for resource of type 'configmap'")
|
||||||
|
} else if _, ok := r.Resource.(*v1.Secret); ok {
|
||||||
|
logrus.Infof("Performing 'Updated' action for resource of type 'secret'")
|
||||||
|
} else {
|
||||||
|
logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found %v", r.Resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/stakater/Reloader/internal/pkg/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := app.Run(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package kube
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetClient gets the client for k8s, if ~/.kube/config exists so get that config else incluster config
|
||||||
|
func GetClient() (*kubernetes.Clientset, error) {
|
||||||
|
var config *rest.Config
|
||||||
|
var err error
|
||||||
|
kubeconfigPath := os.Getenv("KUBECONFIG")
|
||||||
|
if kubeconfigPath == "" {
|
||||||
|
kubeconfigPath = os.Getenv("HOME") + "/.kube/config"
|
||||||
|
}
|
||||||
|
//If file exists so use that config settings
|
||||||
|
if _, err := os.Stat(kubeconfigPath); err == nil {
|
||||||
|
config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else { //Use Incluster Configuration
|
||||||
|
config, err = rest.InClusterConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return kubernetes.NewForConfig(config)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package kube
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceMap are resources from where changes are going to be detected
|
||||||
|
var ResourceMap = map[string]runtime.Object{
|
||||||
|
"configMaps": &v1.ConfigMap{},
|
||||||
|
"secrets": &v1.Secret{},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user