├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── LICENSE_TEMPLATE ├── Makefile ├── OWNERS ├── OWNERS_ALIASES ├── PROJECT ├── README.md ├── SECURITY_CONTACTS ├── VERSION ├── VERSION-DEV ├── api └── v1beta1 │ ├── application_types.go │ ├── application_types_test.go │ ├── groupversion_info.go │ ├── v1beta1_suite_test.go │ └── zz_generated.deepcopy.go ├── code-of-conduct.md ├── config ├── crd │ ├── bases │ │ └── app.k8s.io_applications.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_applications.yaml │ │ └── webhook_in_applications.yaml ├── default │ ├── base │ │ ├── app │ │ │ ├── kustomization.yaml │ │ │ └── manager_auth_proxy_patch.yaml │ │ ├── kustomization.yaml │ │ └── namespace.yaml │ └── scratch │ │ └── kustomization.yaml ├── kube-app-manager │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── monitor.yaml ├── rbac │ ├── application_editor_role.yaml │ ├── application_viewer_role.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ └── role_binding.yaml └── webhook │ ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_webhook_patch.yaml │ ├── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ ├── manifests.yaml │ └── service.yaml │ └── webhookcainjection_patch.yaml ├── controllers ├── application_controller.go ├── application_controller_test.go ├── condition.go ├── status.go └── suite_test.go ├── deploy └── kube-app-manager-aio.yaml ├── docs ├── api.md ├── develop.md ├── examples │ └── wordpress │ │ ├── application.yaml │ │ ├── kustomization.yaml │ │ ├── mysql.yaml │ │ ├── pv.yaml │ │ ├── secrets.txt │ │ └── webserver.yaml ├── quickstart.md └── release.md ├── e2e ├── kind-config.yaml ├── main_test.go ├── resources │ ├── app-crd-v0.8.0.yaml │ └── withcrd │ │ ├── base │ │ ├── application.yaml │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── job.yaml │ │ ├── kustomization.yaml │ │ ├── service.yaml │ │ ├── testcr1.yaml │ │ ├── testcr2.yaml │ │ └── testcr3.yaml │ │ ├── overlays │ │ ├── broken │ │ │ └── kustomization.yaml │ │ └── working │ │ │ └── kustomization.yaml │ │ └── test_crd.yaml └── testutil │ ├── appresource.go │ ├── customresource.go │ └── helpers.go ├── go.mod ├── go.sum ├── hack ├── boilerplate.go.txt └── tools │ ├── common.sh │ ├── install_kubebuilder.sh │ └── install_kustomize.sh └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # specific paths 2 | 3 | config/default/scratch/kustomization.yaml 4 | deploy/kube-app-manager-aio-dev.yaml 5 | 6 | 7 | # OSX leaves these everywhere on SMB shares 8 | ._* 9 | 10 | # OSX trash 11 | .DS_Store 12 | 13 | # Eclipse files 14 | .classpath 15 | .project 16 | .settings/** 17 | 18 | # editor and IDE paraphernalia 19 | .idea 20 | *.iml 21 | *.swp 22 | *.swo 23 | *~ 24 | 25 | # Vscode files 26 | .vscode 27 | 28 | # This is where the result of the go build goes 29 | /output*/ 30 | /_output*/ 31 | /_output 32 | 33 | # Emacs save files 34 | *~ 35 | \#*\# 36 | .\#* 37 | 38 | # Vim-related files 39 | [._]*.s[a-w][a-z] 40 | [._]s[a-w][a-z] 41 | *.un~ 42 | Session.vim 43 | .netrwhist 44 | 45 | # cscope-related files 46 | cscope.* 47 | 48 | # Go test binaries 49 | *.test 50 | /hack/.test-cmd-auth 51 | 52 | # JUnit test output from ginkgo e2e tests 53 | /junit*.xml 54 | 55 | # Mercurial files 56 | **/.hg 57 | **/.hg* 58 | 59 | # Vagrant 60 | .vagrant 61 | 62 | # Karma output 63 | /www/test_out 64 | 65 | # precommit temporary directories created by ./hack/verify-generated-docs.sh and ./hack/lib/util.sh 66 | /_tmp/ 67 | /doc_tmp/ 68 | 69 | # Test artifacts produced by Jenkins jobs 70 | /_artifacts/ 71 | 72 | # Go dependencies installed on Jenkins 73 | /_gopath/ 74 | 75 | # Config directories created by gcloud and gsutil on Jenkins 76 | /.config/gcloud*/ 77 | /.gsutil/ 78 | 79 | # direnv .envrc files 80 | .envrc 81 | 82 | # This file used by some vendor repos (e.g. github.com/go-openapi/...) to store secret variables and should not be ignored 83 | !\.drone\.sec 84 | 85 | # Godeps workspace 86 | /Godeps/_workspace 87 | 88 | /bazel-* 89 | *.pyc 90 | 91 | # Binaries for programs and plugins 92 | *.exe 93 | *.exe~ 94 | *.dll 95 | *.so 96 | *.dylib 97 | bin 98 | 99 | # Output of the go coverage tool, specifically when used with LiteIDE 100 | *.out 101 | 102 | # Kubernetes Generated files - skip generated files, except for vendored files 103 | 104 | !vendor/**/zz_generated.* 105 | 106 | 107 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | language: go 5 | 6 | os: 7 | - linux 8 | - osx 9 | 10 | go: 11 | - "1.13" 12 | 13 | git: 14 | depth: 3 15 | 16 | go_import_path: sigs.k8s.io/application 17 | 18 | # Install must be set to prevent default `go get` to run. 19 | # The dependencies have already been vendored by `dep` so 20 | # we don't need to fetch them. 21 | install: 22 | - 23 | 24 | before_script: 25 | # create Kind cluster 26 | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then make e2e-setup; fi 27 | 28 | script: 29 | # Ensure Build works and unit tests pass 30 | - make 31 | # Run e2e tests 32 | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then make e2e-test; fi 33 | 34 | after_script: 35 | # Delete Kind cluster 36 | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then make e2e-cleanup; fi 37 | 38 | # TBD. Suppressing for now. 39 | notifications: 40 | email: false 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | ## Sign the CLA 4 | 5 | Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests. 6 | 7 | Please see https://git.k8s.io/community/CLA.md for more info 8 | 9 | ## Contributing 10 | 11 | 1. Submit an issue describing your proposed change 12 | 1. The [repo owners](OWNERS) will respond to your issue promptly. 13 | 1. Develop and test your code changes. 14 | 1. Submit a pull request. 15 | 16 | ## CI Tests 17 | 18 | See [Travis](.travis.yml) file to check the travis tests. It is setup to run for all pull requests. 19 | In the Pull request check the CI job `continuous-integration/travis-ci/pr` and click on `Details`. 20 | 21 | ## Changing API 22 | 23 | This project uses and is built with [kubebuilder](https://github.com/kubernetes-sigs/kubebuilder). 24 | To regenerate code after changes to the [Application CRD](api/v1beta1/application_types.go), run `make generate`. Typically `make all` would take care of it. Make sure you add enough [tests](api/v1beta1/application_types_test.go). Update the [example](docs/examples/example.yaml) 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | 5 | # Build the manager binary 6 | FROM golang:1.13 as builder 7 | # Copy in the go src 8 | WORKDIR /workspace 9 | 10 | # Run this with docker build --build_arg $(go env GOPROXY) to override the goproxy 11 | ARG goproxy=https://proxy.golang.org 12 | ENV GOPROXY=$goproxy 13 | 14 | # Copy the Go Modules manifests 15 | COPY go.mod go.mod 16 | COPY go.sum go.sum 17 | # Cache deps before building and copying source so that we don't need to re-download as much 18 | # and so that source changes don't invalidate our downloaded layer 19 | RUN go mod download 20 | 21 | # Copy the go source 22 | COPY main.go main.go 23 | COPY api/ api/ 24 | COPY controllers/ controllers/ 25 | 26 | # Build 27 | ARG ARCH 28 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} GO111MODULE=on \ 29 | go build -a -ldflags '-extldflags "-static"' \ 30 | -o kube-app-manager main.go 31 | 32 | # Use distroless as minimal base image to package the manager binary 33 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 34 | FROM gcr.io/distroless/static:latest 35 | WORKDIR / 36 | COPY --from=builder /workspace/kube-app-manager . 37 | USER nobody 38 | ENTRYPOINT ["/kube-app-manager"] 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE_TEMPLATE: -------------------------------------------------------------------------------- 1 | Copyright {{.Year}} {{.Holder}} 2 | SPDX-License-Identifier: Apache-2.0 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | # Makefile for application 5 | 6 | VERSION_FILE ?= VERSION-DEV 7 | 8 | include $(VERSION_FILE) 9 | 10 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 11 | CRD_OPTIONS ?= "crd:trivialVersions=true,crdVersions=v1" 12 | # Turn on the CRD_OPTIONS below to generate the v1beta1 version of the Application CRD for kubernetes < 1.16 13 | #CRD_OPTIONS ?= "crd:trivialVersions=true,crdVersions=v1beta1" 14 | 15 | # Releases should modify and double check these vars. 16 | VER ?= v${app_major}.${app_minor}.${app_patch} 17 | ARCH ?= amd64 18 | ALL_ARCH = amd64 arm arm64 ppc64le s390x 19 | IMAGE_NAME = kube-app-manager 20 | 21 | ifeq ($(ARCH), amd64) 22 | IMAGE_TAG ?= $(VER) 23 | else 24 | IMAGE_TAG ?= $(ARCH)-$(VER) 25 | endif 26 | 27 | RELEASE_REMOTE ?= origin 28 | RELEASE_BRANCH ?= release-v${app_major}.${app_minor} 29 | RELEASE_TAG ?= v${app_major}.${app_minor}.${app_patch} 30 | 31 | CONTROLLER_IMG ?= $(REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG) 32 | 33 | # Directories. 34 | TOOLS_DIR := $(shell pwd)/hack/tools 35 | TOOLBIN := $(TOOLS_DIR)/bin 36 | 37 | # Allow overriding manifest generation destination directory 38 | MANIFEST_ROOT ?= config 39 | CRD_ROOT ?= $(MANIFEST_ROOT)/crd/bases 40 | WEBHOOK_ROOT ?= $(MANIFEST_ROOT)/webhook 41 | RBAC_ROOT ?= $(MANIFEST_ROOT)/rbac 42 | COVER_FILE ?= cover.out 43 | 44 | VERS := dev v0.8.3 45 | .DEFAULT_GOAL := all 46 | .PHONY: all 47 | all: generate fix vet fmt manifests test lint license misspell tidy bin/kube-app-manager 48 | 49 | 50 | ## -------------------------------------- 51 | ## Tooling Binaries 52 | ## -------------------------------------- 53 | 54 | $(TOOLBIN)/controller-gen: $(TOOLBIN)/kubectl 55 | GOBIN=$(TOOLBIN) GO111MODULE=on go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.4.0 56 | 57 | $(TOOLBIN)/golangci-lint: 58 | GOBIN=$(TOOLBIN) GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.23.6 59 | 60 | $(TOOLBIN)/mockgen: 61 | GOBIN=$(TOOLBIN) GO111MODULE=on go get github.com/golang/mock/mockgen@v1.3.1 62 | 63 | $(TOOLBIN)/conversion-gen: 64 | GOBIN=$(TOOLBIN) GO111MODULE=on go get k8s.io/code-generator/cmd/conversion-gen@v0.18.9 65 | 66 | $(TOOLBIN)/kubebuilder $(TOOLBIN)/etcd $(TOOLBIN)/kube-apiserver $(TOOLBIN)/kubectl: 67 | cd $(TOOLS_DIR); ./install_kubebuilder.sh 68 | cp $(TOOLBIN)/kubectl $(HOME)/bin 69 | 70 | $(TOOLBIN)/kustomize: 71 | cd $(TOOLS_DIR); ./install_kustomize.sh 72 | 73 | $(TOOLBIN)/kind: 74 | GOBIN=$(TOOLBIN) GO111MODULE=on go get sigs.k8s.io/kind@v0.9.0 75 | 76 | $(TOOLBIN)/addlicense: 77 | GOBIN=$(TOOLBIN) GO111MODULE=on go get github.com/google/addlicense 78 | 79 | $(TOOLBIN)/misspell: 80 | GOBIN=$(TOOLBIN) GO111MODULE=on go get github.com/client9/misspell/cmd/misspell@v0.3.4 81 | 82 | .PHONY: install-tools 83 | install-tools: \ 84 | $(TOOLBIN)/controller-gen \ 85 | $(TOOLBIN)/golangci-lint \ 86 | $(TOOLBIN)/mockgen \ 87 | $(TOOLBIN)/conversion-gen \ 88 | $(TOOLBIN)/kubebuilder \ 89 | $(TOOLBIN)/kustomize \ 90 | $(TOOLBIN)/addlicense \ 91 | $(TOOLBIN)/misspell \ 92 | $(TOOLBIN)/kind 93 | 94 | ## -------------------------------------- 95 | ## Tests 96 | ## -------------------------------------- 97 | 98 | # Run tests 99 | .PHONY: test 100 | test: $(TOOLBIN)/etcd $(TOOLBIN)/kube-apiserver $(TOOLBIN)/kubectl 101 | TEST_ASSET_KUBECTL=$(TOOLBIN)/kubectl \ 102 | TEST_ASSET_KUBE_APISERVER=$(TOOLBIN)/kube-apiserver \ 103 | TEST_ASSET_ETCD=$(TOOLBIN)/etcd \ 104 | go test -v ./api/... ./controllers/... -coverprofile $(COVER_FILE) 105 | 106 | # Run e2e-tests 107 | K8S_VERSION := "v1.18.2" 108 | 109 | .PHONY: e2e-setup 110 | e2e-setup: $(TOOLBIN)/kind 111 | $(TOOLBIN)/kind create cluster \ 112 | -v 4 --retain --wait=1m \ 113 | --config e2e/kind-config.yaml \ 114 | --image=kindest/node:$(K8S_VERSION) 115 | 116 | .PHONY: e2e-cleanup 117 | e2e-cleanup: $(TOOLBIN)/kind 118 | $(TOOLBIN)/kind delete cluster 119 | 120 | .PHONY: e2e-test 121 | e2e-test: generate fmt vet $(TOOLBIN)/kind $(TOOLBIN)/kustomize $(TOOLBIN)/kubectl 122 | go test -v ./e2e/main_test.go 123 | 124 | .PHONY: local-e2e-test 125 | local-e2e-test: e2e-setup e2e-test e2e-cleanup 126 | 127 | ## -------------------------------------- 128 | ## Build and run 129 | ## -------------------------------------- 130 | 131 | # Build kube-app-kube-app-manager binary 132 | bin/kube-app-manager: main.go generate fmt vet manifests 133 | go build -o bin/kube-app-manager main.go 134 | 135 | # Run against the configured Kubernetes cluster in ~/.kube/config 136 | .PHONY: runbg 137 | runbg: bin/kube-app-manager 138 | bin/kube-app-manager --metrics-addr ":8083" >& kube-app-manager.log & echo $$! > kube-app-manager.pid 139 | 140 | # Run against the configured Kubernetes cluster in ~/.kube/config 141 | .PHONY: run 142 | run: bin/kube-app-manager 143 | bin/kube-app-manager 144 | 145 | # Debug using the configured Kubernetes cluster in ~/.kube/config 146 | .PHONY: debug 147 | debug: generate fmt vet manifests 148 | dlv debug ./main.go 149 | 150 | 151 | ## -------------------------------------- 152 | ## Code maintenance 153 | ## -------------------------------------- 154 | 155 | .PHONY: fmt 156 | fmt: 157 | go fmt ./api/... ./controllers/... 158 | 159 | .PHONY: vet 160 | vet: 161 | go vet ./api/... ./controllers/... 162 | 163 | .PHONY: fix 164 | fix: 165 | go fix ./api/... ./controllers/... 166 | 167 | .PHONY: license 168 | license: $(TOOLBIN)/addlicense 169 | $(TOOLBIN)/addlicense -y $(shell date +"%Y") -c "The Kubernetes Authors." -f LICENSE_TEMPLATE . 170 | 171 | .PHONY: tidy 172 | tidy: 173 | go mod tidy 174 | 175 | .PHONY: lint 176 | lint: $(TOOLBIN)/golangci-lint 177 | $(TOOLBIN)/golangci-lint run ./... 178 | 179 | .PHONY: misspell 180 | misspell: $(TOOLBIN)/misspell 181 | $(TOOLBIN)/misspell ./** 182 | 183 | .PHONY: misspell-fix 184 | misspell-fix: $(TOOLBIN)/misspell 185 | $(TOOLBIN)/misspell -w ./** 186 | 187 | 188 | ## -------------------------------------- 189 | ## Deploy all (CRDs + Controller) 190 | ## -------------------------------------- 191 | 192 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 193 | # This is expected to be used by user during dev 194 | .PHONY: deploy 195 | deploy: 196 | kubectl apply -f deploy/kube-app-manager-aio.yaml 197 | 198 | # unDeploy controller in the configured Kubernetes cluster in ~/.kube/config 199 | .PHONY: undeploy 200 | undeploy: 201 | kubectl delete -f deploy/kube-app-manager-aio.yaml 202 | 203 | .PHONY: deploy-dev 204 | deploy-dev: $(TOOLBIN)/kubectl generate-resources 205 | $(TOOLBIN)/kubectl apply -f $(AIO_YAML) 206 | 207 | # unDeploy controller in the configured Kubernetes cluster in ~/.kube/config 208 | .PHONY: undeploy-dev 209 | undeploy-dev: $(TOOLBIN)/kubectl generate-resources 210 | $(TOOLBIN)/kubectl delete -f $(AIO_YAML) 211 | 212 | ## -------------------------------------- 213 | ## Deploy CRDs only 214 | ## -------------------------------------- 215 | # Install CRDs into a cluster, 216 | .PHONY: deploy-crd 217 | deploy-crd: $(TOOLBIN)/kustomize $(TOOLBIN)/kubectl 218 | $(TOOLBIN)/kustomize build config/crd| $(TOOLBIN)/kubectl apply -f - 219 | 220 | # Uninstall CRDs from a cluster 221 | .PHONY: undeploy-crd 222 | undeploy-crd: $(TOOLBIN)/kustomize $(TOOLBIN)/kubectl 223 | $(TOOLBIN)/kustomize build config/crd| $(TOOLBIN)/kubectl delete -f - 224 | 225 | ## -------------------------------------- 226 | ## Deploy demo 227 | ## -------------------------------------- 228 | 229 | # Deploy wordpress 230 | .PHONY: deploy-wordpress 231 | deploy-wordpress: $(TOOLBIN)/kustomize $(TOOLBIN)/kubectl 232 | mkdir -p /tmp/data1 /tmp/data2 233 | $(TOOLBIN)/kustomize build docs/examples/wordpress | $(TOOLBIN)/kubectl apply -f - 234 | 235 | # Uneploy wordpress 236 | .PHONY: undeploy-wordpress 237 | undeploy-wordpress: $(TOOLBIN)/kustomize $(TOOLBIN)/kubectl 238 | $(TOOLBIN)/kustomize build docs/examples/wordpress | $(TOOLBIN)/kubectl delete -f - 239 | # $(TOOLBIN)/kubectl delete pvc --all 240 | # sudo rm -fr /tmp/data1 /tmp/data2 241 | 242 | ## -------------------------------------- 243 | ## Generating 244 | ## -------------------------------------- 245 | 246 | .PHONY: generate 247 | generate: license ## Generate code 248 | $(MAKE) generate-go 249 | $(MAKE) manifests 250 | $(MAKE) generate-resources 251 | VERSION_FILE=VERSION $(MAKE) generate-resources 252 | $(MAKE) license 253 | 254 | # Generate manifests e.g. CRD, RBAC etc. 255 | .PHONY: manifests 256 | manifests: $(TOOLBIN)/controller-gen 257 | $(TOOLBIN)/controller-gen \ 258 | $(CRD_OPTIONS) \ 259 | rbac:roleName=kube-app-manager-role \ 260 | paths=./... \ 261 | output:crd:artifacts:config=$(CRD_ROOT) \ 262 | output:crd:dir=$(CRD_ROOT) \ 263 | output:webhook:dir=$(WEBHOOK_ROOT) \ 264 | webhook 265 | @for f in config/crd/bases/*.yaml; do \ 266 | kubectl annotate --overwrite -f $$f --local=true -o yaml api-approved.kubernetes.io=https://github.com/kubernetes-sigs/application/pull/2 > $$f.bk; \ 267 | mv $$f.bk $$f; \ 268 | done 269 | 270 | .PHONY: generate-resources 271 | generate-resources: $(TOOLBIN)/kustomize 272 | cd config/default/scratch && $(TOOLBIN)/kustomize edit set image kube-app-manager=$(CONTROLLER_IMG) 273 | $(TOOLBIN)/kustomize build config/default/scratch/ -o $(AIO_YAML) 274 | 275 | .PHONY: generate-go 276 | generate-go: $(TOOLBIN)/controller-gen $(TOOLBIN)/conversion-gen $(TOOLBIN)/mockgen 277 | go generate ./api/... ./controllers/... 278 | $(TOOLBIN)/controller-gen \ 279 | paths=./api/v1beta1/... \ 280 | object:headerFile=./hack/boilerplate.go.txt 281 | 282 | ## -------------------------------------- 283 | ## Docker 284 | ## -------------------------------------- 285 | .PHONY: set-image 286 | set-image: $(TOOLBIN)/kustomize 287 | @echo "updating kustomize image patch file for kube-app-manager resource" 288 | cd config/kube-app-manager && $(TOOLBIN)/kustomize edit set image kube-app-manager=$(CONTROLLER_IMG) 289 | 290 | .PHONY: docker-build 291 | docker-build: set-image test $(TOOLBIN)/kustomize ## Build the docker image for kube-app-manager 292 | docker build --network=host --pull --build-arg ARCH=$(ARCH) . -t $(CONTROLLER_IMG) 293 | 294 | .PHONY: docker-push 295 | docker-push: ## Push the docker image 296 | docker push $(CONTROLLER_IMG) 297 | 298 | .PHONY: clean 299 | clean: 300 | go clean --cache 301 | rm -f $(COVER_FILE) 302 | rm -f $(TOOLBIN)/kustomize 303 | rm -f $(TOOLBIN)/goimports 304 | rm -f $(TOOLBIN)/golangci-lint 305 | rm -f $(TOOLBIN)/controller-gen 306 | rm -f $(TOOLBIN)/conversion-gen 307 | rm -f $(TOOLBIN)/etcd 308 | rm -f $(TOOLBIN)/kube-apiserver 309 | rm -f $(TOOLBIN)/kubebuilder 310 | rm -f $(TOOLBIN)/addlicense 311 | rm -f $(TOOLBIN)/kubectl 312 | rm -f $(TOOLBIN)/kustomize 313 | rm -f $(TOOLBIN)/misspell 314 | rm -f $(TOOLBIN)/mockgen 315 | rm -f $(TOOLBIN)/kind 316 | 317 | 318 | ## -------------------------------------- 319 | ## Releasing 320 | ## -------------------------------------- 321 | .PHONY: release-branch 322 | release-branch: 323 | echo "checking branch=$(RELEASE_BRANCH)" 324 | git ls-remote --exit-code `git remote get-url $(RELEASE_REMOTE)` $(RELEASE_BRANCH) || make create-release-branch 325 | 326 | .PHONY: create-release-branch 327 | create-release-branch: 328 | git fetch upstream 329 | git checkout master 330 | git rebase upstream/master 331 | git branch -D $(RELEASE_BRANCH) || true 332 | git checkout -b $(RELEASE_BRANCH) 333 | git push -f $(RELEASE_REMOTE) $(RELEASE_BRANCH) 334 | 335 | .PHONY: release-tag 336 | release-tag: release-branch 337 | git branch -D $(RELEASE_BRANCH) || true 338 | git branch ${RELEASE_BRANCH} ${RELEASE_REMOTE}/${RELEASE_BRANCH} 339 | git checkout $(RELEASE_BRANCH) 340 | git tag -a ${RELEASE_TAG} -m "Release ${RELEASE_TAG} on branch ${RELEASE_BRANCH}" 341 | git push $(RELEASE_REMOTE) ${RELEASE_TAG} 342 | 343 | .PHONY: delete-release-tag 344 | delete-release-tag: 345 | git tag --delete $(RELEASE_TAG) 346 | git push $(RELEASE_REMOTE) :refs/tags/$(RELEASE_TAG) 347 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | approvers: 4 | - apps-application-approvers 5 | reviewers: 6 | - apps-application-reviewers -------------------------------------------------------------------------------- /OWNERS_ALIASES: -------------------------------------------------------------------------------- 1 | 2 | # See the OWNERS docs: https://git.k8s.io/community/contributors/devel/owners.md 3 | 4 | aliases: 5 | apps-application-approvers: 6 | - mattfarina 7 | - prydonius 8 | - kow3ns 9 | - janetkuo 10 | - barney-s 11 | - ant31 12 | apps-application-reviewers: 13 | - ant31 14 | - bryanl 15 | - garethr 16 | - pwittrock 17 | - bacongobbler 18 | - jascott1 19 | - mattfarina 20 | - michelleN 21 | - migmartri 22 | - nebril 23 | - prydonius 24 | - seh 25 | - SlickNik 26 | - technosophos 27 | - thomastaylor312 28 | - vaikas-google 29 | - cdrage 30 | - containscafeine 31 | - janetkuo 32 | - kadel 33 | - ngtuna 34 | - runseb 35 | - surajssd 36 | - kow3ns 37 | - barney-s 38 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | version: "1" 2 | domain: k8s.io 3 | repo: sigs.k8s.io/application 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://travis-ci.org/kubernetes-sigs/application "Travis") 2 | [](https://goreportcard.com/report/sigs.k8s.io/application) 3 | 4 | # Kubernetes Applications 5 | 6 | > Kubernetes is an open-source system for automating deployment, scaling, and management of containerized applications. 7 | 8 | The above description, from the [Kubernetes homepage](https://kubernetes.io/), is centered on containerized _applications_. Yet, the Kubernetes metadata, objects, and visualizations (e.g., within Dashboard) are focused on container infrastructure rather than the applications themselves. 9 | 10 | The Application CRD [(Custom Resource Definition)](https://kubernetes.io/docs/concepts/api-extension/custom-resources/#customresourcedefinitions) and [Controller](https://kubernetes.io/docs/concepts/api-extension/custom-resources/#custom-controllers) in this project aim to change that in a way that's interoperable between many supporting tools. 11 | 12 | **It provides:** 13 | 14 | * The ability to describe an application's metadata (e.g., that an application like WordPress is running) 15 | * A point to connect the infrastructure, such as Deployments, to as a root object. This is useful for tying things together and even cleanup (i.e., garbage collection) 16 | * Information for supporting applications to help them query and understand the objects supporting an application 17 | * Application level health checks 18 | 19 | **This can be used by:** 20 | 21 | * Application operators who want to center what they operate on applications 22 | * Tools, such as Helm, that center their package releases on application installations can do so in a way that's interoperable with other tools (e.g., Dashboard) 23 | * Dashboards that want to visualize the applications in addition to or instead of an infrastructure view 24 | 25 | ## Goals 26 | 27 | 1. Provide a standard API for creating, viewing, and managing applications in Kubernetes. 28 | 1. Provide a CLI implementation, via kubectl, that interacts with the Application API. 29 | 1. Provide installation status and garbage collection for applications. 30 | 1. Provide a standard way for applications to surface a basic health check to the UIs. 31 | 1. Provide an explicit mechanism for applications to declare dependencies on another application. 32 | 1. Promote interoperability among ecosystem tools and UIs by creating a standard that tools MAY implement. 33 | 1. Promote the use of common labels and annotations for Kubernetes Applications. 34 | 35 | ## Non-Goals 36 | 37 | 1. Create a standard that all tools MUST implement. 38 | 1. Provide a way for UIs to surface metrics from an application. 39 | 40 | ## Application API 41 | 42 | Refer [API doc](docs/api.md). 43 | For an example look at [wordpress application](docs/examples/wordpress/application.yaml) 44 | 45 | ## Quickstart 46 | 47 | Refer [Quickstart Guide](docs/quickstart.md) 48 | 49 | ## Development 50 | 51 | Refer [Development Guide](docs/develop.md) 52 | 53 | ## Contributing 54 | 55 | Go to the [CONTRIBUTING.md](CONTRIBUTING.md) documentation 56 | 57 | ## Community, discussion, contribution, and support 58 | 59 | Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). 60 | 61 | You can reach the maintainers of this project at: 62 | 63 | * [Slack](http://slack.k8s.io/) 64 | * [Mailing List](https://groups.google.com/d/forum/k8s-app-extension) 65 | 66 | ### Code of conduct 67 | 68 | Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). 69 | 70 | ## Releasing 71 | 72 | Refer [Releasing Guide](docs/release.md) 73 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Team to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://github.com/kubernetes/sig-release/blob/master/security-release-process-documentation/security-release-process.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://github.com/kubernetes/helm/blob/master/CONTRIBUTING.md#reporting-a-security-issue 12 | 13 | mattfarina 14 | prydonius 15 | kow3ns 16 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | export app_major=0 2 | export app_minor=8 3 | export app_patch=3 4 | 5 | 6 | export REGISTRY=quay.io/kubernetes-sigs 7 | export RELEASE_REMOTE=upstream 8 | export AIO_YAML=deploy/kube-app-manager-aio.yaml 9 | -------------------------------------------------------------------------------- /VERSION-DEV: -------------------------------------------------------------------------------- 1 | export app_major=0 2 | export app_minor=0 3 | export app_patch=0.$(shell git log --pretty=format:'%h' -n 1) 4 | 5 | 6 | export REGISTRY=gcr.io/$(shell gcloud config get-value project) 7 | export RELEASE_REMOTE=origin 8 | export AIO_YAML=deploy/kube-app-manager-aio-dev.yaml 9 | -------------------------------------------------------------------------------- /api/v1beta1/application_types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1beta1 5 | 6 | import ( 7 | "regexp" 8 | "strings" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | // Constants for condition 15 | const ( 16 | // Ready => controller considers this resource Ready 17 | Ready = "Ready" 18 | // Qualified => functionally tested 19 | Qualified = "Qualified" 20 | // Settled => observed generation == generation + settled means controller is done acting functionally tested 21 | Settled = "Settled" 22 | // Cleanup => it is set to track finalizer failures 23 | Cleanup = "Cleanup" 24 | // Error => last recorded error 25 | Error = "Error" 26 | 27 | ReasonInit = "Init" 28 | ) 29 | 30 | // Descriptor defines the Metadata and informations about the Application. 31 | type Descriptor struct { 32 | // Type is the type of the application (e.g. WordPress, MySQL, Cassandra). 33 | Type string `json:"type,omitempty"` 34 | 35 | // Version is an optional version indicator for the Application. 36 | Version string `json:"version,omitempty"` 37 | 38 | // Description is a brief string description of the Application. 39 | Description string `json:"description,omitempty"` 40 | 41 | // Icons is an optional list of icons for an application. Icon information includes the source, size, 42 | // and mime type. 43 | Icons []ImageSpec `json:"icons,omitempty"` 44 | 45 | // Maintainers is an optional list of maintainers of the application. The maintainers in this list maintain the 46 | // the source code, images, and package for the application. 47 | Maintainers []ContactData `json:"maintainers,omitempty"` 48 | 49 | // Owners is an optional list of the owners of the installed application. The owners of the application should be 50 | // contacted in the event of a planned or unplanned disruption affecting the application. 51 | Owners []ContactData `json:"owners,omitempty"` 52 | 53 | // Keywords is an optional list of key words associated with the application (e.g. MySQL, RDBMS, database). 54 | Keywords []string `json:"keywords,omitempty"` 55 | 56 | // Links are a list of descriptive URLs intended to be used to surface additional documentation, dashboards, etc. 57 | Links []Link `json:"links,omitempty"` 58 | 59 | // Notes contain a human readable snippets intended as a quick start for the users of the Application. 60 | // CommonMark markdown syntax may be used for rich text representation. 61 | Notes string `json:"notes,omitempty"` 62 | } 63 | 64 | // ApplicationSpec defines the specification for an Application. 65 | type ApplicationSpec struct { 66 | // ComponentGroupKinds is a list of Kinds for Application's components (e.g. Deployments, Pods, Services, CRDs). It 67 | // can be used in conjunction with the Application's Selector to list or watch the Applications components. 68 | ComponentGroupKinds []metav1.GroupKind `json:"componentKinds,omitempty"` 69 | 70 | // Descriptor regroups information and metadata about an application. 71 | Descriptor Descriptor `json:"descriptor,omitempty"` 72 | 73 | // Selector is a label query over kinds that created by the application. It must match the component objects' labels. 74 | // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors 75 | Selector *metav1.LabelSelector `json:"selector,omitempty"` 76 | 77 | // AddOwnerRef objects - flag to indicate if we need to add OwnerRefs to matching objects 78 | // Matching is done by using Selector to query all ComponentGroupKinds 79 | AddOwnerRef bool `json:"addOwnerRef,omitempty"` 80 | 81 | // Info contains human readable key,value pairs for the Application. 82 | // +patchStrategy=merge 83 | // +patchMergeKey=name 84 | Info []InfoItem `json:"info,omitempty" patchStrategy:"merge" patchMergeKey:"name"` 85 | 86 | // AssemblyPhase represents the current phase of the application's assembly. 87 | // An empty value is equivalent to "Succeeded". 88 | AssemblyPhase ApplicationAssemblyPhase `json:"assemblyPhase,omitempty"` 89 | } 90 | 91 | // ComponentList is a generic status holder for the top level resource 92 | type ComponentList struct { 93 | // Object status array for all matching objects 94 | Objects []ObjectStatus `json:"components,omitempty"` 95 | } 96 | 97 | // ObjectStatus is a generic status holder for objects 98 | type ObjectStatus struct { 99 | // Link to object 100 | Link string `json:"link,omitempty"` 101 | // Name of object 102 | Name string `json:"name,omitempty"` 103 | // Kind of object 104 | Kind string `json:"kind,omitempty"` 105 | // Object group 106 | Group string `json:"group,omitempty"` 107 | // Status. Values: InProgress, Ready, Unknown 108 | Status string `json:"status,omitempty"` 109 | } 110 | 111 | // ConditionType encodes information on the condition 112 | type ConditionType string 113 | 114 | // Condition describes the state of an object at a certain point. 115 | type Condition struct { 116 | // Type of condition. 117 | Type ConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=StatefulSetConditionType"` 118 | // Status of the condition, one of True, False, Unknown. 119 | Status corev1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=k8s.io/api/core/v1.ConditionStatus"` 120 | // The reason for the condition's last transition. 121 | // +optional 122 | Reason string `json:"reason,omitempty" protobuf:"bytes,4,opt,name=reason"` 123 | // A human readable message indicating details about the transition. 124 | // +optional 125 | Message string `json:"message,omitempty" protobuf:"bytes,5,opt,name=message"` 126 | // Last time the condition was probed 127 | // +optional 128 | LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty" protobuf:"bytes,3,opt,name=lastProbeTime"` 129 | // Last time the condition transitioned from one status to another. 130 | // +optional 131 | LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,3,opt,name=lastTransitionTime"` 132 | } 133 | 134 | // ApplicationStatus defines controller's the observed state of Application 135 | type ApplicationStatus struct { 136 | // ObservedGeneration is the most recent generation observed. It corresponds to the 137 | // Object's generation, which is updated on mutation by the API Server. 138 | // +optional 139 | ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` 140 | // Conditions represents the latest state of the object 141 | // +optional 142 | // +patchMergeKey=type 143 | // +patchStrategy=merge 144 | Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,10,rep,name=conditions"` 145 | // Resources embeds a list of object statuses 146 | // +optional 147 | ComponentList `json:",inline,omitempty"` 148 | // ComponentsReady: status of the components in the format ready/total 149 | // +optional 150 | ComponentsReady string `json:"componentsReady,omitempty"` 151 | } 152 | 153 | // ImageSpec contains information about an image used as an icon. 154 | type ImageSpec struct { 155 | // The source for image represented as either an absolute URL to the image or a Data URL containing 156 | // the image. Data URLs are defined in RFC 2397. 157 | Source string `json:"src"` 158 | 159 | // (optional) The size of the image in pixels (e.g., 25x25). 160 | Size string `json:"size,omitempty"` 161 | 162 | // (optional) The mine type of the image (e.g., "image/png"). 163 | Type string `json:"type,omitempty"` 164 | } 165 | 166 | // ContactData contains information about an individual or organization. 167 | type ContactData struct { 168 | // Name is the descriptive name. 169 | Name string `json:"name,omitempty"` 170 | 171 | // Url could typically be a website address. 172 | URL string `json:"url,omitempty"` 173 | 174 | // Email is the email address. 175 | Email string `json:"email,omitempty"` 176 | } 177 | 178 | // Link contains information about an URL to surface documentation, dashboards, etc. 179 | type Link struct { 180 | // Description is human readable content explaining the purpose of the link. 181 | Description string `json:"description,omitempty"` 182 | 183 | // Url typically points at a website address. 184 | URL string `json:"url,omitempty"` 185 | } 186 | 187 | // InfoItem is a human readable key,value pair containing important information about how to access the Application. 188 | type InfoItem struct { 189 | // Name is a human readable title for this piece of information. 190 | Name string `json:"name,omitempty"` 191 | 192 | // Type of the value for this InfoItem. 193 | Type InfoItemType `json:"type,omitempty"` 194 | 195 | // Value is human readable content. 196 | Value string `json:"value,omitempty"` 197 | 198 | // ValueFrom defines a reference to derive the value from another source. 199 | ValueFrom *InfoItemSource `json:"valueFrom,omitempty"` 200 | } 201 | 202 | // InfoItemType is a string that describes the value of InfoItem 203 | type InfoItemType string 204 | 205 | const ( 206 | // ValueInfoItemType const string for value type 207 | ValueInfoItemType InfoItemType = "Value" 208 | // ReferenceInfoItemType const string for ref type 209 | ReferenceInfoItemType InfoItemType = "Reference" 210 | ) 211 | 212 | // InfoItemSource represents a source for the value of an InfoItem. 213 | type InfoItemSource struct { 214 | // Type of source. 215 | Type InfoItemSourceType `json:"type,omitempty"` 216 | 217 | // Selects a key of a Secret. 218 | SecretKeyRef *SecretKeySelector `json:"secretKeyRef,omitempty"` 219 | 220 | // Selects a key of a ConfigMap. 221 | ConfigMapKeyRef *ConfigMapKeySelector `json:"configMapKeyRef,omitempty"` 222 | 223 | // Select a Service. 224 | ServiceRef *ServiceSelector `json:"serviceRef,omitempty"` 225 | 226 | // Select an Ingress. 227 | IngressRef *IngressSelector `json:"ingressRef,omitempty"` 228 | } 229 | 230 | // InfoItemSourceType is a string 231 | type InfoItemSourceType string 232 | 233 | // Constants for info type 234 | const ( 235 | SecretKeyRefInfoItemSourceType InfoItemSourceType = "SecretKeyRef" 236 | ConfigMapKeyRefInfoItemSourceType InfoItemSourceType = "ConfigMapKeyRef" 237 | ServiceRefInfoItemSourceType InfoItemSourceType = "ServiceRef" 238 | IngressRefInfoItemSourceType InfoItemSourceType = "IngressRef" 239 | ) 240 | 241 | // ConfigMapKeySelector selects a key from a ConfigMap. 242 | type ConfigMapKeySelector struct { 243 | // The ConfigMap to select from. 244 | corev1.ObjectReference `json:",inline"` 245 | // The key to select. 246 | Key string `json:"key,omitempty"` 247 | } 248 | 249 | // SecretKeySelector selects a key from a Secret. 250 | type SecretKeySelector struct { 251 | // The Secret to select from. 252 | corev1.ObjectReference `json:",inline"` 253 | // The key to select. 254 | Key string `json:"key,omitempty"` 255 | } 256 | 257 | // ServiceSelector selects a Service. 258 | type ServiceSelector struct { 259 | // The Service to select from. 260 | corev1.ObjectReference `json:",inline"` 261 | // The optional port to select. 262 | Port *int32 `json:"port,omitempty"` 263 | // The optional HTTP path. 264 | Path string `json:"path,omitempty"` 265 | // Protocol for the service 266 | Protocol string `json:"protocol,omitempty"` 267 | } 268 | 269 | // IngressSelector selects an Ingress. 270 | type IngressSelector struct { 271 | // The Ingress to select from. 272 | corev1.ObjectReference `json:",inline"` 273 | // The optional host to select. 274 | Host string `json:"host,omitempty"` 275 | // The optional HTTP path. 276 | Path string `json:"path,omitempty"` 277 | // Protocol for the ingress 278 | Protocol string `json:"protocol,omitempty"` 279 | } 280 | 281 | // ApplicationAssemblyPhase tracks the Application CRD phases: pending, succeeded, failed 282 | type ApplicationAssemblyPhase string 283 | 284 | // Constants 285 | const ( 286 | // Used to indicate that not all of application's components 287 | // have been deployed yet. 288 | Pending ApplicationAssemblyPhase = "Pending" 289 | // Used to indicate that all of application's components 290 | // have already been deployed. 291 | Succeeded = "Succeeded" 292 | // Used to indicate that deployment of application's components 293 | // failed. Some components might be present, but deployment of 294 | // the remaining ones will not be re-attempted. 295 | Failed = "Failed" 296 | ) 297 | 298 | // +kubebuilder:object:root=true 299 | // +kubebuilder:resource:categories=all,shortName=app 300 | // +kubebuilder:subresource:status 301 | // +kubebuilder:printcolumn:name="Type",type=string,description="The type of the application",JSONPath=`.spec.descriptor.type`,priority=0 302 | // +kubebuilder:printcolumn:name="Version",type=string,description="The creation date",JSONPath=`.spec.descriptor.version`,priority=0 303 | // +kubebuilder:printcolumn:name="Owner",type=boolean,description="The application object owns the matched resources",JSONPath=`.spec.addOwnerRef`,priority=0 304 | // +kubebuilder:printcolumn:name="Ready",type=string,description="Numbers of components ready",JSONPath=`.status.componentsReady`,priority=0 305 | // +kubebuilder:printcolumn:name="Age",type=date,description="The creation date",JSONPath=`.metadata.creationTimestamp`,priority=0 306 | 307 | // Application is the Schema for the applications API 308 | type Application struct { 309 | metav1.TypeMeta `json:",inline"` 310 | metav1.ObjectMeta `json:"metadata,omitempty"` 311 | 312 | Spec ApplicationSpec `json:"spec,omitempty"` 313 | Status ApplicationStatus `json:"status,omitempty"` 314 | } 315 | 316 | // +kubebuilder:object:root=true 317 | 318 | // ApplicationList contains a list of Application 319 | type ApplicationList struct { 320 | metav1.TypeMeta `json:",inline"` 321 | metav1.ListMeta `json:"metadata,omitempty"` 322 | Items []Application `json:"items"` 323 | } 324 | 325 | func init() { 326 | SchemeBuilder.Register(&Application{}, &ApplicationList{}) 327 | } 328 | 329 | // StripVersion the version part of gv 330 | func StripVersion(gv string) string { 331 | if gv == "" { 332 | return gv 333 | } 334 | 335 | re := regexp.MustCompile(`^[vV][0-9].*`) 336 | // If it begins with only version, (group is nil), return empty string which maps to core group 337 | if re.MatchString(gv) { 338 | return "" 339 | } 340 | 341 | return strings.Split(gv, "/")[0] 342 | } 343 | -------------------------------------------------------------------------------- /api/v1beta1/application_types_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1beta1 5 | 6 | import ( 7 | "testing" 8 | 9 | "context" 10 | "github.com/onsi/gomega" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/types" 13 | ) 14 | 15 | func TestStorageApplication(t *testing.T) { 16 | key := types.NamespacedName{ 17 | Name: "foo", 18 | Namespace: "default", 19 | } 20 | created := &Application{ 21 | ObjectMeta: metav1.ObjectMeta{ 22 | Name: "foo", 23 | Namespace: "default", 24 | }} 25 | g := gomega.NewGomegaWithT(t) 26 | 27 | // Test Create 28 | fetched := &Application{} 29 | g.Expect(c.Create(context.TODO(), created)).NotTo(gomega.HaveOccurred()) 30 | 31 | g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred()) 32 | g.Expect(fetched).To(gomega.Equal(created)) 33 | 34 | // Test Updating the Labels 35 | updated := fetched.DeepCopy() 36 | updated.Labels = map[string]string{"hello": "world"} 37 | g.Expect(c.Update(context.TODO(), updated)).NotTo(gomega.HaveOccurred()) 38 | 39 | g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred()) 40 | g.Expect(fetched).To(gomega.Equal(updated)) 41 | 42 | // Test Delete 43 | g.Expect(c.Delete(context.TODO(), fetched)).NotTo(gomega.HaveOccurred()) 44 | g.Expect(c.Get(context.TODO(), key, fetched)).To(gomega.HaveOccurred()) 45 | 46 | // Test stripVersion() 47 | g.Expect(StripVersion("")).To(gomega.Equal("")) 48 | g.Expect(StripVersion("v1beta1")).To(gomega.Equal("")) 49 | g.Expect(StripVersion("apps/v1")).To(gomega.Equal("apps")) 50 | g.Expect(StripVersion("apps/v1alpha2")).To(gomega.Equal("apps")) 51 | } 52 | -------------------------------------------------------------------------------- /api/v1beta1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package v1beta1 contains API Schema definitions for the app v1beta1 API group 5 | // +kubebuilder:object:generate=true 6 | // +groupName=app.k8s.io 7 | package v1beta1 8 | 9 | import ( 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "sigs.k8s.io/controller-runtime/pkg/scheme" 12 | ) 13 | 14 | var ( 15 | // GroupVersion is group version used to register these objects 16 | GroupVersion = schema.GroupVersion{Group: "app.k8s.io", Version: "v1beta1"} 17 | 18 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 19 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 20 | 21 | // AddToScheme adds the types in this group-version to the given scheme. 22 | AddToScheme = SchemeBuilder.AddToScheme 23 | ) 24 | -------------------------------------------------------------------------------- /api/v1beta1/v1beta1_suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1beta1 5 | 6 | import ( 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "k8s.io/client-go/kubernetes/scheme" 13 | "k8s.io/client-go/rest" 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | "sigs.k8s.io/controller-runtime/pkg/envtest" 16 | ) 17 | 18 | var cfg *rest.Config 19 | var c client.Client 20 | 21 | func TestMain(m *testing.M) { 22 | t := &envtest.Environment{ 23 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 24 | } 25 | 26 | err := SchemeBuilder.AddToScheme(scheme.Scheme) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | if cfg, err = t.Start(); err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | if c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}); err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | code := m.Run() 40 | _ = t.Stop() 41 | os.Exit(code) 42 | } 43 | -------------------------------------------------------------------------------- /api/v1beta1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2018 The Kubernetes Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1beta1 22 | 23 | import ( 24 | "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *Application) DeepCopyInto(out *Application) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | in.Spec.DeepCopyInto(&out.Spec) 34 | in.Status.DeepCopyInto(&out.Status) 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Application. 38 | func (in *Application) DeepCopy() *Application { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(Application) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *Application) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *ApplicationList) DeepCopyInto(out *ApplicationList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]Application, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | } 68 | 69 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationList. 70 | func (in *ApplicationList) DeepCopy() *ApplicationList { 71 | if in == nil { 72 | return nil 73 | } 74 | out := new(ApplicationList) 75 | in.DeepCopyInto(out) 76 | return out 77 | } 78 | 79 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 80 | func (in *ApplicationList) DeepCopyObject() runtime.Object { 81 | if c := in.DeepCopy(); c != nil { 82 | return c 83 | } 84 | return nil 85 | } 86 | 87 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 88 | func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) { 89 | *out = *in 90 | if in.ComponentGroupKinds != nil { 91 | in, out := &in.ComponentGroupKinds, &out.ComponentGroupKinds 92 | *out = make([]v1.GroupKind, len(*in)) 93 | copy(*out, *in) 94 | } 95 | in.Descriptor.DeepCopyInto(&out.Descriptor) 96 | if in.Selector != nil { 97 | in, out := &in.Selector, &out.Selector 98 | *out = new(v1.LabelSelector) 99 | (*in).DeepCopyInto(*out) 100 | } 101 | if in.Info != nil { 102 | in, out := &in.Info, &out.Info 103 | *out = make([]InfoItem, len(*in)) 104 | for i := range *in { 105 | (*in)[i].DeepCopyInto(&(*out)[i]) 106 | } 107 | } 108 | } 109 | 110 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSpec. 111 | func (in *ApplicationSpec) DeepCopy() *ApplicationSpec { 112 | if in == nil { 113 | return nil 114 | } 115 | out := new(ApplicationSpec) 116 | in.DeepCopyInto(out) 117 | return out 118 | } 119 | 120 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 121 | func (in *ApplicationStatus) DeepCopyInto(out *ApplicationStatus) { 122 | *out = *in 123 | if in.Conditions != nil { 124 | in, out := &in.Conditions, &out.Conditions 125 | *out = make([]Condition, len(*in)) 126 | for i := range *in { 127 | (*in)[i].DeepCopyInto(&(*out)[i]) 128 | } 129 | } 130 | in.ComponentList.DeepCopyInto(&out.ComponentList) 131 | } 132 | 133 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationStatus. 134 | func (in *ApplicationStatus) DeepCopy() *ApplicationStatus { 135 | if in == nil { 136 | return nil 137 | } 138 | out := new(ApplicationStatus) 139 | in.DeepCopyInto(out) 140 | return out 141 | } 142 | 143 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 144 | func (in *ComponentList) DeepCopyInto(out *ComponentList) { 145 | *out = *in 146 | if in.Objects != nil { 147 | in, out := &in.Objects, &out.Objects 148 | *out = make([]ObjectStatus, len(*in)) 149 | copy(*out, *in) 150 | } 151 | } 152 | 153 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentList. 154 | func (in *ComponentList) DeepCopy() *ComponentList { 155 | if in == nil { 156 | return nil 157 | } 158 | out := new(ComponentList) 159 | in.DeepCopyInto(out) 160 | return out 161 | } 162 | 163 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 164 | func (in *Condition) DeepCopyInto(out *Condition) { 165 | *out = *in 166 | in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) 167 | in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) 168 | } 169 | 170 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. 171 | func (in *Condition) DeepCopy() *Condition { 172 | if in == nil { 173 | return nil 174 | } 175 | out := new(Condition) 176 | in.DeepCopyInto(out) 177 | return out 178 | } 179 | 180 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 181 | func (in *ConfigMapKeySelector) DeepCopyInto(out *ConfigMapKeySelector) { 182 | *out = *in 183 | out.ObjectReference = in.ObjectReference 184 | } 185 | 186 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapKeySelector. 187 | func (in *ConfigMapKeySelector) DeepCopy() *ConfigMapKeySelector { 188 | if in == nil { 189 | return nil 190 | } 191 | out := new(ConfigMapKeySelector) 192 | in.DeepCopyInto(out) 193 | return out 194 | } 195 | 196 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 197 | func (in *ContactData) DeepCopyInto(out *ContactData) { 198 | *out = *in 199 | } 200 | 201 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContactData. 202 | func (in *ContactData) DeepCopy() *ContactData { 203 | if in == nil { 204 | return nil 205 | } 206 | out := new(ContactData) 207 | in.DeepCopyInto(out) 208 | return out 209 | } 210 | 211 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 212 | func (in *Descriptor) DeepCopyInto(out *Descriptor) { 213 | *out = *in 214 | if in.Icons != nil { 215 | in, out := &in.Icons, &out.Icons 216 | *out = make([]ImageSpec, len(*in)) 217 | copy(*out, *in) 218 | } 219 | if in.Maintainers != nil { 220 | in, out := &in.Maintainers, &out.Maintainers 221 | *out = make([]ContactData, len(*in)) 222 | copy(*out, *in) 223 | } 224 | if in.Owners != nil { 225 | in, out := &in.Owners, &out.Owners 226 | *out = make([]ContactData, len(*in)) 227 | copy(*out, *in) 228 | } 229 | if in.Keywords != nil { 230 | in, out := &in.Keywords, &out.Keywords 231 | *out = make([]string, len(*in)) 232 | copy(*out, *in) 233 | } 234 | if in.Links != nil { 235 | in, out := &in.Links, &out.Links 236 | *out = make([]Link, len(*in)) 237 | copy(*out, *in) 238 | } 239 | } 240 | 241 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Descriptor. 242 | func (in *Descriptor) DeepCopy() *Descriptor { 243 | if in == nil { 244 | return nil 245 | } 246 | out := new(Descriptor) 247 | in.DeepCopyInto(out) 248 | return out 249 | } 250 | 251 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 252 | func (in *ImageSpec) DeepCopyInto(out *ImageSpec) { 253 | *out = *in 254 | } 255 | 256 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageSpec. 257 | func (in *ImageSpec) DeepCopy() *ImageSpec { 258 | if in == nil { 259 | return nil 260 | } 261 | out := new(ImageSpec) 262 | in.DeepCopyInto(out) 263 | return out 264 | } 265 | 266 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 267 | func (in *InfoItem) DeepCopyInto(out *InfoItem) { 268 | *out = *in 269 | if in.ValueFrom != nil { 270 | in, out := &in.ValueFrom, &out.ValueFrom 271 | *out = new(InfoItemSource) 272 | (*in).DeepCopyInto(*out) 273 | } 274 | } 275 | 276 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfoItem. 277 | func (in *InfoItem) DeepCopy() *InfoItem { 278 | if in == nil { 279 | return nil 280 | } 281 | out := new(InfoItem) 282 | in.DeepCopyInto(out) 283 | return out 284 | } 285 | 286 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 287 | func (in *InfoItemSource) DeepCopyInto(out *InfoItemSource) { 288 | *out = *in 289 | if in.SecretKeyRef != nil { 290 | in, out := &in.SecretKeyRef, &out.SecretKeyRef 291 | *out = new(SecretKeySelector) 292 | **out = **in 293 | } 294 | if in.ConfigMapKeyRef != nil { 295 | in, out := &in.ConfigMapKeyRef, &out.ConfigMapKeyRef 296 | *out = new(ConfigMapKeySelector) 297 | **out = **in 298 | } 299 | if in.ServiceRef != nil { 300 | in, out := &in.ServiceRef, &out.ServiceRef 301 | *out = new(ServiceSelector) 302 | (*in).DeepCopyInto(*out) 303 | } 304 | if in.IngressRef != nil { 305 | in, out := &in.IngressRef, &out.IngressRef 306 | *out = new(IngressSelector) 307 | **out = **in 308 | } 309 | } 310 | 311 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfoItemSource. 312 | func (in *InfoItemSource) DeepCopy() *InfoItemSource { 313 | if in == nil { 314 | return nil 315 | } 316 | out := new(InfoItemSource) 317 | in.DeepCopyInto(out) 318 | return out 319 | } 320 | 321 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 322 | func (in *IngressSelector) DeepCopyInto(out *IngressSelector) { 323 | *out = *in 324 | out.ObjectReference = in.ObjectReference 325 | } 326 | 327 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressSelector. 328 | func (in *IngressSelector) DeepCopy() *IngressSelector { 329 | if in == nil { 330 | return nil 331 | } 332 | out := new(IngressSelector) 333 | in.DeepCopyInto(out) 334 | return out 335 | } 336 | 337 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 338 | func (in *Link) DeepCopyInto(out *Link) { 339 | *out = *in 340 | } 341 | 342 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Link. 343 | func (in *Link) DeepCopy() *Link { 344 | if in == nil { 345 | return nil 346 | } 347 | out := new(Link) 348 | in.DeepCopyInto(out) 349 | return out 350 | } 351 | 352 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 353 | func (in *ObjectStatus) DeepCopyInto(out *ObjectStatus) { 354 | *out = *in 355 | } 356 | 357 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectStatus. 358 | func (in *ObjectStatus) DeepCopy() *ObjectStatus { 359 | if in == nil { 360 | return nil 361 | } 362 | out := new(ObjectStatus) 363 | in.DeepCopyInto(out) 364 | return out 365 | } 366 | 367 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 368 | func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) { 369 | *out = *in 370 | out.ObjectReference = in.ObjectReference 371 | } 372 | 373 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector. 374 | func (in *SecretKeySelector) DeepCopy() *SecretKeySelector { 375 | if in == nil { 376 | return nil 377 | } 378 | out := new(SecretKeySelector) 379 | in.DeepCopyInto(out) 380 | return out 381 | } 382 | 383 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 384 | func (in *ServiceSelector) DeepCopyInto(out *ServiceSelector) { 385 | *out = *in 386 | out.ObjectReference = in.ObjectReference 387 | if in.Port != nil { 388 | in, out := &in.Port, &out.Port 389 | *out = new(int32) 390 | **out = **in 391 | } 392 | } 393 | 394 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSelector. 395 | func (in *ServiceSelector) DeepCopy() *ServiceSelector { 396 | if in == nil { 397 | return nil 398 | } 399 | out := new(ServiceSelector) 400 | in.DeepCopyInto(out) 401 | return out 402 | } 403 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | # This kustomization.yaml is not intended to be run by itself, 7 | # since it depends on service name and namespace that are out of this kustomize package. 8 | # It should be run by config/default 9 | resources: 10 | - bases/app.k8s.io_applications.yaml 11 | # +kubebuilder:scaffold:crdkustomizeresource 12 | 13 | patchesStrategicMerge: 14 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 15 | # patches here are for enabling the conversion webhook for each CRD 16 | #- patches/webhook_in_applications.yaml 17 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 18 | 19 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 20 | # patches here are for enabling the CA injection for each CRD 21 | #- patches/cainjection_in_applications.yaml 22 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 23 | 24 | # the following config is for teaching kustomize how to do kustomization for CRDs. 25 | configurations: 26 | - kustomizeconfig.yaml 27 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 5 | nameReference: 6 | - kind: Service 7 | version: v1 8 | fieldSpecs: 9 | - kind: CustomResourceDefinition 10 | group: apiextensions.k8s.io 11 | path: spec/conversion/webhookClientConfig/service/name 12 | 13 | namespace: 14 | - kind: CustomResourceDefinition 15 | group: apiextensions.k8s.io 16 | path: spec/conversion/webhookClientConfig/service/namespace 17 | create: false 18 | 19 | varReference: 20 | - path: metadata/annotations 21 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_applications.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # The following patch adds a directive for certmanager to inject CA into the CRD 5 | # CRD conversion requires k8s 1.13 or later. 6 | apiVersion: apiextensions.k8s.io/v1 7 | kind: CustomResourceDefinition 8 | metadata: 9 | annotations: 10 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 11 | name: applications.app.k8s.io 12 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_applications.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # The following patch enables conversion webhook for CRD 5 | # CRD conversion requires k8s 1.13 or later. 6 | apiVersion: apiextensions.k8s.io/v1 7 | kind: CustomResourceDefinition 8 | metadata: 9 | name: applications.app.k8s.io 10 | spec: 11 | conversion: 12 | strategy: Webhook 13 | webhookClientConfig: 14 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 15 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 16 | caBundle: Cg== 17 | service: 18 | namespace: system 19 | name: webhook-service 20 | path: /convert 21 | -------------------------------------------------------------------------------- /config/default/base/app/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | 7 | # Adds namespace to all resources. 8 | namespace: application-system 9 | 10 | 11 | # Value of this field is prepended to the 12 | # names of all resources, e.g. a deployment named 13 | # "wordpress" becomes "alices-wordpress". 14 | # Note that it should also match with the prefix (text before '-') of the namespace 15 | # field above. 16 | namePrefix: kube-app-manager- 17 | 18 | # Labels to add to all resources and selectors. 19 | 20 | commonLabels: 21 | control-plane: kube-app-manager 22 | app.kubernetes.io/name: kube-app-manager 23 | 24 | bases: 25 | - ../../../crd 26 | - ../../../rbac 27 | - ../../../kube-app-manager 28 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 29 | #- ../../prometheus 30 | 31 | patchesStrategicMerge: 32 | # Protect the /metrics endpoint by putting it behind auth. 33 | - manager_auth_proxy_patch.yaml 34 | -------------------------------------------------------------------------------- /config/default/base/app/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # This patch inject a sidecar container which is a HTTP proxy for the controller manager, 5 | # it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 6 | apiVersion: apps/v1 7 | kind: Deployment 8 | metadata: 9 | name: controller 10 | namespace: system 11 | spec: 12 | template: 13 | spec: 14 | containers: 15 | - name: kube-rbac-proxy 16 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.4.1 17 | args: 18 | - "--secure-listen-address=0.0.0.0:8443" 19 | - "--upstream=http://127.0.0.1:8080/" 20 | - "--logtostderr=true" 21 | - "--v=10" 22 | ports: 23 | - containerPort: 8443 24 | name: https 25 | - name: kube-app-manager 26 | args: 27 | - "--metrics-addr=127.0.0.1:8080" 28 | - "--enable-leader-election" 29 | -------------------------------------------------------------------------------- /config/default/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | resources: 7 | - namespace.yaml 8 | bases: 9 | - app 10 | -------------------------------------------------------------------------------- /config/default/base/namespace.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: v1 5 | kind: Namespace 6 | metadata: 7 | labels: 8 | controller-tools.k8s.io: "1.0" 9 | name: application-system 10 | -------------------------------------------------------------------------------- /config/default/scratch/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | 7 | # Adds namespace to all resources. 8 | namespace: application-system 9 | 10 | 11 | images: 12 | - name: kube-app-manager 13 | newName: quay.io/kubernetes-sigs/kube-app-manager 14 | newTag: v0.8.3 15 | resources: 16 | - ../base 17 | -------------------------------------------------------------------------------- /config/kube-app-manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | 7 | resources: 8 | - manager.yaml 9 | 10 | configurations: 11 | - kustomizeconfig.yaml 12 | images: 13 | - name: kube-app-manager 14 | newName: quay.io/kubernetes-sigs/kube-app-manager 15 | newTag: v0.8.3 16 | -------------------------------------------------------------------------------- /config/kube-app-manager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # the following config is for teaching kustomize where to look at when substituting vars. 5 | # It requires kustomize v2.1.0 or newer to work properly. 6 | -------------------------------------------------------------------------------- /config/kube-app-manager/manager.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | --- 4 | apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: service 8 | namespace: system 9 | labels: 10 | control-plane: controller-manager 11 | controller-tools.k8s.io: "1.0" 12 | spec: 13 | selector: 14 | control-plane: controller-manager 15 | controller-tools.k8s.io: "1.0" 16 | ports: 17 | - port: 443 18 | --- 19 | apiVersion: apps/v1 20 | kind: Deployment 21 | metadata: 22 | name: controller 23 | namespace: system 24 | labels: 25 | control-plane: controller-manager 26 | controller-tools.k8s.io: "1.0" 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | labels: 35 | control-plane: controller-manager 36 | controller-tools.k8s.io: "1.0" 37 | spec: 38 | containers: 39 | - command: 40 | - /kube-app-manager 41 | args: 42 | - --enable-leader-election 43 | image: kube-app-manager:dev 44 | imagePullPolicy: IfNotPresent 45 | name: kube-app-manager 46 | resources: 47 | limits: 48 | cpu: 100m 49 | memory: 30Mi 50 | requests: 51 | cpu: 100m 52 | memory: 20Mi 53 | terminationGracePeriodSeconds: 10 54 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | resources: 5 | - monitor.yaml 6 | 7 | configurations: 8 | - kustomizeconfig.yaml 9 | -------------------------------------------------------------------------------- /config/prometheus/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # the following config is for teaching kustomize where to look at when substituting vars. 5 | # It requires kustomize v2.1.0 or newer to work properly. 6 | 7 | commonLabels: 8 | - path: spec/selector 9 | create: true 10 | kind: ServiceMonitor 11 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | 5 | # Prometheus Monitor Service (Metrics) 6 | apiVersion: monitoring.coreos.com/v1 7 | kind: ServiceMonitor 8 | metadata: 9 | labels: 10 | control-plane: controller-manager 11 | name: metrics-monitor 12 | namespace: system 13 | spec: 14 | endpoints: 15 | - path: /metrics 16 | port: https 17 | selector: 18 | control-plane: controller-manager 19 | -------------------------------------------------------------------------------- /config/rbac/application_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # permissions to do edit applications. 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: ClusterRole 7 | metadata: 8 | name: application-editor-role 9 | rules: 10 | - apiGroups: 11 | - app.k8s.io 12 | resources: 13 | - applications 14 | verbs: 15 | - create 16 | - delete 17 | - get 18 | - list 19 | - patch 20 | - update 21 | - watch 22 | - apiGroups: 23 | - app.k8s.io 24 | resources: 25 | - applications/status 26 | verbs: 27 | - get 28 | - patch 29 | - update 30 | -------------------------------------------------------------------------------- /config/rbac/application_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # permissions to do viewer applications. 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: ClusterRole 7 | metadata: 8 | name: application-viewer-role 9 | rules: 10 | - apiGroups: 11 | - app.k8s.io 12 | resources: 13 | - applications 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - apiGroups: 19 | - app.k8s.io 20 | resources: 21 | - applications/status 22 | verbs: 23 | - get 24 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: rbac.authorization.k8s.io/v1 5 | kind: ClusterRole 6 | metadata: 7 | name: proxy-role 8 | rules: 9 | - apiGroups: ["authentication.k8s.io"] 10 | resources: 11 | - tokenreviews 12 | verbs: ["create"] 13 | - apiGroups: ["authorization.k8s.io"] 14 | resources: 15 | - subjectaccessreviews 16 | verbs: ["create"] 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: rbac.authorization.k8s.io/v1 5 | kind: ClusterRoleBinding 6 | metadata: 7 | name: proxy-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: proxy-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: default 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: v1 5 | kind: Service 6 | metadata: 7 | labels: 8 | control-plane: controller-manager 9 | name: metrics-service 10 | namespace: system 11 | spec: 12 | ports: 13 | - name: https 14 | port: 8443 15 | targetPort: https 16 | selector: 17 | control-plane: controller-manager 18 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | resources: 7 | - role.yaml 8 | - role_binding.yaml 9 | - leader_election_role.yaml 10 | - leader_election_role_binding.yaml 11 | # Comment the following 3 lines if you want to disable 12 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 13 | # which protects your /metrics endpoint. 14 | - auth_proxy_service.yaml 15 | - auth_proxy_role.yaml 16 | - auth_proxy_role_binding.yaml -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # permissions to do leader election. 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: Role 7 | metadata: 8 | name: leader-election-role 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - configmaps 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - create 19 | - update 20 | - patch 21 | - delete 22 | - apiGroups: 23 | - "" 24 | resources: 25 | - configmaps/status 26 | verbs: 27 | - get 28 | - update 29 | - patch 30 | - apiGroups: 31 | - "" 32 | resources: 33 | - events 34 | verbs: 35 | - create 36 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: rbac.authorization.k8s.io/v1 5 | kind: RoleBinding 6 | metadata: 7 | name: leader-election-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: leader-election-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: default 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | 5 | --- 6 | apiVersion: rbac.authorization.k8s.io/v1 7 | kind: ClusterRole 8 | metadata: 9 | creationTimestamp: null 10 | name: kube-app-manager-role 11 | rules: 12 | - apiGroups: 13 | - '*' 14 | resources: 15 | - '*' 16 | verbs: 17 | - get 18 | - list 19 | - patch 20 | - update 21 | - watch 22 | - apiGroups: 23 | - app.k8s.io 24 | resources: 25 | - applications 26 | verbs: 27 | - create 28 | - delete 29 | - get 30 | - list 31 | - patch 32 | - update 33 | - watch 34 | - apiGroups: 35 | - app.k8s.io 36 | resources: 37 | - applications/status 38 | verbs: 39 | - get 40 | - patch 41 | - update 42 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: rbac.authorization.k8s.io/v1 5 | kind: ClusterRoleBinding 6 | metadata: 7 | name: kube-app-manager-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: kube-app-manager-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: default 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/webhook/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # The following manifests contain a self-signed issuer CR and a certificate CR. 5 | # More document can be found at https://docs.cert-manager.io 6 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for breaking changes 7 | apiVersion: cert-manager.io/v1alpha2 8 | kind: Issuer 9 | metadata: 10 | name: selfsigned-issuer 11 | namespace: system 12 | spec: 13 | selfSigned: {} 14 | --- 15 | apiVersion: cert-manager.io/v1alpha2 16 | kind: Certificate 17 | metadata: 18 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 19 | namespace: system 20 | spec: 21 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 22 | dnsNames: 23 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 24 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 25 | issuerRef: 26 | kind: Issuer 27 | name: selfsigned-issuer 28 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 29 | -------------------------------------------------------------------------------- /config/webhook/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | resources: 5 | - certificate.yaml 6 | 7 | configurations: 8 | - kustomizeconfig.yaml 9 | -------------------------------------------------------------------------------- /config/webhook/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # This configuration is for teaching kustomize how to update name ref and var substitution 5 | nameReference: 6 | - kind: Issuer 7 | group: cert-manager.io 8 | fieldSpecs: 9 | - kind: Certificate 10 | group: cert-manager.io 11 | path: spec/issuerRef/name 12 | 13 | varReference: 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/commonName 17 | - kind: Certificate 18 | group: cert-manager.io 19 | path: spec/dnsNames 20 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | 7 | bases: 8 | - ../default 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml 10 | - webhook 11 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 12 | - certmanager 13 | 14 | patchesStrategicMerge: 15 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml 16 | - manager_webhook_patch.yaml 17 | 18 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 19 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 20 | # 'CERTMANAGER' needs to be enabled to use ca injection 21 | - webhookcainjection_patch.yaml 22 | 23 | # the following config is for teaching kustomize how to do var substitution 24 | vars: 25 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 26 | - name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 27 | objref: 28 | kind: Certificate 29 | group: cert-manager.io 30 | version: v1alpha2 31 | name: serving-cert # this name should match the one in certificate.yaml 32 | fieldref: 33 | fieldpath: metadata.namespace 34 | - name: CERTIFICATE_NAME 35 | objref: 36 | kind: Certificate 37 | group: cert-manager.io 38 | version: v1alpha2 39 | name: serving-cert # this name should match the one in certificate.yaml 40 | - name: SERVICE_NAMESPACE # namespace of the service 41 | objref: 42 | kind: Service 43 | version: v1 44 | name: webhook-service 45 | fieldref: 46 | fieldpath: metadata.namespace 47 | - name: SERVICE_NAME 48 | objref: 49 | kind: Service 50 | version: v1 51 | name: webhook-service 52 | -------------------------------------------------------------------------------- /config/webhook/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # This patch inject a sidecar container which is a HTTP proxy for the controller manager, 5 | # it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 6 | apiVersion: apps/v1 7 | kind: Deployment 8 | metadata: 9 | name: controller 10 | namespace: system 11 | spec: 12 | template: 13 | spec: 14 | containers: 15 | - name: kube-rbac-proxy 16 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.4.1 17 | args: 18 | - "--secure-listen-address=0.0.0.0:8443" 19 | - "--upstream=http://127.0.0.1:8080/" 20 | - "--logtostderr=true" 21 | - "--v=10" 22 | ports: 23 | - containerPort: 8443 24 | name: https 25 | - name: kube-app-manager 26 | args: 27 | - "--metrics-addr=127.0.0.1:8080" 28 | - "--enable-leader-election" 29 | -------------------------------------------------------------------------------- /config/webhook/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: apps/v1 5 | kind: Deployment 6 | metadata: 7 | name: controller 8 | namespace: system 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: kube-app-manager 14 | ports: 15 | - containerPort: 9443 16 | name: webhook-server 17 | protocol: TCP 18 | volumeMounts: 19 | - mountPath: /tmp/k8s-webhook-server/serving-certs 20 | name: cert 21 | readOnly: true 22 | volumes: 23 | - name: cert 24 | secret: 25 | defaultMode: 420 26 | secretName: webhook-server-cert 27 | -------------------------------------------------------------------------------- /config/webhook/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | resources: 5 | - manifests.yaml 6 | - service.yaml 7 | 8 | configurations: 9 | - kustomizeconfig.yaml 10 | -------------------------------------------------------------------------------- /config/webhook/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # the following config is for teaching kustomize where to look at when substituting vars. 5 | # It requires kustomize v2.1.0 or newer to work properly. 6 | nameReference: 7 | - kind: Service 8 | version: v1 9 | fieldSpecs: 10 | - kind: MutatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | - kind: ValidatingWebhookConfiguration 14 | group: admissionregistration.k8s.io 15 | path: webhooks/clientConfig/service/name 16 | 17 | namespace: 18 | - kind: MutatingWebhookConfiguration 19 | group: admissionregistration.k8s.io 20 | path: webhooks/clientConfig/service/namespace 21 | create: true 22 | - kind: ValidatingWebhookConfiguration 23 | group: admissionregistration.k8s.io 24 | path: webhooks/clientConfig/service/namespace 25 | create: true 26 | 27 | varReference: 28 | - path: metadata/annotations 29 | -------------------------------------------------------------------------------- /config/webhook/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | -------------------------------------------------------------------------------- /config/webhook/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | 5 | apiVersion: v1 6 | kind: Service 7 | metadata: 8 | name: webhook-service 9 | namespace: system 10 | spec: 11 | ports: 12 | - port: 443 13 | targetPort: 9443 14 | selector: 15 | control-plane: controller-manager 16 | -------------------------------------------------------------------------------- /config/webhook/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # This patch add annotation to admission webhook config and 5 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 6 | apiVersion: admissionregistration.k8s.io/v1beta1 7 | kind: MutatingWebhookConfiguration 8 | metadata: 9 | name: mutating-webhook-configuration 10 | annotations: 11 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 12 | --- 13 | apiVersion: admissionregistration.k8s.io/v1beta1 14 | kind: ValidatingWebhookConfiguration 15 | metadata: 16 | name: validating-webhook-configuration 17 | annotations: 18 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 19 | -------------------------------------------------------------------------------- /controllers/application_controller.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package controllers 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "github.com/go-logr/logr" 11 | "k8s.io/apimachinery/pkg/api/equality" 12 | apierrors "k8s.io/apimachinery/pkg/api/errors" 13 | "k8s.io/apimachinery/pkg/api/meta" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/apimachinery/pkg/runtime/schema" 18 | "k8s.io/apimachinery/pkg/types" 19 | utilerrors "k8s.io/apimachinery/pkg/util/errors" 20 | "k8s.io/client-go/util/retry" 21 | ctrl "sigs.k8s.io/controller-runtime" 22 | "sigs.k8s.io/controller-runtime/pkg/client" 23 | 24 | appv1beta1 "sigs.k8s.io/application/api/v1beta1" 25 | ) 26 | 27 | const ( 28 | loggerCtxKey = "logger" 29 | ) 30 | 31 | // ApplicationReconciler reconciles a Application object 32 | type ApplicationReconciler struct { 33 | client.Client 34 | Mapper meta.RESTMapper 35 | Log logr.Logger 36 | Scheme *runtime.Scheme 37 | } 38 | 39 | // +kubebuilder:rbac:groups=app.k8s.io,resources=applications,verbs=get;list;watch;create;update;patch;delete 40 | // +kubebuilder:rbac:groups=app.k8s.io,resources=applications/status,verbs=get;update;patch 41 | // +kubebuilder:rbac:groups=*,resources=*,verbs=list;get;update;patch;watch 42 | 43 | func (r *ApplicationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 44 | rootCtx := context.Background() 45 | logger := r.Log.WithValues("application", req.NamespacedName) 46 | ctx := context.WithValue(rootCtx, loggerCtxKey, logger) 47 | 48 | var app appv1beta1.Application 49 | err := r.Get(ctx, req.NamespacedName, &app) 50 | if err != nil { 51 | if apierrors.IsNotFound(err) { 52 | return ctrl.Result{}, nil 53 | } 54 | return ctrl.Result{}, err 55 | } 56 | 57 | // Application is in the process of being deleted, so no need to do anything. 58 | if app.DeletionTimestamp != nil { 59 | return ctrl.Result{}, nil 60 | } 61 | 62 | resources, errs := r.updateComponents(ctx, &app) 63 | newApplicationStatus := r.getNewApplicationStatus(ctx, &app, resources, &errs) 64 | 65 | newApplicationStatus.ObservedGeneration = app.Generation 66 | if equality.Semantic.DeepEqual(newApplicationStatus, &app.Status) { 67 | return ctrl.Result{}, nil 68 | } 69 | 70 | err = r.updateApplicationStatus(ctx, req.NamespacedName, newApplicationStatus) 71 | return ctrl.Result{}, err 72 | } 73 | 74 | func (r *ApplicationReconciler) updateComponents(ctx context.Context, app *appv1beta1.Application) ([]*unstructured.Unstructured, []error) { 75 | var errs []error 76 | resources := r.fetchComponentListResources(ctx, app.Spec.ComponentGroupKinds, app.Spec.Selector, app.Namespace, &errs) 77 | 78 | if app.Spec.AddOwnerRef { 79 | ownerRef := metav1.NewControllerRef(app, appv1beta1.GroupVersion.WithKind("Application")) 80 | *ownerRef.Controller = false 81 | if err := r.setOwnerRefForResources(ctx, *ownerRef, resources); err != nil { 82 | errs = append(errs, err) 83 | } 84 | } 85 | return resources, errs 86 | } 87 | 88 | func (r *ApplicationReconciler) getNewApplicationStatus(ctx context.Context, app *appv1beta1.Application, resources []*unstructured.Unstructured, errList *[]error) *appv1beta1.ApplicationStatus { 89 | objectStatuses := r.objectStatuses(ctx, resources, errList) 90 | errs := utilerrors.NewAggregate(*errList) 91 | 92 | aggReady, countReady := aggregateReady(objectStatuses) 93 | 94 | newApplicationStatus := app.Status.DeepCopy() 95 | newApplicationStatus.ComponentList = appv1beta1.ComponentList{ 96 | Objects: objectStatuses, 97 | } 98 | newApplicationStatus.ComponentsReady = fmt.Sprintf("%d/%d", countReady, len(objectStatuses)) 99 | if errs != nil { 100 | setReadyUnknownCondition(newApplicationStatus, "ComponentsReadyUnknown", "failed to aggregate all components' statuses, check the Error condition for details") 101 | } else if aggReady { 102 | setReadyCondition(newApplicationStatus, "ComponentsReady", "all components ready") 103 | } else { 104 | setNotReadyCondition(newApplicationStatus, "ComponentsNotReady", fmt.Sprintf("%d components not ready", len(objectStatuses)-countReady)) 105 | } 106 | 107 | if errs != nil { 108 | setErrorCondition(newApplicationStatus, "ErrorSeen", errs.Error()) 109 | } else { 110 | clearErrorCondition(newApplicationStatus) 111 | } 112 | 113 | return newApplicationStatus 114 | } 115 | 116 | func (r *ApplicationReconciler) fetchComponentListResources(ctx context.Context, groupKinds []metav1.GroupKind, selector *metav1.LabelSelector, namespace string, errs *[]error) []*unstructured.Unstructured { 117 | logger := getLoggerOrDie(ctx) 118 | var resources []*unstructured.Unstructured 119 | 120 | if selector == nil { 121 | logger.Info("No selector is specified") 122 | return resources 123 | } 124 | 125 | for _, gk := range groupKinds { 126 | mapping, err := r.Mapper.RESTMapping(schema.GroupKind{ 127 | Group: appv1beta1.StripVersion(gk.Group), 128 | Kind: gk.Kind, 129 | }) 130 | if err != nil { 131 | logger.Info("NoMappingForGK", "gk", gk.String()) 132 | continue 133 | } 134 | 135 | list := &unstructured.UnstructuredList{} 136 | list.SetGroupVersionKind(mapping.GroupVersionKind) 137 | if err = r.Client.List(ctx, list, client.InNamespace(namespace), client.MatchingLabels(selector.MatchLabels)); err != nil { 138 | logger.Error(err, "unable to list resources for GVK", "gvk", mapping.GroupVersionKind) 139 | *errs = append(*errs, err) 140 | continue 141 | } 142 | 143 | for _, u := range list.Items { 144 | resource := u 145 | resources = append(resources, &resource) 146 | } 147 | } 148 | return resources 149 | } 150 | 151 | func (r *ApplicationReconciler) setOwnerRefForResources(ctx context.Context, ownerRef metav1.OwnerReference, resources []*unstructured.Unstructured) error { 152 | logger := getLoggerOrDie(ctx) 153 | for _, resource := range resources { 154 | ownerRefs := resource.GetOwnerReferences() 155 | ownerRefFound := false 156 | for i, refs := range ownerRefs { 157 | if ownerRef.Kind == refs.Kind && 158 | ownerRef.APIVersion == refs.APIVersion && 159 | ownerRef.Name == refs.Name { 160 | ownerRefFound = true 161 | if ownerRef.UID != refs.UID { 162 | ownerRefs[i] = ownerRef 163 | } 164 | } 165 | } 166 | 167 | if !ownerRefFound { 168 | ownerRefs = append(ownerRefs, ownerRef) 169 | } 170 | resource.SetOwnerReferences(ownerRefs) 171 | err := r.Client.Update(ctx, resource) 172 | if err != nil { 173 | // We log this error, but we continue and try to set the ownerRefs on the other resources. 174 | logger.Error(err, "ErrorSettingOwnerRef", "gvk", resource.GroupVersionKind().String(), 175 | "namespace", resource.GetNamespace(), "name", resource.GetName()) 176 | } 177 | } 178 | return nil 179 | } 180 | 181 | func (r *ApplicationReconciler) objectStatuses(ctx context.Context, resources []*unstructured.Unstructured, errs *[]error) []appv1beta1.ObjectStatus { 182 | logger := getLoggerOrDie(ctx) 183 | var objectStatuses []appv1beta1.ObjectStatus 184 | for _, resource := range resources { 185 | os := appv1beta1.ObjectStatus{ 186 | Group: resource.GroupVersionKind().Group, 187 | Kind: resource.GetKind(), 188 | Name: resource.GetName(), 189 | Link: resource.GetSelfLink(), 190 | } 191 | s, err := status(resource) 192 | if err != nil { 193 | logger.Error(err, "unable to compute status for resource", "gvk", resource.GroupVersionKind().String(), 194 | "namespace", resource.GetNamespace(), "name", resource.GetName()) 195 | *errs = append(*errs, err) 196 | } 197 | os.Status = s 198 | objectStatuses = append(objectStatuses, os) 199 | } 200 | return objectStatuses 201 | } 202 | 203 | func aggregateReady(objectStatuses []appv1beta1.ObjectStatus) (bool, int) { 204 | countReady := 0 205 | for _, os := range objectStatuses { 206 | if os.Status == StatusReady { 207 | countReady++ 208 | } 209 | } 210 | if countReady == len(objectStatuses) { 211 | return true, countReady 212 | } 213 | return false, countReady 214 | } 215 | 216 | func (r *ApplicationReconciler) updateApplicationStatus(ctx context.Context, nn types.NamespacedName, status *appv1beta1.ApplicationStatus) error { 217 | if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { 218 | original := &appv1beta1.Application{} 219 | if err := r.Get(ctx, nn, original); err != nil { 220 | return err 221 | } 222 | original.Status = *status 223 | if err := r.Client.Status().Update(ctx, original); err != nil { 224 | return err 225 | } 226 | return nil 227 | }); err != nil { 228 | return fmt.Errorf("failed to update status of Application %s/%s: %v", nn.Namespace, nn.Name, err) 229 | } 230 | return nil 231 | } 232 | 233 | func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error { 234 | return ctrl.NewControllerManagedBy(mgr). 235 | For(&appv1beta1.Application{}). 236 | Complete(r) 237 | } 238 | 239 | func getLoggerOrDie(ctx context.Context) logr.Logger { 240 | logger, ok := ctx.Value(loggerCtxKey).(logr.Logger) 241 | if !ok { 242 | panic("context didn't contain logger") 243 | } 244 | return logger 245 | } 246 | -------------------------------------------------------------------------------- /controllers/application_controller_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package controllers 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "sync" 10 | "time" 11 | 12 | "github.com/google/uuid" 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | apps "k8s.io/api/apps/v1" 16 | core "k8s.io/api/core/v1" 17 | policy "k8s.io/api/policy/v1beta1" 18 | apierrors "k8s.io/apimachinery/pkg/api/errors" 19 | "k8s.io/apimachinery/pkg/api/resource" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "k8s.io/apimachinery/pkg/types" 25 | "k8s.io/apimachinery/pkg/util/intstr" 26 | "k8s.io/apimachinery/pkg/util/wait" 27 | appv1beta1 "sigs.k8s.io/application/api/v1beta1" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | "sigs.k8s.io/controller-runtime/pkg/manager" 30 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 31 | ) 32 | 33 | var c client.Client 34 | 35 | const timeout = time.Second * 30 36 | 37 | var _ = Describe("Application Reconciler", func() { 38 | var stopMgr chan struct{} 39 | var mgrStopped *sync.WaitGroup 40 | var recFn reconcile.Reconciler 41 | var requests chan reconcile.Request 42 | var ctx context.Context 43 | var applicationReconciler *ApplicationReconciler 44 | var labelSet1 = map[string]string{"foo": "bar"} 45 | var labelSet2 = map[string]string{"baz": "qux"} 46 | var namespace1 = metav1.NamespaceDefault 47 | var namespace2 = "default2" 48 | var deployment *apps.Deployment 49 | var statefulSet *apps.StatefulSet 50 | var service *core.Service 51 | 52 | BeforeEach(func() { 53 | // Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a 54 | // channel when it is finished. 55 | mgr, err := manager.New(cfg, manager.Options{}) 56 | Expect(err).NotTo(HaveOccurred()) 57 | c = mgr.GetClient() 58 | 59 | applicationReconciler = NewReconciler(mgr) 60 | logger := applicationReconciler.Log.WithValues("application", metav1.NamespaceDefault+"/application") 61 | ctx = context.WithValue(context.Background(), loggerCtxKey, logger) 62 | recFn, requests = SetupTestReconcile(applicationReconciler) 63 | Expect(CreateController("app", mgr, recFn)).NotTo(HaveOccurred()) 64 | 65 | stopMgr, mgrStopped = StartTestManager(mgr) 66 | }) 67 | 68 | AfterEach(func() { 69 | close(stopMgr) 70 | mgrStopped.Wait() 71 | }) 72 | 73 | Describe("fetchComponentListResources", func() { 74 | It("should fetch corresponding components with matched labels within a namespace", func() { 75 | var objs []runtime.Object = nil 76 | createNamespace(namespace2, ctx) 77 | deployment = createDeployment(labelSet1, namespace1) 78 | service = createService(labelSet1, namespace1) 79 | statefulSet = createStatefulSet(labelSet1, namespace1) 80 | objs = append(objs, deployment) 81 | objs = append(objs, service) 82 | objs = append(objs, statefulSet) 83 | objs = append(objs, createPod(labelSet2, namespace2)) 84 | objs = append(objs, createDaemonSet(labelSet1, namespace2)) 85 | objs = append(objs, createReplicaSet(labelSet1, namespace2)) 86 | objs = append(objs, CreatePersistentVolumeClaim(labelSet2, namespace2)) 87 | objs = append(objs, createPodDisruptionBudget(labelSet2, namespace2)) 88 | 89 | for _, obj := range objs { 90 | err := c.Create(ctx, obj) 91 | Expect(err).NotTo(HaveOccurred()) 92 | } 93 | 94 | groupKinds := []metav1.GroupKind{ 95 | { 96 | Group: "apps", 97 | Kind: "StatefulSet", 98 | }, 99 | { 100 | Group: "apps", 101 | Kind: "Deployment", 102 | }, 103 | { 104 | Group: "apps", 105 | Kind: "ReplicaSet", 106 | }, 107 | { 108 | Group: "apps", 109 | Kind: "DaemonSet", 110 | }, 111 | { 112 | Group: "batch", 113 | Kind: "Job", 114 | }, 115 | { 116 | Group: "v1", 117 | Kind: "Service", 118 | }, 119 | { 120 | Group: "v1", 121 | Kind: "PersistentVolumeClaim", 122 | }, 123 | { 124 | Group: "v1", 125 | Kind: "Pod", 126 | }, 127 | { 128 | Group: "policy", 129 | Kind: "PodDisruptionBudget", 130 | }, 131 | } 132 | 133 | var errs []error 134 | ns1List := applicationReconciler.fetchComponentListResources(ctx, groupKinds, metav1.SetAsLabelSelector(labelSet1), namespace1, &errs) 135 | Expect(errs).To(BeNil()) 136 | Expect(len(ns1List)).To(Equal(3)) 137 | Expect(componentKinds(ns1List)).To(ConsistOf("StatefulSet", "Deployment", "Service")) 138 | 139 | ns2l1List := applicationReconciler.fetchComponentListResources(ctx, groupKinds, metav1.SetAsLabelSelector(labelSet1), namespace2, &errs) 140 | Expect(errs).To(BeNil()) 141 | Expect(len(ns2l1List)).To(Equal(2)) 142 | Expect(componentKinds(ns2l1List)).To(ConsistOf("ReplicaSet", "DaemonSet")) 143 | 144 | ns2l2List := applicationReconciler.fetchComponentListResources(ctx, groupKinds, metav1.SetAsLabelSelector(labelSet2), namespace2, &errs) 145 | Expect(errs).To(BeNil()) 146 | Expect(len(ns2l2List)).To(Equal(3)) 147 | Expect(componentKinds(ns2l2List)).To(ConsistOf("PersistentVolumeClaim", "Pod", "PodDisruptionBudget")) 148 | 149 | // Empty selector will select ALL resources in the namespace 150 | ns2AllList := applicationReconciler.fetchComponentListResources(ctx, groupKinds, metav1.SetAsLabelSelector(map[string]string{}), namespace2, &errs) 151 | Expect(errs).To(BeNil()) 152 | Expect(len(ns2AllList)).To(Equal(5)) 153 | Expect(componentKinds(ns2AllList)).To(ConsistOf("ReplicaSet", "DaemonSet", "PersistentVolumeClaim", "Pod", "PodDisruptionBudget")) 154 | 155 | // No selector will select NO resources in the namespace 156 | ns2NoList := applicationReconciler.fetchComponentListResources(ctx, groupKinds, nil, namespace2, &errs) 157 | Expect(errs).To(BeNil()) 158 | Expect(ns2NoList).To(BeNil()) 159 | 160 | }) 161 | 162 | It("should fetch components when version is included in the group", func() { 163 | groupKinds := []metav1.GroupKind{ 164 | { 165 | Group: "apps/v1", 166 | Kind: "Deployment", 167 | }, 168 | { 169 | Group: "/v1", 170 | Kind: "Service", 171 | }, 172 | } 173 | 174 | var errs []error 175 | ns1List := applicationReconciler.fetchComponentListResources(ctx, groupKinds, metav1.SetAsLabelSelector(labelSet1), metav1.NamespaceDefault, &errs) 176 | Expect(errs).To(BeNil()) 177 | Expect(len(ns1List)).To(Equal(2)) 178 | Expect(componentKinds(ns1List)).To(ConsistOf("Deployment", "Service")) 179 | 180 | }) 181 | }) 182 | 183 | Describe("setOwnerRefForResources", func() { 184 | var resource = &unstructured.Unstructured{} 185 | resource.SetGroupVersionKind(schema.GroupVersionKind{ 186 | Group: "apps", 187 | Version: "v1", 188 | Kind: "StatefulSet", 189 | }) 190 | var key types.NamespacedName 191 | var resources []*unstructured.Unstructured 192 | var uid types.UID = "old-uid" 193 | var newUID types.UID = "new-uid" 194 | var ownerRef = metav1.OwnerReference{ 195 | APIVersion: "app.k8s.io/v1beta1", 196 | Kind: "Application", 197 | Name: "application-foo", 198 | UID: uid, 199 | } 200 | 201 | It("should append new ownerReference to the resources", func() { 202 | key = types.NamespacedName{ 203 | Name: statefulSet.Name, 204 | Namespace: metav1.NamespaceDefault, 205 | } 206 | resources = append(resources, resource) 207 | 208 | err := c.Get(ctx, key, resource) 209 | Expect(err).NotTo(HaveOccurred()) 210 | Expect(resource.GetOwnerReferences()).To(BeEmpty()) 211 | 212 | err = applicationReconciler.setOwnerRefForResources(ctx, ownerRef, resources) 213 | Expect(err).NotTo(HaveOccurred()) 214 | err = c.Get(ctx, key, resource) 215 | Expect(err).NotTo(HaveOccurred()) 216 | Expect(resource.GetOwnerReferences()).To(HaveLen(1)) 217 | Expect(resource.GetOwnerReferences()).To(ContainElement(ownerRef)) 218 | }) 219 | 220 | It("should update existing ownerReference with new UID", func() { 221 | err := c.Get(ctx, key, resource) 222 | Expect(err).NotTo(HaveOccurred()) 223 | Expect(resource.GetOwnerReferences()).To(HaveLen(1)) 224 | Expect(resource.GetOwnerReferences()[0].UID).To(Equal(uid)) 225 | 226 | ownerRef.UID = newUID 227 | err = applicationReconciler.setOwnerRefForResources(ctx, ownerRef, resources) 228 | Expect(err).NotTo(HaveOccurred()) 229 | err = c.Get(ctx, key, resource) 230 | Expect(err).NotTo(HaveOccurred()) 231 | Expect(resource.GetOwnerReferences()).To(HaveLen(1)) 232 | Expect(resource.GetOwnerReferences()[0].UID).To(Equal(newUID)) 233 | }) 234 | 235 | It("should NOT update identical ownerReference", func() { 236 | err := c.Get(ctx, key, resource) 237 | Expect(err).NotTo(HaveOccurred()) 238 | Expect(resource.GetOwnerReferences()).To(HaveLen(1)) 239 | Expect(resource.GetOwnerReferences()[0].UID).To(Equal(newUID)) 240 | 241 | err = applicationReconciler.setOwnerRefForResources(ctx, ownerRef, resources) 242 | Expect(err).NotTo(HaveOccurred()) 243 | err = c.Get(ctx, key, resource) 244 | Expect(err).NotTo(HaveOccurred()) 245 | Expect(resource.GetOwnerReferences()).To(HaveLen(1)) 246 | Expect(resource.GetOwnerReferences()[0].UID).To(Equal(newUID)) 247 | }) 248 | }) 249 | 250 | Describe("Application Reconciler", func() { 251 | 252 | It("should receive a request when an application instance is created", func() { 253 | instance := &appv1beta1.Application{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}, Spec: appv1beta1.ApplicationSpec{}} 254 | 255 | // Create the Application object and expect the Reconcile and Deployment to be created 256 | err := c.Create(ctx, instance) 257 | // The instance object may not be a valid object because it might be missing some required fields. 258 | // Please modify the instance object by adding required fields and then remove the following if statement. 259 | if apierrors.IsInvalid(err) { 260 | fmt.Printf("failed to create object, got an invalid object error: %v\n", err) 261 | return 262 | } 263 | Expect(err).NotTo(HaveOccurred()) 264 | defer func() { 265 | _ = c.Delete(ctx, instance) 266 | }() 267 | var expectedRequest = reconcile.Request{NamespacedName: types.NamespacedName{Name: "foo", Namespace: "default"}} 268 | Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) 269 | }) 270 | 271 | It("should update the application status, as well as the components' ownerReference", func() { 272 | application := &appv1beta1.Application{ 273 | ObjectMeta: metav1.ObjectMeta{ 274 | Name: "application-01", 275 | Namespace: metav1.NamespaceDefault, 276 | Labels: labelSet1, 277 | }, 278 | Spec: appv1beta1.ApplicationSpec{ 279 | Selector: &metav1.LabelSelector{MatchLabels: labelSet1}, 280 | ComponentGroupKinds: []metav1.GroupKind{ 281 | { 282 | Group: "apps", 283 | Kind: "Deployment", 284 | }, 285 | { 286 | Group: "v1", 287 | Kind: "Service", 288 | }, 289 | }, 290 | AddOwnerRef: true, 291 | }} 292 | 293 | Expect(deployment.OwnerReferences).To(BeNil()) 294 | Expect(service.OwnerReferences).To(BeNil()) 295 | 296 | err := c.Create(ctx, application) 297 | Expect(err).NotTo(HaveOccurred()) 298 | waitForComponentsAddedToStatus(ctx, application, deployment.Name, service.Name) 299 | 300 | _ = wait.PollImmediate(time.Second, timeout, func() (bool, error) { 301 | fetchUpdatedDeployment(ctx, deployment) 302 | fetchUpdatedService(ctx, service) 303 | if len(deployment.OwnerReferences) == 1 && len(service.OwnerReferences) == 1 { 304 | return true, nil 305 | } 306 | return false, nil 307 | }) 308 | 309 | Expect(deployment.OwnerReferences[0].Name).To(Equal(application.Name)) 310 | Expect(service.OwnerReferences[0].Name).To(Equal(application.Name)) 311 | 312 | }) 313 | }) 314 | 315 | }) 316 | 317 | func fetchUpdatedDeployment(ctx context.Context, deployment *apps.Deployment) { 318 | key := types.NamespacedName{ 319 | Name: deployment.Name, 320 | Namespace: deployment.Namespace, 321 | } 322 | err := c.Get(ctx, key, deployment) 323 | Expect(err).NotTo(HaveOccurred()) 324 | } 325 | 326 | func fetchUpdatedService(ctx context.Context, service *core.Service) { 327 | key := types.NamespacedName{ 328 | Name: service.Name, 329 | Namespace: service.Namespace, 330 | } 331 | err := c.Get(ctx, key, service) 332 | Expect(err).NotTo(HaveOccurred()) 333 | } 334 | 335 | func waitForComponentsAddedToStatus(ctx context.Context, app *appv1beta1.Application, expectedNames ...string) { 336 | key := types.NamespacedName{ 337 | Name: app.Name, 338 | Namespace: app.Namespace, 339 | } 340 | _ = wait.PollImmediate(time.Second, timeout, func() (bool, error) { 341 | names, err := applicationStatusComponentNames(ctx, app, key) 342 | if err != nil { 343 | return false, err 344 | } 345 | if len(names) < len(expectedNames) { 346 | return false, nil 347 | } 348 | Expect(names).Should(ConsistOf(expectedNames)) 349 | return true, nil 350 | }) 351 | } 352 | 353 | func applicationStatusComponentNames(ctx context.Context, app *appv1beta1.Application, key types.NamespacedName) ([]string, error) { 354 | var names = make([]string, 0) 355 | if err := c.Get(ctx, key, app); err != nil { 356 | return names, err 357 | } 358 | Expect(app.Status.ComponentList).NotTo(BeNil()) 359 | for _, component := range app.Status.ComponentList.Objects { 360 | names = append(names, component.Name) 361 | } 362 | return names, nil 363 | } 364 | 365 | func componentKinds(list []*unstructured.Unstructured) []string { 366 | var kinds []string 367 | for _, l := range list { 368 | kinds = append(kinds, l.GetKind()) 369 | } 370 | return kinds 371 | } 372 | 373 | func objectMeta(t string, labels map[string]string, ns string) metav1.ObjectMeta { 374 | return metav1.ObjectMeta{ 375 | Name: fmt.Sprintf("%s-%s", t, uuid.New()), 376 | Namespace: ns, 377 | Labels: labels, 378 | } 379 | } 380 | 381 | func podTemplateSpec(labels map[string]string, ns string) core.PodTemplateSpec { 382 | return core.PodTemplateSpec{ 383 | ObjectMeta: objectMeta("pod-template", labels, ns), 384 | Spec: core.PodSpec{ 385 | RestartPolicy: core.RestartPolicyAlways, 386 | DNSPolicy: core.DNSClusterFirst, 387 | Containers: []core.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, 388 | }, 389 | } 390 | } 391 | 392 | func createStatefulSet(labels map[string]string, ns string) *apps.StatefulSet { 393 | podLabels := map[string]string{"xxx": "yyy"} 394 | 395 | return &apps.StatefulSet{ 396 | ObjectMeta: objectMeta("statefulset", labels, ns), 397 | Spec: apps.StatefulSetSpec{ 398 | PodManagementPolicy: apps.OrderedReadyPodManagement, 399 | Selector: &metav1.LabelSelector{MatchLabels: podLabels}, 400 | Template: podTemplateSpec(podLabels, ns), 401 | UpdateStrategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType}, 402 | }, 403 | } 404 | } 405 | 406 | func createNamespace(name string, ctx context.Context) { 407 | namespace := &core.Namespace{ 408 | ObjectMeta: metav1.ObjectMeta{ 409 | Name: name, 410 | }, 411 | } 412 | err := c.Create(ctx, namespace) 413 | Expect(err).NotTo(HaveOccurred()) 414 | } 415 | 416 | func createDeployment(labels map[string]string, ns string) *apps.Deployment { 417 | podLabels := map[string]string{"xxx": "yyy"} 418 | return &apps.Deployment{ 419 | ObjectMeta: objectMeta("deployment", labels, ns), 420 | Spec: apps.DeploymentSpec{ 421 | Selector: &metav1.LabelSelector{ 422 | MatchLabels: podLabels, 423 | }, 424 | Template: podTemplateSpec(podLabels, ns), 425 | }, 426 | } 427 | } 428 | 429 | func createDaemonSet(labels map[string]string, ns string) *apps.DaemonSet { 430 | return &apps.DaemonSet{ 431 | ObjectMeta: objectMeta("daemonset", labels, ns), 432 | Spec: apps.DaemonSetSpec{ 433 | Selector: &metav1.LabelSelector{MatchLabels: labels}, 434 | Template: podTemplateSpec(labels, ns), 435 | UpdateStrategy: apps.DaemonSetUpdateStrategy{ 436 | Type: apps.OnDeleteDaemonSetStrategyType, 437 | }, 438 | }, 439 | } 440 | } 441 | 442 | func createReplicaSet(labels map[string]string, ns string) *apps.ReplicaSet { 443 | return &apps.ReplicaSet{ 444 | ObjectMeta: objectMeta("replicaset", labels, ns), 445 | Spec: apps.ReplicaSetSpec{ 446 | Selector: &metav1.LabelSelector{MatchLabels: labels}, 447 | Template: podTemplateSpec(labels, ns), 448 | }, 449 | } 450 | } 451 | 452 | func CreatePersistentVolumeClaim(labels map[string]string, ns string) *core.PersistentVolumeClaim { 453 | return &core.PersistentVolumeClaim{ 454 | ObjectMeta: objectMeta("pvc", labels, ns), 455 | Spec: core.PersistentVolumeClaimSpec{ 456 | Selector: &metav1.LabelSelector{ 457 | MatchExpressions: []metav1.LabelSelectorRequirement{ 458 | { 459 | Key: "key2", 460 | Operator: "Exists", 461 | }, 462 | }, 463 | }, 464 | AccessModes: []core.PersistentVolumeAccessMode{ 465 | core.ReadWriteOnce, 466 | core.ReadOnlyMany, 467 | }, 468 | Resources: core.ResourceRequirements{ 469 | Requests: core.ResourceList{ 470 | core.ResourceStorage: resource.MustParse("10G"), 471 | }, 472 | }, 473 | }, 474 | } 475 | } 476 | 477 | func createPod(labels map[string]string, ns string) *core.Pod { 478 | return &core.Pod{ 479 | ObjectMeta: objectMeta("pod", labels, ns), 480 | Spec: core.PodSpec{ 481 | Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 482 | Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 483 | RestartPolicy: core.RestartPolicyAlways, 484 | DNSPolicy: core.DNSClusterFirst, 485 | }, 486 | } 487 | } 488 | 489 | func createPodDisruptionBudget(labels map[string]string, ns string) *policy.PodDisruptionBudget { 490 | maxUnavailable := intstr.FromString("10%") 491 | return &policy.PodDisruptionBudget{ 492 | ObjectMeta: objectMeta("pdb", labels, ns), 493 | Spec: policy.PodDisruptionBudgetSpec{ 494 | MaxUnavailable: &maxUnavailable, 495 | }, 496 | } 497 | } 498 | 499 | func createService(labels map[string]string, ns string) *core.Service { 500 | serviceIPFamily := core.IPv4Protocol 501 | return &core.Service{ 502 | ObjectMeta: objectMeta("service", labels, ns), 503 | Spec: core.ServiceSpec{ 504 | SessionAffinity: "None", 505 | Type: core.ServiceTypeClusterIP, 506 | Ports: []core.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675, TargetPort: intstr.FromInt(8675)}}, 507 | IPFamily: &serviceIPFamily, 508 | }, 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /controllers/condition.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package controllers 5 | 6 | import ( 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | appv1beta1 "sigs.k8s.io/application/api/v1beta1" 10 | ) 11 | 12 | func setReadyCondition(appStatus *appv1beta1.ApplicationStatus, reason, message string) { 13 | setCondition(appStatus, appv1beta1.Ready, corev1.ConditionTrue, reason, message) 14 | } 15 | 16 | // NotReady - shortcut to set ready condition to false 17 | func setNotReadyCondition(appStatus *appv1beta1.ApplicationStatus, reason, message string) { 18 | setCondition(appStatus, appv1beta1.Ready, corev1.ConditionFalse, reason, message) 19 | } 20 | 21 | // Unknown - shortcut to set ready condition to unknown 22 | func setReadyUnknownCondition(appStatus *appv1beta1.ApplicationStatus, reason, message string) { 23 | setCondition(appStatus, appv1beta1.Ready, corev1.ConditionUnknown, reason, message) 24 | } 25 | 26 | // setErrorCondition - shortcut to set error condition 27 | func setErrorCondition(appStatus *appv1beta1.ApplicationStatus, reason, message string) { 28 | setCondition(appStatus, appv1beta1.Error, corev1.ConditionTrue, reason, message) 29 | } 30 | 31 | // clearErrorCondition - shortcut to set error condition 32 | func clearErrorCondition(appStatus *appv1beta1.ApplicationStatus) { 33 | setCondition(appStatus, appv1beta1.Error, corev1.ConditionFalse, "NoError", "No error seen") 34 | } 35 | 36 | func setCondition(appStatus *appv1beta1.ApplicationStatus, ctype appv1beta1.ConditionType, status corev1.ConditionStatus, reason, message string) { 37 | var c *appv1beta1.Condition 38 | for i := range appStatus.Conditions { 39 | if appStatus.Conditions[i].Type == ctype { 40 | c = &appStatus.Conditions[i] 41 | } 42 | } 43 | if c == nil { 44 | addCondition(appStatus, ctype, status, reason, message) 45 | } else { 46 | // check message ? 47 | if c.Status == status && c.Reason == reason && c.Message == message { 48 | return 49 | } 50 | now := metav1.Now() 51 | c.LastUpdateTime = now 52 | if c.Status != status { 53 | c.LastTransitionTime = now 54 | } 55 | c.Status = status 56 | c.Reason = reason 57 | c.Message = message 58 | } 59 | } 60 | 61 | func addCondition(appStatus *appv1beta1.ApplicationStatus, ctype appv1beta1.ConditionType, status corev1.ConditionStatus, reason, message string) { 62 | now := metav1.Now() 63 | c := appv1beta1.Condition{ 64 | Type: ctype, 65 | LastUpdateTime: now, 66 | LastTransitionTime: now, 67 | Status: status, 68 | Reason: reason, 69 | Message: message, 70 | } 71 | appStatus.Conditions = append(appStatus.Conditions, c) 72 | } 73 | -------------------------------------------------------------------------------- /controllers/status.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package controllers 5 | 6 | import ( 7 | "strings" 8 | 9 | appsv1 "k8s.io/api/apps/v1" 10 | batchv1 "k8s.io/api/batch/v1" 11 | corev1 "k8s.io/api/core/v1" 12 | policyv1beta1 "k8s.io/api/policy/v1beta1" 13 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | ) 16 | 17 | // Constants defining labels 18 | const ( 19 | StatusReady = "Ready" 20 | StatusInProgress = "InProgress" 21 | StatusUnknown = "Unknown" 22 | StatusDisabled = "Disabled" 23 | ) 24 | 25 | func status(u *unstructured.Unstructured) (string, error) { 26 | gk := u.GroupVersionKind().GroupKind() 27 | switch gk.String() { 28 | case "StatefulSet.apps": 29 | return stsStatus(u) 30 | case "Deployment.apps": 31 | return deploymentStatus(u) 32 | case "ReplicaSet.apps": 33 | return replicasetStatus(u) 34 | case "DaemonSet.apps": 35 | return daemonsetStatus(u) 36 | case "PersistentVolumeClaim": 37 | return pvcStatus(u) 38 | case "Service": 39 | return serviceStatus(u) 40 | case "Pod": 41 | return podStatus(u) 42 | case "PodDisruptionBudget.policy": 43 | return pdbStatus(u) 44 | case "ReplicationController": 45 | return replicationControllerStatus(u) 46 | case "Job.batch": 47 | return jobStatus(u) 48 | default: 49 | return statusFromStandardConditions(u) 50 | } 51 | } 52 | 53 | // Status from standard conditions 54 | func statusFromStandardConditions(u *unstructured.Unstructured) (string, error) { 55 | condition := StatusReady 56 | 57 | // Check Ready condition 58 | _, cs, found, err := getConditionOfType(u, StatusReady) 59 | if err != nil { 60 | return StatusUnknown, err 61 | } 62 | if found && cs == corev1.ConditionFalse { 63 | condition = StatusInProgress 64 | } 65 | 66 | // Check InProgress condition 67 | _, cs, found, err = getConditionOfType(u, StatusInProgress) 68 | if err != nil { 69 | return StatusUnknown, err 70 | } 71 | if found && cs == corev1.ConditionTrue { 72 | condition = StatusInProgress 73 | } 74 | 75 | return condition, nil 76 | } 77 | 78 | // Statefulset 79 | func stsStatus(u *unstructured.Unstructured) (string, error) { 80 | sts := &appsv1.StatefulSet{} 81 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, sts); err != nil { 82 | return StatusUnknown, err 83 | } 84 | 85 | if sts.Status.ObservedGeneration == sts.Generation && 86 | sts.Status.Replicas == *sts.Spec.Replicas && 87 | sts.Status.ReadyReplicas == *sts.Spec.Replicas && 88 | sts.Status.CurrentReplicas == *sts.Spec.Replicas { 89 | return StatusReady, nil 90 | } 91 | return StatusInProgress, nil 92 | } 93 | 94 | // Deployment 95 | func deploymentStatus(u *unstructured.Unstructured) (string, error) { 96 | deployment := &appsv1.Deployment{} 97 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, deployment); err != nil { 98 | return StatusUnknown, err 99 | } 100 | 101 | replicaFailure := false 102 | progressing := false 103 | available := false 104 | 105 | for _, condition := range deployment.Status.Conditions { 106 | switch condition.Type { 107 | case appsv1.DeploymentProgressing: 108 | if condition.Status == corev1.ConditionTrue && condition.Reason == "NewReplicaSetAvailable" { 109 | progressing = true 110 | } 111 | case appsv1.DeploymentAvailable: 112 | if condition.Status == corev1.ConditionTrue { 113 | available = true 114 | } 115 | case appsv1.DeploymentReplicaFailure: 116 | if condition.Status == corev1.ConditionTrue { 117 | replicaFailure = true 118 | break 119 | } 120 | } 121 | } 122 | 123 | if deployment.Status.ObservedGeneration == deployment.Generation && 124 | deployment.Status.Replicas == *deployment.Spec.Replicas && 125 | deployment.Status.ReadyReplicas == *deployment.Spec.Replicas && 126 | deployment.Status.AvailableReplicas == *deployment.Spec.Replicas && 127 | deployment.Status.Conditions != nil && len(deployment.Status.Conditions) > 0 && 128 | (progressing || available) && !replicaFailure { 129 | return StatusReady, nil 130 | } 131 | return StatusInProgress, nil 132 | } 133 | 134 | // Replicaset 135 | func replicasetStatus(u *unstructured.Unstructured) (string, error) { 136 | rs := &appsv1.ReplicaSet{} 137 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, rs); err != nil { 138 | return StatusUnknown, err 139 | } 140 | 141 | replicaFailure := false 142 | for _, condition := range rs.Status.Conditions { 143 | switch condition.Type { 144 | case appsv1.ReplicaSetReplicaFailure: 145 | if condition.Status == corev1.ConditionTrue { 146 | replicaFailure = true 147 | break 148 | } 149 | } 150 | } 151 | if rs.Status.ObservedGeneration == rs.Generation && 152 | rs.Status.Replicas == *rs.Spec.Replicas && 153 | rs.Status.ReadyReplicas == *rs.Spec.Replicas && 154 | rs.Status.AvailableReplicas == *rs.Spec.Replicas && !replicaFailure { 155 | return StatusReady, nil 156 | } 157 | return StatusInProgress, nil 158 | } 159 | 160 | // Daemonset 161 | func daemonsetStatus(u *unstructured.Unstructured) (string, error) { 162 | ds := &appsv1.DaemonSet{} 163 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, ds); err != nil { 164 | return StatusUnknown, err 165 | } 166 | 167 | if ds.Status.ObservedGeneration == ds.Generation && 168 | ds.Status.DesiredNumberScheduled == ds.Status.NumberAvailable && 169 | ds.Status.DesiredNumberScheduled == ds.Status.NumberReady { 170 | return StatusReady, nil 171 | } 172 | return StatusInProgress, nil 173 | } 174 | 175 | // PVC 176 | func pvcStatus(u *unstructured.Unstructured) (string, error) { 177 | pvc := &corev1.PersistentVolumeClaim{} 178 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, pvc); err != nil { 179 | return StatusUnknown, err 180 | } 181 | 182 | if pvc.Status.Phase == corev1.ClaimBound { 183 | return StatusReady, nil 184 | } 185 | return StatusInProgress, nil 186 | } 187 | 188 | // Service 189 | func serviceStatus(u *unstructured.Unstructured) (string, error) { 190 | service := &corev1.Service{} 191 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, service); err != nil { 192 | return StatusUnknown, err 193 | } 194 | stype := service.Spec.Type 195 | 196 | if stype == corev1.ServiceTypeClusterIP || stype == corev1.ServiceTypeNodePort || stype == corev1.ServiceTypeExternalName || 197 | stype == corev1.ServiceTypeLoadBalancer && !isEmpty(service.Spec.ClusterIP) && 198 | len(service.Status.LoadBalancer.Ingress) > 0 && !hasEmptyIngressIP(service.Status.LoadBalancer.Ingress) { 199 | return StatusReady, nil 200 | } 201 | return StatusInProgress, nil 202 | } 203 | 204 | // Pod 205 | func podStatus(u *unstructured.Unstructured) (string, error) { 206 | pod := &corev1.Pod{} 207 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, pod); err != nil { 208 | return StatusUnknown, err 209 | } 210 | 211 | for _, condition := range pod.Status.Conditions { 212 | if condition.Type == corev1.PodReady && (condition.Reason == "PodCompleted" || condition.Status == corev1.ConditionTrue) { 213 | return StatusReady, nil 214 | } 215 | } 216 | return StatusInProgress, nil 217 | } 218 | 219 | // PodDisruptionBudget 220 | func pdbStatus(u *unstructured.Unstructured) (string, error) { 221 | pdb := &policyv1beta1.PodDisruptionBudget{} 222 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, pdb); err != nil { 223 | return StatusUnknown, err 224 | } 225 | 226 | if pdb.Status.ObservedGeneration == pdb.Generation && 227 | pdb.Status.CurrentHealthy >= pdb.Status.DesiredHealthy { 228 | return StatusReady, nil 229 | } 230 | return StatusInProgress, nil 231 | } 232 | 233 | func replicationControllerStatus(u *unstructured.Unstructured) (string, error) { 234 | rc := &corev1.ReplicationController{} 235 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, rc); err != nil { 236 | return StatusUnknown, err 237 | } 238 | 239 | if rc.Status.ObservedGeneration == rc.Generation && 240 | rc.Status.Replicas == *rc.Spec.Replicas && 241 | rc.Status.ReadyReplicas == *rc.Spec.Replicas && 242 | rc.Status.AvailableReplicas == *rc.Spec.Replicas { 243 | return StatusReady, nil 244 | } 245 | return StatusInProgress, nil 246 | } 247 | 248 | func jobStatus(u *unstructured.Unstructured) (string, error) { 249 | job := &batchv1.Job{} 250 | 251 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, job); err != nil { 252 | return StatusUnknown, err 253 | } 254 | 255 | if job.Status.StartTime == nil { 256 | return StatusInProgress, nil 257 | } 258 | 259 | return StatusReady, nil 260 | } 261 | 262 | func hasEmptyIngressIP(ingress []corev1.LoadBalancerIngress) bool { 263 | for _, i := range ingress { 264 | if isEmpty(i.IP) { 265 | return true 266 | } 267 | } 268 | return false 269 | } 270 | 271 | func isEmpty(s string) bool { 272 | return len(strings.TrimSpace(s)) == 0 273 | } 274 | 275 | func getConditionOfType(u *unstructured.Unstructured, conditionType string) (string, corev1.ConditionStatus, bool, error) { 276 | conditions, found, err := unstructured.NestedSlice(u.Object, "status", "conditions") 277 | if err != nil || !found { 278 | return "", corev1.ConditionFalse, false, err 279 | } 280 | 281 | for _, c := range conditions { 282 | condition, ok := c.(map[string]interface{}) 283 | if !ok { 284 | continue 285 | } 286 | t, found := condition["type"] 287 | if !found { 288 | continue 289 | } 290 | condType, ok := t.(string) 291 | if !ok { 292 | continue 293 | } 294 | if condType == conditionType { 295 | reason := condition["reason"].(string) 296 | conditionStatus := condition["status"].(string) 297 | return reason, corev1.ConditionStatus(conditionStatus), true, nil 298 | } 299 | } 300 | return "", corev1.ConditionFalse, false, nil 301 | } 302 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package controllers 5 | 6 | import ( 7 | "path/filepath" 8 | "sync" 9 | "testing" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | "sigs.k8s.io/controller-runtime/pkg/controller" 14 | "sigs.k8s.io/controller-runtime/pkg/handler" 15 | "sigs.k8s.io/controller-runtime/pkg/manager" 16 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 17 | "sigs.k8s.io/controller-runtime/pkg/source" 18 | 19 | "k8s.io/client-go/kubernetes/scheme" 20 | "k8s.io/client-go/rest" 21 | appv1beta1 "sigs.k8s.io/application/api/v1beta1" 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | "sigs.k8s.io/controller-runtime/pkg/client" 24 | "sigs.k8s.io/controller-runtime/pkg/envtest" 25 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 26 | logf "sigs.k8s.io/controller-runtime/pkg/log" 27 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 28 | // +kubebuilder:scaffold:imports 29 | ) 30 | 31 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 32 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 33 | 34 | var cfg *rest.Config 35 | var k8sClient client.Client 36 | var testEnv *envtest.Environment 37 | 38 | func TestAPIs(t *testing.T) { 39 | RegisterFailHandler(Fail) 40 | 41 | RunSpecsWithDefaultAndCustomReporters(t, 42 | "Controller Suite", 43 | []Reporter{printer.NewlineReporter{}}) 44 | } 45 | 46 | var _ = BeforeSuite(func(done Done) { 47 | logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) 48 | 49 | By("bootstrapping test environment") 50 | testEnv = &envtest.Environment{ 51 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 52 | } 53 | 54 | var err error 55 | err = appv1beta1.AddToScheme(scheme.Scheme) 56 | Expect(err).NotTo(HaveOccurred()) 57 | 58 | cfg, err = testEnv.Start() 59 | Expect(err).ToNot(HaveOccurred()) 60 | Expect(cfg).ToNot(BeNil()) 61 | 62 | // +kubebuilder:scaffold:scheme 63 | 64 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 65 | Expect(err).ToNot(HaveOccurred()) 66 | Expect(k8sClient).ToNot(BeNil()) 67 | 68 | close(done) 69 | }, 60) 70 | 71 | var _ = AfterSuite(func() { 72 | By("tearing down the test environment") 73 | err := testEnv.Stop() 74 | Expect(err).ToNot(HaveOccurred()) 75 | }) 76 | 77 | // SetupTestReconcile returns a reconcile.Reconcile implementation that delegates to inner and 78 | // writes the request to requests after Reconcile is finished. 79 | func SetupTestReconcile(inner reconcile.Reconciler) (reconcile.Reconciler, chan reconcile.Request) { 80 | requests := make(chan reconcile.Request) 81 | fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) { 82 | result, err := inner.Reconcile(req) 83 | requests <- req 84 | return result, err 85 | }) 86 | return fn, requests 87 | } 88 | 89 | // StartTestManager adds recFn 90 | func StartTestManager(mgr manager.Manager) (chan struct{}, *sync.WaitGroup) { 91 | stop := make(chan struct{}) 92 | wg := &sync.WaitGroup{} 93 | wg.Add(1) 94 | go func() { 95 | Expect(mgr.Start(stop)).NotTo(HaveOccurred()) 96 | wg.Done() 97 | }() 98 | return stop, wg 99 | } 100 | 101 | func NewReconciler(mgr manager.Manager) *ApplicationReconciler { 102 | return &ApplicationReconciler{ 103 | Client: mgr.GetClient(), 104 | Mapper: mgr.GetRESTMapper(), 105 | Log: ctrl.Log.WithName("controllers").WithName("Application"), 106 | Scheme: mgr.GetScheme(), 107 | } 108 | } 109 | 110 | func CreateController(name string, mgr manager.Manager, r reconcile.Reconciler) error { 111 | // Create a new controller 112 | c, err := controller.New(name+"-ctrl", mgr, controller.Options{Reconciler: r}) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | // Watch for changes to Base resource 118 | err = c.Watch(&source.Kind{Type: &appv1beta1.Application{}}, 119 | &handler.EnqueueRequestForObject{}) 120 | if err != nil { 121 | return err 122 | } 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ## Application Object 2 | 3 | An Application object provides a way for you to aggregate individual Kubernetes components (e.g. Services, Deployments, StatefulSets, Ingresses, CRDs), and manage them as a group. It provides tooling and UI with a resource that allows for the aggregation and display of all the components in the Application. 4 | 5 |
Field | 8 |Type | 9 |Description | 10 |
---|---|---|
spec.descriptor.type | 13 |string | 14 |The type of the application (e.g. WordPress, MySQL, Cassandra). You can have many applications of different 15 | names in the same namespace. They type field is used to indicate that they are all the same type of application. 16 | | 17 |
spec.componentKinds | 20 |[] GroupKind | 21 |This array of GroupKinds is used to indicate the types of resources that the application is composed of. As 22 | an example an Application that has a service and a deployment would set this field to 23 | [{"group":"core","kind": "Service"},{"group":"apps","kind":"Deployment"}] | 24 |
spec.selector | 27 |LabelSelector | 28 |The selector is used to match resources that belong to the Application. All of the applications 29 | resources should have labels such that they match this selector. Users should use the 30 | app.kubernetes.io/name label on all components of the Application and set the selector to 31 | match this label. For instance, {"matchLabels": [{"app.kubernetes.io/name": "my-cool-app"}]} should be 32 | used as the selector for an Application named "my-cool-app", and each component should contain a label that 33 | matches. | 34 |
spec.addOwnerRef | 37 |bool | 38 |Flag controlling if the matched resources need to be adopted by the Application object. When adopting, an OwnerRef to the Application object is inserted into the matched objects .metadata.[]OwnerRefs. 39 | The injected OwnerRef has blockOwnerDeletion set to True and controller set to False. 40 | | 41 |
spec.descriptor.version | 44 |string | 45 |A version indicator for the application (e.g. 5.7 for MySQL version 5.7). | 46 |
spec.descriptor.description | 49 |string | 50 |A short, human readable textual description of the Application. | 51 |
spec.descriptor.icons | 54 |[]ImageSpec | 55 |A list of icons for an application. Icon information includes the source, size, and mime type. | 56 |
spec.descriptor.maintainers | 59 |[]ContactData | 60 |A list of the maintainers of the Application. Each maintainer has a name, email, and URL. This 61 | field is meant for the distributors of the Application to indicate their identity and contact information. | 62 |
spec.descriptor.owners | 65 |[]ContactData | 66 |A list of the operational owners of the application. This field is meant to be left empty by the 67 | distributors of application, and set by the installer to indicate who should be contacted in the event of a 68 | planned or unplanned disruption to the Application | 69 |
spec.descriptor.keywords | 72 |array string | 73 |A list of keywords that identify the application. | 74 |
spec.info | 77 |[]InfoItem | 78 |Info contains human readable key-value pairs for the Application. | 79 |
spec.descriptor.links | 82 |[]Link | 83 |Links are a list of descriptive URLs intended to be used to surface additional documentation, 84 | dashboards, etc. | 85 |
spec.descriptor.notes | 88 |string | 89 |Notes contain human readable snippets intended as a quick start for the users of the 90 | Application. They may be plain text or CommonMark markdown. | 91 |
spec.assemblyPhase | 94 |string: "Pending", "Succeeded" or "Failed" | 95 |The installer can set this field to indicate that the 96 | application's components are still being deployed 97 | ("Pending") or all are deployed already ("Succeeded"). When the 98 | application cannot be successfully assembled, the installer can set this 99 | field to "Failed". | 100 |