diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..234d3a6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.swp +_dist/ +.idea +golib +release +out/ +_gopath/ +.DS_Store +vendor \ No newline at end of file diff --git a/.version b/.version new file mode 100644 index 00000000..8a9ecc2e --- /dev/null +++ b/.version @@ -0,0 +1 @@ +0.0.1 \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..8d76442b --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,8 @@ +#!/usr/bin/groovy +@Library('github.com/stakater/fabric8-pipeline-library@v2.4.0') + +def dummy + +goBuildAndRelease { + +} diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..55e2a25f --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index ba495473..63e2d63a 100644 --- a/README.md +++ b/README.md @@ -1 +1,96 @@ -# Reloader \ No newline at end of file +# 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 +kubectl apply -f deployment.yaml -n +``` + +### 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 + +[![Join Slack](https://stakater.github.io/README/stakater-join-slack-btn.png)](https://stakater-slack.herokuapp.com/) +[![Chat](https://stakater.github.io/README/stakater-chat-btn.png)](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 + +See [our other projects][community] +or contact us in case of professional services and queries on + + [website]: http://stakater.com/ + [community]: https://github.com/stakater/ diff --git a/build/package/Dockerfile.build b/build/package/Dockerfile.build new file mode 100644 index 00000000..517ee45e --- /dev/null +++ b/build/package/Dockerfile.build @@ -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 diff --git a/build/package/Dockerfile.run b/build/package/Dockerfile.run new file mode 100644 index 00000000..379698fb --- /dev/null +++ b/build/package/Dockerfile.run @@ -0,0 +1,8 @@ +FROM alpine:3.4 +MAINTAINER "Stakater Team" + +RUN apk add --update ca-certificates + +COPY Reloader /bin/Reloader + +ENTRYPOINT ["/bin/Reloader"] diff --git a/deployments/kubernetes/chart/reloader/Chart.yaml b/deployments/kubernetes/chart/reloader/Chart.yaml new file mode 100644 index 00000000..deb48769 --- /dev/null +++ b/deployments/kubernetes/chart/reloader/Chart.yaml @@ -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 \ No newline at end of file diff --git a/deployments/kubernetes/chart/reloader/templates/_helpers.tpl b/deployments/kubernetes/chart/reloader/templates/_helpers.tpl new file mode 100644 index 00000000..82aa1448 --- /dev/null +++ b/deployments/kubernetes/chart/reloader/templates/_helpers.tpl @@ -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 -}} \ No newline at end of file diff --git a/deployments/kubernetes/chart/reloader/templates/deployment.yaml b/deployments/kubernetes/chart/reloader/templates/deployment.yaml new file mode 100644 index 00000000..0b562a65 --- /dev/null +++ b/deployments/kubernetes/chart/reloader/templates/deployment.yaml @@ -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" . }} + \ No newline at end of file diff --git a/deployments/kubernetes/chart/reloader/templates/rbac.yaml b/deployments/kubernetes/chart/reloader/templates/rbac.yaml new file mode 100644 index 00000000..678725d0 --- /dev/null +++ b/deployments/kubernetes/chart/reloader/templates/rbac.yaml @@ -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 }} \ No newline at end of file diff --git a/deployments/kubernetes/chart/reloader/values.yaml b/deployments/kubernetes/chart/reloader/values.yaml new file mode 100644 index 00000000..a0ab5ab1 --- /dev/null +++ b/deployments/kubernetes/chart/reloader/values.yaml @@ -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 \ No newline at end of file diff --git a/deployments/kubernetes/templates/chart/Chart.yaml.tmpl b/deployments/kubernetes/templates/chart/Chart.yaml.tmpl new file mode 100644 index 00000000..b270c31e --- /dev/null +++ b/deployments/kubernetes/templates/chart/Chart.yaml.tmpl @@ -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 \ No newline at end of file diff --git a/deployments/kubernetes/templates/chart/values.yaml.tmpl b/deployments/kubernetes/templates/chart/values.yaml.tmpl new file mode 100644 index 00000000..8d6a0c57 --- /dev/null +++ b/deployments/kubernetes/templates/chart/values.yaml.tmpl @@ -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 \ No newline at end of file diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 00000000..d56557dc --- /dev/null +++ b/glide.yaml @@ -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 diff --git a/internal/pkg/app/app.go b/internal/pkg/app/app.go new file mode 100644 index 00000000..8d09188f --- /dev/null +++ b/internal/pkg/app/app.go @@ -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() +} diff --git a/internal/pkg/cmd/reloader.go b/internal/pkg/cmd/reloader.go new file mode 100644 index 00000000..c3fea3b4 --- /dev/null +++ b/internal/pkg/cmd/reloader.go @@ -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 {} +} diff --git a/internal/pkg/controller/controller.go b/internal/pkg/controller/controller.go new file mode 100644 index 00000000..e14ead7e --- /dev/null +++ b/internal/pkg/controller/controller.go @@ -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) +} diff --git a/internal/pkg/controller/controller_test.go b/internal/pkg/controller/controller_test.go new file mode 100644 index 00000000..0035d0fb --- /dev/null +++ b/internal/pkg/controller/controller_test.go @@ -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")}, + } +} diff --git a/internal/pkg/handler/created-handler.go b/internal/pkg/handler/created-handler.go new file mode 100644 index 00000000..ac612c4b --- /dev/null +++ b/internal/pkg/handler/created-handler.go @@ -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 +} diff --git a/internal/pkg/handler/handler.go b/internal/pkg/handler/handler.go new file mode 100644 index 00000000..20b28ee7 --- /dev/null +++ b/internal/pkg/handler/handler.go @@ -0,0 +1,6 @@ +package handler + +// ResourceHandler handles the creation and update of resources +type ResourceHandler interface { + Handle() error +} diff --git a/internal/pkg/handler/updated-handler.go b/internal/pkg/handler/updated-handler.go new file mode 100644 index 00000000..97d4f7ba --- /dev/null +++ b/internal/pkg/handler/updated-handler.go @@ -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 +} diff --git a/main.go b/main.go new file mode 100644 index 00000000..1c429710 --- /dev/null +++ b/main.go @@ -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) +} diff --git a/pkg/kube/client.go b/pkg/kube/client.go new file mode 100644 index 00000000..11c61e16 --- /dev/null +++ b/pkg/kube/client.go @@ -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) +} diff --git a/pkg/kube/resourcemapper.go b/pkg/kube/resourcemapper.go new file mode 100644 index 00000000..bf62cc01 --- /dev/null +++ b/pkg/kube/resourcemapper.go @@ -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{}, +} diff --git a/stk.yaml b/stk.yaml new file mode 100644 index 00000000..e30e7c8f --- /dev/null +++ b/stk.yaml @@ -0,0 +1,4 @@ +issues: + kind: 1 + url: https://aurorasolutions.atlassian.net + project: STK \ No newline at end of file