Implement PR-1 review comments

This commit is contained in:
faizanahmad055
2018-07-11 20:20:48 +05:00
parent ade1e48e34
commit 848745f40a
8 changed files with 257 additions and 191 deletions
+75 -4
View File
@@ -1,8 +1,16 @@
# Reloader
# RELOADER
This controller watches for changes to `ConfigMap` and `Secret` objects and performs rolling upgrades on their associated deployments, deamonsets and statefulsets and updating dynamically.
## WHY NAME RELOADER
This is particularly useful if the `ConfigMap` is used to define environment variables - or your app cannot easily and reliably watch the `ConfigMap` and update itself on the fly.
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 then performs rolling upgrades on their associated `Deployments`, `Deamonsets` and `Statefulsets` and updating these dynamically.
**NOTE:** This controller has been inspired from [configmapController](https://github.com/fabric8io/configmapcontroller)
@@ -22,4 +30,67 @@ Then, providing `Reloader` is running, whenever you edit the `ConfigMap` called
STAKATER_FOO_REVISION=${reloaderRevision}
```
This then triggers a rolling upgrade of your deployment's pods to use the new configuration.
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
[![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 <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/
@@ -1,8 +1,6 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
annotations:
configmap.fabric8.io/update-on-change: {{ template "reloader-name" . }}
labels:
{{ include "reloader-labels.stakater" . | indent 4 }}
{{ include "reloader-labels.chart" . | indent 4 }}
@@ -15,8 +13,6 @@ spec:
{{ include "reloader-labels.selector" . | indent 6 }}
template:
metadata:
annotations:
configmap.fabric8.io/update-on-change: {{ template "reloader-name" . }}
labels:
{{ include "reloader-labels.selector" . | indent 8 }}
spec:
+2 -2
View File
@@ -7,8 +7,8 @@ import:
- package: k8s.io/client-go
version: 5.0.0
- package: github.com/spf13/cobra
version: ef82de70bb3f60c65fb8eebacbb2d122ef517385
version: 0.0.3
- package: github.com/spf13/pflag
version: 583c0c0531f06d5278b7d917446061adc344b5cd
version: 1.0.1
- package: github.com/sirupsen/logrus
version: ~1.0.3
+10 -2
View File
@@ -1,10 +1,13 @@
package cmd
import (
"os"
"github.com/spf13/cobra"
"github.com/stakater/Reloader/internal/pkg/controller"
"github.com/stakater/Reloader/pkg/kube"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
func NewReloaderCommand() *cobra.Command {
@@ -18,6 +21,11 @@ func NewReloaderCommand() *cobra.Command {
func startReloader(cmd *cobra.Command, args []string) {
logrus.Info("Starting Reloader")
currentNamespace := os.Getenv("KUBERNETES_NAMESPACE")
if len(currentNamespace) == 0 {
currentNamespace = v1.NamespaceAll
logrus.Infof("Warning: KUBERNETES_NAMESPACE is unset, will detect changes in all namespaces.")
}
// create the clientset
clientset, err := kube.GetClient()
@@ -25,8 +33,8 @@ func startReloader(cmd *cobra.Command, args []string) {
logrus.Fatal(err)
}
for k, v := range kube.ResourceMap {
c, err := controller.NewController(clientset, k, v)
for k := range kube.ResourceMap {
c, err := controller.NewController(clientset, k, currentNamespace)
if err != nil {
logrus.Fatalf("%s", err)
}
+12 -165
View File
@@ -3,36 +3,24 @@ package controller
import (
"time"
"fmt"
"strings"
"bytes"
"sort"
"k8s.io/client-go/kubernetes"
"k8s.io/apimachinery/pkg/util/runtime"
informerruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/util/wait"
errorHandler "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"github.com/sirupsen/logrus"
"github.com/pkg/errors"
"k8s.io/api/core/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
updateOnChangeAnnotation = "reloader.stakater.com/update-on-change"
// AllNamespaces as our controller will be looking for events in all namespaces
AllNamespaces = "temp-reloader"
"github.com/stakater/Reloader/pkg/kube"
"github.com/stakater/Reloader/internal/pkg/upgrader"
)
// Event indicate the informerEvent
type Event struct {
key string
eventType string
namespace string
resourceType string
}
// Controller for checking events
@@ -42,28 +30,25 @@ type Controller struct {
queue workqueue.RateLimitingInterface
informer cache.Controller
resource string
stopCh chan struct{}
namespace string
}
// NewController for initializing a Controller
func NewController(
client kubernetes.Interface,
resource string, objType informerruntime.Object) (*Controller, error) {
client kubernetes.Interface, resource string, namespace string) (*Controller, error) {
c := Controller{
client: client,
resource: resource,
stopCh: make(chan struct{}),
namespace: namespace,
}
queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
listWatcher := cache.NewListWatchFromClient(client.CoreV1().RESTClient(), resource, AllNamespaces, fields.Everything())
indexer, informer := cache.NewIndexerInformer(listWatcher, objType, 0, cache.ResourceEventHandlerFuncs {
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
@@ -95,11 +80,6 @@ func (c *Controller) Update(old interface{}, new interface{}) {
}
}
// Delete function to add a 'delete' event to the queue in case of deleting a pod
func (c *Controller) Delete(obj interface{}) {
//In current scenario, we dont need to do anything when a pod is deleted so it is empty now
}
//Run function for controller which handles the queue
func (c *Controller) Run(threadiness int, stopCh chan struct{}) {
@@ -161,14 +141,13 @@ func (c *Controller) takeAction(event Event) error {
logrus.Infof("Detected changes in object %s", obj)
// process events based on its type
logrus.Infof("Performing '%s' action for controller of type '%s'", event.eventType, c.resource)
u, _ := upgrader.NewUpgrader(c.client, c.resource)
if c.resource == "configMaps" {
switch event.eventType {
case "create":
ObjectCreated(obj, c.client)
u.ObjectCreated(obj)
case "update":
ObjectUpdated(obj, c.client)
case "delete":
ObjectDeleted(obj)
u.ObjectUpdated(obj)
}
}
}
@@ -199,136 +178,4 @@ func (c *Controller) handleErr(err error, key interface{}) {
// 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)
}
// ObjectCreated Do nothing for default handler
func ObjectCreated(obj interface{}, client kubernetes.Interface) {
message := "Configmap: `" + obj.(*v1.ConfigMap).Name + "`has been created in Namespace: `" + obj.(*v1.ConfigMap).Namespace + "`"
logrus.Infof(message)
err := rollingUpgradeDeployments(obj, client)
if err != nil {
logrus.Errorf("failed to update Deployment: %v", err)
}
}
// ObjectDeleted Do nothing for default handler
func ObjectDeleted(obj interface{}) {
}
// ObjectUpdated Do nothing for default handler
func ObjectUpdated(oldObj interface{}, client kubernetes.Interface) {
message := "Configmap: `" + oldObj.(*v1.ConfigMap).Name + "`has been updated in Namespace: `" + oldObj.(*v1.ConfigMap).Namespace + "`"
logrus.Infof(message)
err := rollingUpgradeDeployments(oldObj, client)
if err != nil {
logrus.Errorf("failed to update Deployment: %v", err)
}
}
// Implementation has been borrowed from fabric8io/configmapcontroller
// Method has been modified a little to use updated liberaries.
func rollingUpgradeDeployments(oldObj interface{}, client kubernetes.Interface) error {
ns := oldObj.(*v1.ConfigMap).Namespace
configMapName := oldObj.(*v1.ConfigMap).Name
configMapVersion := convertConfigMapToToken(oldObj.(*v1.ConfigMap))
deployments, err := client.Apps().Deployments(ns).List(meta_v1.ListOptions{})
if err != nil {
return errors.Wrap(err, "failed to list deployments")
}
for _, d := range deployments.Items {
containers := d.Spec.Template.Spec.Containers
// match deployments with the correct annotation
annotationValue, _ := d.ObjectMeta.Annotations[updateOnChangeAnnotation]
if annotationValue != "" {
values := strings.Split(annotationValue, ",")
matches := false
for _, value := range values {
if value == configMapName {
matches = true
break
}
}
if matches {
updateContainers(containers, annotationValue, configMapVersion)
// update the deployment
_, err := client.Apps().Deployments(ns).Update(&d)
if err != nil {
return errors.Wrap(err, "update deployment failed")
}
logrus.Infof("Updated Deployment %s", d.Name)
}
}
}
return nil
}
func updateContainers(containers []v1.Container, annotationValue, configMapVersion string) bool {
// we can have multiple configmaps to update
answer := false
configmaps := strings.Split(annotationValue, ",")
for _, cmNameToUpdate := range configmaps {
configmapEnvar := "STAKATER_" + convertToEnvVarName(cmNameToUpdate) + "_CONFIGMAP"
for i := range containers {
envs := containers[i].Env
matched := false
for j := range envs {
if envs[j].Name == configmapEnvar {
matched = true
if envs[j].Value != configMapVersion {
logrus.Infof("Updating %s to %s", configmapEnvar, configMapVersion)
envs[j].Value = configMapVersion
answer = true
}
}
}
// if no existing env var exists lets create one
if !matched {
e := v1.EnvVar{
Name: configmapEnvar,
Value: configMapVersion,
}
containers[i].Env = append(containers[i].Env, e)
answer = true
}
}
}
return answer
}
// convertToEnvVarName converts the given text into a usable env var
// removing any special chars with '_'
func convertToEnvVarName(text string) string {
var buffer bytes.Buffer
lower := strings.ToUpper(text)
lastCharValid := false
for i := 0; i < len(lower); i++ {
ch := lower[i]
if (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') {
buffer.WriteString(string(ch))
lastCharValid = true
} else {
if lastCharValid {
buffer.WriteString("_")
}
lastCharValid = false
}
}
return buffer.String()
}
// lets convert the configmap into a unique token based on the data values
func convertConfigMapToToken(cm *v1.ConfigMap) string {
values := []string{}
for k, v := range cm.Data {
values = append(values, k+"="+v)
}
sort.Strings(values)
text := strings.Join(values, ";")
// we could zip and base64 encode
// but for now we could leave this easy to read so that its easier to diagnose when & why things changed
return text
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
)
var (
client, _ = kube.GetClient()
client, err = kube.GetClient()
configmapNamePrefix = "testconfigmap-reloader"
letters = []rune("abcdefghijklmnopqrstuvwxyz")
)
+157
View File
@@ -0,0 +1,157 @@
package upgrader
import (
"bytes"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"k8s.io/api/core/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
const (
updateOnChangeAnnotation = "reloader.stakater.com/update-on-change"
)
type Upgrader struct{
client kubernetes.Interface
resourceType string
}
func NewUpgrader(client kubernetes.Interface, resourceType string) (*Upgrader, error) {
u := Upgrader{
client: client,
resourceType: resourceType,
}
return &u, nil
}
// ObjectCreated Detects if the configmap or secret has been created
func (u *Upgrader)ObjectCreated(obj interface{}) {
message := u.resourceType+": `" + obj.(*v1.ConfigMap).Name + "`has been created in Namespace: `" + obj.(*v1.ConfigMap).Namespace + "`"
logrus.Infof(message)
err := rollingUpgradeDeployments(obj, u.client)
if err != nil {
logrus.Errorf("failed to update Deployment: %v", err)
}
}
// ObjectUpdated Detects if the configmap or secret has been updated
func (u *Upgrader)ObjectUpdated(oldObj interface{}) {
message := u.resourceType+": `" + oldObj.(*v1.ConfigMap).Name + "`has been updated in Namespace: `" + oldObj.(*v1.ConfigMap).Namespace + "`"
logrus.Infof(message)
err := rollingUpgradeDeployments(oldObj, u.client)
if err != nil {
logrus.Errorf("failed to update Deployment: %v", err)
}
}
// Implementation has been borrowed from fabric8io/configmapcontroller
// Method has been modified a little to use updated liberaries.
func rollingUpgradeDeployments(oldObj interface{}, client kubernetes.Interface) error {
ns := oldObj.(*v1.ConfigMap).Namespace
configMapName := oldObj.(*v1.ConfigMap).Name
configMapVersion := convertConfigMapToToken(oldObj.(*v1.ConfigMap))
deployments, err := client.AppsV1().Deployments(ns).List(meta_v1.ListOptions{})
if err != nil {
return errors.Wrap(err, "failed to list deployments")
}
for _, d := range deployments.Items {
containers := d.Spec.Template.Spec.Containers
// match deployments with the correct annotation
annotationValue := d.ObjectMeta.Annotations[updateOnChangeAnnotation]
if annotationValue != "" {
values := strings.Split(annotationValue, ",")
matches := false
for _, value := range values {
if value == configMapName {
matches = true
break
}
}
if matches {
updateContainers(containers, annotationValue, configMapVersion)
// update the deployment
_, err := client.AppsV1().Deployments(ns).Update(&d)
if err != nil {
return errors.Wrap(err, "update deployment failed")
}
logrus.Infof("Updated Deployment %s", d.Name)
}
}
}
return nil
}
func updateContainers(containers []v1.Container, annotationValue, configMapVersion string) bool {
// we can have multiple configmaps to update
answer := false
configmaps := strings.Split(annotationValue, ",")
for _, cmNameToUpdate := range configmaps {
configmapEnvar := "STAKATER_" + convertToEnvVarName(cmNameToUpdate) + "_CONFIGMAP"
for i := range containers {
envs := containers[i].Env
matched := false
for j := range envs {
if envs[j].Name == configmapEnvar {
matched = true
if envs[j].Value != configMapVersion {
logrus.Infof("Updating %s to %s", configmapEnvar, configMapVersion)
envs[j].Value = configMapVersion
answer = true
}
}
}
// if no existing env var exists lets create one
if !matched {
e := v1.EnvVar{
Name: configmapEnvar,
Value: configMapVersion,
}
containers[i].Env = append(containers[i].Env, e)
answer = true
}
}
}
return answer
}
// convertToEnvVarName converts the given text into a usable env var
// removing any special chars with '_'
func convertToEnvVarName(text string) string {
var buffer bytes.Buffer
lower := strings.ToUpper(text)
lastCharValid := false
for i := 0; i < len(lower); i++ {
ch := lower[i]
if (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') {
buffer.WriteString(string(ch))
lastCharValid = true
} else {
if lastCharValid {
buffer.WriteString("_")
}
lastCharValid = false
}
}
return buffer.String()
}
// lets convert the configmap into a unique token based on the data values
func convertConfigMapToToken(cm *v1.ConfigMap) string {
values := []string{}
for k, v := range cm.Data {
values = append(values, k+"="+v)
}
sort.Strings(values)
text := strings.Join(values, ";")
// we could zip and base64 encode
// but for now we could leave this easy to read so that its easier to diagnose when & why things changed
return text
}
-13
View File
@@ -5,19 +5,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)
const (
DefaultResource = "default"
)
// MapToRuntimeObject maps the resource type string to the actual resource
func MapToRuntimeObject(resourceType string) runtime.Object {
rType, ok := ResourceMap[resourceType]
if !ok {
return ResourceMap[DefaultResource]
}
return rType
}
// ResourceMap are resources from where changes are going to be detected
var ResourceMap = map[string]runtime.Object{
"configMaps": &v1.ConfigMap{},