├── .dockerignore ├── .github └── workflows │ ├── build-and-push.yaml │ └── lint.yaml ├── .gitignore ├── .prettierignore ├── Dockerfile ├── Dockerfile-multiarch ├── Dockerfile.hook ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1alpha1 │ ├── apphook_types.go │ ├── groupversion_info.go │ └── zz_generated.deepcopy.go ├── cmd ├── apphook │ └── main.go └── mysql_demo │ └── main.go ├── config ├── crd │ ├── bases │ │ └── ys.jibudata.com_apphooks.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_apphooks.yaml │ │ └── webhook_in_apphooks.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ └── manager_config_patch.yaml ├── manager │ ├── controller_manager_config.yaml │ ├── kustomization.yaml │ └── manager.yaml ├── manifests │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── apphook_editor_role.yaml │ ├── apphook_viewer_role.yaml │ ├── auth_proxy_client_clusterrole.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 │ └── service_account.yaml ├── samples │ ├── kustomization.yaml │ ├── sample-secret.yaml │ └── ys_v1alpha1_apphook.yaml └── scorecard │ ├── bases │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ ├── basic.config.yaml │ └── olm.config.yaml ├── controllers ├── apphook_controller.go ├── driver │ └── driver.go ├── hook_resource.go ├── suite_test.go └── util │ └── k8sutil.go ├── deploy ├── apiextensions.k8s.io_v1_customresourcedefinition_apphooks.ys.jibudata.com.yaml ├── apps_v1_deployment_amberapp-controller-manager.yaml ├── rbac.authorization.k8s.io_v1_clusterrole_amberapp-manager-role.yaml ├── rbac.authorization.k8s.io_v1_clusterrole_amberapp-metrics-reader.yaml ├── rbac.authorization.k8s.io_v1_clusterrole_amberapp-proxy-role.yaml ├── rbac.authorization.k8s.io_v1_clusterrolebinding_amberapp-manager-rolebinding.yaml ├── rbac.authorization.k8s.io_v1_clusterrolebinding_amberapp-proxy-rolebinding.yaml ├── rbac.authorization.k8s.io_v1_role_amberapp-leader-election-role.yaml ├── rbac.authorization.k8s.io_v1_rolebinding_amberapp-leader-election-rolebinding.yaml ├── v1_namespace_amberapp-system.yaml ├── v1_service_amberapp-controller-manager-metrics-service.yaml ├── v1_serviceaccount_amberapp-controller-manager.yaml └── ys1000 │ └── deployments.yaml ├── examples ├── mongodb-sample.yaml ├── mysql-deployment.yaml ├── mysql-generator │ ├── apps.yaml │ ├── common │ │ ├── __pycache__ │ │ │ ├── log.cpython-36.pyc │ │ │ └── rest_api_client.cpython-36.pyc │ │ ├── log.py │ │ └── rest_api_client.py │ └── db-generator.py ├── mysql-sample.yaml ├── postgres-sample.yaml ├── v1_deployment_amberapp-mysql-demo.yaml └── workload │ └── helm-mysql │ ├── README.md │ └── mysql-values.yaml ├── go.mod ├── go.sum ├── hack ├── boilerplate.go.txt ├── build-image.sh ├── create.sh ├── prepare.sh ├── push-image.sh ├── quiesce.sh └── unquiesce.sh ├── main.go └── pkg ├── appconfig └── appconfig.go ├── client └── client.go ├── cmd ├── apphook │ └── apphook.go ├── create │ └── create.go ├── delete │ └── delete.go ├── errors.go ├── quiesce │ └── quiesce.go └── unquiesce │ └── unquiesce.go ├── mongo └── mongo.go ├── mysql └── mysql.go ├── postgres └── postgres.go ├── redis └── redis.go └── util └── util.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /.github/workflows/build-and-push.yaml: -------------------------------------------------------------------------------- 1 | name: Build Images 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'tag' 7 | required: true 8 | default: 'main-latest' 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up QEMU 15 | uses: docker/setup-qemu-action@v2 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v2 18 | - name: Login to Alicloud Docker registry 19 | uses: docker/login-action@v2 20 | with: 21 | registry: registry.cn-shanghai.aliyuncs.com 22 | username: ${{ secrets.ALI_REGISTRY_USER }} 23 | password: ${{ secrets.ALI_REGISTRY_PASS }} 24 | - name: Build and push Docker images 25 | run: | 26 | GOPROXY=https://proxy.golang.org,direct make docker-pushx -e VERSION=${VERSION} 27 | env: 28 | VERSION: ${{ inputs.version }} 29 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main, 'release**'] 7 | 8 | jobs: 9 | golangci: 10 | name: lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/setup-go@v3 14 | with: 15 | go-version: 1.19 16 | - uses: actions/checkout@v3 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v3 19 | with: 20 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 21 | version: latest 22 | args: --timeout=10m -v 23 | - name: vet 24 | run: | 25 | make vet 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | vendor 11 | VERSION 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Kubernetes Generated files - skip generated files, except for vendored files 20 | 21 | !vendor/**/zz_generated.* 22 | 23 | # editor and IDE paraphernalia 24 | .idea 25 | *.swp 26 | *.swo 27 | *~ 28 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.yaml -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.19 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go env -w GO111MODULE=on 11 | RUN go env -w GOPROXY=https://goproxy.cn,direct 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY main.go main.go 16 | COPY api/ api/ 17 | COPY controllers/ controllers/ 18 | COPY pkg/ pkg/ 19 | 20 | # Build 21 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go 22 | 23 | # Use distroless as minimal base image to package the manager binary 24 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 25 | # FROM gcr.io/distroless/static:nonroot 26 | #FROM quay.io/shdn/distrolessstatic:nonroot 27 | FROM jibutech/ubi8-minimal:latest 28 | WORKDIR / 29 | COPY --from=builder /workspace/manager . 30 | USER 65532:65532 31 | 32 | ENTRYPOINT ["/manager"] 33 | -------------------------------------------------------------------------------- /Dockerfile-multiarch: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM --platform=${TARGETPLATFORM} golang:1.17 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go env -w GO111MODULE=on 11 | RUN go env -w GOPROXY=https://goproxy.cn,direct 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY main.go main.go 16 | COPY api/ api/ 17 | COPY controllers/ controllers/ 18 | COPY pkg/ pkg/ 19 | 20 | # Build 21 | ARG TARGETARCH 22 | ARG TARGETOS 23 | RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -a -o manager main.go 24 | 25 | FROM --platform=${TARGETPLATFORM} redhat/ubi8-minimal:latest 26 | WORKDIR / 27 | COPY --from=builder /workspace/manager . 28 | USER 65532:65532 29 | 30 | ENTRYPOINT ["/manager"] 31 | -------------------------------------------------------------------------------- /Dockerfile.hook: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.15 as builder 3 | 4 | ENV GOPROXY=https://goproxy.cn,direct 5 | #ARG GOPROXY 6 | 7 | WORKDIR /workspace 8 | # Copy the Go Modules manifests 9 | COPY go.mod go.mod 10 | COPY go.sum go.sum 11 | # cache deps before building and copying source so that we don't need to re-download as much 12 | # and so that source changes don't invalidate our downloaded layer 13 | RUN go mod download 14 | # Copy the go source 15 | COPY cmd/apphook/main.go cmd/apphook/main.go 16 | COPY cmd/mysql_demo/main.go cmd/mysql_demo/main.go 17 | COPY api/ api/ 18 | #COPY controllers/ controllers/ 19 | COPY pkg/ pkg/ 20 | COPY hack/ hack/ 21 | # COPY vendor/ vendor/ 22 | 23 | 24 | # Build 25 | ENV BUILDTAGS containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp exclude_graphdriver_overlay 26 | RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags "$BUILDTAGS" -a -o apphook cmd/apphook/main.go 27 | RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags "$BUILDTAGS" -a -o mysqlops cmd/mysql_demo/main.go 28 | 29 | # Use distroless as minimal base image to package the manager binary 30 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 31 | 32 | # Download gcr.io/xxx images with following commands 33 | # curl -s https://zhangguanzhang.github.io/bash/pull.sh | bash -s -- gcr.io/distroless/static:nonroot 34 | # FROM registry.access.redhat.com/ubi8/ubi-minimal:latest 35 | #FROM jibutech/ubi8-minimal:latest 36 | FROM centos:7 37 | 38 | WORKDIR / 39 | COPY --from=builder /workspace/apphook . 40 | COPY --from=builder /workspace/mysqlops . 41 | COPY --from=builder /workspace/hack/create.sh . 42 | COPY --from=builder /workspace/hack/quiesce.sh . 43 | COPY --from=builder /workspace/hack/unquiesce.sh . 44 | 45 | ENTRYPOINT ["/bin/sleep", "infinity"] 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | REPO = registry.cn-shanghai.aliyuncs.com 4 | NAMESPACE = jibutech 5 | IMG_NAME = amberapp 6 | HOOK_IMG_NAME = app-hook 7 | VERSION ?= $(shell git rev-parse --abbrev-ref HEAD).$(shell git rev-parse --short HEAD) 8 | DEFAULT_DEPLOY_NS = amberapp-system 9 | JIBU_DEPLOY_NS = qiming-backend 10 | 11 | CHANNELS="stable-v1" 12 | DEFAULT_CHANNEL="stable-v1" 13 | 14 | ifneq ($(origin CHANNELS), undefined) 15 | BUNDLE_CHANNELS := --channels=$(CHANNELS) 16 | endif 17 | 18 | # DEFAULT_CHANNEL defines the default channel used in the bundle. 19 | # Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") 20 | # To re-generate a bundle for any other default channel without changing the default setup, you can: 21 | # - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) 22 | # - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") 23 | ifneq ($(origin DEFAULT_CHANNEL), undefined) 24 | BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) 25 | endif 26 | BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) 27 | 28 | 29 | IMAGE_TAG_BASE = $(REPO)/$(NAMESPACE)/$(IMG_NAME) 30 | IMG = $(IMAGE_TAG_BASE):$(VERSION) 31 | BUNDLE_IMG = $(IMAGE_TAG_BASE)-bundle:$(VERSION) 32 | 33 | HOOK_IMAGE_TAG_BASE = $(REPO)/$(NAMESPACE)/$(HOOK_IMG_NAME) 34 | HOOK_IMG = $(HOOK_IMAGE_TAG_BASE):$(VERSION) 35 | 36 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 37 | ifeq (,$(shell go env GOBIN)) 38 | GOBIN=$(shell go env GOPATH)/bin 39 | else 40 | GOBIN=$(shell go env GOBIN) 41 | endif 42 | 43 | # Setting SHELL to bash allows bash commands to be executed by recipes. 44 | # This is a requirement for 'setup-envtest.sh' in the test target. 45 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 46 | SHELL = /usr/bin/env bash -o pipefail 47 | .SHELLFLAGS = -ec 48 | 49 | all: build 50 | 51 | ##@ General 52 | 53 | # The help target prints out all targets with their descriptions organized 54 | # beneath their categories. The categories are represented by '##@' and the 55 | # target descriptions by '##'. The awk commands is responsible for reading the 56 | # entire set of makefiles included in this invocation, looking for lines of the 57 | # file as xyz: ## something, and then pretty-format the target and help. Then, 58 | # if there's a line with ##@ something, that gets pretty-printed as a category. 59 | # More info on the usage of ANSI control characters for terminal formatting: 60 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 61 | # More info on the awk command: 62 | # http://linuxcommand.org/lc3_adv_awk.php 63 | 64 | help: ## Display this help. 65 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 66 | 67 | ##@ Development 68 | 69 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 70 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 71 | 72 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 73 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 74 | 75 | fmt: ## Run go fmt against code. 76 | go fmt ./... 77 | 78 | vet: ## Run go vet against code. 79 | go vet ./... 80 | 81 | .PHONY: lint 82 | lint: golangci-lint 83 | $(golangci-lint) run --timeout=5m 84 | 85 | test: manifests generate fmt vet envtest ## Run tests. 86 | go test ./... -coverprofile cover.out 87 | 88 | ##@ Build 89 | 90 | build: generate fmt vet ## Build manager binary. 91 | go build -o bin/manager main.go 92 | go build -o bin/apphook cmd/apphook/main.go 93 | go build -o bin/mysqlops cmd/mysql_demo/main.go 94 | 95 | run: manifests generate fmt vet ## Run a controller from your host. 96 | go run ./main.go 97 | 98 | docker-buildx: test ## Build docker image with the manager. 99 | docker buildx build --platform linux/amd64,linux/arm64 -f Dockerfile-multiarch -t ${IMG} . 100 | docker-pushx: ## Push docker image with the manager. 101 | docker buildx build --platform linux/amd64,linux/arm64 -f Dockerfile-multiarch -t ${IMG} . --push 102 | 103 | docker-build: #test ## Build docker image with the manager. 104 | docker build -t ${IMG} . 105 | 106 | docker-push: ## Push docker image with the manager. 107 | docker push ${IMG} 108 | 109 | docker-build-hook-image: test ## Build docker image with the manager. 110 | docker build -f Dockerfile.hook -t ${HOOK_IMG} . 111 | 112 | docker-push-hook-image: ## Push docker image with the manager. 113 | docker push ${HOOK_IMG} 114 | 115 | ##@ Deployment 116 | 117 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 118 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 119 | cp bin/apphook /usr/local/go/bin/ 120 | 121 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. 122 | $(KUSTOMIZE) build config/crd | kubectl delete -f - 123 | 124 | deploy: manifests kustomize set-default-ns## Deploy controller to the K8s cluster specified in ~/.kube/config. 125 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 126 | $(KUSTOMIZE) build config/default | kubectl apply -f - 127 | 128 | ys1000-deploy: manifests kustomize set-jibudata-ns## Deploy controller to the K8s cluster specified in ~/.kube/config. 129 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 130 | $(KUSTOMIZE) build config/default > deploy/ys1000/deployments.yaml 131 | 132 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. 133 | $(KUSTOMIZE) build config/default | kubectl delete -f - 134 | 135 | generate-deployment: set-default-ns ## generate installation crd and deployment 136 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 137 | rm -rf deploy && mkdir deploy && mkdir deploy/ys1000 138 | $(KUSTOMIZE) build config/default -o deploy/ 139 | 140 | generate-all: build manifests kustomize generate-deployment ys1000-deploy 141 | 142 | CONTROLLER_GEN = $(shell pwd)/bin/controller-gen 143 | controller-gen: ## Download controller-gen locally if necessary. 144 | $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0) 145 | 146 | KUSTOMIZE = $(shell pwd)/bin/kustomize 147 | kustomize: ## Download kustomize locally if necessary. 148 | $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7) 149 | 150 | ENVTEST = $(shell pwd)/bin/setup-envtest 151 | envtest: ## Download envtest-setup locally if necessary. 152 | $(call go-get-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest) 153 | $(ENVTEST) use 1.21 154 | 155 | golangci-lint = $(shell pwd)/bin/golangci-lint 156 | .PHONY: golangci-lint 157 | golangci-lint: ## Download golangci-lint locally if necessary. 158 | $(call go-get-tool,$(golangci-lint),github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1) 159 | 160 | set-default-ns: 161 | cd config/manager && $(KUSTOMIZE) edit set namespace ${DEFAULT_DEPLOY_NS} 162 | cd config/default && $(KUSTOMIZE) edit set namespace ${DEFAULT_DEPLOY_NS} 163 | 164 | set-jibudata-ns: 165 | cd config/manager && $(KUSTOMIZE) edit set namespace ${JIBU_DEPLOY_NS} 166 | cd config/default && $(KUSTOMIZE) edit set namespace ${JIBU_DEPLOY_NS} 167 | 168 | # go-get-tool will 'go get' any package $2 and install it to $1. 169 | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) 170 | define go-get-tool 171 | @[ -f $(1) ] || { \ 172 | set -e ;\ 173 | TMP_DIR=$$(mktemp -d) ;\ 174 | cd $$TMP_DIR ;\ 175 | go mod init tmp ;\ 176 | echo "Downloading $(2)" ;\ 177 | GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\ 178 | rm -rf $$TMP_DIR ;\ 179 | } 180 | endef 181 | 182 | .PHONY: bundle 183 | bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files. 184 | operator-sdk generate kustomize manifests -q 185 | cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) 186 | $(KUSTOMIZE) build config/manifests | operator-sdk generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) 187 | operator-sdk bundle validate ./bundle 188 | 189 | .PHONY: bundle-build 190 | bundle-build: ## Build the bundle image. 191 | docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . 192 | 193 | .PHONY: bundle-push 194 | bundle-push: ## Push the bundle image. 195 | $(MAKE) docker-push IMG=$(BUNDLE_IMG) 196 | 197 | .PHONY: opm 198 | OPM = ./bin/opm 199 | opm: ## Download opm locally if necessary. 200 | ifeq (,$(wildcard $(OPM))) 201 | ifeq (,$(shell which opm 2>/dev/null)) 202 | @{ \ 203 | set -e ;\ 204 | mkdir -p $(dir $(OPM)) ;\ 205 | OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ 206 | curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.15.1/$${OS}-$${ARCH}-opm ;\ 207 | chmod +x $(OPM) ;\ 208 | } 209 | else 210 | OPM = $(shell which opm) 211 | endif 212 | endif 213 | 214 | # A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0). 215 | # These images MUST exist in a registry and be pull-able. 216 | BUNDLE_IMGS ?= $(BUNDLE_IMG) 217 | 218 | # The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). 219 | CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:$(VERSION) 220 | 221 | # Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. 222 | ifneq ($(origin CATALOG_BASE_IMG), undefined) 223 | FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) 224 | endif 225 | 226 | # Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. 227 | # This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: 228 | # https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator 229 | .PHONY: catalog-build 230 | catalog-build: opm ## Build a catalog image. 231 | $(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) 232 | 233 | # Push the catalog image. 234 | .PHONY: catalog-push 235 | catalog-push: ## Push a catalog image. 236 | $(MAKE) docker-push IMG=$(CATALOG_IMG) 237 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: jibudata.com 2 | layout: 3 | - go.kubebuilder.io/v3 4 | plugins: 5 | manifests.sdk.operatorframework.io/v2: {} 6 | scorecard.sdk.operatorframework.io/v2: {} 7 | projectName: amberapp 8 | repo: github.com/jibudata/amberapp 9 | resources: 10 | - api: 11 | crdVersion: v1 12 | namespaced: true 13 | controller: true 14 | domain: jibudata.com 15 | group: ys 16 | kind: AppHook 17 | path: github.com/jibudata/amberapp/api/v1alpha1 18 | version: v1alpha1 19 | version: "3" 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AmberApp 2 | 3 | AmberApp is a K8s native framework for application consistency that can work together with Velero and other backup solutions. It will lock databases when backup PVCs. 4 | 5 | ![overview](https://gitee.com/jibutech/tech-docs/raw/master/images/amberapp-architecture.png) 6 | 7 | ## Installation 8 | 9 | 1. Clone the repo 10 | 11 | `git clone git@github.com:jibudata/amberapp.git` 12 | 2. Enter the repo and run 13 | 14 | `kubectl apply -f deploy` 15 | 16 | ## Supported databases 17 | 18 | | # | Type | Databases required | lock method | description | 19 | | -- | ------------ | ------------------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 20 | | 1. | PostgreSQL | y | pg_start_backup | no impact on CRUD | 21 | | 2. | MongoDB | n | fsync lock | lock all DBs in current user, db modify operatrion will hang until unquiesced | 22 | | 3. | MySQL | y | FLUSH TABLES WITH READ LOCK | lock all DBs, cannot create new table, insert or modify data until unquiesced | 23 | | | MySQL > 8.0 | y | LOCK INSTANCE FOR BACKUP | lock current DB, Cannot create, rename or, remove records. Cannot repair, truncate and optimize tables. Can perform DDL operations hat only affect user-created temporary tables. Can create, rename, remove temporary tables. Can create binary log files. | 24 | | 4. | Redis >= 2.4 | n | - | `standalone` and `cluster` mode support for now, no impact on CRUD, use `bgsave` for rbd snapshot or disable `auto aof rewrite` before backup to guarantee consistent aof log | 25 | 26 | ## Usage 27 | 28 | ### CLI example 29 | 30 | 1. Clone repo, do install as above, run `make` to build binaries 31 | 2. Deploy an example application: wordpress, refer to [https://github.com/jibutech/docs/tree/main/examples/workload/wordpress](https://github.com/jibutech/docs/tree/main/examples/workload/wordpress) 32 | 3. Create an hook to MySQL database. NOTE: use `WATCH_NAMESPACE` to specify the namespace where amberapp operator is installed. 33 | 34 | ```bash 35 | # export WATCH_NAMESPACE=amberapp-system 36 | # bin/apphook create -n test -a mysql -e "wordpress-mysql.wordpress" -u root -p passw0rd --databases mysql 37 | 38 | # kubectl get apphooks.ys.jibudata.com -n amberapp-system test-hook 39 | NAME AGE CREATED AT PHASE 40 | test-hook 8s 2021-10-20T12:26:28Z Ready 41 | ``` 42 | 4. Quiesce DB: 43 | 44 | ```bash 45 | # bin/apphook quiesce -n test -w 46 | 47 | # kubectl get apphooks.ys.jibudata.com -n amberapp-system test-hook 48 | test-hook 18m 2021-10-20T12:26:28Z Quiesced 49 | ``` 50 | 5. Unquiesce DB: 51 | 52 | ```bash 53 | # bin/apphook unquiesce -n test 54 | 55 | # kubectl get apphooks.ys.jibudata.com -n amberapp-system test-hook 56 | test-hook 18m 2021-10-20T12:26:28Z Unquiesced 57 | ``` 58 | 6. Delete hook: 59 | 60 | ```bash 61 | # bin/apphook delete -n test 62 | ``` 63 | 64 | ### Use CR 65 | 66 | Other backup solution can use CR for API level integration with AmberApp, below are CR details. 67 | 68 | #### CR spec 69 | 70 | | Param | Type | Supported values | Description | 71 | | -------------- | ---------------------- | ----------------------------------------------------- | -------------------------------------------- | 72 | | appProvider | string | Postgres / Mongodb / MySql / Redis | DB type | 73 | | endPoint | string | serviceName.namespace | Endpoint to connect the applicatio service | 74 | | databases | []string | any | database name array | 75 | | operationType | string | quiesce / unquiesce | | 76 | | timeoutSeconds | \*int32 | >=0 | timeout of operation | 77 | | secret | corev1.SecretReference | name: xxx, namespace: xxx | Secret to access the database | 78 | | params | map[string]string | mysql-lock-method: table, mysql-lock-method: instance | additional parameters for Mysql DB operation | 79 | | | | redis-backup-method: rdb, redis-backup-method: aof | additional parameters for Redis DB operation | 80 | 81 | #### Status 82 | 83 | | status | Description | 84 | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | 85 | | Created | CR is just created with operationType is empty. This status is short, as manager is connecting and will update the status to ready/not ready | 86 | | Ready | driver manager connected to database successfully. only in Ready status, user can do quiesce operation | 87 | | Not Ready | driver manager failed to connect database. user need to check the spec and fill the correct info | 88 | | Quiesce In Progress | driver is trying to quiesce database | 89 | | Quiesced | databases are successfully quiesced | 90 | | Unquiesce In Progress | driver is trying to unquiesce database | 91 | | Unquiesced | databases are successfully unquiesced | 92 | 93 | ## Development 94 | 95 | 1. generate all resources 96 | 97 | ```bash 98 | make generate-all -e VERSION=0.1.0 99 | ``` 100 | 2. build docker image 101 | 102 | ```bash 103 | make docker-build -e VERSION=0.1.0 104 | ``` 105 | multiple arch image 106 | 107 | ```bash 108 | make docker-pushx -e VERSION=0.1.0 109 | ``` 110 | 3. deploy 111 | 112 | ```bash 113 | make deploy -e VERSION=0.1.0 114 | ``` 115 | -------------------------------------------------------------------------------- /api/v1alpha1/apphook_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package v1alpha1 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // Params key 25 | const ( 26 | QuiesceFromPrimary = "QuiesceFromPrimary" 27 | ) 28 | 29 | const ( 30 | // common params 31 | LockMethod = "lock-method" 32 | BackupMethod = "backup-method" 33 | 34 | // mysql param 35 | MysqlTableLock = "table" 36 | MysqlInstanceLock = "instance" 37 | 38 | // redis param 39 | RedisBackupMethodByRDB = "rdb" 40 | RedisBackupMethodByAOF = "aof" 41 | ) 42 | 43 | // AppHookSpec defines the desired state of AppHook 44 | type AppHookSpec struct { 45 | // Name is a job for backup/restore/migration 46 | Name string `json:"name"` 47 | // AppProvider is the application identifier for different vendors, such as mysql 48 | AppProvider string `json:"appProvider,omitempty"` 49 | // Endpoint to connect the applicatio service 50 | EndPoint string `json:"endPoint,omitempty"` 51 | // Databases 52 | Databases []string `json:"databases,omitempty"` 53 | // OperationType is the operation executed in application 54 | //+kubebuilder:validation:Enum=quiesce;unquiesce 55 | OperationType string `json:"operationType,omitempty"` 56 | // TimeoutSeconds is the timeout of operation 57 | //+kubebuilder:validation:Minimum=0 58 | //+kubebuilder:default:0 59 | TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` 60 | // Secret to access the application 61 | Secret corev1.SecretReference `json:"secret,omitempty"` 62 | // Other options 63 | Params map[string]string `json:"params,omitempty"` 64 | } 65 | 66 | type QuiesceResult struct { 67 | Mongo *MongoResult `json:"mongo,omitempty"` 68 | Mysql *MysqlResult `json:"mysql,omitempty"` 69 | Pg *PgResult `json:"pg,omitempty"` 70 | Redis *RedisResult `json:"redis,omitempty"` 71 | } 72 | 73 | type MongoResult struct { 74 | MongoEndpoint string `json:"mongoEndpoint,omitempty"` 75 | IsPrimary bool `json:"isPrimary,omitempty"` 76 | } 77 | 78 | type MysqlResult struct { 79 | } 80 | 81 | type PgResult struct { 82 | } 83 | 84 | type RedisResult struct { 85 | } 86 | 87 | // PreservedConfig saves the origin params before change by quiesce 88 | type PreservedConfig struct { 89 | Params map[string]string `json:"params,omitempty"` 90 | } 91 | 92 | // AppHookStatus defines the observed state of AppHook 93 | // +kubebuilder:subresource:status 94 | type AppHookStatus struct { 95 | Phase string `json:"phase,omitempty"` 96 | ErrMsg string `json:"errMsg,omitempty"` 97 | QuiescedTimestamp *metav1.Time `json:"quiescedTimestamp,omitempty"` 98 | Result *QuiesceResult `json:"result,omitempty"` 99 | PreservedConfig *PreservedConfig `json:"preservedConfig,omitempty"` 100 | } 101 | 102 | //+kubebuilder:object:root=true 103 | //+kubebuilder:subresource:status 104 | //+kubebuilder:printcolumn:name="Age",type=date,JSONPath=.metadata.creationTimestamp 105 | //+kubebuilder:printcolumn:name="Created At",type=string,JSONPath=.metadata.creationTimestamp 106 | //+kubebuilder:printcolumn:name="Phase",type=string,JSONPath=.status.phase,description="Phase" 107 | 108 | // AppHook is the Schema for the apphooks API 109 | type AppHook struct { 110 | metav1.TypeMeta `json:",inline"` 111 | metav1.ObjectMeta `json:"metadata,omitempty"` 112 | 113 | Spec AppHookSpec `json:"spec,omitempty"` 114 | Status AppHookStatus `json:"status,omitempty"` 115 | } 116 | 117 | //+kubebuilder:object:root=true 118 | 119 | // AppHookList contains a list of AppHook 120 | type AppHookList struct { 121 | metav1.TypeMeta `json:",inline"` 122 | metav1.ListMeta `json:"metadata,omitempty"` 123 | Items []AppHook `json:"items"` 124 | } 125 | 126 | // operation type 127 | const ( 128 | // quiesce operation 129 | QUIESCE = "quiesce" 130 | // unquiesce operation 131 | UNQUIESCE = "unquiesce" 132 | ) 133 | 134 | // phase 135 | const ( 136 | HookCreated = "Created" 137 | HookReady = "Ready" 138 | HookNotReady = "NotReady" 139 | HookQUIESCEINPROGRESS = "Quiesce In Progress" 140 | HookQUIESCED = "Quiesced" 141 | HookUNQUIESCEINPROGRESS = "Unquiesce In Progress" 142 | HookUNQUIESCED = "Unquiesced" 143 | ) 144 | 145 | func init() { 146 | SchemeBuilder.Register(&AppHook{}, &AppHookList{}) 147 | } 148 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | // Package v1alpha1 contains API Schema definitions for the v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=ys.jibudata.com 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "ys.jibudata.com", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2021. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by controller-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 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 *AppHook) DeepCopyInto(out *AppHook) { 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 AppHook. 38 | func (in *AppHook) DeepCopy() *AppHook { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(AppHook) 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 *AppHook) 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 *AppHookList) DeepCopyInto(out *AppHookList) { 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([]AppHook, 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 AppHookList. 70 | func (in *AppHookList) DeepCopy() *AppHookList { 71 | if in == nil { 72 | return nil 73 | } 74 | out := new(AppHookList) 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 *AppHookList) 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 *AppHookSpec) DeepCopyInto(out *AppHookSpec) { 89 | *out = *in 90 | if in.Databases != nil { 91 | in, out := &in.Databases, &out.Databases 92 | *out = make([]string, len(*in)) 93 | copy(*out, *in) 94 | } 95 | if in.TimeoutSeconds != nil { 96 | in, out := &in.TimeoutSeconds, &out.TimeoutSeconds 97 | *out = new(int32) 98 | **out = **in 99 | } 100 | out.Secret = in.Secret 101 | if in.Params != nil { 102 | in, out := &in.Params, &out.Params 103 | *out = make(map[string]string, len(*in)) 104 | for key, val := range *in { 105 | (*out)[key] = val 106 | } 107 | } 108 | } 109 | 110 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppHookSpec. 111 | func (in *AppHookSpec) DeepCopy() *AppHookSpec { 112 | if in == nil { 113 | return nil 114 | } 115 | out := new(AppHookSpec) 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 *AppHookStatus) DeepCopyInto(out *AppHookStatus) { 122 | *out = *in 123 | if in.QuiescedTimestamp != nil { 124 | in, out := &in.QuiescedTimestamp, &out.QuiescedTimestamp 125 | *out = (*in).DeepCopy() 126 | } 127 | if in.Result != nil { 128 | in, out := &in.Result, &out.Result 129 | *out = new(QuiesceResult) 130 | (*in).DeepCopyInto(*out) 131 | } 132 | if in.PreservedConfig != nil { 133 | in, out := &in.PreservedConfig, &out.PreservedConfig 134 | *out = new(PreservedConfig) 135 | (*in).DeepCopyInto(*out) 136 | } 137 | } 138 | 139 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppHookStatus. 140 | func (in *AppHookStatus) DeepCopy() *AppHookStatus { 141 | if in == nil { 142 | return nil 143 | } 144 | out := new(AppHookStatus) 145 | in.DeepCopyInto(out) 146 | return out 147 | } 148 | 149 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 150 | func (in *MongoResult) DeepCopyInto(out *MongoResult) { 151 | *out = *in 152 | } 153 | 154 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoResult. 155 | func (in *MongoResult) DeepCopy() *MongoResult { 156 | if in == nil { 157 | return nil 158 | } 159 | out := new(MongoResult) 160 | in.DeepCopyInto(out) 161 | return out 162 | } 163 | 164 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 165 | func (in *MysqlResult) DeepCopyInto(out *MysqlResult) { 166 | *out = *in 167 | } 168 | 169 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MysqlResult. 170 | func (in *MysqlResult) DeepCopy() *MysqlResult { 171 | if in == nil { 172 | return nil 173 | } 174 | out := new(MysqlResult) 175 | in.DeepCopyInto(out) 176 | return out 177 | } 178 | 179 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 180 | func (in *PgResult) DeepCopyInto(out *PgResult) { 181 | *out = *in 182 | } 183 | 184 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PgResult. 185 | func (in *PgResult) DeepCopy() *PgResult { 186 | if in == nil { 187 | return nil 188 | } 189 | out := new(PgResult) 190 | in.DeepCopyInto(out) 191 | return out 192 | } 193 | 194 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 195 | func (in *PreservedConfig) DeepCopyInto(out *PreservedConfig) { 196 | *out = *in 197 | if in.Params != nil { 198 | in, out := &in.Params, &out.Params 199 | *out = make(map[string]string, len(*in)) 200 | for key, val := range *in { 201 | (*out)[key] = val 202 | } 203 | } 204 | } 205 | 206 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PreservedConfig. 207 | func (in *PreservedConfig) DeepCopy() *PreservedConfig { 208 | if in == nil { 209 | return nil 210 | } 211 | out := new(PreservedConfig) 212 | in.DeepCopyInto(out) 213 | return out 214 | } 215 | 216 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 217 | func (in *QuiesceResult) DeepCopyInto(out *QuiesceResult) { 218 | *out = *in 219 | if in.Mongo != nil { 220 | in, out := &in.Mongo, &out.Mongo 221 | *out = new(MongoResult) 222 | **out = **in 223 | } 224 | if in.Mysql != nil { 225 | in, out := &in.Mysql, &out.Mysql 226 | *out = new(MysqlResult) 227 | **out = **in 228 | } 229 | if in.Pg != nil { 230 | in, out := &in.Pg, &out.Pg 231 | *out = new(PgResult) 232 | **out = **in 233 | } 234 | if in.Redis != nil { 235 | in, out := &in.Redis, &out.Redis 236 | *out = new(RedisResult) 237 | **out = **in 238 | } 239 | } 240 | 241 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuiesceResult. 242 | func (in *QuiesceResult) DeepCopy() *QuiesceResult { 243 | if in == nil { 244 | return nil 245 | } 246 | out := new(QuiesceResult) 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 *RedisResult) DeepCopyInto(out *RedisResult) { 253 | *out = *in 254 | } 255 | 256 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisResult. 257 | func (in *RedisResult) DeepCopy() *RedisResult { 258 | if in == nil { 259 | return nil 260 | } 261 | out := new(RedisResult) 262 | in.DeepCopyInto(out) 263 | return out 264 | } 265 | -------------------------------------------------------------------------------- /cmd/apphook/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | 23 | "k8s.io/klog/v2" 24 | 25 | "github.com/jibudata/amberapp/pkg/cmd" 26 | "github.com/jibudata/amberapp/pkg/cmd/apphook" 27 | ) 28 | 29 | func main() { 30 | defer klog.Flush() 31 | 32 | baseName := filepath.Base(os.Args[0]) 33 | 34 | c, err := apphook.NewCommand(baseName) 35 | cmd.CheckError(err) 36 | cmd.CheckError(c.Execute()) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/mysql_demo/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "database/sql" 22 | "fmt" 23 | "os" 24 | "path/filepath" 25 | "time" 26 | 27 | _ "github.com/go-sql-driver/mysql" 28 | "github.com/spf13/cobra" 29 | "github.com/spf13/pflag" 30 | corev1 "k8s.io/api/core/v1" 31 | "k8s.io/apimachinery/pkg/runtime" 32 | "k8s.io/apimachinery/pkg/types" 33 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 34 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 35 | "k8s.io/klog/v2" 36 | 37 | "github.com/jibudata/amberapp/api/v1alpha1" 38 | "github.com/jibudata/amberapp/pkg/client" 39 | "github.com/jibudata/amberapp/pkg/cmd" 40 | "github.com/jibudata/amberapp/pkg/util" 41 | ) 42 | 43 | const ( 44 | DefaultInterval = 250 * time.Millisecond 45 | ) 46 | 47 | const ( 48 | StressOperationReplicate = "replicate" 49 | ) 50 | 51 | const ( 52 | NewTableSuffix = "_backup" 53 | ) 54 | 55 | var ( 56 | scheme = runtime.NewScheme() 57 | ) 58 | 59 | type DBConfig struct { 60 | db *sql.DB 61 | Provider string 62 | Endpoint string 63 | Database string 64 | UserName string 65 | Password string 66 | } 67 | 68 | func init() { 69 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 70 | utilruntime.Must(v1alpha1.AddToScheme(scheme)) 71 | } 72 | 73 | func NewCommand(baseName string) (*cobra.Command, error) { 74 | 75 | kubeconfig, err := client.NewConfig() 76 | if err != nil { 77 | return nil, err 78 | } 79 | kubeclient, err := client.NewClient(kubeconfig, scheme) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | option := &DemoOptions{} 85 | 86 | c := &cobra.Command{ 87 | Use: "mysqlops", 88 | Short: "Do some SQL operations", 89 | Long: "Do some SQL operations", 90 | Run: func(c *cobra.Command, args []string) { 91 | cmd.CheckError(option.Validate(c, kubeclient)) 92 | cmd.CheckError(option.Run(kubeclient)) 93 | }, 94 | } 95 | 96 | option.BindFlags(c.Flags(), c) 97 | 98 | return c, nil 99 | } 100 | 101 | type DemoOptions struct { 102 | HookName string 103 | Database string 104 | TableName string 105 | Operation string 106 | NumLoops int 107 | } 108 | 109 | func (d *DemoOptions) BindFlags(flags *pflag.FlagSet, c *cobra.Command) { 110 | flags.StringVarP(&d.HookName, "name", "n", "", "database hook name") 111 | _ = c.MarkFlagRequired("name") 112 | flags.StringVarP(&d.Database, "database", "d", "", "name of the database instance") 113 | _ = c.MarkFlagRequired("database") 114 | flags.StringVarP(&d.TableName, "table", "t", "employees", "name of the table as data source") 115 | flags.IntVarP(&d.NumLoops, "count", "c", 10, "number of loops to execute") 116 | flags.StringVarP(&d.Operation, "operation", "o", "replicate", "supported operation, onyl replicate is supported right now") 117 | } 118 | 119 | func (d *DemoOptions) Validate(command *cobra.Command, kubeclient *client.Client) error { 120 | // Check WATCH_NAMESPACE, and if namespace exits, apphook operator is running 121 | namespace, err := util.GetOperatorNamespace() 122 | if err != nil { 123 | return err 124 | } 125 | ns := &corev1.Namespace{} 126 | err = kubeclient.Get( 127 | context.TODO(), 128 | types.NamespacedName{ 129 | Name: namespace, 130 | }, 131 | ns) 132 | 133 | if err != nil { 134 | return err 135 | } 136 | 137 | return nil 138 | } 139 | 140 | func queryAndInsert(db *sql.DB, source, target string) error { 141 | query := fmt.Sprintf("SELECT * FROM %s", source) 142 | res, err := db.Query(query) 143 | // nolint: staticcheck 144 | defer res.Close() 145 | 146 | if err != nil { 147 | return nil 148 | } 149 | 150 | for res.Next() { 151 | var id int 152 | var bdate string 153 | var fname string 154 | var lname string 155 | var gender string 156 | var hdate string 157 | err := res.Scan(&id, &bdate, &fname, &lname, &gender, &hdate) 158 | if err != nil { 159 | klog.InfoS("Failed scan table record", "error", err) 160 | return err 161 | } 162 | //klog.InfoS("get record", 163 | // "id", id, 164 | // "birth_date", bdate, 165 | // "first name", fname, 166 | // "last name", lname, 167 | // "gender", gender, 168 | // "hire date", hdate) 169 | 170 | klog.InfoS("insert record", 171 | "id", id, 172 | "birth_date", bdate, 173 | "first name", fname, 174 | "last name", lname, 175 | "gender", gender, 176 | "hire date", hdate) 177 | 178 | time.Sleep(DefaultInterval) 179 | sql := fmt.Sprintf("INSERT INTO %s(emp_no, birth_date, first_name, last_name, gender, hire_date) VALUES (?, ?, ?, ?, ?, ?)", target) 180 | stmt, err := db.Prepare(sql) 181 | if err != nil { 182 | klog.InfoS("Failed to prepare query", "error", err) 183 | return err 184 | } 185 | defer stmt.Close() 186 | res, err := stmt.Exec(id, bdate, fname, lname, gender, hdate) 187 | if err != nil { 188 | klog.InfoS("Failed to exec query", "error", err) 189 | return err 190 | } 191 | 192 | rows, err := res.RowsAffected() 193 | if err != nil { 194 | klog.InfoS("Failed to get rows affected", "error", err) 195 | return err 196 | } 197 | klog.InfoS("insert success", "rows", rows) 198 | } 199 | return nil 200 | } 201 | 202 | func deleteTable(db *sql.DB, table string) error { 203 | sql := fmt.Sprintf("DROP TABLE IF EXISTS %s", table) 204 | _, err := db.Exec(sql) 205 | if err != nil { 206 | klog.InfoS("Failed to insert table record", "error", err) 207 | return err 208 | } 209 | 210 | return nil 211 | } 212 | 213 | func createTable(db *sql.DB, table string) error { 214 | query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s(emp_no int primary key auto_increment, birth_date date, first_name VARCHAR(14), last_name VARCHAR(16), gender ENUM ('M','F'), hire_date DATE)", table) 215 | 216 | ctx, cancelfunc := context.WithTimeout(context.Background(), 5*time.Second) 217 | defer cancelfunc() 218 | res, err := db.ExecContext(ctx, query) 219 | if err != nil { 220 | klog.InfoS("Failed creating table", "error", err) 221 | return err 222 | } 223 | rows, err := res.RowsAffected() 224 | if err != nil { 225 | klog.InfoS("Failed getting rows affected", "error", err) 226 | return err 227 | } 228 | klog.InfoS("Rows affected when creating table", "rows", rows) 229 | return nil 230 | } 231 | 232 | // Create new table; 233 | // Select from source table and insert into new table 234 | // Delete the table, and loop n times 235 | func tableReplicateLoop(config *DBConfig, numLoops int, tableName string) error { 236 | if len(tableName) == 0 { 237 | return fmt.Errorf("Empty table name speicified") 238 | } 239 | 240 | klog.InfoS("table replicate test start", "table", tableName, "loop", numLoops) 241 | for i := 0; i < numLoops; i++ { 242 | newTableName := tableName + NewTableSuffix 243 | err := deleteTable(config.db, newTableName) 244 | if err != nil { 245 | return err 246 | } 247 | err = createTable(config.db, newTableName) 248 | if err != nil { 249 | return err 250 | } 251 | err = queryAndInsert(config.db, tableName, newTableName) 252 | if err != nil { 253 | return err 254 | } 255 | err = deleteTable(config.db, newTableName) 256 | if err != nil { 257 | return err 258 | } 259 | } 260 | return nil 261 | } 262 | 263 | func dbConnect(config *DBConfig) error { 264 | var err error 265 | klog.Info("Connect mysql, endpoint: ", config.Endpoint) 266 | 267 | dsn := fmt.Sprintf("%s:%s@%s(%s)/%s", config.UserName, config.Password, "tcp", config.Endpoint, config.Database) 268 | config.db, err = sql.Open("mysql", dsn) 269 | if err != nil { 270 | klog.InfoS("failed to init connection to mysql database", "database", config.Database, "error", err) 271 | return err 272 | } 273 | err = config.db.Ping() 274 | if err != nil { 275 | klog.InfoS("cannot access mysql databases", "database", config.Database) 276 | return err 277 | } 278 | config.db.SetConnMaxLifetime(time.Second * 10) 279 | //m.db.Close() 280 | 281 | return nil 282 | } 283 | 284 | func (d *DemoOptions) getDBSecret(kubeclient *client.Client, name, namespace string) (*corev1.Secret, error) { 285 | appSecret := &corev1.Secret{} 286 | err := kubeclient.Get( 287 | context.TODO(), 288 | types.NamespacedName{ 289 | Name: name, 290 | Namespace: namespace, 291 | }, appSecret) 292 | 293 | if err != nil { 294 | klog.InfoS("Failed to get DB secret", "error", err, "secret", name, "namespace", namespace) 295 | return nil, err 296 | } 297 | 298 | return appSecret, err 299 | } 300 | 301 | func (d *DemoOptions) dbStress(kubeclient *client.Client, hook *v1alpha1.AppHook) error { 302 | // Get secret 303 | dbSecret, err := d.getDBSecret(kubeclient, hook.Spec.Secret.Name, hook.Spec.Secret.Namespace) 304 | if err != nil { 305 | return err 306 | } 307 | 308 | klog.InfoS("Start stress", "user", string(dbSecret.Data["username"]), "password", dbSecret.Data["password"]) 309 | // Build DB config form hook 310 | dbConfig := &DBConfig{ 311 | Endpoint: hook.Spec.EndPoint, 312 | Database: d.Database, 313 | UserName: string(dbSecret.Data["username"]), 314 | Password: string(dbSecret.Data["password"]), 315 | Provider: hook.Spec.AppProvider, 316 | } 317 | switch d.Operation { 318 | case StressOperationReplicate: 319 | err = dbConnect(dbConfig) 320 | defer dbConfig.db.Close() 321 | if err != nil { 322 | return err 323 | } 324 | err = tableReplicateLoop(dbConfig, d.NumLoops, d.TableName) 325 | if err != nil { 326 | return err 327 | } 328 | } 329 | return nil 330 | } 331 | 332 | func (d *DemoOptions) getHookCR(kubeclient *client.Client, namespace string) (*v1alpha1.AppHook, error) { 333 | foundHook := &v1alpha1.AppHook{} 334 | err := kubeclient.Get( 335 | context.TODO(), 336 | types.NamespacedName{ 337 | Namespace: namespace, 338 | Name: d.HookName, 339 | }, 340 | foundHook) 341 | 342 | if err != nil { 343 | return nil, err 344 | } 345 | if foundHook.Status.Phase == v1alpha1.HookNotReady { 346 | klog.InfoS("Hook not conected", "hook", d.HookName) 347 | return nil, nil 348 | } 349 | return foundHook, nil 350 | } 351 | 352 | func (d *DemoOptions) Run(kubeclient *client.Client) error { 353 | namespace, err := util.GetOperatorNamespace() 354 | if err != nil { 355 | return err 356 | } 357 | 358 | hook, err := d.getHookCR(kubeclient, namespace) 359 | if err == nil { 360 | } else { 361 | klog.InfoS("Get hook CR failed", "error", err, "hook", d.HookName) 362 | return err 363 | } 364 | 365 | err = d.dbStress(kubeclient, hook) 366 | if err != nil { 367 | return err 368 | } 369 | klog.InfoS("database stress done", "hook", d.HookName) 370 | 371 | return err 372 | } 373 | 374 | func main() { 375 | defer klog.Flush() 376 | 377 | baseName := filepath.Base(os.Args[0]) 378 | 379 | c, err := NewCommand(baseName) 380 | cmd.CheckError(err) 381 | cmd.CheckError(c.Execute()) 382 | } 383 | -------------------------------------------------------------------------------- /config/crd/bases/ys.jibudata.com_apphooks.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.8.0 7 | creationTimestamp: null 8 | name: apphooks.ys.jibudata.com 9 | spec: 10 | group: ys.jibudata.com 11 | names: 12 | kind: AppHook 13 | listKind: AppHookList 14 | plural: apphooks 15 | singular: apphook 16 | scope: Namespaced 17 | versions: 18 | - additionalPrinterColumns: 19 | - jsonPath: .metadata.creationTimestamp 20 | name: Age 21 | type: date 22 | - jsonPath: .metadata.creationTimestamp 23 | name: Created At 24 | type: string 25 | - description: Phase 26 | jsonPath: .status.phase 27 | name: Phase 28 | type: string 29 | name: v1alpha1 30 | schema: 31 | openAPIV3Schema: 32 | description: AppHook is the Schema for the apphooks API 33 | properties: 34 | apiVersion: 35 | description: 'APIVersion defines the versioned schema of this representation 36 | of an object. Servers should convert recognized schemas to the latest 37 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 38 | type: string 39 | kind: 40 | description: 'Kind is a string value representing the REST resource this 41 | object represents. Servers may infer this from the endpoint the client 42 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 43 | type: string 44 | metadata: 45 | type: object 46 | spec: 47 | description: AppHookSpec defines the desired state of AppHook 48 | properties: 49 | appProvider: 50 | description: AppProvider is the application identifier for different 51 | vendors, such as mysql 52 | type: string 53 | databases: 54 | description: Databases 55 | items: 56 | type: string 57 | type: array 58 | endPoint: 59 | description: Endpoint to connect the applicatio service 60 | type: string 61 | name: 62 | description: Name is a job for backup/restore/migration 63 | type: string 64 | operationType: 65 | description: OperationType is the operation executed in application 66 | enum: 67 | - quiesce 68 | - unquiesce 69 | type: string 70 | params: 71 | additionalProperties: 72 | type: string 73 | description: Other options 74 | type: object 75 | secret: 76 | description: Secret to access the application 77 | properties: 78 | name: 79 | description: Name is unique within a namespace to reference a 80 | secret resource. 81 | type: string 82 | namespace: 83 | description: Namespace defines the space within which the secret 84 | name must be unique. 85 | type: string 86 | type: object 87 | timeoutSeconds: 88 | description: TimeoutSeconds is the timeout of operation 89 | format: int32 90 | minimum: 0 91 | type: integer 92 | required: 93 | - name 94 | type: object 95 | status: 96 | description: AppHookStatus defines the observed state of AppHook 97 | properties: 98 | errMsg: 99 | type: string 100 | phase: 101 | type: string 102 | preservedConfig: 103 | description: PreservedConfig saves the origin params before change 104 | by quiesce 105 | properties: 106 | params: 107 | additionalProperties: 108 | type: string 109 | type: object 110 | type: object 111 | quiescedTimestamp: 112 | format: date-time 113 | type: string 114 | result: 115 | properties: 116 | mongo: 117 | properties: 118 | isPrimary: 119 | type: boolean 120 | mongoEndpoint: 121 | type: string 122 | type: object 123 | mysql: 124 | type: object 125 | pg: 126 | type: object 127 | redis: 128 | type: object 129 | type: object 130 | type: object 131 | type: object 132 | served: true 133 | storage: true 134 | subresources: 135 | status: {} 136 | status: 137 | acceptedNames: 138 | kind: "" 139 | plural: "" 140 | conditions: [] 141 | storedVersions: [] 142 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/ys.jibudata.com_apphooks.yaml 6 | #+kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- patches/webhook_in_apphooks.yaml 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- patches/cainjection_in_apphooks.yaml 17 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_apphooks.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: apphooks.ys.jibudata.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_apphooks.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: apphooks.ys.jibudata.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: qiming-backend 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: amberapp- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 16 | # crd/kustomization.yaml 17 | #- ../webhook 18 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 19 | #- ../certmanager 20 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 21 | #- ../prometheus 22 | 23 | # Protect the /metrics endpoint by putting it behind auth. 24 | # If you want your controller-manager to expose the /metrics 25 | # endpoint w/o any authn/z, please comment the following line. 26 | patchesStrategicMerge: 27 | - manager_auth_proxy_patch.yaml 28 | 29 | # Mount the controller config file for loading manager configurations 30 | # through a ComponentConfig type 31 | #- manager_config_patch.yaml 32 | 33 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 34 | # crd/kustomization.yaml 35 | #- manager_webhook_patch.yaml 36 | 37 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 38 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 39 | # 'CERTMANAGER' needs to be enabled to use ca injection 40 | #- webhookcainjection_patch.yaml 41 | 42 | # the following config is for teaching kustomize how to do var substitution 43 | apiVersion: kustomize.config.k8s.io/v1beta1 44 | kind: Kustomization 45 | resources: 46 | - ../crd 47 | - ../rbac 48 | - ../manager 49 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: manager 13 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | args: 12 | - "--config=controller_manager_config.yaml" 13 | volumeMounts: 14 | - name: manager-config 15 | mountPath: /controller_manager_config.yaml 16 | subPath: controller_manager_config.yaml 17 | volumes: 18 | - name: manager-config 19 | configMap: 20 | name: manager-config 21 | -------------------------------------------------------------------------------- /config/manager/controller_manager_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 2 | kind: ControllerManagerConfig 3 | health: 4 | healthProbeBindAddress: :8081 5 | metrics: 6 | bindAddress: 127.0.0.1:8080 7 | webhook: 8 | port: 9443 9 | leaderElection: 10 | leaderElect: true 11 | resourceName: d0f11165.jibudata.com 12 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | 7 | apiVersion: kustomize.config.k8s.io/v1beta1 8 | kind: Kustomization 9 | images: 10 | - name: controller 11 | newName: registry.cn-shanghai.aliyuncs.com/jibutech/amberapp 12 | newTag: 0.1.0 13 | namespace: qiming-backend 14 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: amberapp-controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: amberapp-controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: amberapp-controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: amberapp-controller-manager 24 | spec: 25 | securityContext: 26 | runAsNonRoot: true 27 | containers: 28 | - command: 29 | - /manager 30 | args: 31 | - -zap-devel=false 32 | - -zap-encoder=console 33 | - -zap-log-level=debug 34 | image: controller:latest 35 | imagePullPolicy: Always 36 | name: manager 37 | securityContext: 38 | allowPrivilegeEscalation: false 39 | resources: 40 | limits: 41 | cpu: 100m 42 | memory: 300Mi 43 | requests: 44 | cpu: 100m 45 | memory: 50Mi 46 | env: 47 | - name: WATCH_NAMESPACE 48 | valueFrom: 49 | fieldRef: 50 | fieldPath: metadata.namespace 51 | serviceAccountName: controller-manager 52 | terminationGracePeriodSeconds: 10 53 | -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | resources: 4 | - bases/amberapp.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | 9 | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 | #patchesJson6902: 13 | #- target: 14 | # group: apps 15 | # version: v1 16 | # kind: Deployment 17 | # name: controller-manager 18 | # namespace: system 19 | # patch: |- 20 | # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 | # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 | # - op: remove 23 | # path: /spec/template/spec/containers/1/volumeMounts/0 24 | # # Remove the "cert" volume, since OLM will create and mount a set of certs. 25 | # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 26 | # - op: remove 27 | # path: /spec/template/spec/volumes/0 28 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: amberapp-controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | scheme: https 15 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 16 | tlsConfig: 17 | insecureSkipVerify: true 18 | selector: 19 | matchLabels: 20 | control-plane: amberapp-controller-manager 21 | -------------------------------------------------------------------------------- /config/rbac/apphook_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit apphooks. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: apphook-editor-role 6 | rules: 7 | - apiGroups: 8 | - ys.jibudata.com 9 | resources: 10 | - apphooks 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - ys.jibudata.com 21 | resources: 22 | - apphooks/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/apphook_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view apphooks. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: apphook-viewer-role 6 | rules: 7 | - apiGroups: 8 | - ys.jibudata.com 9 | resources: 10 | - apphooks 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - ys.jibudata.com 17 | resources: 18 | - apphooks/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: amberapp-controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: amberapp-controller-manager 15 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - coordination.k8s.io 21 | resources: 22 | - leases 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - events 35 | verbs: 36 | - create 37 | - patch 38 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: manager-role 7 | rules: 8 | - apiGroups: 9 | - apps 10 | resources: 11 | - deployments 12 | verbs: 13 | - create 14 | - delete 15 | - get 16 | - list 17 | - patch 18 | - update 19 | - watch 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - secrets 24 | verbs: 25 | - create 26 | - delete 27 | - get 28 | - list 29 | - patch 30 | - update 31 | - watch 32 | - apiGroups: 33 | - ys.jibudata.com 34 | resources: 35 | - apphooks 36 | verbs: 37 | - create 38 | - delete 39 | - get 40 | - list 41 | - patch 42 | - update 43 | - watch 44 | - apiGroups: 45 | - ys.jibudata.com 46 | resources: 47 | - apphooks/finalizers 48 | verbs: 49 | - update 50 | - apiGroups: 51 | - ys.jibudata.com 52 | resources: 53 | - apphooks/status 54 | verbs: 55 | - get 56 | - patch 57 | - update 58 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: cluster-admin 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - ys_v1alpha1_apphook.yaml 4 | #+kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /config/samples/sample-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: apphook-sample-secret 5 | namespace: amberapp-system 6 | type: Opaque 7 | stringData: 8 | username: postgresadmin 9 | password: Test1234 -------------------------------------------------------------------------------- /config/samples/ys_v1alpha1_apphook.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ys.jibudata.com/v1alpha1 2 | kind: AppHook 3 | metadata: 4 | name: apphook-sample 5 | namespace: amberapp-system 6 | spec: 7 | name: apphook-sample 8 | appProvider: postgres 9 | endPoint: "postgres.postgres-ns" 10 | databases: 11 | - postgresdb 12 | secret: 13 | name: "apphook-sample-secret" 14 | namespace: "amberapp-system" 15 | -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | #+kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.10.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.10.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.10.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.10.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.10.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.10.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /controllers/driver/driver.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "time" 8 | 9 | corev1 "k8s.io/api/core/v1" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/controller-runtime/pkg/log" 12 | 13 | "github.com/jibudata/amberapp/api/v1alpha1" 14 | "github.com/jibudata/amberapp/controllers/util" 15 | "github.com/jibudata/amberapp/pkg/appconfig" 16 | "github.com/jibudata/amberapp/pkg/mongo" 17 | "github.com/jibudata/amberapp/pkg/mysql" 18 | "github.com/jibudata/amberapp/pkg/postgres" 19 | "github.com/jibudata/amberapp/pkg/redis" 20 | ) 21 | 22 | type SupportedDB string 23 | 24 | const ( 25 | MySQL SupportedDB = "MySQL" 26 | Postgres SupportedDB = "Postgres" 27 | MongoDB SupportedDB = "MongoDB" 28 | Redis SupportedDB = "Redis" 29 | ) 30 | 31 | type Database interface { 32 | Init(appconfig.Config) error 33 | Connect() error 34 | Prepare() (*v1alpha1.PreservedConfig, error) 35 | Quiesce() (*v1alpha1.QuiesceResult, error) 36 | Unquiesce(*v1alpha1.PreservedConfig) error 37 | } 38 | 39 | type DriverManager struct { 40 | client.Client 41 | namespace string 42 | appName string 43 | ready bool 44 | db Database 45 | appConfig appconfig.Config 46 | } 47 | 48 | func NewManager(k8sclient client.Client, instance *v1alpha1.AppHook, secret *corev1.Secret) (*DriverManager, error) { 49 | var CacheManager DriverManager 50 | var err error 51 | var usePrimary bool 52 | CacheManager.Client = k8sclient 53 | CacheManager.appName = instance.Name 54 | CacheManager.namespace = instance.Namespace 55 | 56 | // init database 57 | if strings.EqualFold(instance.Spec.AppProvider, string(Postgres)) { // postgres 58 | CacheManager.db = new(postgres.PG) 59 | } else if strings.EqualFold(instance.Spec.AppProvider, string(MySQL)) { // mysql 60 | CacheManager.db = new(mysql.MYSQL) 61 | } else if strings.EqualFold(instance.Spec.AppProvider, string(MongoDB)) { // mongo 62 | CacheManager.db = new(mongo.MG) 63 | } else if strings.EqualFold(instance.Spec.AppProvider, string(Redis)) { // redis 64 | CacheManager.db = new(redis.Redis) 65 | } else { 66 | CacheManager.NotReady() 67 | err = fmt.Errorf("provider %s is not supported", instance.Spec.AppProvider) 68 | log.Log.Error(err, "err") 69 | return &CacheManager, err 70 | } 71 | 72 | if len(instance.Spec.Params) > 0 { 73 | quiesceParam, ok := instance.Spec.Params[v1alpha1.QuiesceFromPrimary] 74 | if ok { 75 | if quiesceParam == "true" { 76 | usePrimary = true 77 | } 78 | } 79 | } 80 | 81 | CacheManager.appConfig = appconfig.Config{ 82 | Name: instance.Name, 83 | Host: instance.Spec.EndPoint, 84 | Databases: instance.Spec.Databases, 85 | Username: string(secret.Data["username"]), 86 | Password: string(secret.Data["password"]), 87 | Provider: instance.Spec.AppProvider, 88 | Operation: instance.Spec.OperationType, 89 | QuiesceFromPrimary: usePrimary, 90 | Params: instance.Spec.Params, 91 | } 92 | 93 | if instance.Spec.TimeoutSeconds != nil { 94 | CacheManager.appConfig.QuiesceTimeout = time.Duration(*instance.Spec.TimeoutSeconds) 95 | } 96 | 97 | err = CacheManager.db.Init(CacheManager.appConfig) 98 | return &CacheManager, err 99 | } 100 | 101 | // danger, do NOT update the config when database is quiesced 102 | func (d *DriverManager) Update(instance *v1alpha1.AppHook, secret *corev1.Secret) error { 103 | if d.appConfig.Name != instance.Name { 104 | return fmt.Errorf("apphook name %s cannot be changed", d.appConfig.Name) 105 | } 106 | if d.appConfig.Provider != instance.Spec.AppProvider { 107 | return fmt.Errorf("apphook %s provider %s cannot be changed", d.appConfig.Name, d.appConfig.Provider) 108 | } 109 | 110 | isChanged := false 111 | if d.appConfig.Host != instance.Spec.EndPoint { 112 | d.appConfig.Host = instance.Spec.EndPoint 113 | isChanged = true 114 | } 115 | if !equalStr(d.appConfig.Databases, instance.Spec.Databases) { 116 | d.appConfig.Databases = instance.Spec.Databases 117 | isChanged = true 118 | } 119 | if d.appConfig.Username != string(secret.Data["username"]) { 120 | d.appConfig.Username = string(secret.Data["username"]) 121 | isChanged = true 122 | } 123 | if d.appConfig.Password != string(secret.Data["password"]) { 124 | d.appConfig.Password = string(secret.Data["password"]) 125 | isChanged = true 126 | } 127 | if !reflect.DeepEqual(d.appConfig.Params, instance.Spec.Params) { 128 | log.Log.Info("parameters changes", "new: ", instance.Spec.Params, "old: ", d.appConfig.Params) 129 | d.appConfig.Params = instance.Spec.Params 130 | isChanged = true 131 | } 132 | 133 | if isChanged { 134 | log.Log.Info(fmt.Sprintf("detected %s configuration was changed, updating", d.appConfig.Name)) 135 | if instance.Status.Phase == v1alpha1.HookQUIESCED { 136 | log.Log.Info(fmt.Sprintf("warning: %s hook status is quiesced when updating configuration", d.appConfig.Name)) 137 | } 138 | err := d.db.Init(d.appConfig) 139 | if err != nil { 140 | return err 141 | } 142 | err = d.db.Connect() 143 | if err != nil { 144 | return err 145 | } 146 | } 147 | 148 | return nil 149 | } 150 | 151 | func (d *DriverManager) Ready() { 152 | d.ready = true 153 | } 154 | 155 | func (d *DriverManager) NotReady() { 156 | d.ready = false 157 | } 158 | 159 | func (d *DriverManager) DBConnect() error { 160 | return d.db.Connect() 161 | } 162 | 163 | func (d *DriverManager) DBPrepare() (*v1alpha1.PreservedConfig, error) { 164 | return d.db.Prepare() 165 | } 166 | 167 | func (d *DriverManager) DBQuiesce() (*v1alpha1.QuiesceResult, error) { 168 | return d.db.Quiesce() 169 | } 170 | 171 | func (d *DriverManager) DBUnquiesce(prev *v1alpha1.PreservedConfig) error { 172 | return d.db.Unquiesce(prev) 173 | } 174 | 175 | func equalStr(str1, str2 []string) bool { 176 | if len(str1) != len(str2) { 177 | return false 178 | } 179 | var i int 180 | for i = 0; i < len(str1); i++ { 181 | if !util.IsContain(str2, str1[i]) { 182 | return false 183 | } 184 | } 185 | for i = 0; i < len(str2); i++ { 186 | if !util.IsContain(str1, str2[i]) { 187 | return false 188 | } 189 | } 190 | return true 191 | } 192 | -------------------------------------------------------------------------------- /controllers/hook_resource.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | package controllers 17 | 18 | import ( 19 | appsv1 "k8s.io/api/apps/v1" 20 | corev1 "k8s.io/api/core/v1" 21 | "k8s.io/apimachinery/pkg/api/resource" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | 24 | "github.com/jibudata/amberapp/api/v1alpha1" 25 | "github.com/jibudata/amberapp/controllers/util" 26 | ) 27 | 28 | const ( 29 | // Hook related dependencies 30 | secretUserKey = "username" 31 | secretPasswdKey = "password" 32 | ) 33 | 34 | func InitHookDeployment( 35 | instance *v1alpha1.AppHook, 36 | secret *corev1.Secret) (*appsv1.Deployment, error) { 37 | 38 | var replicaOne int32 = 1 39 | var pullPolicy = corev1.PullIfNotPresent 40 | var image = "hook:v1" 41 | 42 | deploymentName := instance.Name 43 | 44 | labels := util.GetLabels(instance.Name) 45 | 46 | return &appsv1.Deployment{ 47 | ObjectMeta: metav1.ObjectMeta{ 48 | Name: deploymentName, 49 | Namespace: instance.Namespace, 50 | Labels: labels, 51 | OwnerReferences: []metav1.OwnerReference{ 52 | { 53 | APIVersion: instance.APIVersion, 54 | Kind: instance.Kind, 55 | Name: instance.Name, 56 | UID: instance.UID, 57 | }, 58 | }, 59 | }, 60 | Spec: appsv1.DeploymentSpec{ 61 | Replicas: &replicaOne, 62 | Selector: &metav1.LabelSelector{ 63 | MatchLabels: labels, 64 | }, 65 | Template: corev1.PodTemplateSpec{ 66 | ObjectMeta: metav1.ObjectMeta{ 67 | Labels: labels, 68 | }, 69 | Spec: corev1.PodSpec{ 70 | Containers: []corev1.Container{ 71 | { 72 | Name: deploymentName, 73 | Image: image, 74 | ImagePullPolicy: pullPolicy, 75 | Env: []corev1.EnvVar{ 76 | { 77 | Name: "DEPLOYMENTNAME", 78 | Value: deploymentName, 79 | }, 80 | { 81 | Name: "NAMESPACE", 82 | Value: instance.Namespace, 83 | }, 84 | { 85 | Name: "USERNAME", 86 | ValueFrom: &corev1.EnvVarSource{ 87 | SecretKeyRef: &corev1.SecretKeySelector{ 88 | LocalObjectReference: corev1.LocalObjectReference{ 89 | Name: secret.Name, 90 | }, 91 | Key: secretUserKey, 92 | }, 93 | }, 94 | }, 95 | { 96 | Name: "PASSWORD", 97 | ValueFrom: &corev1.EnvVarSource{ 98 | SecretKeyRef: &corev1.SecretKeySelector{ 99 | LocalObjectReference: corev1.LocalObjectReference{ 100 | Name: secret.Name, 101 | }, 102 | Key: secretPasswdKey, 103 | }, 104 | }, 105 | }, 106 | { 107 | Name: "REST_API_IP", 108 | Value: instance.Spec.EndPoint, 109 | }, 110 | { 111 | Name: "PROVIDER", 112 | Value: instance.Spec.AppProvider, 113 | }, 114 | { 115 | Name: "OPERATION", 116 | Value: instance.Spec.OperationType, 117 | }, 118 | }, 119 | Resources: corev1.ResourceRequirements{ 120 | Requests: corev1.ResourceList{ 121 | corev1.ResourceCPU: resource.MustParse("0.5"), 122 | corev1.ResourceMemory: resource.MustParse("500Mi"), 123 | }, 124 | Limits: corev1.ResourceList{ 125 | corev1.ResourceCPU: resource.MustParse("1"), 126 | corev1.ResourceMemory: resource.MustParse("1Gi"), 127 | }, 128 | }, 129 | }, 130 | }, 131 | ServiceAccountName: "amberapp-controller-manager", 132 | }, 133 | }, 134 | }, 135 | }, nil 136 | } 137 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package controllers 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | "k8s.io/client-go/kubernetes/scheme" 26 | "sigs.k8s.io/controller-runtime/pkg/client" 27 | "sigs.k8s.io/controller-runtime/pkg/envtest" 28 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 29 | logf "sigs.k8s.io/controller-runtime/pkg/log" 30 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 31 | 32 | "github.com/jibudata/amberapp/api/v1alpha1" 33 | //+kubebuilder:scaffold:imports 34 | ) 35 | 36 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 37 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 38 | 39 | // var cfg *rest.Config 40 | var k8sClient client.Client 41 | var testEnv *envtest.Environment 42 | 43 | func TestAPIs(t *testing.T) { 44 | RegisterFailHandler(Fail) 45 | 46 | RunSpecsWithDefaultAndCustomReporters(t, 47 | "Controller Suite", 48 | []Reporter{printer.NewlineReporter{}}) 49 | } 50 | 51 | var _ = BeforeSuite(func() { 52 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 53 | 54 | By("bootstrapping test environment") 55 | testEnv = &envtest.Environment{ 56 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 57 | ErrorIfCRDPathMissing: true, 58 | } 59 | 60 | cfg, err := testEnv.Start() 61 | Expect(err).NotTo(HaveOccurred()) 62 | Expect(cfg).NotTo(BeNil()) 63 | 64 | err = v1alpha1.AddToScheme(scheme.Scheme) 65 | Expect(err).NotTo(HaveOccurred()) 66 | 67 | //+kubebuilder:scaffold:scheme 68 | 69 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(k8sClient).NotTo(BeNil()) 72 | 73 | }, 60) 74 | 75 | var _ = AfterSuite(func() { 76 | By("tearing down the test environment") 77 | err := testEnv.Stop() 78 | Expect(err).NotTo(HaveOccurred()) 79 | }) 80 | -------------------------------------------------------------------------------- /controllers/util/k8sutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | package util 17 | 18 | import ( 19 | "crypto/sha256" 20 | "encoding/hex" 21 | "encoding/json" 22 | "fmt" 23 | "time" 24 | 25 | corev1 "k8s.io/api/core/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | 28 | "github.com/jibudata/amberapp/api/v1alpha1" 29 | ) 30 | 31 | // CalculateDataHash generates a sha256 hex-digest for a data object 32 | func CalculateDataHash(dataObject interface{}) (string, error) { 33 | data, err := json.Marshal(dataObject) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | hash := sha256.New() 39 | hash.Write(data) 40 | return hex.EncodeToString(hash.Sum(nil)), nil 41 | } 42 | 43 | func IsContain(slice []string, s string) bool { 44 | for _, item := range slice { 45 | if item == s { 46 | return true 47 | } 48 | } 49 | return false 50 | } 51 | 52 | func Remove(slice []string, s string) (result []string) { 53 | for _, item := range slice { 54 | if item == s { 55 | continue 56 | } 57 | result = append(result, item) 58 | } 59 | return 60 | } 61 | 62 | func GetLabels(clusterName string) map[string]string { 63 | return map[string]string{ 64 | "app.kubernetes.io/name": clusterName, 65 | } 66 | } 67 | 68 | func InitK8sEvent(instance *v1alpha1.AppHook, eventtype, reason, message string) *corev1.Event { 69 | t := metav1.Time{Time: time.Now()} 70 | selectLabels := GetLabels(instance.Name) 71 | return &corev1.Event{ 72 | ObjectMeta: metav1.ObjectMeta{ 73 | Name: fmt.Sprintf("%v.%x", instance.Name, t.UnixNano()), 74 | Namespace: instance.Namespace, 75 | Labels: selectLabels, 76 | }, 77 | InvolvedObject: corev1.ObjectReference{ 78 | Kind: instance.Kind, 79 | Namespace: instance.Namespace, 80 | Name: instance.Name, 81 | UID: instance.UID, 82 | ResourceVersion: instance.ResourceVersion, 83 | APIVersion: instance.APIVersion, 84 | }, 85 | Reason: reason, 86 | Message: message, 87 | FirstTimestamp: t, 88 | LastTimestamp: t, 89 | Count: 1, 90 | Type: eventtype, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /deploy/apiextensions.k8s.io_v1_customresourcedefinition_apphooks.ys.jibudata.com.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.8.0 6 | creationTimestamp: null 7 | name: apphooks.ys.jibudata.com 8 | spec: 9 | group: ys.jibudata.com 10 | names: 11 | kind: AppHook 12 | listKind: AppHookList 13 | plural: apphooks 14 | singular: apphook 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - jsonPath: .metadata.creationTimestamp 19 | name: Age 20 | type: date 21 | - jsonPath: .metadata.creationTimestamp 22 | name: Created At 23 | type: string 24 | - description: Phase 25 | jsonPath: .status.phase 26 | name: Phase 27 | type: string 28 | name: v1alpha1 29 | schema: 30 | openAPIV3Schema: 31 | description: AppHook is the Schema for the apphooks API 32 | properties: 33 | apiVersion: 34 | description: 'APIVersion defines the versioned schema of this representation 35 | of an object. Servers should convert recognized schemas to the latest 36 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 37 | type: string 38 | kind: 39 | description: 'Kind is a string value representing the REST resource this 40 | object represents. Servers may infer this from the endpoint the client 41 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 42 | type: string 43 | metadata: 44 | type: object 45 | spec: 46 | description: AppHookSpec defines the desired state of AppHook 47 | properties: 48 | appProvider: 49 | description: AppProvider is the application identifier for different 50 | vendors, such as mysql 51 | type: string 52 | databases: 53 | description: Databases 54 | items: 55 | type: string 56 | type: array 57 | endPoint: 58 | description: Endpoint to connect the applicatio service 59 | type: string 60 | name: 61 | description: Name is a job for backup/restore/migration 62 | type: string 63 | operationType: 64 | description: OperationType is the operation executed in application 65 | enum: 66 | - quiesce 67 | - unquiesce 68 | type: string 69 | params: 70 | additionalProperties: 71 | type: string 72 | description: Other options 73 | type: object 74 | secret: 75 | description: Secret to access the application 76 | properties: 77 | name: 78 | description: Name is unique within a namespace to reference a 79 | secret resource. 80 | type: string 81 | namespace: 82 | description: Namespace defines the space within which the secret 83 | name must be unique. 84 | type: string 85 | type: object 86 | timeoutSeconds: 87 | description: TimeoutSeconds is the timeout of operation 88 | format: int32 89 | minimum: 0 90 | type: integer 91 | required: 92 | - name 93 | type: object 94 | status: 95 | description: AppHookStatus defines the observed state of AppHook 96 | properties: 97 | errMsg: 98 | type: string 99 | phase: 100 | type: string 101 | quiescedTimestamp: 102 | format: date-time 103 | type: string 104 | result: 105 | properties: 106 | mongo: 107 | properties: 108 | isPrimary: 109 | type: boolean 110 | mongoEndpoint: 111 | type: string 112 | type: object 113 | mysql: 114 | type: object 115 | pg: 116 | type: object 117 | type: object 118 | type: object 119 | type: object 120 | served: true 121 | storage: true 122 | subresources: 123 | status: {} 124 | status: 125 | acceptedNames: 126 | kind: "" 127 | plural: "" 128 | conditions: [] 129 | storedVersions: [] 130 | -------------------------------------------------------------------------------- /deploy/apps_v1_deployment_amberapp-controller-manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | control-plane: amberapp-controller-manager 6 | name: amberapp-controller-manager 7 | namespace: amberapp-system 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | control-plane: amberapp-controller-manager 13 | template: 14 | metadata: 15 | labels: 16 | control-plane: amberapp-controller-manager 17 | spec: 18 | containers: 19 | - args: 20 | - -zap-devel=false 21 | - -zap-encoder=console 22 | - -zap-log-level=debug 23 | command: 24 | - /manager 25 | env: 26 | - name: WATCH_NAMESPACE 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.namespace 30 | image: registry.cn-shanghai.aliyuncs.com/jibutech/amberapp:0.1.0 31 | imagePullPolicy: Always 32 | name: manager 33 | resources: 34 | limits: 35 | cpu: 100m 36 | memory: 300Mi 37 | requests: 38 | cpu: 100m 39 | memory: 50Mi 40 | securityContext: 41 | allowPrivilegeEscalation: false 42 | securityContext: 43 | runAsNonRoot: true 44 | serviceAccountName: amberapp-controller-manager 45 | terminationGracePeriodSeconds: 10 46 | -------------------------------------------------------------------------------- /deploy/rbac.authorization.k8s.io_v1_clusterrole_amberapp-manager-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | name: amberapp-manager-role 6 | rules: 7 | - apiGroups: 8 | - apps 9 | resources: 10 | - deployments 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - secrets 23 | verbs: 24 | - create 25 | - delete 26 | - get 27 | - list 28 | - patch 29 | - update 30 | - watch 31 | - apiGroups: 32 | - ys.jibudata.com 33 | resources: 34 | - apphooks 35 | verbs: 36 | - create 37 | - delete 38 | - get 39 | - list 40 | - patch 41 | - update 42 | - watch 43 | - apiGroups: 44 | - ys.jibudata.com 45 | resources: 46 | - apphooks/finalizers 47 | verbs: 48 | - update 49 | - apiGroups: 50 | - ys.jibudata.com 51 | resources: 52 | - apphooks/status 53 | verbs: 54 | - get 55 | - patch 56 | - update 57 | -------------------------------------------------------------------------------- /deploy/rbac.authorization.k8s.io_v1_clusterrole_amberapp-metrics-reader.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: amberapp-metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - /metrics 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /deploy/rbac.authorization.k8s.io_v1_clusterrole_amberapp-proxy-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: amberapp-proxy-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /deploy/rbac.authorization.k8s.io_v1_clusterrolebinding_amberapp-manager-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: amberapp-manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: cluster-admin 9 | subjects: 10 | - kind: ServiceAccount 11 | name: amberapp-controller-manager 12 | namespace: amberapp-system 13 | -------------------------------------------------------------------------------- /deploy/rbac.authorization.k8s.io_v1_clusterrolebinding_amberapp-proxy-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: amberapp-proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: amberapp-proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: amberapp-controller-manager 12 | namespace: amberapp-system 13 | -------------------------------------------------------------------------------- /deploy/rbac.authorization.k8s.io_v1_role_amberapp-leader-election-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: amberapp-leader-election-role 5 | namespace: amberapp-system 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - coordination.k8s.io 21 | resources: 22 | - leases 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - events 35 | verbs: 36 | - create 37 | - patch 38 | -------------------------------------------------------------------------------- /deploy/rbac.authorization.k8s.io_v1_rolebinding_amberapp-leader-election-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: amberapp-leader-election-rolebinding 5 | namespace: amberapp-system 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: Role 9 | name: amberapp-leader-election-role 10 | subjects: 11 | - kind: ServiceAccount 12 | name: amberapp-controller-manager 13 | namespace: amberapp-system 14 | -------------------------------------------------------------------------------- /deploy/v1_namespace_amberapp-system.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: amberapp-controller-manager 6 | name: amberapp-system 7 | -------------------------------------------------------------------------------- /deploy/v1_service_amberapp-controller-manager-metrics-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: amberapp-controller-manager 6 | name: amberapp-controller-manager-metrics-service 7 | namespace: amberapp-system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: amberapp-controller-manager 15 | -------------------------------------------------------------------------------- /deploy/v1_serviceaccount_amberapp-controller-manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: amberapp-controller-manager 5 | namespace: amberapp-system 6 | -------------------------------------------------------------------------------- /deploy/ys1000/deployments.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: amberapp-controller-manager 6 | name: qiming-backend 7 | --- 8 | apiVersion: apiextensions.k8s.io/v1 9 | kind: CustomResourceDefinition 10 | metadata: 11 | annotations: 12 | controller-gen.kubebuilder.io/version: v0.8.0 13 | creationTimestamp: null 14 | name: apphooks.ys.jibudata.com 15 | spec: 16 | group: ys.jibudata.com 17 | names: 18 | kind: AppHook 19 | listKind: AppHookList 20 | plural: apphooks 21 | singular: apphook 22 | scope: Namespaced 23 | versions: 24 | - additionalPrinterColumns: 25 | - jsonPath: .metadata.creationTimestamp 26 | name: Age 27 | type: date 28 | - jsonPath: .metadata.creationTimestamp 29 | name: Created At 30 | type: string 31 | - description: Phase 32 | jsonPath: .status.phase 33 | name: Phase 34 | type: string 35 | name: v1alpha1 36 | schema: 37 | openAPIV3Schema: 38 | description: AppHook is the Schema for the apphooks API 39 | properties: 40 | apiVersion: 41 | description: 'APIVersion defines the versioned schema of this representation 42 | of an object. Servers should convert recognized schemas to the latest 43 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 44 | type: string 45 | kind: 46 | description: 'Kind is a string value representing the REST resource this 47 | object represents. Servers may infer this from the endpoint the client 48 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 49 | type: string 50 | metadata: 51 | type: object 52 | spec: 53 | description: AppHookSpec defines the desired state of AppHook 54 | properties: 55 | appProvider: 56 | description: AppProvider is the application identifier for different 57 | vendors, such as mysql 58 | type: string 59 | databases: 60 | description: Databases 61 | items: 62 | type: string 63 | type: array 64 | endPoint: 65 | description: Endpoint to connect the applicatio service 66 | type: string 67 | name: 68 | description: Name is a job for backup/restore/migration 69 | type: string 70 | operationType: 71 | description: OperationType is the operation executed in application 72 | enum: 73 | - quiesce 74 | - unquiesce 75 | type: string 76 | params: 77 | additionalProperties: 78 | type: string 79 | description: Other options 80 | type: object 81 | secret: 82 | description: Secret to access the application 83 | properties: 84 | name: 85 | description: Name is unique within a namespace to reference a 86 | secret resource. 87 | type: string 88 | namespace: 89 | description: Namespace defines the space within which the secret 90 | name must be unique. 91 | type: string 92 | type: object 93 | timeoutSeconds: 94 | description: TimeoutSeconds is the timeout of operation 95 | format: int32 96 | minimum: 0 97 | type: integer 98 | required: 99 | - name 100 | type: object 101 | status: 102 | description: AppHookStatus defines the observed state of AppHook 103 | properties: 104 | errMsg: 105 | type: string 106 | phase: 107 | type: string 108 | quiescedTimestamp: 109 | format: date-time 110 | type: string 111 | result: 112 | properties: 113 | mongo: 114 | properties: 115 | isPrimary: 116 | type: boolean 117 | mongoEndpoint: 118 | type: string 119 | type: object 120 | mysql: 121 | type: object 122 | pg: 123 | type: object 124 | type: object 125 | type: object 126 | type: object 127 | served: true 128 | storage: true 129 | subresources: 130 | status: {} 131 | status: 132 | acceptedNames: 133 | kind: "" 134 | plural: "" 135 | conditions: [] 136 | storedVersions: [] 137 | --- 138 | apiVersion: v1 139 | kind: ServiceAccount 140 | metadata: 141 | name: amberapp-controller-manager 142 | namespace: qiming-backend 143 | --- 144 | apiVersion: rbac.authorization.k8s.io/v1 145 | kind: Role 146 | metadata: 147 | name: amberapp-leader-election-role 148 | namespace: qiming-backend 149 | rules: 150 | - apiGroups: 151 | - "" 152 | resources: 153 | - configmaps 154 | verbs: 155 | - get 156 | - list 157 | - watch 158 | - create 159 | - update 160 | - patch 161 | - delete 162 | - apiGroups: 163 | - coordination.k8s.io 164 | resources: 165 | - leases 166 | verbs: 167 | - get 168 | - list 169 | - watch 170 | - create 171 | - update 172 | - patch 173 | - delete 174 | - apiGroups: 175 | - "" 176 | resources: 177 | - events 178 | verbs: 179 | - create 180 | - patch 181 | --- 182 | apiVersion: rbac.authorization.k8s.io/v1 183 | kind: ClusterRole 184 | metadata: 185 | creationTimestamp: null 186 | name: amberapp-manager-role 187 | rules: 188 | - apiGroups: 189 | - apps 190 | resources: 191 | - deployments 192 | verbs: 193 | - create 194 | - delete 195 | - get 196 | - list 197 | - patch 198 | - update 199 | - watch 200 | - apiGroups: 201 | - "" 202 | resources: 203 | - secrets 204 | verbs: 205 | - create 206 | - delete 207 | - get 208 | - list 209 | - patch 210 | - update 211 | - watch 212 | - apiGroups: 213 | - ys.jibudata.com 214 | resources: 215 | - apphooks 216 | verbs: 217 | - create 218 | - delete 219 | - get 220 | - list 221 | - patch 222 | - update 223 | - watch 224 | - apiGroups: 225 | - ys.jibudata.com 226 | resources: 227 | - apphooks/finalizers 228 | verbs: 229 | - update 230 | - apiGroups: 231 | - ys.jibudata.com 232 | resources: 233 | - apphooks/status 234 | verbs: 235 | - get 236 | - patch 237 | - update 238 | --- 239 | apiVersion: rbac.authorization.k8s.io/v1 240 | kind: ClusterRole 241 | metadata: 242 | name: amberapp-metrics-reader 243 | rules: 244 | - nonResourceURLs: 245 | - /metrics 246 | verbs: 247 | - get 248 | --- 249 | apiVersion: rbac.authorization.k8s.io/v1 250 | kind: ClusterRole 251 | metadata: 252 | name: amberapp-proxy-role 253 | rules: 254 | - apiGroups: 255 | - authentication.k8s.io 256 | resources: 257 | - tokenreviews 258 | verbs: 259 | - create 260 | - apiGroups: 261 | - authorization.k8s.io 262 | resources: 263 | - subjectaccessreviews 264 | verbs: 265 | - create 266 | --- 267 | apiVersion: rbac.authorization.k8s.io/v1 268 | kind: RoleBinding 269 | metadata: 270 | name: amberapp-leader-election-rolebinding 271 | namespace: qiming-backend 272 | roleRef: 273 | apiGroup: rbac.authorization.k8s.io 274 | kind: Role 275 | name: amberapp-leader-election-role 276 | subjects: 277 | - kind: ServiceAccount 278 | name: amberapp-controller-manager 279 | namespace: qiming-backend 280 | --- 281 | apiVersion: rbac.authorization.k8s.io/v1 282 | kind: ClusterRoleBinding 283 | metadata: 284 | name: amberapp-manager-rolebinding 285 | roleRef: 286 | apiGroup: rbac.authorization.k8s.io 287 | kind: ClusterRole 288 | name: cluster-admin 289 | subjects: 290 | - kind: ServiceAccount 291 | name: amberapp-controller-manager 292 | namespace: qiming-backend 293 | --- 294 | apiVersion: rbac.authorization.k8s.io/v1 295 | kind: ClusterRoleBinding 296 | metadata: 297 | name: amberapp-proxy-rolebinding 298 | roleRef: 299 | apiGroup: rbac.authorization.k8s.io 300 | kind: ClusterRole 301 | name: amberapp-proxy-role 302 | subjects: 303 | - kind: ServiceAccount 304 | name: amberapp-controller-manager 305 | namespace: qiming-backend 306 | --- 307 | apiVersion: v1 308 | kind: Service 309 | metadata: 310 | labels: 311 | control-plane: amberapp-controller-manager 312 | name: amberapp-controller-manager-metrics-service 313 | namespace: qiming-backend 314 | spec: 315 | ports: 316 | - name: https 317 | port: 8443 318 | targetPort: https 319 | selector: 320 | control-plane: amberapp-controller-manager 321 | --- 322 | apiVersion: apps/v1 323 | kind: Deployment 324 | metadata: 325 | labels: 326 | control-plane: amberapp-controller-manager 327 | name: amberapp-controller-manager 328 | namespace: qiming-backend 329 | spec: 330 | replicas: 1 331 | selector: 332 | matchLabels: 333 | control-plane: amberapp-controller-manager 334 | template: 335 | metadata: 336 | labels: 337 | control-plane: amberapp-controller-manager 338 | spec: 339 | containers: 340 | - args: 341 | - -zap-devel=false 342 | - -zap-encoder=console 343 | - -zap-log-level=debug 344 | command: 345 | - /manager 346 | env: 347 | - name: WATCH_NAMESPACE 348 | valueFrom: 349 | fieldRef: 350 | fieldPath: metadata.namespace 351 | image: registry.cn-shanghai.aliyuncs.com/jibutech/amberapp:0.1.0 352 | imagePullPolicy: Always 353 | name: manager 354 | resources: 355 | limits: 356 | cpu: 100m 357 | memory: 300Mi 358 | requests: 359 | cpu: 100m 360 | memory: 50Mi 361 | securityContext: 362 | allowPrivilegeEscalation: false 363 | securityContext: 364 | runAsNonRoot: true 365 | serviceAccountName: amberapp-controller-manager 366 | terminationGracePeriodSeconds: 10 367 | -------------------------------------------------------------------------------- /examples/mongodb-sample.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: mongo-secret 5 | namespace: amberapp-system 6 | type: Opaque 7 | stringData: 8 | username: admin 9 | password: password 10 | --- 11 | apiVersion: ys.jibudata.com/v1alpha1 12 | kind: AppHook 13 | metadata: 14 | name: mongo-sample 15 | namespace: amberapp-system 16 | spec: 17 | name: mongo-sample 18 | appProvider: mongodb 19 | endPoint: "mongodb.mongodb-ns" 20 | secret: 21 | name: mongo-secret 22 | namespace: "amberapp-system" 23 | -------------------------------------------------------------------------------- /examples/mysql-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: mysql-demo 5 | --- 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | name: mysql-demo-svc 10 | namespace: mysql-demo 11 | labels: 12 | app: mysql-demo 13 | spec: 14 | ports: 15 | - port: 3306 16 | protocol: TCP 17 | targetPort: 3306 18 | selector: 19 | app: mysql-demo 20 | tier: mysql 21 | type: ClusterIP 22 | #clusterIP: None 23 | --- 24 | apiVersion: v1 25 | kind: PersistentVolumeClaim 26 | metadata: 27 | name: mysql-pv-claim 28 | namespace: mysql-demo 29 | labels: 30 | app: mysql-demo 31 | spec: 32 | accessModes: 33 | - ReadWriteOnce 34 | storageClassName: rook-ceph-block 35 | #storageClassName: managed-nfs-storage 36 | resources: 37 | requests: 38 | storage: 10Gi 39 | --- 40 | apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 41 | kind: Deployment 42 | metadata: 43 | name: mysql-demo-mysql 44 | namespace: mysql-demo 45 | labels: 46 | app: mysql-demo 47 | spec: 48 | selector: 49 | matchLabels: 50 | app: mysql-demo 51 | tier: mysql 52 | strategy: 53 | type: Recreate 54 | template: 55 | metadata: 56 | labels: 57 | app: mysql-demo 58 | tier: mysql 59 | spec: 60 | containers: 61 | - image: mysql:8.0 62 | name: mysql 63 | env: 64 | - name: MYSQL_ROOT_PASSWORD 65 | value: passw0rd 66 | ports: 67 | - containerPort: 3306 68 | name: mysql 69 | volumeMounts: 70 | - name: mysql-persistent-storage 71 | mountPath: /var/lib/mysql 72 | livenessProbe: 73 | exec: 74 | command: ["mysqladmin", "ping"] 75 | initialDelaySeconds: 30 76 | periodSeconds: 10 77 | timeoutSeconds: 5 78 | readinessProbe: 79 | exec: 80 | # Check we can execute queries over TCP (skip-networking is off). 81 | #command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"] 82 | command: 83 | - bash 84 | - "-c" 85 | - | 86 | set -ex 87 | mysql -h 127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD -e "SELECT 1" &> /dev/null 88 | initialDelaySeconds: 5 89 | periodSeconds: 2 90 | timeoutSeconds: 1 91 | # velero hook need to embed this to app pod 92 | #- image: registry.cn-shanghai.aliyuncs.com/jibudata/amberapp:0.0.3 93 | # name: app-hook 94 | # env: 95 | # - name: WATCH_NAMESPACE 96 | # value: amberapp-system 97 | # securityContext: 98 | # privileged: true 99 | volumes: 100 | - name: mysql-persistent-storage 101 | persistentVolumeClaim: 102 | claimName: mysql-pv-claim 103 | -------------------------------------------------------------------------------- /examples/mysql-generator/apps.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: mysql-generator 5 | --- 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | name: autotest-mysql 10 | namespace: mysql-generator 11 | labels: 12 | app: autotest 13 | spec: 14 | ports: 15 | - port: 3306 16 | selector: 17 | app: autotest 18 | tier: mysql 19 | clusterIP: None 20 | --- 21 | apiVersion: v1 22 | kind: PersistentVolumeClaim 23 | metadata: 24 | name: mysql-pv-claim 25 | namespace: mysql-generator 26 | labels: 27 | app: autotest 28 | spec: 29 | accessModes: 30 | - ReadWriteOnce 31 | storageClassName: managed-nfs-storage 32 | resources: 33 | requests: 34 | storage: 10Gi 35 | --- 36 | apiVersion: apps/v1 37 | kind: Deployment 38 | metadata: 39 | name: autotest-mysql 40 | namespace: mysql-generator 41 | labels: 42 | app: autotest 43 | spec: 44 | selector: 45 | matchLabels: 46 | app: autotest 47 | tier: mysql 48 | strategy: 49 | type: Recreate 50 | template: 51 | metadata: 52 | labels: 53 | app: autotest 54 | tier: mysql 55 | #annotations: 56 | # pre.hook.backup.velero.io/container: app-hook 57 | # pre.hook.backup.velero.io/command: '["/quiesce.sh"]' 58 | # post.hook.backup.velero.io/container: app-hook 59 | # post.hook.backup.velero.io/command: '["/unquiesce.sh"]' 60 | spec: 61 | containers: 62 | #- image: registry.cn-shanghai.aliyuncs.com/jibudata/app-hook:add-velero-example-latest 63 | # name: app-hook 64 | # env: 65 | # - name: NAMESPACE 66 | # valueFrom: 67 | # fieldRef: 68 | # fieldPath: metadata.namespace 69 | # - name: APP_NAME 70 | # value: autotest 71 | # - name: WATCH_NAMESPACE 72 | # value: app-hook-operator-system 73 | # securityContext: 74 | # privileged: true 75 | - image: mysql:5.7 76 | name: mysql 77 | env: 78 | - name: MYSQL_ROOT_PASSWORD 79 | value: password 80 | ports: 81 | - containerPort: 3306 82 | name: mysql 83 | volumeMounts: 84 | - name: mysql-persistent-storage 85 | mountPath: /var/lib/mysql 86 | volumes: 87 | - name: mysql-persistent-storage 88 | persistentVolumeClaim: 89 | claimName: mysql-pv-claim 90 | --- 91 | apiVersion: apps/v1 92 | kind: Deployment 93 | metadata: 94 | name: autotest-deployment 95 | namespace: mysql-generator 96 | labels: 97 | app: autotestapp 98 | spec: 99 | selector: 100 | matchLabels: 101 | app: autotestapp 102 | template: 103 | metadata: 104 | labels: 105 | app: autotestapp 106 | spec: 107 | initContainers: 108 | - name: init-mysql 109 | image: busybox:latest 110 | command: ['sh', '-c', 'echo -e "Checking MySQL"; while ! nc -z autotest-mysql 3306; do sleep 1; printf "-"; done; echo -e " >> MySQL started";'] 111 | containers: 112 | - name: autotestapp 113 | image: jibutech/app-test:main-latest 114 | ports: 115 | - containerPort: 80 116 | env: 117 | - name: AUTO_TEST_HOST 118 | value: autotest-mysql 119 | --- 120 | kind: Service 121 | apiVersion: v1 122 | metadata: 123 | name: rest-svc 124 | namespace: mysql-generator 125 | spec: 126 | ports: 127 | - nodePort: 30176 128 | protocol: TCP 129 | port: 2581 130 | targetPort: 2581 131 | selector: 132 | app: autotestapp 133 | type: NodePort 134 | -------------------------------------------------------------------------------- /examples/mysql-generator/common/__pycache__/log.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jibudata/amberapp/8cc9651da06ddd9ea19bb654cfd92784689277b0/examples/mysql-generator/common/__pycache__/log.cpython-36.pyc -------------------------------------------------------------------------------- /examples/mysql-generator/common/__pycache__/rest_api_client.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jibudata/amberapp/8cc9651da06ddd9ea19bb654cfd92784689277b0/examples/mysql-generator/common/__pycache__/rest_api_client.cpython-36.pyc -------------------------------------------------------------------------------- /examples/mysql-generator/common/log.py: -------------------------------------------------------------------------------- 1 | """ 2 | 封装log方法 3 | 4 | """ 5 | 6 | import logging 7 | import os 8 | import time 9 | 10 | LEVELS = { 11 | 'debug': logging.DEBUG, 12 | 'info': logging.INFO, 13 | 'warning': logging.WARNING, 14 | 'error': logging.ERROR, 15 | 'critical': logging.CRITICAL 16 | } 17 | 18 | logger = logging.getLogger() 19 | level = 'default' 20 | 21 | 22 | def create_file(filename): 23 | path = filename[0:filename.rfind('/')] 24 | if not os.path.isdir(path): 25 | os.makedirs(path) 26 | if not os.path.isfile(filename): 27 | fd = open(filename, mode='w', encoding='utf-8') 28 | fd.close() 29 | else: 30 | pass 31 | 32 | 33 | def set_handler(levels): 34 | if levels == 'error': 35 | logger.addHandler(MyLog.err_handler) 36 | logger.addHandler(MyLog.handler) 37 | 38 | 39 | def remove_handler(levels): 40 | if levels == 'error': 41 | logger.removeHandler(MyLog.err_handler) 42 | logger.removeHandler(MyLog.handler) 43 | 44 | 45 | def get_current_time(): 46 | return time.strftime(MyLog.date, time.localtime(time.time())) 47 | 48 | 49 | class MyLog: 50 | path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 51 | log_file = path+'/log/log.log' 52 | err_file = path+'/log/err.log' 53 | logger.setLevel(LEVELS.get(level, logging.NOTSET)) 54 | create_file(log_file) 55 | create_file(err_file) 56 | date = '%Y-%m-%d %H:%M:%S' 57 | 58 | handler = logging.FileHandler(log_file, encoding='utf-8') 59 | err_handler = logging.FileHandler(err_file, encoding='utf-8') 60 | 61 | @staticmethod 62 | def debug(log_meg): 63 | set_handler('debug') 64 | logger.debug("[DEBUG " + get_current_time() + "]" + log_meg) 65 | remove_handler('debug') 66 | 67 | @staticmethod 68 | def info(log_meg): 69 | set_handler('info') 70 | logger.info("[INFO " + get_current_time() + "]" + log_meg) 71 | remove_handler('info') 72 | 73 | @staticmethod 74 | def warning(log_meg): 75 | set_handler('warning') 76 | logger.warning("[WARNING " + get_current_time() + "]" + log_meg) 77 | remove_handler('warning') 78 | 79 | @staticmethod 80 | def error(log_meg): 81 | set_handler('error') 82 | logger.error("[ERROR " + get_current_time() + "]" + log_meg) 83 | remove_handler('error') 84 | 85 | @staticmethod 86 | def critical(log_meg): 87 | set_handler('critical') 88 | logger.error("[CRITICAL " + get_current_time() + "]" + log_meg) 89 | remove_handler('critical') 90 | 91 | 92 | if __name__ == "__main__": 93 | MyLog.debug("This is debug message") 94 | MyLog.info("This is info message") 95 | MyLog.warning("This is warning message") 96 | MyLog.error("This is error") 97 | MyLog.critical("This is critical message") 98 | 99 | -------------------------------------------------------------------------------- /examples/mysql-generator/common/rest_api_client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from common import log 3 | import json as complexjson 4 | 5 | logger = log.MyLog() 6 | 7 | 8 | class RestClient(): 9 | 10 | def __init__(self, api_root_url): 11 | self.api_root_url = api_root_url 12 | self.session = requests.session() 13 | 14 | def login(self, url, **kwargs): 15 | result = self.request(url, "GET", **kwargs) 16 | assert result.ok 17 | return self 18 | 19 | 20 | def get(self, url, **kwargs): 21 | return self.request(url, "GET", **kwargs) 22 | 23 | def post(self, url, data=None, json=None, **kwargs): 24 | return self.request(url, "POST", data, json, **kwargs) 25 | 26 | def put(self, url, data=None, **kwargs): 27 | return self.request(url, "PUT", data, **kwargs) 28 | 29 | def delete(self, url, **kwargs): 30 | return self.request(url, "DELETE", **kwargs) 31 | 32 | def patch(self, url, data=None, **kwargs): 33 | return self.request(url, "PATCH", data, **kwargs) 34 | 35 | def request(self, url, method, data=None, json=None, **kwargs): 36 | url = self.api_root_url + url 37 | headers = dict(**kwargs).get("headers") 38 | params = dict(**kwargs).get("params") 39 | files = dict(**kwargs).get("files") 40 | cookies = dict(**kwargs).get("cookies") 41 | self.request_log(url, method, data, json, params, headers, files, cookies) 42 | if method == "GET": 43 | return self.session.get(url, **kwargs) 44 | if method == "POST": 45 | return self.session.post(url, data, json, **kwargs) 46 | if method == "PUT": 47 | if json: 48 | data = complexjson.dumps(json) 49 | return self.session.put(url, data, **kwargs) 50 | if method == "DELETE": 51 | return self.session.delete(url, **kwargs) 52 | if method == "PATCH": 53 | if json: 54 | data = complexjson.dumps(json) 55 | return self.session.patch(url, data, **kwargs) 56 | 57 | def request_log(self, url, method, data=None, json=None, params=None, headers=None, files=None, cookies=None, **kwargs): 58 | logger.info("addr ==>> {}".format(url)) 59 | logger.info("method ==>> {}".format(method)) 60 | if(headers is not None): 61 | logger.info("headers ==>> {}".format(complexjson.dumps(headers, indent=4, ensure_ascii=False))) 62 | if (params is not None): 63 | logger.info("params ==>> {}".format(complexjson.dumps(params, indent=4, ensure_ascii=False))) 64 | if (data is not None): 65 | logger.info("data ==>> {}".format(complexjson.dumps(data, indent=4, ensure_ascii=False))) 66 | if (json is not None): 67 | logger.info("json ==>> {}".format(complexjson.dumps(json, indent=4, ensure_ascii=False))) 68 | if (files is not None): 69 | logger.info("files ==>> {}".format(files)) 70 | if (cookies is not None): 71 | logger.info("cookies ==>> {}".format(complexjson.dumps(cookies, indent=4, ensure_ascii=False))) 72 | 73 | -------------------------------------------------------------------------------- /examples/mysql-generator/db-generator.py: -------------------------------------------------------------------------------- 1 | import sys, getopt 2 | import random 3 | import json 4 | import time 5 | import signal 6 | 7 | from requests_toolbelt import MultipartEncoder 8 | from common import rest_api_client, log 9 | from datetime import datetime 10 | 11 | logger = log.MyLog() 12 | 13 | TYPE_INSERT = '--insert' 14 | TYPE_DUMP = '--dump' 15 | 16 | def signal_handler(sig, frame): 17 | sys.exit(0) 18 | 19 | class StatefulAppUtils: 20 | 21 | def __init__(self, url): 22 | self.url = url 23 | self.session = rest_api_client.RestClient(url).session 24 | 25 | def db_insert(self, name): 26 | dateTimeObj = datetime.now() 27 | timestampStr = dateTimeObj.strftime("%d-%b-%Y (%H:%M:%S.%f)") 28 | name = name + timestampStr 29 | age = random.randint(10, 50) 30 | user_info = { 31 | "name": name, 32 | "age": age 33 | } 34 | result = self.session.post(self.url + "/user/add", json=user_info) 35 | return json.loads(result.text) 36 | 37 | def db_query_all(self): 38 | result = self.session.get(self.url + "/user/all") 39 | return json.loads(result.text) 40 | 41 | def db_query(self, user_id): 42 | result = self.session.get(self.url + "/user/" + str(user_id)) 43 | # print(result.text) 44 | return json.loads(result.text) 45 | 46 | def db_delete(self, user_id): 47 | result = self.session.post(self.url + "/user/delete/" + str(user_id)) 48 | # print(result.text) 49 | return json.loads(result.text) 50 | 51 | 52 | if __name__ == '__main__': 53 | signal.signal(signal.SIGINT, signal_handler) 54 | 55 | type = '' 56 | if len(sys.argv) != 2: 57 | print('db-generator.py', TYPE_INSERT, '|', TYPE_DUMP) 58 | sys.exit(1) 59 | 60 | if sys.argv[1] == TYPE_INSERT: 61 | type = TYPE_INSERT 62 | elif sys.argv[1] == TYPE_DUMP: 63 | type = TYPE_DUMP 64 | else: 65 | print('db-generator.py', TYPE_INSERT, '|', TYPE_DUMP) 66 | sys.exit(1) 67 | 68 | stateful_app_utils = StatefulAppUtils("http://127.0.0.1:30176/jibu") 69 | if type == TYPE_DUMP: 70 | result = stateful_app_utils.db_query_all() 71 | #print(result) 72 | for item in result: 73 | print(item) 74 | sys.exit(0) 75 | 76 | while True: 77 | time.sleep(3) 78 | result = stateful_app_utils.db_insert("test-") 79 | print('saved db record: ', result) 80 | 81 | -------------------------------------------------------------------------------- /examples/mysql-sample.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: mysql-secret 5 | namespace: amberapp-system 6 | type: Opaque 7 | stringData: 8 | username: root 9 | password: password 10 | --- 11 | apiVersion: ys.jibudata.com/v1alpha1 12 | kind: AppHook 13 | metadata: 14 | name: mysql-sample 15 | namespace: amberapp-system 16 | spec: 17 | name: mysql-sample 18 | appProvider: mysql 19 | endPoint: "mysql.mysql-ns" 20 | databases: 21 | - test 22 | secret: 23 | name: "mysql-secret" 24 | namespace: "amberapp-system" 25 | -------------------------------------------------------------------------------- /examples/postgres-sample.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: postgres-secret 5 | namespace: amberapp-system 6 | type: Opaque 7 | stringData: 8 | username: postgresadmin 9 | password: Test1234 10 | --- 11 | apiVersion: ys.jibudata.com/v1alpha1 12 | kind: AppHook 13 | metadata: 14 | name: postgres-sample 15 | namespace: amberapp-system 16 | spec: 17 | name: postgres-sample 18 | appProvider: postgres 19 | endPoint: "postgres.postgres-ns" 20 | databases: 21 | - postgresdb 22 | secret: 23 | name: "postgres-secret" 24 | namespace: "amberapp-system" 25 | -------------------------------------------------------------------------------- /examples/v1_deployment_amberapp-mysql-demo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: controller-manager 6 | name: amberapp-mysql-demo 7 | namespace: amberapp-system 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: mysql-demo 13 | template: 14 | metadata: 15 | labels: 16 | app: mysql-demo 17 | spec: 18 | containers: 19 | - image: registry.cn-shanghai.aliyuncs.com/jibudata/app-hook:0.0.4 20 | name: hook-runner 21 | env: 22 | - name: WATCH_NAMESPACE 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: metadata.namespace 26 | resources: 27 | limits: 28 | cpu: 100m 29 | memory: 30Mi 30 | requests: 31 | cpu: 100m 32 | memory: 20Mi 33 | serviceAccountName: amberapp-controller-manager 34 | terminationGracePeriodSeconds: 10 35 | -------------------------------------------------------------------------------- /examples/workload/helm-mysql/README.md: -------------------------------------------------------------------------------- 1 | # install mysql through helm chart. 2 | 3 | ```bash 4 | 5 | [root@gyj-dev ~]# helm repo add bitnami https://charts.bitnami.com/bitnami 6 | "bitnami" already exists with the same configuration, skipping 7 | 8 | [root@gyj-dev ~]# helm search repo bitnami/mysql -l |grep 8.2.3 9 | bitnami/mysql 8.2.3 8.0.22 Chart to create a Highly available MySQL cluster 10 | 11 | 12 | [root@gyj-dev mysql-example]# helm install mysql bitnami/mysql --version 8.2.3 -n mysql-test --create-namespace -f ./mysql-values.yaml 13 | NAME: mysql 14 | LAST DEPLOYED: Tue Jan 11 21:49:10 2022 15 | NAMESPACE: mysql-test 16 | STATUS: deployed 17 | REVISION: 1 18 | TEST SUITE: None 19 | NOTES: 20 | ** Please be patient while the chart is being deployed ** 21 | 22 | Tip: 23 | 24 | Watch the deployment status using the command: kubectl get pods -w --namespace mysql-test 25 | 26 | Services: 27 | 28 | echo Primary: mysql-primary.mysql-test.svc.cluster.local:3306 29 | echo Secondary: mysql-secondary.mysql-test.svc.cluster.local:3306 30 | 31 | Administrator credentials: 32 | 33 | echo Username: root 34 | echo Password : $(kubectl get secret --namespace mysql-test mysql -o jsonpath="{.data.mysql-root-password}" | base64 --decode) 35 | 36 | To connect to your database: 37 | 38 | 1. Run a pod that you can use as a client: 39 | 40 | kubectl run mysql-client --rm --tty -i --restart='Never' --image docker.io/bitnami/mysql:8.0.22-debian-10-r44 --namespace mysql-test --command -- bash 41 | 42 | 2. To connect to primary service (read/write): 43 | 44 | mysql -h mysql-primary.mysql-test.svc.cluster.local -uroot -p my_database 45 | 46 | 3. To connect to secondary service (read-only): 47 | 48 | mysql -h mysql-secondary.mysql-test.svc.cluster.local -uroot -p my_database 49 | 50 | To upgrade this helm chart: 51 | 52 | 1. Obtain the password as described on the 'Administrator credentials' section and set the 'root.password' parameter as shown below: 53 | 54 | ROOT_PASSWORD=$(kubectl get secret --namespace mysql-test mysql} -o jsonpath="{.data.mysql-root-password}" | base64 --decode) 55 | helm upgrade mysql bitnami/mysql --set auth.rootPassword=$ROOT_PASSWORD 56 | 57 | ``` 58 | 59 | check mysql status 60 | 61 | ```bash 62 | 63 | [root@gyj-dev mysql-example]# kubectl -n mysql-test get pods 64 | NAME READY STATUS RESTARTS AGE 65 | mysql-primary-0 1/1 Running 0 33m 66 | mysql-secondary-0 1/1 Running 0 33m 67 | [root@gyj-dev mysql-example]# kubectl -n mysql-test get pvc 68 | NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE 69 | data-mysql-primary-0 Bound pvc-717c331d-19ca-4e4a-80d3-fd81cdca722e 10Gi RWO managed-nfs-storage 33m 70 | data-mysql-secondary-0 Bound pvc-6886d9e8-0f0b-4310-9c9b-457adfc68a80 10Gi RWO managed-nfs-storage 33m 71 | [root@gyj-dev mysql-example]# kubectl -n mysql-test get svc 72 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 73 | mysql-primary ClusterIP 10.96.11.223 3306/TCP 33m 74 | mysql-primary-headless ClusterIP None 3306/TCP 33m 75 | mysql-secondary ClusterIP 10.96.252.16 3306/TCP 33m 76 | mysql-secondary-headless ClusterIP None 3306/TCP 33m 77 | ``` -------------------------------------------------------------------------------- /examples/workload/helm-mysql/mysql-values.yaml: -------------------------------------------------------------------------------- 1 | architecture: replication 2 | 3 | auth: 4 | rootPassword: "randompassw0rd" 5 | replicationUser: replicator 6 | replicationPassword: "randompassw0rd" 7 | 8 | primary: 9 | extraEnvVars: 10 | - name: TZ 11 | value: "Asia/Shanghai" 12 | 13 | extraFlags: "--innodb-doublewrite=OFF" 14 | 15 | persistence: 16 | enabled: true 17 | storageClass: "managed-nfs-storage" 18 | accessModes: 19 | - ReadWriteOnce 20 | size: 10Gi 21 | 22 | secondary: 23 | replicaCount: 1 24 | extraEnvVars: 25 | - name: TZ 26 | value: "Asia/Shanghai" 27 | 28 | extraFlags: "--innodb-doublewrite=OFF" 29 | 30 | persistence: 31 | enabled: true 32 | storageClass: "managed-nfs-storage" 33 | accessModes: 34 | - ReadWriteOnce 35 | size: 10Gi 36 | 37 | volumePermissions: 38 | enabled: true 39 | 40 | metrics: 41 | enabled: false 42 | serviceMonitor: 43 | enabled: false 44 | additionalLabels: 45 | monitoring.supremind.com: 'true' -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jibudata/amberapp 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-redis/redis/v8 v8.11.5 7 | github.com/go-sql-driver/mysql v1.6.0 8 | github.com/lib/pq v1.10.3 9 | github.com/onsi/ginkgo v1.16.5 10 | github.com/onsi/gomega v1.18.1 11 | github.com/pkg/errors v0.9.1 12 | github.com/spf13/cobra v1.1.1 13 | github.com/spf13/pflag v1.0.5 14 | go.mongodb.org/mongo-driver v1.7.3 15 | golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 16 | k8s.io/api v0.21.2 17 | k8s.io/apimachinery v0.21.2 18 | k8s.io/client-go v0.21.2 19 | k8s.io/klog/v2 v2.10.0 20 | sigs.k8s.io/controller-runtime v0.9.2 21 | ) 22 | 23 | require ( 24 | cloud.google.com/go v0.54.0 // indirect 25 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 26 | github.com/Azure/go-autorest/autorest v0.11.12 // indirect 27 | github.com/Azure/go-autorest/autorest/adal v0.9.5 // indirect 28 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 29 | github.com/Azure/go-autorest/logger v0.2.0 // indirect 30 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 31 | github.com/beorn7/perks v1.0.1 // indirect 32 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 35 | github.com/evanphx/json-patch v4.11.0+incompatible // indirect 36 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect 37 | github.com/fsnotify/fsnotify v1.4.9 // indirect 38 | github.com/go-logr/logr v0.4.0 // indirect 39 | github.com/go-logr/zapr v0.4.0 // indirect 40 | github.com/go-stack/stack v1.8.0 // indirect 41 | github.com/gogo/protobuf v1.3.2 // indirect 42 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 43 | github.com/golang/protobuf v1.5.2 // indirect 44 | github.com/golang/snappy v0.0.1 // indirect 45 | github.com/google/go-cmp v0.5.5 // indirect 46 | github.com/google/gofuzz v1.1.0 // indirect 47 | github.com/google/uuid v1.1.2 // indirect 48 | github.com/googleapis/gnostic v0.5.5 // indirect 49 | github.com/hashicorp/golang-lru v0.5.4 // indirect 50 | github.com/imdario/mergo v0.3.12 // indirect 51 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 52 | github.com/json-iterator/go v1.1.11 // indirect 53 | github.com/klauspost/compress v1.13.6 // indirect 54 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 55 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 56 | github.com/modern-go/reflect2 v1.0.1 // indirect 57 | github.com/nxadm/tail v1.4.8 // indirect 58 | github.com/prometheus/client_golang v1.11.0 // indirect 59 | github.com/prometheus/client_model v0.2.0 // indirect 60 | github.com/prometheus/common v0.26.0 // indirect 61 | github.com/prometheus/procfs v0.6.0 // indirect 62 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 63 | github.com/xdg-go/scram v1.0.2 // indirect 64 | github.com/xdg-go/stringprep v1.0.2 // indirect 65 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect 66 | go.uber.org/atomic v1.7.0 // indirect 67 | go.uber.org/multierr v1.6.0 // indirect 68 | go.uber.org/zap v1.17.0 // indirect 69 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect 70 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 // indirect 71 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect 72 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect 73 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect 74 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect 75 | golang.org/x/text v0.3.6 // indirect 76 | golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect 77 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 78 | google.golang.org/appengine v1.6.7 // indirect 79 | google.golang.org/protobuf v1.26.0 // indirect 80 | gopkg.in/inf.v0 v0.9.1 // indirect 81 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 82 | gopkg.in/yaml.v2 v2.4.0 // indirect 83 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 84 | k8s.io/apiextensions-apiserver v0.21.2 // indirect 85 | k8s.io/component-base v0.21.2 // indirect 86 | k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 // indirect 87 | k8s.io/utils v0.0.0-20210527160623-6fdb442a123b // indirect 88 | sigs.k8s.io/structured-merge-diff/v4 v4.1.0 // indirect 89 | sigs.k8s.io/yaml v1.2.0 // indirect 90 | ) 91 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | */ -------------------------------------------------------------------------------- /hack/build-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rootDir=$(pwd) 4 | 5 | echo "root dir: ${rootDir}" 6 | 7 | GOPROXY=${GOPROXY:-"https://proxy.golang.org,direct"} 8 | 9 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 10 | COMMITID=$(git rev-parse --short HEAD) 11 | TAG=${TAG:-"${BRANCH}.${COMMITID}"} 12 | 13 | REGISTRY=${REGISTRY:-"registry.cn-shanghai.aliyuncs.com/jibutech"} 14 | IMAGENAME=${IMAGENAME:-"amberapp"} 15 | 16 | DOCKERFILE=${DOCKERFILE:-"$rootDir/Dockerfile"} 17 | FULLTAG="$REGISTRY/$IMAGENAME:$TAG" 18 | 19 | git show --oneline -s > VERSION 20 | echo "compiled time: `date`" >> VERSION 21 | 22 | echo "$DOCKERFILE $FULLTAG" 23 | docker build -f $DOCKERFILE -t $FULLTAG --build-arg GOPROXY=${GOPROXY} $rootDir 24 | if [ $? -ne 0 ];then 25 | echo "failed to build $FULLTAG" 26 | exit 1 27 | fi 28 | 29 | 30 | -------------------------------------------------------------------------------- /hack/create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 6 ]; then 4 | echo "Illegal number of parameters, need 6, actual: $#" 5 | echo "Usage: create.sh " 6 | exit 1 7 | fi 8 | 9 | hookname=$1 10 | provider=$2 11 | endpoint=$3 12 | dbname=$4 13 | username=$5 14 | password=$6 15 | 16 | echo "create hook" 17 | ./apphook create -n $hookname -a $provider -e $endpoint -u $username -p $password --databases $dbname 18 | -------------------------------------------------------------------------------- /hack/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is only for velero hook integration, which needs to add annotations to 3 | # application pod, to specify hook container and commands. 4 | # As the apphook binary is executing inside application pod, we need a way to 5 | # apply required cluster role to the command, which need the hack below. 6 | 7 | if [ "$#" -ne 4 ]; then 8 | echo "Illegal number of parameters" 9 | echo "Usage: ./hack/prepare.sh " 10 | exit 1 11 | fi 12 | 13 | namespace=$1 14 | appname=$2 15 | hookname=$3 16 | dbname=$4 17 | 18 | echo "remove hook" 19 | apphook delete -n $hookname 20 | 21 | echo "remove annotation of pod" 22 | kubectl annotate pod -n $namespace -l app=$appname pre.hook.backup.velero.io/command- 23 | kubectl annotate pod -n $namespace -l app=$appname pre.hook.backup.velero.io/container- 24 | kubectl annotate pod -n $namespace -l app=$appname post.hook.backup.velero.io/command- 25 | kubectl annotate pod -n $namespace -l app=$appname post.hook.backup.velero.io/container- 26 | 27 | podname=`kubectl get pod -n $namespace -l app=$appname | grep -v NAME | awk '{print $1}'` 28 | echo "pod name: $podname" 29 | 30 | servicename=`kubectl get svc -n $namespace | grep -v NAME | awk '{print $1}'` 31 | 32 | endpoint=$servicename"."$namespace 33 | echo "endpoint name: $endpoint" 34 | 35 | echo "create hook" 36 | apphook create -n $hookname -a mysql -e $endpoint -u root -p passw0rd --databases $dbname 37 | 38 | # Hack: copy the root config to the container 39 | kubectl cp ~/.kube/config -n $namespace -c app-hook $podname:/root/ 40 | 41 | echo "annotate pod" 42 | kubectl annotate pod -n $namespace -l app=$appname \ 43 | pre.hook.backup.velero.io/command='["/bin/bash", "-c", "./quiesce.sh"]' \ 44 | pre.hook.backup.velero.io/container=app-hook \ 45 | post.hook.backup.velero.io/command='["/bin/bash", "-c", "./unquiesce.sh"]' \ 46 | post.hook.backup.velero.io/container=app-hook 47 | 48 | -------------------------------------------------------------------------------- /hack/push-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rootDir=$(pwd) 4 | 5 | echo "root dir: ${rootDir}" 6 | 7 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 8 | COMMITID=$(git rev-parse --short HEAD) 9 | TAG=${TAG:-"${BRANCH}.${COMMITID}"} 10 | 11 | REGISTRY=${REGISTRY:-"registry.cn-shanghai.aliyuncs.com/jibutech"} 12 | 13 | IMAGENAME=${IMAGENAME:-"amberapp"} 14 | 15 | FULLTAG="$REGISTRY/$IMAGENAME:$TAG" 16 | DEVTAG="$REGISTRY/$IMAGENAME:${BRANCH}-latest" 17 | 18 | # pushd $rootDir 19 | docker push $FULLTAG 20 | if [ $? -ne 0 ];then 21 | echo "failed to push $FULLTAG" 22 | exit 1 23 | fi 24 | 25 | function docker_push () { 26 | old_tag=$1 27 | new_tag=$2 28 | docker tag $1 $2 29 | docker push $2 30 | if [ $? -ne 0 ];then 31 | echo "failed to push $new_tag " 32 | exit 1 33 | fi 34 | 35 | echo "completes to push image $new_tag " 36 | } 37 | 38 | docker_push $FULLTAG $DEVTAG 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /hack/quiesce.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 1 ]; then 4 | if [[ ! -z "${NAMESPACE}" ]] && [[ ! -z "${APP_NAME}" ]]; then 5 | hookname="${NAMESPACE}-${APP_NAME}" 6 | else 7 | echo "Illegal number of parameters" 8 | echo "Usage: quiesce.sh " 9 | exit 1 10 | fi 11 | else 12 | hookname=$1 13 | fi 14 | 15 | echo "hook name: ${hookname}" 16 | 17 | /apphook quiesce -n $hookname -w 18 | -------------------------------------------------------------------------------- /hack/unquiesce.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 1 ]; then 4 | if [[ ! -z "${NAMESPACE}" ]] && [[ ! -z "${APP_NAME}" ]]; then 5 | hookname="${NAMESPACE}-${APP_NAME}" 6 | else 7 | echo "Illegal number of parameters" 8 | echo "Usage: unquiesce.sh " 9 | exit 1 10 | fi 11 | else 12 | hookname=$1 13 | fi 14 | 15 | echo "hook name: ${hookname}" 16 | 17 | ./apphook unquiesce -n $hookname 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 24 | // to ensure that exec-entrypoint and run can make use of them. 25 | _ "k8s.io/client-go/plugin/pkg/client/auth" 26 | 27 | "k8s.io/apimachinery/pkg/runtime" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/healthz" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | 34 | "github.com/jibudata/amberapp/api/v1alpha1" 35 | "github.com/jibudata/amberapp/controllers" 36 | drivermanager "github.com/jibudata/amberapp/controllers/driver" 37 | //+kubebuilder:scaffold:imports 38 | ) 39 | 40 | var ( 41 | scheme = runtime.NewScheme() 42 | setupLog = ctrl.Log.WithName("setup") 43 | ) 44 | 45 | func init() { 46 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 47 | 48 | utilruntime.Must(v1alpha1.AddToScheme(scheme)) 49 | //+kubebuilder:scaffold:scheme 50 | } 51 | 52 | func main() { 53 | var metricsAddr string 54 | var enableLeaderElection bool 55 | var probeAddr string 56 | 57 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 58 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 59 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 60 | "Enable leader election for controller manager. "+ 61 | "Enabling this will ensure there is only one active controller manager.") 62 | opts := zap.Options{ 63 | Development: true, 64 | } 65 | opts.BindFlags(flag.CommandLine) 66 | flag.Parse() 67 | 68 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 69 | setupLog.V(1).Info("debug mode enabled") 70 | 71 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 72 | Scheme: scheme, 73 | MetricsBindAddress: metricsAddr, 74 | Port: 9443, 75 | HealthProbeBindAddress: probeAddr, 76 | LeaderElection: enableLeaderElection, 77 | LeaderElectionID: "d0f11165.jibudata.com", 78 | }) 79 | if err != nil { 80 | setupLog.Error(err, "unable to start manager") 81 | os.Exit(1) 82 | } 83 | 84 | if err = (&controllers.AppHookReconciler{ 85 | Client: mgr.GetClient(), 86 | Scheme: mgr.GetScheme(), 87 | AppMap: make(map[string]*drivermanager.DriverManager), 88 | }).SetupWithManager(mgr); err != nil { 89 | setupLog.Error(err, "unable to create controller", "controller", "AppHook") 90 | os.Exit(1) 91 | } 92 | //+kubebuilder:scaffold:builder 93 | 94 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 95 | setupLog.Error(err, "unable to set up health check") 96 | os.Exit(1) 97 | } 98 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 99 | setupLog.Error(err, "unable to set up ready check") 100 | os.Exit(1) 101 | } 102 | 103 | setupLog.Info("starting manager") 104 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 105 | setupLog.Error(err, "problem running manager") 106 | os.Exit(1) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/appconfig/appconfig.go: -------------------------------------------------------------------------------- 1 | package appconfig 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const ( 8 | ConnectionTimeout = 10 * time.Second 9 | ) 10 | 11 | type Config struct { 12 | Name string 13 | Host string 14 | Databases []string 15 | Username string 16 | Password string 17 | Provider string 18 | Operation string 19 | QuiesceFromPrimary bool 20 | QuiesceTimeout time.Duration 21 | Params map[string]string 22 | } 23 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package client 18 | 19 | import ( 20 | "github.com/pkg/errors" 21 | "k8s.io/apimachinery/pkg/runtime" 22 | "k8s.io/client-go/rest" 23 | "k8s.io/client-go/tools/clientcmd" 24 | "sigs.k8s.io/controller-runtime/pkg/client" 25 | ) 26 | 27 | type Client struct { 28 | client.Client 29 | *rest.Config 30 | } 31 | 32 | // Config returns a *rest.Config, using in-cluster configuration. 33 | func NewConfig() (*rest.Config, error) { 34 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 35 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) 36 | 37 | clientConfig, err := kubeConfig.ClientConfig() 38 | if err != nil { 39 | return nil, errors.Wrap(err, "error finding Kubernetes API server config") 40 | } 41 | 42 | return clientConfig, nil 43 | } 44 | 45 | func NewClient(restConfig *rest.Config, scheme *runtime.Scheme) (*Client, error) { 46 | client, err := client.New(restConfig, client.Options{Scheme: scheme}) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | kubeclient := &Client{ 52 | Client: client, 53 | Config: restConfig, 54 | } 55 | 56 | return kubeclient, nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/cmd/apphook/apphook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package apphook 18 | 19 | import ( 20 | "github.com/spf13/cobra" 21 | "k8s.io/apimachinery/pkg/runtime" 22 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 23 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 24 | 25 | "github.com/jibudata/amberapp/api/v1alpha1" 26 | "github.com/jibudata/amberapp/pkg/client" 27 | "github.com/jibudata/amberapp/pkg/cmd/create" 28 | "github.com/jibudata/amberapp/pkg/cmd/delete" 29 | "github.com/jibudata/amberapp/pkg/cmd/quiesce" 30 | "github.com/jibudata/amberapp/pkg/cmd/unquiesce" 31 | ) 32 | 33 | var ( 34 | scheme = runtime.NewScheme() 35 | ) 36 | 37 | func init() { 38 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 39 | utilruntime.Must(v1alpha1.AddToScheme(scheme)) 40 | } 41 | 42 | func NewCommand(baseName string) (*cobra.Command, error) { 43 | 44 | kubeconfig, err := client.NewConfig() 45 | if err != nil { 46 | return nil, err 47 | } 48 | kubeclient, err := client.NewClient(kubeconfig, scheme) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | cmd := &cobra.Command{} 54 | cmd.AddCommand( 55 | create.NewCommand(kubeclient), 56 | delete.NewCommand(kubeclient), 57 | quiesce.NewCommand(kubeclient), 58 | unquiesce.NewCommand(kubeclient), 59 | ) 60 | return cmd, nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/cmd/create/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package create 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "time" 23 | 24 | "github.com/spf13/cobra" 25 | "github.com/spf13/pflag" 26 | corev1 "k8s.io/api/core/v1" 27 | "k8s.io/apimachinery/pkg/api/errors" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/types" 30 | "k8s.io/apimachinery/pkg/util/wait" 31 | 32 | "github.com/jibudata/amberapp/api/v1alpha1" 33 | "github.com/jibudata/amberapp/pkg/client" 34 | "github.com/jibudata/amberapp/pkg/cmd" 35 | "github.com/jibudata/amberapp/pkg/util" 36 | ) 37 | 38 | const ( 39 | UserNameKey = "username" 40 | PasswordKey = "password" 41 | 42 | DefaultPollInterval = 1 * time.Second 43 | DefaultPollTimeout = 30 * time.Second 44 | ) 45 | 46 | type CreateOptions struct { 47 | Name string 48 | Provider string 49 | Endpoint string 50 | Databases []string 51 | UserName string 52 | Password string 53 | } 54 | 55 | func NewCommand(client *client.Client) *cobra.Command { 56 | 57 | option := &CreateOptions{} 58 | 59 | c := &cobra.Command{ 60 | Use: "create", 61 | Short: "Create a Database configuration", 62 | Long: "Create a Database configraution which will be used for quiesce/resume operations", 63 | Run: func(c *cobra.Command, args []string) { 64 | cmd.CheckError(option.Validate(c, client)) 65 | cmd.CheckError(option.Run(client)) 66 | }, 67 | } 68 | 69 | option.BindFlags(c.Flags(), c) 70 | 71 | return c 72 | } 73 | 74 | func (c *CreateOptions) BindFlags(flags *pflag.FlagSet, command *cobra.Command) { 75 | flags.StringVarP(&c.Name, "name", "n", "", "database configration name") 76 | _ = command.MarkFlagRequired("name") 77 | flags.StringVarP(&c.Provider, "app-provider", "a", "", "database provider, e.g., MySQL") 78 | _ = command.MarkFlagRequired("app-provider") 79 | flags.StringVarP(&c.Endpoint, "endpoint", "e", "", "database endpoint, e.g., 'service.namespace', or 'ip:port'") 80 | _ = command.MarkFlagRequired("endpoint") 81 | flags.StringArrayVar(&c.Databases, "databases", nil, "databases created inside the DB") 82 | _ = command.MarkFlagRequired("databases") 83 | flags.StringVarP(&c.UserName, "username", "u", "", "username of the DB") 84 | _ = command.MarkFlagRequired("username") 85 | flags.StringVarP(&c.Password, "password", "p", "", "password for the DB user") 86 | _ = command.MarkFlagRequired("password") 87 | } 88 | 89 | func (c *CreateOptions) Validate(command *cobra.Command, kubeclient *client.Client) error { 90 | // Check WATCH_NAMESPACE, and if namespace exits, apphook operator is running 91 | namespace, err := util.GetOperatorNamespace() 92 | if err != nil { 93 | return err 94 | } 95 | ns := &corev1.Namespace{} 96 | err = kubeclient.Get( 97 | context.TODO(), 98 | types.NamespacedName{ 99 | Name: namespace, 100 | }, 101 | ns) 102 | 103 | if err != nil { 104 | return err 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (c *CreateOptions) createSecret(kubeclient *client.Client, secretName, namespace string) error { 111 | 112 | secret := &corev1.Secret{ 113 | ObjectMeta: metav1.ObjectMeta{ 114 | Name: secretName, 115 | Namespace: namespace, 116 | }, 117 | Data: map[string][]byte{ 118 | UserNameKey: []byte(c.UserName), 119 | PasswordKey: []byte(c.Password), 120 | }, 121 | } 122 | 123 | err := kubeclient.Create(context.TODO(), secret) 124 | if err != nil { 125 | if errors.IsAlreadyExists(err) { 126 | fmt.Printf("secret already exists: %s, namespace: %s\n", c.Name, namespace) 127 | return nil 128 | } 129 | return err 130 | } 131 | 132 | err = wait.PollImmediate(DefaultPollInterval, DefaultPollTimeout, func() (bool, error) { 133 | foundSecret := &corev1.Secret{} 134 | err := kubeclient.Get( 135 | context.TODO(), 136 | types.NamespacedName{ 137 | Namespace: namespace, 138 | Name: secretName, 139 | }, 140 | foundSecret) 141 | 142 | if err != nil { 143 | return false, err 144 | } 145 | return true, nil 146 | }) 147 | 148 | return err 149 | } 150 | 151 | func (c *CreateOptions) waitUntilReady(kubeclient *client.Client, namespace string) (error, bool) { 152 | crName := c.Name + "-hook" 153 | done := false 154 | 155 | err := wait.PollImmediate(DefaultPollInterval, DefaultPollTimeout, func() (bool, error) { 156 | foundHook := &v1alpha1.AppHook{} 157 | err := kubeclient.Get( 158 | context.TODO(), 159 | types.NamespacedName{ 160 | Namespace: namespace, 161 | Name: crName, 162 | }, 163 | foundHook) 164 | 165 | if err != nil { 166 | return false, err 167 | } 168 | if foundHook.Status.Phase == v1alpha1.HookReady { 169 | done = true 170 | return true, nil 171 | } 172 | return false, nil 173 | }) 174 | 175 | return err, done 176 | } 177 | 178 | func (c *CreateOptions) createApphookCR(kubeclient *client.Client, secretName, namespace string) error { 179 | 180 | hookCR := &v1alpha1.AppHook{ 181 | TypeMeta: metav1.TypeMeta{ 182 | APIVersion: v1alpha1.GroupVersion.String(), 183 | Kind: "AppHook", 184 | }, 185 | ObjectMeta: metav1.ObjectMeta{ 186 | Name: c.Name + "-hook", 187 | Namespace: namespace, 188 | }, 189 | Spec: v1alpha1.AppHookSpec{ 190 | Name: c.Name, 191 | AppProvider: c.Provider, 192 | EndPoint: c.Endpoint, 193 | Databases: c.Databases, 194 | Secret: corev1.SecretReference{ 195 | Name: secretName, 196 | Namespace: namespace, 197 | }, 198 | }, 199 | } 200 | 201 | err := kubeclient.Create(context.TODO(), hookCR) 202 | if err != nil { 203 | if errors.IsAlreadyExists(err) { 204 | fmt.Printf("apphook already exists: %s, namespace: %s\n", c.Name, namespace) 205 | return nil 206 | } 207 | return err 208 | } 209 | return nil 210 | } 211 | 212 | func (c *CreateOptions) Run(kubeclient *client.Client) error { 213 | secretName := c.Name + "-token" 214 | crName := c.Name + "-hook" 215 | namespace, err := util.GetOperatorNamespace() 216 | if err != nil { 217 | return err 218 | } 219 | 220 | fmt.Printf("Create secret from username and password, secret name: %s, namespace: %s\n", secretName, namespace) 221 | err = c.createSecret(kubeclient, secretName, namespace) 222 | if err != nil { 223 | return err 224 | } 225 | 226 | fmt.Printf("Create apphook: %s, namespace: %s\n", c.Name, namespace) 227 | err = c.createApphookCR(kubeclient, secretName, namespace) 228 | if err != nil { 229 | return err 230 | } 231 | 232 | //fmt.Printf("Created apphook: %s, use `kubectl get apphook -n %s %s` to look at the status\n", crName, namespace, crName) 233 | fmt.Printf("Waiting for db get ready: %s, namespace: %s\n", crName, namespace) 234 | err, done := c.waitUntilReady(kubeclient, namespace) 235 | if err != nil { 236 | fmt.Printf("wait for hook ready error: %s, namespace: %s\n", crName, namespace) 237 | return err 238 | } 239 | if done { 240 | fmt.Printf("Database is successfully connected and ready: %s, namespace: %s\n", crName, namespace) 241 | } 242 | 243 | return err 244 | } 245 | -------------------------------------------------------------------------------- /pkg/cmd/delete/delete.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package delete 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/spf13/cobra" 24 | "github.com/spf13/pflag" 25 | corev1 "k8s.io/api/core/v1" 26 | "k8s.io/apimachinery/pkg/api/errors" 27 | "k8s.io/apimachinery/pkg/types" 28 | 29 | "github.com/jibudata/amberapp/api/v1alpha1" 30 | "github.com/jibudata/amberapp/pkg/client" 31 | "github.com/jibudata/amberapp/pkg/cmd" 32 | "github.com/jibudata/amberapp/pkg/util" 33 | ) 34 | 35 | type DeleteOptions struct { 36 | Name string 37 | } 38 | 39 | func NewCommand(client *client.Client) *cobra.Command { 40 | 41 | option := &DeleteOptions{} 42 | 43 | c := &cobra.Command{ 44 | Use: "delete", 45 | Short: "Delete a Database configuration", 46 | Long: "Delete a Database configraution", 47 | Run: func(c *cobra.Command, args []string) { 48 | cmd.CheckError(option.Validate(c, client)) 49 | cmd.CheckError(option.Run(client)) 50 | }, 51 | } 52 | 53 | option.BindFlags(c.Flags(), c) 54 | 55 | return c 56 | } 57 | 58 | func (d *DeleteOptions) BindFlags(flags *pflag.FlagSet, command *cobra.Command) { 59 | flags.StringVarP(&d.Name, "name", "n", "", "database configration name") 60 | _ = command.MarkFlagRequired("name") 61 | } 62 | 63 | func (d *DeleteOptions) Validate(command *cobra.Command, kubeclient *client.Client) error { 64 | // Check WATCH_NAMESPACE, and if namespace exits, apphook operator is running 65 | namespace, err := util.GetOperatorNamespace() 66 | if err != nil { 67 | return err 68 | } 69 | ns := &corev1.Namespace{} 70 | err = kubeclient.Get( 71 | context.TODO(), 72 | types.NamespacedName{ 73 | Name: namespace, 74 | }, 75 | ns) 76 | 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (c *DeleteOptions) deleteSecret(kubeclient *client.Client, secretName, namespace string) error { 85 | foundSecret := &corev1.Secret{} 86 | err := kubeclient.Get( 87 | context.TODO(), 88 | types.NamespacedName{ 89 | Namespace: namespace, 90 | Name: secretName, 91 | }, 92 | foundSecret) 93 | 94 | if err != nil { 95 | return err 96 | } 97 | 98 | return kubeclient.Delete(context.TODO(), foundSecret) 99 | } 100 | 101 | func (d *DeleteOptions) deleteHookCR(kubeclient *client.Client, namespace string) error { 102 | crName := d.Name + "-hook" 103 | 104 | foundHook := &v1alpha1.AppHook{} 105 | err := kubeclient.Get( 106 | context.TODO(), 107 | types.NamespacedName{ 108 | Namespace: namespace, 109 | Name: crName, 110 | }, 111 | foundHook) 112 | 113 | if err != nil { 114 | return err 115 | } 116 | 117 | return kubeclient.Delete(context.TODO(), foundHook) 118 | } 119 | 120 | func (d *DeleteOptions) Run(kubeclient *client.Client) error { 121 | secretName := d.Name + "-token" 122 | crName := d.Name + "-hook" 123 | namespace, err := util.GetOperatorNamespace() 124 | if err != nil { 125 | return err 126 | } 127 | 128 | err = d.deleteSecret(kubeclient, secretName, namespace) 129 | if err != nil { 130 | if errors.IsNotFound(err) { 131 | fmt.Printf("secret not found: %s, namespace: %s\n", secretName, namespace) 132 | } else { 133 | return err 134 | } 135 | } else { 136 | fmt.Printf("Delete secret success: %s, namespace: %s\n", secretName, namespace) 137 | } 138 | 139 | err = d.deleteHookCR(kubeclient, namespace) 140 | if err != nil { 141 | if errors.IsNotFound(err) { 142 | fmt.Printf("hook not found: %s, namespace: %s\n", crName, namespace) 143 | return nil 144 | } else { 145 | return err 146 | } 147 | } else { 148 | fmt.Printf("Delete database configuration success: %s, namespace: %s\n", crName, namespace) 149 | } 150 | 151 | return err 152 | } 153 | -------------------------------------------------------------------------------- /pkg/cmd/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package cmd 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | ) 24 | 25 | // CheckError prints err to stderr and exits with code 1 if err is not nil. Otherwise, it is a 26 | // no-op. 27 | func CheckError(err error) { 28 | if err != nil { 29 | if err != context.Canceled { 30 | fmt.Fprintf(os.Stderr, "An error occurred: %v\n", err) 31 | } 32 | os.Exit(1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/cmd/quiesce/quiesce.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package quiesce 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "time" 23 | 24 | "github.com/spf13/cobra" 25 | "github.com/spf13/pflag" 26 | corev1 "k8s.io/api/core/v1" 27 | "k8s.io/apimachinery/pkg/types" 28 | "k8s.io/apimachinery/pkg/util/wait" 29 | 30 | "github.com/jibudata/amberapp/api/v1alpha1" 31 | "github.com/jibudata/amberapp/pkg/client" 32 | "github.com/jibudata/amberapp/pkg/cmd" 33 | "github.com/jibudata/amberapp/pkg/util" 34 | ) 35 | 36 | const ( 37 | DefaultPollInterval = 1 * time.Second 38 | DefaultPollTimeout = 60 * time.Second 39 | ) 40 | 41 | type QuiesceOptions struct { 42 | Name string 43 | Wait bool 44 | //Database string 45 | } 46 | 47 | func NewCommand(client *client.Client) *cobra.Command { 48 | 49 | option := &QuiesceOptions{} 50 | 51 | c := &cobra.Command{ 52 | Use: "quiesce", 53 | Short: "Quiesce a Database", 54 | Long: "Quiesce a Database", 55 | Run: func(c *cobra.Command, args []string) { 56 | cmd.CheckError(option.Validate(c, client)) 57 | cmd.CheckError(option.Run(client)) 58 | }, 59 | } 60 | 61 | option.BindFlags(c.Flags(), c) 62 | 63 | return c 64 | } 65 | 66 | func (q *QuiesceOptions) BindFlags(flags *pflag.FlagSet, c *cobra.Command) { 67 | flags.StringVarP(&q.Name, "name", "n", "", "database configration name") 68 | _ = c.MarkFlagRequired("name") 69 | flags.BoolVarP(&q.Wait, "wait", "w", false, "wait for quiescd") 70 | //flags.StringVarP(&c.Database, "database", "d", "", "name of the database instance") 71 | } 72 | 73 | func (q *QuiesceOptions) Validate(command *cobra.Command, kubeclient *client.Client) error { 74 | // Check WATCH_NAMESPACE, and if namespace exits, apphook operator is running 75 | namespace, err := util.GetOperatorNamespace() 76 | if err != nil { 77 | return err 78 | } 79 | ns := &corev1.Namespace{} 80 | err = kubeclient.Get( 81 | context.TODO(), 82 | types.NamespacedName{ 83 | Name: namespace, 84 | }, 85 | ns) 86 | 87 | if err != nil { 88 | return err 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func (q *QuiesceOptions) updateHookCR(kubeclient *client.Client, namespace string) error { 95 | crName := q.Name + "-hook" 96 | 97 | foundHook := &v1alpha1.AppHook{} 98 | err := kubeclient.Get( 99 | context.TODO(), 100 | types.NamespacedName{ 101 | Namespace: namespace, 102 | Name: crName, 103 | }, 104 | foundHook) 105 | 106 | if err != nil { 107 | return err 108 | } 109 | 110 | switch foundHook.Status.Phase { 111 | // Valid states 112 | case v1alpha1.HookReady: 113 | case v1alpha1.HookUNQUIESCED: 114 | // Invalid 115 | case v1alpha1.HookNotReady: 116 | return fmt.Errorf("hook CR %s not ready yet", foundHook.Name) 117 | case v1alpha1.HookQUIESCED: 118 | return fmt.Errorf("hook CR %s already quiesced", foundHook.Name) 119 | case v1alpha1.HookQUIESCEINPROGRESS: 120 | return fmt.Errorf("hook CR %s quiesce already in progress", foundHook.Name) 121 | case v1alpha1.HookUNQUIESCEINPROGRESS: 122 | return fmt.Errorf("hook CR %s unquiesce is in progress", foundHook.Name) 123 | } 124 | 125 | foundHook.Spec.OperationType = v1alpha1.QUIESCE 126 | 127 | return kubeclient.Update(context.TODO(), foundHook) 128 | } 129 | 130 | func (q *QuiesceOptions) waitUntilQuiesced(kubeclient *client.Client, namespace string) (error, bool) { 131 | crName := q.Name + "-hook" 132 | done := false 133 | 134 | err := wait.PollImmediate(DefaultPollInterval, DefaultPollTimeout, func() (bool, error) { 135 | foundHook := &v1alpha1.AppHook{} 136 | err := kubeclient.Get( 137 | context.TODO(), 138 | types.NamespacedName{ 139 | Namespace: namespace, 140 | Name: crName, 141 | }, 142 | foundHook) 143 | 144 | if err != nil { 145 | return false, err 146 | } 147 | if foundHook.Status.Phase == v1alpha1.HookQUIESCED { 148 | done = true 149 | return true, nil 150 | } 151 | return false, nil 152 | }) 153 | 154 | return err, done 155 | } 156 | 157 | func (q *QuiesceOptions) Run(kubeclient *client.Client) error { 158 | crName := q.Name + "-hook" 159 | namespace, err := util.GetOperatorNamespace() 160 | if err != nil { 161 | return err 162 | } 163 | 164 | err = q.updateHookCR(kubeclient, namespace) 165 | if err == nil { 166 | fmt.Printf("Update hook success: %s, namespace: %s\n", crName, namespace) 167 | } else { 168 | return err 169 | } 170 | 171 | if q.Wait { 172 | startTime := time.Now() 173 | fmt.Printf("Waiting for db get quiesced: %s, namespace: %s\n", crName, namespace) 174 | err, done := q.waitUntilQuiesced(kubeclient, namespace) 175 | doneTime := time.Now() 176 | duration := doneTime.Sub(startTime) 177 | if err != nil { 178 | fmt.Printf("wait for hook into quiesced state error: %s, namespace: %s\n", crName, namespace) 179 | return err 180 | } 181 | if done { 182 | fmt.Printf("Database is successfully quiesced: %s, namespace: %s, duration: %s\n", crName, namespace, duration) 183 | } 184 | } 185 | 186 | return nil 187 | } 188 | -------------------------------------------------------------------------------- /pkg/cmd/unquiesce/unquiesce.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package unquiesce 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "time" 23 | 24 | "github.com/spf13/cobra" 25 | "github.com/spf13/pflag" 26 | corev1 "k8s.io/api/core/v1" 27 | "k8s.io/apimachinery/pkg/types" 28 | "k8s.io/apimachinery/pkg/util/wait" 29 | 30 | "github.com/jibudata/amberapp/api/v1alpha1" 31 | "github.com/jibudata/amberapp/pkg/client" 32 | "github.com/jibudata/amberapp/pkg/cmd" 33 | "github.com/jibudata/amberapp/pkg/util" 34 | ) 35 | 36 | const ( 37 | DefaultPollInterval = 1 * time.Second 38 | DefaultPollTimeout = 20 * time.Second 39 | ) 40 | 41 | type UnquiesceOptions struct { 42 | Name string 43 | //Database string 44 | } 45 | 46 | func NewCommand(client *client.Client) *cobra.Command { 47 | 48 | option := &UnquiesceOptions{} 49 | 50 | c := &cobra.Command{ 51 | Use: "unquiesce", 52 | Short: "Unquiesce a Database", 53 | Long: "Unquiesce a Database which has been quiesced", 54 | Run: func(c *cobra.Command, args []string) { 55 | cmd.CheckError(option.Validate(c, client)) 56 | cmd.CheckError(option.Run(client)) 57 | }, 58 | } 59 | 60 | option.BindFlags(c.Flags(), c) 61 | 62 | return c 63 | } 64 | 65 | func (u *UnquiesceOptions) BindFlags(flags *pflag.FlagSet, c *cobra.Command) { 66 | flags.StringVarP(&u.Name, "name", "n", "", "database configration name") 67 | _ = c.MarkFlagRequired("name") 68 | //flags.StringVarP(&c.Database, "database", "d", "", "name of the database instance") 69 | } 70 | 71 | func (u *UnquiesceOptions) Validate(command *cobra.Command, kubeclient *client.Client) error { 72 | // Check WATCH_NAMESPACE, and if namespace exits, apphook operator is running 73 | namespace, err := util.GetOperatorNamespace() 74 | if err != nil { 75 | return err 76 | } 77 | ns := &corev1.Namespace{} 78 | err = kubeclient.Get( 79 | context.TODO(), 80 | types.NamespacedName{ 81 | Name: namespace, 82 | }, 83 | ns) 84 | 85 | if err != nil { 86 | return err 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func (u *UnquiesceOptions) updateHookCR(kubeclient *client.Client, namespace string) error { 93 | crName := u.Name + "-hook" 94 | 95 | foundHook := &v1alpha1.AppHook{} 96 | err := kubeclient.Get( 97 | context.TODO(), 98 | types.NamespacedName{ 99 | Namespace: namespace, 100 | Name: crName, 101 | }, 102 | foundHook) 103 | 104 | if err != nil { 105 | return err 106 | } 107 | 108 | switch foundHook.Status.Phase { 109 | case v1alpha1.HookReady: 110 | return fmt.Errorf("hook CR %s not quiesced yet", foundHook.Name) 111 | case v1alpha1.HookNotReady: 112 | return fmt.Errorf("hook CR %s not ready yet", foundHook.Name) 113 | case v1alpha1.HookUNQUIESCED: 114 | return fmt.Errorf("hook CR %s already unquiesced", foundHook.Name) 115 | case v1alpha1.HookQUIESCEINPROGRESS: 116 | return fmt.Errorf("hook CR %s quiesce still in progress, please wait", foundHook.Name) 117 | case v1alpha1.HookUNQUIESCEINPROGRESS: 118 | return fmt.Errorf("hook CR %s unquiesce already in progress", foundHook.Name) 119 | case v1alpha1.HookQUIESCED: 120 | } 121 | 122 | foundHook.Spec.OperationType = v1alpha1.UNQUIESCE 123 | 124 | return kubeclient.Update(context.TODO(), foundHook) 125 | } 126 | 127 | func (u *UnquiesceOptions) waitUntilUnquiesced(kubeclient *client.Client, namespace string) (error, bool) { 128 | crName := u.Name + "-hook" 129 | done := false 130 | 131 | err := wait.PollImmediate(DefaultPollInterval, DefaultPollTimeout, func() (bool, error) { 132 | foundHook := &v1alpha1.AppHook{} 133 | err := kubeclient.Get( 134 | context.TODO(), 135 | types.NamespacedName{ 136 | Namespace: namespace, 137 | Name: crName, 138 | }, 139 | foundHook) 140 | 141 | if err != nil { 142 | return false, err 143 | } 144 | if foundHook.Status.Phase == v1alpha1.HookUNQUIESCED { 145 | done = true 146 | return true, nil 147 | } 148 | return false, nil 149 | }) 150 | 151 | return err, done 152 | } 153 | 154 | func (u *UnquiesceOptions) Run(kubeclient *client.Client) error { 155 | crName := u.Name + "-hook" 156 | namespace, err := util.GetOperatorNamespace() 157 | if err != nil { 158 | return err 159 | } 160 | 161 | err = u.updateHookCR(kubeclient, namespace) 162 | if err == nil { 163 | fmt.Printf("Update hook success: %s, namespace: %s\n", crName, namespace) 164 | } else { 165 | return err 166 | } 167 | 168 | startTime := time.Now() 169 | err, done := u.waitUntilUnquiesced(kubeclient, namespace) 170 | doneTime := time.Now() 171 | duration := doneTime.Sub(startTime) 172 | if err != nil { 173 | return err 174 | } 175 | if done { 176 | fmt.Printf("Database is successfully unquiesced: %s, namespace: %s, duration: %s\n", crName, namespace, duration) 177 | } 178 | 179 | return err 180 | } 181 | -------------------------------------------------------------------------------- /pkg/mongo/mongo.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | "go.mongodb.org/mongo-driver/mongo/readpref" 13 | ctrllog "sigs.k8s.io/controller-runtime/pkg/log" 14 | 15 | "github.com/jibudata/amberapp/api/v1alpha1" 16 | "github.com/jibudata/amberapp/pkg/appconfig" 17 | ) 18 | 19 | type MG struct { 20 | config appconfig.Config 21 | } 22 | 23 | var log = ctrllog.Log.WithName("mongo") 24 | 25 | func (mg *MG) Init(appConfig appconfig.Config) error { 26 | mg.config = appConfig 27 | return nil 28 | } 29 | 30 | func (mg *MG) Connect() error { 31 | var err error 32 | var result bson.M 33 | var opts *options.RunCmdOptions 34 | 35 | log.Info("mongodb connecting...") 36 | 37 | client, err := getMongodbClient(mg.config) 38 | if err != nil { 39 | return err 40 | } 41 | // list all databases to check the connection is ok 42 | filter := bson.D{{}} 43 | _, err = client.ListDatabaseNames(context.TODO(), filter) 44 | if err != nil { 45 | log.Error(err, "failed to list databases") 46 | return err 47 | } 48 | 49 | // Get hello result, determine if it's secondary 50 | if !mg.config.QuiesceFromPrimary { 51 | opts = options.RunCmd().SetReadPreference(readpref.Secondary()) 52 | } 53 | db := client.Database("admin") 54 | 55 | // Run hello 56 | err = mg.runHello(db, opts, &result) 57 | if err != nil { 58 | log.Error(err, "failed to run hello") 59 | return err 60 | } 61 | 62 | secondary := result["secondary"] 63 | 64 | if secondary == false { 65 | log.Info("Warning, not connected to secondary for quiesce") 66 | } else { 67 | log.Info("connected to secondary") 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (mg *MG) Prepare() (*v1alpha1.PreservedConfig, error) { 74 | return nil, nil 75 | } 76 | 77 | func (mg *MG) Quiesce() (*v1alpha1.QuiesceResult, error) { 78 | var err error 79 | var result bson.M 80 | var opts *options.RunCmdOptions 81 | 82 | log.Info("mongodb quiesce in progress") 83 | client, err := getMongodbClient(mg.config) 84 | if err != nil { 85 | return nil, err 86 | } 87 | db := client.Database("admin") 88 | if !mg.config.QuiesceFromPrimary { 89 | opts = options.RunCmd().SetReadPreference(readpref.Secondary()) 90 | } 91 | 92 | err = mg.runHello(db, opts, &result) 93 | if err != nil { 94 | log.Error(err, "failed to run hello") 95 | return nil, err 96 | } 97 | 98 | secondary := result["secondary"] 99 | primary := false 100 | if secondary == false { 101 | primary = true 102 | } 103 | 104 | mongoResult := &v1alpha1.MongoResult{ 105 | IsPrimary: primary, 106 | } 107 | if result["me"] == nil { 108 | // standalone mongo 109 | mongoResult.MongoEndpoint = mg.config.Host 110 | } else { 111 | mongoResult.MongoEndpoint = result["me"].(string) 112 | } 113 | quiResult := &v1alpha1.QuiesceResult{Mongo: mongoResult} 114 | 115 | isLocked, err := isDBLocked(db, opts) 116 | if err != nil { 117 | log.Error(err, "failed to check lock status of database to quiesce", "instance name", mg.config.Name) 118 | return quiResult, err 119 | } 120 | if isLocked { 121 | log.Info("mongodb already locked", "instacne", mg.config.Name) 122 | return quiResult, nil 123 | } 124 | 125 | log.Info("quiesce mongo", "endpoint", mongoResult.MongoEndpoint, "primary", primary) 126 | 127 | cmdResult := db.RunCommand(context.TODO(), bson.D{{Key: "fsync", Value: 1}, {Key: "lock", Value: true}}, opts) 128 | if cmdResult.Err() != nil { 129 | log.Error(cmdResult.Err(), fmt.Sprintf("failed to quiesce %s", mg.config.Name)) 130 | return quiResult, cmdResult.Err() 131 | } 132 | 133 | return quiResult, nil 134 | } 135 | 136 | func (mg *MG) Unquiesce(prev *v1alpha1.PreservedConfig) error { 137 | log.Info("mongodb unquiesce in progress") 138 | 139 | client, err := getMongodbClient(mg.config) 140 | if err != nil { 141 | return err 142 | } 143 | db := client.Database("admin") 144 | var opts *options.RunCmdOptions 145 | if !mg.config.QuiesceFromPrimary { 146 | opts = options.RunCmd().SetReadPreference(readpref.Secondary()) 147 | } 148 | 149 | isLocked := true 150 | for isLocked { 151 | isLocked, err = isDBLocked(db, opts) 152 | if err != nil { 153 | log.Error(err, "failed to check lock status of database to unquiesce", "instance name", mg.config.Name) 154 | return err 155 | } 156 | if !isLocked { 157 | return nil 158 | } 159 | 160 | result := db.RunCommand(context.TODO(), bson.D{{Key: "fsyncUnlock", Value: 1}}, opts) 161 | if result.Err() != nil { 162 | // fsyncUnlock called when not locked 163 | if strings.Contains(result.Err().Error(), "not locked") { 164 | return nil 165 | } 166 | log.Error(result.Err(), fmt.Sprintf("failed to unquiesce %s", mg.config.Name)) 167 | return result.Err() 168 | } 169 | } 170 | 171 | return nil 172 | } 173 | 174 | func (mg *MG) runHello(db *mongo.Database, opts *options.RunCmdOptions, result *bson.M) error { 175 | cmd := bson.D{{Key: "hello", Value: 1}} 176 | 177 | err := db.RunCommand(context.TODO(), cmd, opts).Decode(result) 178 | if err != nil { 179 | log.Error(err, "failed to run hello command") 180 | return err 181 | } 182 | 183 | return nil 184 | } 185 | 186 | func getMongodbClient(appConfig appconfig.Config) (*mongo.Client, error) { 187 | ctx, cancel := context.WithTimeout(context.Background(), appconfig.ConnectionTimeout) 188 | defer cancel() 189 | 190 | host := fmt.Sprintf("mongodb://%s:%s@%s", 191 | appConfig.Username, 192 | appConfig.Password, 193 | appConfig.Host) 194 | clientOptions := options.Client().ApplyURI(host) 195 | client, err := mongo.Connect(ctx, clientOptions) 196 | if err != nil { 197 | log.Error(err, fmt.Sprintf("failed to connect mongodb %s", appConfig.Name)) 198 | return client, err 199 | } 200 | return client, nil 201 | } 202 | 203 | type LockResult struct { 204 | LockInfo []interface{} 205 | } 206 | 207 | func isDBLocked(db *mongo.Database, opts *options.RunCmdOptions) (bool, error) { 208 | result := db.RunCommand(context.TODO(), bson.D{{Key: "lockInfo", Value: 1}}, opts) 209 | if result.Err() != nil { 210 | return false, result.Err() 211 | } 212 | 213 | resultData, err := result.DecodeBytes() 214 | if err != nil { 215 | return false, err 216 | } 217 | 218 | lockResult := &LockResult{} 219 | err = json.Unmarshal([]byte(resultData.String()), lockResult) 220 | if err != nil { 221 | return false, err 222 | } 223 | if len(lockResult.LockInfo) > 0 { 224 | return true, nil 225 | } 226 | return false, nil 227 | } 228 | -------------------------------------------------------------------------------- /pkg/mysql/mysql.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | _ "github.com/go-sql-driver/mysql" 8 | "sigs.k8s.io/controller-runtime/pkg/log" 9 | 10 | amberappApi "github.com/jibudata/amberapp/api/v1alpha1" 11 | "github.com/jibudata/amberapp/pkg/appconfig" 12 | ) 13 | 14 | const ( 15 | TableLockCmd = "FLUSH TABLES WITH READ LOCK;" 16 | TableUnLockCmd = "UNLOCK TABLES;" 17 | InstanceLockCmd = "LOCK INSTANCE FOR BACKUP;" 18 | InstanceUnLockCmd = "UNLOCK INSTANCE;" 19 | ) 20 | 21 | type MYSQL struct { 22 | config appconfig.Config 23 | db *sql.DB 24 | } 25 | 26 | func (m *MYSQL) Init(appConfig appconfig.Config) error { 27 | m.config = appConfig 28 | if m.db != nil { 29 | m.db.Close() 30 | } 31 | m.db = nil 32 | dbs := m.config.Databases 33 | if len(dbs) == 0 { 34 | err := fmt.Errorf("no database specified in %s", m.config.Name) 35 | log.Log.Error(err, "") 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | func (m *MYSQL) Connect() error { 42 | var err error 43 | log.Log.Info("mysql init") 44 | dbs := m.config.Databases 45 | if len(dbs) == 0 { 46 | err = fmt.Errorf("no database specified in %s", m.config.Name) 47 | log.Log.Error(err, "") 48 | return err 49 | } 50 | for _, database := range dbs { 51 | dsn := fmt.Sprintf("%s:%s@%s(%s)/%s", m.config.Username, m.config.Password, "tcp", m.config.Host, database) 52 | db, err := sql.Open("mysql", dsn) 53 | if err != nil { 54 | log.Log.Error(err, fmt.Sprintf("failed to init connection to mysql database %s, in %s", database, m.config.Name)) 55 | return err 56 | } 57 | err = db.Ping() 58 | if err != nil { 59 | log.Log.Error(err, fmt.Sprintf("cannot access mysql databases %s in %s", database, m.config.Name)) 60 | return err 61 | } 62 | db.Close() 63 | } 64 | log.Log.Info("mysql connected") 65 | return nil 66 | } 67 | 68 | func (m *MYSQL) Prepare() (*amberappApi.PreservedConfig, error) { 69 | return nil, nil 70 | } 71 | 72 | func (m *MYSQL) Quiesce() (*amberappApi.QuiesceResult, error) { 73 | var err error 74 | log.Log.Info("mysql quiesce in progress...") 75 | 76 | dsn := fmt.Sprintf("%s:%s@%s(%s)/%s", m.config.Username, m.config.Password, "tcp", m.config.Host, m.config.Databases[0]) 77 | m.db, err = sql.Open("mysql", dsn) 78 | if err != nil { 79 | log.Log.Error(err, fmt.Sprintf("failed to init connection to mysql database %s, in %s", m.config.Databases[0], m.config.Name)) 80 | return nil, err 81 | } 82 | 83 | return nil, m.mysqlLock() 84 | } 85 | 86 | func (m *MYSQL) Unquiesce(prev *amberappApi.PreservedConfig) error { 87 | log.Log.Info("mysql unquiesce in progress...") 88 | return m.mysqlUnlock() 89 | } 90 | 91 | func (m *MYSQL) mysqlLock() error { 92 | cmd := m.getLockCmd() 93 | _, err := m.db.Exec(cmd) 94 | return err 95 | } 96 | 97 | func (m *MYSQL) mysqlUnlock() error { 98 | if m.db == nil { 99 | return nil 100 | } 101 | cmd := m.getUnLockCmd() 102 | _, err := m.db.Exec(cmd) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | return m.db.Close() 108 | } 109 | 110 | func (m *MYSQL) getLockCmd() string { 111 | if m.config.Params == nil { 112 | // default table lock 113 | return TableLockCmd 114 | } 115 | 116 | lockMethod, ok := m.config.Params[amberappApi.LockMethod] 117 | if ok { 118 | switch lockMethod { 119 | case amberappApi.MysqlTableLock: 120 | return TableLockCmd 121 | case amberappApi.MysqlInstanceLock: 122 | return InstanceLockCmd 123 | default: 124 | return TableLockCmd 125 | } 126 | } 127 | 128 | return TableLockCmd 129 | } 130 | 131 | func (m *MYSQL) getUnLockCmd() string { 132 | if m.config.Params == nil { 133 | // default table lock 134 | return TableUnLockCmd 135 | } 136 | 137 | lockMethod, ok := m.config.Params[amberappApi.LockMethod] 138 | if ok { 139 | switch lockMethod { 140 | case amberappApi.MysqlTableLock: 141 | return TableUnLockCmd 142 | case amberappApi.MysqlInstanceLock: 143 | return InstanceUnLockCmd 144 | default: 145 | return TableUnLockCmd 146 | } 147 | } 148 | 149 | return TableUnLockCmd 150 | } 151 | -------------------------------------------------------------------------------- /pkg/postgres/postgres.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | _ "github.com/lib/pq" 10 | "sigs.k8s.io/controller-runtime/pkg/log" 11 | 12 | "github.com/jibudata/amberapp/api/v1alpha1" 13 | "github.com/jibudata/amberapp/pkg/appconfig" 14 | ) 15 | 16 | const ( 17 | // < v15.0 18 | PG_START_BACKUP = "pg_start_backup" 19 | PG_STOP_BACKUP = "pg_stop_backup" 20 | // >= v15.0 21 | PG_BACKUP_START = "pg_backup_start" 22 | PG_BACKUP_STOP = "pg_backup_stop" 23 | ) 24 | 25 | type PG struct { 26 | config appconfig.Config 27 | db *sql.DB 28 | version string 29 | } 30 | 31 | func (pg *PG) Init(appConfig appconfig.Config) error { 32 | pg.config = appConfig 33 | return nil 34 | } 35 | 36 | func (pg *PG) Connect() error { 37 | var err error 38 | log.Log.Info("postgres connecting") 39 | 40 | connectionConfigStrings := pg.getConnectionString() 41 | if len(connectionConfigStrings) == 0 { 42 | return fmt.Errorf("no database found in %s", pg.config.Name) 43 | } 44 | 45 | for i := 0; i < len(connectionConfigStrings); i++ { 46 | pg.db, err = sql.Open("postgres", connectionConfigStrings[i]) 47 | if err != nil { 48 | log.Log.Error(err, "cannot connect to postgres") 49 | return err 50 | } 51 | 52 | err = pg.db.Ping() 53 | if err != nil { 54 | log.Log.Error(err, fmt.Sprintf("cannot connect to postgres database %s", pg.config.Databases[i])) 55 | return err 56 | } 57 | 58 | err = pg.getVersion() 59 | if err != nil { 60 | return err 61 | } 62 | 63 | pg.db.Close() 64 | } 65 | log.Log.Info("connected to postgres") 66 | return nil 67 | } 68 | 69 | func (pg *PG) Prepare() (*v1alpha1.PreservedConfig, error) { 70 | return nil, nil 71 | } 72 | 73 | func (pg *PG) Quiesce() (*v1alpha1.QuiesceResult, error) { 74 | var err error 75 | log.Log.Info("postgres quiesce in progress...") 76 | 77 | backupName := "test" 78 | fastStartString := "true" 79 | 80 | connectionConfigStrings := pg.getConnectionString() 81 | if len(connectionConfigStrings) == 0 { 82 | return nil, fmt.Errorf("no database found in %s", pg.config.Name) 83 | } 84 | 85 | for i := 0; i < len(connectionConfigStrings); i++ { 86 | pg.db, err = sql.Open("postgres", connectionConfigStrings[i]) 87 | if err != nil { 88 | log.Log.Error(err, "cannot connect to postgres") 89 | return nil, err 90 | } 91 | 92 | queryStr := fmt.Sprintf("select %s('%s', %s);", pg.getQuiesceCmd(), backupName, fastStartString) 93 | 94 | result, queryErr := pg.db.Query(queryStr) 95 | 96 | if queryErr != nil { 97 | if strings.Contains(queryErr.Error(), "backup is already in progress") { 98 | pg.db.Close() 99 | continue 100 | } 101 | log.Log.Error(queryErr, "could not start postgres backup") 102 | return nil, queryErr 103 | } 104 | 105 | var snapshotLocation string 106 | result.Next() 107 | 108 | scanErr := result.Scan(&snapshotLocation) 109 | if scanErr != nil { 110 | log.Log.Error(scanErr, "Postgres backup apparently started but could not understand server response") 111 | return nil, scanErr 112 | } 113 | log.Log.Info(fmt.Sprintf("Successfully reach consistent recovery state at %s", snapshotLocation)) 114 | pg.db.Close() 115 | } 116 | return nil, nil 117 | } 118 | 119 | func (pg *PG) Unquiesce(prev *v1alpha1.PreservedConfig) error { 120 | var err error 121 | log.Log.Info("postgres unquiesce in progress...") 122 | connectionConfigStrings := pg.getConnectionString() 123 | if len(connectionConfigStrings) == 0 { 124 | return fmt.Errorf("no database found in %s", pg.config.Name) 125 | } 126 | 127 | for i := 0; i < len(connectionConfigStrings); i++ { 128 | pg.db, err = sql.Open("postgres", connectionConfigStrings[i]) 129 | if err != nil { 130 | log.Log.Error(err, "cannot connect to postgres") 131 | return err 132 | } 133 | defer pg.db.Close() 134 | 135 | result, queryErr := pg.db.Query(fmt.Sprintf("select %s();", pg.getUnQuiesceCmd())) 136 | if queryErr != nil { 137 | if strings.Contains(queryErr.Error(), "not in progress") { 138 | pg.db.Close() 139 | continue 140 | } 141 | log.Log.Error(queryErr, "could not stop backup") 142 | return queryErr 143 | } 144 | 145 | var snapshotLocation string 146 | result.Next() 147 | 148 | scanErr := result.Scan(&snapshotLocation) 149 | if scanErr != nil { 150 | log.Log.Error(scanErr, "Postgres backup apparently stopped but could not understand server response") 151 | return scanErr 152 | } 153 | } 154 | return nil 155 | } 156 | 157 | func (pg *PG) getConnectionString() []string { 158 | var dbname string 159 | var connstr []string 160 | 161 | if len(pg.config.Databases) == 0 { 162 | log.Log.Error(fmt.Errorf("no database found in %s", pg.config.Name), "") 163 | return connstr 164 | } 165 | 166 | for i := 0; i < len(pg.config.Databases); i++ { 167 | dbname = pg.config.Databases[i] 168 | connstr = append(connstr, fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", pg.config.Host, pg.config.Username, pg.config.Password, dbname)) 169 | } 170 | return connstr 171 | } 172 | 173 | func (pg *PG) getVersion() error { 174 | var version string 175 | result, queryErr := pg.db.Query("show server_version;") 176 | if queryErr != nil { 177 | log.Log.Error(queryErr, "could get postgres version") 178 | return queryErr 179 | } 180 | 181 | result.Next() 182 | scanErr := result.Scan(&version) 183 | if scanErr != nil { 184 | log.Log.Error(scanErr, "scan postgres version with error") 185 | return scanErr 186 | } 187 | 188 | pg.version = strings.Split(version, " ")[0] 189 | log.Log.Info("get postgres version", "version", version, "instance", pg.config.Name) 190 | 191 | return nil 192 | } 193 | 194 | func (pg *PG) isVersionAboveV15() bool { 195 | aboveV15 := false 196 | if pg.version != "" { 197 | version, err := strconv.ParseFloat(pg.version, 64) 198 | if err != nil { 199 | log.Log.Error(err, "failed to convert version to number", "version", pg.version) 200 | } else { 201 | if version >= 15.0 { 202 | aboveV15 = true 203 | } 204 | } 205 | } 206 | 207 | return aboveV15 208 | } 209 | 210 | func (pg *PG) getQuiesceCmd() string { 211 | if pg.isVersionAboveV15() { 212 | return PG_BACKUP_START 213 | } 214 | return PG_START_BACKUP 215 | } 216 | 217 | func (pg *PG) getUnQuiesceCmd() string { 218 | if pg.isVersionAboveV15() { 219 | return PG_BACKUP_STOP 220 | } 221 | return PG_STOP_BACKUP 222 | } 223 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | 17 | package util 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | ) 23 | 24 | // GetOperatorNamespace returns the Namespace the operator should be watching for changes 25 | func GetOperatorNamespace() (string, error) { 26 | // WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE 27 | // which specifies the Namespace to watch. 28 | // An empty value means the operator is running with cluster scope. 29 | var watchNamespaceEnvVar = "WATCH_NAMESPACE" 30 | 31 | ns, found := os.LookupEnv(watchNamespaceEnvVar) 32 | if !found { 33 | return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar) 34 | } 35 | return ns, nil 36 | } 37 | --------------------------------------------------------------------------------