├── .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 | [![Build Status](https://travis-ci.org/kubernetes-sigs/application.svg?branch=master)](https://travis-ci.org/kubernetes-sigs/application "Travis") 2 | [![Go Report Card](https://goreportcard.com/badge/sigs.k8s.io/application)](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 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 85 | 86 | 87 | 88 | 89 | 91 | 92 | 93 | 94 | 95 | 100 | 101 |
FieldTypeDescription
spec.descriptor.typestringThe 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 |
spec.componentKinds[] GroupKind 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"}]
spec.selectorLabelSelectorThe 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.
spec.addOwnerRefboolFlag 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 |
spec.descriptor.versionstringA version indicator for the application (e.g. 5.7 for MySQL version 5.7).
spec.descriptor.descriptionstringA short, human readable textual description of the Application.
spec.descriptor.icons[]ImageSpecA list of icons for an application. Icon information includes the source, size, and mime type.
spec.descriptor.maintainers[]ContactDataA 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.
spec.descriptor.owners[]ContactDataA 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
spec.descriptor.keywordsarray stringA list of keywords that identify the application.
spec.info[]InfoItemInfo contains human readable key-value pairs for the Application.
spec.descriptor.links[]LinkLinks are a list of descriptive URLs intended to be used to surface additional documentation, 84 | dashboards, etc.
spec.descriptor.notesstringNotes contain human readable snippets intended as a quick start for the users of the 90 | Application. They may be plain text or CommonMark markdown.
spec.assemblyPhasestring: "Pending", "Succeeded" or "Failed"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".
102 | 103 | -------------------------------------------------------------------------------- /docs/develop.md: -------------------------------------------------------------------------------- 1 | # Development Guide 2 | 3 | ## Prerequisites 4 | 5 | ### Tools 6 | - make 7 | - [go](https://golang.org/dl/) version v1.13+. 8 | - [docker](https://docs.docker.com/install/) version 17.03+. 9 | 10 | ### Other tools 11 | This repo uses other tools for development, building and testing. 12 | These tools are installed with `make install-tools`: 13 | - controller-gen 14 | - golangci-lint 15 | - mockgen 16 | - conversion-gen 17 | - kubebuilder 18 | - [kustomize](https://github.com/kubernetes-sigs/kustomize) 19 | - addlicense 20 | - misspell 21 | - [kind](https://github.com/kubernetes-sigs/kind) 22 | 23 | ### Cluster 24 | - Access to a Kubernetes v1.11.3+ cluster. 25 | 26 | ## Development 27 | 28 | ### Fork and Clone 29 | 30 | Fork [Application Repo](https://github.com/kubernetes-sigs/application). 31 | Then clone your fork locally. 32 | 33 | ```bash 34 | mkdir -p $GOPATH/src/sigs.k8s.io 35 | cd $GOPATH/src/sigs.k8s.io 36 | 37 | GITHUBID= 38 | git clone git@github.com:${GITHUBID}/application.git $GOPATH/src/sigs.k8s.io/application 39 | ``` 40 | 41 | ### Cluster access 42 | For running e2e tests and development testing you need access to a cluster. You could create a cluster with your cloud provider and ensure the `kubeconfig` points to the cluster. 43 | 44 | ##### Local cluster 45 | For local testing you could create a `kind`. 46 | 47 | ```bash 48 | # this created a kind cluster and updates kubeconfig to point to it 49 | make e2e-setup 50 | ``` 51 | ### Building the controller binary 52 | 53 | ```bash 54 | # make or make all will build 55 | make 56 | 57 | # individual run make targets 58 | # 59 | # generate code 60 | make generate 61 | 62 | # create manifests 63 | make manifests 64 | 65 | # Inject license header to all generated files 66 | make license 67 | 68 | # building the kube-app-manager 69 | make bin/kube-app-manager 70 | ``` 71 | 72 | ### Running tests 73 | After making changes use these targets to test locally. 74 | 75 | ##### unit tests 76 | ```bash 77 | # running unitests 78 | make test 79 | ``` 80 | 81 | ##### e2e tests 82 | ```bash 83 | # running e2e tests 84 | 85 | # If your kubeconfig points to your test cluster skip this step 86 | # This will create a kind cluster for testing 87 | make e2e-setup 88 | 89 | # run e2e tests 90 | make e2e-test 91 | 92 | # Tear down kind cluster 93 | make e2e-cleanup 94 | ``` 95 | 96 | ### Building controller docker 97 | To build the controller into an image named `image` use the following command. 98 | 99 | NOTE: 100 | `CONTROLLER_IMG` is optional. The default value is `gcr.io/$(shell gcloud config get-value project)/kube-app-manager` 101 | 102 | ```commandline 103 | make docker-build CONTROLLER_IMG= 104 | ``` 105 | 106 | To push the controller image, run: 107 | ```commandline 108 | make docker-push CONTROLLER_IMG= 109 | ``` 110 | 111 | ### Installing CRD in the cluster 112 | Once kubeconfig is setup with a cluster. 113 | ```bash 114 | make install 115 | ``` 116 | ### Deploying the controller in cluster 117 | 118 | - This will install the controller into the application-system namespace and with the default RBAC permissions. 119 | - It will also install the Application CRD. 120 | - Ensure the docker image is built and pushed first. 121 | 122 | ```commandline 123 | make deploy CONTROLLER_IMG= 124 | ``` 125 | 126 | ## Using the Application CRD 127 | 128 | The application CRD can be used both via manifests and programmatically. 129 | 130 | ### Manifests 131 | 132 | The docs directory contains an example [manifest](docs/examples/wordpress/application.yaml) that shows how to you can integrate the Application CRD with a [WordPress deployment](docs/examples/wordpress). 133 | 134 | The Application object uses StatefulSets and Services. It also contains some other relevant metadata describing wordpress application. Notice that each Service and StatefulSet is labeled such that Application's Selector matches the labels. The additional labels on the Applications components come from the recommended application labels and annotations. 135 | 136 | ```bash 137 | # Deploying the example 138 | 139 | make deploy-wordpress 140 | kubectl get application 141 | 142 | # cleanup 143 | make undeploy-wordpress 144 | ``` 145 | ### Programmatically 146 | 147 | Kubebuilder provides a client to get, create, update and delete resources and this also works for application resources. This is documented in the kubebuilder book: https://book.kubebuilder.io/ 148 | 149 | Create a client: 150 | ```go 151 | kubeClient, err := client.New(config) 152 | ``` 153 | 154 | Get an application resource: 155 | ```go 156 | object := &applicationsv1beta1.Application{} 157 | objectKey := types.NamespacedName{ 158 | Namespace: "namespace", 159 | Name: "name", 160 | } 161 | err = kubeClient.Get(context.TODO(), objectKey, object) 162 | ``` 163 | 164 | Create a new application resource: 165 | ```go 166 | app := &applicationsv1beta1.Application{ 167 | ... 168 | } 169 | err = kubeClient.Create(context.TODO(), app) 170 | ``` 171 | -------------------------------------------------------------------------------- /docs/examples/wordpress/application.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: app.k8s.io/v1beta1 5 | kind: Application 6 | metadata: 7 | name: "wordpress-01" 8 | labels: 9 | app.kubernetes.io/name: "wordpress-01" 10 | spec: 11 | selector: 12 | matchLabels: 13 | app.kubernetes.io/name: "wordpress-01" 14 | componentKinds: 15 | - group: v1 16 | kind: Service 17 | - group: apps 18 | kind: StatefulSet 19 | addOwnerRef: true 20 | descriptor: 21 | type: "wordpress" 22 | keywords: 23 | - "cms" 24 | - "blog" 25 | - "wordpress" 26 | links: 27 | - description: About 28 | url: "https://wordpress.org/" 29 | - description: Web Server Dashboard 30 | url: "https://metrics/internal/wordpress-01/web-app" 31 | - description: Mysql Dashboard 32 | url: "https://metrics/internal/wordpress-01/mysql" 33 | version: "4.9.4" 34 | description: "WordPress is open source software you can use to create a beautiful website, blog, or app." 35 | icons: 36 | - src: "https://s.w.org/style/images/about/WordPress-logotype-wmark.png" 37 | type: "image/png" 38 | size: "1000x1000" 39 | - src: "https://s.w.org/style/images/about/WordPress-logotype-standard.png" 40 | type: "image/png" 41 | size: "2000x680" 42 | maintainers: 43 | - name: Wordpress Dev 44 | email: dev@wordpress.org 45 | owners: 46 | - name: Wordpress Admin 47 | email: admin@wordpress.org 48 | -------------------------------------------------------------------------------- /docs/examples/wordpress/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 | # namePrefix: application- 11 | 12 | # Labels to add to all resources and selectors. 13 | commonLabels: 14 | app.kubernetes.io/name: "wordpress-01" 15 | 16 | resources: 17 | - application.yaml 18 | - pv.yaml 19 | - mysql.yaml 20 | - webserver.yaml 21 | 22 | secretGenerator: 23 | - name: mysql-pass 24 | type: Opaque 25 | env: secrets.txt 26 | -------------------------------------------------------------------------------- /docs/examples/wordpress/mysql.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: wordpress-mysql-hsvc 9 | labels: 10 | app.kubernetes.io/name: "wordpress-01" 11 | app.kubernetes.io/version: "3" 12 | app.kubernetes.io/component: "mysql-hsvc" 13 | app.kubernetes.io/tier: "backend" 14 | spec: 15 | ports: 16 | - port: 3306 17 | selector: 18 | app.kubernetes.io/name: "wordpress-01" 19 | app.kubernetes.io/component: "mysql-rdbms" 20 | clusterIP: None 21 | --- 22 | apiVersion: apps/v1 23 | kind: StatefulSet 24 | metadata: 25 | name: wordpress-mysql 26 | labels: 27 | app.kubernetes.io/name: "wordpress-01" 28 | app.kubernetes.io/version: "3" 29 | app.kubernetes.io/component: "mysql-rdbms" 30 | app.kubernetes.io/tier: "backend" 31 | spec: 32 | selector: 33 | matchLabels: 34 | app.kubernetes.io/name: "wordpress-01" 35 | app.kubernetes.io/component: "mysql-rdbms" 36 | app.kubernetes.io/tier: "backend" 37 | replicas: 1 38 | serviceName: wordpress-mysql-hsvc 39 | template: 40 | metadata: 41 | labels: 42 | app.kubernetes.io/name: "wordpress-01" 43 | app.kubernetes.io/component: "mysql-rdbms" 44 | app.kubernetes.io/tier: "backend" 45 | spec: 46 | containers: 47 | - image: mysql:5.6 48 | name: mysql 49 | env: 50 | - name: MYSQL_ROOT_PASSWORD 51 | valueFrom: 52 | secretKeyRef: 53 | name: mysql-pass 54 | key: password 55 | ports: 56 | - containerPort: 3306 57 | name: mysql 58 | volumeMounts: 59 | - name: mysql-persistent-storage 60 | mountPath: /var/lib/mysql 61 | volumeClaimTemplates: 62 | - metadata: 63 | name: mysql-persistent-storage 64 | spec: 65 | storageClassName: manual 66 | accessModes: [ "ReadWriteOnce" ] 67 | resources: 68 | requests: 69 | storage: 5Gi 70 | -------------------------------------------------------------------------------- /docs/examples/wordpress/pv.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: v1 5 | kind: PersistentVolume 6 | metadata: 7 | name: pv-volume-1 8 | labels: 9 | type: local 10 | spec: 11 | storageClassName: manual 12 | capacity: 13 | storage: 7Gi 14 | accessModes: 15 | - ReadWriteOnce 16 | hostPath: 17 | path: "/tmp/data1" 18 | --- 19 | apiVersion: v1 20 | kind: PersistentVolume 21 | metadata: 22 | name: pv-volume-2 23 | labels: 24 | type: local 25 | spec: 26 | storageClassName: manual 27 | capacity: 28 | storage: 7Gi 29 | accessModes: 30 | - ReadWriteOnce 31 | hostPath: 32 | path: "/tmp/data2" 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/examples/wordpress/secrets.txt: -------------------------------------------------------------------------------- 1 | password=thisIsJustAnExample2019! 2 | -------------------------------------------------------------------------------- /docs/examples/wordpress/webserver.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: wordpress-webserver-svc 9 | labels: 10 | app.kubernetes.io/name: "wordpress-01" 11 | app.kubernetes.io/version: "3" 12 | app.kubernetes.io/component: "wordpress-svc" 13 | app.kubernetes.io/tier: "frontend" 14 | spec: 15 | ports: 16 | - port: 80 17 | selector: 18 | app.kubernetes.io/name: "wordpress-01" 19 | app.kubernetes.io/component: "wordpress-webserver" 20 | clusterIP: None 21 | --- 22 | apiVersion: v1 23 | kind: Service 24 | metadata: 25 | name: wordpress-webserver-hsvc 26 | labels: 27 | app.kubernetes.io/name: "wordpress-01" 28 | app.kubernetes.io/version: "3" 29 | app.kubernetes.io/component: "wordpress-hsvc" 30 | app.kubernetes.io/tier: "frontend" 31 | spec: 32 | ports: 33 | - port: 3306 34 | selector: 35 | app.kubernetes.io/name: "wordpress-01" 36 | app.kubernetes.io/component: "wordpress-webserver" 37 | clusterIP: None 38 | --- 39 | apiVersion: apps/v1 40 | kind: StatefulSet 41 | metadata: 42 | name: wordpress-webserver 43 | labels: 44 | app.kubernetes.io/name: "wordpress-01" 45 | app.kubernetes.io/version: "3" 46 | app.kubernetes.io/component: "wordpress-webserver" 47 | app.kubernetes.io/tier: "frontend" 48 | annotations: 49 | kubernetes.io/application: wordpress 50 | spec: 51 | replicas: 1 52 | serviceName: wordpress-webserver-hsvc 53 | selector: 54 | matchLabels: 55 | app.kubernetes.io/name: "wordpress-01" 56 | app.kubernetes.io/component: "wordpress-webserver" 57 | template: 58 | metadata: 59 | labels: 60 | app.kubernetes.io/name: "wordpress-01" 61 | app.kubernetes.io/version: "3" 62 | app.kubernetes.io/component: "wordpress-webserver" 63 | app.kubernetes.io/tier: "frontend" 64 | spec: 65 | containers: 66 | - image: wordpress:4.8-apache 67 | name: wordpress 68 | env: 69 | - name: WORDPRESS_DB_HOST 70 | value: wordpress-mysql-hsvc 71 | - name: WORDPRESS_DB_PASSWORD 72 | valueFrom: 73 | secretKeyRef: 74 | name: mysql-pass 75 | key: password 76 | ports: 77 | - containerPort: 80 78 | name: wordpress 79 | volumeMounts: 80 | - name: wordpress-persistent-storage 81 | mountPath: /var/www/html 82 | volumeClaimTemplates: 83 | - metadata: 84 | name: wordpress-persistent-storage 85 | spec: 86 | storageClassName: manual 87 | accessModes: [ "ReadWriteOnce" ] 88 | resources: 89 | requests: 90 | storage: 5Gi 91 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | Clone 3 | ```bash 4 | mkdir -p $GOPATH/src/sigs.k8s.io 5 | cd $GOPATH/src/sigs.k8s.io 6 | 7 | # clone 8 | git clone git@github.com:kubernetes-sigs/application.git 9 | ``` 10 | 11 | Deploy to cluster 12 | ```bash 13 | # deploy to cluster 14 | make deploy 15 | 16 | # un-deploy from cluster 17 | make undeploy 18 | ``` 19 | 20 | # Dev Quick Start 21 | 22 | Fork and clone 23 | ```bash 24 | mkdir -p $GOPATH/src/sigs.k8s.io 25 | cd $GOPATH/src/sigs.k8s.io 26 | 27 | # fork https://github.com/kubernetes-sigs/application 28 | # clone 29 | GITHUBID= 30 | git clone git@github.com:${GITHUBID}/application.git $GOPATH/src/sigs.k8s.io/application 31 | ``` 32 | 33 | Run locally 34 | ```bash 35 | # create local cluster 36 | make e2e-setup 37 | 38 | # install CRD 39 | make deploy-crd 40 | 41 | # run locally 42 | make run 43 | 44 | # tear down 45 | make e2e-cleanup 46 | ``` 47 | 48 | Run against cluster 49 | ```bash 50 | # The default image is `gcr.io/$(shell gcloud config get-value project)/kube-app-manager` 51 | # to use different image edit VERSION-DEV file 52 | 53 | # build docker image 54 | make docker-build 55 | make docker-push 56 | 57 | # deploy to cluster. This generates the manifests and deploys 58 | make deploy-dev 59 | 60 | # deploy example 61 | make deploy-wordpress 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/release.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | [semver]: https://semver.org 4 | [`VERSION`]: ../VERSION 5 | [`VERSION-DEV`]: ../VERSION-DEV 6 | 7 | This document describes how to perform a [semver] release. 8 | 9 | ## Release artifacts 10 | 1. Branch `release-vMajor.Minor` for each `Major.Minor.*` version. Example `release-v0.8` 11 | 2. Tag `vMajor.Minor.Patch` for each `Major.Minor.Patch` version. Example `v0.8.1` 12 | 3. All in one deployment yaml for the Application controller 13 | 4. Container image for the Application controller 14 | 15 | ## Release roles 16 | 17 | Only Repo Owners can create branches in the [Application Repo](https://github.com/kubernetes-sigs/application). Developers who fork the repo can create releases in their repo as well. 18 | 19 | #### Version files 20 | For official releases [`VERSION`] file is used. 21 | For developers [`VERSION-DEV`] file is used. 22 | 23 | The default file used is [`VERSION-DEV`]. 24 | To use [`VERSION`], set the `VERSION_FILE` env variable. 25 | 26 | Developers should edit the [`VERSION-DEV`] file to set their choice of container registry and version. 27 | 28 | ## Release procedure 29 | 30 | #### Create release branch 31 | 32 | Release are always cut from the `master` branch `HEAD`. 33 | Ensure that all necessary fixes are merged, documentation updated and most importantly the [`VERSION`] file is updated. 34 | The steps to create a release branch are: 35 | ```bash 36 | 37 | # Repo owners use this for official releases: 38 | VERSION_FILE=VERSION make release-branch 39 | 40 | # Developers use this for their fork 41 | make release-branch 42 | ``` 43 | 44 | #### Create release tag 45 | 46 | Patch releases are created from the patch branch. 47 | Ensure that all necessary fixes are merged, documentation updated and most importantly the `patch` version is updated in the [`VERSION`] file. 48 | The steps to create a release tag are: 49 | ```bash 50 | 51 | # Repo owners use this for official releases: 52 | VERSION_FILE=VERSION make release-tag 53 | 54 | # Developers use this for their fork 55 | make release-tag 56 | ``` 57 | 58 | #### Deleting release tag 59 | 60 | The steps to delete a release tag are: 61 | ```bash 62 | 63 | # Repo owners use this for official releases: 64 | VERSION_FILE=VERSION make delete-release-tag 65 | 66 | # Developers use this for their fork 67 | make delete-release-tag 68 | ``` 69 | 70 | ### TODO 71 | - Release notes 72 | - Generate changes between releases 73 | 74 | -------------------------------------------------------------------------------- /e2e/kind-config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | 5 | kind: Cluster 6 | apiVersion: kind.x-k8s.io/v1alpha4 7 | nodes: 8 | - role: control-plane 9 | - role: worker 10 | - role: worker 11 | - role: worker 12 | -------------------------------------------------------------------------------- /e2e/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "fmt" 10 | "io" 11 | "log" 12 | "os" 13 | "os/exec" 14 | "path" 15 | "testing" 16 | "time" 17 | 18 | . "github.com/onsi/ginkgo" 19 | "github.com/onsi/ginkgo/reporters" 20 | . "github.com/onsi/gomega" 21 | corev1 "k8s.io/api/core/v1" 22 | apiextcs "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "k8s.io/apimachinery/pkg/runtime/schema" 27 | "k8s.io/apimachinery/pkg/types" 28 | "k8s.io/apimachinery/pkg/util/wait" 29 | "k8s.io/client-go/kubernetes/scheme" 30 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 31 | "k8s.io/client-go/rest" 32 | "k8s.io/client-go/tools/clientcmd" 33 | appv1beta1 "sigs.k8s.io/application/api/v1beta1" 34 | "sigs.k8s.io/application/controllers" 35 | "sigs.k8s.io/application/e2e/testutil" 36 | "sigs.k8s.io/controller-runtime/pkg/client" 37 | ) 38 | 39 | func TestE2e(t *testing.T) { 40 | RegisterFailHandler(Fail) 41 | junitReporter := reporters.NewJUnitReporter("/workspace/_artifacts/junit.xml") 42 | RunSpecsWithDefaultAndCustomReporters(t, "Application Type Suite", []Reporter{junitReporter}) 43 | } 44 | 45 | func getClientConfig() (*rest.Config, error) { 46 | return clientcmd.BuildConfigFromFlags("", path.Join(os.Getenv("HOME"), ".kube/config")) 47 | } 48 | 49 | func getKubeClientOrDie(config *rest.Config, s *runtime.Scheme) client.Client { 50 | c, err := client.New(config, client.Options{Scheme: s}) 51 | if err != nil { 52 | panic(err) 53 | } 54 | return c 55 | } 56 | 57 | const ( 58 | crdPath = "../config/crd/bases/app.k8s.io_applications.yaml" 59 | crdv080Path = "resources/app-crd-v0.8.0.yaml" 60 | testCrdPath = "resources/withcrd/test_crd.yaml" 61 | applicationPath = "resources/withcrd/base/application.yaml" 62 | waitTimeout = time.Second * 120 63 | pullPeriod = time.Second * 2 64 | syncPeriod = "2" 65 | ) 66 | 67 | var _ = Describe("Application CRD e2e", func() { 68 | s := scheme.Scheme 69 | _ = appv1beta1.AddToScheme(s) 70 | 71 | crdv080, err := testutil.ParseCRDYaml(crdv080Path) 72 | if err != nil { 73 | log.Fatal("Unable to parse CRD YAML with version 0.8.0", err) 74 | } 75 | 76 | crd, err := testutil.ParseCRDYaml(crdPath) 77 | if err != nil { 78 | log.Fatal("Unable to parse CRD YAML", err) 79 | } 80 | 81 | testcrd, err := testutil.ParseCRDYaml(testCrdPath) 82 | if err != nil { 83 | log.Fatal("Unable to parse test CRD YAML", err) 84 | } 85 | 86 | config, err := getClientConfig() 87 | if err != nil { 88 | log.Fatal("Unable to get client configuration", err) 89 | } 90 | 91 | extClient, err := apiextcs.NewForConfig(config) 92 | if err != nil { 93 | log.Fatal("Unable to construct extensions client", err) 94 | } 95 | 96 | var managerStdout bytes.Buffer 97 | var managerStderr bytes.Buffer 98 | managerCmd := exec.Command("../bin/kube-app-manager", "--sync-period", syncPeriod) 99 | managerCmd.Stdout = &managerStdout 100 | managerCmd.Stderr = &managerStderr 101 | 102 | It("should create CRD v0.8.0", func() { 103 | err = testutil.CreateOrUpdateCRD(extClient, crdv080) 104 | Expect(err).NotTo(HaveOccurred()) 105 | err = testutil.WaitForCRDOrDie(extClient, crdv080.Name) 106 | Expect(err).NotTo(HaveOccurred()) 107 | }) 108 | 109 | It("should create the wordpress application", func() { 110 | err = applyKustomize("../docs/examples/wordpress") 111 | Expect(err).NotTo(HaveOccurred()) 112 | }) 113 | 114 | It("should update the CRD v0.8.0 to new CRD", func() { 115 | err = testutil.CreateOrUpdateCRD(extClient, crd) 116 | Expect(err).NotTo(HaveOccurred()) 117 | err = testutil.WaitForCRDOrDie(extClient, crd.Name) 118 | Expect(err).NotTo(HaveOccurred()) 119 | }) 120 | 121 | It("should create test CRD", func() { 122 | err = testutil.CreateOrUpdateCRD(extClient, testcrd) 123 | Expect(err).NotTo(HaveOccurred()) 124 | err = testutil.WaitForCRDOrDie(extClient, testcrd.Name) 125 | Expect(err).NotTo(HaveOccurred()) 126 | }) 127 | 128 | It("should register an application", func() { 129 | client := getKubeClientOrDie(config, s) //Make sure to create the client after CRD has been created. 130 | err = testutil.CreateApplication(client, "default", applicationPath) 131 | Expect(err).NotTo(HaveOccurred()) 132 | }) 133 | 134 | It("should delete application", func() { 135 | client := getKubeClientOrDie(config, s) 136 | err = testutil.DeleteApplication(client, "default", applicationPath) 137 | Expect(err).NotTo(HaveOccurred()) 138 | }) 139 | 140 | It("should start the controller", func() { 141 | err = managerCmd.Start() 142 | Expect(err).NotTo(HaveOccurred()) 143 | }) 144 | 145 | It("should create the test application with custom resources", func() { 146 | err = applyKustomize("resources/withcrd/overlays/working") 147 | Expect(err).NotTo(HaveOccurred()) 148 | }) 149 | 150 | It("should update wordpress-01 status", func() { 151 | kubeClient := getKubeClientOrDie(config, s) 152 | application := &appv1beta1.Application{} 153 | objectKey := types.NamespacedName{ 154 | Namespace: metav1.NamespaceDefault, 155 | Name: "wordpress-01", 156 | } 157 | waitForApplicationStatusToHaveNComponents(kubeClient, objectKey, application, 5, 5) 158 | Expect(application.Status.ObservedGeneration).To(BeNumerically("<=", 5)) 159 | Expect(application.Status.ComponentList.Objects).To(HaveLen(5)) 160 | }) 161 | 162 | It("should update ok-withcrd status", func() { 163 | kubeClient := getKubeClientOrDie(config, s) 164 | application := &appv1beta1.Application{} 165 | objectKey := types.NamespacedName{ 166 | Namespace: metav1.NamespaceDefault, 167 | Name: "ok-withcrd", 168 | } 169 | waitForApplicationStatusToHaveNComponents(kubeClient, objectKey, application, 7, 7) 170 | Expect(application.Status.ObservedGeneration).To(BeNumerically("<=", 7)) 171 | Expect(application.Status.ComponentList.Objects).To(HaveLen(7)) 172 | }) 173 | 174 | It("should add ownerReference to components", func() { 175 | kubeClient := getKubeClientOrDie(config, s) 176 | matchingLabels := map[string]string{"app.kubernetes.io/name": "wordpress-01"} 177 | 178 | list := &unstructured.UnstructuredList{} 179 | list.SetGroupVersionKind(schema.GroupVersionKind{ 180 | Group: "", 181 | Kind: "Service", 182 | Version: "v1", 183 | }) 184 | validateComponentOwnerReferences(kubeClient, list, matchingLabels, "wordpress-01") 185 | 186 | list.SetGroupVersionKind(schema.GroupVersionKind{ 187 | Group: "apps", 188 | Kind: "StatefulSet", 189 | Version: "v1", 190 | }) 191 | validateComponentOwnerReferences(kubeClient, list, matchingLabels, "wordpress-01") 192 | 193 | matchingLabels = map[string]string{"app.kubernetes.io/name": "test-01"} 194 | list.SetGroupVersionKind(schema.GroupVersionKind{ 195 | Group: "test.crd.com", 196 | Kind: "TestCRD", 197 | Version: "v1", 198 | }) 199 | validateComponentOwnerReferences(kubeClient, list, matchingLabels, "test-application-01") 200 | }) 201 | 202 | It("should mark the application not-ready if not all components are ready", func() { 203 | err = applyKustomize("resources/withcrd/overlays/broken") 204 | Expect(err).NotTo(HaveOccurred()) 205 | 206 | kubeClient := getKubeClientOrDie(config, s) 207 | application := &appv1beta1.Application{} 208 | objectKey := types.NamespacedName{ 209 | Namespace: metav1.NamespaceDefault, 210 | Name: "nok-withcrd", 211 | } 212 | waitForApplicationStatusToHaveNComponents(kubeClient, objectKey, application, 6, 7) 213 | Expect(application.Status.ObservedGeneration).To(BeNumerically("<=", 7)) 214 | Expect(hasConditionTypeStatusAndReason(application.Status.Conditions, controllers.StatusReady, corev1.ConditionFalse, "ComponentsNotReady")).To(BeTrue()) 215 | }) 216 | 217 | It("should stop the controller", func() { 218 | err = managerCmd.Process.Signal(os.Interrupt) 219 | _, _ = io.Copy(os.Stderr, &managerStderr) 220 | _, _ = io.Copy(os.Stdout, &managerStdout) 221 | Expect(err).NotTo(HaveOccurred()) 222 | }) 223 | 224 | It("should delete application CRD", func() { 225 | err = testutil.DeleteCRD(extClient, crd.Name) 226 | Expect(err).NotTo(HaveOccurred()) 227 | }) 228 | 229 | It("should delete test CRD", func() { 230 | err = testutil.DeleteCRD(extClient, testcrd.Name) 231 | Expect(err).NotTo(HaveOccurred()) 232 | }) 233 | }) 234 | 235 | func validateComponentOwnerReferences(kubeClient client.Client, list *unstructured.UnstructuredList, matchedingLabels map[string]string, ownerName string) { 236 | err := wait.PollImmediate(pullPeriod, waitTimeout, func() (bool, error) { 237 | 238 | log.Println("Pulling the component with Kind = ", list.GetKind()) 239 | if err := kubeClient.List(context.TODO(), list, client.InNamespace(metav1.NamespaceDefault), client.MatchingLabels(matchedingLabels)); err != nil { 240 | return false, nil 241 | } 242 | 243 | for _, item := range list.Items { 244 | if item.GetOwnerReferences() == nil || len(item.GetOwnerReferences()) < 1 || item.GetOwnerReferences()[0].Name != ownerName { 245 | log.Println("Component ownerReferences has NOT been updated yet") 246 | return false, nil 247 | } 248 | } 249 | log.Println("Component ownerReferences has been updated successfully") 250 | return true, nil 251 | }) 252 | Expect(err).NotTo(HaveOccurred()) 253 | } 254 | 255 | func waitForApplicationStatusToHaveNComponents(kubeClient client.Client, key client.ObjectKey, app *appv1beta1.Application, ready int, total int) { 256 | err := wait.PollImmediate(pullPeriod, waitTimeout, func() (bool, error) { 257 | log.Println("Pulling the application status") 258 | if err := kubeClient.Get(context.TODO(), key, app); err != nil { 259 | return false, nil 260 | } 261 | 262 | if app.Status.ComponentList.Objects != nil { 263 | if len(app.Status.ComponentList.Objects) == total && app.Status.Conditions != nil && app.Status.ComponentsReady == fmt.Sprintf("%d/%d", ready, total) { 264 | log.Println("Application status has been updated successfully") 265 | return true, nil 266 | } 267 | log.Printf("Application status ready components: %s", app.Status.ComponentsReady) 268 | return false, nil 269 | } 270 | log.Println("Application status has NOT been updated yet") 271 | return false, nil 272 | }) 273 | Expect(err).NotTo(HaveOccurred()) 274 | } 275 | 276 | func applyKustomize(path string) error { 277 | var err error 278 | var kubectlOP bytes.Buffer 279 | var kubectlError bytes.Buffer 280 | var kustError bytes.Buffer 281 | 282 | kustomize := exec.Command("../hack/tools/bin/kustomize", "build", path) 283 | kubectl := exec.Command("../hack/tools/bin/kubectl", "apply", "-f", "-") 284 | 285 | r, w := io.Pipe() 286 | kustomize.Stdout = w 287 | kustomize.Stderr = &kustError 288 | kubectl.Stdin = r 289 | kubectl.Stderr = &kubectlError 290 | kubectl.Stdout = &kubectlOP 291 | 292 | err = kustomize.Start() 293 | if err != nil { 294 | return err 295 | } 296 | err = kubectl.Start() 297 | if err != nil { 298 | return err 299 | } 300 | err = kustomize.Wait() 301 | if err != nil { 302 | _, _ = io.Copy(os.Stdout, &kustError) 303 | return err 304 | } 305 | w.Close() 306 | err = kubectl.Wait() 307 | if err != nil { 308 | _, _ = io.Copy(os.Stdout, &kubectlError) 309 | return err 310 | } 311 | _, _ = io.Copy(os.Stdout, &kubectlOP) 312 | 313 | return nil 314 | } 315 | 316 | func hasConditionTypeStatusAndReason(conditions []appv1beta1.Condition, t appv1beta1.ConditionType, s corev1.ConditionStatus, r string) bool { 317 | for _, condition := range conditions { 318 | if condition.Type == t && condition.Status == s && condition.Reason == r { 319 | return true 320 | } 321 | } 322 | return false 323 | } 324 | -------------------------------------------------------------------------------- /e2e/resources/app-crd-v0.8.0.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: apiextensions.k8s.io/v1 5 | kind: CustomResourceDefinition 6 | metadata: 7 | annotations: 8 | api-approved.kubernetes.io: https://github.com/kubernetes-sigs/application/pull/2 9 | controller-gen.kubebuilder.io/version: v0.3.0 10 | creationTimestamp: null 11 | labels: 12 | controller-tools.k8s.io: "1.0" 13 | name: applications.app.k8s.io 14 | spec: 15 | group: app.k8s.io 16 | names: 17 | kind: Application 18 | plural: applications 19 | scope: Namespaced 20 | versions: 21 | - name: v1beta1 22 | schema: 23 | openAPIV3Schema: 24 | properties: 25 | apiVersion: 26 | type: string 27 | kind: 28 | type: string 29 | metadata: 30 | type: object 31 | spec: 32 | properties: 33 | addOwnerRef: 34 | type: boolean 35 | assemblyPhase: 36 | type: string 37 | componentKinds: 38 | items: 39 | properties: 40 | group: 41 | type: string 42 | kind: 43 | type: string 44 | required: 45 | - group 46 | - kind 47 | type: object 48 | type: array 49 | descriptor: 50 | properties: 51 | description: 52 | type: string 53 | icons: 54 | items: 55 | properties: 56 | size: 57 | type: string 58 | src: 59 | type: string 60 | type: 61 | type: string 62 | required: 63 | - src 64 | type: object 65 | type: array 66 | keywords: 67 | items: 68 | type: string 69 | type: array 70 | links: 71 | items: 72 | properties: 73 | description: 74 | type: string 75 | url: 76 | type: string 77 | type: object 78 | type: array 79 | maintainers: 80 | items: 81 | properties: 82 | email: 83 | type: string 84 | name: 85 | type: string 86 | url: 87 | type: string 88 | type: object 89 | type: array 90 | notes: 91 | type: string 92 | owners: 93 | items: 94 | properties: 95 | email: 96 | type: string 97 | name: 98 | type: string 99 | url: 100 | type: string 101 | type: object 102 | type: array 103 | type: 104 | type: string 105 | version: 106 | type: string 107 | type: object 108 | info: 109 | items: 110 | properties: 111 | name: 112 | type: string 113 | type: 114 | type: string 115 | value: 116 | type: string 117 | valueFrom: 118 | properties: 119 | configMapKeyRef: 120 | properties: 121 | apiVersion: 122 | type: string 123 | fieldPath: 124 | type: string 125 | key: 126 | type: string 127 | kind: 128 | type: string 129 | name: 130 | type: string 131 | namespace: 132 | type: string 133 | resourceVersion: 134 | type: string 135 | uid: 136 | type: string 137 | type: object 138 | ingressRef: 139 | properties: 140 | apiVersion: 141 | type: string 142 | fieldPath: 143 | type: string 144 | host: 145 | type: string 146 | kind: 147 | type: string 148 | name: 149 | type: string 150 | namespace: 151 | type: string 152 | path: 153 | type: string 154 | protocol: 155 | type: string 156 | resourceVersion: 157 | type: string 158 | uid: 159 | type: string 160 | type: object 161 | secretKeyRef: 162 | properties: 163 | apiVersion: 164 | type: string 165 | fieldPath: 166 | type: string 167 | key: 168 | type: string 169 | kind: 170 | type: string 171 | name: 172 | type: string 173 | namespace: 174 | type: string 175 | resourceVersion: 176 | type: string 177 | uid: 178 | type: string 179 | type: object 180 | serviceRef: 181 | properties: 182 | apiVersion: 183 | type: string 184 | fieldPath: 185 | type: string 186 | kind: 187 | type: string 188 | name: 189 | type: string 190 | namespace: 191 | type: string 192 | path: 193 | type: string 194 | port: 195 | format: int32 196 | type: integer 197 | protocol: 198 | type: string 199 | resourceVersion: 200 | type: string 201 | uid: 202 | type: string 203 | type: object 204 | type: 205 | type: string 206 | type: object 207 | type: object 208 | type: array 209 | selector: 210 | properties: 211 | matchExpressions: 212 | items: 213 | properties: 214 | key: 215 | type: string 216 | operator: 217 | type: string 218 | values: 219 | items: 220 | type: string 221 | type: array 222 | required: 223 | - key 224 | - operator 225 | type: object 226 | type: array 227 | matchLabels: 228 | additionalProperties: 229 | type: string 230 | type: object 231 | type: object 232 | type: object 233 | status: 234 | properties: 235 | components: 236 | items: 237 | properties: 238 | group: 239 | type: string 240 | kind: 241 | type: string 242 | link: 243 | type: string 244 | name: 245 | type: string 246 | status: 247 | type: string 248 | type: object 249 | type: array 250 | componentsReady: 251 | type: string 252 | conditions: 253 | items: 254 | properties: 255 | lastTransitionTime: 256 | format: date-time 257 | type: string 258 | lastUpdateTime: 259 | format: date-time 260 | type: string 261 | message: 262 | type: string 263 | reason: 264 | type: string 265 | status: 266 | type: string 267 | type: 268 | type: string 269 | required: 270 | - status 271 | - type 272 | type: object 273 | type: array 274 | observedGeneration: 275 | format: int64 276 | type: integer 277 | type: object 278 | type: object 279 | served: true 280 | storage: true 281 | subresources: 282 | status: {} 283 | status: 284 | acceptedNames: 285 | kind: "" 286 | plural: "" 287 | conditions: [] 288 | storedVersions: [] 289 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/base/application.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: app.k8s.io/v1beta1 5 | kind: Application 6 | metadata: 7 | name: "withcrd" 8 | spec: 9 | selector: 10 | matchLabels: 11 | app.kubernetes.io/name: "withcrd-ok" 12 | componentKinds: 13 | - group: apps 14 | kind: Deployment 15 | - group: batch 16 | kind: Job 17 | - group: v1 18 | kind: Service 19 | - group: v1 20 | kind: ConfigMap 21 | - group: test.crd.com 22 | kind: TestCRD 23 | addOwnerRef: true 24 | descriptor: 25 | type: "test" 26 | keywords: 27 | - "test" 28 | version: "0.0.1" 29 | description: "It is a simple E2E test app" 30 | maintainers: 31 | - name: Test Dev 32 | email: dev@test.org 33 | owners: 34 | - name: Test Admin 35 | email: admin@test.org 36 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/base/configmap.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: v1 5 | kind: ConfigMap 6 | metadata: 7 | name: config 8 | data: 9 | data-1: value-1 10 | data-2: value-2 11 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/base/deployment.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: hello 8 | labels: 9 | app.kubernetes.io/version: "3" 10 | app.kubernetes.io/component: "hello-world" 11 | app.kubernetes.io/tier: "backend" 12 | spec: 13 | selector: 14 | matchLabels: 15 | app.kubernetes.io/name: "test-01" 16 | app.kubernetes.io/component: "hello-world" 17 | app.kubernetes.io/tier: "backend" 18 | replicas: 1 19 | template: 20 | metadata: 21 | labels: 22 | app.kubernetes.io/name: "test-01" 23 | app.kubernetes.io/component: "hello-world" 24 | app.kubernetes.io/tier: "backend" 25 | spec: 26 | containers: 27 | - image: gcr.io/google-samples/node-hello:1.0 28 | name: hello-world 29 | ports: 30 | - containerPort: 8080 31 | protocol: TCP 32 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/base/job.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: batch/v1 5 | kind: Job 6 | metadata: 7 | name: pi 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: pi 13 | image: perl 14 | command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] 15 | restartPolicy: Never 16 | backoffLimit: 4 17 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/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 | 7 | resources: 8 | - application.yaml 9 | - configmap.yaml 10 | - deployment.yaml 11 | - job.yaml 12 | - service.yaml 13 | - testcr1.yaml 14 | - testcr2.yaml 15 | - testcr3.yaml 16 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/base/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 | name: websvc 8 | labels: 9 | app.kubernetes.io/version: "3" 10 | app.kubernetes.io/component: "test-svc" 11 | app.kubernetes.io/tier: "frontend" 12 | spec: 13 | ports: 14 | - port: 80 15 | selector: 16 | app.kubernetes.io/component: "test-webserver" 17 | clusterIP: None 18 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/base/testcr1.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: test.crd.com/v1 5 | kind: TestCRD 6 | metadata: 7 | name: cr1 8 | spec: 9 | foo: bar 10 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/base/testcr2.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: test.crd.com/v1 5 | kind: TestCRD 6 | metadata: 7 | name: cr2 8 | spec: 9 | foo: bar 10 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/base/testcr3.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: test.crd.com/v1 5 | kind: TestCRD 6 | metadata: 7 | name: cr3 8 | spec: 9 | foo: bar 10 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/overlays/broken/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 | - ../../base 9 | 10 | namePrefix: nok- 11 | 12 | # Labels to add to all resources and selectors. 13 | commonLabels: 14 | app.kubernetes.io/name: "withcrd-nok" 15 | 16 | patchesStrategicMerge: 17 | - |- 18 | apiVersion: app.k8s.io/v1beta1 19 | kind: Application 20 | metadata: 21 | name: withcrd 22 | spec: 23 | selector: 24 | matchLabels: 25 | app.kubernetes.io/name: "withcrd-nok" 26 | 27 | images: 28 | - name: gcr.io/google-samples/node-hello 29 | newName: badimage 30 | newTag: badtag 31 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/overlays/working/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 | - ../../base 9 | 10 | namePrefix: ok- 11 | 12 | # Labels to add to all resources and selectors. 13 | commonLabels: 14 | app.kubernetes.io/name: "withcrd-ok" 15 | -------------------------------------------------------------------------------- /e2e/resources/withcrd/test_crd.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | apiVersion: apiextensions.k8s.io/v1 5 | kind: CustomResourceDefinition 6 | metadata: 7 | annotations: 8 | api-approved.kubernetes.io: "unapproved, testing-only" 9 | controller-gen.kubebuilder.io/version: v0.3.0 10 | creationTimestamp: null 11 | name: testcrds.test.crd.com 12 | spec: 13 | group: test.crd.com 14 | names: 15 | kind: TestCRD 16 | listKind: TestCRDList 17 | plural: testcrds 18 | singular: testcrd 19 | scope: Namespaced 20 | versions: 21 | - name: v1 22 | served: true 23 | storage: true 24 | schema: 25 | openAPIV3Schema: 26 | description: TestCRD is the Schema for the testcrds API 27 | properties: 28 | apiVersion: 29 | description: 'APIVersion defines the versioned schema of this representation 30 | of an object. Servers should convert recognized schemas to the latest 31 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 32 | type: string 33 | kind: 34 | description: 'Kind is a string value representing the REST resource this 35 | object represents. Servers may infer this from the endpoint the client 36 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 37 | type: string 38 | metadata: 39 | type: object 40 | spec: 41 | description: TestCRDSpec defines the desired state of TestCRD 42 | properties: 43 | foo: 44 | description: Foo is an example field of TestCRD. Edit TestCRD_types.go 45 | to remove/update 46 | type: string 47 | type: object 48 | status: 49 | description: TestCRDStatus defines the observed state of TestCRD 50 | type: object 51 | type: object 52 | status: 53 | acceptedNames: 54 | kind: "" 55 | plural: "" 56 | conditions: [] 57 | storedVersions: [] 58 | -------------------------------------------------------------------------------- /e2e/testutil/appresource.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package testutil 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "io" 10 | "os" 11 | 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | "k8s.io/apimachinery/pkg/types" 14 | "k8s.io/apimachinery/pkg/util/json" 15 | "k8s.io/apimachinery/pkg/util/yaml" 16 | applicationsv1beta1 "sigs.k8s.io/application/api/v1beta1" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | ) 19 | 20 | // CreateApplication - create application object 21 | func CreateApplication(kubeClient client.Client, ns string, relativePath string) error { 22 | app, err := parseApplicationYaml(relativePath) 23 | if err != nil { 24 | return err 25 | } 26 | app.Namespace = ns 27 | 28 | object := &applicationsv1beta1.Application{} 29 | objectKey := types.NamespacedName{ 30 | Namespace: ns, 31 | Name: app.Name, 32 | } 33 | err = kubeClient.Get(context.TODO(), objectKey, object) 34 | 35 | if err == nil { 36 | // Application already exists -> Update 37 | err = kubeClient.Update(context.TODO(), app) 38 | if err != nil { 39 | return err 40 | } 41 | } else { 42 | // Application doesn't exist -> Create 43 | fmt.Printf("Creating new Application\n") 44 | err = kubeClient.Create(context.TODO(), app) 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // DeleteApplication - delete application object 54 | func DeleteApplication(kubeClient client.Client, ns string, relativePath string) error { 55 | app, err := parseApplicationYaml(relativePath) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | object := &applicationsv1beta1.Application{} 61 | objectKey := types.NamespacedName{ 62 | Namespace: ns, 63 | Name: app.Name, 64 | } 65 | err = kubeClient.Get(context.TODO(), objectKey, object) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | return kubeClient.Delete(context.TODO(), object) 71 | } 72 | 73 | func parseApplicationYaml(relativePath string) (*applicationsv1beta1.Application, error) { 74 | var manifest *os.File 75 | var err error 76 | 77 | var app applicationsv1beta1.Application 78 | if manifest, err = PathToOSFile(relativePath); err != nil { 79 | return nil, err 80 | } 81 | 82 | decoder := yaml.NewYAMLOrJSONDecoder(manifest, 100) 83 | for { 84 | var out unstructured.Unstructured 85 | err = decoder.Decode(&out) 86 | if err != nil { 87 | // this would indicate it's malformed YAML. 88 | break 89 | } 90 | 91 | if out.GetKind() == "Application" { 92 | var marshaled []byte 93 | marshaled, err = out.MarshalJSON() 94 | _ = json.Unmarshal(marshaled, &app) 95 | break 96 | } 97 | } 98 | 99 | if err != io.EOF && err != nil { 100 | return nil, err 101 | } 102 | return &app, nil 103 | } 104 | -------------------------------------------------------------------------------- /e2e/testutil/customresource.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package testutil 5 | 6 | import ( 7 | "context" 8 | "io" 9 | "os" 10 | "time" 11 | 12 | apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 13 | apiextcs "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 16 | "k8s.io/apimachinery/pkg/util/json" 17 | "k8s.io/apimachinery/pkg/util/wait" 18 | "k8s.io/apimachinery/pkg/util/yaml" 19 | ) 20 | 21 | // CreateOrUpdateCRD - create or update application CRD 22 | func CreateOrUpdateCRD(kubeClient apiextcs.Interface, crd *apiextensions.CustomResourceDefinition) error { 23 | 24 | currentCrd, err := kubeClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), crd.Name, metav1.GetOptions{}) 25 | 26 | if err == nil { 27 | // CustomResourceDefinition already exists -> Update 28 | 29 | // Bypass the 'metadata.resourceVersion: Invalid value: 0x0: must be specified for an update' error 30 | crd.ResourceVersion = currentCrd.ResourceVersion 31 | _, err = kubeClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{}) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | } else { 37 | // CustomResourceDefinition doesn't exist -> Create 38 | _, err = kubeClient.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{}) 39 | if err != nil { 40 | return err 41 | } 42 | } 43 | 44 | return nil 45 | } 46 | 47 | // WaitForCRDOrDie - wait for CRD conditions to be set 48 | func WaitForCRDOrDie(kubeClient apiextcs.Interface, name string) error { 49 | err := wait.PollImmediate(2*time.Second, 20*time.Second, func() (bool, error) { 50 | crd, err := kubeClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{}) 51 | if err != nil { 52 | return false, err 53 | } 54 | return establishedCondition(crd.Status.Conditions), nil 55 | }) 56 | return err 57 | } 58 | 59 | func establishedCondition(conditions []apiextensions.CustomResourceDefinitionCondition) bool { 60 | for _, condition := range conditions { 61 | if condition.Type == apiextensions.Established && condition.Status == apiextensions.ConditionTrue { 62 | return true 63 | } 64 | } 65 | return false 66 | } 67 | 68 | // DeleteCRD - Delete CRD from cluster 69 | func DeleteCRD(kubeClient apiextcs.Interface, crdName string) error { 70 | err := kubeClient.ApiextensionsV1().CustomResourceDefinitions().Delete(context.TODO(), crdName, metav1.DeleteOptions{}) 71 | return err 72 | } 73 | 74 | // ParseCRDYaml - load crd from file 75 | func ParseCRDYaml(relativePath string) (*apiextensions.CustomResourceDefinition, error) { 76 | var manifest *os.File 77 | var err error 78 | 79 | var crd apiextensions.CustomResourceDefinition 80 | if manifest, err = PathToOSFile(relativePath); err != nil { 81 | return nil, err 82 | } 83 | 84 | decoder := yaml.NewYAMLOrJSONDecoder(manifest, 100) 85 | for { 86 | var out unstructured.Unstructured 87 | err = decoder.Decode(&out) 88 | if err != nil { 89 | // this would indicate it's malformed YAML. 90 | break 91 | } 92 | 93 | if out.GetKind() == "CustomResourceDefinition" { 94 | var marshaled []byte 95 | marshaled, err = out.MarshalJSON() 96 | _ = json.Unmarshal(marshaled, &crd) 97 | break 98 | } 99 | } 100 | 101 | if err != io.EOF && err != nil { 102 | return nil, err 103 | } 104 | return &crd, nil 105 | } 106 | -------------------------------------------------------------------------------- /e2e/testutil/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package testutil 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func PathToOSFile(relativPath string) (*os.File, error) { 15 | path, err := filepath.Abs(relativPath) 16 | if err != nil { 17 | return nil, errors.Wrap(err, fmt.Sprintf("failed generate absolut file path of %s", relativPath)) 18 | } 19 | 20 | manifest, err := os.Open(path) 21 | if err != nil { 22 | return nil, errors.Wrap(err, fmt.Sprintf("failed to open file %s", path)) 23 | } 24 | 25 | return manifest, nil 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/application 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-logr/logr v0.1.0 7 | github.com/golang/mock v1.3.1 // indirect 8 | github.com/google/addlicense v0.0.0-20200906110928-a0294312aa76 // indirect 9 | github.com/google/uuid v1.1.1 10 | github.com/onsi/ginkgo v1.11.0 11 | github.com/onsi/gomega v1.8.1 12 | github.com/pkg/errors v0.9.1 13 | k8s.io/api v0.18.2 14 | k8s.io/apiextensions-apiserver v0.18.2 15 | k8s.io/apimachinery v0.18.2 16 | k8s.io/client-go v0.18.2 17 | k8s.io/code-generator v0.18.9 // indirect 18 | sigs.k8s.io/controller-runtime v0.6.0 19 | sigs.k8s.io/controller-tools v0.4.0 // indirect 20 | sigs.k8s.io/kind v0.8.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /hack/tools/common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2020 The Kubernetes Authors. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | 6 | set -o errexit 7 | set -o nounset 8 | set -o pipefail 9 | 10 | arch=amd64 11 | os="unknown" 12 | 13 | if [[ "$OSTYPE" == "linux-gnu" ]]; then 14 | os="linux" 15 | elif [[ "$OSTYPE" == "darwin"* ]]; then 16 | os="darwin" 17 | fi 18 | 19 | if [[ "$os" == "unknown" ]]; then 20 | echo "OS '$OSTYPE' not supported. Aborting." >&2 21 | exit 1 22 | fi 23 | 24 | go_workspace='' 25 | for p in ${GOPATH//:/ }; do 26 | if [[ $PWD/ = $p/* ]]; then 27 | go_workspace=$p 28 | fi 29 | done 30 | 31 | if [ -z $go_workspace ]; then 32 | echo 'Current directory is not in $GOPATH' >&2 33 | exit 1 34 | fi 35 | 36 | # Turn colors in this script off by setting the NO_COLOR variable in your 37 | # environment to any value: 38 | # 39 | # $ NO_COLOR=1 test.sh 40 | NO_COLOR=${NO_COLOR:-""} 41 | if [ -z "$NO_COLOR" ]; then 42 | header=$'\e[1;33m' 43 | reset=$'\e[0m' 44 | else 45 | header='' 46 | reset='' 47 | fi 48 | 49 | function header_text { 50 | echo "$header$*$reset" 51 | } 52 | -------------------------------------------------------------------------------- /hack/tools/install_kubebuilder.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2020 The Kubernetes Authors. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | 6 | source ./common.sh 7 | 8 | version=2.3.1 9 | 10 | header_text "Checking for bin/kubebuilder" 11 | [[ -f bin/kubebuilder ]] && exit 0 12 | 13 | header_text "Installing bin/kubebuilder" 14 | mkdir -p ./bin 15 | curl -L -O "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${version}/kubebuilder_${version}_${os}_${arch}.tar.gz" 16 | 17 | tar -zxvf kubebuilder_${version}_${os}_${arch}.tar.gz 18 | mv kubebuilder_${version}_${os}_${arch}/bin/* bin 19 | 20 | rm kubebuilder_${version}_${os}_${arch}.tar.gz 21 | rm -r kubebuilder_${version}_${os}_${arch} 22 | -------------------------------------------------------------------------------- /hack/tools/install_kustomize.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2020 The Kubernetes Authors. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | 6 | source ./common.sh 7 | 8 | version=3.2.0 9 | 10 | header_text "Checking for bin/kustomize" 11 | [[ -f bin/kustomize ]] && exit 0 12 | 13 | header_text "Installing for bin/kustomize" 14 | mkdir -p ./bin 15 | curl -L https://github.com/kubernetes-sigs/kustomize/releases/download/v${version}/kustomize_${version}_${os}_${arch} -o ./bin/kustomize 16 | chmod +x ./bin/kustomize 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "os" 9 | "time" 10 | 11 | "k8s.io/apimachinery/pkg/runtime" 12 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 13 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 14 | appv1beta1 "sigs.k8s.io/application/api/v1beta1" 15 | "sigs.k8s.io/application/controllers" 16 | ctrl "sigs.k8s.io/controller-runtime" 17 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 18 | // +kubebuilder:scaffold:imports 19 | ) 20 | 21 | var ( 22 | scheme = runtime.NewScheme() 23 | setupLog = ctrl.Log.WithName("setup") 24 | ) 25 | 26 | func init() { 27 | _ = clientgoscheme.AddToScheme(scheme) 28 | 29 | _ = appv1beta1.AddToScheme(scheme) 30 | // +kubebuilder:scaffold:scheme 31 | } 32 | 33 | func main() { 34 | var namespace string 35 | var metricsAddr string 36 | var syncPeriod int64 37 | var enableLeaderElection bool 38 | flag.StringVar(&namespace, "namespace", "", "Namespace within which CRD controller is running.") 39 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 40 | flag.Int64Var(&syncPeriod, "sync-period", 120, "Sync every sync-period seconds.") 41 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 42 | "Enable leader election for controller kube-app-manager. Enabling this will ensure there is only one active controller kube-app-manager.") 43 | flag.Parse() 44 | 45 | ctrl.SetLogger(zap.New(func(o *zap.Options) { 46 | o.Development = true 47 | })) 48 | 49 | syncPeriodD := time.Duration(int64(time.Second) * syncPeriod) 50 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 51 | Scheme: scheme, 52 | MetricsBindAddress: metricsAddr, 53 | LeaderElection: enableLeaderElection, 54 | Port: 9443, 55 | SyncPeriod: &syncPeriodD, 56 | Namespace: namespace, 57 | }) 58 | if err != nil { 59 | setupLog.Error(err, "unable to start kube-app-manager") 60 | os.Exit(1) 61 | } 62 | 63 | if err = (&controllers.ApplicationReconciler{ 64 | Client: mgr.GetClient(), 65 | Mapper: mgr.GetRESTMapper(), 66 | Log: ctrl.Log.WithName("controllers").WithName("Application"), 67 | Scheme: mgr.GetScheme(), 68 | }).SetupWithManager(mgr); err != nil { 69 | setupLog.Error(err, "unable to create controller", "controller", "Application") 70 | os.Exit(1) 71 | } 72 | // +kubebuilder:scaffold:builder 73 | 74 | setupLog.Info("starting kube-app-manager") 75 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 76 | setupLog.Error(err, "problem running kube-app-manager") 77 | os.Exit(1) 78 | } 79 | } 80 | --------------------------------------------------------------------------------