├── .VERSION ├── .gitignore ├── .goreleaser.yaml ├── .version ├── Jenkinsfile ├── LICENSE ├── Makefile ├── README.md ├── assets └── web │ └── jamadar-round-100px.png ├── build ├── README.md ├── ci │ └── .keep └── package │ ├── .keep │ ├── Dockerfile │ ├── Dockerfile.build │ ├── Dockerfile.run │ ├── jamadaar │ └── jamadar ├── cmd └── app │ └── app.go ├── configs ├── config.yaml └── testConfigs │ ├── CorrectSlackConfig.yaml │ └── Empty.yaml ├── deployments └── kubernetes │ ├── chart │ └── jamadar │ │ ├── Chart.yaml │ │ ├── templates │ │ ├── _helpers.tpl │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ └── rbac.yaml │ │ └── values.yaml │ ├── jamadar.yaml │ ├── manifests │ ├── configmap.yaml │ ├── deployment.yaml │ └── rbac.yaml │ └── templates │ └── chart │ ├── Chart.yaml.tmpl │ └── values.yaml.tmpl ├── docs ├── .keep ├── design │ ├── DELETE-NAMESPACES.md │ └── README.md └── user │ └── README.md ├── glide.lock ├── glide.yaml ├── internal └── pkg │ ├── actions │ ├── action.go │ ├── populate.go │ ├── populate_test.go │ └── slack │ │ ├── slack.go │ │ └── slack_test.go │ ├── cmd │ └── Jamadar.go │ ├── config │ ├── config.go │ └── config_test.go │ ├── controller │ ├── controller.go │ └── controller_test.go │ └── tasks │ ├── namespaces │ ├── namespaces.go │ └── namespaces_test.go │ ├── tasks.go │ └── tasks_test.go ├── main.go ├── pkg └── kube │ └── client.go └── stk.yaml /.VERSION: -------------------------------------------------------------------------------- 1 | version: v0.0.18 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | vendor 15 | 16 | .idea -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | - CGO_ENABLED=0 4 | goos: 5 | - windows 6 | - darwin 7 | - linux 8 | goarch: 9 | - 386 10 | - amd64 11 | - arm 12 | - arm64 13 | archives: 14 | - name_template: "{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 15 | snapshot: 16 | name_template: "{{ .Tag }}-next" 17 | checksum: 18 | name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" 19 | changelog: 20 | sort: asc 21 | filters: 22 | exclude: 23 | - '^docs:' 24 | - '^test:' 25 | env_files: 26 | github_token: /home/jenkins/.apitoken/hub 27 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | v0.0.15 2 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/groovy 2 | @Library('github.com/stakater/stakater-pipeline-library@v2.16.24') _ 3 | 4 | goBuildViaGoReleaser { 5 | publicChartRepositoryURL = 'https://stakater.github.io/stakater-charts' 6 | publicChartGitURL = 'git@github.com:stakater/stakater-charts.git' 7 | toolsImage = 'stakater/pipeline-tools:v2.0.18' 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # note: call scripts from /scripts 2 | 3 | .PHONY: default build builder-image binary-image test stop clean-images clean push apply deploy 4 | 5 | BUILDER ?= jamadar-builder 6 | BINARY ?= Jamadar 7 | DOCKER_IMAGE ?= stakater/jamadar 8 | # Default value "dev" 9 | DOCKER_TAG ?= dev 10 | REPOSITORY = ${DOCKER_IMAGE}:${DOCKER_TAG} 11 | 12 | VERSION=$(shell cat .version) 13 | BUILD= 14 | 15 | GOCMD = go 16 | GLIDECMD = glide 17 | GOFLAGS ?= $(GOFLAGS:) 18 | LDFLAGS = 19 | 20 | default: build test 21 | 22 | install: 23 | "$(GLIDECMD)" install 24 | 25 | build: 26 | "$(GOCMD)" build ${GOFLAGS} ${LDFLAGS} -o "${BINARY}" 27 | 28 | builder-image: 29 | @docker build --network host -t "${BUILDER}" -f build/package/Dockerfile.build . 30 | 31 | binary-image: builder-image 32 | @docker run --network host --rm "${BUILDER}" | docker build --network host -t "${REPOSITORY}" -f Dockerfile.run - 33 | 34 | test: 35 | "$(GOCMD)" test -v ./... 36 | 37 | stop: 38 | @docker stop "${BINARY}" 39 | 40 | clean-images: stop 41 | @docker rmi "${BUILDER}" "${BINARY}" 42 | 43 | clean: 44 | "$(GOCMD)" clean -i 45 | 46 | push: ## push the latest Docker image to DockerHub 47 | docker push $(REPOSITORY) 48 | 49 | apply: 50 | kubectl apply -f deployments/manifests/ 51 | 52 | deploy: binary-image push apply 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![](assets/web/jamadar-round-100px.png) Jamadar 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/stakater/jamadar?style=flat-square)](https://goreportcard.com/report/github.com/stakater/jamadar) 4 | [![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/stakater/jamadar) 5 | [![Release](https://img.shields.io/github/release/stakater/jamadar.svg?style=flat-square)](https://github.com/stakater/jamadar/releases/latest) 6 | [![GitHub tag](https://img.shields.io/github/tag/stakater/jamadar.svg?style=flat-square)](https://github.com/stakater/jamadar/releases/latest) 7 | [![Docker Pulls](https://img.shields.io/docker/pulls/stakater/jamadar.svg?style=flat-square)](https://hub.docker.com/r/stakater/jamadar/) 8 | [![Docker Stars](https://img.shields.io/docker/stars/stakater/jamadar.svg?style=flat-square)](https://hub.docker.com/r/stakater/jamadar/) 9 | [![MicroBadger Size](https://img.shields.io/microbadger/image-size/stakater/jamadar.svg?style=flat-square)](https://microbadger.com/images/stakater/jamadar) 10 | [![MicroBadger Layers](https://img.shields.io/microbadger/layers/stakater/jamadar.svg?style=flat-square)](https://microbadger.com/images/stakater/jamadar) 11 | [![license](https://img.shields.io/github/license/stakater/jamadar.svg?style=flat-square)](LICENSE) 12 | 13 | [![Get started with Stakater](https://stakater.github.io/README/stakater-github-banner.png)](http://stakater.com/?utm_source=Jamadar&utm_medium=github) 14 | 15 | 16 | ## WHY NAME JAMADAR? 17 | Jamadar, an Urdu word, is used for Sweepers/Cleaners in Pakistan. This Jamadar will keep your cluster clean and sweep away the left overs of your cluster and will act as you want it to. 18 | 19 | ## Problem 20 | Dangling/Redundant resources take a lot of space and memory in a cluster. So we want to delete these unneeded resources depending upon the age and pre-defined annotations. e.g. I would like to delete namespaces that were without a specific annotation and are almost a month old and would like to take action whenever that happens. 21 | 22 | ## Solution 23 | 24 | Jamadar is a Kubernetes controller that can poll at configured time intervals and watch for dangling resources that are an 'X' time period old and don't have a specific annotation, and will delete them and take corresponding actions. 25 | 26 | ## Configuring 27 | 28 | First of all you need to modify `configs/config.yaml` file. Following are the available options that you can use to customize Jamadar: 29 | 30 | | Key |Description | 31 | |-----------------------|-------------------------------------------------------------------------------| 32 | | pollTimeInterval | The time interval after which the controller will poll and look for dangling resources, The value can be in "ms", "s", "m", "h" or even combined like 2h45m | 33 | | age | The time period that a dangling resource has been created e.g. delete only resources that are 7 days old, The value can be in "d", "w", "m", "y", Combined format is not supported | 34 | | resources | The resources that you want to be taken care of by Jamadar, e.g. namespaces, pods, etc | 35 | | actions | The Array of actions that you want to take, e.g. send message to Slack, etc | 36 | | restrictedNamespaces | The Array of string which contains the namespaces names to ignore | 37 | 38 | ### Supported Resources 39 | Currently we are supporting the following dangling resources, 40 | - namespaces 41 | 42 | 43 | We will be adding support for other Resources as well in the future 44 | 45 | ### Supported Actions 46 | Currently we are supporting following Actions with their Parameters, 47 | - Default: No parameters needed, it will just log to console the details. 48 | - Slack: you need to provide `token` and `Channel Name` as Parameters in the yaml file 49 | 50 | We will be adding support for other Actions as well in the future 51 | 52 | ## Deploying to Kubernetes 53 | 54 | You have to first clone or download the repository contents. The kubernetes deployment and files are provided inside `deployments/kubernetes/manifests` folder. 55 | 56 | ### Deploying through kubectl 57 | 58 | You can deploy Jamadar by running the following kubectl commands: 59 | 60 | ```bash 61 | kubectl apply -f configmap.yaml -n 62 | kubectl apply -f rbac.yaml -n 63 | kubectl apply -f deployment.yaml -n 64 | ``` 65 | 66 | ### Helm Charts 67 | 68 | Or alternatively if you configured `helm` on your cluster, you can deploy Jamadar via helm chart located under `deployments/kubernetes/chart/Jamadar` folder. 69 | 70 | ## Help 71 | 72 | **Got a question?** 73 | File a GitHub [issue](https://github.com/stakater/Jamadar/issues), or send us an [email](mailto:stakater@gmail.com). 74 | 75 | ### Talk to us on Slack 76 | Join and talk to us on the #tools-imc channel for discussing Jamadar 77 | 78 | [![Join Slack](https://stakater.github.io/README/stakater-join-slack-btn.png)](https://slack.stakater.com/) 79 | [![Chat](https://stakater.github.io/README/stakater-chat-btn.png)](https://stakater-community.slack.com/messages/CAPTSU1EX) 80 | 81 | ## Contributing 82 | 83 | ### Bug Reports & Feature Requests 84 | 85 | Please use the [issue tracker](https://github.com/stakater/Jamadar/issues) to report any bugs or file feature requests. 86 | 87 | ### Developing 88 | 89 | PRs are welcome. In general, we follow the "fork-and-pull" Git workflow. 90 | 91 | 1. **Fork** the repo on GitHub 92 | 2. **Clone** the project to your own machine 93 | 3. **Commit** changes to your own branch 94 | 4. **Push** your work back up to your fork 95 | 5. Submit a **Pull request** so that we can review your changes 96 | 97 | NOTE: Be sure to merge the latest from "upstream" before making a pull request! 98 | 99 | ## Changelog 100 | 101 | View our closed [Pull Requests](https://github.com/stakater/Jamadar/pulls?q=is%3Apr+is%3Aclosed). 102 | 103 | ## License 104 | 105 | Apache2 © [Stakater](http://stakater.com) 106 | 107 | ## About 108 | 109 | `Jamadar` is maintained by [Stakater][website]. Like it? Please let us know at 110 | 111 | See [our other projects][community] 112 | or contact us in case of professional services and queries on 113 | 114 | [website]: http://stakater.com/ 115 | [community]: https://github.com/stakater/ 116 | -------------------------------------------------------------------------------- /assets/web/jamadar-round-100px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stakater/Jamadar/4ca86697e6b5cb88bd4a1147d7cccde9770636ea/assets/web/jamadar-round-100px.png -------------------------------------------------------------------------------- /build/README.md: -------------------------------------------------------------------------------- 1 | # `/build` 2 | 3 | Packaging and Continous Integration. 4 | 5 | Put your cloud (AMI), container (Docker), OS (deb, rpm, pkg) package configurations and scripts in the `/build/package` directory. 6 | 7 | Put your CI (travis, circle, drone) configurations and scripts in the `/build/ci` directory. 8 | -------------------------------------------------------------------------------- /build/ci/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stakater/Jamadar/4ca86697e6b5cb88bd4a1147d7cccde9770636ea/build/ci/.keep -------------------------------------------------------------------------------- /build/package/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stakater/Jamadar/4ca86697e6b5cb88bd4a1147d7cccde9770636ea/build/package/.keep -------------------------------------------------------------------------------- /build/package/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM stakater/base-alpine:3.7 2 | LABEL author="stakater" 3 | 4 | COPY ./jamadar / 5 | 6 | ENTRYPOINT [ "/jamadar" ] -------------------------------------------------------------------------------- /build/package/Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM stakater/go-glide:1.9.3 2 | MAINTAINER "Stakater Team" 3 | 4 | RUN apk update 5 | 6 | RUN apk -v --update \ 7 | add git build-base && \ 8 | rm -rf /var/cache/apk/* && \ 9 | mkdir -p "$GOPATH/src/github.com/stakater/Jamadar" 10 | 11 | ADD . "$GOPATH/src/github.com/stakater/Jamadar" 12 | 13 | RUN cd "$GOPATH/src/github.com/stakater/Jamadar" && \ 14 | glide update && \ 15 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a --installsuffix cgo --ldflags="-s" -o /Jamadar 16 | 17 | COPY build/package/Dockerfile.run / 18 | 19 | # Running this image produces a tarball suitable to be piped into another 20 | # Docker build command. 21 | CMD tar -cf - -C / Dockerfile.run Jamadar 22 | -------------------------------------------------------------------------------- /build/package/Dockerfile.run: -------------------------------------------------------------------------------- 1 | FROM alpine:3.9 2 | MAINTAINER "Stakater Team" 3 | 4 | RUN apk add --update ca-certificates 5 | 6 | COPY Jamadar /bin/Jamadar 7 | 8 | ENTRYPOINT ["/bin/Jamadar"] 9 | -------------------------------------------------------------------------------- /build/package/jamadaar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stakater/Jamadar/4ca86697e6b5cb88bd4a1147d7cccde9770636ea/build/package/jamadaar -------------------------------------------------------------------------------- /build/package/jamadar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stakater/Jamadar/4ca86697e6b5cb88bd4a1147d7cccde9770636ea/build/package/jamadar -------------------------------------------------------------------------------- /cmd/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "github.com/stakater/Jamadar/internal/pkg/cmd" 4 | 5 | // Run runs the command 6 | func Run() error { 7 | cmd := cmd.NewJamadarCommand() 8 | return cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /configs/config.yaml: -------------------------------------------------------------------------------- 1 | pollTimeInterval: 12h # Values: "ms", "s", "m", "h". 2 | age: 1m # Values: "d" or "w" or "m" or "y". 3 | resources: 4 | - namespaces 5 | actions: 6 | - name: default 7 | - name: slack 8 | params: 9 | token: 10 | channel: 11 | restrictedNamespaces: 12 | - kube-system 13 | - default 14 | - kube-public 15 | - dev 16 | - prod 17 | - tools -------------------------------------------------------------------------------- /configs/testConfigs/CorrectSlackConfig.yaml: -------------------------------------------------------------------------------- 1 | pollTimeInterval: 20s 2 | age: 5d 3 | actions: 4 | - name: default 5 | - name: slack 6 | params: 7 | token: "123" 8 | channel: "channelName" 9 | restrictedNamespaces: -------------------------------------------------------------------------------- /configs/testConfigs/Empty.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stakater/Jamadar/4ca86697e6b5cb88bd4a1147d7cccde9770636ea/configs/testConfigs/Empty.yaml -------------------------------------------------------------------------------- /deployments/kubernetes/chart/jamadar/Chart.yaml: -------------------------------------------------------------------------------- 1 | # Generated from deployments/kubernetes/templates/chart/Chart.yaml.tmpl 2 | 3 | apiVersion: v1 4 | name: jamadar 5 | description: Jamadar chart that runs on kubernetes 6 | version: v0.0.18 7 | keywords: 8 | - Jamadar 9 | - kubernetes 10 | home: https://github.com/stakater/Jamadar 11 | maintainers: 12 | - name: Stakater 13 | email: hello@stakater.com -------------------------------------------------------------------------------- /deployments/kubernetes/chart/jamadar/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" | lower -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "fullname" -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 16 | {{- end -}} 17 | 18 | {{- define "labels.selector" -}} 19 | app: {{ template "name" . }} 20 | group: {{ .Values.jamadar.labels.group }} 21 | provider: {{ .Values.jamadar.labels.provider }} 22 | {{- end -}} 23 | 24 | {{- define "labels.stakater" -}} 25 | {{ template "labels.selector" . }} 26 | version: {{ .Values.jamadar.labels.version }} 27 | {{- end -}} 28 | 29 | {{- define "labels.chart" -}} 30 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 31 | release: {{ .Release.Name | quote }} 32 | heritage: {{ .Release.Service | quote }} 33 | {{- end -}} -------------------------------------------------------------------------------- /deployments/kubernetes/chart/jamadar/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | labels: 5 | {{ include "labels.stakater" . | indent 4 }} 6 | {{ include "labels.chart" . | indent 4 }} 7 | name: {{ template "name" . }} 8 | data: 9 | config.yaml: |- 10 | pollTimeInterval: {{ .Values.jamadar.pollTimeInterval }} 11 | age: {{ .Values.jamadar.age }} 12 | resources: 13 | {{- range .Values.jamadar.resources }} 14 | - {{ . }} 15 | {{- end }} 16 | actions: 17 | {{- range .Values.jamadar.actions }} 18 | - name: {{ .name }} 19 | params: 20 | {{- range $key, $value := .params }} 21 | {{ $key }}: {{ $value }} 22 | {{- end }} 23 | {{- end }} 24 | restrictedNamespaces: 25 | {{- range .Values.jamadar.restrictedNamespaces }} 26 | - {{ . }} 27 | {{- end }} 28 | -------------------------------------------------------------------------------- /deployments/kubernetes/chart/jamadar/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | configmap.fabric8.io/update-on-change: {{ template "name" . }} 6 | labels: 7 | {{ include "labels.stakater" . | indent 4 }} 8 | {{ include "labels.chart" . | indent 4 }} 9 | name: {{ template "name" . }} 10 | spec: 11 | replicas: 1 12 | revisionHistoryLimit: 2 13 | selector: 14 | matchLabels: 15 | {{ include "labels.selector" . | indent 6 }} 16 | template: 17 | metadata: 18 | annotations: 19 | configmap.fabric8.io/update-on-change: {{ template "name" . }} 20 | labels: 21 | {{ include "labels.selector" . | indent 8 }} 22 | spec: 23 | containers: 24 | - env: 25 | - name: KUBERNETES_NAMESPACE 26 | valueFrom: 27 | fieldRef: 28 | fieldPath: metadata.namespace 29 | - name: CONFIG_FILE_PATH 30 | value: {{ .Values.jamadar.configFilePath }} 31 | image: "{{ .Values.jamadar.image.name }}:{{ .Values.jamadar.image.tag }}" 32 | imagePullPolicy: {{ .Values.jamadar.image.pullPolicy }} 33 | name: {{ template "name" . }} 34 | volumeMounts: 35 | - mountPath: /configs 36 | name: config-volume 37 | serviceAccountName: {{ template "name" . }} 38 | volumes: 39 | - configMap: 40 | name: {{ template "name" . }} 41 | name: config-volume 42 | -------------------------------------------------------------------------------- /deployments/kubernetes/chart/jamadar/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | {{ include "labels.stakater" . | indent 4 }} 6 | {{ include "labels.chart" . | indent 4 }} 7 | name: {{ template "name" . }} 8 | --- 9 | apiVersion: rbac.authorization.k8s.io/v1beta1 10 | kind: ClusterRole 11 | metadata: 12 | labels: 13 | {{ include "labels.stakater" . | indent 4 }} 14 | {{ include "labels.chart" . | indent 4 }} 15 | name: {{ template "name" . }}-role 16 | rules: 17 | - apiGroups: 18 | - "" 19 | resources: 20 | - namespaces 21 | verbs: 22 | - list 23 | - get 24 | - watch 25 | - create 26 | - delete 27 | --- 28 | apiVersion: rbac.authorization.k8s.io/v1beta1 29 | kind: ClusterRoleBinding 30 | metadata: 31 | labels: 32 | {{ include "labels.stakater" . | indent 4 }} 33 | {{ include "labels.chart" . | indent 4 }} 34 | name: {{ template "name" . }}-role-binding 35 | roleRef: 36 | apiGroup: rbac.authorization.k8s.io 37 | kind: ClusterRole 38 | name: {{ template "name" . }}-role 39 | subjects: 40 | - kind: ServiceAccount 41 | name: {{ template "name" . }} 42 | namespace: {{ .Release.Namespace }} -------------------------------------------------------------------------------- /deployments/kubernetes/chart/jamadar/values.yaml: -------------------------------------------------------------------------------- 1 | # Generated from deployments/kubernetes/templates/chart/values.yaml.tmpl 2 | 3 | kubernetes: 4 | host: https://kubernetes.default 5 | 6 | jamadar: 7 | labels: 8 | provider: stakater 9 | group: com.stakater.platform 10 | version: v0.0.18 11 | image: 12 | name: stakater/jamadar 13 | tag: "v0.0.18" 14 | pullPolicy: IfNotPresent 15 | pollTimeInterval: 20m 16 | age: 7d 17 | resources: 18 | - namespaces 19 | actions: 20 | - name: slack 21 | params: 22 | token: 23 | channel: 24 | restrictedNamespaces: 25 | - kube-system 26 | - default 27 | - kube-public 28 | 29 | configFilePath: /configs/config.yaml -------------------------------------------------------------------------------- /deployments/kubernetes/jamadar.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: jamadar/templates/configmap.yaml 3 | apiVersion: v1 4 | kind: ConfigMap 5 | metadata: 6 | labels: 7 | app: jamadar 8 | group: com.stakater.platform 9 | provider: stakater 10 | version: v0.0.18 11 | chart: "jamadar-v0.0.18" 12 | release: "jamadar" 13 | heritage: "Tiller" 14 | name: jamadar 15 | data: 16 | config.yaml: |- 17 | pollTimeInterval: 20m 18 | age: 7d 19 | resources: 20 | - namespaces 21 | actions: 22 | - name: slack 23 | params: 24 | channel: 25 | token: 26 | restrictedNamespaces: 27 | - kube-system 28 | - default 29 | - kube-public 30 | 31 | --- 32 | # Source: jamadar/templates/deployment.yaml 33 | apiVersion: extensions/v1beta1 34 | kind: Deployment 35 | metadata: 36 | annotations: 37 | configmap.fabric8.io/update-on-change: jamadar 38 | labels: 39 | app: jamadar 40 | group: com.stakater.platform 41 | provider: stakater 42 | version: v0.0.18 43 | chart: "jamadar-v0.0.18" 44 | release: "jamadar" 45 | heritage: "Tiller" 46 | name: jamadar 47 | spec: 48 | replicas: 1 49 | revisionHistoryLimit: 2 50 | selector: 51 | matchLabels: 52 | app: jamadar 53 | group: com.stakater.platform 54 | provider: stakater 55 | template: 56 | metadata: 57 | annotations: 58 | configmap.fabric8.io/update-on-change: jamadar 59 | labels: 60 | app: jamadar 61 | group: com.stakater.platform 62 | provider: stakater 63 | spec: 64 | containers: 65 | - env: 66 | - name: KUBERNETES_NAMESPACE 67 | valueFrom: 68 | fieldRef: 69 | fieldPath: metadata.namespace 70 | - name: CONFIG_FILE_PATH 71 | value: /configs/config.yaml 72 | image: "stakater/jamadar:v0.0.18" 73 | imagePullPolicy: IfNotPresent 74 | name: jamadar 75 | volumeMounts: 76 | - mountPath: /configs 77 | name: config-volume 78 | serviceAccountName: jamadar 79 | volumes: 80 | - configMap: 81 | name: jamadar 82 | name: config-volume 83 | 84 | --- 85 | # Source: jamadar/templates/rbac.yaml 86 | apiVersion: v1 87 | kind: ServiceAccount 88 | metadata: 89 | labels: 90 | app: jamadar 91 | group: com.stakater.platform 92 | provider: stakater 93 | version: v0.0.18 94 | chart: "jamadar-v0.0.18" 95 | release: "jamadar" 96 | heritage: "Tiller" 97 | name: jamadar 98 | --- 99 | apiVersion: rbac.authorization.k8s.io/v1beta1 100 | kind: ClusterRole 101 | metadata: 102 | labels: 103 | app: jamadar 104 | group: com.stakater.platform 105 | provider: stakater 106 | version: v0.0.18 107 | chart: "jamadar-v0.0.18" 108 | release: "jamadar" 109 | heritage: "Tiller" 110 | name: jamadar-role 111 | rules: 112 | - apiGroups: 113 | - "" 114 | resources: 115 | - namespaces 116 | verbs: 117 | - list 118 | - get 119 | - watch 120 | - create 121 | - delete 122 | --- 123 | apiVersion: rbac.authorization.k8s.io/v1beta1 124 | kind: ClusterRoleBinding 125 | metadata: 126 | labels: 127 | app: jamadar 128 | group: com.stakater.platform 129 | provider: stakater 130 | version: v0.0.18 131 | chart: "jamadar-v0.0.18" 132 | release: "jamadar" 133 | heritage: "Tiller" 134 | name: jamadar-role-binding 135 | roleRef: 136 | apiGroup: rbac.authorization.k8s.io 137 | kind: ClusterRole 138 | name: jamadar-role 139 | subjects: 140 | - kind: ServiceAccount 141 | name: jamadar 142 | namespace: default 143 | -------------------------------------------------------------------------------- /deployments/kubernetes/manifests/configmap.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: jamadar/templates/configmap.yaml 3 | apiVersion: v1 4 | kind: ConfigMap 5 | metadata: 6 | labels: 7 | app: jamadar 8 | group: com.stakater.platform 9 | provider: stakater 10 | version: v0.0.18 11 | chart: "jamadar-v0.0.18" 12 | release: "jamadar" 13 | heritage: "Tiller" 14 | name: jamadar 15 | data: 16 | config.yaml: |- 17 | pollTimeInterval: 20m 18 | age: 7d 19 | resources: 20 | - namespaces 21 | actions: 22 | - name: slack 23 | params: 24 | channel: 25 | token: 26 | restrictedNamespaces: 27 | - kube-system 28 | - default 29 | - kube-public 30 | 31 | -------------------------------------------------------------------------------- /deployments/kubernetes/manifests/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: jamadar/templates/deployment.yaml 3 | apiVersion: extensions/v1beta1 4 | kind: Deployment 5 | metadata: 6 | annotations: 7 | configmap.fabric8.io/update-on-change: jamadar 8 | labels: 9 | app: jamadar 10 | group: com.stakater.platform 11 | provider: stakater 12 | version: v0.0.18 13 | chart: "jamadar-v0.0.18" 14 | release: "jamadar" 15 | heritage: "Tiller" 16 | name: jamadar 17 | spec: 18 | replicas: 1 19 | revisionHistoryLimit: 2 20 | selector: 21 | matchLabels: 22 | app: jamadar 23 | group: com.stakater.platform 24 | provider: stakater 25 | template: 26 | metadata: 27 | annotations: 28 | configmap.fabric8.io/update-on-change: jamadar 29 | labels: 30 | app: jamadar 31 | group: com.stakater.platform 32 | provider: stakater 33 | spec: 34 | containers: 35 | - env: 36 | - name: KUBERNETES_NAMESPACE 37 | valueFrom: 38 | fieldRef: 39 | fieldPath: metadata.namespace 40 | - name: CONFIG_FILE_PATH 41 | value: /configs/config.yaml 42 | image: "stakater/jamadar:v0.0.18" 43 | imagePullPolicy: IfNotPresent 44 | name: jamadar 45 | volumeMounts: 46 | - mountPath: /configs 47 | name: config-volume 48 | serviceAccountName: jamadar 49 | volumes: 50 | - configMap: 51 | name: jamadar 52 | name: config-volume 53 | 54 | -------------------------------------------------------------------------------- /deployments/kubernetes/manifests/rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: jamadar/templates/rbac.yaml 3 | apiVersion: v1 4 | kind: ServiceAccount 5 | metadata: 6 | labels: 7 | app: jamadar 8 | group: com.stakater.platform 9 | provider: stakater 10 | version: v0.0.18 11 | chart: "jamadar-v0.0.18" 12 | release: "jamadar" 13 | heritage: "Tiller" 14 | name: jamadar 15 | --- 16 | apiVersion: rbac.authorization.k8s.io/v1beta1 17 | kind: ClusterRole 18 | metadata: 19 | labels: 20 | app: jamadar 21 | group: com.stakater.platform 22 | provider: stakater 23 | version: v0.0.18 24 | chart: "jamadar-v0.0.18" 25 | release: "jamadar" 26 | heritage: "Tiller" 27 | name: jamadar-role 28 | rules: 29 | - apiGroups: 30 | - "" 31 | resources: 32 | - namespaces 33 | verbs: 34 | - list 35 | - get 36 | - watch 37 | - create 38 | - delete 39 | --- 40 | apiVersion: rbac.authorization.k8s.io/v1beta1 41 | kind: ClusterRoleBinding 42 | metadata: 43 | labels: 44 | app: jamadar 45 | group: com.stakater.platform 46 | provider: stakater 47 | version: v0.0.18 48 | chart: "jamadar-v0.0.18" 49 | release: "jamadar" 50 | heritage: "Tiller" 51 | name: jamadar-role-binding 52 | roleRef: 53 | apiGroup: rbac.authorization.k8s.io 54 | kind: ClusterRole 55 | name: jamadar-role 56 | subjects: 57 | - kind: ServiceAccount 58 | name: jamadar 59 | namespace: default 60 | -------------------------------------------------------------------------------- /deployments/kubernetes/templates/chart/Chart.yaml.tmpl: -------------------------------------------------------------------------------- 1 | # Generated from deployments/kubernetes/templates/chart/Chart.yaml.tmpl 2 | 3 | apiVersion: v1 4 | name: jamadar 5 | description: Jamadar chart that runs on kubernetes 6 | version: {{ getenv "VERSION" }} 7 | keywords: 8 | - Jamadar 9 | - kubernetes 10 | home: https://github.com/stakater/Jamadar 11 | maintainers: 12 | - name: Stakater 13 | email: hello@stakater.com -------------------------------------------------------------------------------- /deployments/kubernetes/templates/chart/values.yaml.tmpl: -------------------------------------------------------------------------------- 1 | # Generated from deployments/kubernetes/templates/chart/values.yaml.tmpl 2 | 3 | kubernetes: 4 | host: https://kubernetes.default 5 | 6 | jamadar: 7 | labels: 8 | provider: stakater 9 | group: com.stakater.platform 10 | version: {{ getenv "VERSION" }} 11 | image: 12 | name: {{ getenv "DOCKER_IMAGE" }} 13 | tag: "{{ getenv "VERSION" }}" 14 | pullPolicy: IfNotPresent 15 | pollTimeInterval: 20m 16 | age: 7d 17 | resources: 18 | - namespaces 19 | actions: 20 | - name: slack 21 | params: 22 | token: 23 | channel: 24 | restrictedNamespaces: 25 | - kube-system 26 | - default 27 | - kube-public 28 | 29 | configFilePath: /configs/config.yaml -------------------------------------------------------------------------------- /docs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stakater/Jamadar/4ca86697e6b5cb88bd4a1147d7cccde9770636ea/docs/.keep -------------------------------------------------------------------------------- /docs/design/DELETE-NAMESPACES.md: -------------------------------------------------------------------------------- 1 | # PROBLEM 2 | 3 | We would like to delete the dangling/undeed namespaces &/ projects to clean up the cluster. 4 | 5 | # SOLUTION 6 | 7 | Delete all Namespaces (k8s) & projects (OpenShift) which meet following criteria: 8 | 9 | 1. Don't contain the annotation `jamadar.stakater.com/persist=true` 10 | 2. Date of creation is older than X period e.g. 1 week (this should be configurable) 11 | 12 | Notify on slack when an item is deleted. 13 | 14 | This should run regularly and do the cleanup. 15 | 16 | Other needs: 17 | 18 | - it should work both for vanilla kubernetes & openshift 19 | - it should delete namespaces 20 | - it should delete projects (OpenShift) 21 | 22 | So, it will evaluate some expressions and then takes actions; keep in mind this is just first task of Jamadar and we will be adding a lot more; so, we need to think of doing it in a pluggable way; where one can add new "cleanup" task and Jamadar should be able to perform it! -------------------------------------------------------------------------------- /docs/design/README.md: -------------------------------------------------------------------------------- 1 | Design docs -------------------------------------------------------------------------------- /docs/user/README.md: -------------------------------------------------------------------------------- 1 | User Docs -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 5a0b1e44eead3badd91d996b1f31ca8532e2e38ae15bd3891dc89ab3b1a45cd1 2 | updated: 2018-05-16T11:16:09.049111555+05:00 3 | imports: 4 | - name: github.com/asiyani/slack 5 | version: 150279a4ad4b11910003e30be7ffe7fdb5523634 6 | - name: github.com/davecgh/go-spew 7 | version: 782f4967f2dc4564575ca782fe2d04090b5faca8 8 | subpackages: 9 | - spew 10 | - name: github.com/emicklei/go-restful 11 | version: ff4f55a206334ef123e4f79bbf348980da81ca46 12 | subpackages: 13 | - log 14 | - name: github.com/emicklei/go-restful-swagger12 15 | version: dcef7f55730566d41eae5db10e7d6981829720f6 16 | - name: github.com/ghodss/yaml 17 | version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee 18 | - name: github.com/go-openapi/jsonpointer 19 | version: 46af16f9f7b149af66e5d1bd010e3574dc06de98 20 | - name: github.com/go-openapi/jsonreference 21 | version: 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272 22 | - name: github.com/go-openapi/spec 23 | version: 6aced65f8501fe1217321abf0749d354824ba2ff 24 | - name: github.com/go-openapi/swag 25 | version: 1d0bd113de87027671077d3c71eb3ac5d7dbba72 26 | - name: github.com/gogo/protobuf 27 | version: c0656edd0d9eab7c66d1eb0c568f9039345796f7 28 | subpackages: 29 | - proto 30 | - sortkeys 31 | - name: github.com/golang/glog 32 | version: 44145f04b68cf362d9c4df2182967c2275eaefed 33 | - name: github.com/golang/protobuf 34 | version: 4bd1920723d7b7c925de087aa32e2187708897f7 35 | subpackages: 36 | - proto 37 | - ptypes 38 | - ptypes/any 39 | - ptypes/duration 40 | - ptypes/timestamp 41 | - name: github.com/google/btree 42 | version: 7d79101e329e5a3adf994758c578dab82b90c017 43 | - name: github.com/google/gofuzz 44 | version: 44d81051d367757e1c7c6a5a86423ece9afcf63c 45 | - name: github.com/googleapis/gnostic 46 | version: 0c5108395e2debce0d731cf0287ddf7242066aba 47 | subpackages: 48 | - OpenAPIv2 49 | - compiler 50 | - extensions 51 | - name: github.com/gorilla/websocket 52 | version: 21ab95fa12b9bdd8fecf5fa3586aad941cc98785 53 | - name: github.com/gregjones/httpcache 54 | version: 787624de3eb7bd915c329cba748687a3b22666a6 55 | subpackages: 56 | - diskcache 57 | - name: github.com/howeyc/gopass 58 | version: bf9dde6d0d2c004a008c27aaee91170c786f6db8 59 | - name: github.com/imdario/mergo 60 | version: 6633656539c1639d9d78127b7d47c622b5d7b6dc 61 | - name: github.com/inconshreveable/mousetrap 62 | version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 63 | - name: github.com/json-iterator/go 64 | version: 36b14963da70d11297d313183d7e6388c8510e1e 65 | - name: github.com/juju/ratelimit 66 | version: 5b9ff866471762aa2ab2dced63c9fb6f53921342 67 | - name: github.com/mailru/easyjson 68 | version: d5b7844b561a7bc640052f1b935f7b800330d7e0 69 | subpackages: 70 | - buffer 71 | - jlexer 72 | - jwriter 73 | - name: github.com/mitchellh/mapstructure 74 | version: bb74f1db0675b241733089d5a1faa5dd8b0ef57b 75 | - name: github.com/peterbourgon/diskv 76 | version: 5f041e8faa004a95c88a202771f4cc3e991971e6 77 | - name: github.com/PuerkitoBio/purell 78 | version: 8a290539e2e8629dbc4e6bad948158f790ec31f4 79 | - name: github.com/PuerkitoBio/urlesc 80 | version: 5bd2802263f21d8788851d5305584c82a5c75d7e 81 | - name: github.com/spf13/cobra 82 | version: ef82de70bb3f60c65fb8eebacbb2d122ef517385 83 | - name: github.com/spf13/pflag 84 | version: 583c0c0531f06d5278b7d917446061adc344b5cd 85 | - name: golang.org/x/crypto 86 | version: 81e90905daefcd6fd217b62423c0908922eadb30 87 | subpackages: 88 | - ssh/terminal 89 | - name: golang.org/x/net 90 | version: 1c05540f6879653db88113bc4a2b70aec4bd491f 91 | subpackages: 92 | - http2 93 | - http2/hpack 94 | - idna 95 | - lex/httplex 96 | - name: golang.org/x/sys 97 | version: 7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce 98 | subpackages: 99 | - unix 100 | - windows 101 | - name: golang.org/x/text 102 | version: b19bf474d317b857955b12035d2c5acb57ce8b01 103 | subpackages: 104 | - cases 105 | - internal 106 | - internal/tag 107 | - language 108 | - runes 109 | - secure/bidirule 110 | - secure/precis 111 | - transform 112 | - unicode/bidi 113 | - unicode/norm 114 | - width 115 | - name: gopkg.in/inf.v0 116 | version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 117 | - name: gopkg.in/yaml.v2 118 | version: 5420a8b6744d3b0345ab293f6fcba19c978f1183 119 | - name: k8s.io/api 120 | version: fe29995db37613b9c5b2a647544cf627bfa8d299 121 | subpackages: 122 | - admissionregistration/v1alpha1 123 | - apps/v1beta1 124 | - apps/v1beta2 125 | - authentication/v1 126 | - authentication/v1beta1 127 | - authorization/v1 128 | - authorization/v1beta1 129 | - autoscaling/v1 130 | - autoscaling/v2beta1 131 | - batch/v1 132 | - batch/v1beta1 133 | - batch/v2alpha1 134 | - certificates/v1beta1 135 | - core/v1 136 | - extensions/v1beta1 137 | - networking/v1 138 | - policy/v1beta1 139 | - rbac/v1 140 | - rbac/v1alpha1 141 | - rbac/v1beta1 142 | - scheduling/v1alpha1 143 | - settings/v1alpha1 144 | - storage/v1 145 | - storage/v1beta1 146 | - name: k8s.io/apimachinery 147 | version: 019ae5ada31de202164b118aee88ee2d14075c31 148 | subpackages: 149 | - pkg/api/equality 150 | - pkg/api/errors 151 | - pkg/api/meta 152 | - pkg/api/resource 153 | - pkg/apis/meta/v1 154 | - pkg/apis/meta/v1/unstructured 155 | - pkg/apis/meta/v1alpha1 156 | - pkg/conversion 157 | - pkg/conversion/queryparams 158 | - pkg/conversion/unstructured 159 | - pkg/fields 160 | - pkg/labels 161 | - pkg/runtime 162 | - pkg/runtime/schema 163 | - pkg/runtime/serializer 164 | - pkg/runtime/serializer/json 165 | - pkg/runtime/serializer/protobuf 166 | - pkg/runtime/serializer/recognizer 167 | - pkg/runtime/serializer/streaming 168 | - pkg/runtime/serializer/versioning 169 | - pkg/selection 170 | - pkg/types 171 | - pkg/util/clock 172 | - pkg/util/diff 173 | - pkg/util/errors 174 | - pkg/util/framer 175 | - pkg/util/intstr 176 | - pkg/util/json 177 | - pkg/util/net 178 | - pkg/util/runtime 179 | - pkg/util/sets 180 | - pkg/util/validation 181 | - pkg/util/validation/field 182 | - pkg/util/wait 183 | - pkg/util/yaml 184 | - pkg/version 185 | - pkg/watch 186 | - third_party/forked/golang/reflect 187 | - name: k8s.io/client-go 188 | version: 35874c597fed17ca62cd197e516d7d5ff9a2958c 189 | subpackages: 190 | - discovery 191 | - discovery/fake 192 | - kubernetes 193 | - kubernetes/fake 194 | - kubernetes/scheme 195 | - kubernetes/typed/admissionregistration/v1alpha1 196 | - kubernetes/typed/admissionregistration/v1alpha1/fake 197 | - kubernetes/typed/apps/v1beta1 198 | - kubernetes/typed/apps/v1beta1/fake 199 | - kubernetes/typed/apps/v1beta2 200 | - kubernetes/typed/apps/v1beta2/fake 201 | - kubernetes/typed/authentication/v1 202 | - kubernetes/typed/authentication/v1/fake 203 | - kubernetes/typed/authentication/v1beta1 204 | - kubernetes/typed/authentication/v1beta1/fake 205 | - kubernetes/typed/authorization/v1 206 | - kubernetes/typed/authorization/v1/fake 207 | - kubernetes/typed/authorization/v1beta1 208 | - kubernetes/typed/authorization/v1beta1/fake 209 | - kubernetes/typed/autoscaling/v1 210 | - kubernetes/typed/autoscaling/v1/fake 211 | - kubernetes/typed/autoscaling/v2beta1 212 | - kubernetes/typed/autoscaling/v2beta1/fake 213 | - kubernetes/typed/batch/v1 214 | - kubernetes/typed/batch/v1/fake 215 | - kubernetes/typed/batch/v1beta1 216 | - kubernetes/typed/batch/v1beta1/fake 217 | - kubernetes/typed/batch/v2alpha1 218 | - kubernetes/typed/batch/v2alpha1/fake 219 | - kubernetes/typed/certificates/v1beta1 220 | - kubernetes/typed/certificates/v1beta1/fake 221 | - kubernetes/typed/core/v1 222 | - kubernetes/typed/core/v1/fake 223 | - kubernetes/typed/extensions/v1beta1 224 | - kubernetes/typed/extensions/v1beta1/fake 225 | - kubernetes/typed/networking/v1 226 | - kubernetes/typed/networking/v1/fake 227 | - kubernetes/typed/policy/v1beta1 228 | - kubernetes/typed/policy/v1beta1/fake 229 | - kubernetes/typed/rbac/v1 230 | - kubernetes/typed/rbac/v1/fake 231 | - kubernetes/typed/rbac/v1alpha1 232 | - kubernetes/typed/rbac/v1alpha1/fake 233 | - kubernetes/typed/rbac/v1beta1 234 | - kubernetes/typed/rbac/v1beta1/fake 235 | - kubernetes/typed/scheduling/v1alpha1 236 | - kubernetes/typed/scheduling/v1alpha1/fake 237 | - kubernetes/typed/settings/v1alpha1 238 | - kubernetes/typed/settings/v1alpha1/fake 239 | - kubernetes/typed/storage/v1 240 | - kubernetes/typed/storage/v1/fake 241 | - kubernetes/typed/storage/v1beta1 242 | - kubernetes/typed/storage/v1beta1/fake 243 | - pkg/version 244 | - rest 245 | - rest/watch 246 | - testing 247 | - tools/auth 248 | - tools/clientcmd 249 | - tools/clientcmd/api 250 | - tools/clientcmd/api/latest 251 | - tools/clientcmd/api/v1 252 | - tools/metrics 253 | - tools/reference 254 | - transport 255 | - util/cert 256 | - util/flowcontrol 257 | - util/homedir 258 | - util/integer 259 | - name: k8s.io/kube-openapi 260 | version: 868f2f29720b192240e18284659231b440f9cda5 261 | subpackages: 262 | - pkg/common 263 | testImports: [] 264 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: . 2 | import: 3 | - package: k8s.io/api 4 | version: kubernetes-1.8.0 5 | - package: k8s.io/apimachinery 6 | version: kubernetes-1.8.0 7 | - package: k8s.io/client-go 8 | version: 5.0.0 9 | - package: gopkg.in/yaml.v2 10 | version: v2.2.1 11 | - package: github.com/spf13/cobra 12 | version: ef82de70bb3f60c65fb8eebacbb2d122ef517385 13 | - package: github.com/spf13/pflag 14 | version: 583c0c0531f06d5278b7d917446061adc344b5cd 15 | # TODO: Change this back to nlopes when that issue is fixed 16 | # URL: https://github.com/nlopes/slack/issues/294 17 | - package: github.com/asiyani/slack 18 | version: 150279a4ad4b11910003e30be7ffe7fdb5523634 19 | # - package: github.com/nlopes/slack 20 | - package: github.com/mitchellh/mapstructure 21 | version: bb74f1db0675b241733089d5a1faa5dd8b0ef57b -------------------------------------------------------------------------------- /internal/pkg/actions/action.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/stakater/Jamadar/internal/pkg/actions/slack" 7 | "k8s.io/api/core/v1" 8 | ) 9 | 10 | func assertActionImplementations() { 11 | var _ Action = (*Default)(nil) 12 | var _ Action = (*slack.Slack)(nil) 13 | } 14 | 15 | // DefaultAction the name for default action name 16 | const ( 17 | DefaultAction = "default" 18 | ) 19 | 20 | // Action interface so that other actions like slack can implement this 21 | type Action interface { 22 | Init(map[interface{}]interface{}) error 23 | TakeAction(obj interface{}) 24 | } 25 | 26 | // Default class with empty implementations for any action that we dont support currently 27 | type Default struct { 28 | } 29 | 30 | // Init initializes handler configuration 31 | // Do nothing for default handler 32 | func (d *Default) Init(params map[interface{}]interface{}) error { 33 | return nil 34 | } 35 | 36 | // TakeAction the main business logic of Action 37 | func (d *Default) TakeAction(obj interface{}) { 38 | message := "Default Action -> Namespace " + obj.(v1.Namespace).Name + " deleted" 39 | log.Printf(message) 40 | } 41 | -------------------------------------------------------------------------------- /internal/pkg/actions/populate.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/stakater/Jamadar/internal/pkg/actions/slack" 7 | "github.com/stakater/Jamadar/internal/pkg/config" 8 | ) 9 | 10 | // PopulateFromConfig populates the actions for a specific controller from config 11 | func PopulateFromConfig(configActions []config.Action) []Action { 12 | var populatedActions []Action 13 | if len(configActions) == 0 { 14 | configActions = []config.Action{ 15 | config.Action{ 16 | Name: "default", 17 | }, 18 | } 19 | } 20 | for _, configAction := range configActions { 21 | actionToAdd := MapToAction(configAction.Name) 22 | err := actionToAdd.Init(configAction.Params) 23 | if err != nil { 24 | log.Println(err) 25 | } 26 | populatedActions = append(populatedActions, actionToAdd) 27 | } 28 | return populatedActions 29 | } 30 | 31 | // MapToAction maps the action name to the actual action type 32 | func MapToAction(actionName string) Action { 33 | action, ok := actionMap[actionName] 34 | if !ok { 35 | return actionMap[DefaultAction] 36 | } 37 | return action 38 | } 39 | 40 | var actionMap = map[string]Action{ 41 | "default": &Default{}, 42 | "slack": &slack.Slack{}, 43 | } 44 | -------------------------------------------------------------------------------- /internal/pkg/actions/populate_test.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stakater/Jamadar/internal/pkg/config" 8 | ) 9 | 10 | func TestPopulateFromConfig(t *testing.T) { 11 | type args struct { 12 | configActions []config.Action 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want []Action 18 | }{ 19 | { 20 | name: "PopulateSlackAction", 21 | args: args{ 22 | configActions: []config.Action{ 23 | config.Action{ 24 | Name: "slack", 25 | Params: map[interface{}]interface{}{ 26 | "token": "123", 27 | "channel": "channelName", 28 | }, 29 | }, 30 | }, 31 | }, 32 | want: []Action{ 33 | MapToAction("slack"), 34 | }, 35 | }, 36 | { 37 | name: "PopulateSlackActionError", 38 | args: args{ 39 | configActions: []config.Action{ 40 | config.Action{ 41 | Name: "slack", 42 | Params: map[interface{}]interface{}{ 43 | "tok": "123", 44 | "chan": "channelName", 45 | }, 46 | }, 47 | }, 48 | }, 49 | want: []Action{ 50 | MapToAction("slack"), 51 | }, 52 | }, 53 | { 54 | name: "PopulateDefaultAction", 55 | args: args{ 56 | configActions: []config.Action{ 57 | config.Action{ 58 | Name: "default", 59 | }, 60 | }, 61 | }, 62 | want: []Action{ 63 | MapToAction("default"), 64 | }, 65 | }, 66 | { 67 | name: "PopulateEmptyDefaultAction", 68 | args: args{ 69 | configActions: []config.Action{}, 70 | }, 71 | want: []Action{ 72 | MapToAction("default"), 73 | }, 74 | }, 75 | } 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | if got := PopulateFromConfig(tt.args.configActions); !reflect.DeepEqual(got, tt.want) { 79 | t.Errorf("PopulateFromConfig() = %v, want %v", got, tt.want) 80 | } 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/pkg/actions/slack/slack.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | 7 | "github.com/asiyani/slack" 8 | "github.com/mitchellh/mapstructure" 9 | "k8s.io/api/core/v1" 10 | ) 11 | 12 | type SlackService interface { 13 | SendNotification(message string) error 14 | } 15 | 16 | // Slack action class implementing the Action interface 17 | type Slack struct { 18 | Token string 19 | Channel string 20 | } 21 | 22 | // Init initializes the Slack Configuration like token and channel 23 | func (s *Slack) Init(params map[interface{}]interface{}) error { 24 | err := mapstructure.Decode(params, &s) //Converts the params to slack struct fields 25 | if err != nil { 26 | return err 27 | } 28 | if s.Token == "" || s.Channel == "" { 29 | return errors.New("Missing slack token or channel") 30 | } 31 | return nil 32 | } 33 | 34 | // TakeAction handles the main logic for slack action 35 | func (s *Slack) TakeAction(obj interface{}) { 36 | message := "Namespace " + obj.(v1.Namespace).Name + " Deleted" 37 | err := s.SendNotification(message) 38 | if err != nil { 39 | log.Println("Error: ", err) 40 | } 41 | } 42 | 43 | // SendNotification sends the Notification to the channel 44 | func (s *Slack) SendNotification(message string) error { 45 | api := slack.New(s.Token) 46 | params := slack.PostMessageParameters{} 47 | params.Attachments = []slack.Attachment{prepareMessage(s, message)} 48 | params.AsUser = false 49 | 50 | _, _, err := api.PostMessage(s.Channel, "Jamadar Alert", params) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | log.Printf("Message successfully sent to Slack Channel `%s`", s.Channel) 56 | return nil 57 | } 58 | 59 | // Prepares the attachments to send in POST request 60 | func prepareMessage(s *Slack, message string) slack.Attachment { 61 | return slack.Attachment{ 62 | Fields: []slack.AttachmentField{ 63 | slack.AttachmentField{ 64 | Title: message, 65 | }, 66 | }, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/pkg/actions/slack/slack_test.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/stakater/Jamadar/internal/pkg/config" 8 | "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | var ( 13 | configFilePath = "../../../../configs/testConfigs/CorrectSlackConfig.yaml" 14 | configuration, _ = config.ReadConfig(configFilePath) 15 | ) 16 | 17 | type SlackMock struct { 18 | } 19 | 20 | func (s *SlackMock) SendNotification(message string) error { 21 | log.Print(message) 22 | return nil 23 | } 24 | func TestSlack_Init(t *testing.T) { 25 | type fields struct { 26 | Token string 27 | Channel string 28 | } 29 | type args struct { 30 | params map[interface{}]interface{} 31 | } 32 | tests := []struct { 33 | name string 34 | fields fields 35 | args args 36 | wantErr bool 37 | }{ 38 | { 39 | name: "MissingSlackToken", 40 | args: args{ 41 | params: map[interface{}]interface{}{ 42 | "token": "", 43 | "channel": "channelName", 44 | }, 45 | }, 46 | wantErr: true, 47 | }, 48 | { 49 | name: "CorrectScenario", 50 | args: args{ 51 | params: map[interface{}]interface{}{ 52 | "token": "123", 53 | "channel": "channelName", 54 | }, 55 | }, 56 | fields: fields{ 57 | Token: "123", 58 | Channel: "channelName", 59 | }, 60 | }, 61 | 62 | { 63 | name: "ErrorInDecoding", 64 | args: args{ 65 | params: map[interface{}]interface{}{ 66 | "tokens": "123", 67 | "channel": "channelName", 68 | }, 69 | }, 70 | wantErr: true, 71 | }, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | s := &Slack{ 76 | Token: tt.fields.Token, 77 | Channel: tt.fields.Channel, 78 | } 79 | if err := s.Init(tt.args.params); (err != nil) != tt.wantErr { 80 | t.Errorf("Slack.Init() error = %v, wantErr %v", err, tt.wantErr) 81 | } 82 | }) 83 | } 84 | } 85 | 86 | func TestSlack_TakeAction(t *testing.T) { 87 | type fields struct { 88 | Token string 89 | Channel string 90 | } 91 | type args struct { 92 | obj interface{} 93 | } 94 | tests := []struct { 95 | name string 96 | fields fields 97 | args args 98 | }{ 99 | { 100 | name: "TakeActionPass", 101 | args: args{ 102 | obj: &v1.Namespace{ 103 | ObjectMeta: metav1.ObjectMeta{ 104 | Name: "ns-test", 105 | }, 106 | }, 107 | }, 108 | }, 109 | } 110 | for _, tt := range tests { 111 | t.Run(tt.name, func(t *testing.T) { 112 | s := &SlackMock{} 113 | message := "Namespace " + tt.args.obj.(*v1.Namespace).Name + " Deleted" 114 | s.SendNotification(message) 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /internal/pkg/cmd/Jamadar.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/stakater/Jamadar/internal/pkg/config" 8 | "github.com/stakater/Jamadar/internal/pkg/controller" 9 | "github.com/stakater/Jamadar/pkg/kube" 10 | ) 11 | 12 | //NewJamadarCommand to start and run Jamadar 13 | func NewJamadarCommand() *cobra.Command { 14 | cmds := &cobra.Command{ 15 | Use: "jamadar", 16 | Short: "A kubernetes controller which cleans up left overs", 17 | Run: startJamadar, 18 | } 19 | return cmds 20 | } 21 | 22 | func startJamadar(cmd *cobra.Command, args []string) { 23 | log.Println("Starting Jamadar") 24 | // create the clientset 25 | clientset, err := kube.GetClient() 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | // get the Controller config file 31 | config := config.GetConfiguration() 32 | 33 | controller, err := controller.NewController(clientset, config) 34 | if err != nil { 35 | log.Printf("Error occured while creating controller. Reason: %s", err.Error()) 36 | } 37 | 38 | go controller.Run() 39 | 40 | // Wait forever 41 | select {} 42 | } 43 | -------------------------------------------------------------------------------- /internal/pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | 8 | yaml "gopkg.in/yaml.v2" 9 | ) 10 | 11 | // Config which would be read from the config.yaml 12 | type Config struct { 13 | PollTimeInterval string `yaml:"pollTimeInterval"` 14 | Age string 15 | Resources []string 16 | Actions []Action 17 | RestrictedNamespaces []string `yaml:"restrictedNamespaces"` 18 | } 19 | 20 | // Action that the controller will be taking based on the Parameters 21 | type Action struct { 22 | Name string 23 | Params map[interface{}]interface{} 24 | } 25 | 26 | // ReadConfig function that reads the yaml file 27 | func ReadConfig(filePath string) (Config, error) { 28 | var config Config 29 | // Read YML 30 | source, err := ioutil.ReadFile(filePath) 31 | if err != nil { 32 | return config, err 33 | } 34 | 35 | // Unmarshall 36 | err = yaml.Unmarshal(source, &config) 37 | if err != nil { 38 | return config, err 39 | } 40 | 41 | return config, nil 42 | } 43 | 44 | // WriteConfig function that can write to the yaml file 45 | func WriteConfig(config Config, path string) error { 46 | b, err := yaml.Marshal(config) 47 | if err != nil { 48 | return err 49 | } 50 | err = ioutil.WriteFile(path, b, 0644) 51 | if err != nil { 52 | return err 53 | } 54 | return nil 55 | } 56 | 57 | // GetConfiguration gets the yaml configuration for the controller 58 | func GetConfiguration() Config { 59 | configFilePath := os.Getenv("CONFIG_FILE_PATH") 60 | if len(configFilePath) == 0 { 61 | //Default config file is placed in configs/ folder 62 | configFilePath = "configs/config.yaml" 63 | } 64 | configuration, err := ReadConfig(configFilePath) 65 | if err != nil { 66 | log.Panic(err) 67 | } 68 | return configuration 69 | } 70 | -------------------------------------------------------------------------------- /internal/pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | configFilePath = "../../../configs/testConfigs/" 10 | ) 11 | 12 | func TestReadConfig(t *testing.T) { 13 | type args struct { 14 | filePath string 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want Config 20 | wantErr bool 21 | }{ 22 | { 23 | name: "TestingWithCorrectValues", 24 | args: args{filePath: configFilePath + "CorrectSlackConfig.yaml"}, 25 | want: Config{ 26 | Age: "5d", 27 | PollTimeInterval: "20s", 28 | Actions: []Action{ 29 | Action{ 30 | Name: "default", 31 | }, 32 | Action{ 33 | Name: "slack", 34 | Params: map[interface{}]interface{}{ 35 | "token": "123", 36 | "channel": "channelName", 37 | }, 38 | }, 39 | }, 40 | }, 41 | }, 42 | { 43 | name: "TestingWithEmptyFile", 44 | args: args{filePath: configFilePath + "Empty.yaml"}, 45 | want: Config{}, 46 | }, 47 | { 48 | name: "TestingWithFileNotPresent", 49 | args: args{filePath: configFilePath + "FileNotFound.yaml"}, 50 | wantErr: true, 51 | }, 52 | } 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | got, err := ReadConfig(tt.args.filePath) 56 | if (err != nil) != tt.wantErr { 57 | t.Errorf("ReadConfig() error = %v, wantErr %v", err, tt.wantErr) 58 | return 59 | } 60 | if !reflect.DeepEqual(got, tt.want) { 61 | t.Errorf("ReadConfig() = %v, want %v", got, tt.want) 62 | } 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/pkg/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/stakater/Jamadar/internal/pkg/actions" 8 | "github.com/stakater/Jamadar/internal/pkg/config" 9 | "github.com/stakater/Jamadar/internal/pkg/tasks" 10 | clientset "k8s.io/client-go/kubernetes" 11 | ) 12 | 13 | // Controller Jamadar Controller to check for left over items 14 | type Controller struct { 15 | clientset clientset.Interface 16 | config config.Config 17 | Actions []actions.Action 18 | } 19 | 20 | // NewController for initializing the Controller 21 | func NewController(clientset clientset.Interface, config config.Config) (*Controller, error) { 22 | controller := &Controller{ 23 | clientset: clientset, 24 | config: config, 25 | } 26 | controller.Actions = actions.PopulateFromConfig(config.Actions) 27 | return controller, nil 28 | } 29 | 30 | //Run function for controller which handles the logic 31 | func (c *Controller) Run() { 32 | for { 33 | c.handleTasks() 34 | timeInterval := c.config.PollTimeInterval 35 | duration, err := time.ParseDuration(timeInterval) 36 | if err != nil { 37 | log.Printf("Error Parsing Time Interval: %v", err) 38 | return 39 | } 40 | time.Sleep(duration) 41 | } 42 | } 43 | 44 | func (c *Controller) handleTasks() { 45 | task := tasks.NewTask(c.clientset, c.Actions, c.config) 46 | task.PerformTasks() 47 | } 48 | -------------------------------------------------------------------------------- /internal/pkg/controller/controller_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stakater/Jamadar/internal/pkg/config" 7 | "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | testclient "k8s.io/client-go/kubernetes/fake" 10 | ) 11 | 12 | func TestControllerPass(t *testing.T) { 13 | configuration := config.Config{ 14 | PollTimeInterval: "1s", 15 | Age: "7d", 16 | Actions: []config.Action{ 17 | config.Action{ 18 | Name: "default", 19 | }, 20 | }, 21 | } 22 | clientset := testclient.NewSimpleClientset() 23 | namespace := v1.Namespace{ 24 | ObjectMeta: metav1.ObjectMeta{ 25 | Name: "ns-test", 26 | Annotations: map[string]string{ 27 | "jamadar.stakater.com/persist": "false", 28 | }, 29 | }, 30 | } 31 | clientset.CoreV1().Namespaces().Create(&namespace) 32 | controller, _ := NewController(clientset, configuration) 33 | controller.handleTasks() 34 | } 35 | -------------------------------------------------------------------------------- /internal/pkg/tasks/namespaces/namespaces.go: -------------------------------------------------------------------------------- 1 | package namespaces 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/stakater/Jamadar/internal/pkg/actions" 10 | "github.com/stakater/Jamadar/internal/pkg/config" 11 | "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | clientset "k8s.io/client-go/kubernetes" 14 | ) 15 | 16 | const jamadarDisableAnnotation = "jamadar.stakater.com/persist" 17 | 18 | // NamespaceToDelete represents the namespace object to be deleted 19 | type NamespaceToDelete struct { 20 | clientset clientset.Interface 21 | actions []actions.Action 22 | age string 23 | restrictedNamespaces []string 24 | } 25 | 26 | // NewNamespaceToDelete creates a new NamespaceToDelete object 27 | func NewNamespaceToDelete(clientSet clientset.Interface, actions []actions.Action, conf config.Config) *NamespaceToDelete { 28 | return &NamespaceToDelete{ 29 | clientset: clientSet, 30 | actions: actions, 31 | age: conf.Age, 32 | restrictedNamespaces: conf.RestrictedNamespaces, 33 | } 34 | } 35 | 36 | // DeleteNamespaces deletes the namespaces and takes the actions 37 | func (n *NamespaceToDelete) DeleteNamespaces(namespaceList *v1.NamespaceList) error { 38 | 39 | for _, namespace := range namespaceList.Items { 40 | isDeleted, err := n.DeleteNamespace(namespace) 41 | if err != nil { 42 | return err 43 | } 44 | if isDeleted { 45 | log.Println("Namespace : " + namespace.Name + " deleted, Now Performing actions.") 46 | for _, action := range n.actions { 47 | action.TakeAction(namespace) 48 | } 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | // DeleteNamespace deletes a single namespace 55 | func (n *NamespaceToDelete) DeleteNamespace(namespace v1.Namespace) (bool, error) { 56 | annotations := namespace.Annotations 57 | value, ok := annotations[jamadarDisableAnnotation] 58 | // check if annotation is not present and its value is not true 59 | if !ok || value != "true" { 60 | if !n.isNamespaceinRestrictedNamespaces(namespace.Name) { 61 | if checkIfOld(namespace, n.age) { 62 | err := n.clientset.CoreV1().Namespaces().Delete(namespace.Name, &metav1.DeleteOptions{}) 63 | if err != nil { 64 | return false, err 65 | } 66 | return true, nil 67 | } 68 | } 69 | return false, nil 70 | } 71 | return false, nil 72 | } 73 | 74 | func (n *NamespaceToDelete) isNamespaceinRestrictedNamespaces(namespace string) bool { 75 | for _, restrictedNamespace := range n.restrictedNamespaces { 76 | if restrictedNamespace == namespace { 77 | return true 78 | } 79 | } 80 | return false 81 | 82 | } 83 | 84 | // checkIfOld checks if the namespace is 7 days old 85 | func checkIfOld(namespace v1.Namespace, age string) bool { 86 | creationTime := namespace.CreationTimestamp 87 | var day, month, year int 88 | if age[len(age)-1] == 'd' { 89 | age := strings.TrimSuffix(age, "d") 90 | days, _ := strconv.Atoi(age) 91 | day = -1 * days 92 | month = 0 93 | year = 0 94 | } else if age[len(age)-1] == 'w' { 95 | age := strings.TrimSuffix(age, "w") 96 | weeks, _ := strconv.Atoi(age) 97 | day = -1 * 7 * weeks 98 | month = 0 99 | year = 0 100 | } else if age[len(age)-1] == 'm' { 101 | age := strings.TrimSuffix(age, "m") 102 | months, _ := strconv.Atoi(age) 103 | day = 0 104 | month = -1 * months 105 | year = 0 106 | } else if age[len(age)-1] == 'y' { 107 | age := strings.TrimSuffix(age, "y") 108 | years, _ := strconv.Atoi(age) 109 | day = 0 110 | month = 0 111 | year = -1 * years 112 | } 113 | 114 | weekAgoTime := time.Now().AddDate(year, month, day) 115 | weekAgo := metav1.NewTime(weekAgoTime) 116 | return creationTime.Before(&weekAgo) 117 | } 118 | -------------------------------------------------------------------------------- /internal/pkg/tasks/namespaces/namespaces_test.go: -------------------------------------------------------------------------------- 1 | package namespaces 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stakater/Jamadar/internal/pkg/actions" 8 | "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | clientset "k8s.io/client-go/kubernetes" 11 | testclient "k8s.io/client-go/kubernetes/fake" 12 | ) 13 | 14 | func TestNamespaceToDelete_DeleteNamespaces(t *testing.T) { 15 | type fields struct { 16 | clientset clientset.Interface 17 | actions []actions.Action 18 | age string 19 | restrictedNamespaces []string 20 | } 21 | type args struct { 22 | namespaceList *v1.NamespaceList 23 | } 24 | tests := []struct { 25 | name string 26 | fields fields 27 | args args 28 | wantErr bool 29 | }{ 30 | { 31 | // 1st has a less time so don't delete 32 | // 2nd will be deleted 33 | name: "DeleteNamespaces", 34 | args: args{ 35 | namespaceList: &v1.NamespaceList{ 36 | Items: []v1.Namespace{ 37 | v1.Namespace{ 38 | ObjectMeta: metav1.ObjectMeta{ 39 | Name: "ns-test1", 40 | Annotations: map[string]string{ 41 | "jamadar.stakater.com/persist": "false", 42 | }, 43 | CreationTimestamp: metav1.NewTime(time.Now()), 44 | }, 45 | }, 46 | v1.Namespace{ 47 | ObjectMeta: metav1.ObjectMeta{ 48 | Name: "ns-test2", 49 | Annotations: map[string]string{ 50 | "jamadar.stakater.com/persist": "false", 51 | }, 52 | }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | fields: fields{ 58 | clientset: testclient.NewSimpleClientset(), 59 | age: "1m", 60 | actions: []actions.Action{ 61 | &actions.Default{}, 62 | }, 63 | }, 64 | wantErr: false, 65 | }, 66 | { 67 | name: "DontDeleteNamespaces", 68 | args: args{ 69 | namespaceList: &v1.NamespaceList{ 70 | Items: []v1.Namespace{ 71 | v1.Namespace{ 72 | ObjectMeta: metav1.ObjectMeta{ 73 | Name: "ns-test1", 74 | Annotations: map[string]string{ 75 | "jamadar.stakater.com/persist": "false", 76 | }, 77 | }, 78 | }, 79 | v1.Namespace{ 80 | ObjectMeta: metav1.ObjectMeta{ 81 | Name: "ns-test2", 82 | // Annotations: map[string]string{ 83 | // "jamadar.stakater.com/persist": "false", 84 | // }, 85 | }, 86 | }, 87 | }, 88 | }, 89 | }, 90 | fields: fields{ 91 | clientset: testclient.NewSimpleClientset(), 92 | age: "1m", 93 | actions: []actions.Action{ 94 | &actions.Default{}, 95 | }, 96 | restrictedNamespaces: []string{ 97 | "ns-test1", 98 | }, 99 | }, 100 | wantErr: false, 101 | }, 102 | } 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | n := &NamespaceToDelete{ 106 | clientset: tt.fields.clientset, 107 | actions: tt.fields.actions, 108 | age: tt.fields.age, 109 | restrictedNamespaces: tt.fields.restrictedNamespaces, 110 | } 111 | tt.fields.clientset.CoreV1().Namespaces().Create(&tt.args.namespaceList.Items[0]) 112 | tt.fields.clientset.CoreV1().Namespaces().Create(&tt.args.namespaceList.Items[1]) 113 | if err := n.DeleteNamespaces(tt.args.namespaceList); (err != nil) != tt.wantErr { 114 | t.Errorf("NamespaceToDelete.DeleteNamespaces() error = %v, wantErr %v", err, tt.wantErr) 115 | } 116 | }) 117 | } 118 | } 119 | func TestDeleteNamespacesWithNoNamespaceCreatedError(t *testing.T) { 120 | namespaceList := &v1.NamespaceList{ 121 | Items: []v1.Namespace{ 122 | // Not Old 123 | v1.Namespace{ 124 | ObjectMeta: metav1.ObjectMeta{ 125 | Name: "ns-test1", 126 | Annotations: map[string]string{ 127 | "jamadar.stakater.com/persist": "false", 128 | }, 129 | CreationTimestamp: metav1.NewTime(time.Now()), 130 | }, 131 | }, 132 | // Actual test with actions that passes 133 | v1.Namespace{ 134 | ObjectMeta: metav1.ObjectMeta{ 135 | Name: "ns-test2", 136 | Annotations: map[string]string{ 137 | "jamadar.stakater.com/persist": "false", 138 | }, 139 | }, 140 | }, 141 | // Gives error as namespace not created 142 | v1.Namespace{ 143 | ObjectMeta: metav1.ObjectMeta{ 144 | Name: "ns-test3", 145 | Annotations: map[string]string{ 146 | "jamadar.stakater.com/persist": "false", 147 | }, 148 | }, 149 | }, 150 | }, 151 | } 152 | clientset := testclient.NewSimpleClientset() 153 | clientset.CoreV1().Namespaces().Create(&namespaceList.Items[1]) 154 | wantErr := true 155 | actions := []actions.Action{ 156 | &actions.Default{}, 157 | } 158 | n := &NamespaceToDelete{ 159 | clientset: clientset, 160 | actions: actions, 161 | age: "1m", 162 | restrictedNamespaces: []string{}, 163 | } 164 | err := n.DeleteNamespaces(namespaceList) 165 | if (err != nil) != wantErr { 166 | t.Errorf("DeleteNamespaces() error = %v, wantErr %v", err, wantErr) 167 | return 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /internal/pkg/tasks/tasks.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/stakater/Jamadar/internal/pkg/actions" 7 | "github.com/stakater/Jamadar/internal/pkg/config" 8 | "github.com/stakater/Jamadar/internal/pkg/tasks/namespaces" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | clientset "k8s.io/client-go/kubernetes" 11 | ) 12 | 13 | // Task represents the actual tasks and actions to be taken by Jamadar 14 | type Task struct { 15 | clientset clientset.Interface 16 | actions []actions.Action 17 | config config.Config 18 | } 19 | 20 | // NewTask creates a new Task object 21 | func NewTask(clientSet clientset.Interface, actions []actions.Action, conf config.Config) *Task { 22 | return &Task{ 23 | clientset: clientSet, 24 | actions: actions, 25 | config: conf, 26 | } 27 | } 28 | 29 | // PerformTasks handles all the cleanup tasks 30 | func (t *Task) PerformTasks() { 31 | functionMap := map[string]interface{}{ 32 | "namespaces": t.performNamespaceDeletion, 33 | "default": t.performDefault, 34 | } 35 | for _, resource := range t.config.Resources { 36 | functionMap[resource].(func())() 37 | } 38 | } 39 | 40 | // performNamespaceDeletion handles the deletion of namespaces 41 | func (t *Task) performNamespaceDeletion() { 42 | log.Println("Starting to delete Namespaces") 43 | namespaceList, err := t.clientset.CoreV1().Namespaces().List(metav1.ListOptions{}) 44 | if err != nil { 45 | log.Printf("Error getting namespaces: %v", err) 46 | return 47 | } 48 | namespace := namespaces.NewNamespaceToDelete(t.clientset, t.actions, t.config) 49 | err = namespace.DeleteNamespaces(namespaceList) 50 | if err != nil { 51 | log.Printf("Error deleting namespaces: %v", err) 52 | return 53 | } 54 | } 55 | 56 | // performDefault is the Default implementation 57 | func (t *Task) performDefault() { 58 | log.Println("Performing Default Tasks.") 59 | } 60 | -------------------------------------------------------------------------------- /internal/pkg/tasks/tasks_test.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stakater/Jamadar/internal/pkg/actions" 7 | "github.com/stakater/Jamadar/internal/pkg/config" 8 | clientset "k8s.io/client-go/kubernetes" 9 | testclient "k8s.io/client-go/kubernetes/fake" 10 | ) 11 | 12 | func TestTask_PerformTasks(t *testing.T) { 13 | type fields struct { 14 | clientset clientset.Interface 15 | actions []actions.Action 16 | conf config.Config 17 | } 18 | tests := []struct { 19 | name string 20 | fields fields 21 | }{ 22 | { 23 | name: "PerformTasksPass", 24 | fields: fields{ 25 | clientset: testclient.NewSimpleClientset(), 26 | actions: []actions.Action{ 27 | &actions.Default{}, 28 | }, 29 | conf: config.Config{ 30 | Age: "1d", 31 | Resources: []string{ 32 | "namespaces", 33 | }, 34 | }, 35 | }, 36 | }, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | task := &Task{ 41 | clientset: tt.fields.clientset, 42 | actions: tt.fields.actions, 43 | config: tt.fields.conf, 44 | } 45 | task.PerformTasks() 46 | }) 47 | } 48 | } 49 | func TestPerformTasksNoNamespaces(t *testing.T) { 50 | actions := []actions.Action{ 51 | &actions.Default{}, 52 | } 53 | conf := config.Config{ 54 | Age: "1y", 55 | Resources: []string{ 56 | "namespaces", 57 | }, 58 | } 59 | task := NewTask(testclient.NewSimpleClientset(), actions, conf) 60 | task.PerformTasks() 61 | } 62 | 63 | func TestPerformDefault(t *testing.T) { 64 | actions := []actions.Action{ 65 | &actions.Default{}, 66 | } 67 | conf := config.Config{ 68 | Age: "1y", 69 | Resources: []string{ 70 | "default", 71 | }, 72 | } 73 | task := NewTask(testclient.NewSimpleClientset(), actions, conf) 74 | task.PerformTasks() 75 | } 76 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/stakater/Jamadar/cmd/app" 7 | ) 8 | 9 | func main() { 10 | if err := app.Run(); err != nil { 11 | os.Exit(1) 12 | } 13 | os.Exit(0) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/kube/client.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "os" 5 | 6 | "k8s.io/client-go/kubernetes" 7 | "k8s.io/client-go/rest" 8 | "k8s.io/client-go/tools/clientcmd" 9 | ) 10 | 11 | // GetClient gets the client for k8s, if ~/.kube/config exists so get that config else incluster config 12 | func GetClient() (*kubernetes.Clientset, error) { 13 | var config *rest.Config 14 | var err error 15 | kubeconfigPath := os.Getenv("KUBECONFIG") 16 | if kubeconfigPath == "" { 17 | kubeconfigPath = os.Getenv("HOME") + "/.kube/config" 18 | } 19 | //If file exists so use that config settings 20 | if _, err := os.Stat(kubeconfigPath); err == nil { 21 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath) 22 | } else { //Use Incluster Configuration 23 | config, err = rest.InClusterConfig() 24 | } 25 | if err != nil { 26 | return nil, err 27 | } 28 | return kubernetes.NewForConfig(config) 29 | } 30 | -------------------------------------------------------------------------------- /stk.yaml: -------------------------------------------------------------------------------- 1 | issues: 2 | kind: 1 3 | url: https://aurorasolutions.atlassian.net 4 | project: STK --------------------------------------------------------------------------------