├── .github └── CODEOWNERS ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Contribution.md ├── LICENCE.txt ├── Makefile ├── PROJECT ├── README.md ├── SECURITY.md ├── _base-operator ├── Dockerfile ├── Makefile ├── controllers │ ├── access.go │ ├── definition_manager.go │ ├── myresource_controller.go │ └── resource_manager.go ├── main.go ├── reconciler │ ├── access.go │ ├── access_permissions.go │ ├── definition_manager.go │ ├── generic_controller.go │ ├── instance_updater.go │ ├── reconcile_finalizer.go │ ├── reconcile_runner.go │ ├── reconcile_status.go │ ├── resource_manager.go │ └── string_helper.go └── v1 │ └── resource.go ├── cmd ├── base │ └── version.go ├── build.go ├── code.go ├── common.go ├── create.go ├── image.go ├── init.go ├── root.go ├── update.go ├── validate.go └── version │ └── version.go ├── docs ├── README.md ├── book.toml └── src │ ├── .DS_Store │ ├── README.md │ ├── SUMMARY.md │ ├── craft_cli.md │ ├── img │ └── craft_create_flow.png │ ├── operator_source_code │ ├── README.md │ └── controller_reconciler.md │ ├── quick_setup │ └── README.md │ └── tutorial │ ├── README.md │ ├── controller_file.md │ ├── deploy_operator.md │ ├── docker_exit_codes.md │ ├── namespace_file.md │ ├── operator_file.md │ ├── resource_dockerfile.md │ ├── resource_file.md │ ├── step1.md │ └── step2.md ├── examples └── wordpress-operator │ ├── README.md │ ├── config │ ├── controller.json │ ├── deploy │ │ └── operator.yaml │ └── resource.json │ ├── resource │ ├── Dockerfile │ ├── initwordpress.sh │ ├── kubectl │ ├── templates │ │ ├── kustomization.yaml │ │ ├── mysql-deployment.yaml │ │ └── wordpress-deployment.yaml │ └── wordpress_manager.py │ └── sample │ └── wordpress.yaml ├── go.mod ├── go.sum ├── images ├── Declarative_Operator.jpeg └── craft_quick_start.gif ├── init ├── controller.json └── resource.json ├── main.go ├── scripts ├── build.sh └── install.sh └── utils ├── utils.go └── validation.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. 2 | # Unless a later match takes precedence, these users will be requested 3 | # for review when someone opens a pull request. 4 | * @maheswarasunil @vnzongzna 5 | #ECCN:Open Source -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | build/ 3 | notes 4 | bin/craft 5 | .vscode 6 | craft.tar.gz 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | bin 14 | *.db 15 | # Test binary, build with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Kubernetes Generated files - skip generated files, except for vendored files 22 | 23 | !vendor/**/zz_generated.* 24 | 25 | # editor and IDE paraphernalia 26 | .idea 27 | *.swp 28 | *.swo 29 | *~ 30 | 31 | # docs generated 32 | docs/book 33 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Salesforce Open Source Community Code of Conduct 2 | 3 | ## About the Code of Conduct 4 | 5 | Equality is a core value at Salesforce. We believe a diverse and inclusive 6 | community fosters innovation and creativity, and are committed to building a 7 | culture where everyone feels included. 8 | 9 | Salesforce open-source projects are committed to providing a friendly, safe, and 10 | welcoming environment for all, regardless of gender identity and expression, 11 | sexual orientation, disability, physical appearance, body size, ethnicity, nationality, 12 | race, age, religion, level of experience, education, socioeconomic status, or 13 | other similar personal characteristics. 14 | 15 | The goal of this code of conduct is to specify a baseline standard of behavior so 16 | that people with different social values and communication styles can work 17 | together effectively, productively, and respectfully in our open source community. 18 | It also establishes a mechanism for reporting issues and resolving conflicts. 19 | 20 | All questions and reports of abusive, harassing, or otherwise unacceptable behavior 21 | in a Salesforce open-source project may be reported by contacting the Salesforce 22 | Open Source Conduct Committee at ossconduct@salesforce.com. 23 | 24 | ## Our Pledge 25 | 26 | In the interest of fostering an open and welcoming environment, we as 27 | contributors and maintainers pledge to making participation in our project and 28 | our community a harassment-free experience for everyone, regardless of gender 29 | identity and expression, sexual orientation, disability, physical appearance, 30 | body size, ethnicity, nationality, race, age, religion, level of experience, education, 31 | socioeconomic status, or other similar personal characteristics. 32 | 33 | ## Our Standards 34 | 35 | Examples of behavior that contributes to creating a positive environment 36 | include: 37 | 38 | * Using welcoming and inclusive language 39 | * Being respectful of differing viewpoints and experiences 40 | * Gracefully accepting constructive criticism 41 | * Focusing on what is best for the community 42 | * Showing empathy toward other community members 43 | 44 | Examples of unacceptable behavior by participants include: 45 | 46 | * The use of sexualized language or imagery and unwelcome sexual attention or 47 | advances 48 | * Personal attacks, insulting/derogatory comments, or trolling 49 | * Public or private harassment 50 | * Publishing, or threatening to publish, others' private information—such as 51 | a physical or electronic address—without explicit permission 52 | * Other conduct which could reasonably be considered inappropriate in a 53 | professional setting 54 | * Advocating for or encouraging any of the above behaviors 55 | 56 | ## Our Responsibilities 57 | 58 | Project maintainers are responsible for clarifying the standards of acceptable 59 | behavior and are expected to take appropriate and fair corrective action in 60 | response to any instances of unacceptable behavior. 61 | 62 | Project maintainers have the right and responsibility to remove, edit, or 63 | reject comments, commits, code, wiki edits, issues, and other contributions 64 | that are not aligned with this Code of Conduct, or to ban temporarily or 65 | permanently any contributor for other behaviors that they deem inappropriate, 66 | threatening, offensive, or harmful. 67 | 68 | ## Scope 69 | 70 | This Code of Conduct applies both within project spaces and in public spaces 71 | when an individual is representing the project or its community. Examples of 72 | representing a project or community include using an official project email 73 | address, posting via an official social media account, or acting as an appointed 74 | representative at an online or offline event. Representation of a project may be 75 | further defined and clarified by project maintainers. 76 | 77 | ## Enforcement 78 | 79 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 80 | reported by contacting the Salesforce Open Source Conduct Committee 81 | at ossconduct@salesforce.com. All complaints will be reviewed and investigated 82 | and will result in a response that is deemed necessary and appropriate to the 83 | circumstances. The committee is obligated to maintain confidentiality with 84 | regard to the reporter of an incident. Further details of specific enforcement 85 | policies may be posted separately. 86 | 87 | Project maintainers who do not follow or enforce the Code of Conduct in good 88 | faith may face temporary or permanent repercussions as determined by other 89 | members of the project's leadership and the Salesforce Open Source Conduct 90 | Committee. 91 | 92 | ## Attribution 93 | 94 | This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home], 95 | version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. 96 | It includes adaptions and additions from [Go Community Code of Conduct][golang-coc], 97 | [CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc]. 98 | 99 | This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. 100 | 101 | [contributor-covenant-home]: https://www.contributor-covenant.org (https://www.contributor-covenant.org/) 102 | [golang-coc]: https://golang.org/conduct 103 | [cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md 104 | [microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ 105 | [cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/ 106 | -------------------------------------------------------------------------------- /Contribution.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Never use `fmt.Printf`, `fmt.Println` in code for debugging we use a logger which has `Debug, Warn, Fatal, Info`. 13 | 3. Update the README.md with details of changes to the interface, this includes new environment 14 | variables, exposed ports, useful file locations and container parameters. 15 | 4. Increase the version numbers in any examples files and the README.md to the new version that this 16 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 17 | 5. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 18 | do not have permission to do that, you may request the second reviewer to merge it for you. 19 | 6. Always run `make fmt` for formatting code. 20 | 21 | ## Code of Conduct 22 | 23 | ### Our Pledge 24 | 25 | In the interest of fostering an open and welcoming environment, we as 26 | contributors and maintainers pledge to making participation in our project and 27 | our community a harassment-free experience for everyone, regardless of age, body 28 | size, disability, ethnicity, gender identity and expression, level of experience, 29 | nationality, personal appearance, race, religion, or sexual identity and 30 | orientation. 31 | 32 | ### Our Standards 33 | 34 | Examples of behavior that contributes to creating a positive environment 35 | include: 36 | 37 | * Using welcoming and inclusive language 38 | * Being respectful of differing viewpoints and experiences 39 | * Gracefully accepting constructive criticism 40 | * Focusing on what is best for the community 41 | * Showing empathy towards other community members 42 | 43 | Examples of unacceptable behavior by participants include: 44 | 45 | * The use of sexualized language or imagery and unwelcome sexual attention or 46 | advances 47 | * Trolling, insulting/derogatory comments, and personal or political attacks 48 | * Public or private harassment 49 | * Publishing others' private information, such as a physical or electronic 50 | address, without explicit permission 51 | * Other conduct which could reasonably be considered inappropriate in a 52 | professional setting 53 | 54 | ### Our Responsibilities 55 | 56 | Project maintainers are responsible for clarifying the standards of acceptable 57 | behavior and are expected to take appropriate and fair corrective action in 58 | response to any instances of unacceptable behavior. 59 | 60 | Project maintainers have the right and responsibility to remove, edit, or 61 | reject comments, commits, code, wiki edits, issues, and other contributions 62 | that are not aligned to this Code of Conduct, or to ban temporarily or 63 | permanently any contributor for other behaviors that they deem inappropriate, 64 | threatening, offensive, or harmful. 65 | 66 | ### Scope 67 | 68 | This Code of Conduct applies both within project spaces and in public spaces 69 | when an individual is representing the project or its community. Examples of 70 | representing a project or community include using an official project e-mail 71 | address, posting via an official social media account, or acting as an appointed 72 | representative at an online or offline event. Representation of a project may be 73 | further defined and clarified by project maintainers. 74 | 75 | ### Enforcement 76 | 77 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 78 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 79 | complaints will be reviewed and investigated and will result in a response that 80 | is deemed necessary and appropriate to the circumstances. The project team is 81 | obligated to maintain confidentiality with regard to the reporter of an incident. 82 | Further details of specific enforcement policies may be posted separately. 83 | 84 | Project maintainers who do not follow or enforce the Code of Conduct in good 85 | faith may face temporary or permanent repercussions as determined by other 86 | members of the project's leadership. 87 | 88 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Salesforce.com, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO := go 2 | pkgs = $(shell $(GO) list ./... | grep -v vendor) 3 | 4 | build: 5 | @./scripts/build.sh 6 | 7 | fmt: 8 | @go fmt $(go list ./... | grep -v _base-operator) &> /dev/null 9 | 10 | release : build 11 | @echo ">>> Built release" 12 | @rm -rf build/craft 13 | @mkdir -p build/craft 14 | @cp -r _base-operator bin init build/craft 15 | @cd build ; tar -zcf ../craft.tar.gz craft ; cd .. 16 | 17 | .PHONY: build format test check_format 18 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: my.domain 2 | repo: example 3 | resources: 4 | - group: mygroup 5 | kind: MyResource 6 | version: v1 7 | version: "2" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom Resource Abstraction Fabrication Tool 2 | 3 | CRAFT removes the language barrier to create Kubernetes Operators. CRAFT declares Kubernetes Operators in a robust and generic way for any resource, letting developers focus on CRUD (create, read, update and delete) operations of resource management in a Dockerfile. With CRAFT you can create operators without a dependent layer and in the language of your choice! 4 | 5 | ## Features: 6 | 7 | 1. Automated reconciliation using Docker entrypoint exit codes. 8 | 2. Kubernetes structural schema validation for a custom resource (CRD) happens within CRAFT while creating an operator. 9 | 3. Craft can be installed as a binary tool. 10 | 11 | These features allow CRAFT to achieve the objectives listed in this [Kubernetes Architecture Design Proposal.](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/declarative-application-management.md#bespoke-application-deployment) 12 | 13 | ## Demo: 14 | As an example, we have created [wordpress operator](https://opensource.salesforce.com/craft/tutorial/index.html) that is comparable to one provided by [Presslabs](https://github.com/presslabs/wordpress-operator) 15 | 16 | ## Advantages 17 | 1. **Easy onboarding** : Create an operator in your language of choice. 18 | 2. **Segregation of duties** : Developers can work in the docker file while the Site Reliability or DevOps engineer can declaratively configure the operator. 19 | 3. **Versioning** : Work on a different version of the operator or resource than your users. 20 | 4. **Validations** : Get schema and input validation feedback before runtime. 21 | 5. **Controlled reconciliation** : Define resource reconciliation frequency to lower your maintenance workload. 22 | 23 | ## Built with 24 | CRAFT is built with open source projects Operatify and Kubebuilder: 25 | 26 | 1. [Operatify](https://github.com/operatify/operatify) : CRAFT leverages Operatify’s automated reconciliation capabilities. Our inspiration is this [blog post](https://www.stephenzoio.com/kubernetes-operators-for-resource-management/), where we have realized that we can overcome the last barrier to declaratively create operators. 27 | 2. [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder) : CRAFT augments the operator skeleton generated by Kubebuilder with custom resource definitions and controller capabilities. 28 | 29 | ## Documentation 30 | Check out [documentaion here](https://opensource.salesforce.com/craft/) 31 | 32 | ## Resources 33 | GitHub Repo: [salesforce/craft](https://github.com/salesforce/craft). 34 | Slack channel: [Kubernetes/craft](https://kubernetes.slack.com/archives/C01AD4W4NEP) 35 | 36 | ## Contribution 37 | Please refer [Contribution.md](Contribution.md) before pushing the code. If you wish to make a contribution, create a branch, push your code into the branch and create a PR. For more details, check [this article](https://opensource.com/article/19/7/create-pull-request-github). 38 | 39 | ## Acknowledgements 40 | CRAFT was started by a small team of developers, namely [Harsh Jain](https://github.com/harsh-98), [Anji Devarasetty](https://github.com/anjidevarasetty), [Maheswara Sunil Varma](https://github.com/maheswarasunil) and [Avvari Sai Bharadwaj](https://github.com/AvvariSaiBharadwaj). 41 | 42 | Thanks to all of the amazing contributors, the full list can be found [here](https://github.com/salesforce/craft/graphs/contributors). 43 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. 8 | -------------------------------------------------------------------------------- /_base-operator/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, salesforce.com, inc. 2 | # All rights reserved. 3 | # SPDX-License-Identifier: BSD-3-Clause 4 | # For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | # Build the manager binary 7 | FROM golang:1.13 as builder 8 | 9 | WORKDIR /workspace 10 | # Copy the Go Modules manifests 11 | COPY go.mod go.mod 12 | COPY go.sum go.sum 13 | # cache deps before building and copying source so that we don't need to re-download as much 14 | # and so that source changes don't invalidate our downloaded layer 15 | RUN go mod download 16 | 17 | # Copy the go source 18 | COPY controller.json controller.json 19 | COPY main.go main.go 20 | COPY reconciler/ reconciler/ 21 | COPY api/ api/ 22 | COPY controllers/ controllers/ 23 | 24 | # Build 25 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go 26 | 27 | # Use distroless as minimal base image to package the manager binary 28 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 29 | FROM gcr.io/distroless/static:nonroot 30 | WORKDIR / 31 | COPY --from=builder /workspace/controller.json . 32 | COPY --from=builder /workspace/manager . 33 | USER nonroot:nonroot 34 | 35 | ENTRYPOINT ["/manager"] 36 | -------------------------------------------------------------------------------- /_base-operator/Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Image URL to use all building/pushing image targets 3 | IMG ?= controller:latest 4 | NAMESPACE ?= default 5 | IMAGEPULLSECRETS ?= '' 6 | 7 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 8 | CRD_OPTIONS ?= "crd:trivialVersions=true" 9 | 10 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 11 | ifeq (,$(shell go env GOBIN)) 12 | GOBIN=$(shell go env GOPATH)/bin 13 | else 14 | GOBIN=$(shell go env GOBIN) 15 | endif 16 | 17 | #namespace = 18 | #secret_name = 19 | #secret_engine = 20 | #docker_registry = 21 | 22 | #user = $(shell VAULT_TOKEN=$(VAULT_TOKEN) vault read -address=$(VAULT_ADDR) -field=username $(secret_engine)) 23 | #password = $(shell VAULT_TOKEN=$(VAULT_TOKEN) vault read -address=$(VAULT_ADDR) -field=password $(secret_engine)) 24 | #server = $(shell VAULT_TOKEN=$(VAULT_TOKEN) vault read -address=$(VAULT_ADDR) -field=server $(secret_engine)) 25 | 26 | all: manager 27 | 28 | # Run tests 29 | test: generate fmt vet manifests 30 | go test ./... -coverprofile cover.out 31 | 32 | # Build manager binary 33 | manager: generate fmt vet 34 | go build -o bin/manager main.go 35 | 36 | # Run against the configured Kubernetes cluster in ~/.kube/config 37 | run: generate fmt vet manifests 38 | go run ./main.go 39 | 40 | # Install CRDs into a cluster 41 | install: manifests 42 | kustomize build config/crd | kubectl apply -f - 43 | 44 | # Uninstall CRDs from a cluster 45 | uninstall: manifests 46 | kustomize build config/crd | kubectl delete -f - 47 | 48 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 49 | deploy: manifests 50 | @ kubectl create secret docker-registry --dry-run=true $(secret_name) --docker-server=$(server) --docker-username=$(user) --docker-password=$(password) --namespace=$(namespace) -o yaml > config/manager/secret.yaml 51 | 52 | cd config/manager && kustomize edit set image controller=${IMG} && kustomize edit set namespace ${NAMESPACE} 53 | kustomize build config/default | kubectl apply -f - 54 | 55 | # Generate manifests to install an operator 56 | operator: manifests 57 | 58 | cd config/manager && kustomize edit set image controller=${IMG} 59 | cd config/default && kustomize edit set namespace ${NAMESPACE} 60 | kustomize build config/default > ${FILE} 61 | 62 | 63 | # Generate manifests e.g. CRD, RBAC etc. 64 | manifests: controller-gen 65 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 66 | 67 | # Run go fmt against code 68 | fmt: 69 | go fmt ./... 70 | 71 | # Run go vet against code 72 | vet: 73 | go vet ./... 74 | 75 | # Generate code 76 | generate: controller-gen 77 | $(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths="./..." 78 | 79 | # Docker login 80 | docker-login: 81 | @ echo $(password) | docker login $(server) -u $(user) --password-stdin 82 | 83 | # Build the docker image 84 | docker-build: test 85 | docker build . -t ${IMG} 86 | 87 | # Push the docker image 88 | docker-push: 89 | docker push ${IMG} 90 | 91 | # find or download controller-gen 92 | # download controller-gen if necessary 93 | controller-gen: 94 | ifeq (, $(shell which controller-gen)) 95 | # @{ \ 96 | # set -e ;\ 97 | # CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ 98 | # cd $$CONTROLLER_GEN_TMP_DIR ;\ 99 | # go mod init tmp ;\ 100 | # go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.2.4 ;\ 101 | # rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ 102 | # } 103 | 104 | CONTROLLER_GEN=$(GOBIN)/controller-gen 105 | else 106 | CONTROLLER_GEN=$(shell which controller-gen) 107 | endif 108 | -------------------------------------------------------------------------------- /_base-operator/controllers/access.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package controllers 7 | 8 | import ( 9 | "fmt" 10 | 11 | api "{{ .Repo }}/api/{{ .Version }}" 12 | "{{ .Repo }}/reconciler" 13 | 14 | "k8s.io/apimachinery/pkg/runtime" 15 | ) 16 | 17 | func GetStatus(instance runtime.Object) (*reconciler.Status, error) { 18 | x, err := convertInstance(instance) 19 | if err != nil { 20 | return nil, err 21 | } 22 | status := x.Status 23 | 24 | return &reconciler.Status{ 25 | State: reconciler.ReconcileState(status.State), 26 | Message: status.Message, 27 | }, nil 28 | } 29 | 30 | func updateStatus(instance runtime.Object, status *reconciler.Status) error { 31 | x, err := convertInstance(instance) 32 | if err != nil { 33 | return err 34 | } 35 | x.Status.State = string(status.State) 36 | x.Status.Message = status.Message 37 | if status.Pod != (api.Pod{}) { 38 | x.Status.Pod = status.Pod 39 | } 40 | x.Status.Terminated = status.Terminated 41 | 42 | switch status.StatusPayload.(type) { 43 | case string: 44 | x.Status.StatusPayload = status.StatusPayload.(string) 45 | } 46 | return nil 47 | } 48 | 49 | func convertInstance(obj runtime.Object) (*api.{{ .Resource }}, error) { 50 | local, ok := obj.(*api.{{ .Resource }}) 51 | if !ok { 52 | return nil, fmt.Errorf("failed type assertion on kind: A") 53 | } 54 | return local, nil 55 | } 56 | 57 | func getSpec(object runtime.Object) (*api.{{ .Resource }}, error) { 58 | instance, err := convertInstance(object) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return instance, nil 63 | } 64 | 65 | func GetSuccess(object runtime.Object) (bool, error) { 66 | instance, err := GetStatus(object) 67 | if err != nil { 68 | return false, err 69 | } 70 | return instance.IsSucceeded(), nil 71 | } 72 | -------------------------------------------------------------------------------- /_base-operator/controllers/definition_manager.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package controllers 7 | 8 | import ( 9 | "context" 10 | 11 | "{{ .Repo }}/api/{{ .Version }}" 12 | "{{ .Repo }}/reconciler" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/types" 15 | ) 16 | 17 | type definitionManager struct{} 18 | 19 | func (dm *definitionManager) GetDefinition(ctx context.Context, namespacedName types.NamespacedName) *reconciler.ResourceDefinition { 20 | return &reconciler.ResourceDefinition{ 21 | InitialInstance: &{{ .Version }}.{{ .Resource }}{}, 22 | StatusAccessor: GetStatus, 23 | StatusUpdater: updateStatus, 24 | } 25 | } 26 | 27 | func (dm *definitionManager) GetDependencies(ctx context.Context, thisInstance runtime.Object) (*reconciler.DependencyDefinitions, error) { 28 | return &reconciler.NoDependencies, nil 29 | } 30 | -------------------------------------------------------------------------------- /_base-operator/controllers/myresource_controller.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 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 | http://www.apache.org/licenses/LICENSE-2.0 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package controllers 19 | 20 | import ( 21 | api "{{ .Repo }}/api/{{ .Version }}" 22 | "{{ .Repo }}/reconciler" 23 | 24 | "k8s.io/apimachinery/pkg/runtime" 25 | "k8s.io/client-go/tools/record" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/event" 29 | "sigs.k8s.io/controller-runtime/pkg/predicate" 30 | 31 | "github.com/go-logr/logr" 32 | ) 33 | 34 | type ControllerFactory struct { 35 | ResourceManagerCreator func(logr.Logger, record.EventRecorder) ResourceManager 36 | Scheme *runtime.Scheme 37 | } 38 | 39 | // +kubebuilder:rbac:groups="",resources=pods;events,verbs=get;list;watch;create;update;patch;delete 40 | // +kubebuilder:rbac:groups=extensions;apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete 41 | // +kubebuilder:rbac:groups={{ .Group }}.{{ .Domain }},resources={{ .LowerRes }}s,verbs=get;list;watch;create;update;patch;delete 42 | // +kubebuilder:rbac:groups={{ .Group }}.{{ .Domain }},resources={{ .LowerRes }}s/status,verbs=get;update;patch 43 | 44 | // +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state` 45 | // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.message` 46 | 47 | const ResourceKind = "{{ .Resource }}" 48 | const FinalizerName = "{{ .LowerRes }}s.{{ .Group }}.{{ .Domain }}" 49 | const AnnotationBaseName = "{{ .Group }}.{{ .Domain }}" 50 | 51 | func (factory *ControllerFactory) SetupWithManager(mgr ctrl.Manager, parameters reconciler.ReconcileParameters, log *logr.Logger) error { 52 | if log == nil { 53 | l := ctrl.Log.WithName("controllers") 54 | log = &l 55 | } 56 | gc, err := factory.createGenericController(mgr.GetClient(), 57 | (*log).WithName(ResourceKind), 58 | mgr.GetEventRecorderFor(ResourceKind+"-controller"), parameters) 59 | if err != nil { 60 | return err 61 | } 62 | // https://stuartleeks.com/posts/kubebuilder-event-filters-part-1-delete/ 63 | // https://godoc.org/sigs.k8s.io/controller-runtime/pkg/event 64 | // https://github.com/kubernetes-sigs/kubebuilder/issues/618 65 | // https://godoc.org/sigs.k8s.io/controller-runtime/pkg/predicate#Predicate 66 | // https://book-v1.book.kubebuilder.io/beyond_basics/controller_watches.html 67 | return ctrl.NewControllerManagedBy(mgr). 68 | For(&api.{{ .Resource }}{}). 69 | WithEventFilter(predicate.Funcs{ 70 | UpdateFunc: func(e event.UpdateEvent) bool { 71 | isSame := true 72 | for k, v := range e.ObjectOld.(*api.{{ .Resource }}).ObjectMeta.Annotations { 73 | isSame = isSame && (v == e.ObjectNew.(*api.{{ .Resource }}).ObjectMeta.Annotations[k]) 74 | } 75 | if e.ObjectNew.(*api.{{ .Resource }}).ObjectMeta.DeletionTimestamp != nil { 76 | return true 77 | } 78 | return !isSame 79 | }, 80 | }). 81 | Complete(gc) 82 | } 83 | 84 | func (factory *ControllerFactory) createGenericController(kubeClient client.Client, logger logr.Logger, recorder record.EventRecorder, parameters reconciler.ReconcileParameters) (*reconciler.GenericController, error) { 85 | resourceManagerClient := factory.ResourceManagerCreator(logger, recorder) 86 | 87 | return reconciler.CreateGenericController(parameters, ResourceKind, kubeClient, logger, recorder, factory.Scheme, &resourceManagerClient, &definitionManager{}, FinalizerName, AnnotationBaseName, nil) 88 | } 89 | 90 | func CreateResourceManager(logger logr.Logger, recorder record.EventRecorder) ResourceManager { 91 | return ResourceManager{ 92 | Logger: logger, 93 | Recorder: recorder, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /_base-operator/controllers/resource_manager.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package controllers 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/go-logr/logr" 12 | "{{ .Repo }}/reconciler" 13 | "k8s.io/client-go/tools/record" 14 | 15 | ) 16 | 17 | 18 | type ResourceManager struct { 19 | Logger logr.Logger 20 | Recorder record.EventRecorder 21 | } 22 | 23 | func (resManager *ResourceManager) Create(ctx context.Context) (reconciler.PodSpec, error) { 24 | return reconciler.PodSpec{ 25 | Image: "{{ .Image }}", 26 | Arguments: "--type create --spec {spec}", 27 | Name: "create", 28 | Namespace: "{{ .Namespace }}", 29 | ImagePullPolicy: "{{ .ImagePullPolicy }}", 30 | ImagePullSecrets: "{{ .ImagePullSecrets }}", 31 | },nil 32 | } 33 | func (resManager *ResourceManager) Update(ctx context.Context) (reconciler.PodSpec, error) { 34 | return reconciler.PodSpec{ 35 | Image: "{{ .Image }}", 36 | Arguments: "--type update --spec {spec}", 37 | Name: "update", 38 | Namespace: "{{ .Namespace }}", 39 | ImagePullPolicy: "{{ .ImagePullPolicy }}", 40 | ImagePullSecrets: "{{ .ImagePullSecrets }}", 41 | }, nil 42 | } 43 | 44 | func (resManager *ResourceManager) Verify(ctx context.Context) (reconciler.PodSpec, error) { 45 | return reconciler.PodSpec{ 46 | Image: "{{ .Image }}", 47 | Arguments: "--type verify --spec {spec}", 48 | Name: "verify", 49 | Namespace: "{{ .Namespace }}", 50 | ImagePullPolicy: "{{ .ImagePullPolicy }}", 51 | ImagePullSecrets: "{{ .ImagePullSecrets }}", 52 | },nil 53 | } 54 | 55 | func (resManager *ResourceManager) Delete(ctx context.Context) (reconciler.PodSpec, error) { 56 | return reconciler.PodSpec{ 57 | Image: "{{ .Image }}", 58 | Arguments: "--type delete --spec {spec}", 59 | Name: "delete", 60 | Namespace: "{{ .Namespace }}", 61 | ImagePullPolicy: "{{ .ImagePullPolicy }}", 62 | ImagePullSecrets: "{{ .ImagePullSecrets }}", 63 | }, nil 64 | } 65 | -------------------------------------------------------------------------------- /_base-operator/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | /* 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package main 22 | 23 | import ( 24 | "flag" 25 | "os" 26 | "strconv" 27 | 28 | "{{ .Repo }}/controllers" 29 | 30 | mygroup{{ .Version }} "{{ .Repo }}/api/{{ .Version }}" 31 | "{{ .Repo }}/reconciler" 32 | "k8s.io/apimachinery/pkg/runtime" 33 | uberzap "go.uber.org/zap" 34 | "go.uber.org/zap/zapcore" 35 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 36 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 37 | ctrl "sigs.k8s.io/controller-runtime" 38 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 39 | // +kubebuilder:scaffold:imports 40 | ) 41 | 42 | var ( 43 | scheme = runtime.NewScheme() 44 | setupLog = ctrl.Log.WithName("setup") 45 | ) 46 | 47 | func init() { 48 | _ = clientgoscheme.AddToScheme(scheme) 49 | 50 | _ = mygroup{{ .Version }}.AddToScheme(scheme) 51 | // +kubebuilder:scaffold:scheme 52 | } 53 | 54 | func main() { 55 | var metricsAddr string 56 | var enableLeaderElection bool 57 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 58 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 59 | "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") 60 | flag.Parse() 61 | 62 | logger := uberzap.NewAtomicLevelAt(zapcore.InfoLevel) 63 | //ctrl.SetLogger(zap.New(zap.UseDevMode(true))) 64 | ctrl.SetLogger(zap.New(func(o *zap.Options) { 65 | o.Development = true 66 | o.Level = &logger 67 | })) 68 | 69 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 70 | Scheme: scheme, 71 | MetricsBindAddress: metricsAddr, 72 | LeaderElection: enableLeaderElection, 73 | LeaderElectionID: "{{.LeaderElectionID}}", 74 | Port: 9443, 75 | }) 76 | if err != nil { 77 | setupLog.Error(err, "unable to start manager") 78 | os.Exit(1) 79 | } 80 | 81 | var requeueAfter int 82 | freq_min, err := strconv.Atoi("{{.ReconcileFreq}}") 83 | 84 | if err != nil { 85 | requeueAfter = 30000 86 | } else { 87 | requeueAfter = freq_min * 60 * 1000 //mins to milliseconds 88 | } 89 | controllerParams := reconciler.ReconcileParameters{ 90 | RequeueAfter : requeueAfter, 91 | RequeueAfterSuccess : 15000, 92 | RequeueAfterFailure : 30000, 93 | } 94 | if err = (&controllers.ControllerFactory{ 95 | ResourceManagerCreator: controllers.CreateResourceManager, 96 | Scheme: scheme, 97 | }).SetupWithManager(mgr, controllerParams, nil); err != nil { 98 | setupLog.Error(err, "unable to create controller", "controller", "B") 99 | os.Exit(1) 100 | } 101 | // +kubebuilder:scaffold:builder 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 | -------------------------------------------------------------------------------- /_base-operator/reconciler/access.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package reconciler 7 | 8 | import ( 9 | "fmt" 10 | 11 | api "{{ .Repo }}/api/{{ .Version }}" 12 | 13 | "k8s.io/apimachinery/pkg/runtime" 14 | ) 15 | 16 | var statusCodeMap = map[int32]string{ 17 | 201: "Succeeded", // create or update 18 | 202: "AwaitingVerification", // create or update 19 | 203: "Error", // create or update 20 | 211: "Ready", // verify 21 | 212: "InProgress", // verify 22 | 213: "Error", // verify 23 | 214: "Missing", // verify 24 | 215: "UpdateRequired", // verify 25 | 216: "RecreateRequired", // verify 26 | 217: "Deleting", // verify 27 | 221: "Succeeded", // delete 28 | 222: "InProgress", // delete 29 | 223: "Error", // delete 30 | 224: "Missing", // delete 31 | } 32 | 33 | func convertInstance(obj runtime.Object) (*api.{{ .Resource }}, error) { 34 | local, ok := obj.(*api.{{ .Resource }}) 35 | if !ok { 36 | return nil, fmt.Errorf("failed type assertion on kind: A") 37 | } 38 | return local, nil 39 | } 40 | 41 | func getSpec(object runtime.Object) (*api.{{ .Resource }}, error) { 42 | instance, err := convertInstance(object) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return instance, nil 47 | } -------------------------------------------------------------------------------- /_base-operator/reconciler/access_permissions.go: -------------------------------------------------------------------------------- 1 | package reconciler 2 | 3 | import "strings" 4 | 5 | const ( 6 | AccessPermissionAnnotation = "/access-permissions" 7 | ) 8 | 9 | type AccessPermissions string 10 | 11 | func (p AccessPermissions) create() bool { return containsOrEmpty(p, "c") } 12 | func (p AccessPermissions) update() bool { return containsOrEmpty(p, "u") } 13 | func (p AccessPermissions) delete() bool { return containsOrEmpty(p, "d") } 14 | 15 | // Note that all permissions are enabled by default 16 | func containsOrEmpty(p AccessPermissions, c string) bool { 17 | return strings.Contains(strings.ToLower(string(p)), c) || p == "" 18 | } 19 | -------------------------------------------------------------------------------- /_base-operator/reconciler/definition_manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package reconciler 17 | 18 | import ( 19 | "context" 20 | 21 | "k8s.io/apimachinery/pkg/types" 22 | 23 | "k8s.io/apimachinery/pkg/runtime" 24 | ) 25 | 26 | // DefinitionManager is used to retrieve the required custom resource definitions 27 | // and convert them into a state that can be consumed and updated (where applicable) generically 28 | type DefinitionManager interface { 29 | // returns a ResourceDefinition 30 | GetDefinition(ctx context.Context, namespacedName types.NamespacedName) *ResourceDefinition 31 | // returns the dependencies for a resource 32 | GetDependencies(ctx context.Context, thisInstance runtime.Object) (*DependencyDefinitions, error) 33 | } 34 | 35 | // Details of the current resource being reconciled 36 | type ResourceDefinition struct { 37 | // This can be an empty resource definition object of the required Kind 38 | InitialInstance runtime.Object 39 | // A function to get the Status of the kubernetes object 40 | StatusAccessor StatusAccessor 41 | // A function to update the Status of a kubernetes object instance 42 | StatusUpdater StatusUpdater 43 | } 44 | 45 | // The information required to pull the resource definition of a dependency from kubernetes 46 | // and determine whether it has been successfully applied or not 47 | type Dependency struct { 48 | // This can be an empty resource definition object of the required Kind 49 | InitialInstance runtime.Object 50 | NamespacedName types.NamespacedName 51 | // A function to return whether the object has been successfully applied. The current object will only 52 | // continue once this returns true for all dependencies 53 | SucceededAccessor SucceededAccessor 54 | } 55 | 56 | // Details of the owner and the dependencies of the resource 57 | type DependencyDefinitions struct { 58 | Owner *Dependency 59 | Dependencies []*Dependency 60 | } 61 | 62 | // A shortcut for objects with no dependencies 63 | var NoDependencies = DependencyDefinitions{ 64 | Dependencies: []*Dependency{}, 65 | Owner: nil, 66 | } 67 | 68 | // fetches the Status of the instance of runtime.Object 69 | type StatusAccessor = func(instance runtime.Object) (*Status, error) 70 | 71 | // updates the Status of the instance of runtime.Object with Status 72 | type StatusUpdater = func(instance runtime.Object, status *Status) error 73 | 74 | // unfortunately if doesn't seem possible to make this an extension method with a receiver 75 | // unfortunately if doesn't seem possible to make this an extension method with a receiver 76 | func AsSuccessAccessor(s StatusAccessor) SucceededAccessor { 77 | return func(instance runtime.Object) (bool, error) { 78 | status, err := s(instance) 79 | if err != nil { 80 | return false, err 81 | } 82 | if status == nil { 83 | return false, nil 84 | } 85 | return status.IsSucceeded(), nil 86 | } 87 | } 88 | 89 | // fetches a boolean flag to indicate whether the resource is in a succeeded (i.e. ready) state 90 | type SucceededAccessor = func(instance runtime.Object) (bool, error) 91 | -------------------------------------------------------------------------------- /_base-operator/reconciler/generic_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package reconciler 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | 22 | "github.com/go-logr/logr" 23 | apimeta "k8s.io/apimachinery/pkg/api/meta" 24 | "k8s.io/apimachinery/pkg/runtime" 25 | 26 | "k8s.io/client-go/tools/record" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | ) 30 | 31 | // GenericController is a generic implementation of a Kubebuilder controller 32 | type GenericController struct { 33 | Parameters ReconcileParameters 34 | ResourceKind string 35 | KubeClient client.Client 36 | Log logr.Logger 37 | Recorder record.EventRecorder 38 | Scheme *runtime.Scheme 39 | ResourceManager ResourceManager 40 | DefinitionManager DefinitionManager 41 | FinalizerName string 42 | AnnotationBaseName string 43 | CompletionFactory func(*GenericController) CompletionHandler 44 | } 45 | 46 | // A handler that is invoked after the resource has been successfully created 47 | // and it has been verified to be ready for consumption (ReconcileState=Success) 48 | // This is typically used for example to create secrets with authentication information 49 | type CompletionHandler interface { 50 | Run(ctx context.Context, r runtime.Object) error 51 | } 52 | 53 | type ReconcileParameters struct { 54 | RequeueAfter int 55 | RequeueAfterSuccess int 56 | RequeueAfterFailure int 57 | } 58 | 59 | func CreateGenericController( 60 | parameters ReconcileParameters, 61 | resourceKind string, 62 | kubeClient client.Client, 63 | logger logr.Logger, 64 | recorder record.EventRecorder, 65 | scheme *runtime.Scheme, 66 | resourceManager ResourceManager, 67 | defMgr DefinitionManager, 68 | finalizerName string, 69 | annotationBaseName string, 70 | completionFactory func(*GenericController) CompletionHandler) (*GenericController, error) { 71 | gc := &GenericController{ 72 | Parameters: parameters, 73 | ResourceKind: resourceKind, 74 | KubeClient: kubeClient, 75 | Log: logger, 76 | Recorder: recorder, 77 | Scheme: scheme, 78 | ResourceManager: resourceManager, 79 | DefinitionManager: defMgr, 80 | FinalizerName: finalizerName, 81 | AnnotationBaseName: annotationBaseName, 82 | CompletionFactory: completionFactory, 83 | } 84 | if err := gc.validate(); err != nil { 85 | return nil, err 86 | } 87 | return gc, nil 88 | } 89 | 90 | func (gc *GenericController) validate() error { 91 | if gc.ResourceKind == "" { 92 | return fmt.Errorf("resource Kind must be defined for GenericController") 93 | } 94 | kind := gc.ResourceKind 95 | if gc.Scheme == nil { 96 | return fmt.Errorf("no Scheme defined for controller for %s", kind) 97 | } 98 | if gc.ResourceManager == nil { 99 | return fmt.Errorf("no ResourceManager defined for controller for %s", kind) 100 | } 101 | if gc.DefinitionManager == nil { 102 | return fmt.Errorf("no DefinitionManager defined for controller for %s", kind) 103 | } 104 | if gc.FinalizerName == "" { 105 | return fmt.Errorf("no FinalizerName set for controller for %s", kind) 106 | } 107 | return nil 108 | } 109 | 110 | func (gc *GenericController) Reconcile(req ctrl.Request) (ctrl.Result, error) { 111 | ctx := context.TODO() 112 | log := gc.Log.WithValues("Name", req.NamespacedName) 113 | 114 | // fetch the manifest object 115 | thisDefs := gc.DefinitionManager.GetDefinition(ctx, req.NamespacedName) 116 | 117 | err := gc.KubeClient.Get(ctx, req.NamespacedName, thisDefs.InitialInstance) 118 | if err != nil { 119 | log.Info("Unable to retrieve resource", "err", err.Error()) 120 | // we'll ignore not-found errors, since they can't be fixed by an immediate 121 | // requeue (we'll need to wait for a new notification), and we can get them 122 | // on deleted requests. 123 | return ctrl.Result{}, client.IgnoreNotFound(err) 124 | } 125 | 126 | instance := thisDefs.InitialInstance 127 | status, err := thisDefs.StatusAccessor(instance) 128 | metaObject, _ := apimeta.Accessor(instance) 129 | 130 | instanceUpdater := instanceUpdater{ 131 | StatusUpdater: thisDefs.StatusUpdater, 132 | } 133 | 134 | // get dependency details 135 | dependencies, err := gc.DefinitionManager.GetDependencies(ctx, instance) 136 | // this is only the names and initial values, if we can't fetch these it's terminal 137 | if err != nil { 138 | log.Info("Unable to retrieve dependencies for resource ", "err", err.Error()) 139 | return ctrl.Result{}, client.IgnoreNotFound(err) 140 | } 141 | 142 | // create a reconcile runner object. this runs a single cycle of the reconcile loop 143 | reconcileRunner := ReconcileRunner{ 144 | GenericController: gc, 145 | ResourceDefinition: thisDefs, 146 | DependencyDefinitions: dependencies, 147 | NamespacedName: req.NamespacedName, 148 | instance: instance, 149 | objectMeta: metaObject, 150 | status: status, 151 | req: req, 152 | log: log, 153 | instanceUpdater: &instanceUpdater, 154 | } 155 | 156 | // handle finalization first 157 | reconcileFinalizer := reconcileFinalizer{ 158 | ReconcileRunner: reconcileRunner, 159 | } 160 | 161 | // if it's being deleted go straight to the finalizer step 162 | isBeingDeleted := !metaObject.GetDeletionTimestamp().IsZero() 163 | if isBeingDeleted { 164 | return reconcileFinalizer.handle() 165 | } 166 | 167 | // if no finalizers have been defined, do that and requeue 168 | if !reconcileFinalizer.isDefined() { 169 | return reconcileFinalizer.add(ctx) 170 | } 171 | 172 | // run a single cycle of the reconcile loop 173 | return reconcileRunner.run(ctx) 174 | } 175 | -------------------------------------------------------------------------------- /_base-operator/reconciler/instance_updater.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package reconciler 7 | 8 | import ( 9 | apimeta "k8s.io/apimachinery/pkg/api/meta" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | corev1 "k8s.io/api/core/v1" 13 | api "{{ .Repo }}/api/{{ .Version }}" 14 | ) 15 | 16 | // modifies the runtime.Object in place 17 | type statusUpdate = func(status *Status) 18 | type metaUpdate = func(meta metav1.Object) 19 | 20 | // instanceUpdater is a mechanism to enable updating the shared sections of the manifest 21 | // Typically the Status section and the metadata. 22 | type instanceUpdater struct { 23 | StatusUpdater 24 | metaUpdates []metaUpdate 25 | statusUpdates []statusUpdate 26 | } 27 | 28 | func (updater *instanceUpdater) addFinalizer(name string) { 29 | updateFunc := func(meta metav1.Object) { addFinalizer(meta, name) } 30 | updater.metaUpdates = append(updater.metaUpdates, updateFunc) 31 | } 32 | 33 | func (updater *instanceUpdater) removeFinalizer(name string) { 34 | updateFunc := func(meta metav1.Object) { removeFinalizer(meta, name) } 35 | updater.metaUpdates = append(updater.metaUpdates, updateFunc) 36 | } 37 | 38 | func (updater *instanceUpdater) setStatusPayload(statusPayload interface{}) { 39 | updateFunc := func(s *Status) { 40 | s.StatusPayload = statusPayload 41 | } 42 | updater.statusUpdates = append(updater.statusUpdates, updateFunc) 43 | } 44 | 45 | func (updater *instanceUpdater) setTerminated(terminated *corev1.ContainerStateTerminated) { 46 | updateFunc := func(s *Status) { 47 | s.Terminated = terminated 48 | } 49 | updater.statusUpdates = append(updater.statusUpdates, updateFunc) 50 | } 51 | 52 | func (updater *instanceUpdater) setPodConfig(pod api.Pod) { 53 | updateFunc := func(s *Status) { 54 | s.Pod = pod 55 | } 56 | updater.statusUpdates = append(updater.statusUpdates, updateFunc) 57 | } 58 | 59 | func (updater *instanceUpdater) setReconcileState(state ReconcileState, message string) { 60 | updateFunc := func(s *Status) { 61 | s.State = state 62 | s.Message = message 63 | } 64 | updater.statusUpdates = append(updater.statusUpdates, updateFunc) 65 | } 66 | 67 | func (updater *instanceUpdater) setAnnotation(name string, value string) { 68 | updateFunc := func(meta metav1.Object) { 69 | annotations := meta.GetAnnotations() 70 | if annotations == nil { 71 | annotations = map[string]string{} 72 | } 73 | annotations[name] = value 74 | meta.SetAnnotations(annotations) 75 | } 76 | updater.metaUpdates = append(updater.metaUpdates, updateFunc) 77 | } 78 | 79 | func (updater *instanceUpdater) setOwnerReferences(owners []runtime.Object) { 80 | updateFunc := func(s metav1.Object) { 81 | references := make([]metav1.OwnerReference, len(owners)) 82 | for i, o := range owners { 83 | controller := true 84 | meta, _ := apimeta.Accessor(o) 85 | references[i] = metav1.OwnerReference{ 86 | APIVersion: "v1", 87 | Kind: o.GetObjectKind().GroupVersionKind().Kind, 88 | Name: meta.GetName(), 89 | UID: meta.GetUID(), 90 | Controller: &controller, 91 | } 92 | } 93 | s.SetOwnerReferences(references) 94 | } 95 | updater.metaUpdates = append(updater.metaUpdates, updateFunc) 96 | } 97 | 98 | func (updater *instanceUpdater) applyUpdates(instance runtime.Object, status *Status) error { 99 | for _, f := range updater.statusUpdates { 100 | f(status) 101 | } 102 | err := updater.StatusUpdater(instance, status) 103 | m, _ := apimeta.Accessor(instance) 104 | for _, f := range updater.metaUpdates { 105 | f(m) 106 | } 107 | return err 108 | } 109 | 110 | func (updater *instanceUpdater) clear() { 111 | updater.metaUpdates = []metaUpdate{} 112 | updater.statusUpdates = []statusUpdate{} 113 | } 114 | 115 | func (updater *instanceUpdater) hasUpdates() bool { 116 | return len(updater.metaUpdates) > 0 || len(updater.statusUpdates) > 0 117 | } 118 | -------------------------------------------------------------------------------- /_base-operator/reconciler/reconcile_finalizer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | /* 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package reconciler 22 | 23 | import ( 24 | "context" 25 | "fmt" 26 | 27 | "github.com/prometheus/common/log" 28 | apierrors "k8s.io/apimachinery/pkg/api/errors" 29 | "k8s.io/apimachinery/pkg/types" 30 | 31 | corev1 "k8s.io/api/core/v1" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | ) 34 | 35 | func (r *reconcileFinalizer) isDefined() bool { 36 | return hasFinalizer(r.objectMeta, r.FinalizerName) 37 | } 38 | 39 | func (r *reconcileFinalizer) add(ctx context.Context) (ctrl.Result, error) { 40 | updater := r.instanceUpdater 41 | 42 | updater.addFinalizer(r.FinalizerName) 43 | r.log.Info("Adding finalizer to resource") 44 | return r.applyTransition(ctx, "Finalizer", Pending, nil) 45 | } 46 | func (r *ReconcileRunner) terminationCheck(ctx context.Context) (string, string) { 47 | 48 | instance, err := convertInstance(r.instance) 49 | 50 | pod := instance.Status.Pod 51 | found := &corev1.Pod{} 52 | 53 | err = r.KubeClient.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, found) 54 | 55 | if err != nil && apierrors.IsNotFound(err) { 56 | return pod.Type, "Checking" 57 | } 58 | if len(found.Status.ContainerStatuses) == 0 { 59 | return pod.Type, "Checking" 60 | } 61 | terminated := found.Status.ContainerStatuses[0].State.Terminated 62 | 63 | if terminated != nil { 64 | // put the exitcode later 65 | r.instanceUpdater.setTerminated(terminated) 66 | r.KubeClient.Delete(ctx, found) 67 | return pod.Type, "PodDeleting" 68 | // return pod.Type, statusCodeMap[terminated.ExitCode] 69 | } 70 | return pod.Type, "Checking" 71 | } 72 | 73 | func (r *ReconcileRunner) deleteCheck(ctx context.Context) (string, string) { 74 | 75 | instance, err := convertInstance(r.instance) 76 | 77 | pod := instance.Status.Pod 78 | found := &corev1.Pod{} 79 | 80 | err = r.KubeClient.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, found) 81 | 82 | if err != nil && apierrors.IsNotFound(err) { 83 | terminated := instance.Status.Terminated 84 | return pod.Type, statusCodeMap[terminated.ExitCode] 85 | } 86 | return pod.Type, "PodDeleting" 87 | 88 | } 89 | 90 | func (r *reconcileFinalizer) handle() (ctrl.Result, error) { 91 | instance := r.instance 92 | updater := r.instanceUpdater 93 | ctx := context.Background() 94 | removeFinalizer := false 95 | requeue := false 96 | 97 | isTerminating := r.status.IsTerminating() 98 | r.log.Info(fmt.Sprintf("Finalizer: %s", r.status)) 99 | if r.isDefined() { 100 | // Even before we cal ResourceManager.Delete, we verify the state of the resource 101 | // If it has not been created, we don't need to delete anything. 102 | var verifyResult VerifyResult 103 | var deleteResult DeleteResult 104 | var delete, checkState string 105 | 106 | if r.status.IsPodDeleting() { 107 | delete, checkState = r.deleteCheck(ctx) 108 | } else if r.status.IsChecking() { 109 | delete, checkState = r.terminationCheck(ctx) 110 | } 111 | 112 | if checkState == "PodDeleting" { 113 | return r.applyTransition(ctx, "deleteCheck", PodDeleting, nil) 114 | } else if checkState == "Checking" { 115 | return r.applyTransition(ctx, "terminationCheck", Checking, nil) 116 | } else if delete == "delete" { 117 | deleteResult := DeleteResult(checkState) 118 | r.log.Info(fmt.Sprintf("Finalizer deleteresult: %s", deleteResult)) 119 | } else { 120 | // result is verify 121 | verifyResult = VerifyResult(checkState) 122 | r.log.Info(fmt.Sprintf("Finalizer verify result: %s", verifyResult)) 123 | } 124 | 125 | if delete == "delete" { 126 | deleteResult = DeleteResult(checkState) 127 | if deleteResult.error() { 128 | log.Info("An error occurred attempting to delete managed object in finalizer. Cannot confirm that managed object has been deleted. Continuing deletion of kubernetes object anyway.") 129 | removeFinalizer = true 130 | } else if deleteResult.alreadyDeleted() || deleteResult.succeeded() { 131 | removeFinalizer = true 132 | } else if deleteResult.awaitingVerification() { 133 | requeue = true 134 | } else { 135 | // assert no more cases 136 | removeFinalizer = true 137 | } 138 | } else if verifyResult.missing() { 139 | removeFinalizer = true 140 | } else if verifyResult.deleting() { 141 | requeue = true 142 | } else if !isTerminating { // and one of verifyResult.ready() || verifyResult.recreateRequired() || verifyResult.updateRequired() || verifyResult.error() 143 | if verifyResult.error() { 144 | log.Info("An error occurred verifying state of managed object in finalizer. Cannot confirm that managed object can be deleted. Continuing deletion of kubernetes object anyway.") 145 | // TODO: maybe should rather retry a certain number of times before failing 146 | } 147 | permissions := r.getAccessPermissions() 148 | if !permissions.delete() { 149 | // if delete permission is turned off, just finalize, but don't delete 150 | r.log.Info("Resource is not managed by operator, bypassing delete of resource") 151 | removeFinalizer = true 152 | } else { 153 | // This block of code should only ever get called once. 154 | r.log.Info("Deleting resource") 155 | // delete state will be saved in the status 156 | podSpec, _ := r.ResourceManager.Delete(ctx) 157 | podValue, _ := r.ReconcileRunner.spawnPod("delete", podSpec) 158 | r.ReconcileRunner.instanceUpdater.setPodConfig(podValue) 159 | return r.applyTransition(ctx, "terminationCheck", Checking, nil) 160 | } 161 | } else { 162 | // this should never be called, as the first time r.ResourceManager.Delete is called isTerminating should be false 163 | // this implies that r.ResourceManager.Delete didn't throw an error, but didn't do anything either 164 | removeFinalizer = true 165 | } 166 | } 167 | 168 | if !isTerminating { 169 | updater.setReconcileState(Terminating, "") 170 | } 171 | if removeFinalizer { 172 | updater.removeFinalizer(r.FinalizerName) 173 | } 174 | 175 | requeueAfter := r.getRequeueAfter(Terminating) 176 | if removeFinalizer || !isTerminating { 177 | if err := r.updateInstance(ctx); err != nil { 178 | // if we can't update we have to requeue and hopefully it will remove the finalizer next time 179 | return ctrl.Result{Requeue: true, RequeueAfter: requeueAfter}, fmt.Errorf("Error removing finalizer: %v", err) 180 | } 181 | if !isTerminating { 182 | r.Recorder.Event(instance, corev1.EventTypeNormal, "Finalizer", "Setting state to terminating for "+r.Name) 183 | } 184 | if removeFinalizer { 185 | r.Recorder.Event(instance, corev1.EventTypeNormal, "Finalizer", "Removing finalizer for "+r.Name) 186 | } 187 | } 188 | 189 | if requeue { 190 | return ctrl.Result{Requeue: true, RequeueAfter: requeueAfter}, nil 191 | } else { 192 | r.Recorder.Event(instance, corev1.EventTypeNormal, "Finalizer", r.Name+" finalizer complete") 193 | return ctrl.Result{}, nil 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /_base-operator/reconciler/reconcile_runner.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | /* 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package reconciler 22 | 23 | import ( 24 | "context" 25 | "encoding/json" 26 | "io/ioutil" 27 | "os" 28 | 29 | // "os/exec" 30 | "fmt" 31 | "strings" 32 | "time" 33 | 34 | "errors" 35 | {{ .Resource }}{{ .Version }} "{{ .Repo }}/api/{{ .Version }}" 36 | 37 | "github.com/hashicorp/vault/api" 38 | v1 "k8s.io/api/core/v1" 39 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 40 | "k8s.io/apimachinery/pkg/runtime" 41 | "k8s.io/apimachinery/pkg/types" 42 | "sigs.k8s.io/controller-runtime/pkg/client" 43 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 44 | 45 | "github.com/go-logr/logr" 46 | 47 | apierrors "k8s.io/apimachinery/pkg/api/errors" 48 | "k8s.io/apimachinery/pkg/api/resource" 49 | ctrl "sigs.k8s.io/controller-runtime" 50 | ) 51 | 52 | var vault *api.Logical 53 | var token = os.Getenv("VAULT_TOKEN") 54 | var vault_addr = os.Getenv("VAULT_ADDR") 55 | 56 | const LastAppliedAnnotation = "/last-applied-spec" 57 | 58 | // Contains all the state involved in running a single reconcile event in the reconcile loo[ 59 | type ReconcileRunner struct { 60 | *GenericController 61 | *ResourceDefinition 62 | *DependencyDefinitions 63 | types.NamespacedName 64 | instance runtime.Object 65 | objectMeta metav1.Object 66 | status *Status 67 | req ctrl.Request 68 | log logr.Logger 69 | instanceUpdater *instanceUpdater 70 | owner runtime.Object 71 | dependencies map[types.NamespacedName]runtime.Object 72 | } 73 | 74 | type reconcileFinalizer struct { 75 | ReconcileRunner 76 | } 77 | 78 | //runs a single reconcile on the 79 | func (r *ReconcileRunner) run(ctx context.Context) (ctrl.Result, error) { 80 | 81 | // Verify that all dependencies are present in the cluster, and they are 82 | owner := r.Owner 83 | var allDeps []*Dependency 84 | if owner != nil { 85 | allDeps = append([]*Dependency{owner}, r.Dependencies...) 86 | } else { 87 | allDeps = r.Dependencies 88 | } 89 | status := r.status 90 | r.dependencies = map[types.NamespacedName]runtime.Object{} 91 | 92 | // jump out and requeue if any of the dependencies are missing 93 | for i, dep := range allDeps { 94 | instance := dep.InitialInstance 95 | err := r.KubeClient.Get(ctx, dep.NamespacedName, instance) 96 | log := r.log.WithValues("Dependency", dep.NamespacedName) 97 | 98 | // if any of the dependencies are not found, we jump out. 99 | if err != nil { // note that dependencies should be an empty array 100 | if apierrors.IsNotFound(err) { 101 | log.Info("Dependency not found for " + dep.NamespacedName.Name + ". Requeuing request.") 102 | } else { 103 | log.Info(fmt.Sprintf("Unable to retrieve dependency for %s: %v", dep.NamespacedName.Name, err.Error())) 104 | } 105 | return r.applyTransition(ctx, "Dependency", Pending, client.IgnoreNotFound(err)) 106 | } 107 | 108 | // set the owner reference if owner is present and references have not been set 109 | // currently we only have single object ownership, but it is poosible to have multiple owners 110 | if owner != nil && i == 0 { 111 | if len(r.objectMeta.GetOwnerReferences()) == 0 { 112 | return r.setOwner(ctx, instance) 113 | } 114 | r.owner = instance 115 | } 116 | r.dependencies[dep.NamespacedName] = instance 117 | 118 | succeeded, err := dep.SucceededAccessor(instance) 119 | if err != nil { 120 | log.Info(fmt.Sprintf("Cannot get success state for %s. terminal failure.", dep.NamespacedName.Name)) 121 | // Fail if cannot get Status accessor for dependency 122 | return r.applyTransition(ctx, "Dependency", Failed, err) 123 | } 124 | 125 | if !succeeded { 126 | log.Info("One of the dependencies is not in 'Succeeded' state, requeuing") 127 | return r.applyTransition(ctx, "Dependency", Pending, nil) 128 | } 129 | } 130 | // status = &Status{State: Checking} 131 | r.log.Info(fmt.Sprintf("ReconcileState: %s", status)) 132 | // **** checking for termination of dockerfile 133 | if status.IsChecking() { 134 | return r.check(ctx) 135 | } 136 | 137 | // **** podPreviousPod for termination of dockerfile 138 | if status.IsPodDeleting() { 139 | return r.podDelete(ctx) 140 | } 141 | 142 | if status.IsCompleted(){ 143 | return r.applyTransition(ctx, "from completed", Pending, nil) 144 | } 145 | 146 | if status.IsFailed(){ 147 | return r.applyTransition(ctx, "from failed", Pending, nil) 148 | } 149 | 150 | // Verify the resource state 151 | if status.IsVerifying() || status.IsPending() || status.IsSucceeded() || status.IsRecreating() { 152 | podSpec, _ := r.ResourceManager.Verify(ctx) 153 | podValue, err := r.spawnPod("verify", podSpec) 154 | fmt.Printf("%+v\n", err) 155 | r.instanceUpdater.setPodConfig(podValue) 156 | return r.applyTransition(ctx, "Check", Checking, nil) 157 | } 158 | 159 | // dependencies are now satisfied, can now reconcile the manifest and create or update the resource 160 | if status.IsCreating() { 161 | podSpec, _ := r.ResourceManager.Create(ctx) 162 | podValue, err := r.spawnPod("create", podSpec) 163 | fmt.Printf("%+v\n", err) 164 | r.instanceUpdater.setPodConfig(podValue) 165 | return r.applyTransition(ctx, "Check", Checking, nil) 166 | } 167 | 168 | // **** Updating 169 | if status.IsUpdating() { 170 | podSpec, _ := r.ResourceManager.Update(ctx) 171 | podValue, err := r.spawnPod("update", podSpec) 172 | fmt.Printf("%+v\n", err) 173 | r.instanceUpdater.setPodConfig(podValue) 174 | return r.applyTransition(ctx, "Check", Checking, nil) 175 | } 176 | 177 | // **** Completing 178 | // has created or updated, running completion step 179 | if status.IsCompleting() { 180 | return r.runCompletion(ctx) 181 | } 182 | 183 | // **** Terminating 184 | if status.IsTerminating() { 185 | r.log.Info("unexpected condition. Terminating state should be handled in finalizer") 186 | return ctrl.Result{}, nil 187 | } 188 | 189 | // if has no Status, set to pending 190 | return r.applyTransition(ctx, "run", Pending, nil) 191 | } 192 | 193 | func (r *ReconcileRunner) podDelete(ctx context.Context) (ctrl.Result, error) { 194 | instance, err := convertInstance(r.instance) 195 | 196 | pod := instance.Status.Pod 197 | found := &v1.Pod{} 198 | 199 | err = r.KubeClient.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, found) 200 | terminated := instance.Status.Terminated 201 | permissions := r.getAccessPermissions() 202 | r.log.Info(fmt.Sprintf("PodDeleting status: %v", err)) 203 | if err != nil && apierrors.IsNotFound(err) { 204 | switch pod.Type { 205 | case "recreate": 206 | state, err := r.recreate(DeleteResult("Succeeded")) 207 | return r.applyTransition(ctx, "check", state, err) 208 | case "create": 209 | if !permissions.create() { 210 | // this should never be the case - this is more of an assertion (as the state Verify or Create should never have been set in the first place) 211 | return r.applyTransition(ctx, "check", Failed, fmt.Errorf(rejectCreateManagedResource)) 212 | } 213 | return r.apply(ctx, ApplyResponse{ 214 | Result: ApplyResult(statusCodeMap[terminated.ExitCode]), 215 | Status: terminated.Message, 216 | }) 217 | case "update": 218 | if !permissions.update() { 219 | // this should never be the case - this is more of an assertion (as the state Verify or Create should never have been set in the first place) 220 | return r.applyTransition(ctx, "check", Failed, fmt.Errorf(rejectCreateManagedResource)) 221 | } 222 | return r.apply(ctx, ApplyResponse{ 223 | Result: ApplyResult(statusCodeMap[terminated.ExitCode]), 224 | Status: terminated.Message, 225 | }) 226 | case "verify": 227 | return r.verify(ctx, VerifyResponse{ 228 | Result: VerifyResult(statusCodeMap[terminated.ExitCode]), 229 | Status: terminated.Message, 230 | }) 231 | } 232 | } 233 | return r.applyTransition(ctx, "podDelete", PodDeleting, nil) 234 | } 235 | 236 | func (r *ReconcileRunner) check(ctx context.Context) (ctrl.Result, error) { 237 | 238 | instance, err := convertInstance(r.instance) 239 | 240 | pod := instance.Status.Pod 241 | found := &v1.Pod{} 242 | 243 | err = r.KubeClient.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, found) 244 | r.log.Info(fmt.Sprintf("PodChecking status: %v", err)) 245 | if err != nil && apierrors.IsNotFound(err) { 246 | return r.applyTransition(ctx, "check", Pending, err) 247 | } 248 | if len(found.Status.ContainerStatuses) == 0 { 249 | return r.applyTransition(ctx, "check", Checking, errors.New("ContainerStatuses array is of 0 length")) 250 | } 251 | terminated := found.Status.ContainerStatuses[0].State.Terminated 252 | // fmt.Printf("%+v\n", terminated) 253 | 254 | if terminated != nil { 255 | r.instanceUpdater.setTerminated(terminated) 256 | r.KubeClient.Delete(ctx, found) 257 | return r.applyTransition(ctx, "check", PodDeleting, nil) 258 | } 259 | return r.applyTransition(ctx, "check", Checking, nil) 260 | } 261 | 262 | func (r *ReconcileRunner) setOwner(ctx context.Context, owner runtime.Object) (ctrl.Result, error) { 263 | //set owner reference if it exists 264 | r.instanceUpdater.setOwnerReferences([]runtime.Object{owner}) 265 | if err := r.updateAndLog(ctx, v1.EventTypeNormal, "OwnerReferences", "setting OwnerReferences for "+r.Name); err != nil { 266 | return ctrl.Result{}, err 267 | } 268 | return ctrl.Result{}, nil 269 | } 270 | 271 | func (r *ReconcileRunner) verify(ctx context.Context, verifyResp VerifyResponse) (ctrl.Result, error) { 272 | nextState, ensureErr := r.verifyExecute(ctx, verifyResp) 273 | return r.applyTransition(ctx, "Verify", nextState, ensureErr) 274 | } 275 | 276 | const rejectCreateManagedResource = "permission to create resource is not set. Annotation '*/access-permissions' is present, but the flag 'C' is not set" 277 | const rejectUpdateManagedResource = "permission to update resource is not set. Annotation '*/access-permissions' is present, but the flag 'U' is not set" 278 | const rejectDeleteManagedResource = "permission to delete or recreate resource is not set. Annotation '*/access-permissions' is present, but the flag 'D' is not set" 279 | 280 | func (r *ReconcileRunner) verifyExecute(ctx context.Context, verifyResp VerifyResponse) (ReconcileState, error) { 281 | status := r.status 282 | currentState := status.State 283 | 284 | r.log.Info("Verifying state of resource") 285 | // verifyResponse, err := r.ResourceManager.Verify(ctx, r.resourceSpec()) 286 | verifyResult := verifyResp.Result 287 | permissions := r.getAccessPermissions() 288 | // **** Error 289 | // either an error was returned from the SDK, or the 290 | // if err != nil { 291 | // return Failed, err 292 | // } 293 | 294 | // **** Deleting 295 | // if the resource is any state where it exists, but the K8s resource is recreating 296 | // we assume that the resource is being deleted asynchronously, but there is no way to distinguish 297 | // from the SDK that is deleting - it is either present or not. so we requeue the loop and wait for it to become `missing` 298 | if verifyResult.deleting() || verifyResult.exists() && status.IsRecreating() { 299 | r.log.Info("Retrying verification: resource awaiting deletion before recreation can begin, requeuing reconcile loop") 300 | return currentState, nil 301 | } 302 | 303 | // **** Ready 304 | // The resource is finished creating or updating, completion step can take place if necessary 305 | if verifyResult.ready() { 306 | // set the Status payload if there is any 307 | r.instanceUpdater.setStatusPayload(verifyResp.Status) 308 | nextState := Completed 309 | // nextState := r.succeedOrComplete() 310 | // if ( "{{.RunOnce}}" == "1" ) { 311 | // nextState = Completed 312 | // } 313 | return nextState, nil 314 | } 315 | 316 | // **** Missing 317 | // We can now create the resource 318 | if verifyResult.missing() { 319 | // if not requeing failure, leave as failed 320 | if status.IsFailed() && r.Parameters.RequeueAfterFailure == 0 { 321 | return Failed, nil 322 | } 323 | if !permissions.create() { 324 | // fail if permission to create is not present 325 | return Failed, fmt.Errorf(rejectCreateManagedResource) 326 | } 327 | return Creating, nil 328 | } 329 | 330 | // **** InProgress 331 | // if still is in progress with create or update, requeue the reconcile loop 332 | if verifyResult.inProgress() { 333 | r.log.Info("Retrying verification: create or update in progress, requeuing reconcile loop") 334 | return currentState, nil 335 | } 336 | 337 | // **** UpdateRequired 338 | // The resource exists and is invalid but updateable, so doesn't need to be recreated 339 | if verifyResult.updateRequired() { 340 | if !permissions.update() { 341 | // fail if permission to update is not present 342 | return Failed, fmt.Errorf(rejectUpdateManagedResource) 343 | } 344 | return Updating, nil 345 | } 346 | 347 | // **** RecreateRequired 348 | // The resource exists and is invalid and needs to be created 349 | if verifyResult.recreateRequired() { 350 | if !permissions.delete() { 351 | // fail if permission to delete is not present 352 | return Failed, fmt.Errorf(rejectDeleteManagedResource) 353 | } 354 | // deleteResult, err := r.ResourceManager.Delete(ctx, r) 355 | podSpec, _ := r.ResourceManager.Delete(ctx) 356 | podValue, err := r.spawnPod("recreate", podSpec) 357 | fmt.Printf("%+v\n", err) 358 | r.instanceUpdater.setPodConfig(podValue) 359 | return Checking, nil 360 | // if err != nil || deleteResult == DeleteError { 361 | // return Failed, err 362 | // } 363 | 364 | // // set it back to pending and let it go through the whole process again 365 | // if deleteResult.awaitingVerification() { 366 | // return Recreating, err 367 | // } 368 | 369 | // if deleteResult.alreadyDeleted() || deleteResult.succeeded() { 370 | // return Creating, err 371 | // } 372 | 373 | // return Failed, fmt.Errorf("invalid DeleteResult for %s %s in Verify", r.ResourceKind, r.Name) 374 | } 375 | 376 | // **** Error 377 | return Failed, fmt.Errorf("invalid VerifyResult for %s %s in Verify, and no error was specified", r.ResourceKind, r.Name) 378 | } 379 | func (r *ReconcileRunner) recreate(deleteResult DeleteResult) (ReconcileState, error) { 380 | if deleteResult == DeleteError { 381 | return Failed, errors.New("Undetermined error") 382 | } 383 | 384 | // set it back to pending and let it go through the whole process again 385 | if deleteResult.awaitingVerification() { 386 | return Recreating, errors.New("Undetermined error") 387 | } 388 | 389 | if deleteResult.alreadyDeleted() || deleteResult.succeeded() { 390 | return Creating, errors.New("Undetermined error") 391 | } 392 | 393 | return Failed, fmt.Errorf("invalid DeleteResult for %s %s in Verify", r.ResourceKind, r.Name) 394 | } 395 | func (r *ReconcileRunner) apply(ctx context.Context, applyResp ApplyResponse) (ctrl.Result, error) { 396 | r.log.Info("Ready to create or update resource") 397 | nextState, ensureErr := r.applyExecute(ctx, applyResp) 398 | return r.applyTransition(ctx, "Ensure", nextState, ensureErr) 399 | } 400 | 401 | func (r *ReconcileRunner) applyExecute(ctx context.Context, applyResp ApplyResponse) (ReconcileState, error) { 402 | 403 | resourceName := r.Name 404 | instance := r.instance 405 | lastAppliedAnnotation := r.AnnotationBaseName + LastAppliedAnnotation 406 | 407 | // apply that the resource is created or updated (though it won't necessarily be ready, it still needs to be verified) 408 | // var err error 409 | // if status.IsCreating() { 410 | // if !permissions.create() { 411 | // // this should never be the case - this is more of an assertion (as the state Verify or Create should never have been set in the first place) 412 | // return Failed, fmt.Errorf(rejectCreateManagedResource) 413 | // } 414 | // applyResponse, err = r.ResourceManager.Create(ctx, r.resourceSpec()) 415 | // } else { 416 | // if !permissions.update() { 417 | // // this should never be the case - this is more of an assertion (as the state Verify or Create should never have been set in the first place) 418 | // return Failed, fmt.Errorf(rejectCreateManagedResource) 419 | // } 420 | // applyResponse, err = r.ResourceManager.Update(ctx, r.resourceSpec()) 421 | // } 422 | applyResult := applyResp.Result 423 | if applyResult == "" || applyResult.failed() { 424 | // clear last update annotation 425 | r.instanceUpdater.setAnnotation(lastAppliedAnnotation, "") 426 | errMsg := "Undetermined error" 427 | // if err != nil { 428 | // errMsg = err.Error() 429 | // } 430 | r.Recorder.Event(instance, v1.EventTypeWarning, "Failed", fmt.Sprintf("Couldn't create or update resource: %v", errMsg)) 431 | return Failed, errors.New(errMsg) 432 | } 433 | 434 | // if successful 435 | // save the last updated spec as a metadata annotation 436 | r.instanceUpdater.setAnnotation(lastAppliedAnnotation, r.getJsonSpec()) 437 | 438 | // set it to succeeded, completing (if there is a CompletionHandler), or await verification 439 | if applyResult.awaitingVerification() { 440 | r.instanceUpdater.setStatusPayload(applyResp.Status) 441 | return Verifying, nil 442 | } else if applyResult.succeeded() { 443 | r.instanceUpdater.setStatusPayload(applyResp.Status) 444 | return r.succeedOrComplete(), nil 445 | } else { 446 | return Failed, fmt.Errorf("invalid response from Create for resource '%s'", resourceName) 447 | } 448 | } 449 | 450 | func (r *ReconcileRunner) succeedOrComplete() ReconcileState { 451 | if r.CompletionFactory == nil || r.status.IsSucceeded() { 452 | return Succeeded 453 | } else { 454 | return Completing 455 | } 456 | } 457 | 458 | func (r *ReconcileRunner) runCompletion(ctx context.Context) (ctrl.Result, error) { 459 | var ppError error = nil 460 | if r.CompletionFactory != nil { 461 | if handler := r.CompletionFactory(r.GenericController); handler != nil { 462 | ppError = handler.Run(ctx, r.instance) 463 | } 464 | } 465 | if ppError != nil { 466 | return r.applyTransition(ctx, "Completion", Failed, ppError) 467 | } else { 468 | return r.applyTransition(ctx, "Completion", Succeeded, nil) 469 | } 470 | } 471 | 472 | func (r *ReconcileRunner) updateInstance(ctx context.Context) error { 473 | if !r.instanceUpdater.hasUpdates() { 474 | return nil 475 | } 476 | return r.tryUpdateInstance(ctx, 5) 477 | } 478 | 479 | // this is to get rid of the pesky errors 480 | // "Operation cannot be fulfilled on xxx:the object has been modified; please apply your changes to the latest version and try again" 481 | func (r *ReconcileRunner) tryUpdateInstance(ctx context.Context, count int) error { 482 | // refetch the instance and apply the updates to it 483 | baseInstance := r.instance 484 | instance := baseInstance.DeepCopyObject() 485 | err := r.KubeClient.Get(ctx, r.NamespacedName, baseInstance) 486 | if err != nil { 487 | if apierrors.IsNotFound(err) { 488 | r.log.Info("Unable to update deleted resource. it may have already been finalized. this error is ignorable. Resource: " + r.Name) 489 | return nil 490 | } else { 491 | r.log.Info("Unable to retrieve resource. falling back to prior instance: " + r.Name + ": err " + err.Error()) 492 | } 493 | } 494 | status, _ := r.StatusAccessor(instance) 495 | err = r.instanceUpdater.applyUpdates(instance, status) 496 | if err != nil { 497 | r.log.Info("Unable to convert Object to resource") 498 | r.instanceUpdater.clear() 499 | return err 500 | } 501 | err = r.KubeClient.Update(ctx, instance) 502 | if err != nil { 503 | if count == 0 { 504 | r.Recorder.Event(instance, v1.EventTypeWarning, "Update", fmt.Sprintf("failed to update %s instance %s on K8s cluster.", r.ResourceKind, r.Name)) 505 | r.instanceUpdater.clear() 506 | return err 507 | } 508 | r.log.Info(fmt.Sprintf("Failed to update CRD instance on K8s cluster. retries left=%d", count)) 509 | time.Sleep(2 * time.Second) 510 | return r.tryUpdateInstance(ctx, count-1) 511 | } else { 512 | r.instanceUpdater.clear() 513 | return nil 514 | } 515 | } 516 | 517 | func (r *ReconcileRunner) updateAndLog(ctx context.Context, eventType string, reason string, message string) error { 518 | instance := r.instance 519 | if !r.instanceUpdater.hasUpdates() { 520 | return nil 521 | } 522 | if err := r.updateInstance(ctx); err != nil { 523 | r.log.Info(fmt.Sprintf("K8s update failure: %v", err)) 524 | r.Recorder.Event(instance, v1.EventTypeWarning, reason, fmt.Sprintf("failed to update instance of %s %s in kubernetes cluster", r.ResourceKind, r.Name)) 525 | return err 526 | } 527 | r.Recorder.Event(instance, eventType, reason, message) 528 | return nil 529 | } 530 | 531 | func (r *ReconcileRunner) getTransitionDetails(nextState ReconcileState) (ctrl.Result, string) { 532 | requeueAfter := r.getRequeueAfter(nextState) 533 | requeueResult := ctrl.Result{Requeue: requeueAfter > 0, RequeueAfter: requeueAfter} 534 | message := "" 535 | switch nextState { 536 | case Pending: 537 | message = fmt.Sprintf("%s %s in pending state.", r.ResourceKind, r.Name) 538 | case Creating: 539 | message = fmt.Sprintf("%s %s ready for creation.", r.ResourceKind, r.Name) 540 | case Updating: 541 | message = fmt.Sprintf("%s %s ready to be updated.", r.ResourceKind, r.Name) 542 | case Verifying: 543 | message = fmt.Sprintf("%s %s verification in progress.", r.ResourceKind, r.Name) 544 | case Completing: 545 | message = fmt.Sprintf("%s %s create or update succeeded and ready for completion step", r.ResourceKind, r.Name) 546 | case Succeeded: 547 | message = fmt.Sprintf("%s %s successfully applied and ready for use.", r.ResourceKind, r.Name) 548 | case Recreating: 549 | message = fmt.Sprintf("%s %s deleting and recreating in progress.", r.ResourceKind, r.Name) 550 | case Failed: 551 | message = fmt.Sprintf("%s %s failed.", r.ResourceKind, r.Name) 552 | case Checking: 553 | message = fmt.Sprint("Checking.") 554 | case PodDeleting: 555 | message = fmt.Sprint("PodDeleting.") 556 | case Terminating: 557 | message = fmt.Sprintf("%s %s termination in progress.", r.ResourceKind, r.Name) 558 | case Completed: 559 | message = fmt.Sprintf("completed") 560 | default: 561 | message = fmt.Sprintf("%s %s set to state %s", r.ResourceKind, r.Name, nextState) 562 | } 563 | return requeueResult, message 564 | } 565 | 566 | func (r *ReconcileRunner) applyTransition(ctx context.Context, reason string, nextState ReconcileState, transitionErr error) (ctrl.Result, error) { 567 | eventType := v1.EventTypeNormal 568 | if nextState == Failed { 569 | eventType = v1.EventTypeWarning 570 | } 571 | errorMsg := "" 572 | if transitionErr != nil { 573 | errorMsg = transitionErr.Error() 574 | } 575 | if nextState != r.status.State { 576 | r.instanceUpdater.setReconcileState(nextState, errorMsg) 577 | } 578 | result, transitionMsg := r.getTransitionDetails(nextState) 579 | updateErr := r.updateAndLog(ctx, eventType, reason, transitionMsg) 580 | if transitionErr != nil { 581 | if updateErr != nil { 582 | // TODO: is the transition error is more important? 583 | // we don't requeue if there is an update error 584 | return ctrl.Result{}, transitionErr 585 | } else { 586 | return result, nil 587 | } 588 | } 589 | if updateErr != nil { 590 | return ctrl.Result{}, updateErr 591 | } 592 | return result, nil 593 | } 594 | 595 | func (r *ReconcileRunner) getRequeueAfter(transitionState ReconcileState) time.Duration { 596 | parameters := r.Parameters 597 | requeueAfterDuration := func(requeueSeconds int) time.Duration { 598 | requeueAfter := time.Duration(requeueSeconds) * time.Millisecond 599 | return requeueAfter 600 | } 601 | 602 | if transitionState == Completed { 603 | if ("{{.RunOnce}}" == "1") { 604 | r.log.Info("Suspended reconciliation as per the configuration") 605 | return time.Duration(0) 606 | } else{ 607 | requeueAfter := requeueAfterDuration(parameters.RequeueAfter) 608 | r.log.Info(fmt.Sprintf("Reconcile runner freq set to - %s", requeueAfter)) 609 | return requeueAfter 610 | } 611 | } 612 | 613 | if transitionState == Verifying || 614 | transitionState == PodDeleting || 615 | transitionState == Checking || 616 | transitionState == Creating || 617 | transitionState == Updating || 618 | transitionState == Pending || 619 | transitionState == Succeeded || 620 | transitionState == Recreating { 621 | // must by default have a non zero requeue for these states 622 | return requeueAfterDuration(parameters.RequeueAfterSuccess) 623 | } else if transitionState == Failed { 624 | r.log.Info("Terminated reconciliation as pod failed") 625 | return time.Duration(0) 626 | } 627 | return 0 628 | } 629 | 630 | func (r *ReconcileRunner) getJsonSpec() string { 631 | fetch := func() (string, error) { 632 | b, err := json.Marshal(r.instance) 633 | if err != nil { 634 | return "", err 635 | } 636 | var asMap map[string]interface{} 637 | err = json.Unmarshal(b, &asMap) 638 | if err != nil { 639 | return "", err 640 | } 641 | spec := asMap["spec"] 642 | b, err = json.Marshal(spec) 643 | if err != nil { 644 | return "", err 645 | } 646 | return string(b), nil 647 | } 648 | 649 | jsonSpec, err := fetch() 650 | if err != nil { 651 | r.log.Info("Error fetching Json for instance spec") 652 | return "" 653 | } 654 | return jsonSpec 655 | } 656 | 657 | func (r *ReconcileRunner) getAccessPermissions() AccessPermissions { 658 | annotations := r.objectMeta.GetAnnotations() 659 | return AccessPermissions(annotations[r.AnnotationBaseName+AccessPermissionAnnotation]) 660 | } 661 | 662 | type PodSpec struct { 663 | Name string 664 | Namespace string 665 | LogPath string 666 | Arguments string 667 | Image string 668 | ImagePullPolicy string 669 | ImagePullSecrets string 670 | } 671 | 672 | func (r *ReconcileRunner) spawnPod(event string, podInputSpec PodSpec) ({{ .Resource }}{{ .Version }}.Pod, error) { 673 | instance, err := getSpec(r.instance) 674 | var args []string 675 | for _, v := range strings.Split(podInputSpec.Arguments, " ") { 676 | vToAppend := v 677 | switch v { 678 | case "{spec}": 679 | tmp, _ := instance.Spec.MarshalJSON() 680 | vToAppend = string(tmp) 681 | // vToAppend = instance.Spec 682 | case "{type}": 683 | vToAppend = event 684 | case "{status}": 685 | tmp, _ := json.Marshal(instance.Status) 686 | vToAppend = string(tmp) 687 | case "{logPath}": 688 | vToAppend = podInputSpec.LogPath 689 | } 690 | args = append(args, vToAppend) 691 | } 692 | podName := fmt.Sprintf("%s-%s", r.NamespacedName.Name, podInputSpec.Name) 693 | podNamespace := podInputSpec.Namespace 694 | 695 | pod := &v1.Pod{ 696 | ObjectMeta: metav1.ObjectMeta{ 697 | Name: podName, 698 | Namespace: podNamespace, 699 | Labels: map[string]string{ 700 | "app": podInputSpec.Name, 701 | }, 702 | }, 703 | Spec: v1.PodSpec{ 704 | Containers: []v1.Container{ 705 | { 706 | Name: podInputSpec.Name, 707 | Image: podInputSpec.Image, 708 | Env: create_envs(), 709 | Args: args, 710 | TerminationMessagePath: podInputSpec.LogPath, 711 | ImagePullPolicy: v1.PullPolicy(podInputSpec.ImagePullPolicy), 712 | Resources: getResourceRequirements(getResourceList("", ""), getResourceList("{{ .CpuLimit }}", "{{ .MemoryLimit }}")), 713 | }, 714 | }, 715 | 716 | ImagePullSecrets: []v1.LocalObjectReference{{"{{"}} 717 | Name: podInputSpec.ImagePullSecrets{{"}}"}}, 718 | RestartPolicy: v1.RestartPolicyNever, 719 | }, 720 | } 721 | 722 | if err := controllerutil.SetControllerReference(instance, pod, r.GenericController.Scheme); err != nil { 723 | // requeue with error 724 | return {{ .Resource }}{{ .Version }}.Pod{}, err 725 | } 726 | 727 | found := &v1.Pod{} 728 | err = r.KubeClient.Get(context.TODO(), types.NamespacedName{Name: podName, Namespace: podNamespace}, found) 729 | // fmt.Println(event, err) 730 | if err != nil && apierrors.IsNotFound(err) { 731 | er2 := r.KubeClient.Create(context.TODO(), pod) 732 | r.log.Info(fmt.Sprintf("SpawnPod status: %v", err)) 733 | if er2 != nil { 734 | return {{ .Resource }}{{ .Version }}.Pod{}, er2 735 | } 736 | } 737 | return {{ .Resource }}{{ .Version }}.Pod{ 738 | Name: podName, 739 | Namespace: podNamespace, 740 | Type: event, 741 | }, nil 742 | } 743 | 744 | func getResourceRequirements(requests, limits v1.ResourceList) v1.ResourceRequirements { 745 | res := v1.ResourceRequirements{} 746 | res.Requests = requests 747 | res.Limits = limits 748 | return res 749 | } 750 | 751 | func getResourceList(cpu, memory string) v1.ResourceList { 752 | res := v1.ResourceList{} 753 | if cpu != "" { 754 | res[v1.ResourceCPU] = resource.MustParse(cpu) 755 | } 756 | if memory != "" { 757 | res[v1.ResourceMemory] = resource.MustParse(memory) 758 | } 759 | return res 760 | } 761 | 762 | func create_envs() []v1.EnvVar { 763 | filePath := "./controller.json" 764 | //fmt.Printf("// reading file %s\n", filePath) 765 | file, err1 := ioutil.ReadFile(filePath) 766 | if err1 != nil { 767 | fmt.Printf("// error while reading file %s\n", filePath) 768 | fmt.Printf("File error: %v\n", err1) 769 | os.Exit(1) 770 | } 771 | 772 | var apiconfigs map[string]string 773 | 774 | err2 := json.Unmarshal(file, &apiconfigs) 775 | if err2 != nil { 776 | fmt.Println("error:", err2) 777 | } 778 | 779 | var envs []v1.EnvVar 780 | for k := range apiconfigs { 781 | envs = append(envs, v1.EnvVar{Name: k, Value: apiconfigs[k]}) 782 | } 783 | 784 | return envs 785 | 786 | } 787 | func (r *ReconcileRunner) get_docker_creds(secret_engine string) (string, string, string) { 788 | defer func() { 789 | if err := recover(); err != nil { 790 | r.log.Info(fmt.Sprintf("Exception occured while secret reveal from vault - %v", err)) 791 | return 792 | } 793 | }() 794 | 795 | secret, err := vault.Read(secret_engine) 796 | 797 | if err != nil { 798 | r.log.Info(fmt.Sprintf("Panic occured while reading secret, error - %s", err.Error())) 799 | return "", "", "" 800 | } 801 | 802 | username := secret.Data["username"].(string) 803 | password := secret.Data["password"].(string) 804 | server := secret.Data["server"].(string) 805 | 806 | return username, password, server 807 | } 808 | -------------------------------------------------------------------------------- /_base-operator/reconciler/reconcile_status.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | /* 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package reconciler 22 | import ( 23 | api "{{ .Repo }}/api/{{ .Version }}" 24 | corev1 "k8s.io/api/core/v1" 25 | ) 26 | type ReconcileState string 27 | 28 | const ( 29 | Pending ReconcileState = "Pending" 30 | Creating ReconcileState = "Creating" 31 | Updating ReconcileState = "Updating" 32 | Verifying ReconcileState = "Verifying" 33 | Checking ReconcileState = "Checking" 34 | Completing ReconcileState = "Completing" 35 | PodDeleting ReconcileState = "PodDeleting" 36 | Succeeded ReconcileState = "Succeeded" 37 | Recreating ReconcileState = "Recreating" 38 | Failed ReconcileState = "Failed" 39 | Terminating ReconcileState = "Terminating" 40 | Completed ReconcileState = "Completed" 41 | ) 42 | 43 | func (s *Status) IsPending() bool { return s.State == Pending } 44 | func (s *Status) IsCreating() bool { return s.State == Creating } 45 | func (s *Status) IsUpdating() bool { return s.State == Updating } 46 | func (s *Status) IsVerifying() bool { return s.State == Verifying } 47 | func (s *Status) IsCompleting() bool { return s.State == Completing } 48 | func (s *Status) IsChecking() bool { return s.State == Checking } 49 | func (s *Status) IsPodDeleting() bool { return s.State == PodDeleting } 50 | func (s *Status) IsSucceeded() bool { return s.State == Succeeded } 51 | func (s *Status) IsRecreating() bool { return s.State == Terminating } 52 | func (s *Status) IsFailed() bool { return s.State == Failed } 53 | func (s *Status) IsTerminating() bool { return s.State == Terminating } 54 | func (s *Status) IsCompleted() bool { return s.State == Completed } 55 | 56 | type Status struct { 57 | State ReconcileState 58 | Message string 59 | StatusPayload interface{} 60 | Pod api.Pod 61 | Terminated *corev1.ContainerStateTerminated 62 | } 63 | -------------------------------------------------------------------------------- /_base-operator/reconciler/resource_manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package reconciler 17 | 18 | import ( 19 | "context" 20 | 21 | "k8s.io/apimachinery/pkg/runtime" 22 | "k8s.io/apimachinery/pkg/types" 23 | ) 24 | 25 | type ResourceSpec struct { 26 | Instance runtime.Object 27 | Dependencies map[types.NamespacedName]runtime.Object 28 | } 29 | 30 | // ResourceManager is a common abstraction for the controller to interact with external resources 31 | // The ResourceManager cannot modify the runtime.Object kubernetes object 32 | // it only needs to query or mutate the external resource, return the Result of the operation 33 | type ResourceManager interface { 34 | // Creates an external resource, though it doesn't verify the readiness for consumption 35 | Create(context.Context) (PodSpec, error) 36 | // Updates an external resource 37 | Update(context.Context) (PodSpec, error) 38 | // Verifies the state of the external resource 39 | Verify(context.Context) (PodSpec, error) 40 | // Deletes external resource 41 | Delete(context.Context) (PodSpec, error) 42 | } 43 | 44 | // The Result of a create or update operation 45 | type ApplyResult string 46 | 47 | const ( 48 | ApplyResultAwaitingVerification ApplyResult = "AwaitingVerification" 49 | ApplyResultSucceeded ApplyResult = "Succeeded" 50 | ApplyResultError ApplyResult = "Error" 51 | ) 52 | 53 | // The Result of a verify operation 54 | type VerifyResult string 55 | 56 | const ( 57 | VerifyResultMissing VerifyResult = "Missing" 58 | VerifyResultRecreateRequired VerifyResult = "RecreateRequired" 59 | VerifyResultUpdateRequired VerifyResult = "UpdateRequired" 60 | VerifyResultInProgress VerifyResult = "InProgress" 61 | VerifyResultDeleting VerifyResult = "Deleting" 62 | VerifyResultReady VerifyResult = "Ready" 63 | VerifyResultError VerifyResult = "Error" 64 | ) 65 | 66 | // The Result of a delete operation 67 | type DeleteResult string 68 | 69 | const ( 70 | DeleteAlreadyDeleted DeleteResult = "AlreadyDeleted" 71 | DeleteSucceeded DeleteResult = "Succeeded" 72 | DeleteAwaitingVerification DeleteResult = "AwaitingVerification" 73 | DeleteError DeleteResult = "Error" 74 | ) 75 | 76 | func (r VerifyResult) error() bool { return r == VerifyResultError } 77 | func (r VerifyResult) missing() bool { return r == VerifyResultMissing } 78 | func (r VerifyResult) recreateRequired() bool { return r == VerifyResultRecreateRequired } 79 | func (r VerifyResult) updateRequired() bool { return r == VerifyResultUpdateRequired } 80 | func (r VerifyResult) inProgress() bool { return r == VerifyResultInProgress } 81 | func (r VerifyResult) deleting() bool { return r == VerifyResultDeleting } 82 | func (r VerifyResult) ready() bool { return r == VerifyResultReady } 83 | func (r VerifyResult) exists() bool { return !r.error() && !r.missing() } 84 | 85 | func (r ApplyResult) succeeded() bool { return r == ApplyResultSucceeded } 86 | func (r ApplyResult) awaitingVerification() bool { return r == ApplyResultAwaitingVerification } 87 | func (r ApplyResult) failed() bool { return r == ApplyResultError } 88 | 89 | func (r DeleteResult) error() bool { return r == DeleteError } 90 | func (r DeleteResult) alreadyDeleted() bool { return r == DeleteAlreadyDeleted } 91 | func (r DeleteResult) succeeded() bool { return r == DeleteSucceeded } 92 | func (r DeleteResult) awaitingVerification() bool { return r == DeleteAwaitingVerification } 93 | 94 | // The Result of a create or update operation, along with Status information, if present 95 | type ApplyResponse struct { 96 | Result ApplyResult 97 | Status interface{} 98 | } 99 | 100 | var ( 101 | ApplyAwaitingVerification = ApplyResponse{Result: ApplyResultAwaitingVerification} 102 | ApplySucceeded = ApplyResponse{Result: ApplyResultSucceeded} 103 | ApplyError = ApplyResponse{Result: ApplyResultError} 104 | ) 105 | 106 | func ApplyAwaitingVerificationWithStatus(status interface{}) ApplyResponse { 107 | return ApplyResponse{ 108 | Result: ApplyResultAwaitingVerification, 109 | Status: status, 110 | } 111 | } 112 | 113 | func ApplySucceededWithStatus(status interface{}) ApplyResponse { 114 | return ApplyResponse{ 115 | Result: ApplyResultSucceeded, 116 | Status: status, 117 | } 118 | } 119 | 120 | type VerifyResponse struct { 121 | Result VerifyResult 122 | Status interface{} 123 | } 124 | 125 | var ( 126 | VerifyError = VerifyResponse{Result: VerifyResultError} 127 | VerifyMissing = VerifyResponse{Result: VerifyResultMissing} 128 | VerifyRecreateRequired = VerifyResponse{Result: VerifyResultRecreateRequired} 129 | VerifyUpdateRequired = VerifyResponse{Result: VerifyResultUpdateRequired} 130 | VerifyInProgress = VerifyResponse{Result: VerifyResultInProgress} 131 | VerifyDeleting = VerifyResponse{Result: VerifyResultDeleting} 132 | VerifyReady = VerifyResponse{Result: VerifyResultReady} 133 | ) 134 | 135 | func VerifyReadyWithStatus(status interface{}) VerifyResponse { 136 | return VerifyResponse{ 137 | Result: VerifyResultReady, 138 | Status: status, 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /_base-operator/reconciler/string_helper.go: -------------------------------------------------------------------------------- 1 | package reconciler 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | func containsString(slice []string, s string) bool { 8 | for _, item := range slice { 9 | if item == s { 10 | return true 11 | } 12 | } 13 | return false 14 | } 15 | 16 | // addFinalizer accepts a metav1 object and adds the provided finalizer if not present. 17 | func addFinalizer(o metav1.Object, finalizer string) { 18 | f := o.GetFinalizers() 19 | for _, e := range f { 20 | if e == finalizer { 21 | return 22 | } 23 | } 24 | o.SetFinalizers(append(f, finalizer)) 25 | } 26 | 27 | // removeFinalizer accepts a metav1 object and removes the provided finalizer if present. 28 | func removeFinalizer(o metav1.Object, finalizer string) { 29 | f := o.GetFinalizers() 30 | for i, e := range f { 31 | if e == finalizer { 32 | f = append(f[:i], f[i+1:]...) 33 | o.SetFinalizers(f) 34 | return 35 | } 36 | } 37 | } 38 | 39 | // hasFinalizer accepts a metav1 object and returns true if the the object has the provided finalizer. 40 | func hasFinalizer(o metav1.Object, finalizer string) bool { 41 | return containsString(o.GetFinalizers(), finalizer) 42 | } 43 | -------------------------------------------------------------------------------- /_base-operator/v1/resource.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | /* 7 | 8 | Licensed under the Apache License, .Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package {{ .Version }} 22 | 23 | import ( 24 | meta{{ .Version }} "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | corev1 "k8s.io/api/core/v1" 26 | ) 27 | 28 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 29 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 30 | 31 | // {{ .Resource }}Spec defines the desired state of {{ .Resource }} 32 | 33 | type Pod struct { 34 | Name string `json:"name,omitempty"` 35 | Namespace string `json:"namespace,omitempty"` 36 | Type string `json:"type,omitempty"` 37 | } 38 | 39 | // {{ .Resource }}Status defines the observed state of {{ .Resource }} 40 | type {{ .Resource }}Status struct { 41 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 42 | // Important: Run "make" to regenerate code after modifying this file 43 | StatusPayload string `json:"statusPayload,omitempty"` 44 | Pod Pod `json:"pod,omitempty"` 45 | State string `json:"state,omitempty"` 46 | Message string `json:"message,omitempty"` 47 | Terminated *corev1.ContainerStateTerminated `json:"terminated,omitempty"` 48 | } 49 | 50 | // +kubebuilder:object:root=true 51 | 52 | // {{ .Resource }} is the Schema for the {{ .Resource }}s API 53 | type {{ .Resource }} struct { 54 | meta{{ .Version }}.TypeMeta `json:",inline"` 55 | meta{{ .Version }}.ObjectMeta `json:"metadata,omitempty"` 56 | 57 | Spec Root `json:"spec,omitempty"` 58 | Status {{ .Resource }}Status `json:"status,omitempty"` 59 | } 60 | 61 | // +kubebuilder:object:root=true 62 | 63 | // {{ .Resource }}List contains a list of {{ .Resource }} 64 | type {{ .Resource }}List struct { 65 | meta{{ .Version }}.TypeMeta `json:",inline"` 66 | meta{{ .Version }}.ListMeta `json:"metadata,omitempty"` 67 | Items []{{ .Resource }} `json:"items"` 68 | } 69 | 70 | func init() { 71 | SchemeBuilder.Register(&{{ .Resource }}{}, &{{ .Resource }}List{}) 72 | } 73 | -------------------------------------------------------------------------------- /cmd/base/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package base 7 | 8 | // Build information. Populated at build-time. 9 | var ( 10 | Version string 11 | Revision string 12 | Branch string 13 | BuildUser string 14 | BuildDate string 15 | GoVersion string 16 | ) 17 | 18 | // Info provides the iterable version information. 19 | var Info = map[string]string{ 20 | "version": Version, 21 | "revision": Revision, 22 | "branch": Branch, 23 | "buildUser": BuildUser, 24 | "buildDate": BuildDate, 25 | "goVersion": GoVersion, 26 | } 27 | 28 | var VersionStr = ` 29 | ****************** craft ****************** 30 | Version : %s 31 | Revision : %s 32 | Branch : %s 33 | Build-User : %s 34 | Build-Date : %s 35 | Go-Version : %s 36 | ***************************************** 37 | ` 38 | -------------------------------------------------------------------------------- /cmd/build.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package cmd 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | 16 | pathLib "path" 17 | 18 | "craft/utils" 19 | 20 | log "github.com/sirupsen/logrus" 21 | "github.com/spf13/cobra" 22 | "github.com/spf13/viper" 23 | apps "k8s.io/api/apps/v1" 24 | corev1 "k8s.io/api/core/v1" 25 | apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/runtime/serializer" 28 | "k8s.io/cli-runtime/pkg/printers" 29 | "k8s.io/client-go/kubernetes/scheme" 30 | ) 31 | 32 | func buildCmd() *cobra.Command { 33 | cmd := &cobra.Command{ 34 | Use: "build", 35 | Aliases: []string{"b"}, 36 | Short: "for building (code | deploy| images)", 37 | Long: `for building (code |deploy| images)`, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | }, 40 | } 41 | cmd.AddCommand( 42 | imageBuildCmd(), 43 | deployBuildCmd(), 44 | codeBuildCmd(), 45 | ) 46 | cmd.PersistentFlags().StringVarP(&apiFile, "controllerFile", "c", "", "controller file with group, resource and other info") 47 | cmd.MarkPersistentFlagRequired("controllerFile") 48 | 49 | if err := viper.BindPFlag("controllerFile", cmd.Flags().Lookup("controllerFile")); err != nil { 50 | log.Fatal(err) 51 | } 52 | return cmd 53 | } 54 | 55 | var ( 56 | environ string 57 | ) 58 | 59 | func RWYaml(deployFile string) { 60 | sch := runtime.NewScheme() 61 | _ = scheme.AddToScheme(sch) 62 | _ = apiextv1beta1.AddToScheme(sch) 63 | decode := serializer.NewCodecFactory(sch).UniversalDeserializer().Decode 64 | stream, err := ioutil.ReadFile(deployFile) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | objList := strings.Split(fmt.Sprintf("%s", stream), "---\n") 69 | newFile, err := os.Create(deployFile) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | defer newFile.Close() 74 | y := printers.YAMLPrinter{} 75 | for _, f := range objList { 76 | obj, gKV, err := decode([]byte(f), nil, nil) 77 | if err != nil { 78 | log.Println(fmt.Sprintf("Error while decoding YAML object. Err was: %s", err)) 79 | continue 80 | } 81 | switch gKV.Kind { 82 | case "Namespace": 83 | n := obj.(*corev1.Namespace) 84 | n.ObjectMeta.Name = apiFileObj.Namespace 85 | y.PrintObj(n, newFile) 86 | case "Deployment": 87 | n := obj.(*apps.Deployment) 88 | n.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{ 89 | {Name: apiFileObj.ImagePullSecrets}, 90 | } 91 | y.PrintObj(n, newFile) 92 | case "CustomResourceDefinition": 93 | n := obj.(*apiextv1beta1.CustomResourceDefinition) 94 | var m apiextv1beta1.JSONSchemaProps 95 | s, err := ioutil.ReadFile(resourceFile) 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | json.Unmarshal(s, &m) 100 | n.Spec.Validation.OpenAPIV3Schema.Properties["spec"] = m 101 | y.PrintObj(n, newFile) 102 | default: 103 | y.PrintObj(obj, newFile) 104 | } 105 | } 106 | } 107 | 108 | func deployBuildCmd() *cobra.Command { 109 | cmd := &cobra.Command{ 110 | Use: "deploy", 111 | Aliases: []string{"d"}, 112 | Short: "build deploy", 113 | Long: `build deploy`, 114 | Run: func(cmd *cobra.Command, args []string) { 115 | absPath() 116 | apiFileObj.loadApi(apiFile) 117 | baseDir := filepath.Dir(apiFile) 118 | newOperatorPath := pathLib.Join(goSrc, apiFileObj.Repo) 119 | 120 | deployPath := pathLib.Join(baseDir, "deploy") 121 | deployFile := pathLib.Join(deployPath, "operator.yaml") 122 | 123 | log.Debugf("Mkdir deploy %s", deployPath) 124 | os.MkdirAll(deployPath, os.ModePerm) 125 | 126 | utils.EnvCmdExec("go build -a -o bin/manager main.go", 127 | newOperatorPath, 128 | []string{"CGO_ENABLED=0", "GOOS=linux", "GOARCH=amd64", "GO111MODULE=on"}) 129 | 130 | cmdString := fmt.Sprintf("make operator IMG=%s NAMESPACE=%s FILE=%s", 131 | apiFileObj.OperatorImage, 132 | apiFileObj.Namespace, 133 | deployFile) 134 | utils.CmdExec(cmdString, newOperatorPath) 135 | 136 | RWYaml(deployFile) 137 | 138 | utils.Validate(deployFile) 139 | 140 | }, 141 | } 142 | cmd.PersistentFlags().StringVarP(&resourceFile, "resourceFile", "r", "", "resourcefile with properties of resource") 143 | cmd.PersistentFlags().StringVarP(&environ, "environ", "e", "", "which environment to use for envyaml") 144 | 145 | cmd.MarkPersistentFlagRequired("resourceFile") 146 | 147 | if err := viper.BindPFlag("resourceFile", cmd.Flags().Lookup("resourceFile")); err != nil { 148 | log.Fatal(err) 149 | } 150 | if err := viper.BindPFlag("environ", cmd.Flags().Lookup("environ")); err != nil { 151 | log.Fatal(err) 152 | } 153 | return cmd 154 | } 155 | -------------------------------------------------------------------------------- /cmd/code.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package cmd 7 | 8 | import ( 9 | "craft/utils" 10 | "fmt" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | "io/ioutil" 15 | "os" 16 | pathLib "path" 17 | "path/filepath" 18 | "strings" 19 | "text/template" 20 | ) 21 | 22 | var ( 23 | resourceFile string 24 | baseOperator string 25 | ) 26 | 27 | func renderTemplate(operatorPath string) { 28 | resourceDef := fmt.Sprintf("api/%s/%s_types.go", 29 | apiFileObj.Version, 30 | strings.ToLower(apiFileObj.Resource)) 31 | 32 | utils.MinCmdExec("svn checkout https://github.com/salesforce/craft/trunk/_base-operator", operatorPath) 33 | baseOperator = pathLib.Join(operatorPath, "_base-operator") 34 | 35 | dirs := []string{"controllers", "reconciler", "main.go", "Dockerfile", "v1/resource.go", "Makefile"} 36 | for _, dir := range dirs { 37 | path := pathLib.Join(baseOperator, dir) 38 | 39 | err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 40 | if utils.FileExists(path) { 41 | tpl, err := template.ParseFiles(path) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | newPath := strings.Replace(path, baseOperator, operatorPath, 1) 47 | if strings.HasSuffix(path, "v1/resource.go") { 48 | newPath = pathLib.Join(operatorPath, resourceDef) 49 | } 50 | log.Debugf("Rendering file: %s", newPath) 51 | fi, err := os.Create(newPath) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | err = tpl.Execute(fi, apiFileObj) 57 | 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | } 62 | return nil 63 | }) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | } 68 | 69 | utils.CmdExec("rm -rf _base-operator", operatorPath) 70 | } 71 | 72 | func cpFile(operatorPath string) { 73 | dirs := []string{"controllers", "reconciler"} 74 | for _, dir := range dirs { 75 | pth := pathLib.Join(operatorPath, dir) 76 | os.MkdirAll(pth, os.ModePerm) 77 | } 78 | } 79 | 80 | func cpAPIFile(apiFile string, operatorPath string) { 81 | input, err := ioutil.ReadFile(apiFile) 82 | if err != nil { 83 | fmt.Println(err) 84 | return 85 | } 86 | 87 | dstFile := filepath.Join(operatorPath, "controller.json") 88 | err = ioutil.WriteFile(dstFile, input, 0644) 89 | if err != nil { 90 | fmt.Println("Error creating", dstFile) 91 | fmt.Println(err) 92 | return 93 | } 94 | } 95 | 96 | func codeBuildCmd() *cobra.Command { 97 | cmd := &cobra.Command{ 98 | Use: "code", 99 | Aliases: []string{"c"}, 100 | Short: "create operator template in $GOPATH/src", 101 | Long: `create operator template in $GOPATH/src`, 102 | Run: func(cmd *cobra.Command, args []string) { 103 | absPath() 104 | apiFileObj.loadApi(apiFile) 105 | 106 | apiFileObj.LowerRes = strings.ToLower(apiFileObj.Resource) 107 | var kubeCmdString string 108 | newOperatorPath := pathLib.Join(goSrc, apiFileObj.Repo) 109 | 110 | os.RemoveAll(newOperatorPath) 111 | os.MkdirAll(newOperatorPath, os.ModePerm) 112 | 113 | kubeCmdString = fmt.Sprintf("kubebuilder init --domain %s --repo %s", apiFileObj.Domain, apiFileObj.Repo) 114 | utils.CmdExec(kubeCmdString, newOperatorPath) 115 | 116 | kubeCmdString = fmt.Sprintf("kubebuilder create api --group %s --version %s --kind %s --resource=true --controller=true", 117 | apiFileObj.Group, 118 | apiFileObj.Version, 119 | apiFileObj.Resource, 120 | ) 121 | utils.CmdExec(kubeCmdString, newOperatorPath) 122 | 123 | utils.CmdExec("rm -rf controllers", newOperatorPath) 124 | 125 | cpFile(newOperatorPath) 126 | cpAPIFile(apiFile, newOperatorPath) 127 | 128 | kubeCmdString = fmt.Sprintf("rm -rf api/%s/%s_types.go", 129 | apiFileObj.Version, 130 | apiFileObj.LowerRes) 131 | utils.CmdExec(kubeCmdString, newOperatorPath) 132 | 133 | kubeCmdString = fmt.Sprintf("schema-generate -p %s -o api/%s/spec_type.go %s", 134 | apiFileObj.Version, 135 | apiFileObj.Version, 136 | resourceFile) 137 | utils.CmdExec(kubeCmdString, newOperatorPath) 138 | 139 | renderTemplate(newOperatorPath) 140 | utils.CmdExec("make generate", newOperatorPath) 141 | }, 142 | } 143 | 144 | cmd.PersistentFlags().StringVarP(&apiFile, "controllerFile", "c", "", "controller file with group, resource and other info") 145 | cmd.PersistentFlags().StringVarP(&resourceFile, "resourceFile", "r", "", "resourcefile with properties of resource") 146 | cmd.MarkPersistentFlagRequired("controllerFile") 147 | cmd.MarkPersistentFlagRequired("resourceFile") 148 | 149 | if err := viper.BindPFlag("controllerFile", cmd.Flags().Lookup("controllerFile")); err != nil { 150 | log.Fatal(err) 151 | } 152 | if err := viper.BindPFlag("resourceFile", cmd.Flags().Lookup("resourceFile")); err != nil { 153 | log.Fatal(err) 154 | } 155 | 156 | return cmd 157 | } 158 | -------------------------------------------------------------------------------- /cmd/common.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package cmd 7 | 8 | import ( 9 | "crypto/rand" 10 | "encoding/hex" 11 | "io/ioutil" 12 | "log" 13 | 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | var ( 18 | apiFile string 19 | apiFileObj ApiFileStruct 20 | ) 21 | 22 | type ApiFileStruct struct { 23 | Group string `json:"group" yaml:"group"` 24 | Repo string `json:"repo" yaml:"repo"` 25 | Resource string `json:"resource" yaml:"resource"` 26 | Domain string `json:"domain" yaml:"domain"` 27 | Version string `json:"version" yaml:"version"` 28 | Namespace string `json:"namespace" yaml:"namespace"` 29 | Image string `json:"image" yaml:"image"` 30 | CpuLimit string `json:"cpu_limit" yaml:"cpu_limit"` 31 | MemoryLimit string `json:"memory_limit" yaml:"memory_limit"` 32 | ImagePullPolicy string `json:"imagePullPolicy" yaml:"imagePullPolicy"` 33 | ImagePullSecrets string `json:"imagePullSecrets" yaml:"imagePullSecrets"` 34 | OperatorImage string `json:"operator_image" yaml:"operator_image"` 35 | ReconcileFreq string `json:"reconcileFreq" yaml:"reconcileFreq"` 36 | RunOnce string `json:"runOnce" yaml:"runOnce"` 37 | LeaderElectionID string 38 | LowerRes string 39 | } 40 | 41 | func (api *ApiFileStruct) loadApi(path string) { 42 | stream, err := ioutil.ReadFile(path) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | _ = yaml.Unmarshal([]byte(stream), &api) 47 | 48 | api.LeaderElectionID = randomHex() 49 | } 50 | 51 | func randomHex() string { 52 | bytes := make([]byte, 20) 53 | if _, err := rand.Read(bytes); err != nil { 54 | return "123.salesforce.com" 55 | } 56 | 57 | return hex.EncodeToString(bytes) 58 | } 59 | -------------------------------------------------------------------------------- /cmd/create.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package cmd 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | "time" 13 | 14 | "craft/utils" 15 | log "github.com/sirupsen/logrus" 16 | "github.com/spf13/cobra" 17 | "github.com/spf13/viper" 18 | ) 19 | 20 | var ( 21 | dockerPush bool 22 | ) 23 | 24 | func absAPIPath() { 25 | var err error 26 | apiFile, err = filepath.Abs(apiFile) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | log.Debug("apiFile: ", apiFile) 31 | } 32 | func absResourcePath() { 33 | var err error 34 | resourceFile, err = filepath.Abs(resourceFile) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | log.Debug("resourceFile: ", resourceFile) 39 | 40 | } 41 | func absPodPath() { 42 | var err error 43 | podDockerFile, err = filepath.Abs(podDockerFile) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | log.Debug("podDockerFile: ", podDockerFile) 48 | } 49 | func absPath() { 50 | absAPIPath() 51 | absResourcePath() 52 | absPodPath() 53 | } 54 | 55 | func createCmd() *cobra.Command { 56 | cmd := &cobra.Command{ 57 | Use: "create", 58 | Aliases: []string{"c"}, 59 | Short: "create operator in $GOPATH/src", 60 | Long: `create operator in $GOPATH/src`, 61 | Run: func(cmd *cobra.Command, args []string) { 62 | absPath() 63 | apiFileObj.loadApi(apiFile) 64 | pwd, err := os.Getwd() 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | total := time.Now() 69 | fmt.Println("Creating operator in $GOPATH/src") 70 | start := time.Now() 71 | cmdString := fmt.Sprintf("craft build code -c %s -r %s", apiFile, resourceFile) 72 | utils.CmdExec(cmdString, pwd) 73 | elapsed:= time.Since(start) 74 | fmt.Println("Created operator in $GOPATH/src in", elapsed.Round(time.Second).String()) 75 | 76 | fmt.Println("Building operator.yaml for deployment") 77 | start = time.Now() 78 | cmdString = fmt.Sprintf("craft build deploy -c %s -r %s", apiFile, resourceFile) 79 | utils.CmdExec(cmdString, pwd) 80 | elapsed = time.Since(start) 81 | fmt.Println("Built operator.yaml for deployment in", elapsed.Round(time.Second).String()) 82 | 83 | fmt.Println("Building operator and resource docker images") 84 | start = time.Now() 85 | cmdString = fmt.Sprintf("craft build image -b -c %s --podDockerFile %s", apiFile, podDockerFile) 86 | utils.CmdExec(cmdString, pwd) 87 | elapsed = time.Since(start) 88 | fmt.Println("Built operator and resource docker images in", elapsed.Round(time.Second).String()) 89 | 90 | if dockerPush { 91 | fmt.Println("Pushing operator image to docker") 92 | start = time.Now() 93 | cmdString = fmt.Sprintf("docker push %s", apiFileObj.OperatorImage) 94 | utils.CmdExec(cmdString, pwd) 95 | elapsed = time.Since(start) 96 | fmt.Println("Pushed operator image to docker in", elapsed.Round(time.Second).String()) 97 | 98 | fmt.Println("Pushing resource image to docker") 99 | start = time.Now() 100 | cmdString = fmt.Sprintf("docker push %s", apiFileObj.Image) 101 | utils.CmdExec(cmdString, pwd) 102 | elapsed = time.Since(start) 103 | fmt.Println("Pushed resource image to docker in", elapsed.Round(time.Second).String()) 104 | } 105 | totalTime := time.Since(total) 106 | fmt.Println("Total time required for completion is", totalTime.Round(time.Second).String()) 107 | }, 108 | } 109 | 110 | cmd.PersistentFlags().StringVarP(&apiFile, "controllerFile", "c", "", "controller file with group, resource and other info") 111 | cmd.PersistentFlags().StringVarP(&resourceFile, "resourceFile", "r", "", "resourcefile with properties of resource") 112 | cmd.PersistentFlags().StringVarP(&podDockerFile, "podDockerFile", "P", "", "pod Dockerfile") 113 | cmd.PersistentFlags().BoolVarP(&dockerPush, "push", "p", false, "If set to true, pushes images to docker") 114 | cmd.MarkPersistentFlagRequired("controllerFile") 115 | cmd.MarkPersistentFlagRequired("resourceFile") 116 | cmd.MarkPersistentFlagRequired("podDockerFile") 117 | // cmd.MarkPersistentFlagRequired("environ") 118 | 119 | if err := viper.BindPFlag("controllerFile", cmd.Flags().Lookup("controllerFile")); err != nil { 120 | log.Fatal(err) 121 | } 122 | if err := viper.BindPFlag("resourceFile", cmd.Flags().Lookup("resourceFile")); err != nil { 123 | log.Fatal(err) 124 | } 125 | if err := viper.BindPFlag("podDockerFile", cmd.Flags().Lookup("podDockerFile")); err != nil { 126 | log.Fatal(err) 127 | } 128 | return cmd 129 | } -------------------------------------------------------------------------------- /cmd/image.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package cmd 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | pathLib "path" 12 | "path/filepath" 13 | 14 | "craft/utils" 15 | log "github.com/sirupsen/logrus" 16 | "github.com/spf13/cobra" 17 | "github.com/spf13/viper" 18 | ) 19 | 20 | var ( 21 | podDockerFile string 22 | operatorDockerFile string 23 | buildImage bool 24 | ) 25 | 26 | func absOperatorPath() { 27 | var err error 28 | operatorDockerFile, err = filepath.Abs(operatorDockerFile) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | log.Debug("operatorDockerFile: ", operatorDockerFile) 33 | } 34 | func logDockerBuild(cmd, dir, imageName string) { 35 | log.Infof("cd %s", dir) 36 | log.Infof("Run command for building %s : %s", imageName, cmd) 37 | } 38 | func imageBuildCmd() *cobra.Command { 39 | cmd := &cobra.Command{ 40 | Use: "image", 41 | Aliases: []string{"i"}, 42 | Short: "build images", 43 | Long: `build images`, 44 | Run: func(cmd *cobra.Command, args []string) { 45 | absAPIPath() 46 | apiFileObj.loadApi(apiFile) 47 | newOperatorPath := pathLib.Join(goSrc, apiFileObj.Repo) 48 | 49 | var podDockerDir, operatorDockerDir string 50 | 51 | if podDockerFile == "" { 52 | podDockerDir = filepath.Dir(apiFile) 53 | podDockerFile = pathLib.Join(podDockerDir, "Dockerfile") 54 | } else { 55 | absPodPath() 56 | podDockerDir = filepath.Dir(podDockerFile) 57 | } 58 | if operatorDockerFile == "" { 59 | operatorDockerDir = newOperatorPath 60 | operatorDockerFile = pathLib.Join(operatorDockerDir, "Dockerfile") 61 | } else { 62 | absOperatorPath() 63 | operatorDockerDir = filepath.Dir(operatorDockerFile) 64 | } 65 | 66 | if !utils.FileExists(operatorDockerFile) { 67 | log.Fatal("specify -o for operator docker file.") 68 | } 69 | if !utils.FileExists(podDockerFile) { 70 | log.Fatal("specify -p for pod docker file.") 71 | } 72 | 73 | if !buildImage { 74 | log.Info(` 75 | 76 | 77 | Pass -b for craft to build image (but since building image takes long time you won't see any output during that) 78 | At end we specified instructions for building images yourself 79 | 80 | 81 | `) 82 | } 83 | imageCmd := fmt.Sprintf("docker build -t %s -f %s .", 84 | apiFileObj.OperatorImage, 85 | operatorDockerFile) 86 | if buildImage { 87 | utils.CmdExec(imageCmd, operatorDockerDir) 88 | } else { 89 | logDockerBuild(imageCmd, operatorDockerDir, "operator") 90 | } 91 | 92 | imageCmd = fmt.Sprintf("docker build --build-arg vault_token=%s -t %s -f %s .", 93 | os.Getenv("VAULT_TOKEN"), 94 | apiFileObj.Image, 95 | podDockerFile) 96 | if buildImage { 97 | utils.CmdExec(imageCmd, podDockerDir) 98 | } else { 99 | logDockerBuild(imageCmd, podDockerDir, "pod") 100 | } 101 | }, 102 | } 103 | cmd.PersistentFlags().StringVarP(&podDockerFile, "podDockerFile", "p", "", "pod Dockerfile") 104 | cmd.PersistentFlags().StringVarP(&operatorDockerFile, "operatorDockerFile", "o", "", "pod Dockerfile") 105 | cmd.PersistentFlags().BoolVarP(&buildImage, "build", "b", false, "pod Dockerfile") 106 | 107 | if err := viper.BindPFlag("podDockerFile", cmd.Flags().Lookup("podDockerFile")); err != nil { 108 | log.Fatal(err) 109 | } 110 | if err := viper.BindPFlag("operatorDockerFile", cmd.Flags().Lookup("operatorDockerFile")); err != nil { 111 | log.Fatal(err) 112 | } 113 | return cmd 114 | } 115 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package cmd 7 | 8 | import ( 9 | log "github.com/sirupsen/logrus" 10 | "os" 11 | "path/filepath" 12 | 13 | "craft/utils" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func initCmd() *cobra.Command { 18 | cmd := &cobra.Command{ 19 | Use: "init", 20 | Aliases: []string{"i"}, 21 | Short: "init folder with craft sample declaration", 22 | Long: `init folder with craft sample declaration`, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | folderName, err := os.Getwd() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | folder, err := filepath.Abs(folderName) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | cmdString := "svn checkout https://github.com/salesforce/craft/trunk/init" 34 | utils.MinCmdExec(cmdString, folder) 35 | log.Infof("Created sample controller and resource files") 36 | }, 37 | } 38 | 39 | return cmd 40 | } 41 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package cmd 7 | 8 | import ( 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | log "github.com/sirupsen/logrus" 14 | 15 | "craft/cmd/version" 16 | "craft/utils" 17 | "github.com/spf13/cobra" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | var ( 22 | craftInstallPath = "/usr/local/craft" 23 | goSrc = os.ExpandEnv("$GOPATH/src") 24 | craftDir string 25 | debug bool 26 | ) 27 | 28 | func setCraftDir() { 29 | var err error 30 | craftDir, err = filepath.Abs(craftDir) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | log.Info("CraftDir: ", craftDir) 36 | } 37 | func initLoad() { 38 | setCraftDir() 39 | utils.CheckGoPath() 40 | log.SetOutput(os.Stdout) 41 | if debug { 42 | log.SetLevel(log.DebugLevel) 43 | } else { 44 | log.SetLevel(log.InfoLevel) 45 | } 46 | } 47 | func init() { 48 | cobra.OnInitialize(initLoad) 49 | cobra.EnableCommandSorting = false 50 | 51 | rootCmd.SilenceUsage = true 52 | 53 | // Register subcommands 54 | rootCmd.AddCommand( 55 | version.VersionCmd(), 56 | createCmd(), 57 | initCmd(), 58 | validateCmd(), 59 | buildCmd(), 60 | updateCmd(), 61 | ) 62 | rootCmd.PersistentFlags().StringVarP(&craftDir, "craftDir", "C", craftInstallPath, "craft dir") 63 | rootCmd.PersistentFlags().MarkHidden("craftDir") 64 | rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "debug") 65 | 66 | if err := viper.BindPFlag("craftDir", rootCmd.Flags().Lookup("craftDir")); err != nil { 67 | log.Fatal(err) 68 | } 69 | if err := viper.BindPFlag("debug", rootCmd.Flags().Lookup("debug")); err != nil { 70 | log.Fatal(err) 71 | } 72 | } 73 | 74 | var rootCmd = &cobra.Command{ 75 | Use: "craft", 76 | Short: "Craft is tool for creating generic operator", 77 | Long: strings.TrimSpace(``), 78 | } 79 | 80 | func Execute() { 81 | if err := rootCmd.Execute(); err != nil { 82 | os.Exit(1) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /cmd/update.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package cmd 7 | 8 | import ( 9 | "craft/utils" 10 | "fmt" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | "os" 14 | "runtime" 15 | ) 16 | 17 | func updateCmd() *cobra.Command { 18 | cmd := &cobra.Command{ 19 | Use: "update", 20 | Aliases: []string{"u"}, 21 | Short: "update existing version of craft to latest version", 22 | Long: `update existing version of craft to latest version`, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | path, err := os.Getwd() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | goos := runtime.GOOS 29 | cmdStr := fmt.Sprintf("wget https://github.com/salesforce/craft/releases/latest/download/craft_%s.tar.gz", goos) 30 | utils.MinCmdExec(cmdStr, path) 31 | cmdStr = fmt.Sprintf("sudo tar -xzf craft_%s.tar.gz -C /usr/local/craft", goos) 32 | utils.CmdExec(cmdStr, path) 33 | cmdStr = fmt.Sprintf("sudo rm -rf craft_%s.tar.gz", goos) 34 | utils.CmdExec(cmdStr, path) 35 | }, 36 | } 37 | 38 | return cmd 39 | } -------------------------------------------------------------------------------- /cmd/validate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package cmd 7 | 8 | import ( 9 | "log" 10 | "path/filepath" 11 | 12 | "craft/utils" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | ) 16 | 17 | var crdPath string 18 | 19 | func validateCmd() *cobra.Command { 20 | cmd := &cobra.Command{ 21 | Use: "validate", 22 | Aliases: []string{"v"}, 23 | Short: "validate crd", 24 | Long: `validate crd`, 25 | Run: func(cmd *cobra.Command, args []string) { 26 | crdPath, err := filepath.Abs(crdPath) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | utils.Validate(crdPath) 31 | }, 32 | } 33 | cmd.PersistentFlags().StringVarP(&crdPath, "crdPath", "v", "", "path to crd definition") 34 | if err := viper.BindPFlag("crdPath", rootCmd.Flags().Lookup("crdPath")); err != nil { 35 | log.Fatal(err) 36 | } 37 | cmd.MarkPersistentFlagRequired("crdPath") 38 | 39 | return cmd 40 | } 41 | -------------------------------------------------------------------------------- /cmd/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package version 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | 12 | "craft/cmd/base" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func VersionCmd() *cobra.Command { 17 | cmd := &cobra.Command{ 18 | Use: "version", 19 | Short: "Displays the version of the current build of craft", 20 | Long: `Displays the version of the current build of craft`, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | fmt.Printf(base.VersionStr, 23 | base.Info["version"], 24 | base.Info["revision"], 25 | base.Info["branch"], 26 | base.Info["buildUser"], 27 | base.Info["buildDate"], 28 | base.Info["goVersion"]) 29 | os.Exit(0) 30 | }, 31 | } 32 | 33 | return cmd 34 | } -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Running mdBook 2 | Docs are served using mdBook. If you want to test changes to the docs locally, follow these directions: 3 | 4 | * Follow the instructions at https://github.com/rust-lang-nursery/mdBook#installation to install mdBook. 5 | * Run mdbook serve 6 | * Visit http://localhost:3000 7 | 8 | # Steps to Deploy 9 | Currently we are using Github Pages to deploy our docs. To send changes to the docs 10 | * Run `mdbook build -d /tmp/craft-doc` 11 | * `mv /tmp/craft-doc .` 12 | * Send the Pull Request to `gh-pages` 13 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["salesforce"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "CRAFT" 7 | -------------------------------------------------------------------------------- /docs/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/craft/387ce5ec20500071f5d3cd3180f8f1b6a3c45ade/docs/src/.DS_Store -------------------------------------------------------------------------------- /docs/src/README.md: -------------------------------------------------------------------------------- 1 | # Introduction to CRAFT 2 | 3 | CRAFT (Custom Resource Abstraction and Fabrication Tool) declares Kubernetes operators in a robust, idempotent, and generic way for any resource. 4 | 5 | Creating a Kubernetes operator requires domain knowledge of abstraction and expertise in Kubernetes and Golang. With CRAFT you can create operators without a dependent layer and in the language of your choice! 6 | 7 | Declare your custom resource in JSON files and let CRAFT generate all the files needed to deploy your operator into the cluster. CRAFT Wordpress operator generates 571 lines of code for you, a task that otherwise takes a few months to complete. 8 | 9 | Reduce your workload by automating resource reconciliation with CRAFT to ensure your resource stays in its desired state. CRAFT also performs schema validations on your custom resource while creating the operator. Through automated reconciliation and schema validations, CRAFT achieves the objectives listed in the [Kubernetes Architecture Design Proposal](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/declarative-application-management.md#bespoke-application-deployment). 10 | 11 | ## Advantages of using Craft 12 | 13 | 1. **Easy onboarding** : Create an operator in your language of choice. 14 | 2. **Segregation of duties** : Developers can work in the docker file while the Site Reliability or DevOps engineer can declaratively configure the operator. 15 | 3. **Versioning** : Work on a different version of the operator or resource than your users. 16 | 4. **Validations** : Get schema and input validation feedback before runtime. 17 | 5. **Controlled reconciliation** : Define resource reconciliation frequency to lower your maintenance workload. 18 | 19 | ## Built with 20 | 21 | CRAFT is built with open source projects Kubebuilder and Operatify: 22 | * **Kubebuilder** : CRAFT augments the operator skeleton generated by Kubebuilder with custom resource definitions and controller capabilities. 23 | * **Operatify** : CRAFT leverages Operatify’s automated reconciliation capabilities. 24 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Introduction](README.md) 4 | * [Quickstart](quick_setup/README.md) 5 | * [Wordpress Operator Tutorial](tutorial/README.md) 6 | * [Step 1: Create the Custom Resource Files and Docker Image](tutorial/step1.md) 7 | * [Controller.json](tutorial/controller_file.md) 8 | * [Resource.json](tutorial/resource_file.md) 9 | * [CRUD operations Exit Codes](tutorial/docker_exit_codes.md) 10 | * [Resource DockerFile](tutorial/resource_dockerfile.md) 11 | * [Step 2: Create an Operator](tutorial/step2.md) 12 | * [Namespace.yaml](tutorial/namespace_file.md) 13 | * [Operator.yaml](tutorial/operator_file.md) 14 | * [Step3: Deploy Operator onto the cluster](tutorial/deploy_operator.md) 15 | * [Operator source code](operator_source_code/README.md) 16 | * [Controllers and Reconcilers](operator_source_code/controller_reconciler.md) 17 | * [Craft CLI](craft_cli.md) 18 | -------------------------------------------------------------------------------- /docs/src/craft_cli.md: -------------------------------------------------------------------------------- 1 | # Commands of CRAFT 2 | 3 | ## craft init 4 | Usage : 5 | ``` 6 | craft init 7 | ``` 8 | Initialises a new project with sample controller.json and resource.json 9 | 10 | ## craft build 11 | Has 3 sub commands, code, deploy and image. 12 | 13 | ### build code 14 | Usage: 15 | ``` 16 | craft build code -c "controller.json" -r "resource.json 17 | ``` 18 | Creates Operator's code in $GOPATH/src 19 | 20 | ### build deploy 21 | Usage: 22 | ``` 23 | craft build deploy -c "controller.json" -r "resource.json 24 | ``` 25 | Builds operator.yaml for deployment onto cluster. 26 | 27 | ### build image 28 | Usage: 29 | ``` 30 | craft build image -b -c "controller.json" --podDockerFile "dockerFile" 31 | ``` 32 | Builds operator and resource docker images. 33 | 34 | ## craft create 35 | Usage : 36 | ``` 37 | craft create -c "controller.json" -r "resource.json --podDockerFile "dockerFile" -p 38 | ``` 39 | ![Craft Create Flow](img/craft_create_flow.png) 40 | 41 | ## validate 42 | Usage: 43 | ``` 44 | craft validate -v "operator.yaml" 45 | ``` 46 | Validates operator.yaml to see if the CRD and operator manifest that we created is Valid for k8s's Structural schema & API 47 | 48 | ### craft version 49 | Usage : 50 | ``` 51 | craft version 52 | ``` 53 | Displays the information about craft, namely version, revision, build user, build date & time, go version. 54 | -------------------------------------------------------------------------------- /docs/src/img/craft_create_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/craft/387ce5ec20500071f5d3cd3180f8f1b6a3c45ade/docs/src/img/craft_create_flow.png -------------------------------------------------------------------------------- /docs/src/operator_source_code/README.md: -------------------------------------------------------------------------------- 1 | # Where is the Operator Code? 2 | 3 | The source code for the operator is stored in the `$GOPATH/src` path. 4 | ``` 5 | $ cd $GOPATH/src/wordpress 6 | $ ls 7 | 8 | ``` 9 | 10 | The folder contains all the files required to run an operator like the configurations, API files, controllers, reconciliation files, main files, etc. All these files contain information about the operator and it's runtime characteristics, such as the CRUD logic, reconciliation frequency, etc. These files can be classified in four sections: 11 | 12 | 1. Build infrastructure. 13 | 14 | 2. Launch Configuration. 15 | 16 | 3. Entry Point 17 | 18 | 4. Controllers and Reconciler. 19 | 20 | CRAFT creates these files when you create an operator. This saves you a few weeks of effort to write and connect your operator. 21 | 22 | ## Build Infrastructure 23 | 24 | These files are used to build the operator: 25 | - go.mod : A Go module for the project that lists all the dependencies. 26 | - Makefile : File makes targets for building and deploying the controller and reconciler. 27 | - PROJECT : Kubebuilder metadata for scaffolding new components. 28 | - DockerFile : File with instructions on running the operator. Specifies the docker entrypoint for the operator. 29 | 30 | 31 | ## Launch Configuration 32 | 33 | 34 | The launch configurations are in the config/ directory. It holds the CustomResourceDefinitions, RBAC configuration, and WebhookConfigurations. Each folder in config/ contains a refactored part of the launch configuration. 35 | - `config/default` contains a Kustomize base for launching the controller with standard configurations. 36 | - `config/manager` can be used to launch controllers as pods in the cluster 37 | - `config/rbac` contains permissions required to run your controllers under their own service account. 38 | 39 | 40 | ## Entry point 41 | 42 | The basic entry point for the operator is in the main.go file. This file can be used to: 43 | - Set up flags for metrics. 44 | - Initialise all the controller parameters, including the reconciliation frequency and the parameters we received from controller.json and resource.json 45 | - Instantiate a manager to keep track of all the running controllers and clients to the API server. 46 | - Run a manager that runs all of the controllers and keeps track of them until it receives a shutdown signal when it stops all of the controllers. 47 | 48 | -------------------------------------------------------------------------------- /docs/src/operator_source_code/controller_reconciler.md: -------------------------------------------------------------------------------- 1 | # Controllers and Reconciler 2 | 3 | A controller is the core of Kubernetes and operators. A controller ensures that, for any given object, the actual state of the world (both the cluster state, and potentially external state like running containers for Kubelet or loadbalancers for a cloud provider) matches the desired state in the object. Each controller focuses on one root API type but may interact with other API types. 4 | 5 | A reconciler tracks changes to the root API type, checks and updates changes in the operator image at controller-runtime. It runs an operation and returns an exit code, and through this process it checks if reconciliation is needed and determines the frequency for reconciliation. Based on the exit code, the next operation is added to the controller queue. 6 | 7 | These are the 14 exit codes that a reconciler can return: 8 | 9 | ``` 10 | ## ExitCode to state mapping 11 | 201: "Succeeded", // create or update 12 | 202: "AwaitingVerification", // create or update 13 | 203: "Error", // create or update 14 | 211: "Ready", // verify 15 | 212: "InProgress", // verify 16 | 213: "Error", // verify 17 | 214: "Missing", // verify 18 | 215: "UpdateRequired", // verify 19 | 216: "RecreateRequired", // verify 20 | 217: "Deleting", // verify 21 | 221: "Succeeded", // delete 22 | 222: "InProgress", // delete 23 | 223: "Error", // delete 24 | 224: "Missing", // delete 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /docs/src/quick_setup/README.md: -------------------------------------------------------------------------------- 1 | # Quick Setup 2 | ## Prerequisites 3 | 4 | * [Go](https://golang.org/dl/) version v1.13+ 5 | * [Docker](https://docs.docker.com/get-docker/) version 17.03+ 6 | * [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) version v1.11.3+ 7 | * [Kustomize](https://kubernetes-sigs.github.io/kustomize/installation/) version v3.1.0+ 8 | * [Kubebuilder](https://book.kubebuilder.io/quick-start.html#installation) version v2.0.0+ 9 | 10 | ## Installation 11 | 12 | ``` 13 | # dowload latest craft binary from releases and extract 14 | curl -L https://github.com/salesforce/craft/releases/download/v0.1.0-alpha/craft_${os}.tar.gz | tar -xz -C /tmp/ 15 | 16 | # move to a path that you can use for long term 17 | sudo mv /tmp/craft /usr/local/craft 18 | export PATH=$PATH:/usr/local/craft/bin 19 | ``` 20 | Instead of having to add to PATH everytime you open a new terminal, we can add our path to PATH permanently. 21 | ``` 22 | $ sudo vim /etc/paths 23 | ``` 24 | Add the line "/usr/local/craft/bin" at the end of the file and save the file. 25 | ## Create a CRAFT Application 26 | 27 | From the command line, **cd** into a directory where you'd like to store your CRAFT application and run this command: 28 | 29 | ``` 30 | craft init 31 | ``` 32 | 33 | This will initiate a CRAFT application in your current directory and create the following skeleton files: 34 | 35 | - controller.json: This file holds Custom Resource Definition (CRD) information like group, domain, operator image, and reconciliation frequency. 36 | - resource.json: This file contains the schema information for validating inputs while creating the CRD. 37 | 38 | ## Next Steps 39 | 40 | Follow the Wordpress operator tutorial to understand how to use CRAFT to create and deploy an operator into a cluster. This deep-dive tutorial demonstrates the entire scope and scale of a CRAFT application. 41 | -------------------------------------------------------------------------------- /docs/src/tutorial/README.md: -------------------------------------------------------------------------------- 1 | # Tutorial : Wordpress Operator 2 | Unlike most tutorials who start with some really contrived setup, or some toy application that gets the basics across, this tutorial will take you through the full extent of the CRAFT application and how it is useful. We start off simple and in the end, build something pretty full-featured and meaningful, namely, an operator for the Wordpress application. 3 | 4 | The job of the Wordpress operator is to host the Wordpress application and perform operations given by the user on the cluster. It reconciles regularly, checking for updates in the resource and therefore, can be termed as level-triggered. 5 | 6 | We will see how the controller.json and resource.json required to run the wordpress operator have been developed. Then, we'll see how to use CRAFT to create the operator and deploy it onto the cluster. 7 | 8 | The config files required to create the operator are already present in `example/wordpress-operator` 9 | 10 | Let's go ahead and see how we have created our files for the wordpress application to understand and generalise this process for any application. First, we start with the controller.json file in the next section. 11 | 12 | -------------------------------------------------------------------------------- /docs/src/tutorial/controller_file.md: -------------------------------------------------------------------------------- 1 | # Controller.json 2 | 3 | Custom Resource Definition (CRD) information like the domain, group, image, repository, etc. are stored in the controller.json file. This skeleton file for controller.json is created when you [create a CRAFT application in quickstart](quick_setup/README.md): 4 | 5 | "group": "", 6 | "resource": "", 7 | "repo": "", 8 | "domain": "", 9 | "namespace": "", 10 | "version": "", 11 | "operator_image": "", 12 | "image": "", 13 | "imagePullSecrets": "", 14 | "imagePullPolicy": "", 15 | "cpu_limit": "", 16 | "memory_limit": "", 17 | "vault_addr": "", 18 | "runOnce": "", 19 | "reconcileFreq": "" 20 | 21 | This table explains the controller.json attributes: 22 | 23 | | Attribute | Description | | | | 24 | |------------------|--------------------------------------------------------------------------------------------------------------------------------|---|---|---| 25 | | group | See the Kubernetes API Concepts page for more information. | | | | 26 | | resource | | | | | 27 | | namespace | | | | | 28 | | version | | | | | 29 | | repo | The repo where you want to store the operator template. | | | | 30 | | domain | The domain web address for this project. | | | | 31 | | operator_image | The docker registry files used to push operator image into docker. | | | | 32 | | image | The docker registry files used to push resource image into docker. | | | | 33 | | imagePullSecrets | Restricted data to be stored in the operator like access, permissions, etc. | | | | 34 | | imagePullPolicy | Method of updating images. Default pull policy is IfNotPresent causes Kubelet to skip pulling an image if one already exists. | | | | 35 | | cpu_limit | CPU limit allocated to the operator created. | | | | 36 | | memory_limit | Memory limit allocated to the operator created. | | | | 37 | | vault_addr | Address of the vault. | | | | 38 | | runOnce | If set to 0 reconciliation stops. If set to 1, reconciliation runs according to the specified frequency. | | | | 39 | | reconcileFreq | Frequency interval (in minutes) between two reconciliations. | | | | 40 | 41 | 42 | Here’s an example of a controller.json file for the Wordpress operator: 43 | 44 | ``` 45 | { 46 | "group": "wordpress", 47 | "resource": "WordpressAPI", 48 | "repo": "wordpress", 49 | "domain": "salesforce.com", 50 | "namespace": "default", 51 | "version": "v1", 52 | "operator_image": "ops0-artifactrepo1-0-prd.data.sfdc.net/cco/wordpress-operator", 53 | "image": "ops0-artifactrepo1-0-prd.data.sfdc.net/cco/wordpress:latest", 54 | "imagePullSecrets": "registrycredential", 55 | "imagePullPolicy": "IfNotPresent", 56 | "cpu_limit": "500m", 57 | "memory_limit": "200Mi", 58 | "vault_addr": "http://10.215.194.253:8200" 59 | } 60 | ``` 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/src/tutorial/deploy_operator.md: -------------------------------------------------------------------------------- 1 | # Deploy operator onto the cluster 2 | 3 | In the previous step, we created an operator. Now, we deploy it to the cluster. This involves deploying the namespace and the operator files to the cluster. Create the namespace where you want to deploy the operator with the namespace.yaml file we created in step 2 using this command: 4 | 5 | ``` 6 | kubectl apply -f config/deploy/namespace.yaml 7 | ``` 8 | When the command runs successfully, it returns `namespace/craft created`. You can check the namespace created by running this command: 9 | 10 | ``` 11 | kubectl get namespace 12 | ``` 13 | This should display all the existing namespaces, out of which `craft` is one. Install the operator onto the cluster with this command: 14 | 15 | ``` 16 | kubectl apply -f config/deploy/operator.yaml 17 | ``` 18 | 19 | This will create the required pod in the cluster. We can verify the creation by running: 20 | 21 | ``` 22 | kubectl get pods 23 | ``` 24 | 25 | This returns the wordpress pod along with the other pods running on your machine: 26 | ``` 27 | NAME READY STATUS RESTARTS AGE 28 | wordpress-controller-manager-8844cf545-gn5rt 1/2 Running 0 11s 29 | ``` 30 | 31 | Great, your pod is running! You are ready to deploy the resource. 32 | 33 | --- 34 | ***NOTE*** 35 | 36 | If your pod’s status is `ContainerCreating`, run the command again in a few seconds and the status should change to running. 37 | 38 | --- 39 | 40 | Deploy the resource onto the cluster using the wordpress-dev-withoutvault YAML file created by CRAFT. 41 | 42 | ``` 43 | kubectl -n craft apply -f config/deploy/wordpress-dev-withoutvault.yaml 44 | ``` 45 | 46 | This deploys the wordpress resource onto the cluster. To verify, run: 47 | ``` 48 | kubectl -n craft port-forward svc/wordpress-dev 9090:80 49 | ``` 50 | 51 | Open `http://localhost:9090` on the browser and you’ll see the Wordpress application. You can check the logs to see that reconciliation is running as configured. To see that, we can use [stern](https://github.com/wercker/stern). 52 | 53 | ``` 54 | stern -n craft . 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/src/tutorial/docker_exit_codes.md: -------------------------------------------------------------------------------- 1 | # CRUD operations in CRAFT operators 2 | 3 | Define CRUD (Create, Read, Update, and Delete) operations for your operator. This diagram illustrates the flow for the 14 possible outputs of a CRUD operation: 4 | 5 | ![operator-full-lifecycle](https://www.stephenzoio.com/images/operator-full-lifecycle.png) 6 | *Credits: Stephen Zoio & his project operatify* 7 | 8 | With CRAFT, you can account for all the14 cases by using 14 unique docker exit codes. Use these docker exit codes to check the result of the operation and route the path accordingly. The 14 exit codes that CRAFT provides are: 9 | 10 | ``` 11 | 201: "Succeeded", // create or update 12 | 202: "AwaitingVerification", // create or update 13 | 203: "Error", // create or update 14 | 211: "Ready", // verify 15 | 212: "InProgress", // verify 16 | 213: "Error", // verify 17 | 214: "Missing", // verify 18 | 215: "UpdateRequired", // verify 19 | 216: "RecreateRequired", // verify 20 | 217: "Deleting", // verify 21 | 221: "Succeeded", // delete 22 | 222: "InProgress", // delete 23 | 223: "Error", // delete 24 | 224: "Missing", // delete 25 | ``` 26 | 27 | The 14 exit codes are correspondingly mapped to the 14 possibilities that arise from the operations. 28 | 29 | While writing our CRUD operation definitions, we use the exit codes to specify output. For example, in the `wordpress_manager.py`, we can see the CUD operations: 30 | 31 | ``` 32 | def create_wordpress(spec): 33 | /* 34 | .. 35 | */ 36 | if result.returncode == 0: 37 | init_wordpress(spec) 38 | sys.exit(201) 39 | else: 40 | sys.exit(203) 41 | 42 | def delete_wordpress(spec): 43 | /* 44 | .. 45 | */ 46 | if result.returncode == 0: 47 | sys.exit(221) 48 | else: 49 | sys.exit(223) 50 | 51 | def update_wordpress(spec): 52 | /* 53 | .. 54 | */ 55 | if result.returncode == 0: 56 | sys.exit(201) 57 | else: 58 | sys.exit(203) 59 | 60 | def verify_wordpress(spec): 61 | /* 62 | .. 63 | */ 64 | if result.returncode == 0: 65 | result = subprocess.run(['kubectl', 'get', 'deployment', 'wordpress-' + spec['instance'], '-o', 'yaml'], stdout=subprocess.PIPE) 66 | deployment_out = yaml.safe_load(result.stdout) 67 | if deployment_out['spec']['replicas'] != spec['replicas']: 68 | print("Change in replicas.") 69 | sys.exit(214) 70 | sys.exit(211) 71 | else: 72 | sys.exit(214) 73 | ``` 74 | 75 | We map the corresponding output possibility to the exit code. 76 | 77 | Now that we have defined our CRUD operations, let's create our operator. 78 | -------------------------------------------------------------------------------- /docs/src/tutorial/namespace_file.md: -------------------------------------------------------------------------------- 1 | # Namespace.yaml 2 | 3 | The `namespace.yaml` file contains information about the namespace of the cluster in which you want to deploy the operator. The namespace.yaml for the Wordpress operator looks like this: 4 | 5 | 6 | ``` 7 | apiVersion: v1 8 | kind: Namespace 9 | metadata: 10 | labels: 11 | control-plane: controller-manager 12 | name: craft 13 | ``` 14 | 15 | -------------------------------------------------------------------------------- /docs/src/tutorial/operator_file.md: -------------------------------------------------------------------------------- 1 | The `operator.yaml` file contains all the metadata required to deploy the operator into the cluster. It is the backbone of the operator as it contains the schema validations, the specification properties, the API version rules, etc. This file is automatically populated by CRAFT based on the information provided in the controller.json and the resource.json file. The `operator.yaml` that CRAFT generates for the Wordpress operator can be found at `examples/wordpress-operator/config/deploy/operator.yaml`. 2 | 3 | --- 4 | ***Note*** 5 | 6 | Our operator's default user currently holds the minimum *rbac* required to run the operator and only the operator itself. If you need any more control of the *rbac* add those permissions in the `operator.yaml`. 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /docs/src/tutorial/resource_dockerfile.md: -------------------------------------------------------------------------------- 1 | # Resource DockerFile - How does it help? 2 | 3 | For our Wordpress operator, the resource Dockerfile looks like this: 4 | 5 | ``` 6 | FROM centos/python-36-centos7:latest 7 | 8 | USER root 9 | 10 | RUN pip install --upgrade pip 11 | RUN python3 -m pip install pyyaml 12 | RUN python3 -m pip install jinja2 13 | RUN python3 -m pip install hvac 14 | 15 | ADD kubectl kubectl 16 | RUN chmod +x ./kubectl 17 | RUN mv ./kubectl /usr/local/bin/kubectl 18 | 19 | ARG vault_token=dummy 20 | ENV VAULT_TOKEN=${vault_token} 21 | 22 | ADD templates templates 23 | 24 | ADD initwordpress.sh . 25 | RUN chmod 755 initwordpress.sh 26 | ADD wordpress_manager.py . 27 | RUN chmod 755 wordpress_manager.py 28 | 29 | RUN find / -perm /6000 -type f -exec chmod a-s {} \; || true 30 | 31 | ENTRYPOINT ["python3", "wordpress_manager.py"] 32 | ``` 33 | The above resource Dockerfile for the Wordpress operator has Docker run two files/scripts: 34 | 35 | 1. `initwordpress.sh` : This file contains instructions to initialize the wordpress resource and install the required components. 36 | 2. `wordpress_manager.py` : This file contains CRUD operations defined by the Wordpress resource. These operations don’t return the usual output, but return exit codes. 37 | 38 | --- 39 | **FAQ** 40 | 41 | **Q. The methods in wordpress_manager.py take input_data/spec as input parameter. Where is it being passed in?** 42 | 43 | **A.** Both the input_data/spec and action_type are passed as args at pod spec. Operator deploys a pod for each execution of action_type (create/update/delete/verify) 44 | 45 | **Q. The kubectl binary is being packaged within the Operator's container image. How/where is it being given the credentials to access the API server?** 46 | 47 | **A.** We can also use the `client-go` library instead of `kubectl` binary in Wordpress example. `kubectl` also uses the default service account available credentials in the pod itself. Just like client-go kubectl default to that service account and we are not specifically configuring any other credentials. [K8s Docs](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/#accessing-the-api-from-a-pod) 48 | 49 | --- 50 | -------------------------------------------------------------------------------- /docs/src/tutorial/resource_file.md: -------------------------------------------------------------------------------- 1 | # Resource.json 2 | 3 | Resource.json contains the schema information for validating inputs while creating the Custom Resource Definition (CRD). The resource.json has a list of properties and required attributes for a certain operator. 4 | 5 | "type": "object" 6 | "properties": {}, 7 | "required": [], 8 | 9 | The properties field contains the field name, data type and the data patterns. The required field contains the data names which are mandatory for the operator to be created. 10 | 11 | Note: Resource.json remains the same for an operator regardless of the developer. controller.json for an operator may look different for each developer. 12 | 13 | Populate resource.json by identifying required fields and their properties from the resource code. Here’s an example resource.json file for the Wordpress operator: 14 | 15 | ``` 16 | "type": "object", 17 | "properties": { 18 | "bootstrap_email": { 19 | "pattern": "^(.*)$", 20 | "type": "string" 21 | }, 22 | "bootstrap_password": { 23 | "pattern": "^(.*)$", 24 | "type": "string" 25 | }, 26 | "bootstrap_title": { 27 | "pattern": "^(.*)$", 28 | "type": "string" 29 | }, 30 | "bootstrap_url": { 31 | "pattern": "^(.*)$", 32 | "type": "string" 33 | }, 34 | "bootstrap_user": { 35 | "pattern": "^(.*)$", 36 | "type": "string" 37 | }, 38 | "db_password": { 39 | "pattern": "^(.*)$", 40 | "type": "string" 41 | }, 42 | "dbVolumeMount": { 43 | "pattern": "^(.*)$", 44 | "type": "string" 45 | }, 46 | "host": { 47 | "pattern": "^(.*)$", 48 | "type": "string" 49 | }, 50 | "instance": { 51 | "enum": [ 52 | "prod", 53 | "dev" 54 | ], 55 | "type": "string" 56 | }, 57 | "name": { 58 | "pattern": "^(.*)$", 59 | "type": "string" 60 | }, 61 | "replicas": { 62 | "format": "int64", 63 | "type": "integer", 64 | "minimum": 1, 65 | "maximum": 5 66 | }, 67 | "user": { 68 | "pattern": "^(.*)$", 69 | "type": "string" 70 | }, 71 | "wordpressVolumeMount": { 72 | "pattern": "^(.*)$", 73 | "type": "string" 74 | } 75 | }, 76 | "required": [ 77 | "bootstrap_email", 78 | "bootstrap_password", 79 | "bootstrap_title", 80 | "bootstrap_url", 81 | "bootstrap_user", 82 | "db_password", 83 | "dbVolumeMount", 84 | "host", 85 | "instance", 86 | "name", 87 | "replicas", 88 | "user", 89 | "wordpressVolumeMount" 90 | ] 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/src/tutorial/step1.md: -------------------------------------------------------------------------------- 1 | The following files are involved in creating a Custom Resource: 2 | 3 | - controller.json  4 | - resource.json 5 | - Resource DockerFile -------------------------------------------------------------------------------- /docs/src/tutorial/step2.md: -------------------------------------------------------------------------------- 1 | # Creating the Operator using CRAFT 2 | 3 | Now that we have created controller.json, resource.json and the DockerFile, let's create the Operator using CRAFT. 4 | 5 | First, let us check whether CRAFT is working in our machine. 6 | ``` 7 | $ craft version 8 | ``` 9 | This should display the version and other info regarding CRAFT. 10 | 11 | --- 12 | ***!TIP*** 13 | 14 | If this gives an error saying "$GOPATH is not set", then set GOPATH to the location where you've installed Go. 15 | 16 | --- 17 | 18 | Now that we have verified that CRAFT is working properly, creating the Operator with CRAFT is a fairly straight forward process: 19 | ``` 20 | craft create -c config/controller.json -r config/resource.json \ 21 | --podDockerFile resource/DockerFile -p 22 | ``` 23 | --- 24 | ***NOTE*** 25 | 26 | If the execution in the terminal stops at a certain point, do not assume that it has hanged. The command takes a little while to execute, so give it some time. 27 | 28 | --- 29 | This will create the Operator template in $GOPATH/src, build operator.yaml for deployment, build and push Docker images for operator and resource. We shall see what these are individually in the next section. 30 | -------------------------------------------------------------------------------- /examples/wordpress-operator/README.md: -------------------------------------------------------------------------------- 1 | # CRAFTing Wordpress 2 | 3 | ### Creating Operator 4 | #### Configure Private Registry 5 | Update `config/controller.json` with your environment variables like docker registry, docker secret name, etc. 6 | ``` 7 | "imagePullSecrets": "docker-registry-credentials", 8 | ``` 9 | #### Generate Wordpress operator 10 | ``` 11 | craft create -c config/controller.json -r config/resource.json --podDockerFile resource/DockerFile -p 12 | ``` 13 | 14 | ### Deploy operator to cluster 15 | #### Install operator 16 | ``` 17 | kubectl apply -f config/deploy/operator.yaml 18 | ``` 19 | #### Create docker private registry secret 20 | ``` 21 | kubectl create secret docker-registry docker-registry-credentials --docker-server= \ 22 | --docker-username= --docker-password= --namespace=craft 23 | ``` 24 | 25 | ### Deploy wordpress resource 26 | ``` 27 | kubectl apply -f config/deploy/operator.yaml 28 | ``` 29 | #### Verfication of the wordpress operator 30 | 31 | For testing we are going to access the wordpress service using kubectl proxy 32 | ``` 33 | kubectl -n craft port-forward svc/wordpress-dev 9090:80 34 | ``` 35 | To access the wordpress application, open `http://localhost:9090` 36 | 37 | 38 | ### Cleanup: Delete resource, operator and namespace. 39 | ``` 40 | kubectl delete -f conf/deploy/operator.yaml 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /examples/wordpress-operator/config/controller.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": "wordpress", 3 | "resource": "WordpressAPI", 4 | "repo": "wordpress", 5 | "domain": "example.com", 6 | "namespace": "craft", 7 | "version": "v1", 8 | "operator_image": "craftsf/wordpress-operator:v0.0.1", 9 | "image": "craftsf/wordpress:v0.0.1", 10 | "imagePullSecrets": "", 11 | "imagePullPolicy": "IfNotPresent", 12 | "cpu_limit": "500m", 13 | "memory_limit": "200Mi" 14 | } 15 | -------------------------------------------------------------------------------- /examples/wordpress-operator/config/deploy/operator.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | control-plane: controller-manager 7 | name: craft 8 | spec: {} 9 | status: {} 10 | --- 11 | apiVersion: apiextensions.k8s.io/v1beta1 12 | kind: CustomResourceDefinition 13 | metadata: 14 | annotations: 15 | controller-gen.kubebuilder.io/version: v0.2.5 16 | creationTimestamp: null 17 | name: wordpressapis.wordpress.example.com 18 | spec: 19 | group: wordpress.example.com 20 | names: 21 | kind: WordpressAPI 22 | listKind: WordpressAPIList 23 | plural: wordpressapis 24 | singular: wordpressapi 25 | scope: Namespaced 26 | validation: 27 | openAPIV3Schema: 28 | description: WordpressAPI is the Schema for the WordpressAPIs API 29 | properties: 30 | apiVersion: 31 | description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 32 | type: string 33 | kind: 34 | description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 35 | type: string 36 | metadata: 37 | type: object 38 | spec: 39 | properties: 40 | bootstrap_email: 41 | pattern: ^(.*)$ 42 | type: string 43 | bootstrap_password: 44 | pattern: ^(.*)$ 45 | type: string 46 | bootstrap_title: 47 | pattern: ^(.*)$ 48 | type: string 49 | bootstrap_url: 50 | pattern: ^(.*)$ 51 | type: string 52 | bootstrap_user: 53 | pattern: ^(.*)$ 54 | type: string 55 | db_password: 56 | pattern: ^(.*)$ 57 | type: string 58 | dbVolumeMount: 59 | pattern: ^(.*)$ 60 | type: string 61 | host: 62 | pattern: ^(.*)$ 63 | type: string 64 | instance: 65 | enum: 66 | - prod 67 | - dev 68 | type: string 69 | name: 70 | pattern: ^(.*)$ 71 | type: string 72 | replicas: 73 | format: int64 74 | maximum: 5 75 | minimum: 1 76 | type: integer 77 | user: 78 | pattern: ^(.*)$ 79 | type: string 80 | wordpressVolumeMount: 81 | pattern: ^(.*)$ 82 | type: string 83 | required: 84 | - bootstrap_email 85 | - bootstrap_password 86 | - bootstrap_title 87 | - bootstrap_url 88 | - bootstrap_user 89 | - db_password 90 | - dbVolumeMount 91 | - host 92 | - instance 93 | - name 94 | - replicas 95 | - user 96 | - wordpressVolumeMount 97 | type: object 98 | status: 99 | description: WordpressAPIStatus defines the observed state of WordpressAPI 100 | properties: 101 | message: 102 | type: string 103 | pod: 104 | properties: 105 | name: 106 | type: string 107 | namespace: 108 | type: string 109 | type: 110 | type: string 111 | type: object 112 | state: 113 | type: string 114 | statusPayload: 115 | description: 'INSERT ADDITIONAL STATUS FIELD - define observed state of cluster Important: Run "make" to regenerate code after modifying this file' 116 | type: string 117 | terminated: 118 | description: ContainerStateTerminated is a terminated state of a container. 119 | properties: 120 | containerID: 121 | description: Container's ID in the format 'docker://' 122 | type: string 123 | exitCode: 124 | description: Exit status from the last termination of the container 125 | format: int32 126 | type: integer 127 | finishedAt: 128 | description: Time at which the container last terminated 129 | format: date-time 130 | type: string 131 | message: 132 | description: Message regarding the last termination of the container 133 | type: string 134 | reason: 135 | description: (brief) reason from the last termination of the container 136 | type: string 137 | signal: 138 | description: Signal from the last termination of the container 139 | format: int32 140 | type: integer 141 | startedAt: 142 | description: Time at which previous execution of the container started 143 | format: date-time 144 | type: string 145 | required: 146 | - exitCode 147 | type: object 148 | type: object 149 | type: object 150 | version: v1 151 | versions: 152 | - name: v1 153 | served: true 154 | storage: true 155 | status: 156 | acceptedNames: 157 | kind: "" 158 | plural: "" 159 | conditions: [] 160 | storedVersions: [] 161 | --- 162 | apiVersion: rbac.authorization.k8s.io/v1 163 | kind: Role 164 | metadata: 165 | creationTimestamp: null 166 | name: wordpress-leader-election-role 167 | namespace: craft 168 | rules: 169 | - apiGroups: 170 | - "" 171 | resources: 172 | - configmaps 173 | verbs: 174 | - get 175 | - list 176 | - watch 177 | - create 178 | - update 179 | - patch 180 | - delete 181 | - apiGroups: 182 | - "" 183 | resources: 184 | - configmaps/status 185 | verbs: 186 | - get 187 | - update 188 | - patch 189 | - apiGroups: 190 | - "" 191 | resources: 192 | - events 193 | verbs: 194 | - create 195 | --- 196 | apiVersion: rbac.authorization.k8s.io/v1 197 | kind: ClusterRole 198 | metadata: 199 | creationTimestamp: null 200 | name: wordpress-manager-role 201 | rules: 202 | - apiGroups: 203 | - "" 204 | resources: 205 | - events 206 | - pods 207 | verbs: 208 | - create 209 | - delete 210 | - get 211 | - list 212 | - patch 213 | - update 214 | - watch 215 | - apiGroups: 216 | - apps 217 | - extensions 218 | resources: 219 | - deployments 220 | verbs: 221 | - create 222 | - delete 223 | - get 224 | - list 225 | - patch 226 | - update 227 | - watch 228 | - apiGroups: 229 | - wordpress.example.com 230 | resources: 231 | - wordpressapis 232 | verbs: 233 | - create 234 | - delete 235 | - get 236 | - list 237 | - patch 238 | - update 239 | - watch 240 | - apiGroups: 241 | - wordpress.example.com 242 | resources: 243 | - wordpressapis/status 244 | verbs: 245 | - get 246 | - patch 247 | - update 248 | --- 249 | apiVersion: rbac.authorization.k8s.io/v1 250 | kind: ClusterRole 251 | metadata: 252 | creationTimestamp: null 253 | name: wordpress-proxy-role 254 | rules: 255 | - apiGroups: 256 | - authentication.k8s.io 257 | resources: 258 | - tokenreviews 259 | verbs: 260 | - create 261 | - apiGroups: 262 | - authorization.k8s.io 263 | resources: 264 | - subjectaccessreviews 265 | verbs: 266 | - create 267 | --- 268 | apiVersion: rbac.authorization.k8s.io/v1beta1 269 | kind: ClusterRole 270 | metadata: 271 | creationTimestamp: null 272 | name: wordpress-metrics-reader 273 | rules: 274 | - nonResourceURLs: 275 | - /metrics 276 | verbs: 277 | - get 278 | --- 279 | apiVersion: rbac.authorization.k8s.io/v1 280 | kind: RoleBinding 281 | metadata: 282 | creationTimestamp: null 283 | name: wordpress-leader-election-rolebinding 284 | namespace: craft 285 | roleRef: 286 | apiGroup: rbac.authorization.k8s.io 287 | kind: Role 288 | name: wordpress-leader-election-role 289 | subjects: 290 | - kind: ServiceAccount 291 | name: default 292 | namespace: craft 293 | --- 294 | apiVersion: rbac.authorization.k8s.io/v1 295 | kind: ClusterRoleBinding 296 | metadata: 297 | creationTimestamp: null 298 | name: wordpress-manager-rolebinding 299 | roleRef: 300 | apiGroup: rbac.authorization.k8s.io 301 | kind: ClusterRole 302 | name: wordpress-manager-role 303 | subjects: 304 | - kind: ServiceAccount 305 | name: default 306 | namespace: craft 307 | --- 308 | apiVersion: rbac.authorization.k8s.io/v1 309 | kind: ClusterRoleBinding 310 | metadata: 311 | creationTimestamp: null 312 | name: wordpress-proxy-rolebinding 313 | roleRef: 314 | apiGroup: rbac.authorization.k8s.io 315 | kind: ClusterRole 316 | name: wordpress-proxy-role 317 | subjects: 318 | - kind: ServiceAccount 319 | name: default 320 | namespace: craft 321 | --- 322 | apiVersion: v1 323 | kind: Service 324 | metadata: 325 | creationTimestamp: null 326 | labels: 327 | control-plane: controller-manager 328 | name: wordpress-controller-manager-metrics-service 329 | namespace: craft 330 | spec: 331 | ports: 332 | - name: https 333 | port: 8443 334 | targetPort: https 335 | selector: 336 | control-plane: controller-manager 337 | status: 338 | loadBalancer: {} 339 | --- 340 | apiVersion: apps/v1 341 | kind: Deployment 342 | metadata: 343 | creationTimestamp: null 344 | labels: 345 | control-plane: controller-manager 346 | name: wordpress-controller-manager 347 | namespace: craft 348 | spec: 349 | replicas: 1 350 | selector: 351 | matchLabels: 352 | control-plane: controller-manager 353 | strategy: {} 354 | template: 355 | metadata: 356 | creationTimestamp: null 357 | labels: 358 | control-plane: controller-manager 359 | spec: 360 | containers: 361 | - args: 362 | - --metrics-addr=127.0.0.1:8080 363 | - --enable-leader-election 364 | command: 365 | - /manager 366 | image: craftsf/wordpress-operator:v0.0.1 367 | name: manager 368 | resources: 369 | limits: 370 | cpu: 100m 371 | memory: 30Mi 372 | requests: 373 | cpu: 100m 374 | memory: 20Mi 375 | - args: 376 | - --secure-listen-address=0.0.0.0:8443 377 | - --upstream=http://127.0.0.1:8080/ 378 | - --logtostderr=true 379 | - --v=10 380 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 381 | name: kube-rbac-proxy 382 | ports: 383 | - containerPort: 8443 384 | name: https 385 | resources: {} 386 | imagePullSecrets: 387 | - {} 388 | terminationGracePeriodSeconds: 10 389 | status: {} 390 | -------------------------------------------------------------------------------- /examples/wordpress-operator/config/resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "bootstrap_email": { 5 | "pattern": "^(.*)$", 6 | "type": "string" 7 | }, 8 | "bootstrap_password": { 9 | "pattern": "^(.*)$", 10 | "type": "string" 11 | }, 12 | "bootstrap_title": { 13 | "pattern": "^(.*)$", 14 | "type": "string" 15 | }, 16 | "bootstrap_url": { 17 | "pattern": "^(.*)$", 18 | "type": "string" 19 | }, 20 | "bootstrap_user": { 21 | "pattern": "^(.*)$", 22 | "type": "string" 23 | }, 24 | "db_password": { 25 | "pattern": "^(.*)$", 26 | "type": "string" 27 | }, 28 | "dbVolumeMount": { 29 | "pattern": "^(.*)$", 30 | "type": "string" 31 | }, 32 | "host": { 33 | "pattern": "^(.*)$", 34 | "type": "string" 35 | }, 36 | "instance": { 37 | "enum": [ 38 | "prod", 39 | "dev" 40 | ], 41 | "type": "string" 42 | }, 43 | "name": { 44 | "pattern": "^(.*)$", 45 | "type": "string" 46 | }, 47 | "replicas": { 48 | "format": "int64", 49 | "type": "integer", 50 | "minimum": 1, 51 | "maximum": 5 52 | }, 53 | "user": { 54 | "pattern": "^(.*)$", 55 | "type": "string" 56 | }, 57 | "wordpressVolumeMount": { 58 | "pattern": "^(.*)$", 59 | "type": "string" 60 | } 61 | }, 62 | "required": [ 63 | "bootstrap_email", 64 | "bootstrap_password", 65 | "bootstrap_title", 66 | "bootstrap_url", 67 | "bootstrap_user", 68 | "db_password", 69 | "dbVolumeMount", 70 | "host", 71 | "instance", 72 | "name", 73 | "replicas", 74 | "user", 75 | "wordpressVolumeMount" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /examples/wordpress-operator/resource/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos/python-36-centos7:latest 2 | 3 | USER root 4 | 5 | RUN pip install --upgrade pip 6 | RUN python3 -m pip install pyyaml 7 | RUN python3 -m pip install jinja2 8 | RUN python3 -m pip install hvac 9 | 10 | ADD kubectl kubectl 11 | RUN chmod +x ./kubectl 12 | RUN mv ./kubectl /usr/local/bin/kubectl 13 | 14 | ARG vault_token=dummy 15 | ENV VAULT_TOKEN=${vault_token} 16 | 17 | ADD templates templates 18 | 19 | ADD initwordpress.sh . 20 | RUN chmod 755 initwordpress.sh 21 | ADD wordpress_manager.py . 22 | RUN chmod 755 wordpress_manager.py 23 | 24 | RUN find / -perm /6000 -type f -exec chmod a-s {} \; || true 25 | 26 | ENTRYPOINT ["python3", "wordpress_manager.py"] 27 | -------------------------------------------------------------------------------- /examples/wordpress-operator/resource/initwordpress.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar 4 | chmod +x wp-cli.phar 5 | mv wp-cli.phar /usr/local/bin/wp 6 | 7 | wp --info 8 | 9 | 10 | wp core is-installed --allow-root 11 | status=$? 12 | echo $status 13 | path="wordpress-$5" 14 | url=$6 15 | if [ "$status" -eq "1" ]; then 16 | wp core install --url=$url --title=$1 --admin_user=$2 --admin_password=$3 --admin_email=$4 --allow-root 17 | 18 | 19 | cat <> /var/www/html/wp-config.php 20 | 21 | \$host = '$url'; 22 | define('WP_HOME', \$host); 23 | define('WP_SITEURL', \$host); 24 | PHP 25 | 26 | fi 27 | 28 | -------------------------------------------------------------------------------- /examples/wordpress-operator/resource/kubectl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/craft/387ce5ec20500071f5d3cd3180f8f1b6a3c45ade/examples/wordpress-operator/resource/kubectl -------------------------------------------------------------------------------- /examples/wordpress-operator/resource/templates/kustomization.yaml: -------------------------------------------------------------------------------- 1 | secretGenerator: 2 | - name: mysql-pass-{{ instance }} 3 | literals: 4 | - password={{ db_password }} 5 | resources: 6 | - mysql-deployment.yaml 7 | - wordpress-deployment.yaml 8 | -------------------------------------------------------------------------------- /examples/wordpress-operator/resource/templates/mysql-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: wordpress-mysql-{{ instance }} 5 | labels: 6 | Confidentiality: Internal 7 | app: wordpress-{{ instance }} 8 | spec: 9 | ports: 10 | - port: 3306 11 | selector: 12 | app: wordpress-{{ instance }} 13 | tier: mysql 14 | clusterIP: None 15 | --- 16 | apiVersion: v1 17 | kind: PersistentVolumeClaim 18 | metadata: 19 | name: mysql-pv-claim-{{ instance }} 20 | labels: 21 | Confidentiality: Internal 22 | app: wordpress-{{ instance }} 23 | spec: 24 | accessModes: 25 | - ReadWriteOnce 26 | resources: 27 | requests: 28 | storage: 20Gi 29 | --- 30 | apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 31 | kind: Deployment 32 | metadata: 33 | name: wordpress-mysql-{{ instance }} 34 | labels: 35 | Confidentiality: Internal 36 | app: wordpress-{{ instance }} 37 | spec: 38 | selector: 39 | matchLabels: 40 | app: wordpress-{{ instance }} 41 | tier: mysql 42 | strategy: 43 | type: Recreate 44 | template: 45 | metadata: 46 | labels: 47 | app: wordpress-{{ instance }} 48 | tier: mysql 49 | spec: 50 | containers: 51 | - image: mysql:5.6 52 | name: mysql-{{ instance }} 53 | env: 54 | - name: MYSQL_ROOT_PASSWORD 55 | valueFrom: 56 | secretKeyRef: 57 | name: mysql-pass-{{ instance }} 58 | key: password 59 | ports: 60 | - containerPort: 3306 61 | name: mysql-{{ instance }} 62 | volumeMounts: 63 | - name: mysql-persistent-storage-{{ instance }} 64 | mountPath: {{ dbVolumeMount }} 65 | volumes: 66 | - name: mysql-persistent-storage-{{ instance }} 67 | persistentVolumeClaim: 68 | claimName: mysql-pv-claim-{{ instance }} 69 | -------------------------------------------------------------------------------- /examples/wordpress-operator/resource/templates/wordpress-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: wordpress-{{ instance }} 5 | labels: 6 | Confidentiality: Internal 7 | app: wordpress-{{ instance }} 8 | spec: 9 | ports: 10 | - port: 80 11 | selector: 12 | app: wordpress-{{ instance }} 13 | tier: frontend 14 | type: ClusterIP 15 | --- 16 | apiVersion: v1 17 | kind: PersistentVolumeClaim 18 | metadata: 19 | name: wp-pv-claim-{{ instance }} 20 | labels: 21 | Confidentiality: Internal 22 | app: wordpress-{{ instance }} 23 | spec: 24 | accessModes: 25 | - ReadWriteOnce 26 | resources: 27 | requests: 28 | storage: 20Gi 29 | --- 30 | apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 31 | kind: Deployment 32 | metadata: 33 | name: wordpress-{{ instance }} 34 | labels: 35 | Confidentiality: Internal 36 | app: wordpress-{{ instance }} 37 | spec: 38 | replicas: {{ replicas }} 39 | selector: 40 | matchLabels: 41 | app: wordpress-{{ instance }} 42 | tier: frontend 43 | strategy: 44 | type: Recreate 45 | template: 46 | metadata: 47 | labels: 48 | app: wordpress-{{ instance }} 49 | tier: frontend 50 | spec: 51 | containers: 52 | - image: wordpress:4.8-apache 53 | name: wordpress 54 | env: 55 | - name: WORDPRESS_DB_HOST 56 | value: wordpress-mysql-{{ instance }} 57 | - name: WORDPRESS_DB_PASSWORD 58 | valueFrom: 59 | secretKeyRef: 60 | name: mysql-pass-{{ instance }} 61 | key: password 62 | ports: 63 | - containerPort: 80 64 | name: wordpress-{{ instance }} 65 | volumeMounts: 66 | - name: wordpress-persistent-storage-{{ instance }} 67 | mountPath: {{ wordpressVolumeMount }} 68 | - name: initwordpress-{{ instance }} 69 | mountPath: /scripts 70 | volumes: 71 | - name: wordpress-persistent-storage-{{ instance }} 72 | persistentVolumeClaim: 73 | claimName: wp-pv-claim-{{ instance }} 74 | - name: initwordpress-{{ instance }} 75 | configMap: 76 | name: initwordpress-{{ instance }} 77 | defaultMode: 0744 78 | -------------------------------------------------------------------------------- /examples/wordpress-operator/resource/wordpress_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import ast 4 | import sys 5 | import yaml 6 | import json 7 | import argparse 8 | import subprocess 9 | from jinja2 import Environment, FileSystemLoader 10 | import glob 11 | import io 12 | import time 13 | import hvac 14 | import base64 15 | 16 | base_dir = os.getcwd() 17 | 18 | # def get_secret(secret, path, key): 19 | 20 | # env = os.environ['environment'] 21 | # if os.environ['environment'] == "falcon": 22 | 23 | # vault_addr = os.environ['vault_addr'] 24 | 25 | # awsclient = hvac.Client(url=vault_addr, verify=False) 26 | 27 | # namespace = os.environ['namespace'] 28 | # user= os.environ['vault_kubernetes_secret'] 29 | # process = subprocess.Popen(['kubectl', '-n', namespace, 'get', 'secret', user, "-o", "json"], stdout=subprocess.PIPE) 30 | # process.wait() 31 | # data, err = process.communicate() 32 | # if process.returncode is 0: 33 | # json_data = json.loads(data.decode('utf-8')) 34 | 35 | # decode_data = base64.b64decode(json_data['data']['credentials.json']).decode("utf-8") 36 | # cred_data = json.loads(decode_data) 37 | 38 | # access_key = cred_data["AccessKeyId"] 39 | # secret_key = cred_data["SecretAccessKey"] 40 | # session_token = cred_data["SessionToken"] 41 | 42 | # header_value = "api.vault.secrets." + os.environ['iac_instance'] + ".aws.sfdc.cl" 43 | # role = "kv_" + os.environ['vault_rootpath'] + "-ro" 44 | # vault_aws = awsclient.auth.aws.iam_login(access_key=access_key, 45 | # secret_key=secret_key,session_token=session_token, 46 | # header_value=header_value, 47 | # role=role) 48 | 49 | # client = hvac.Client( 50 | # url=vault_addr, 51 | # token=vault_aws['auth']['client_token'], 52 | # verify=False 53 | # ) 54 | 55 | 56 | # return str(client.read(secret + '/data/' + path)['data'][key]) 57 | # else: 58 | # client = hvac.Client( 59 | # url=os.environ['vault_addr'], 60 | # token=os.environ['VAULT_TOKEN'] 61 | # ) 62 | 63 | # return str(client.read(secret + '/' + path)['data'][key]) 64 | 65 | # def decrypt_passwords(spec): 66 | 67 | # spec_t = {} 68 | # for data in spec: 69 | # if str(spec[data]).startswith('vault:'): 70 | # t_vault = spec[data].split(":") 71 | # spec_t[data] = get_secret(t_vault[1],t_vault[2],t_vault[3]) 72 | # print(spec_t[data]) 73 | # else: 74 | # spec_t[data] = spec[data] 75 | 76 | # return spec_t 77 | 78 | def init_wordpress(spec): 79 | 80 | pods_list = [] 81 | while True: 82 | retry = False 83 | time.sleep(10) 84 | result = subprocess.Popen(['kubectl', 'get', 'pods', '-l=app=wordpress-' + spec['instance']], stdout=subprocess.PIPE) 85 | for line in io.TextIOWrapper(result.stdout, encoding="utf-8"): 86 | if 'wordpress-' + spec['instance'] in line: 87 | if 'Running' not in line: 88 | time.sleep(5) 89 | retry = True 90 | else: 91 | pods_list.append(line.split(' ')[0]) 92 | 93 | if not retry: 94 | break 95 | 96 | time.sleep(20) 97 | for pod in pods_list: 98 | result = subprocess.run(['kubectl', 'exec', '-it', pod, '--', 'sh', '-x', '/scripts/initwordpress.sh', spec['bootstrap_title'], spec['bootstrap_user'], spec['bootstrap_password'], spec['bootstrap_email'] , spec['instance'], spec['bootstrap_url']], stdout=subprocess.PIPE) 99 | print(result) 100 | 101 | def create_init(spec): 102 | result = subprocess.run(['kubectl', 'create', 'configmap', 'initwordpress-' + spec['instance'], '--from-file=initwordpress.sh'], stdout=subprocess.PIPE) 103 | print(result) 104 | return 105 | 106 | def delete_init(spec): 107 | result = subprocess.run(['kubectl', 'delete', 'configmap', 'initwordpress-' + spec['instance']], stdout=subprocess.PIPE) 108 | print(result) 109 | return 110 | 111 | def create_wordpress(spec): 112 | 113 | file_loader = FileSystemLoader('') 114 | env = Environment(loader=file_loader) 115 | env.trim_blocks = True 116 | env.lstrip_blocks = True 117 | env.rstrip_blocks = True 118 | 119 | print("Create") 120 | # spec = decrypt_passwords(spec) 121 | create_init(spec) 122 | try: 123 | os.mkdir( spec['instance'] ) 124 | except: 125 | print("Dir exists") 126 | for template in ['templates/*.yaml']: 127 | for filename in glob.iglob(template, recursive=True): 128 | print(filename) 129 | template = env.get_template( filename ) 130 | new_filename = template 131 | head, new_filename = os.path.split(filename) 132 | _ = head 133 | 134 | output = template.render(instance=spec['instance'], replicas=spec['replicas'], db_password=spec['db_password'], dbVolumeMount=spec['dbVolumeMount'], wordpressVolumeMount=spec['wordpressVolumeMount']) 135 | newpath = os.path.join( spec['instance'] + '/' + new_filename) 136 | with open(newpath, 'w') as f: 137 | f.write(output) 138 | 139 | result = subprocess.run(['kubectl', 'apply', '-k', spec['instance']], stdout=subprocess.PIPE) 140 | print(result) 141 | print("***********************") 142 | 143 | if result.returncode == 0: 144 | init_wordpress(spec) 145 | sys.exit(201) 146 | else: 147 | sys.exit(203) 148 | 149 | 150 | def delete_wordpress(spec): 151 | 152 | print("Delete") 153 | file_loader = FileSystemLoader('') 154 | env = Environment(loader=file_loader) 155 | env.trim_blocks = True 156 | env.lstrip_blocks = True 157 | env.rstrip_blocks = True 158 | 159 | # spec = decrypt_passwords(spec) 160 | delete_init(spec) 161 | try: 162 | os.mkdir( spec['instance'] ) 163 | except: 164 | print("Dir exists") 165 | for template in ['templates/*.yaml']: 166 | for filename in glob.iglob(template, recursive=True): 167 | print(filename) 168 | template = env.get_template( filename ) 169 | new_filename = template 170 | head, new_filename = os.path.split(filename) 171 | _ = head 172 | 173 | output = template.render(instance=spec['instance'], replicas=spec['replicas'], db_password=spec['db_password'], dbVolumeMount=spec['dbVolumeMount'], wordpressVolumeMount=spec['wordpressVolumeMount']) 174 | newpath = os.path.join( spec['instance'] + '/' + new_filename) 175 | with open(newpath, 'w') as f: 176 | f.write(output) 177 | 178 | result = subprocess.run(['kubectl', 'delete', '-k', spec['instance']], stdout=subprocess.PIPE) 179 | print(result) 180 | print("***********************") 181 | if result.returncode == 0: 182 | sys.exit(221) 183 | else: 184 | sys.exit(223) 185 | 186 | def update_wordpress(spec): 187 | 188 | print("Update") 189 | file_loader = FileSystemLoader('') 190 | env = Environment(loader=file_loader) 191 | env.trim_blocks = True 192 | env.lstrip_blocks = True 193 | env.rstrip_blocks = True 194 | # spec = decrypt_passwords(spec) 195 | try: 196 | os.mkdir( spec['instance'] ) 197 | except: 198 | print("Dir exists") 199 | for template in ['templates/*.yaml']: 200 | for filename in glob.iglob(template, recursive=True): 201 | print(filename) 202 | template = env.get_template( filename ) 203 | new_filename = template 204 | head, new_filename = os.path.split(filename) 205 | _ = head 206 | 207 | output = template.render(instance=spec['instance'], replicas=spec['replicas'], db_password=spec['db_password'], dbVolumeMount=spec['dbVolumeMount'], wordpressVolumeMount=spec['wordpressVolumeMount']) 208 | 209 | newpath = os.path.join( spec['instance'] + '/' + new_filename) 210 | with open(newpath, 'w') as f: 211 | f.write(output) 212 | 213 | result = subprocess.run(['kubectl', 'delete', '-k', spec['instance']], stdout=subprocess.PIPE) 214 | print(result) 215 | if result.returncode == 0: 216 | result = subprocess.run(['kubectl', 'apply', '-k', spec['instance']], stdout=subprocess.PIPE) 217 | print(result) 218 | if result.returncode == 0: 219 | sys.exit(201) 220 | else: 221 | sys.exit(203) 222 | 223 | def verify_wordpress(spec): 224 | 225 | print("Verify") 226 | result = subprocess.run(['kubectl', 'get', 'deployment', 'wordpress-' + spec['instance'] ], stdout=subprocess.PIPE) 227 | print(result) 228 | print("***********************") 229 | if result.returncode == 0: 230 | result = subprocess.run(['kubectl', 'get', 'deployment', 'wordpress-' + spec['instance'], '-o', 'yaml'], stdout=subprocess.PIPE) 231 | deployment_out = yaml.safe_load(result.stdout) 232 | if deployment_out['spec']['replicas'] != spec['replicas']: 233 | print("Change in replicas.") 234 | sys.exit(214) 235 | sys.exit(211) 236 | else: 237 | sys.exit(214) 238 | 239 | 240 | def convert_to_dict(tmp_config): 241 | t_config=tmp_config 242 | if not isinstance(t_config,dict): 243 | try: 244 | t_config = t_config.replace('\'','\"') 245 | t_config=json.loads(t_config) 246 | except: 247 | t_config=ast.literal_eval(tmp_config) 248 | return t_config 249 | return tmp_config 250 | 251 | if __name__ == "__main__": 252 | 253 | parser = argparse.ArgumentParser(description='Initiating clone_autobuild') 254 | parser.add_argument('--type', type=str, required=True) 255 | parser.add_argument('--spec', type=str, required=True) 256 | 257 | args = parser.parse_args() 258 | action_type = args.type 259 | # print(args.spec) 260 | input_data = convert_to_dict(args.spec) 261 | 262 | if action_type == 'create': 263 | create_wordpress(input_data) 264 | if action_type == 'verify': 265 | verify_wordpress(input_data) 266 | if action_type == 'update': 267 | update_wordpress(input_data) 268 | if action_type == 'delete': 269 | delete_wordpress(input_data) 270 | 271 | -------------------------------------------------------------------------------- /examples/wordpress-operator/sample/wordpress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: wordpress.example.com/v1 2 | kind: WordpressAPI 3 | metadata: 4 | name: wordpress-dev 5 | namespace: craft 6 | spec: 7 | # Add fields here 8 | instance: dev 9 | replicas: 1 10 | host: mysite-mysql 11 | user: user 12 | db_password: testpassfordemo 13 | name: wordpress 14 | dbVolumeMount: /var/www/mysql-dev 15 | wordpressVolumeMount: /var/www/html 16 | bootstrap_title: "DEV" 17 | bootstrap_user: "admin" 18 | bootstrap_password: testpassfordemo 19 | bootstrap_url: "http://localhost:9090" 20 | bootstrap_email: "info@example.com" 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module craft 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/briandowns/spinner v1.12.0 7 | github.com/fsnotify/fsnotify v1.4.9 // indirect 8 | github.com/golang/protobuf v1.4.2 // indirect 9 | github.com/hashicorp/golang-lru v0.5.4 // indirect 10 | github.com/imdario/mergo v0.3.9 // indirect 11 | github.com/json-iterator/go v1.1.10 // indirect 12 | github.com/prometheus/procfs v0.0.11 // indirect 13 | github.com/sirupsen/logrus v1.4.2 14 | github.com/spf13/cobra v1.0.0 15 | github.com/spf13/viper v1.6.2 16 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 // indirect 17 | golang.org/x/text v0.3.3 // indirect 18 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 19 | gopkg.in/yaml.v2 v2.3.0 20 | k8s.io/api v0.18.6 21 | k8s.io/apiextensions-apiserver v0.18.6 22 | k8s.io/apimachinery v0.18.6 23 | k8s.io/cli-runtime v0.18.3 24 | k8s.io/client-go v0.18.6 25 | k8s.io/utils v0.0.0-20200603063816-c1c6865ac451 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /images/Declarative_Operator.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/craft/387ce5ec20500071f5d3cd3180f8f1b6a3c45ade/images/Declarative_Operator.jpeg -------------------------------------------------------------------------------- /images/craft_quick_start.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/craft/387ce5ec20500071f5d3cd3180f8f1b6a3c45ade/images/craft_quick_start.gif -------------------------------------------------------------------------------- /init/controller.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": "", 3 | "resource": "", 4 | "repo": "", 5 | "domain": "", 6 | "namespace": "", 7 | "version": "", 8 | "operator_image": "", 9 | "image": "", 10 | "imagePullSecrets": "", 11 | "imagePullPolicy": "", 12 | "cpu_limit": "", 13 | "memory_limit": "", 14 | "vault_addr": "", 15 | "runOnce": "", 16 | "reconcileFreq": "" 17 | } -------------------------------------------------------------------------------- /init/resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": {}, 3 | "required": [], 4 | "type": "object" 5 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package main 7 | 8 | import "craft/cmd" 9 | 10 | func main() { 11 | cmd.Execute() 12 | } 13 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build craft 4 | 5 | set -e 6 | 7 | BUILD_USER=${BUILD_USER:-"${USER}@${HOSTNAME}"} 8 | BUILD_DATE=${BUILD_DATE:-$( date +%Y%m%d-%H:%M:%S )} 9 | VERBOSE=${VERBOSE:-} 10 | 11 | repo_path="craft/cmd" 12 | 13 | version=`git tag --points-at HEAD` 14 | revision=$( git rev-parse --short HEAD 2> /dev/null || echo 'unknown' ) 15 | branch=$( git rev-parse --abbrev-ref HEAD 2> /dev/null || echo 'unknown' ) 16 | go_version=$( go version | sed -e 's/^[^0-9.]*\([0-9.]*\).*/\1/' ) 17 | 18 | 19 | # go 1.4 requires ldflags format to be "-X key value", not "-X key=value" 20 | # ldseparator here is for cross compatibility 21 | ldseparator="=" 22 | if [ "${go_version:0:3}" = "1.4" ]; then 23 | ldseparator=" " 24 | fi 25 | 26 | ldflags=" 27 | -X ${repo_path}/base.Version${ldseparator}${version} 28 | -X ${repo_path}/base.Revision${ldseparator}${revision} 29 | -X ${repo_path}/base.Branch${ldseparator}${branch} 30 | -X ${repo_path}/base.BuildUser${ldseparator}${BUILD_USER} 31 | -X ${repo_path}/base.BuildDate${ldseparator}${BUILD_DATE} 32 | -X ${repo_path}/base.GoVersion${ldseparator}${go_version}" 33 | 34 | echo ">>> Building craft..." 35 | 36 | if [ -n "$VERBOSE" ]; then 37 | echo "Building with -ldflags $ldflags" 38 | fi 39 | 40 | GOBIN=$PWD go build -ldflags "${ldflags}" -o bin/craft main.go 41 | GOBIN=$PWD env GOOS=linux GOARCH=amd64 go build -ldflags "${ldflags}" -o bin/craft_linux main.go 42 | GOBIN=$PWD env GOOS=darwin GOARCH=amd64 go build -ldflags "${ldflags}" -o bin/craft_darwin main.go 43 | GOBIN=$PWD env GOOS=windows GOARCH=amd64 go build -ldflags "${ldflags}" -o bin/craft_windows main.go 44 | exit 0 45 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | OKGREEN='\033[92m' 6 | FAIL='\033[91m' 7 | WARN='\033[93m' 8 | INFO='\033[94m' 9 | ENDC='\033[0m' 10 | function PassPrint() { 11 | echo "$OKGREEN $1 $ENDC" 12 | } 13 | function FailPrint() { 14 | echo "$FAIL $1 $ENDC" 15 | } 16 | function WarnPrint() { 17 | echo "$WARN $1 $ENDC" 18 | } 19 | function InfoPrint() { 20 | echo "$INFO $1 $ENDC" 21 | } 22 | 23 | 24 | OS=$(uname -s) 25 | ARCH=$(uname -m) 26 | OS=$(echo $OS | tr '[:upper:]' '[:lower:]') 27 | VERSION="1.13.1" 28 | NEWPATH="" 29 | 30 | function installKB() { 31 | version=2.2.0 # latest stable version 32 | arch=amd64 33 | 34 | # download the release 35 | curl -L -O "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${version}/kubebuilder_${version}_${OS}_${arch}.tar.gz" 36 | 37 | # extract the archive 38 | tar -zxvf kubebuilder_${version}_${OS}_${arch}.tar.gz 39 | mv kubebuilder_${version}_${OS}_${arch} kubebuilder && sudo mv kubebuilder /usr/local/ 40 | 41 | # update your PATH to include /usr/local/kubebuilder/bin 42 | NEWPATH+=":/usr/local/kubebuilder/bin" 43 | export PATH=$PATH:/usr/local/kubebuilder/bin 44 | } 45 | 46 | function installKustomize() { 47 | curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash 48 | } 49 | 50 | function installGo(){ 51 | arch="amd64" 52 | curl -L -O https://dl.google.com/go/go$VERSION.$OS-$arch.tar.gz 53 | sudo tar -C /usr/local -xzf go$VERSION.$OS-$arch.tar.gz 54 | 55 | NEWPATH+=":/usr/local/go/bin" 56 | export PATH=$PATH:/usr/local/go/bin 57 | } 58 | function installDependency(){ 59 | cmd=`command -v curl` || { 60 | "curl is missing, it is required for downloading dependencies" 61 | exit 1 62 | } 63 | CUR=`pwd` 64 | cd /tmp 65 | cmd1=`command -v kubebuilder` || { 66 | PassPrint "Installing kubebuilder" 67 | installKB 68 | } 69 | cmd2=`command -v go` || { 70 | PassPrint "Installing go" 71 | installGo 72 | } 73 | cmd3=`command -v kustomize` || { 74 | PassPrint "Installing kustomize" 75 | installKustomize 76 | } 77 | cmd4=`command -v schema-generate` || { 78 | if [ -z '$GOPATH' ] ; then 79 | WarnPrint "set GOPATH and install schema-generate by: go get -u github.com/a-h/generate/..." 80 | else 81 | PassPrint "Installing schema-generate" 82 | go get -u github.com/a-h/generate/... 83 | fi 84 | } 85 | 86 | cd $CUR 87 | 88 | if [[ -n $cmd1 && -n $cmd2 && cmd3 ]] ; then 89 | InfoPrint "Dependencies already exist" 90 | fi 91 | } 92 | 93 | function installCraft() { 94 | VERSION=$1 95 | PassPrint "Installing craft@$VERSION in /usr/local" 96 | sudo rm -rf /usr/local/craft 97 | CUR=`pwd` 98 | cd /tmp 99 | curl -L -O https://github.com/salesforce/craft/releases/download/$VERSION/craft.tar.gz 100 | TYPE=`file craft.tar.gz` 101 | NEWPATH+=":/usr/local/craft/bin" 102 | if [[ "$TYPE" != *"gzip compressed data"* ]]; then 103 | FailPrint "Downloaded craft.tar.gz is not of correct format. Maybe SSO is required." 104 | echo """ 105 | Try downloading https://github.com/salesforce/craft/releases/download/$VERSION/craft.tar.gz from browser. 106 | Then follow: 107 | sudo tar -C /usr/local -xzf craft.tar.gz 108 | export PATH=\$PATH$NEWPATH 109 | """ 110 | exit 1 111 | fi 112 | tar -xf craft.tar.gz 113 | sudo mv craft /usr/local 114 | cd $CUR 115 | } 116 | 117 | function install(){ 118 | installDependency 119 | installCraft $1 120 | if [[ -n $NEWPATH ]] ; then 121 | WarnPrint "export PATH=\$PATH$NEWPATH" 122 | fi 123 | } 124 | case $OS in 125 | darwin | linux) 126 | case $ARCH in 127 | x86_64) 128 | install ${1:-"v0.1.0-alpha"} 129 | ;; 130 | *) 131 | echo "There is no linkerd $OS support for $arch. Please open an issue with your platform details." 132 | exit 1 133 | ;; 134 | esac 135 | ;; 136 | *) 137 | echo "There is no linkerd support for $OS/$arch. Please open an issue with your platform details." 138 | exit 1 139 | ;; 140 | esac 141 | 142 | 143 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package utils 7 | 8 | import ( 9 | "bufio" 10 | "fmt" 11 | "os" 12 | "os/exec" 13 | "strings" 14 | 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | func FileExists(filename string) bool { 19 | info, err := os.Stat(filename) 20 | if os.IsNotExist(err) { 21 | return false 22 | } 23 | return !info.IsDir() 24 | } 25 | 26 | func CmdExec(cmdStr, dir string) { 27 | log.Debugf("$(%s) %s", dir, cmdStr) 28 | cmdList := strings.Split(cmdStr, " ") 29 | 30 | out := exec.Command(cmdList[0], cmdList[1:]...) 31 | out.Dir = dir 32 | // stdoutStderr, err := out.CombinedOutput() 33 | stdoutStderr, err := out.StdoutPipe() 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | out.Stderr = out.Stdout 38 | done := make(chan struct{}) 39 | scanner := bufio.NewScanner(stdoutStderr) 40 | go func() { 41 | for scanner.Scan() { 42 | output := scanner.Text() 43 | if strings.Contains(output, "msg") { 44 | slice := strings.SplitAfter(output, "msg=") 45 | output = slice[len(slice)-1] 46 | log.Infof(strings.Trim(output, "\"")) 47 | } else { 48 | log.Infof(output) 49 | } 50 | } 51 | done <- struct{}{} 52 | }() 53 | err = out.Start() 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | <-done 58 | err = out.Wait() 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | } 63 | 64 | func MinCmdExec(cmdStr, dir string) { 65 | log.Debugf("$(%s): %s", dir, cmdStr) 66 | cmdList := strings.Split(cmdStr, " ") 67 | 68 | out := exec.Command(cmdList[0], cmdList[1:]...) 69 | out.Dir = dir 70 | out.Env = os.Environ() 71 | stdoutStderr, err := out.CombinedOutput() 72 | log.Debug("%s", stdoutStderr) 73 | if err != nil { 74 | MinCmdExec(cmdStr, dir) 75 | //log.Fatal(err) 76 | } 77 | } 78 | 79 | func EnvCmdExec(cmdStr, dir string, env []string) { 80 | log.Debugf("$(%s): %s", dir, cmdStr) 81 | log.Debugf("env %s", env) 82 | cmdList := strings.Split(cmdStr, " ") 83 | 84 | out := exec.Command(cmdList[0], cmdList[1:]...) 85 | out.Dir = dir 86 | out.Env = os.Environ() 87 | for _, e := range env { 88 | out.Env = append(out.Env, e) 89 | } 90 | stdoutStderr, err := out.CombinedOutput() 91 | log.Infof("%s", stdoutStderr) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | 96 | } 97 | 98 | func ReturnCmdExec(cmdStr, dir string) string{ 99 | log.Debugf("$(%s): %s", dir, cmdStr) 100 | cmdList := strings.Split(cmdStr, " ") 101 | 102 | out := exec.Command(cmdList[0], cmdList[1:]...) 103 | out.Dir = dir 104 | out.Env = os.Environ() 105 | stdoutStderr, err := out.CombinedOutput() 106 | log.Infof("%s", stdoutStderr) 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | return string(stdoutStderr) 111 | } 112 | 113 | func Exists(path string) { 114 | if _, err := os.Stat(path); os.IsNotExist(err) { 115 | log.Fatal(err) 116 | } 117 | } 118 | 119 | func CheckGoPath() { 120 | if os.ExpandEnv("$GOPATH") == "" { 121 | fmt.Println("$GOPATH is not set.") 122 | os.Exit(0) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /utils/validation.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, salesforce.com, inc. 2 | // All rights reserved. 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | // For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 5 | 6 | package utils 7 | 8 | import ( 9 | "encoding/json" 10 | "io/ioutil" 11 | "strings" 12 | 13 | log "github.com/sirupsen/logrus" 14 | 15 | "gopkg.in/yaml.v2" 16 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" 17 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 18 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation" 19 | "k8s.io/apimachinery/pkg/conversion" 20 | "k8s.io/apimachinery/pkg/runtime/schema" 21 | ) 22 | 23 | func hasAnyStatusEnabled(crd *apiextensions.CustomResourceDefinitionSpec) bool { 24 | if hasStatusEnabled(crd.Subresources) { 25 | return true 26 | } 27 | for _, v := range crd.Versions { 28 | if hasStatusEnabled(v.Subresources) { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | 35 | // hasStatusEnabled returns true if given CRD Subresources has non-nil Status set. 36 | func hasStatusEnabled(subresources *apiextensions.CustomResourceSubresources) bool { 37 | if subresources != nil && subresources.Status != nil { 38 | return true 39 | } 40 | return false 41 | } 42 | 43 | func Validate(crdPath string) { 44 | jsonFile, err := ioutil.ReadFile(crdPath) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | sepYamlfiles := strings.Split(string(jsonFile), "---") 49 | for _, f := range sepYamlfiles { 50 | if strings.Contains(f, "kind: CustomResourceDefinition") { 51 | var obj v1beta1.CustomResourceDefinition 52 | var apiObj apiextensions.CustomResourceDefinition 53 | var s conversion.Scope 54 | var body interface{} 55 | yaml.Unmarshal([]byte(f), &body) 56 | body = convert(body) 57 | if b, err := json.Marshal(body); err != nil { 58 | log.Fatal(err) 59 | } else { 60 | json.Unmarshal(b, &obj) 61 | // log.Debugf("%+v\n", obj) 62 | v1beta1.Convert_v1beta1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(&obj, &apiObj, s) 63 | // log.Debugf("%+v\n", apiObj.Spec.Validation.OpenAPIV3Schema) 64 | // version v0.18.2 65 | requestGV := schema.GroupVersion{ 66 | Group: apiObj.Spec.Group, 67 | Version: apiObj.Spec.Version, 68 | } 69 | errList := validation.ValidateCustomResourceDefinition(&apiObj, requestGV) 70 | for _, e := range errList { 71 | if !strings.Contains(e.Error(), "status.storedVersion") { 72 | log.Warnf("Error: %s\n", e) 73 | } 74 | } 75 | } 76 | } 77 | 78 | } 79 | } 80 | 81 | func convert(i interface{}) interface{} { 82 | switch x := i.(type) { 83 | case map[interface{}]interface{}: 84 | m2 := map[string]interface{}{} 85 | for k, v := range x { 86 | m2[k.(string)] = convert(v) 87 | } 88 | return m2 89 | case []interface{}: 90 | for i, v := range x { 91 | x[i] = convert(v) 92 | } 93 | } 94 | return i 95 | } 96 | --------------------------------------------------------------------------------