├── .github ├── PULL_REQUEST_TEMPLATE.md ├── semantic.yml └── workflows │ ├── latest-image-push.yaml │ ├── release-docker.yaml │ └── test-release.yaml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.testing ├── LICENSE ├── Makefile ├── README.md ├── cmd └── main.go ├── common ├── controller │ └── reconciler.go ├── json │ └── json.go ├── labels │ ├── labels.go │ └── labels_test.go ├── metac │ ├── hook.go │ ├── hook_test.go │ └── validation.go ├── pointer │ └── pointer.go ├── string │ └── string.go └── unstruct │ ├── list.go │ ├── list_test.go │ ├── unstruct.go │ └── unstruct_test.go ├── config └── metac.yaml ├── controller ├── command │ ├── finalizer.go │ ├── reconciler.go │ └── reconciler_test.go ├── doperator │ └── reconciler.go ├── http │ ├── reconciler.go │ └── reconciler_test.go ├── recipe │ ├── finalizer.go │ └── reconciler.go └── run │ ├── assert.go │ ├── assert_test.go │ ├── create.go │ ├── create_test.go │ ├── createordelete.go │ ├── createordelete_test.go │ ├── delete.go │ ├── delete_test.go │ ├── reconciler.go │ ├── run.go │ ├── run_test.go │ ├── task.go │ ├── task_test.go │ ├── update.go │ └── update_test.go ├── deploy ├── crd.yaml ├── namespace.yaml ├── operator.yaml └── rbac.yaml ├── go.mod ├── go.sum ├── pkg ├── command │ ├── job_builder.go │ ├── job_builder_test.go │ ├── reconciler.go │ └── reconciler_test.go ├── http │ └── http.go ├── kubernetes │ ├── retry.go │ ├── utility.go │ ├── utility_int_test.go │ └── utility_test.go ├── lock │ └── lock.go ├── recipe │ ├── apply.go │ ├── apply_int_test.go │ ├── assert.go │ ├── base.go │ ├── common.go │ ├── crd.go │ ├── crd_test.go │ ├── crd_v1.go │ ├── crd_v1_int_test.go │ ├── crd_v1beta1.go │ ├── crd_v1beta1_int_test.go │ ├── create.go │ ├── create_int_test.go │ ├── eligible.go │ ├── eligible_test.go │ ├── errors.go │ ├── fixture.go │ ├── fixture_test.go │ ├── get.go │ ├── labeling.go │ ├── list.go │ ├── list_int_crds_test.go │ ├── list_int_items_test.go │ ├── lock.go │ ├── noop_test.go │ ├── path_check.go │ ├── path_check_test.go │ ├── recipe.go │ ├── recipe_int_labeling_test.go │ ├── recipe_int_list_test.go │ ├── recipe_int_simple_test.go │ ├── recipe_test.go │ ├── retry.go │ ├── state_check.go │ ├── task.go │ └── util.go └── schema │ ├── validation.go │ └── validation_test.go ├── release.config.js ├── test ├── declarative │ ├── BEST_PRACTICES.md │ ├── ci.yaml │ ├── experiments │ │ ├── assert-deprecated-daemonset.yaml │ │ ├── assert-github-search-invalid-method-neg.yaml │ │ ├── assert-github-search-invalid-method.yaml │ │ ├── assert-lock-persists-for-recipe-that-runs-once.yaml │ │ ├── assert-recipe-invalid-schema.yaml │ │ ├── assert-recipe-lock-is-garbage-collected.yaml │ │ ├── assert-recipe-teardown.yaml │ │ ├── command-creation-deletion.yaml │ │ ├── create-assert-fifty-configmaps-in-time.yaml │ │ ├── create-assert-fifty-configmaps.yaml │ │ └── crud-ops-on-pod.yaml │ ├── inference.yaml │ ├── registries.yaml │ └── suite.sh └── integration │ ├── it.yaml │ ├── registries.yaml │ └── suite.sh ├── tools └── d-action │ ├── Dockerfile │ ├── Makefile │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── pkg │ ├── action │ ├── run_command.go │ ├── run_command_test.go │ ├── shell_command.go │ └── shell_command_test.go │ └── util │ ├── json.go │ ├── list.go │ ├── pointer.go │ └── unstruct.go └── types ├── command ├── command.go └── command_test.go ├── cronresource └── types.go ├── doperator └── types.go ├── git └── git.go ├── gvk └── gvk.go ├── http └── types.go ├── recipe ├── apply.go ├── assert.go ├── create.go ├── delete.go ├── get.go ├── label.go ├── labels.go ├── list.go ├── patch_check.go ├── recipe.go ├── schema.go ├── state_check.go └── task.go └── run └── types.go /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Pull Request template 2 | Please, go through these steps before you submit a PR. 3 | 4 | 1. This repository follows semantic versioning convention, therefore each PR title/commit message must follow convention: `(): [#issue_number] - `. 5 | `type` is defining if release will be triggering after merging submitted changes, details in [CONTRIBUTING.md](../CONTRIBUTING.md). `#issue_number` is optional, when commit resolves any github issue. 6 | Most common types are: 7 | * `feat` - for new features 8 | * `fix` - for bug fixes or improvements 9 | * `chore` - changes not related to code 10 | 11 | 12 | 13 | IMPORTANT: Please review the [CONTRIBUTING.md](../CONTRIBUTING.md) file for detailed contributing guidelines. 14 | 15 | **PLEASE REMOVE THIS TEMPLATE BEFORE SUBMITTING** -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | titleAndCommits: true 2 | allowMergeCommits: true -------------------------------------------------------------------------------- /.github/workflows/latest-image-push.yaml: -------------------------------------------------------------------------------- 1 | name: Push latest-images to docker 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | push-latest: 9 | runs-on: ubuntu-18.04 10 | strategy: 11 | matrix: 12 | push: ['push-latest'] 13 | name: ${{ matrix.push }} 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v2 17 | - name: Login to DockerHub 18 | uses: docker/login-action@v1 19 | with: 20 | username: ${{ secrets.DOCKER_USERNAME }} 21 | password: ${{ secrets.DOCKER_PASSWORD }} 22 | - name: Build and push dope latest image 23 | run: | 24 | make ${{ matrix.push }} 25 | - name: Build and push d-action latest image 26 | run: | 27 | cd tools/d-action 28 | make ${{ matrix.push }} -------------------------------------------------------------------------------- /.github/workflows/release-docker.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release Docker Image 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | jobs: 8 | release-docker: 9 | name: Release docker dope image 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - name: Checkout Code 13 | uses: actions/checkout@v2 14 | - name: Build push dope image 15 | uses: docker/build-push-action@v1 16 | with: 17 | username: ${{ secrets.DOCKER_USERNAME }} 18 | password: ${{ secrets.DOCKER_PASSWORD }} 19 | repository: mayadataio/dope 20 | tag_with_ref: true 21 | add_git_labels: true 22 | release-docker-daction: 23 | name: Release docker daction image 24 | runs-on: ubuntu-18.04 25 | steps: 26 | - name: Checkout Code 27 | uses: actions/checkout@v2 28 | - name: Login to DockerHub 29 | run: cd tools/d-action 30 | - name: Build push daction image 31 | uses: docker/build-push-action@v1 32 | with: 33 | username: ${{ secrets.DOCKER_USERNAME }} 34 | password: ${{ secrets.DOCKER_PASSWORD }} 35 | repository: mayadataio/daction 36 | tag_with_ref: true 37 | add_git_labels: true -------------------------------------------------------------------------------- /.github/workflows/test-release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Testing and Github Release (if on master) 3 | on: [push, pull_request] 4 | jobs: 5 | unittest: 6 | runs-on: ubuntu-18.04 7 | name: Unit Test 8 | steps: 9 | - name: Checkout Code 10 | uses: actions/checkout@v2 11 | - name: Setup Golang 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: 1.13.5 15 | - run: make test 16 | integrationtest: 17 | runs-on: ubuntu-18.04 18 | name: Integration Test 19 | steps: 20 | - name: Checkout Code 21 | uses: actions/checkout@v2 22 | - name: Setup Golang 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: 1.13.5 26 | - run: sudo make integration-test-suite 27 | declarativetest: 28 | runs-on: ubuntu-18.04 29 | name: Declarative Test 30 | steps: 31 | - name: Checkout Code 32 | uses: actions/checkout@v2 33 | - name: Setup Golang 34 | uses: actions/setup-go@v2 35 | with: 36 | go-version: 1.13.5 37 | - run: sudo make declarative-test-suite 38 | release: 39 | name: Make Github Release 40 | runs-on: ubuntu-18.04 41 | needs: ['unittest', 'declarativetest', 'integrationtest'] 42 | steps: 43 | - name: Checkout Code 44 | uses: actions/checkout@v1 45 | - name: Setup Node.js 46 | uses: actions/setup-node@v1 47 | with: 48 | node-version: 12 49 | - name: Install NPM Dependencies to Make Release 50 | run: npm install ci 51 | - name: Make Semantic Release 52 | env: 53 | GH_TOKEN: ${{ secrets.PAT }} 54 | run: npx semantic-release 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | tools/d-action/vendor/ 3 | d-operators 4 | test/bin/ 5 | test/kubebin/ 6 | test/declarative/uninstall-k3s.txt 7 | test/integration/uninstall-k3s.txt 8 | uninstall-k3s.txt 9 | dope 10 | daction 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. 4 | 5 | ## Contributor License Agreement 6 | 7 | Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. 8 | 9 | You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. 10 | 11 | ## Code Reviews 12 | 13 | All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult 14 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. 15 | 16 | ## Commit messages 17 | 18 | This repository uses [semantic versioning](https://semver.org/), therefore every commit and PR must follow naming convention. We require commit header to be in form `(): [#issue_number] - `, where `type`, `scope` and 19 | `subject` as defined in [angular commit message convention](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type). `#issue_number` is optional, if commit resolves any gihub issue. 20 | 21 | Please be aware that new release will be triggered for: 22 | * `feat` - minor release 23 | * `perf/fix` - patch release 24 | 25 | where minor/patch as in semantic versioning definition. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # -------------------------- 2 | # Build d-operators binary 3 | # -------------------------- 4 | FROM golang:1.13.5 as builder 5 | 6 | WORKDIR /mayadata.io/d-operators/ 7 | 8 | # copy go modules manifests 9 | COPY go.mod go.mod 10 | COPY go.sum go.sum 11 | 12 | # ensure vendoring is up-to-date by running make vendor 13 | # in your local setup 14 | # 15 | # we cache the vendored dependencies before building and 16 | # copying source so that we don't need to re-download when 17 | # source changes don't invalidate our downloaded layer 18 | RUN go mod download 19 | RUN go mod tidy 20 | RUN go mod vendor 21 | 22 | # copy build manifests 23 | COPY Makefile Makefile 24 | 25 | # copy source files 26 | COPY cmd/ cmd/ 27 | COPY common/ common/ 28 | COPY config/ config/ 29 | COPY controller/ controller/ 30 | COPY pkg/ pkg/ 31 | COPY types/ types/ 32 | 33 | # build binary 34 | RUN make 35 | 36 | # --------------------------- 37 | # Use distroless as minimal base image to package the final binary 38 | # Refer https://github.com/GoogleContainerTools/distroless 39 | # --------------------------- 40 | FROM gcr.io/distroless/static:nonroot 41 | 42 | WORKDIR / 43 | 44 | COPY config/metac.yaml /etc/config/metac/metac.yaml 45 | COPY --from=builder /mayadata.io/d-operators/dope /usr/bin/ 46 | 47 | USER nonroot:nonroot 48 | 49 | CMD ["/usr/bin/dope"] -------------------------------------------------------------------------------- /Dockerfile.testing: -------------------------------------------------------------------------------- 1 | # -------------------------- 2 | # Build d-operators binary 3 | # -------------------------- 4 | FROM golang:1.13.5 as builder 5 | 6 | WORKDIR /mayadata.io/d-operators/ 7 | 8 | # copy all manifests 9 | COPY . . -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Fetch the latest tags & then set the package version 2 | PACKAGE_VERSION ?= $(shell git fetch --all --tags | echo "" | git describe --always --tags) 3 | ALL_SRC = $(shell find . -name "*.go" | grep -v -e "vendor") 4 | 5 | # We are using docker hub as the default registry 6 | IMG_NAME ?= dope 7 | IMG_REPO ?= mayadataio/dope 8 | 9 | all: bins 10 | 11 | bins: vendor test $(IMG_NAME) 12 | 13 | $(IMG_NAME): $(ALL_SRC) 14 | @echo "+ Generating $(IMG_NAME) binary" 15 | @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on \ 16 | go build -o $@ ./cmd/main.go 17 | 18 | $(ALL_SRC): ; 19 | 20 | # go mod download modules to local cache 21 | # make vendored copy of dependencies 22 | # install other go binaries for code generation 23 | .PHONY: vendor 24 | vendor: go.mod go.sum 25 | @GO111MODULE=on go mod download 26 | @GO111MODULE=on go mod tidy 27 | @GO111MODULE=on go mod vendor 28 | 29 | .PHONY: test 30 | test: 31 | @go test ./... -cover 32 | 33 | .PHONY: testv 34 | testv: 35 | @go test ./... -cover -v -args --logtostderr -v=2 36 | 37 | .PHONY: integration-test 38 | integration-test: 39 | # Uncomment to list verbose output 40 | # @go test ./... -cover --tags=integration -v -args --logtostderr -v=1 41 | @go test ./... -cover --tags=integration 42 | 43 | .PHONY: declarative-test-suite 44 | declarative-test-suite: 45 | @cd test/declarative && ./suite.sh 46 | 47 | .PHONY: integration-test-suite 48 | integration-test-suite: 49 | @cd test/integration && ./suite.sh 50 | 51 | .PHONY: image 52 | image: 53 | docker build -t $(IMG_REPO):$(PACKAGE_VERSION) -t $(IMG_REPO):latest . 54 | 55 | .PHONY: push 56 | push: image 57 | docker push $(IMG_REPO):$(PACKAGE_VERSION) 58 | 59 | .PHONY: push-latest 60 | push-latest: image 61 | docker push $(IMG_REPO):latest 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## D-operators 2 | D-operators define various declarative patterns to write kubernetes controllers. This uses [metac](https://github.com/AmitKumarDas/metac/) under the hood. Users can _create_, _delete_, _update_, _assert_, _patch_, _clone_, & _schedule_ one or more kubernetes resources _(native as well as custom)_ using a yaml file. D-operators expose a bunch of kubernetes custom resources that provide the building blocks to implement higher order controller(s). 3 | 4 | D-operators follow a pure intent based approach to writing specifications **instead of** having to deal with yamls that are cluttered with scripts, kubectl, loops, conditions, templating and so on. 5 | 6 | ### A sample declarative intent 7 | ```yaml 8 | apiVersion: dope.mayadata.io/v1 9 | kind: Recipe 10 | metadata: 11 | name: crud-ops-on-pod 12 | namespace: d-testing 13 | spec: 14 | tasks: 15 | - name: apply-a-namespace 16 | apply: 17 | state: 18 | kind: Namespace 19 | apiVersion: v1 20 | metadata: 21 | name: my-ns 22 | - name: create-a-pod 23 | create: 24 | state: 25 | kind: Pod 26 | apiVersion: v1 27 | metadata: 28 | name: my-pod 29 | namespace: my-ns 30 | spec: 31 | containers: 32 | - name: web 33 | image: nginx 34 | - name: delete-the-pod 35 | delete: 36 | state: 37 | kind: Pod 38 | apiVersion: v1 39 | metadata: 40 | name: my-pod 41 | namespace: my-ns 42 | - name: delete-the-namespace 43 | delete: 44 | state: 45 | kind: Namespace 46 | apiVersion: v1 47 | metadata: 48 | name: my-ns 49 | ``` 50 | 51 | ### Programmatic vs. Declarative 52 | It is important to understand that these declarative patterns are built upon programmatic ones. The low level constructs _(read native Kubernetes resources & custom resources)_ might be implemented in programming language(s) of one's choice. Use d-controller's YAMLs to aggregate these low level resources in a particular way to build a completely new kubernetes controller. 53 | 54 | ### When to use D-operators 55 | D-operators is not meant to build complex controller logic like Deployment, StatefulSet or Pod in a declarative yaml. However, if one needs to use available Kubernetes resources to build new k8s controller(s) then d-operators should be considered to build one. D-operators helps implement the last mile automation needed to manage applications & infrastructure in Kubernetes clusters. 56 | 57 | ### Declarative Testing 58 | D-operators make use of its custom resource(s) to test its controllers. One can imagine these custom resources acting as the building blocks to implement a custom CI framework. One of the primary advantages with this approach, is to let custom resources remove the need to write code to implement test cases. 59 | 60 | _NOTE: One can make use of these YAMLs (kind: Recipe) to test any Kubernetes controllers declaratively_ 61 | 62 | Navigate to test/declarative/experiments to learn more on these YAMLs. 63 | 64 | ```sh 65 | # Following runs the declarative test suite 66 | # 67 | # NOTE: test/declarative/suite.sh does the following: 68 | # - d-operators' image known as 'dope' is built 69 | # - a docker container is started & acts as the image registry 70 | # - dope image is pushed to this image registry 71 | # - k3s is installed with above image registry 72 | # - d-operators' manifests are applied 73 | # - experiments _(i.e. test code written as YAMLs)_ are applied 74 | # - experiments are asserted 75 | # - if all experiments pass then this testing is a success else it failed 76 | # - k3s is un-installed 77 | # - local image registry is stopped 78 | sudo make declarative-test-suite 79 | ``` 80 | 81 | ### Programmatic Testing 82 | D-operators also lets one to write testing Kubernetes controllers using Golang. This involves building the docker image (refer Dockerfile.testing) of the entire codebase and letting it run as a Kubernetes pod (refer test/integration/it.yaml). The setup required run these tests can be found at test/integration folder. Actual test logic are regular _test.go files found in respective packages. These _test.go files need to be tagged appropriately. These mode of testing has the additional advantage of getting the code coverage. 83 | 84 | ```go 85 | // +build integration 86 | ``` 87 | 88 | ```sh 89 | make integration-test-suite 90 | ``` 91 | 92 | ### Available Kubernetes controllers 93 | - [x] kind: Recipe 94 | - [ ] kind: RecipeClass 95 | - [ ] kind: RecipeGroupReport 96 | - [ ] kind: RecipeDebug 97 | - [ ] kind: Blueprint 98 | - [ ] kind: Validation 99 | - [ ] kind: CodeCov 100 | - [ ] kind: HTTP 101 | - [ ] kind: HTTPFlow 102 | - [ ] kind: Command 103 | - [ ] kind: DaemonJob 104 | - [ ] kind: UberLoop 105 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | 22 | "k8s.io/klog/v2" 23 | "mayadata.io/d-operators/controller/command" 24 | "mayadata.io/d-operators/controller/doperator" 25 | "mayadata.io/d-operators/controller/http" 26 | "mayadata.io/d-operators/controller/recipe" 27 | "mayadata.io/d-operators/controller/run" 28 | "openebs.io/metac/controller/generic" 29 | "openebs.io/metac/start" 30 | ) 31 | 32 | // main function is the entry point of this binary. 33 | // 34 | // This registers various controller (i.e. kubernetes reconciler) 35 | // handler functions. Each handler function gets triggered due 36 | // to any changes (add, update or delete) to configured watch 37 | // resource. 38 | // 39 | // NOTE: 40 | // These functions will also be triggered in case this binary 41 | // gets deployed or redeployed (due to restarts, etc.). 42 | // 43 | // NOTE: 44 | // One can consider each registered function as an independent 45 | // kubernetes controller & this project as the operator. 46 | func main() { 47 | flag.Set("alsologtostderr", "true") 48 | flag.Parse() 49 | 50 | klogFlags := flag.NewFlagSet("klog", flag.ExitOnError) 51 | klog.InitFlags(klogFlags) 52 | 53 | // Sync the glog and klog flags. 54 | flag.CommandLine.VisitAll(func(f1 *flag.Flag) { 55 | f2 := klogFlags.Lookup(f1.Name) 56 | if f2 != nil { 57 | value := f1.Value.String() 58 | f2.Value.Set(value) 59 | } 60 | }) 61 | defer klog.Flush() 62 | 63 | // controller name & corresponding controller reconcile function 64 | var controllers = map[string]generic.InlineInvokeFn{ 65 | "sync/recipe": recipe.Sync, 66 | "finalize/recipe": recipe.Finalize, 67 | "sync/http": http.Sync, 68 | "sync/doperator": doperator.Sync, 69 | "sync/run": run.Sync, 70 | "sync/command": command.Sync, 71 | "finalize/command": command.Finalize, 72 | } 73 | 74 | for name, ctrl := range controllers { 75 | generic.AddToInlineRegistry(name, ctrl) 76 | } 77 | start.Start() 78 | } 79 | -------------------------------------------------------------------------------- /common/json/json.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package json 18 | 19 | import ( 20 | "encoding/json" 21 | ) 22 | 23 | // JSONable holds any object that can be marshaled to json 24 | type JSONable struct { 25 | Obj interface{} 26 | } 27 | 28 | // New returns a new type of JSONable 29 | func New(obj interface{}) *JSONable { 30 | return &JSONable{obj} 31 | } 32 | 33 | // MustMarshal marshals the JSONable type 34 | func (j *JSONable) MustMarshal() string { 35 | raw, err := json.MarshalIndent(j.Obj, "", ".") 36 | if err != nil { 37 | panic(err) 38 | } 39 | return string(raw) 40 | } 41 | -------------------------------------------------------------------------------- /common/labels/labels.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package labels 18 | 19 | // Pair represents the labels 20 | type Pair map[string]string 21 | 22 | // New returns a new Pair type 23 | func New(labels map[string]string) Pair { 24 | return Pair(labels) 25 | } 26 | 27 | // Has returns true if all the given labels are 28 | // available 29 | func (p Pair) Has(given map[string]string) bool { 30 | if len(given) == len(p) && len(p) == 0 { 31 | return true 32 | } 33 | if len(p) == 0 { 34 | return false 35 | } 36 | for k, v := range given { 37 | if p[k] != v { 38 | return false 39 | } 40 | } 41 | return true 42 | } 43 | -------------------------------------------------------------------------------- /common/labels/labels_test.go: -------------------------------------------------------------------------------- 1 | // +build !integration 2 | 3 | /* 4 | Copyright 2020 The MayaData Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package labels 20 | 21 | import "testing" 22 | 23 | func TestLabelsHas(t *testing.T) { 24 | var tests = map[string]struct { 25 | src map[string]string 26 | target map[string]string 27 | isContains bool 28 | }{ 29 | "no src no target": { 30 | isContains: true, 31 | }, 32 | "no src": { 33 | target: map[string]string{ 34 | "hi": "there", 35 | }, 36 | isContains: false, 37 | }, 38 | "no target": { 39 | src: map[string]string{ 40 | "hi": "there", 41 | }, 42 | isContains: true, 43 | }, 44 | "src has target": { 45 | src: map[string]string{ 46 | "hi": "there", 47 | "hello": "world", 48 | }, 49 | target: map[string]string{ 50 | "hello": "world", 51 | }, 52 | isContains: true, 53 | }, 54 | "src does not have target": { 55 | src: map[string]string{ 56 | "hi": "there", 57 | "hello": "world", 58 | }, 59 | target: map[string]string{ 60 | "hello": "earth", 61 | }, 62 | isContains: false, 63 | }, 64 | } 65 | for name, mock := range tests { 66 | name := name 67 | mock := mock 68 | t.Run(name, func(t *testing.T) { 69 | p := New(mock.src) 70 | got := p.Has(mock.target) 71 | if got != mock.isContains { 72 | t.Fatalf( 73 | "Expected isContains %t got %t", 74 | mock.isContains, 75 | got, 76 | ) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /common/metac/hook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package metac 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "openebs.io/metac/controller/generic" 24 | ) 25 | 26 | // GetDetailsFromRequest returns details of provided 27 | // response in string format 28 | func GetDetailsFromRequest(req *generic.SyncHookRequest) string { 29 | if req == nil { 30 | return "" 31 | } 32 | var details []string 33 | if req.Watch == nil || req.Watch.Object == nil { 34 | details = append( 35 | details, 36 | "GCtl request watch = nil:", 37 | ) 38 | } else { 39 | details = append( 40 | details, 41 | fmt.Sprintf( 42 | "GCtl request watch %q / %q:", 43 | req.Watch.GetNamespace(), 44 | req.Watch.GetName()), 45 | ) 46 | } 47 | var allKinds map[string]int = map[string]int{} 48 | if req.Attachments == nil { 49 | details = append( 50 | details, 51 | "GCtl request attachments = nil", 52 | ) 53 | } else { 54 | for _, attachment := range req.Attachments.List() { 55 | if attachment == nil || attachment.Object == nil { 56 | continue 57 | } 58 | kind := attachment.GetKind() 59 | if kind == "" { 60 | kind = "NA" 61 | } 62 | count := allKinds[kind] 63 | allKinds[kind] = count + 1 64 | } 65 | if len(allKinds) > 0 { 66 | details = append( 67 | details, 68 | "GCtl request attachments", 69 | ) 70 | } 71 | for kind, count := range allKinds { 72 | details = append( 73 | details, 74 | fmt.Sprintf("[%s %d]", kind, count), 75 | ) 76 | } 77 | } 78 | return strings.Join(details, " ") 79 | } 80 | 81 | // GetDetailsFromResponse returns details of provided 82 | // response in string format 83 | func GetDetailsFromResponse(resp *generic.SyncHookResponse) string { 84 | if resp == nil || len(resp.Attachments) == 0 { 85 | return "" 86 | } 87 | var allKinds map[string]int = map[string]int{} 88 | for _, attachment := range resp.Attachments { 89 | if attachment == nil || attachment.Object == nil { 90 | continue 91 | } 92 | kind := attachment.GetKind() 93 | if kind == "" { 94 | kind = "NA" 95 | } 96 | count := allKinds[kind] 97 | allKinds[kind] = count + 1 98 | } 99 | var details []string 100 | details = append( 101 | details, 102 | "GCtl response attachments", 103 | ) 104 | for kind, count := range allKinds { 105 | details = append( 106 | details, 107 | fmt.Sprintf("[%s %d]", kind, count)) 108 | } 109 | return strings.Join(details, " ") 110 | } 111 | -------------------------------------------------------------------------------- /common/metac/hook_test.go: -------------------------------------------------------------------------------- 1 | // +build !integration 2 | 3 | /* 4 | Copyright 2020 The MayaData Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package metac 20 | 21 | import ( 22 | "testing" 23 | 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | "openebs.io/metac/controller/common" 26 | "openebs.io/metac/controller/generic" 27 | ) 28 | 29 | func TestGetDetailsFromRequest(t *testing.T) { 30 | var tests = map[string]struct { 31 | request *generic.SyncHookRequest 32 | isExpect bool 33 | }{ 34 | "nil request": { 35 | isExpect: false, 36 | }, 37 | "nil watch & attachments": { 38 | request: &generic.SyncHookRequest{}, 39 | isExpect: true, 40 | }, 41 | "not nil watch & nil attachments": { 42 | request: &generic.SyncHookRequest{ 43 | Watch: &unstructured.Unstructured{ 44 | Object: map[string]interface{}{ 45 | "kind": "Some", 46 | "metadata": map[string]interface{}{ 47 | "name": "test", 48 | "namespace": "test", 49 | }, 50 | }, 51 | }, 52 | }, 53 | isExpect: true, 54 | }, 55 | "not nil watch & not nil attachments": { 56 | request: &generic.SyncHookRequest{ 57 | Watch: &unstructured.Unstructured{ 58 | Object: map[string]interface{}{ 59 | "kind": "Some", 60 | "metadata": map[string]interface{}{ 61 | "name": "test", 62 | "namespace": "test", 63 | }, 64 | }, 65 | }, 66 | Attachments: common.AnyUnstructRegistry( 67 | map[string]map[string]*unstructured.Unstructured{ 68 | "gvk": map[string]*unstructured.Unstructured{ 69 | "nsname1": &unstructured.Unstructured{ 70 | Object: map[string]interface{}{ 71 | "kind": "Some1", 72 | }, 73 | }, 74 | "nsname2": &unstructured.Unstructured{ 75 | Object: map[string]interface{}{ 76 | "kind": "Some2", 77 | }, 78 | }, 79 | }, 80 | }, 81 | ), 82 | }, 83 | isExpect: true, 84 | }, 85 | } 86 | for name, mock := range tests { 87 | name := name 88 | mock := mock 89 | t.Run(name, func(t *testing.T) { 90 | got := GetDetailsFromRequest(mock.request) 91 | if len(got) != 0 != mock.isExpect { 92 | t.Fatalf( 93 | "Expected response %t got %s", 94 | mock.isExpect, 95 | got, 96 | ) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | func TestGetDetailsFromResponse(t *testing.T) { 103 | var tests = map[string]struct { 104 | response *generic.SyncHookResponse 105 | isExpect bool 106 | }{ 107 | "nil response": {}, 108 | "nil response attachments": { 109 | response: &generic.SyncHookResponse{}, 110 | }, 111 | "1 response attachment": { 112 | response: &generic.SyncHookResponse{ 113 | Attachments: []*unstructured.Unstructured{ 114 | &unstructured.Unstructured{ 115 | Object: map[string]interface{}{ 116 | "kind": "Some", 117 | }, 118 | }, 119 | }, 120 | }, 121 | isExpect: true, 122 | }, 123 | } 124 | for name, mock := range tests { 125 | name := name 126 | mock := mock 127 | t.Run(name, func(t *testing.T) { 128 | got := GetDetailsFromResponse(mock.response) 129 | if len(got) != 0 != mock.isExpect { 130 | t.Fatalf( 131 | "Expected response %t got %s", 132 | mock.isExpect, 133 | got, 134 | ) 135 | } 136 | }) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /common/metac/validation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package metac 18 | 19 | import ( 20 | "github.com/pkg/errors" 21 | "openebs.io/metac/controller/generic" 22 | ) 23 | 24 | // ValidateGenericControllerArgs validates the given request & 25 | // response 26 | func ValidateGenericControllerArgs( 27 | request *generic.SyncHookRequest, 28 | response *generic.SyncHookResponse, 29 | ) error { 30 | if request == nil { 31 | return errors.Errorf("Invalid gctl args: Nil request") 32 | } 33 | if request.Watch == nil || request.Watch.Object == nil { 34 | return errors.Errorf("Invalid gctl args: Nil watch") 35 | } 36 | if response == nil { 37 | return errors.Errorf("Invalid gctl args: Nil response") 38 | } 39 | // this is a valid request & response 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /common/pointer/pointer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package pointer 18 | 19 | // Bool returns a pointer to the given bool 20 | func Bool(b bool) *bool { 21 | o := b 22 | return &o 23 | } 24 | 25 | // Int returns a pointer to the given int 26 | func Int(i int) *int { 27 | o := i 28 | return &o 29 | } 30 | 31 | // Int32 returns a pointer to the given int32 32 | func Int32(i int32) *int32 { 33 | o := i 34 | return &o 35 | } 36 | 37 | // Int64 returns a pointer to the given int64 38 | func Int64(i int64) *int64 { 39 | o := i 40 | return &o 41 | } 42 | 43 | // String returns a pointer to the given string 44 | func String(s string) *string { 45 | o := s 46 | return &o 47 | } 48 | -------------------------------------------------------------------------------- /common/string/string.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package string 18 | 19 | import "strings" 20 | 21 | // List is a representation of list of strings 22 | type List []string 23 | 24 | // Map is a mapped representation of string with 25 | // boolean value 26 | type Map map[string]bool 27 | 28 | // String implements Stringer interface 29 | func (l List) String() string { 30 | return strings.Join(l, ", ") 31 | } 32 | 33 | // ContainsExact returns true if given string is exact 34 | // match with one if the items in the list 35 | func (l List) ContainsExact(given string) bool { 36 | for _, available := range l { 37 | if available == given { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | // Contains returns true if given string is a 45 | // substring of the items in the list 46 | func (l List) Contains(substr string) bool { 47 | for _, available := range l { 48 | if strings.Contains(available, substr) { 49 | return true 50 | } 51 | } 52 | return false 53 | } 54 | 55 | // Equality helps in finding difference or merging 56 | // list of string based items. 57 | type Equality struct { 58 | src List 59 | dest List 60 | } 61 | 62 | // NewEquality returns a populated Equality structure 63 | func NewEquality(src, dest List) Equality { 64 | return Equality{ 65 | src: src, 66 | dest: dest, 67 | } 68 | } 69 | 70 | // Diff finds the difference between source list & destination 71 | // list and returns the no change, addition & removal items 72 | // respectively 73 | func (e Equality) Diff() (noops Map, additions []string, removals []string) { 74 | noops = map[string]bool{} 75 | for _, source := range e.src { 76 | if e.dest.ContainsExact(source) { 77 | noops[source] = true 78 | continue 79 | } 80 | removals = append(removals, source) 81 | } 82 | for _, destination := range e.dest { 83 | if e.src.ContainsExact(destination) { 84 | continue 85 | } 86 | additions = append(additions, destination) 87 | } 88 | 89 | return 90 | } 91 | 92 | // IsDiff flags if there was any changes between src list & dest list 93 | func (e Equality) IsDiff() bool { 94 | noops, additions, removals := e.Diff() 95 | if !(len(noops) == len(e.src) && len(removals) == 0 && len(additions) == 0) { 96 | return true 97 | } 98 | return false 99 | } 100 | 101 | // Merge merges the source items with destination items 102 | // by keeping the order of source items. Source items that 103 | // need to be replaced as replaced from new destination 104 | // items. It appends new used items to the end of the resulting 105 | // list. 106 | // 107 | // TODO (@amitkumardas): 108 | // This logic may not be sufficient for cases when group of items 109 | // needs to be removed without their replacements. Current logic 110 | // will just move some or all of the items up i.e. their position 111 | // index will decrease. 112 | func (e Equality) Merge() []string { 113 | var new []string 114 | var used = map[string]bool{} 115 | var merge []string 116 | for _, destItem := range e.dest { 117 | // check if src contains this dest item 118 | if e.src.ContainsExact(destItem) { 119 | // nothing to be done here 120 | continue 121 | } 122 | // store this as a new item since src does not have it 123 | new = append(new, destItem) 124 | } 125 | // we want to merge by following the order of source list 126 | for _, sourceItem := range e.src { 127 | // check if dest contains this src item 128 | if e.dest.ContainsExact(sourceItem) { 129 | // continue keeping this src item as merge item 130 | merge = append(merge, sourceItem) 131 | continue 132 | } 133 | // donot use this source item 134 | // replace source item with a new item if available 135 | if len(new) == 0 || len(new) == len(used) { 136 | // NOTE: 137 | // no replacement from new list 138 | // will get replaced by next suitable item from src list 139 | continue 140 | } 141 | // extract the replacement item from new list 142 | newItem := new[len(used)] 143 | merge = append(merge, newItem) 144 | // mark this replacement item as used 145 | used[newItem] = true 146 | } 147 | // check for extras 148 | for _, newItem := range new { 149 | if len(used) == 0 || !used[newItem] { 150 | // use this new item since it has not been 151 | // used as a replacement previously 152 | merge = append(merge, newItem) 153 | } 154 | } 155 | return merge 156 | } 157 | -------------------------------------------------------------------------------- /common/unstruct/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package unstruct 18 | 19 | import ( 20 | "reflect" 21 | 22 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 | ) 24 | 25 | // List is a custom datatype representing a list of 26 | // unstructured instances 27 | type List []*unstructured.Unstructured 28 | 29 | // ContainsByIdentity returns true if provided target is available 30 | // by its name, uid & other metadata fields 31 | func (s List) ContainsByIdentity(target *unstructured.Unstructured) bool { 32 | if target == nil || target.Object == nil { 33 | // we don't know how to compare against a nil 34 | return false 35 | } 36 | for _, obj := range s { 37 | if obj == nil || obj.Object == nil { 38 | continue 39 | } 40 | if obj.GetName() == target.GetName() && 41 | obj.GetNamespace() == target.GetNamespace() && 42 | obj.GetUID() == target.GetUID() && 43 | obj.GetKind() == target.GetKind() && 44 | obj.GetAPIVersion() == target.GetAPIVersion() { 45 | return true 46 | } 47 | } 48 | return false 49 | } 50 | 51 | // IdentifiesAll returns true if each item in the provided 52 | // targets is available & match by their identity 53 | func (s List) IdentifiesAll(targets []*unstructured.Unstructured) bool { 54 | if len(s) == len(targets) && len(s) == 0 { 55 | return true 56 | } 57 | if len(s) != len(targets) { 58 | return false 59 | } 60 | for _, t := range targets { 61 | if !s.ContainsByIdentity(t) { 62 | // return false if any item does not match 63 | return false 64 | } 65 | } 66 | return true 67 | } 68 | 69 | // ContainsByEquality does a field to field match of provided target 70 | // against the corresponding object present in this list 71 | func (s List) ContainsByEquality(target *unstructured.Unstructured) bool { 72 | if target == nil || target.Object == nil { 73 | // we can't match a nil target 74 | return false 75 | } 76 | for _, src := range s { 77 | if src == nil || src.Object == nil { 78 | continue 79 | } 80 | // use meta fields as much as possible to verify 81 | // if target & src do not match 82 | if src.GetName() != target.GetName() || 83 | src.GetNamespace() != target.GetNamespace() || 84 | src.GetUID() != target.GetUID() || 85 | src.GetKind() != target.GetKind() || 86 | src.GetAPIVersion() != target.GetAPIVersion() || 87 | len(src.GetAnnotations()) != len(target.GetAnnotations()) || 88 | len(src.GetLabels()) != len(target.GetLabels()) || 89 | len(src.GetOwnerReferences()) != len(target.GetOwnerReferences()) || 90 | len(src.GetFinalizers()) != len(target.GetFinalizers()) { 91 | // continue since target does not match src 92 | continue 93 | } 94 | // Since target matches with this src based on meta 95 | // information we need to **verify further** by running 96 | // reflect based match 97 | return reflect.DeepEqual(target, src) 98 | } 99 | return false 100 | } 101 | 102 | // EqualsAll does a field to field match of each target against 103 | // the corresponding object present in this list 104 | func (s List) EqualsAll(targets []*unstructured.Unstructured) bool { 105 | if len(s) == len(targets) && len(s) == 0 { 106 | return true 107 | } 108 | if len(s) != len(targets) { 109 | return false 110 | } 111 | for _, t := range targets { 112 | if !s.ContainsByEquality(t) { 113 | return false 114 | } 115 | } 116 | return true 117 | } 118 | -------------------------------------------------------------------------------- /common/unstruct/unstruct.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package unstruct 18 | 19 | import ( 20 | "github.com/pkg/errors" 21 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/util/json" 24 | ) 25 | 26 | // ToTyped transforms the provided unstruct instance 27 | // to target type 28 | func ToTyped(src *unstructured.Unstructured, target interface{}) error { 29 | if src == nil || src.Object == nil { 30 | return errors.Errorf( 31 | "Can't transform unstruct to typed: Nil unstruct content", 32 | ) 33 | } 34 | if target == nil { 35 | return errors.Errorf( 36 | "Can't transform unstruct to typed: Nil target", 37 | ) 38 | } 39 | return runtime.DefaultUnstructuredConverter.FromUnstructured( 40 | src.UnstructuredContent(), 41 | target, 42 | ) 43 | } 44 | 45 | // MarshalThenUnmarshal marshals the provided src and unmarshals 46 | // it back into the dest 47 | func MarshalThenUnmarshal(src interface{}, dest interface{}) error { 48 | data, err := json.Marshal(src) 49 | if err != nil { 50 | return err 51 | } 52 | return json.Unmarshal(data, dest) 53 | } 54 | 55 | // SetLabels updates the given labels with the ones 56 | // found in the provided unstructured instance 57 | func SetLabels(obj *unstructured.Unstructured, lbls map[string]string) { 58 | if len(lbls) == 0 { 59 | return 60 | } 61 | if obj == nil || obj.Object == nil { 62 | return 63 | } 64 | got := obj.GetLabels() 65 | if got == nil { 66 | got = make(map[string]string) 67 | } 68 | for k, v := range lbls { 69 | // update given label against existing 70 | got[k] = v 71 | } 72 | obj.SetLabels(got) 73 | } 74 | -------------------------------------------------------------------------------- /config/metac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dope/v1 2 | kind: GenericController 3 | metadata: 4 | name: sync-recipe 5 | namespace: dope 6 | spec: 7 | watch: 8 | apiVersion: dope.mayadata.io/v1 9 | resource: recipes 10 | hooks: 11 | sync: 12 | inline: 13 | funcName: sync/recipe 14 | --- 15 | apiVersion: dope/v1 16 | kind: GenericController 17 | metadata: 18 | name: finalize-recipe 19 | namespace: dope 20 | spec: 21 | watch: 22 | apiVersion: dope.mayadata.io/v1 23 | resource: recipes 24 | attachments: 25 | - apiVersion: v1 26 | resource: configmaps 27 | advancedSelector: 28 | selectorTerms: 29 | # select ConfigMap if its labels has following 30 | - matchLabels: 31 | recipe.dope.mayadata.io/lock: "true" 32 | matchReferenceExpressions: 33 | - key: metadata.labels.recipe\.dope\.mayadata\.io/name 34 | operator: EqualsWatchName # match this lbl value against watch Name 35 | hooks: 36 | finalize: 37 | inline: 38 | funcName: finalize/recipe 39 | --- 40 | apiVersion: dope/v1 41 | kind: GenericController 42 | metadata: 43 | name: sync-http 44 | namespace: dope 45 | spec: 46 | watch: 47 | # kind: HTTP custom resource is watched 48 | apiVersion: dope.mayadata.io/v1 49 | resource: https 50 | hooks: 51 | sync: 52 | inline: 53 | funcName: sync/http 54 | --- 55 | apiVersion: dope/v1 56 | kind: GenericController 57 | metadata: 58 | name: sync-command 59 | namespace: dope 60 | spec: 61 | watch: 62 | apiVersion: dope.mayadata.io/v1 63 | resource: commands 64 | hooks: 65 | sync: 66 | inline: 67 | funcName: sync/command 68 | --- 69 | apiVersion: dope/v1 70 | kind: GenericController 71 | metadata: 72 | name: finalize-command 73 | namespace: dope 74 | spec: 75 | watch: 76 | apiVersion: dope.mayadata.io/v1 77 | resource: commands 78 | attachments: 79 | # Delete pod 80 | - apiVersion: v1 81 | resource: pods 82 | advancedSelector: 83 | selectorTerms: 84 | # select Pod if its labels has following 85 | - matchReferenceExpressions: 86 | - key: metadata.namespace 87 | operator: EqualsWatchNamespace 88 | - key: metadata.labels.job-name 89 | operator: EqualsWatchName # match this lbl value against watch Name 90 | # Delete job 91 | - apiVersion: batch/v1 92 | resource: jobs 93 | advancedSelector: 94 | selectorTerms: 95 | # select job if its labels has following 96 | - matchLabels: 97 | command.dope.mayadata.io/controller: "true" 98 | matchReferenceExpressions: 99 | - key: metadata.labels.command\.dope\.mayadata\.io/uid 100 | operator: EqualsWatchUID # match this lbl value against watch UID 101 | - apiVersion: v1 102 | resource: configmaps 103 | advancedSelector: 104 | selectorTerms: 105 | # select ConfigMap if its labels has following 106 | - matchLabels: 107 | command.dope.mayadata.io/lock: "true" 108 | matchReferenceExpressions: 109 | - key: metadata.labels.command\.dope\.mayadata\.io/uid 110 | operator: EqualsWatchUID # match this lbl value against watch Name 111 | hooks: 112 | finalize: 113 | inline: 114 | funcName: finalize/command 115 | --- 116 | -------------------------------------------------------------------------------- /controller/command/finalizer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package command 18 | 19 | import ( 20 | "openebs.io/metac/controller/generic" 21 | ) 22 | 23 | var ( 24 | defaultDeletionResyncTime = float64(30) 25 | ) 26 | 27 | // Finalize implements the idempotent logic that gets executed when 28 | // Command instance is deleted. A Command instance may have child job & 29 | // dedicated lock in form of a ConfigMap. 30 | // Finalize logic tries to delete child pod, job & ConfigMap 31 | // 32 | // NOTE: 33 | // When finalize hook is set in the config metac automatically sets 34 | // a finalizer entry against the Command metadata's finalizers field . 35 | // This finalizer entry is removed when SyncHookResponse's Finalized 36 | // field is set to true 37 | // 38 | // NOTE: 39 | // SyncHookRequest is the payload received as part of finalize 40 | // request. Similarly, SyncHookResponse is the payload sent as a 41 | // response as part of finalize request. 42 | // 43 | // NOTE: 44 | // Returning error will panic this process. We would rather want this 45 | // controller to run continuously. Hence, the errors are handled. 46 | func Finalize(request *generic.SyncHookRequest, response *generic.SyncHookResponse) error { 47 | if request.Attachments.IsEmpty() { 48 | // Since no Dependents found it is safe to delete Command 49 | response.Finalized = true 50 | return nil 51 | } 52 | 53 | response.ResyncAfterSeconds = defaultDeletionResyncTime 54 | // Observed attachments will get deleted 55 | response.ExplicitDeletes = request.Attachments.List() 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /controller/http/reconciler_test.go: -------------------------------------------------------------------------------- 1 | // +build !integration 2 | 3 | /* 4 | Copyright 2020 The MayaData Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package http 20 | -------------------------------------------------------------------------------- /controller/recipe/finalizer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | import ( 20 | "openebs.io/metac/controller/generic" 21 | ) 22 | 23 | // Finalize implements the idempotent logic that gets executed when 24 | // Recipe instance is deleted. A Recipe instance is associated with 25 | // a dedicated lock in form of a ConfigMap. Finalize logic tries to 26 | // delete this ConfigMap. 27 | // 28 | // NOTE: 29 | // When finalize hook is set in the config metac automatically sets 30 | // a finalizer entry against the Recipe metadata's finalizers field . 31 | // This finalizer entry is removed when SyncHookResponse's Finalized 32 | // field is set to true. 33 | // 34 | // NOTE: 35 | // SyncHookRequest is the payload received as part of finalize 36 | // request. Similarly, SyncHookResponse is the payload sent as a 37 | // response as part of finalize request. 38 | // 39 | // NOTE: 40 | // Returning error will panic this process. We would rather want this 41 | // controller to run continuously. Hence, the errors are handled. 42 | func Finalize(request *generic.SyncHookRequest, response *generic.SyncHookResponse) error { 43 | if request.Attachments.IsEmpty() { 44 | // Since no ConfigMap is found it is safe to delete Recipe 45 | response.Finalized = true 46 | return nil 47 | } 48 | // A single ConfigMap instance is expected in the attachments 49 | // That needs to be deleted explicitly 50 | response.ExplicitDeletes = request.Attachments.List() 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /controller/run/reconciler.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package run 18 | 19 | import ( 20 | "github.com/pkg/errors" 21 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 22 | "openebs.io/metac/controller/generic" 23 | 24 | ctrlutil "mayadata.io/d-operators/common/controller" 25 | "mayadata.io/d-operators/common/unstruct" 26 | types "mayadata.io/d-operators/types/run" 27 | ) 28 | 29 | // Reconciler manages reconciliation of HTTP resources 30 | type Reconciler struct { 31 | ctrlutil.Reconciler 32 | 33 | isWatchAndRunSame bool 34 | observedRun *types.Run 35 | 36 | runResponse *Response 37 | } 38 | 39 | func (r *Reconciler) evalRun() { 40 | var run types.Run 41 | // convert from unstructured instance to typed Run instance 42 | err := unstruct.ToTyped( 43 | r.HookRequest.Watch, 44 | &run, 45 | ) 46 | if err != nil { 47 | r.Err = err 48 | return 49 | } 50 | r.observedRun = &run 51 | } 52 | 53 | func (r *Reconciler) invokeRun() { 54 | // if this run is for a resource that is being reconciled 55 | // via the Run reconciler 56 | var runForWatch *unstructured.Unstructured 57 | runAnns := r.observedRun.GetAnnotations() 58 | if len(runAnns) == 0 { 59 | // watch & run as same 60 | r.isWatchAndRunSame = true 61 | runForWatch = r.HookRequest.Watch 62 | } else if runAnns[string(types.RunForWatchEnabled)] == "true" { 63 | runForWatch := r.HookRequest.Attachments.FindByGroupKindName( 64 | runAnns[string(types.RunForWatchAPIGroup)], 65 | runAnns[string(types.RunForWatchKind)], 66 | runAnns[string(types.RunForWatchName)], 67 | ) 68 | if runForWatch == nil { 69 | r.Err = errors.Errorf( 70 | "Can't reconcile run: Watch not found: %s/%s %s: %s/%s %s", 71 | r.observedRun.GetNamespace(), 72 | r.observedRun.GetName(), 73 | r.observedRun.GroupVersionKind().String(), 74 | runAnns[string(types.RunForWatchAPIGroup)], 75 | runAnns[string(types.RunForWatchKind)], 76 | runAnns[string(types.RunForWatchName)], 77 | ) 78 | return 79 | } 80 | } 81 | r.runResponse, r.Err = ExecRun(Request{ 82 | ObservedResources: r.HookRequest.Attachments.List(), 83 | Run: r.HookRequest.Watch, 84 | Watch: runForWatch, 85 | RunCond: r.observedRun.Spec.RunIf, 86 | Tasks: r.observedRun.Spec.Tasks, 87 | IncludeInfo: r.observedRun.Spec.IncludeInfo, 88 | }) 89 | } 90 | 91 | func (r *Reconciler) fillSyncResponse() { 92 | r.HookResponse.Attachments = append( 93 | r.HookResponse.Attachments, 94 | r.runResponse.DesiredResources..., 95 | ) 96 | // TODO (@amitkumardas) @ metac -then-> here: 97 | // add explicit deletes 98 | // add explicit updates 99 | } 100 | 101 | func (r *Reconciler) trySetWatchStatus() { 102 | if r.isWatchAndRunSame { 103 | r.HookResponse.Status = map[string]interface{}{ 104 | "status": r.runResponse.RunStatus, 105 | } 106 | } else { 107 | // TODO (@amitkumardas): 108 | // 109 | // add one event against the watch resource into 110 | // response attachments 111 | // 112 | // event may be error or normal or warning based 113 | // on status 114 | } 115 | } 116 | 117 | // Sync implements the idempotent logic to sync Run resource 118 | // 119 | // NOTE: 120 | // SyncHookRequest is the payload received as part of reconcile 121 | // request. Similarly, SyncHookResponse is the payload sent as a 122 | // response as part of reconcile response. 123 | // 124 | // NOTE: 125 | // This controller watches Run custom resource 126 | func Sync(request *generic.SyncHookRequest, response *generic.SyncHookResponse) error { 127 | r := &Reconciler{ 128 | Reconciler: ctrlutil.Reconciler{ 129 | HookRequest: request, 130 | HookResponse: response, 131 | }, 132 | } 133 | // add functions to achieve desired state 134 | r.ReconcileFns = []func(){ 135 | r.evalRun, 136 | r.invokeRun, 137 | r.fillSyncResponse, 138 | } 139 | // add functions to achieve desired watch 140 | r.DesiredWatchFns = []func(){ 141 | r.trySetWatchStatus, 142 | } 143 | // run reconcile 144 | return r.Reconcile() 145 | } 146 | -------------------------------------------------------------------------------- /deploy/crd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1beta1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | name: recipes.dope.mayadata.io 7 | spec: 8 | group: dope.mayadata.io 9 | names: 10 | kind: Recipe 11 | listKind: RecipeList 12 | plural: recipes 13 | shortNames: 14 | - rcp 15 | singular: recipe 16 | scope: Namespaced 17 | subresources: 18 | status: {} 19 | version: v1 20 | versions: 21 | - name: v1 22 | served: true 23 | storage: true 24 | additionalPrinterColumns: 25 | - name: Age 26 | type: date 27 | description: Age of this Recipe 28 | JSONPath: .metadata.creationTimestamp 29 | - name: TimeTaken 30 | type: string 31 | description: Time taken to execute this Recipe 32 | JSONPath: .status.executionTime.readableValue 33 | - name: Status 34 | type: string 35 | description: Current phase of this Recipe 36 | JSONPath: .status.phase 37 | - name: Reason 38 | type: string 39 | description: Description of this phase 40 | JSONPath: .status.reason 41 | --- 42 | apiVersion: apiextensions.k8s.io/v1beta1 43 | kind: CustomResourceDefinition 44 | metadata: 45 | annotations: 46 | name: https.dope.mayadata.io 47 | spec: 48 | group: dope.mayadata.io 49 | names: 50 | kind: HTTP 51 | listKind: HTTPList 52 | plural: https 53 | shortNames: 54 | - http 55 | singular: http 56 | scope: Namespaced 57 | subresources: 58 | status: {} 59 | version: v1 60 | versions: 61 | - name: v1 62 | served: true 63 | storage: true 64 | --- 65 | apiVersion: apiextensions.k8s.io/v1beta1 66 | kind: CustomResourceDefinition 67 | metadata: 68 | annotations: 69 | name: commands.dope.mayadata.io 70 | spec: 71 | group: dope.mayadata.io 72 | names: 73 | kind: Command 74 | listKind: CommandList 75 | plural: commands 76 | shortNames: 77 | - cmds 78 | singular: command 79 | scope: Namespaced 80 | subresources: 81 | status: {} 82 | version: v1 83 | versions: 84 | - name: v1 85 | served: true 86 | storage: true 87 | -------------------------------------------------------------------------------- /deploy/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: dope 5 | --- 6 | # d-testing is the Kubernetes namespace that can 7 | # be used to deploy any test artifacts 8 | apiVersion: v1 9 | kind: Namespace 10 | metadata: 11 | name: d-testing -------------------------------------------------------------------------------- /deploy/operator.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # This StatefulSet deploys dope 3 | apiVersion: apps/v1 4 | kind: StatefulSet 5 | metadata: 6 | labels: 7 | app.mayadata.io/name: dope 8 | name: dope 9 | namespace: dope 10 | spec: 11 | replicas: 1 12 | selector: 13 | matchLabels: 14 | app.mayadata.io/name: dope 15 | serviceName: "" 16 | template: 17 | metadata: 18 | labels: 19 | app.mayadata.io/name: dope 20 | spec: 21 | serviceAccountName: dope 22 | containers: 23 | - name: dope 24 | image: mayadataio/dope:v1.9.0 25 | command: ["/usr/bin/dope"] 26 | imagePullPolicy: Always 27 | args: 28 | - --logtostderr 29 | - --run-as-local 30 | - -v=3 31 | - --discovery-interval=30s 32 | - --cache-flush-interval=240s 33 | env: 34 | - name: DOPE_SERVICE_ACCOUNT 35 | valueFrom: 36 | fieldRef: 37 | fieldPath: spec.serviceAccountName -------------------------------------------------------------------------------- /deploy/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: dope 5 | namespace: dope 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: dope 11 | rules: 12 | - apiGroups: 13 | - "*" 14 | resources: 15 | - "*" 16 | verbs: 17 | - "*" 18 | --- 19 | apiVersion: rbac.authorization.k8s.io/v1 20 | kind: ClusterRoleBinding 21 | metadata: 22 | name: dope 23 | subjects: 24 | - kind: ServiceAccount 25 | name: dope 26 | namespace: dope 27 | roleRef: 28 | kind: ClusterRole 29 | name: dope 30 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module mayadata.io/d-operators 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/ghodss/yaml v1.0.0 7 | github.com/go-resty/resty/v2 v2.2.0 8 | github.com/google/go-cmp v0.4.0 9 | github.com/pkg/errors v0.9.1 10 | k8s.io/apiextensions-apiserver v0.17.3 11 | k8s.io/apimachinery v0.17.3 12 | k8s.io/client-go v0.17.3 13 | k8s.io/klog/v2 v2.0.0 14 | openebs.io/metac v0.5.0 15 | ) 16 | 17 | replace ( 18 | k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.17.3 19 | k8s.io/apimachinery => k8s.io/apimachinery v0.17.3 20 | k8s.io/client-go => k8s.io/client-go v0.17.3 21 | openebs.io/metac => github.com/AmitKumarDas/metac v0.5.0 22 | ) 23 | -------------------------------------------------------------------------------- /pkg/command/job_builder.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package command 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | "github.com/pkg/errors" 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | types "mayadata.io/d-operators/types/command" 26 | dynamicapply "openebs.io/metac/dynamic/apply" 27 | ) 28 | 29 | // JobBuildingConfig helps create new instances of JobBuilding 30 | type JobBuildingConfig struct { 31 | Command types.Command 32 | } 33 | 34 | // JobBuilding builds Kubernetes Job resource 35 | type JobBuilding struct { 36 | Command types.Command 37 | } 38 | 39 | // NewJobBuilder returns a new instance of JobBuilding 40 | func NewJobBuilder(config JobBuildingConfig) *JobBuilding { 41 | return &JobBuilding{ 42 | Command: config.Command, 43 | } 44 | } 45 | 46 | func (b *JobBuilding) getDefaultJob() *unstructured.Unstructured { 47 | return &unstructured.Unstructured{ 48 | Object: map[string]interface{}{ 49 | "kind": types.KindJob, 50 | "apiVersion": types.JobAPIVersion, 51 | "metadata": map[string]interface{}{ 52 | "name": b.Command.GetName(), 53 | "namespace": b.Command.GetNamespace(), 54 | "labels": map[string]interface{}{ 55 | types.LblKeyCommandIsController: "true", 56 | types.LblKeyCommandName: b.Command.GetName(), 57 | types.LblKeyCommandUID: string(b.Command.GetUID()), 58 | }, 59 | }, 60 | "spec": map[string]interface{}{ 61 | "ttlSecondsAfterFinished": int64(0), 62 | "template": map[string]interface{}{ 63 | "spec": map[string]interface{}{ 64 | "restartPolicy": "Never", 65 | "backoffLimit": int64(0), 66 | "serviceAccountName": os.Getenv("DOPE_SERVICE_ACCOUNT"), 67 | "containers": []interface{}{ 68 | map[string]interface{}{ 69 | "name": "daction", 70 | "image": "mayadataio/daction", 71 | "imagePullPolicy": "Always", 72 | "command": []interface{}{ 73 | "/usr/bin/daction", 74 | }, 75 | "args": []interface{}{ 76 | "-v=3", 77 | fmt.Sprintf("--command-name=%s", b.Command.GetName()), 78 | fmt.Sprintf("--command-ns=%s", b.Command.GetNamespace()), 79 | }, 80 | }, 81 | }, 82 | "imagePullSecrets": []interface{}{ 83 | map[string]interface{}{ 84 | "name": "mayadataio-cred", 85 | }, 86 | }, 87 | }, 88 | }, 89 | }, 90 | }, 91 | } 92 | } 93 | 94 | // Build returns the final job specifications 95 | // 96 | // NOTE: 97 | // This Job uses image capable of running commands 98 | // specified in the Command resource specs. 99 | func (b *JobBuilding) Build() (*unstructured.Unstructured, error) { 100 | var defaultJob = b.getDefaultJob() 101 | // Start off by initialising final Job to default 102 | final := defaultJob 103 | if b.Command.Spec.Template.Job != nil { 104 | // Job specs found in Command is the desired 105 | desired := b.Command.Spec.Template.Job 106 | // NOTE: 107 | // - desired Job spec must use Job kind & api version 108 | desired.SetKind(types.KindJob) 109 | desired.SetAPIVersion(types.JobAPIVersion) 110 | // NOTE: 111 | // - desired Job spec must use Command name & namespace 112 | desired.SetName(b.Command.GetName()) 113 | desired.SetNamespace(b.Command.GetNamespace()) 114 | // All other fields will be a 3-way merge between 115 | // default specifications & desired specifications 116 | finalObj, err := dynamicapply.Merge( 117 | defaultJob.UnstructuredContent(), // observed = default 118 | desired.UnstructuredContent(), // last applied = desired 119 | desired.UnstructuredContent(), // desired 120 | ) 121 | if err != nil { 122 | return nil, errors.Wrapf( 123 | err, 124 | "Failed to build job spec: 3-way merge failed", 125 | ) 126 | } 127 | // Reset the final specifications 128 | final.Object = finalObj 129 | } 130 | return final, nil 131 | } 132 | -------------------------------------------------------------------------------- /pkg/http/http.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package http 18 | 19 | import ( 20 | "strings" 21 | 22 | resty "github.com/go-resty/resty/v2" 23 | "github.com/pkg/errors" 24 | 25 | types "mayadata.io/d-operators/types/http" 26 | ) 27 | 28 | // InvocableConfig is used to initialise a new instance of 29 | // Invocable 30 | type InvocableConfig struct { 31 | Username string 32 | Password string 33 | HTTPMethod string 34 | URL string 35 | Body string 36 | Headers map[string]string 37 | QueryParams map[string]string 38 | PathParams map[string]string 39 | } 40 | 41 | // Invocable helps invoking a http request 42 | type Invocable struct { 43 | Username string 44 | Password string 45 | HTTPMethod string 46 | URL string 47 | Body string 48 | Headers map[string]string 49 | QueryParams map[string]string 50 | PathParams map[string]string 51 | } 52 | 53 | // Invoker returns a new instance of Invocable 54 | func Invoker(config InvocableConfig) *Invocable { 55 | return &Invocable{ 56 | Username: config.Username, 57 | Password: config.Password, 58 | HTTPMethod: config.HTTPMethod, 59 | URL: config.URL, 60 | Body: config.Body, 61 | Headers: config.Headers, 62 | PathParams: config.PathParams, 63 | QueryParams: config.QueryParams, 64 | } 65 | } 66 | 67 | func (i *Invocable) buildStatus(response *resty.Response) types.HTTPResponse { 68 | var resp = &types.HTTPResponse{} 69 | // set http response details 70 | if response != nil { 71 | resp.Body = response.Result() 72 | resp.HTTPStatusCode = response.StatusCode() 73 | resp.HTTPStatus = response.Status() 74 | resp.HTTPError = response.Error() 75 | resp.IsError = response.IsError() 76 | } 77 | return *resp 78 | } 79 | 80 | // Invoke executes the http request 81 | func (i *Invocable) Invoke() (types.HTTPResponse, error) { 82 | req := resty.New().R(). 83 | SetBody(i.Body). 84 | SetHeaders(i.Headers). 85 | SetQueryParams(i.QueryParams). 86 | SetPathParams(i.PathParams) 87 | 88 | // set credentials only if it was provided 89 | if i.Username != "" || i.Password != "" { 90 | req.SetBasicAuth(i.Username, i.Password) 91 | } 92 | 93 | var response *resty.Response 94 | var err error 95 | 96 | switch strings.ToUpper(i.HTTPMethod) { 97 | case types.POST: 98 | response, err = req.Post(i.URL) 99 | case types.GET: 100 | response, err = req.Get(i.URL) 101 | default: 102 | err = errors.Errorf( 103 | "HTTP method not supported: URL %q: Method %q", 104 | i.URL, 105 | i.HTTPMethod, 106 | ) 107 | } 108 | 109 | if err != nil { 110 | return types.HTTPResponse{}, err 111 | } 112 | return i.buildStatus(response), nil 113 | } 114 | -------------------------------------------------------------------------------- /pkg/kubernetes/retry.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package kubernetes 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | 23 | "k8s.io/klog/v2" 24 | ) 25 | 26 | // RetryTimeout is an error implementation that is thrown 27 | // when retry fails post timeout 28 | type RetryTimeout struct { 29 | Err string 30 | } 31 | 32 | // Error implements error interface 33 | func (rt *RetryTimeout) Error() string { 34 | return rt.Err 35 | } 36 | 37 | // Retryable helps executing user provided functions as 38 | // conditions in a repeated manner till this condition succeeds 39 | type Retryable struct { 40 | //Message string 41 | 42 | WaitTimeout time.Duration 43 | WaitInterval time.Duration 44 | 45 | // RunOnce will run the function only once 46 | // 47 | // NOTE: 48 | // In other words, this makes the retry option 49 | // as a No Operation i.e. noop 50 | RunOnce bool 51 | } 52 | 53 | // RetryConfig helps in creating an instance of Retryable 54 | type RetryConfig struct { 55 | WaitTimeout *time.Duration 56 | WaitInterval *time.Duration 57 | RunOnce bool 58 | } 59 | 60 | // NewRetry returns a new instance of Retryable 61 | func NewRetry(config RetryConfig) *Retryable { 62 | // timeout defaults to 60 seconds 63 | timeout := 60 * time.Second 64 | // sleep interval defaults to 1 second 65 | interval := 1 * time.Second 66 | 67 | // override timeout with user specified value 68 | if config.WaitTimeout != nil { 69 | timeout = *config.WaitTimeout 70 | } 71 | 72 | // override interval with user specified value 73 | if config.WaitInterval != nil { 74 | interval = *config.WaitInterval 75 | } 76 | 77 | return &Retryable{ 78 | WaitTimeout: timeout, 79 | WaitInterval: interval, 80 | RunOnce: config.RunOnce, 81 | } 82 | } 83 | 84 | // Waitf retries this provided function as a condition till 85 | // this condition succeeds. 86 | // 87 | // NOTE: 88 | // Clients invoking this method need to return appropriate 89 | // values (i.e. bool & error) within the condition implementation. 90 | // These return values let the condition to be either returned or 91 | // retried. 92 | func (r *Retryable) Waitf( 93 | condition func() (bool, error), // condition that gets retried 94 | msgFormat string, 95 | msgArgs ...interface{}, 96 | ) error { 97 | if r.RunOnce { 98 | // No need to retry if this condition is meant to be run once 99 | _, err := condition() 100 | return err 101 | } 102 | 103 | context := fmt.Sprintf( 104 | msgFormat, 105 | msgArgs..., 106 | ) 107 | // mark the start time 108 | start := time.Now() 109 | // check the condition in a forever loop 110 | for { 111 | done, err := condition() 112 | if err == nil && done { 113 | klog.V(3).Infof( 114 | "Retryable condition succeeded: %s", context, 115 | ) 116 | return nil 117 | } 118 | if err != nil && done { 119 | klog.V(3).Infof( 120 | "Retryable condition completed with error: %s: %s", 121 | context, 122 | err, 123 | ) 124 | return err 125 | } 126 | if time.Since(start) > r.WaitTimeout { 127 | var errmsg = "No errors found" 128 | if err != nil { 129 | errmsg = fmt.Sprintf("%+v", err) 130 | } 131 | return &RetryTimeout{ 132 | fmt.Sprintf( 133 | "Retryable condition timed out after %s: %s: %s", 134 | r.WaitTimeout, 135 | context, 136 | errmsg, 137 | ), 138 | } 139 | } 140 | // Just log keep trying until timeout or success 141 | if err != nil { 142 | klog.V(4).Infof( 143 | "Retryable condition has errors: Will retry: %s: %s", 144 | context, 145 | err, 146 | ) 147 | } else { 148 | klog.V(4).Infof( 149 | "Retryable condition did not succeed: Will retry: %s", 150 | context, 151 | ) 152 | } 153 | // retry after sleeping for specified interval 154 | time.Sleep(r.WaitInterval) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /pkg/kubernetes/utility_int_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | /* 4 | Copyright 2020 The MayaData Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package kubernetes 20 | 21 | import ( 22 | "testing" 23 | 24 | "k8s.io/client-go/rest" 25 | ) 26 | 27 | func TestNewFixtureIT(t *testing.T) { 28 | var tests = map[string]struct { 29 | kubeConfig *rest.Config 30 | isErr bool 31 | }{ 32 | "nil kubeconfig": {}, 33 | } 34 | for name, mock := range tests { 35 | name := name 36 | mock := mock 37 | t.Run(name, func(t *testing.T) { 38 | _, err := NewUtility(UtilityConfig{ 39 | KubeConfig: mock.kubeConfig, 40 | }) 41 | if mock.isErr && err == nil { 42 | t.Fatal("Expected error got none") 43 | } 44 | if !mock.isErr && err != nil { 45 | t.Fatalf("Expected no error got %s", err.Error()) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pkg/kubernetes/utility_test.go: -------------------------------------------------------------------------------- 1 | // +build !integration 2 | 3 | /* 4 | Copyright 2020 The MayaData Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package kubernetes 20 | 21 | import ( 22 | "testing" 23 | 24 | "k8s.io/client-go/rest" 25 | ) 26 | 27 | func TestNewFixture(t *testing.T) { 28 | var tests = map[string]struct { 29 | kubeConfig *rest.Config 30 | isErr bool 31 | }{ 32 | "nil kubeconfig": { 33 | isErr: true, 34 | }, 35 | "empty kubeconfig": { 36 | kubeConfig: &rest.Config{}, 37 | }, 38 | } 39 | for name, mock := range tests { 40 | name := name 41 | mock := mock 42 | t.Run(name, func(t *testing.T) { 43 | _, err := NewUtility(UtilityConfig{ 44 | KubeConfig: mock.kubeConfig, 45 | }) 46 | if mock.isErr && err == nil { 47 | t.Fatal("Expected error got none") 48 | } 49 | if !mock.isErr && err != nil { 50 | t.Fatalf("Expected no error got %s", err.Error()) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/recipe/assert.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | import ( 20 | "github.com/pkg/errors" 21 | types "mayadata.io/d-operators/types/recipe" 22 | ) 23 | 24 | // Assertable is used to perform matches of desired state(s) 25 | // against observed state(s) 26 | type Assertable struct { 27 | BaseRunner 28 | Assert *types.Assert 29 | 30 | assertCheckType types.AssertCheckType 31 | retryOnDiff bool 32 | retryOnEqual bool 33 | status *types.AssertStatus 34 | 35 | // error as value 36 | err error 37 | } 38 | 39 | // AssertableConfig is used to create an instance of Assertable 40 | type AssertableConfig struct { 41 | BaseRunner 42 | Assert *types.Assert 43 | } 44 | 45 | // NewAsserter returns a new instance of Assertion 46 | func NewAsserter(config AssertableConfig) *Assertable { 47 | return &Assertable{ 48 | BaseRunner: config.BaseRunner, 49 | Assert: config.Assert, 50 | status: &types.AssertStatus{}, 51 | } 52 | } 53 | 54 | func (a *Assertable) init() { 55 | var checks int 56 | if a.Assert.PathCheck != nil { 57 | checks++ 58 | a.assertCheckType = types.AssertCheckTypePath 59 | } 60 | if a.Assert.StateCheck != nil { 61 | checks++ 62 | a.assertCheckType = types.AssertCheckTypeState 63 | } 64 | if checks > 1 { 65 | a.err = errors.Errorf( 66 | "Failed to assert %q: More than one assert checks found", 67 | a.TaskName, 68 | ) 69 | return 70 | } 71 | if checks == 0 { 72 | // assert default to StateCheck based assertion 73 | a.Assert.StateCheck = &types.StateCheck{ 74 | Operator: types.StateCheckOperatorEquals, 75 | } 76 | } 77 | } 78 | 79 | func (a *Assertable) runAssertByPath() { 80 | chk := NewPathChecker( 81 | PathCheckingConfig{ 82 | BaseRunner: a.BaseRunner, 83 | State: a.Assert.State, 84 | PathCheck: *a.Assert.PathCheck, 85 | }, 86 | ) 87 | got, err := chk.Run() 88 | if err != nil { 89 | a.err = err 90 | return 91 | } 92 | a.status = &types.AssertStatus{ 93 | Phase: got.Phase.ToAssertResultPhase(), 94 | Message: got.Message, 95 | Verbose: got.Verbose, 96 | Warning: got.Warning, 97 | Timeout: got.Timeout, 98 | } 99 | } 100 | 101 | func (a *Assertable) runAssertByState() { 102 | chk := NewStateChecker( 103 | StateCheckingConfig{ 104 | BaseRunner: a.BaseRunner, 105 | State: a.Assert.State, 106 | StateCheck: *a.Assert.StateCheck, 107 | }, 108 | ) 109 | got, err := chk.Run() 110 | if err != nil { 111 | a.err = err 112 | return 113 | } 114 | a.status = &types.AssertStatus{ 115 | Phase: got.Phase.ToAssertResultPhase(), 116 | Message: got.Message, 117 | Verbose: got.Verbose, 118 | Warning: got.Warning, 119 | Timeout: got.Timeout, 120 | } 121 | } 122 | 123 | func (a *Assertable) runAssert() { 124 | switch a.assertCheckType { 125 | case types.AssertCheckTypePath: 126 | a.runAssertByPath() 127 | case types.AssertCheckTypeState: 128 | a.runAssertByState() 129 | default: 130 | a.err = errors.Errorf( 131 | "Failed to run assert %q: Invalid operator %q", 132 | a.TaskName, 133 | a.assertCheckType, 134 | ) 135 | } 136 | } 137 | 138 | // Run executes the assertion 139 | func (a *Assertable) Run() (types.AssertStatus, error) { 140 | if a.TaskName == "" { 141 | return types.AssertStatus{}, errors.Errorf( 142 | "Failed to run assert: Missing assert name", 143 | ) 144 | } 145 | if a.Assert == nil || a.Assert.State == nil { 146 | return types.AssertStatus{}, errors.Errorf( 147 | "Failed to run assert %q: Nil assert state", 148 | a.TaskName, 149 | ) 150 | } 151 | var fns = []func(){ 152 | a.init, 153 | a.runAssert, 154 | } 155 | for _, fn := range fns { 156 | fn() 157 | if a.err != nil { 158 | return types.AssertStatus{}, a.err 159 | } 160 | } 161 | var errorOnAssertFailure bool 162 | if a.Assert.ErrorOnAssertFailure != nil { 163 | errorOnAssertFailure = *a.Assert.ErrorOnAssertFailure 164 | } 165 | if errorOnAssertFailure && 166 | a.status.Phase != types.AssertResultPassed { 167 | return types.AssertStatus{}, errors.Errorf( 168 | "%s: ErrorOnAssertFailure=%t", 169 | a.status, 170 | errorOnAssertFailure, 171 | ) 172 | } 173 | return *a.status, nil 174 | } 175 | -------------------------------------------------------------------------------- /pkg/recipe/base.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | import ( 20 | "mayadata.io/d-operators/pkg/kubernetes" 21 | types "mayadata.io/d-operators/types/recipe" 22 | ) 23 | 24 | // BaseRunner is the common runner used by all action runners 25 | type BaseRunner struct { 26 | *Fixture 27 | TaskIndex int 28 | TaskName string 29 | Retry *kubernetes.Retryable 30 | FailFastRule types.FailFastRule 31 | } 32 | 33 | // NewDefaultBaseRunner returns a new instance of BaseRunner 34 | // set with defaults 35 | func NewDefaultBaseRunner(taskname string) (*BaseRunner, error) { 36 | inst, err := kubernetes.Singleton(kubernetes.UtilityConfig{}) 37 | if err != nil { 38 | return nil, err 39 | } 40 | f, err := NewFixture(FixtureConfig{ 41 | KubeConfig: inst.GetKubeConfig(), 42 | APIDiscovery: inst.GetAPIResourceDiscovery(), 43 | }) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return &BaseRunner{ 48 | Fixture: f, 49 | TaskName: taskname, 50 | Retry: kubernetes.NewRetry(kubernetes.RetryConfig{}), 51 | }, nil 52 | } 53 | 54 | // NewDefaultBaseRunnerWithTeardown returns a new instance of BaseRunner 55 | // set with defaults 56 | func NewDefaultBaseRunnerWithTeardown(taskname string) (*BaseRunner, error) { 57 | inst, err := kubernetes.Singleton(kubernetes.UtilityConfig{}) 58 | if err != nil { 59 | return nil, err 60 | } 61 | f, err := NewFixture(FixtureConfig{ 62 | IsTearDown: true, 63 | KubeConfig: inst.GetKubeConfig(), 64 | APIDiscovery: inst.GetAPIResourceDiscovery(), 65 | }) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return &BaseRunner{ 70 | Fixture: f, 71 | TaskName: taskname, 72 | Retry: kubernetes.NewRetry(kubernetes.RetryConfig{}), 73 | }, nil 74 | } 75 | 76 | // IsFailFastOnDiscoveryError returns true if logic that leads to 77 | // discovery error should not be re-tried 78 | func (r *BaseRunner) IsFailFastOnDiscoveryError() bool { 79 | return r.FailFastRule == types.FailFastOnDiscoveryError 80 | } 81 | 82 | // IsFailFastOnError returns true if logic that lead to given error 83 | // should not be re-tried 84 | func (r *BaseRunner) IsFailFastOnError(err error) bool { 85 | if _, discoveryErr := err.(*DiscoveryError); discoveryErr { 86 | return r.IsFailFastOnDiscoveryError() 87 | } 88 | return false 89 | } 90 | -------------------------------------------------------------------------------- /pkg/recipe/common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | import ( 20 | "fmt" 21 | 22 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 | ) 24 | 25 | // NamespaceName is a utility to hold namespace & name 26 | // of any object 27 | type NamespaceName struct { 28 | Namespace string `json:"namespace,omitempty"` 29 | Name string `json:"name"` 30 | } 31 | 32 | // String implements stringer interface 33 | func (nn NamespaceName) String() string { 34 | return fmt.Sprintf("NS=%s Name=%s", nn.Namespace, nn.Name) 35 | } 36 | 37 | // NewNamespaceName returns a new NamespaceName from the provided 38 | // unstructured object 39 | func NewNamespaceName(u unstructured.Unstructured) NamespaceName { 40 | return NamespaceName{ 41 | Namespace: u.GetNamespace(), 42 | Name: u.GetName(), 43 | } 44 | } 45 | 46 | // NewNamespaceNameList returns a new list of NamespaceName 47 | func NewNamespaceNameList(ul []unstructured.Unstructured) (out []NamespaceName) { 48 | for _, u := range ul { 49 | out = append(out, NewNamespaceName(u)) 50 | } 51 | return 52 | } 53 | 54 | // ResourceMappedNamespaceNames returns a map of resource to 55 | // corresponding list of NamespaceNames 56 | func ResourceMappedNamespaceNames( 57 | given map[string][]unstructured.Unstructured, 58 | ) map[string][]NamespaceName { 59 | var out = make(map[string][]NamespaceName) 60 | for resource, list := range given { 61 | var nsNames []NamespaceName 62 | for _, unstruct := range list { 63 | nsNames = append( 64 | nsNames, 65 | NewNamespaceName(unstruct), 66 | ) 67 | } 68 | out[resource] = nsNames 69 | } 70 | return out 71 | } 72 | -------------------------------------------------------------------------------- /pkg/recipe/crd_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | import ( 20 | "testing" 21 | 22 | gy "github.com/ghodss/yaml" 23 | v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | ) 26 | 27 | var crd = `apiVersion: apiextensions.k8s.io/v1 28 | kind: CustomResourceDefinition 29 | metadata: 30 | name: mayastorpools.openebs.io 31 | spec: 32 | group: openebs.io 33 | versions: 34 | - name: v1alpha1 35 | served: true 36 | storage: true 37 | subresources: 38 | status: {} 39 | schema: 40 | openAPIV3Schema: 41 | type: object 42 | properties: 43 | apiVersion: 44 | type: string 45 | kind: 46 | type: string 47 | metadata: 48 | type: object 49 | spec: 50 | description: Specification of the mayastor pool. 51 | type: object 52 | required: 53 | - node 54 | - disks 55 | properties: 56 | node: 57 | description: Name of the k8s node where the storage pool is located. 58 | type: string 59 | disks: 60 | description: Disk devices (paths or URIs) that should be used for the pool. 61 | type: array 62 | items: 63 | type: string 64 | status: 65 | description: Status part updated by the pool controller. 66 | type: object 67 | properties: 68 | state: 69 | description: Pool state. 70 | type: string 71 | reason: 72 | description: Reason for the pool state value if applicable. 73 | type: string 74 | disks: 75 | description: Disk device URIs that are actually used for the pool. 76 | type: array 77 | items: 78 | type: string 79 | capacity: 80 | description: Capacity of the pool in bytes. 81 | type: integer 82 | format: int64 83 | minimum: 0 84 | used: 85 | description: How many bytes are used in the pool. 86 | type: integer 87 | format: int64 88 | minimum: 0 89 | additionalPrinterColumns: 90 | - name: Node 91 | type: string 92 | description: Node where the storage pool is located 93 | jsonPath: .spec.node 94 | - name: State 95 | type: string 96 | description: State of the storage pool 97 | jsonPath: .status.state 98 | - name: Age 99 | type: date 100 | jsonPath: .metadata.creationTimestamp 101 | scope: Namespaced 102 | names: 103 | kind: MayastorPool 104 | listKind: MayastorPoolList 105 | plural: mayastorpools 106 | singular: mayastorpool 107 | shortNames: ["msp"] 108 | ` 109 | 110 | func TestMSPCRDV1FromStringUnmarshal(t *testing.T) { 111 | var unstructObj unstructured.Unstructured 112 | err := gy.Unmarshal([]byte(crd), &unstructObj) 113 | if err != nil { 114 | t.Fatalf("Failed to unmarshal: %v", err) 115 | } 116 | 117 | var crdObj v1.CustomResourceDefinition 118 | err = UnstructToTyped(&unstructObj, &crdObj) 119 | if err == nil { 120 | t.Fatal( 121 | "Convert to CRD type: Expected error got none", 122 | ) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /pkg/recipe/crd_v1.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/pkg/errors" 23 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 | ) 25 | 26 | // ExecutableCRDV1 helps to apply or create desired CRD state 27 | // against the cluster 28 | type ExecutableCRDV1 struct { 29 | ExecutableCRD 30 | } 31 | 32 | // ExecutableCRDV1Config helps in creating new instance of 33 | // ExecutableCRDV1 34 | type ExecutableCRDV1Config struct { 35 | BaseRunner BaseRunner 36 | IgnoreDiscovery bool 37 | State *unstructured.Unstructured 38 | DesiredCRVersion string 39 | } 40 | 41 | // NewCRDV1Executor returns a new instance of ExecutableCRDV1Beta1 42 | func NewCRDV1Executor(config ExecutableCRDV1Config) (*ExecutableCRDV1, error) { 43 | e := &ExecutableCRDV1{ 44 | ExecutableCRD: ExecutableCRD{ 45 | BaseRunner: config.BaseRunner, 46 | IgnoreDiscovery: config.IgnoreDiscovery, 47 | State: config.State, 48 | DesiredCRVersion: config.DesiredCRVersion, 49 | }, 50 | } 51 | err := e.setCRResourceAndAPIVersion() 52 | if err != nil { 53 | return nil, err 54 | } 55 | return e, nil 56 | } 57 | 58 | func (e *ExecutableCRDV1) setCRResourceAndAPIVersion() error { 59 | plural, found, err := unstructured.NestedString( 60 | e.State.Object, 61 | "spec", 62 | "names", 63 | "plural", 64 | ) 65 | if err != nil { 66 | return errors.Wrapf(err, "Failed to get spec.names.plural") 67 | } 68 | if !found { 69 | return errors.Errorf("Missing spec.names.plural") 70 | } 71 | group, found, err := unstructured.NestedString( 72 | e.State.Object, 73 | "spec", 74 | "group", 75 | ) 76 | if err != nil { 77 | return errors.Wrapf(err, "Failed to get spec.group") 78 | } 79 | if !found { 80 | return errors.Errorf("Missing spec.group") 81 | } 82 | // Get the version that is found first in the list 83 | // if nothing has been set 84 | if e.DesiredCRVersion == "" { 85 | vers, found, err := unstructured.NestedSlice( 86 | e.State.Object, 87 | "spec", 88 | "versions", 89 | ) 90 | if err != nil { 91 | return errors.Wrapf(err, "Failed to get spec.versions") 92 | } 93 | if !found { 94 | return errors.Errorf("Missing spec.versions") 95 | } 96 | for _, item := range vers { 97 | itemObj, ok := item.(map[string]interface{}) 98 | if !ok { 99 | return errors.Errorf( 100 | "Expected spec.versions type as map[string]interface{} got %T", 101 | item, 102 | ) 103 | } 104 | e.DesiredCRVersion = itemObj["name"].(string) 105 | // -- 106 | // First version in the list is only considered 107 | // This value is only used for CRD discovery which 108 | // again is an optional feature 109 | // -- 110 | break 111 | } 112 | } 113 | 114 | apiver := fmt.Sprintf("%s/%s", group, e.DesiredCRVersion) 115 | 116 | // memoize 117 | e.CRResource = plural 118 | e.CRAPIVersion = apiver 119 | 120 | // return resource name & apiVersion of the resource 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /pkg/recipe/crd_v1beta1.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/pkg/errors" 23 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 | ) 25 | 26 | // ExecutableCRDV1Beta1 helps to apply or create desired CRD state 27 | // against the cluster 28 | type ExecutableCRDV1Beta1 struct { 29 | ExecutableCRD 30 | } 31 | 32 | // ExecutableCRDV1Beta1Config helps in creating new instance of 33 | // ExecutableCRDV1Beta1 34 | type ExecutableCRDV1Beta1Config struct { 35 | BaseRunner BaseRunner 36 | IgnoreDiscovery bool 37 | State *unstructured.Unstructured 38 | } 39 | 40 | // NewCRDV1Beta1Executor returns a new instance of ExecutableCRDV1Beta1 41 | func NewCRDV1Beta1Executor(config ExecutableCRDV1Beta1Config) (*ExecutableCRDV1Beta1, error) { 42 | e := &ExecutableCRDV1Beta1{ 43 | ExecutableCRD: ExecutableCRD{ 44 | BaseRunner: config.BaseRunner, 45 | IgnoreDiscovery: config.IgnoreDiscovery, 46 | State: config.State, 47 | }, 48 | } 49 | err := e.setCRResourceAndAPIVersion() 50 | if err != nil { 51 | return nil, err 52 | } 53 | return e, nil 54 | } 55 | 56 | func (e *ExecutableCRDV1Beta1) setCRResourceAndAPIVersion() error { 57 | plural, found, err := unstructured.NestedString( 58 | e.State.Object, 59 | "spec", 60 | "names", 61 | "plural", 62 | ) 63 | if err != nil { 64 | return errors.Wrapf(err, "Failed to get spec.names.plural") 65 | } 66 | if !found { 67 | return errors.Errorf("Missing spec.names.plural") 68 | } 69 | group, found, err := unstructured.NestedString( 70 | e.State.Object, 71 | "spec", 72 | "group", 73 | ) 74 | if err != nil { 75 | return errors.Wrapf(err, "Failed to get spec.group") 76 | } 77 | if !found { 78 | return errors.Errorf("Missing spec.group") 79 | } 80 | ver, found, err := unstructured.NestedString( 81 | e.State.Object, 82 | "spec", 83 | "version", 84 | ) 85 | if err != nil { 86 | return errors.Wrapf(err, "Failed to get spec.version") 87 | } 88 | if !found || ver == "" { 89 | return errors.Errorf("Missing spec.version") 90 | } 91 | apiver := fmt.Sprintf("%s/%s", group, ver) 92 | 93 | // memoize 94 | e.CRResource = plural 95 | e.CRAPIVersion = apiver 96 | 97 | // return resource name & apiVersion of the resource 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/recipe/crd_v1beta1_int_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | /* 4 | Copyright 2020 The MayaData Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package recipe 20 | 21 | import ( 22 | "testing" 23 | 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | types "mayadata.io/d-operators/types/recipe" 26 | ) 27 | 28 | func TestCRDV1Beta1Apply(t *testing.T) { 29 | state := &unstructured.Unstructured{ 30 | Object: map[string]interface{}{ 31 | "apiVersion": "apiextensions.k8s.io/v1beta1", 32 | "kind": "CustomResourceDefinition", 33 | "metadata": map[string]interface{}{ 34 | "name": "somethings.openebs.io", 35 | }, 36 | "spec": map[string]interface{}{ 37 | "group": "openebs.io", 38 | "scope": "Namespaced", 39 | "names": map[string]interface{}{ 40 | "kind": "SomeThing", 41 | "listKind": "SomeThingList", 42 | "plural": "somethings", 43 | "singular": "something", 44 | "shortNames": []interface{}{ 45 | "sme", 46 | }, 47 | }, 48 | "version": "v1alpha1", 49 | "versions": []interface{}{ 50 | map[string]interface{}{ 51 | "name": "v1alpha1", 52 | "served": true, 53 | "storage": true, 54 | }, 55 | }, 56 | }, 57 | }, 58 | } 59 | 60 | br, err := NewDefaultBaseRunnerWithTeardown("apply crd testing") 61 | if err != nil { 62 | t.Fatalf( 63 | "Failed to create kubernetes base runner: %v", 64 | err, 65 | ) 66 | } 67 | e, err := NewCRDV1Beta1Executor(ExecutableCRDV1Beta1Config{ 68 | BaseRunner: *br, 69 | State: state, 70 | }) 71 | if err != nil { 72 | t.Fatalf( 73 | "Failed to construct crd v1beta1 executor: %v", 74 | err, 75 | ) 76 | } 77 | 78 | result, err := e.Apply() 79 | if err != nil { 80 | t.Fatalf( 81 | "Error while testing create CRD via apply: %v: %s", 82 | err, 83 | result, 84 | ) 85 | } 86 | if result.Phase != types.ApplyStatusPassed { 87 | t.Fatalf("Test failed while creating CRD via apply: %s", result) 88 | } 89 | 90 | // --------------- 91 | // UPDATE i.e. 3-WAY MERGE 92 | // --------------- 93 | update := &unstructured.Unstructured{ 94 | Object: map[string]interface{}{ 95 | "apiVersion": "apiextensions.k8s.io/v1beta1", 96 | "kind": "CustomResourceDefinition", 97 | "metadata": map[string]interface{}{ 98 | "name": "somethings.openebs.io", 99 | }, 100 | "spec": map[string]interface{}{ 101 | "group": "openebs.io", 102 | "names": map[string]interface{}{ 103 | "plural": "somethings", 104 | "shortNames": []interface{}{ 105 | "sme", 106 | "smethng", 107 | }, 108 | }, 109 | "version": "v1alpha1", 110 | }, 111 | }, 112 | } 113 | e, err = NewCRDV1Beta1Executor(ExecutableCRDV1Beta1Config{ 114 | BaseRunner: *br, 115 | State: update, 116 | }) 117 | if err != nil { 118 | t.Fatalf( 119 | "Failed to construct crd v1beta1 executor: %v", 120 | err, 121 | ) 122 | } 123 | 124 | result, err = e.Apply() 125 | if err != nil { 126 | t.Fatalf( 127 | "Error while testing update CRD via apply: %v: %s", 128 | err, 129 | result, 130 | ) 131 | } 132 | if result.Phase != types.ApplyStatusPassed { 133 | t.Fatalf("Test failed while updating CRD via apply: %s", result) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /pkg/recipe/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | // DiscoveryError defines a discovery error 20 | type DiscoveryError struct { 21 | Err string 22 | } 23 | 24 | // Error implements error interface 25 | func (e *DiscoveryError) Error() string { 26 | return e.Err 27 | } 28 | -------------------------------------------------------------------------------- /pkg/recipe/fixture_test.go: -------------------------------------------------------------------------------- 1 | // +build !integration 2 | 3 | /* 4 | Copyright 2020 The MayaData Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package recipe 20 | 21 | import ( 22 | "testing" 23 | 24 | "k8s.io/client-go/rest" 25 | ) 26 | 27 | func TestNewFixture(t *testing.T) { 28 | var tests = map[string]struct { 29 | kubeConfig *rest.Config 30 | isErr bool 31 | }{ 32 | "nil kubeconfig": { 33 | isErr: true, 34 | }, 35 | "empty kubeconfig": { 36 | kubeConfig: &rest.Config{}, 37 | }, 38 | } 39 | for name, mock := range tests { 40 | name := name 41 | mock := mock 42 | t.Run(name, func(t *testing.T) { 43 | _, err := NewFixture(FixtureConfig{ 44 | KubeConfig: mock.kubeConfig, 45 | }) 46 | if mock.isErr && err == nil { 47 | t.Fatal("Expected error got none") 48 | } 49 | if !mock.isErr && err != nil { 50 | t.Fatalf("Expected no error got %s", err.Error()) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/recipe/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/pkg/errors" 23 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | 26 | types "mayadata.io/d-operators/types/recipe" 27 | ) 28 | 29 | // Gettable helps fetching desired state from the cluster 30 | type Gettable struct { 31 | BaseRunner 32 | Get *types.Get 33 | 34 | result *types.GetResult 35 | err error 36 | } 37 | 38 | // GettableConfig helps in creating new instance of Gettable 39 | type GettableConfig struct { 40 | BaseRunner 41 | Get *types.Get 42 | } 43 | 44 | // NewGetter returns a new instance of Gettable 45 | func NewGetter(config GettableConfig) *Gettable { 46 | return &Gettable{ 47 | BaseRunner: config.BaseRunner, 48 | Get: config.Get, 49 | result: &types.GetResult{}, 50 | } 51 | } 52 | 53 | func (g *Gettable) getCRD() (*types.GetResult, error) { 54 | var crd *v1beta1.CustomResourceDefinition 55 | err := UnstructToTyped(g.Get.State, &crd) 56 | if err != nil { 57 | return nil, errors.Wrapf( 58 | err, 59 | "Failed to transform unstruct instance to crd equivalent", 60 | ) 61 | } 62 | // use crd client to get crds 63 | obj, err := g.crdClient. 64 | CustomResourceDefinitions(). 65 | Get(g.Get.State.GetName(), metav1.GetOptions{}) 66 | if err != nil { 67 | return nil, errors.Wrapf( 68 | err, 69 | "Failed to get crd %q", 70 | g.Get.State.GetName(), 71 | ) 72 | } 73 | return &types.GetResult{ 74 | Phase: types.GetStatusPassed, 75 | Message: fmt.Sprintf( 76 | "Get CRD: Kind %s: APIVersion %s: Name %s", 77 | crd.Spec.Names.Singular, 78 | crd.Spec.Group+"/"+crd.Spec.Version, 79 | g.Get.State.GetName(), 80 | ), 81 | V1Beta1CRD: obj, 82 | }, nil 83 | } 84 | 85 | func (g *Gettable) getResource() (*types.GetResult, error) { 86 | var message = fmt.Sprintf( 87 | "Get resource with %s / %s: GVK %s", 88 | g.Get.State.GetNamespace(), 89 | g.Get.State.GetName(), 90 | g.Get.State.GroupVersionKind(), 91 | ) 92 | client, err := g.GetClientForAPIVersionAndKind( 93 | g.Get.State.GetAPIVersion(), 94 | g.Get.State.GetKind(), 95 | ) 96 | if err != nil { 97 | return nil, errors.Wrapf( 98 | err, 99 | "Failed to get resource client", 100 | ) 101 | } 102 | obj, err := client. 103 | Namespace(g.Get.State.GetNamespace()). 104 | Get(g.Get.State.GetName(), metav1.GetOptions{}) 105 | if err != nil { 106 | return nil, errors.Wrapf( 107 | err, 108 | "Failed to get resource", 109 | ) 110 | } 111 | return &types.GetResult{ 112 | Phase: types.GetStatusPassed, 113 | Message: message, 114 | Object: obj, 115 | }, nil 116 | } 117 | 118 | // Run executes applying the desired state against the 119 | // cluster 120 | func (g *Gettable) Run() (*types.GetResult, error) { 121 | if g.Get.State.GetKind() == "CustomResourceDefinition" { 122 | // get CRD 123 | return g.getCRD() 124 | } 125 | return g.getResource() 126 | } 127 | -------------------------------------------------------------------------------- /pkg/recipe/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/pkg/errors" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/labels" 25 | "openebs.io/metac/dynamic/clientset" 26 | 27 | types "mayadata.io/d-operators/types/recipe" 28 | ) 29 | 30 | // Listable helps listing desired state from the cluster 31 | type Listable struct { 32 | BaseRunner 33 | List *types.List 34 | 35 | result *types.ListResult 36 | err error 37 | } 38 | 39 | // ListableConfig helps in creating new instance of Listable 40 | type ListableConfig struct { 41 | BaseRunner 42 | List *types.List 43 | } 44 | 45 | // NewLister returns a new instance of Listable 46 | func NewLister(config ListableConfig) *Listable { 47 | return &Listable{ 48 | BaseRunner: config.BaseRunner, 49 | List: config.List, 50 | result: &types.ListResult{}, 51 | } 52 | } 53 | 54 | func (l *Listable) listResources() (*types.ListResult, error) { 55 | var message = fmt.Sprintf( 56 | "List resources with %s / %s: GVK %s", 57 | l.List.State.GetNamespace(), 58 | l.List.State.GetName(), 59 | l.List.State.GroupVersionKind(), 60 | ) 61 | 62 | var client *clientset.ResourceClient 63 | var err error 64 | 65 | // --- 66 | // Retry in-case resource client is not yet 67 | // discovered 68 | // --- 69 | err = l.Retry.Waitf( 70 | func() (bool, error) { 71 | client, err = l.GetClientForAPIVersionAndKind( 72 | l.List.State.GetAPIVersion(), 73 | l.List.State.GetKind(), 74 | ) 75 | return err == nil, err 76 | }, 77 | message, 78 | ) 79 | if err != nil { 80 | return nil, errors.Wrapf( 81 | err, 82 | "Failed to get resource client", 83 | ) 84 | } 85 | items, err := client. 86 | Namespace(l.List.State.GetNamespace()). 87 | List(metav1.ListOptions{ 88 | LabelSelector: labels.Set( 89 | l.List.State.GetLabels(), 90 | ).String(), 91 | }) 92 | if err != nil { 93 | return nil, errors.Wrapf( 94 | err, 95 | "Failed to list resources", 96 | ) 97 | } 98 | return &types.ListResult{ 99 | Phase: types.ListStatusPassed, 100 | Message: message, 101 | Items: items, 102 | }, nil 103 | } 104 | 105 | // Run executes applying the desired state against the 106 | // cluster 107 | func (l *Listable) Run() (*types.ListResult, error) { 108 | return l.listResources() 109 | } 110 | -------------------------------------------------------------------------------- /pkg/recipe/noop_test.go: -------------------------------------------------------------------------------- 1 | // +build !integration 2 | 3 | /* 4 | Copyright 2020 The MayaData Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package recipe 20 | 21 | import ( 22 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 | "k8s.io/apimachinery/pkg/runtime" 24 | "k8s.io/apimachinery/pkg/runtime/schema" 25 | dynamicfake "k8s.io/client-go/dynamic/fake" 26 | "openebs.io/metac/dynamic/clientset" 27 | dynamicdiscovery "openebs.io/metac/dynamic/discovery" 28 | ) 29 | 30 | // NoopFixture is a BaseFixture instance useful for unit testing 31 | var NoopFixture = &BaseFixture{ 32 | // get a dynamic resource client that does nothing 33 | getClientForAPIVersionAndKindFn: func( 34 | apiversion string, 35 | kind string, 36 | ) (*clientset.ResourceClient, error) { 37 | di := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme()) 38 | nri := di.Resource(schema.GroupVersionResource{}) 39 | // we don't care about namespace 40 | ri := nri.Namespace("") 41 | return &clientset.ResourceClient{ 42 | ResourceInterface: ri, 43 | // this is a noop api resource 44 | APIResource: &dynamicdiscovery.APIResource{}, 45 | }, nil 46 | }, 47 | 48 | // get a dynamic resource client that does nothing 49 | getClientForAPIVersionAndResourceFn: func( 50 | apiversion string, 51 | resource string, 52 | ) (*clientset.ResourceClient, error) { 53 | di := &dynamicfake.FakeDynamicClient{} 54 | nri := di.Resource(schema.GroupVersionResource{}) 55 | // we don't care about namespace 56 | ri := nri.Namespace("") 57 | return &clientset.ResourceClient{ 58 | ResourceInterface: ri, 59 | // this is a noop api resource 60 | APIResource: &dynamicdiscovery.APIResource{}, 61 | }, nil 62 | }, 63 | 64 | // get a noop api resource 65 | getAPIForAPIVersionAndResourceFn: func( 66 | apiversion string, 67 | resource string, 68 | ) *dynamicdiscovery.APIResource { 69 | return &dynamicdiscovery.APIResource{} 70 | }, 71 | } 72 | 73 | // NoopConfigMapFixture is a BaseFixture instance useful for unit testing 74 | var NoopConfigMapFixture = &BaseFixture{ 75 | // get a fake dynamic client that is loaded with 76 | // a dummy config map 77 | getClientForAPIVersionAndKindFn: func( 78 | apiversion string, 79 | kind string, 80 | ) (*clientset.ResourceClient, error) { 81 | di := dynamicfake.NewSimpleDynamicClient( 82 | runtime.NewScheme(), 83 | // this fake dynamic client is loaded with this 84 | // dummy configmap 85 | &unstructured.Unstructured{ 86 | Object: map[string]interface{}{ 87 | "kind": "ConfigMap", 88 | "apiVersion": "v1", 89 | "metadata": map[string]interface{}{ 90 | "name": "cm-1", 91 | }, 92 | "spec": nil, 93 | }, 94 | }, 95 | ) 96 | nri := di.Resource(schema.GroupVersionResource{ 97 | Version: "v1", 98 | Resource: "configmaps", 99 | }) 100 | ri := nri.Namespace("") 101 | return &clientset.ResourceClient{ 102 | ResourceInterface: ri, 103 | APIResource: &dynamicdiscovery.APIResource{}, 104 | }, nil 105 | }, 106 | 107 | // get a fake dynamic client that is loaded with a 108 | // dummy config map 109 | getClientForAPIVersionAndResourceFn: func( 110 | apiversion string, 111 | resource string, 112 | ) (*clientset.ResourceClient, error) { 113 | di := dynamicfake.NewSimpleDynamicClient( 114 | runtime.NewScheme(), 115 | &unstructured.Unstructured{ 116 | Object: map[string]interface{}{ 117 | "kind": "ConfigMap", 118 | "apiVersion": "v1", 119 | "metadata": map[string]interface{}{ 120 | "name": "cm-1", 121 | }, 122 | "spec": nil, 123 | }, 124 | }, 125 | ) 126 | nri := di.Resource(schema.GroupVersionResource{ 127 | Version: "v1", 128 | Resource: "configmaps", 129 | }) 130 | ri := nri.Namespace("") 131 | return &clientset.ResourceClient{ 132 | ResourceInterface: ri, 133 | APIResource: &dynamicdiscovery.APIResource{}, 134 | }, nil 135 | }, 136 | 137 | // get a noop api resourece 138 | getAPIForAPIVersionAndResourceFn: func( 139 | apiversion string, 140 | resource string, 141 | ) *dynamicdiscovery.APIResource { 142 | return &dynamicdiscovery.APIResource{} 143 | }, 144 | } 145 | -------------------------------------------------------------------------------- /pkg/recipe/path_check_test.go: -------------------------------------------------------------------------------- 1 | // +build !integration 2 | 3 | package recipe 4 | 5 | import ( 6 | "testing" 7 | 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | types "mayadata.io/d-operators/types/recipe" 10 | ) 11 | 12 | func TestPathCheckingAssertValueString(t *testing.T) { 13 | var tests = map[string]struct { 14 | State *unstructured.Unstructured 15 | TaskName string 16 | PathCheck string 17 | Path string 18 | Value string 19 | retryIfValueEquals bool 20 | retryIfValueNotEquals bool 21 | IsError bool 22 | ExpectedAssert bool 23 | }{ 24 | "assert ipaddress None == None": { 25 | State: &unstructured.Unstructured{ 26 | Object: map[string]interface{}{ 27 | "apiVersion": "v1", 28 | "kind": "Service", 29 | "metadata": map[string]interface{}{ 30 | "name": "test", 31 | "namespace": "default", 32 | }, 33 | "spec": map[string]interface{}{ 34 | "ipAddress": "None", 35 | }, 36 | }, 37 | }, 38 | Path: "spec.ipAddress", 39 | Value: "None", 40 | retryIfValueNotEquals: true, 41 | ExpectedAssert: true, 42 | }, 43 | "assert ipaddress ip != 12.123.12.11": { 44 | State: &unstructured.Unstructured{ 45 | Object: map[string]interface{}{ 46 | "apiVersion": "v1", 47 | "kind": "Service", 48 | "metadata": map[string]interface{}{ 49 | "name": "test", 50 | "namespace": "default", 51 | }, 52 | "spec": map[string]interface{}{ 53 | "ipAddress": "12.123.12.11", 54 | }, 55 | }, 56 | }, 57 | Path: "spec.ipAddress", 58 | Value: "None", 59 | retryIfValueEquals: true, 60 | ExpectedAssert: true, 61 | }, 62 | "assert ipaddress ip != ": { 63 | State: &unstructured.Unstructured{ 64 | Object: map[string]interface{}{ 65 | "apiVersion": "v1", 66 | "kind": "Service", 67 | "metadata": map[string]interface{}{ 68 | "name": "test", 69 | "namespace": "default", 70 | }, 71 | "spec": map[string]interface{}{ 72 | "ipAddress": "12.123.12.11", 73 | }, 74 | }, 75 | }, 76 | Path: "spec.ipAddress", 77 | Value: "", 78 | retryIfValueEquals: true, 79 | ExpectedAssert: true, 80 | }, 81 | "assert ipaddress Nil != None": { 82 | State: &unstructured.Unstructured{ 83 | Object: map[string]interface{}{ 84 | "apiVersion": "v1", 85 | "kind": "Service", 86 | "metadata": map[string]interface{}{ 87 | "name": "test", 88 | "namespace": "default", 89 | }, 90 | "spec": map[string]interface{}{ 91 | "ipAddress": "None", 92 | }, 93 | }, 94 | }, 95 | Path: "spec.ipAddress", 96 | Value: "Nil", 97 | retryIfValueNotEquals: true, 98 | ExpectedAssert: false, 99 | }, 100 | } 101 | for scenario, tObj := range tests { 102 | scenario := scenario 103 | tObj := tObj 104 | t.Run(scenario, func(t *testing.T) { 105 | pc := &PathChecking{ 106 | PathCheck: types.PathCheck{ 107 | Path: tObj.Path, 108 | Value: tObj.Value, 109 | }, 110 | retryIfValueEquals: tObj.retryIfValueEquals, 111 | retryIfValueNotEquals: tObj.retryIfValueNotEquals, 112 | result: &types.PathCheckResult{}, 113 | } 114 | got, err := pc.assertValueString(tObj.State) 115 | if tObj.IsError && err == nil { 116 | t.Fatalf("Expected error got none") 117 | } 118 | if !tObj.IsError && err != nil { 119 | t.Fatalf("Expected no error got %s", err.Error()) 120 | } 121 | if tObj.IsError { 122 | return 123 | } 124 | if got != tObj.ExpectedAssert { 125 | t.Fatalf("Expected assert = %t got %t", tObj.ExpectedAssert, got) 126 | } 127 | 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /pkg/recipe/recipe_int_list_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | /* 4 | Copyright 2020 The MayaData Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package recipe 20 | 21 | import ( 22 | "testing" 23 | 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | "mayadata.io/d-operators/common/pointer" 26 | types "mayadata.io/d-operators/types/recipe" 27 | ) 28 | 29 | func TestListSimpleRun(t *testing.T) { 30 | tasks := []types.Task{ 31 | { 32 | Name: "create-ns", 33 | Create: &types.Create{ 34 | State: &unstructured.Unstructured{ 35 | Object: map[string]interface{}{ 36 | "apiVersion": "v1", 37 | "kind": "Namespace", 38 | "metadata": map[string]interface{}{ 39 | "name": "list-simple-integration-testing", 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | { 46 | Name: "create-configmap", 47 | Create: &types.Create{ 48 | State: &unstructured.Unstructured{ 49 | Object: map[string]interface{}{ 50 | "apiVersion": "v1", 51 | "kind": "ConfigMap", 52 | "metadata": map[string]interface{}{ 53 | "name": "cm-one", 54 | "namespace": "list-simple-integration-testing", 55 | "labels": map[string]interface{}{ 56 | "ns": "list-simple-integration-testing", 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | { 64 | Name: "create-configmap-2", 65 | Create: &types.Create{ 66 | State: &unstructured.Unstructured{ 67 | Object: map[string]interface{}{ 68 | "apiVersion": "v1", 69 | "kind": "ConfigMap", 70 | "metadata": map[string]interface{}{ 71 | "name": "cm-two", 72 | "namespace": "list-simple-integration-testing", 73 | "labels": map[string]interface{}{ 74 | "ns": "list-simple-integration-testing", 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | }, 81 | { 82 | Name: "assert-ns", 83 | Assert: &types.Assert{ 84 | State: &unstructured.Unstructured{ 85 | Object: map[string]interface{}{ 86 | "apiVersion": "v1", 87 | "kind": "Namespace", 88 | "metadata": map[string]interface{}{ 89 | "name": "list-simple-integration-testing", 90 | }, 91 | }, 92 | }, 93 | }, 94 | }, 95 | { 96 | Name: "assert-configmap", 97 | Assert: &types.Assert{ 98 | State: &unstructured.Unstructured{ 99 | Object: map[string]interface{}{ 100 | "apiVersion": "v1", 101 | "kind": "ConfigMap", 102 | "metadata": map[string]interface{}{ 103 | "name": "cm-one", 104 | "namespace": "list-simple-integration-testing", 105 | }, 106 | }, 107 | }, 108 | }, 109 | }, 110 | { 111 | Name: "assert-configmap-list", 112 | Assert: &types.Assert{ 113 | State: &unstructured.Unstructured{ 114 | Object: map[string]interface{}{ 115 | "apiVersion": "v1", 116 | "kind": "ConfigMap", 117 | "metadata": map[string]interface{}{ 118 | "namespace": "list-simple-integration-testing", 119 | "labels": map[string]interface{}{ 120 | "ns": "list-simple-integration-testing", 121 | }, 122 | }, 123 | }, 124 | }, 125 | StateCheck: &types.StateCheck{ 126 | Operator: types.StateCheckOperatorListCountEquals, 127 | Count: pointer.Int(2), 128 | }, 129 | }, 130 | }, 131 | } 132 | recipe := types.Recipe{ 133 | Spec: types.RecipeSpec{ 134 | Tasks: tasks, 135 | }, 136 | } 137 | runner, err := NewNonCustomResourceRunnerWithOptions( 138 | "list-simple-integration-testing", 139 | recipe, 140 | NonCustomResourceRunnerOption{ 141 | SingleTry: true, 142 | Teardown: true, 143 | }, 144 | ) 145 | if err != nil { 146 | t.Fatalf( 147 | "Failed to create kubernetes runner: %v", 148 | err, 149 | ) 150 | } 151 | result, err := runner.RunWithoutLocking() 152 | if err != nil { 153 | t.Fatalf("Error while testing: %v: %s", err, result) 154 | } 155 | if !(result.Phase == types.RecipeStatusCompleted || 156 | result.Phase == types.RecipeStatusPassed) { 157 | t.Fatalf("Test failed: %s", result) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /pkg/recipe/recipe_int_simple_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | /* 4 | Copyright 2020 The MayaData Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package recipe 20 | 21 | import ( 22 | "testing" 23 | 24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 | types "mayadata.io/d-operators/types/recipe" 26 | ) 27 | 28 | func TestRecipeSimpleRun(t *testing.T) { 29 | tasks := []types.Task{ 30 | { 31 | Name: "create-ns", 32 | Create: &types.Create{ 33 | State: &unstructured.Unstructured{ 34 | Object: map[string]interface{}{ 35 | "apiVersion": "v1", 36 | "kind": "Namespace", 37 | "metadata": map[string]interface{}{ 38 | "name": "recipe-integration-testing-simple", 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | { 45 | Name: "create-configmap", 46 | Create: &types.Create{ 47 | State: &unstructured.Unstructured{ 48 | Object: map[string]interface{}{ 49 | "apiVersion": "v1", 50 | "kind": "ConfigMap", 51 | "metadata": map[string]interface{}{ 52 | "name": "cm-one", 53 | "namespace": "recipe-integration-testing-simple", 54 | "labels": map[string]interface{}{ 55 | "common": "true", 56 | "cm-one": "true", 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | { 64 | Name: "apply-ns", 65 | Apply: &types.Apply{ 66 | State: &unstructured.Unstructured{ 67 | Object: map[string]interface{}{ 68 | "apiVersion": "v1", 69 | "kind": "Namespace", 70 | "metadata": map[string]interface{}{ 71 | "name": "recipe-integration-testing-simple", 72 | "labels": map[string]interface{}{ 73 | "new": "new", 74 | }, 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | { 81 | Name: "apply-configmap", 82 | Apply: &types.Apply{ 83 | State: &unstructured.Unstructured{ 84 | Object: map[string]interface{}{ 85 | "apiVersion": "v1", 86 | "kind": "ConfigMap", 87 | "metadata": map[string]interface{}{ 88 | "name": "cm-one", 89 | "namespace": "recipe-integration-testing-simple", 90 | "labels": map[string]interface{}{ 91 | "cm-new": "new", 92 | }, 93 | }, 94 | }, 95 | }, 96 | }, 97 | }, 98 | { 99 | Name: "assert-ns", 100 | Assert: &types.Assert{ 101 | State: &unstructured.Unstructured{ 102 | Object: map[string]interface{}{ 103 | "apiVersion": "v1", 104 | "kind": "Namespace", 105 | "metadata": map[string]interface{}{ 106 | "name": "recipe-integration-testing-simple", 107 | "labels": map[string]interface{}{ 108 | "new": "new", 109 | }, 110 | }, 111 | }, 112 | }, 113 | }, 114 | }, 115 | { 116 | Name: "assert-configmap", 117 | Assert: &types.Assert{ 118 | State: &unstructured.Unstructured{ 119 | Object: map[string]interface{}{ 120 | "apiVersion": "v1", 121 | "kind": "ConfigMap", 122 | "metadata": map[string]interface{}{ 123 | "name": "cm-one", 124 | "namespace": "recipe-integration-testing-simple", 125 | "labels": map[string]interface{}{ 126 | "common": "true", 127 | "cm-one": "true", 128 | "cm-new": "new", 129 | }, 130 | }, 131 | }, 132 | }, 133 | }, 134 | }, 135 | } 136 | recipe := types.Recipe{ 137 | Spec: types.RecipeSpec{ 138 | Tasks: tasks, 139 | }, 140 | } 141 | runner, err := NewNonCustomResourceRunnerWithOptions( 142 | "integration-testing-simple-recipe", 143 | recipe, 144 | NonCustomResourceRunnerOption{ 145 | SingleTry: true, 146 | Teardown: true, 147 | }, 148 | ) 149 | if err != nil { 150 | t.Fatalf( 151 | "Failed to create kubernetes runner: %v", 152 | err, 153 | ) 154 | } 155 | result, err := runner.RunWithoutLocking() 156 | if err != nil { 157 | t.Fatalf("Error while testing: %v: %s", err, result) 158 | } 159 | if !(result.Phase == types.RecipeStatusCompleted || 160 | result.Phase == types.RecipeStatusPassed) { 161 | t.Fatalf("Test failed: %s", result) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /pkg/recipe/retry.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | 23 | "k8s.io/klog/v2" 24 | ) 25 | 26 | // RetryTimeout is an error implementation that is thrown 27 | // when retry fails post timeout 28 | type RetryTimeout struct { 29 | Err string 30 | } 31 | 32 | // Error implements error interface 33 | func (rt *RetryTimeout) Error() string { 34 | return rt.Err 35 | } 36 | 37 | // Retryable helps executing user provided functions as 38 | // conditions in a repeated manner till this condition succeeds 39 | type Retryable struct { 40 | Message string 41 | 42 | WaitTimeout time.Duration 43 | WaitInterval time.Duration 44 | } 45 | 46 | // RetryConfig helps in creating an instance of Retryable 47 | type RetryConfig struct { 48 | WaitTimeout *time.Duration 49 | WaitInterval *time.Duration 50 | } 51 | 52 | // NewRetry returns a new instance of Retryable 53 | func NewRetry(config RetryConfig) *Retryable { 54 | var timeout, interval time.Duration 55 | if config.WaitTimeout != nil { 56 | timeout = *config.WaitTimeout 57 | } else { 58 | timeout = 60 * time.Second 59 | } 60 | if config.WaitInterval != nil { 61 | interval = *config.WaitInterval 62 | } else { 63 | interval = 1 * time.Second 64 | } 65 | return &Retryable{ 66 | WaitTimeout: timeout, 67 | WaitInterval: interval, 68 | } 69 | } 70 | 71 | // Waitf retries this provided function as a condition till 72 | // this condition succeeds. 73 | // 74 | // Clients invoking this method need to return appropriate 75 | // values in the function implementation to let this function 76 | // to be either returned, or exited or retried. 77 | func (r *Retryable) Waitf( 78 | condition func() (bool, error), 79 | message string, 80 | args ...interface{}, 81 | ) error { 82 | context := fmt.Sprintf( 83 | message, 84 | args..., 85 | ) 86 | // mark the start time 87 | start := time.Now() 88 | for { 89 | done, err := condition() 90 | if err == nil && done { 91 | klog.V(3).Infof( 92 | "Retryable condition succeeded: %s", context, 93 | ) 94 | return nil 95 | } 96 | if err != nil && done { 97 | klog.V(3).Infof( 98 | "Retryable condition completed with error: %s: %s", 99 | context, 100 | err, 101 | ) 102 | return err 103 | } 104 | if time.Since(start) > r.WaitTimeout { 105 | var errmsg = "No errors found" 106 | if err != nil { 107 | errmsg = fmt.Sprintf("%+v", err) 108 | } 109 | return &RetryTimeout{ 110 | fmt.Sprintf( 111 | "Retryable condition timed out after %s: %s: %s", 112 | r.WaitTimeout, 113 | context, 114 | errmsg, 115 | ), 116 | } 117 | } 118 | if err != nil { 119 | // Log error, but keep trying until timeout 120 | klog.V(3).Infof( 121 | "Retryable condition has errors: Will retry: %s: %s", 122 | context, 123 | err, 124 | ) 125 | } else { 126 | klog.V(3).Infof( 127 | "Waiting for retryable condition to succeed: Will retry: %s", 128 | context, 129 | ) 130 | } 131 | time.Sleep(r.WaitInterval) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /pkg/recipe/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package recipe 18 | 19 | import ( 20 | "github.com/pkg/errors" 21 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | ) 24 | 25 | // UnstructToTyped transforms the provided unstruct instance 26 | // to target type 27 | func UnstructToTyped( 28 | src *unstructured.Unstructured, 29 | target interface{}, 30 | ) error { 31 | if src == nil || src.UnstructuredContent() == nil { 32 | return errors.Errorf( 33 | "Failed to transform unstruct to typed: Nil unstruct", 34 | ) 35 | } 36 | if target == nil { 37 | return errors.Errorf( 38 | "Failed to transform unstruct to typed: Nil target", 39 | ) 40 | } 41 | return runtime.DefaultUnstructuredConverter.FromUnstructured( 42 | src.UnstructuredContent(), 43 | target, 44 | ) 45 | } 46 | 47 | // TypedToUnstruct transforms the provided typed instance 48 | // to unstructured instance 49 | func TypedToUnstruct( 50 | typed interface{}, 51 | ) (*unstructured.Unstructured, error) { 52 | if typed == nil { 53 | return nil, errors.Errorf( 54 | "Failed to transform typed to unstruct: Nil typed", 55 | ) 56 | } 57 | got, err := runtime.DefaultUnstructuredConverter.ToUnstructured( 58 | typed, 59 | ) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return &unstructured.Unstructured{ 64 | Object: got, 65 | }, nil 66 | } 67 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "branches": ["master"], 3 | "plugins": [ 4 | '@semantic-release/commit-analyzer', 5 | '@semantic-release/release-notes-generator', 6 | '@semantic-release/github', 7 | ] 8 | } -------------------------------------------------------------------------------- /test/declarative/BEST_PRACTICES.md: -------------------------------------------------------------------------------- 1 | ### Best Practices 2 | Following are some of the best practices to write an E to E experiment: 3 | - An experiment yaml file can have one or more `kind: Recipe` custom resource(s) 4 | - An experiment yaml file should be built to execute exactly one scenario 5 | - A Recipe can depend on another Recipe via former's `spec.eligible` field 6 | - An experiment yaml file may have only one Recipe with below label: 7 | - `d-testing.dope.mayadata.io/inference: "true"` 8 | - This label enables the Recipe to be considered by `inference.yaml` 9 | - `inference.yaml` is used to decide if the ci passed or failed 10 | - `inference.yaml` needs to be modified with every new test scenario 11 | - `inference.yaml` can be used to include `failed`, `error`, `positive`, etc. scenarios 12 | - Experiment yamls meant to run negative testing can be suffixed with `-neg` 13 | - Where `neg` stands for negative 14 | - A negative test experiment will have its `status.phase` set with `Failed` by dope controller 15 | - Presence of one or more failed test experiments will not fail the E to E 16 | - `inference.yaml` needs to be updated to include failed test cases 17 | - Presence of one or more error test experiments will not fail the E to E 18 | - `inference.yaml` needs to be updated to include error test cases -------------------------------------------------------------------------------- /test/declarative/experiments/assert-deprecated-daemonset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dope.mayadata.io/v1 2 | kind: Recipe 3 | metadata: 4 | name: assert-absence-of-deprecated-daemonsets 5 | namespace: d-testing 6 | labels: 7 | d-testing.dope.mayadata.io/inference: "true" 8 | spec: 9 | tasks: 10 | - name: assert-daemonset-with-extensions-v1beta1 11 | failFast: 12 | when: OnDiscoveryError 13 | ignoreError: AsWarning # TODO (refactor) 14 | assert: 15 | state: 16 | kind: DaemonSet 17 | apiVersion: extensions/v1beta1 18 | stateCheck: 19 | stateCheckOperator: ListCountEquals 20 | count: 0 21 | --- -------------------------------------------------------------------------------- /test/declarative/experiments/assert-github-search-invalid-method-neg.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dope.mayadata.io/v1 2 | kind: Recipe 3 | metadata: 4 | name: assert-github-search-with-invalid-method-neg 5 | namespace: d-testing 6 | spec: 7 | tasks: 8 | # Start by applying this HTTP resource 9 | - name: apply-github-search-with-invalid-method-neg 10 | apply: 11 | state: 12 | apiVersion: dope.mayadata.io/v1 13 | kind: HTTP 14 | metadata: 15 | name: github-search-with-invalid-method-neg 16 | namespace: d-testing 17 | spec: 18 | url: https://github.com/search 19 | # This is an invalid value 20 | method: GETT 21 | # Then assert status of this HTTP resource 22 | - name: assert-github-search-with-invalid-method-neg 23 | assert: 24 | state: 25 | apiVersion: dope.mayadata.io/v1 26 | kind: HTTP 27 | metadata: 28 | name: github-search-with-invalid-method-neg 29 | namespace: d-testing 30 | status: 31 | # Phase of this HTTP resource will never be Completed 32 | # It will have Error instead 33 | # 34 | # This is a negative test case 35 | # In other words this Recipe will Fail 36 | phase: Completed 37 | --- -------------------------------------------------------------------------------- /test/declarative/experiments/assert-github-search-invalid-method.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dope.mayadata.io/v1 2 | kind: Recipe 3 | metadata: 4 | name: assert-github-search-with-invalid-method 5 | namespace: d-testing 6 | spec: 7 | tasks: 8 | # Start by applying this HTTP custom resource 9 | - name: apply-github-search-with-invalid-method 10 | apply: 11 | state: 12 | apiVersion: dope.mayadata.io/v1 13 | kind: HTTP 14 | metadata: 15 | name: github-search-with-invalid-method 16 | namespace: d-testing 17 | spec: 18 | url: https://github.com/search 19 | # Method is set to an invalid value 20 | method: GETT 21 | # Then assert this HTTP resource 22 | - name: assert-github-search-with-invalid-method 23 | assert: 24 | state: 25 | apiVersion: dope.mayadata.io/v1 26 | kind: HTTP 27 | metadata: 28 | name: github-search-with-invalid-method 29 | namespace: d-testing 30 | status: 31 | # This HTTP resource is expected to Error 32 | # since its method is set to an invalid value 33 | # 34 | # NOTE: 35 | # The Recipe will complete successfully since 36 | # this assertion is correct 37 | phase: Error 38 | --- -------------------------------------------------------------------------------- /test/declarative/experiments/assert-lock-persists-for-recipe-that-runs-once.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dope.mayadata.io/v1 2 | kind: Recipe 3 | metadata: 4 | name: assert-lock-persists-for-recipe-that-runs-once 5 | namespace: d-testing 6 | labels: 7 | d-testing.dope.mayadata.io/inference: "true" 8 | spec: 9 | tasks: 10 | - name: create-a-recipe-that-runs-only-once 11 | create: 12 | state: 13 | kind: Recipe 14 | apiVersion: dope.mayadata.io/v1 15 | metadata: 16 | name: recipe-that-runs-only-once 17 | namespace: d-testing 18 | spec: 19 | tasks: 20 | - name: assert-presence-of-namespace 21 | assert: 22 | state: 23 | kind: Namespace 24 | apiVersion: v1 25 | metadata: 26 | name: d-testing 27 | - name: assert-completion-of-recipe 28 | assert: 29 | state: 30 | kind: Recipe 31 | apiVersion: dope.mayadata.io/v1 32 | metadata: 33 | name: recipe-that-runs-only-once 34 | namespace: d-testing 35 | status: 36 | phase: Completed 37 | - name: assert-presence-of-lock 38 | assert: 39 | state: 40 | kind: ConfigMap 41 | apiVersion: v1 42 | metadata: 43 | name: recipe-that-runs-only-once-lock 44 | namespace: d-testing 45 | --- -------------------------------------------------------------------------------- /test/declarative/experiments/assert-recipe-invalid-schema.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dope.mayadata.io/v1 2 | kind: Recipe 3 | metadata: 4 | name: create-an-invalid-recipe 5 | namespace: d-testing 6 | spec: 7 | tasks: 8 | - name: create-an-invalid-recipe 9 | create: 10 | state: 11 | kind: Recipe 12 | apiVersion: dope.mayadata.io/v1 13 | metadata: 14 | name: i-am-an-invalid-recipe 15 | namespace: d-testing 16 | labels: 17 | recipe/name: i-am-an-invalid-recipe 18 | spec: 19 | iDontExist: none # invalid field 20 | tasks: 21 | - name: assert-a-namespace 22 | assert: 23 | state: 24 | kind: Namespace 25 | apiVersion: v1 26 | metadata: 27 | name: d-testing 28 | --- 29 | apiVersion: dope.mayadata.io/v1 30 | kind: Recipe 31 | metadata: 32 | name: assert-the-invalid-recipe 33 | namespace: d-testing 34 | labels: 35 | d-testing.dope.mayadata.io/inference: "true" 36 | spec: 37 | eligible: 38 | checks: 39 | - labelSelector: 40 | matchLabels: 41 | recipe/name: i-am-an-invalid-recipe 42 | matchExpressions: 43 | - key: recipe.dope.mayadata.io/phase 44 | operator: Exists 45 | when: ListCountEquals 46 | count: 1 47 | resync: 48 | onNotEligibleResyncInSeconds: 5 49 | tasks: 50 | - name: assert-recipe-is-invalid 51 | assert: 52 | state: 53 | kind: Recipe 54 | apiVersion: dope.mayadata.io/v1 55 | metadata: 56 | name: i-am-an-invalid-recipe 57 | namespace: d-testing 58 | status: 59 | phase: InvalidSchema 60 | --- -------------------------------------------------------------------------------- /test/declarative/experiments/assert-recipe-lock-is-garbage-collected.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dope.mayadata.io/v1 2 | kind: Recipe 3 | metadata: 4 | name: create-recipe-to-verify-its-lock-is-garbage-collected 5 | namespace: d-testing 6 | labels: 7 | create-recipe-to-verify-its-lock-is-garbage-collected: "true" 8 | spec: 9 | tasks: 10 | - name: create-a-recipe 11 | create: 12 | state: 13 | kind: Recipe 14 | apiVersion: dope.mayadata.io/v1 15 | metadata: 16 | name: my-lock-is-garbage-collected 17 | namespace: d-testing 18 | spec: 19 | tasks: 20 | - name: assert-a-namespace 21 | assert: 22 | state: 23 | kind: Namespace 24 | apiVersion: v1 25 | metadata: 26 | name: d-testing 27 | --- 28 | apiVersion: dope.mayadata.io/v1 29 | kind: Recipe 30 | metadata: 31 | name: delete-recipe-to-verify-its-lock-garbage-collection 32 | namespace: d-testing 33 | labels: 34 | delete-recipe-to-verify-its-lock-garbage-collection: "true" 35 | spec: 36 | # This Recipe is eligible to run only when the checks succeed 37 | # 38 | # NOTE: 39 | # In this case, this Recipe will be eligible only after the 40 | # number of Recipes with matching labels equal the given count 41 | eligible: 42 | checks: 43 | - labelSelector: 44 | matchLabels: 45 | create-recipe-to-verify-its-lock-is-garbage-collected: "true" 46 | recipe.dope.mayadata.io/phase: Completed 47 | when: ListCountEquals 48 | count: 1 49 | resync: 50 | onNotEligibleResyncInSeconds: 5 51 | tasks: 52 | - name: assert-presence-of-recipe-lock 53 | assert: 54 | stateCheck: 55 | stateCheckOperator: ListCountEquals 56 | count: 1 57 | state: 58 | kind: ConfigMap 59 | apiVersion: v1 60 | metadata: 61 | namespace: d-testing 62 | labels: 63 | recipe.dope.mayadata.io/lock: "true" 64 | recipe.dope.mayadata.io/name: my-lock-is-garbage-collected 65 | - name: delete-the-recipe 66 | delete: 67 | state: 68 | kind: Recipe 69 | apiVersion: dope.mayadata.io/v1 70 | metadata: 71 | name: my-lock-is-garbage-collected 72 | namespace: d-testing 73 | --- 74 | apiVersion: dope.mayadata.io/v1 75 | kind: Recipe 76 | metadata: 77 | name: assert-recipe-to-verify-its-lock-garbage-collection 78 | namespace: d-testing 79 | labels: 80 | #d-testing.dope.mayadata.io/inference: "true" 81 | spec: 82 | # This Recipe is eligible to run only when the checks succeed 83 | # 84 | # NOTE: 85 | # In this case, this Recipe will be eligible only after the 86 | # number of ConfigMaps with matching labels equal the given count 87 | eligible: 88 | checks: 89 | - apiVersion: v1 90 | kind: ConfigMap 91 | labelSelector: 92 | matchLabels: 93 | recipe.dope.mayadata.io/lock: "true" 94 | recipe.dope.mayadata.io/name: my-lock-is-garbage-collected 95 | when: ListCountEquals 96 | count: 0 97 | resync: 98 | onNotEligibleResyncInSeconds: 5 99 | tasks: 100 | - name: re-assert-recipe-lock-was-deleted 101 | assert: 102 | stateCheck: 103 | stateCheckOperator: ListCountEquals 104 | count: 0 105 | state: 106 | kind: ConfigMap 107 | apiVersion: v1 108 | metadata: 109 | namespace: d-testing 110 | labels: 111 | recipe.dope.mayadata.io/lock: "true" 112 | recipe.dope.mayadata.io/name: my-lock-is-garbage-collected 113 | - name: assert-recipe-was-deleted 114 | assert: 115 | stateCheck: 116 | stateCheckOperator: NotFound 117 | state: 118 | kind: Recipe 119 | apiVersion: dope.mayadata.io/v1 120 | metadata: 121 | name: my-lock-is-garbage-collected 122 | namespace: d-testing 123 | --- -------------------------------------------------------------------------------- /test/declarative/experiments/assert-recipe-teardown.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dope.mayadata.io/v1 2 | kind: Recipe 3 | metadata: 4 | name: assert-recipe-teardown 5 | namespace: d-testing 6 | labels: 7 | d-testing.dope.mayadata.io/inference: "true" 8 | spec: 9 | tasks: 10 | - name: create-a-recipe-with-teardown-enabled 11 | create: 12 | state: 13 | kind: Recipe 14 | apiVersion: dope.mayadata.io/v1 15 | metadata: 16 | name: recipe-with-teardown-enabled 17 | namespace: d-testing 18 | spec: 19 | teardown: true # target under test 20 | tasks: 21 | - name: create-a-configmap 22 | create: 23 | state: 24 | kind: ConfigMap 25 | apiVersion: v1 26 | metadata: 27 | name: i-get-auto-deleted 28 | namespace: d-testing 29 | - name: assert-presence-of-configmap 30 | assert: 31 | state: 32 | kind: ConfigMap 33 | apiVersion: v1 34 | metadata: 35 | name: i-get-auto-deleted 36 | namespace: d-testing 37 | - name: assert-completion-of-recipe 38 | assert: 39 | state: 40 | kind: Recipe 41 | apiVersion: dope.mayadata.io/v1 42 | metadata: 43 | name: recipe-with-teardown-enabled 44 | namespace: d-testing 45 | status: 46 | phase: Completed 47 | - name: assert-absense-of-configmap 48 | assert: 49 | stateCheck: 50 | stateCheckOperator: NotFound 51 | state: 52 | kind: ConfigMap 53 | apiVersion: v1 54 | metadata: 55 | name: i-get-auto-deleted 56 | namespace: d-testing 57 | --- -------------------------------------------------------------------------------- /test/declarative/experiments/create-assert-fifty-configmaps-in-time.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dope.mayadata.io/v1 2 | kind: Recipe 3 | metadata: 4 | name: create-fifty-configmaps-in-time 5 | namespace: d-testing 6 | labels: 7 | i-create-50-configs: "true" 8 | i-am-tested-if-creation-of-configs-happen-in-time: "true" 9 | spec: 10 | tasks: 11 | # This task creates 50 config maps 12 | # The names of these config maps are suffixed 13 | # with numbers 14 | # 15 | # NOTE: 16 | # Following config maps will get created: 17 | # - create-cm-in-time-0, 18 | # - create-cm-in-time-1, 19 | # ... 20 | # - create-cm-in-time-48, 21 | # - create-cm-in-time-49 22 | # 23 | # NOTE: 24 | # Task will error out if any of these config maps 25 | # already exist in the cluster 26 | - name: create-fifty-configmaps 27 | create: 28 | state: 29 | kind: ConfigMap 30 | apiVersion: v1 31 | metadata: 32 | name: create-cm-in-time 33 | namespace: d-testing 34 | labels: 35 | "fifty-configs": "true" 36 | "i-should-not-take-much-time": "true" 37 | replicas: 50 38 | - name: assert-fifty-configmaps-were-created 39 | assert: 40 | state: 41 | kind: ConfigMap 42 | apiVersion: v1 43 | metadata: 44 | namespace: d-testing 45 | labels: 46 | "fifty-configs": "true" 47 | "i-should-not-take-much-time": "true" 48 | stateCheck: 49 | stateCheckOperator: ListCountEquals 50 | count: 50 51 | --- 52 | apiVersion: dope.mayadata.io/v1 53 | kind: Recipe 54 | metadata: 55 | name: assert-creation-of-fifty-configmaps-in-time 56 | namespace: d-testing 57 | labels: 58 | d-testing.dope.mayadata.io/inference: "true" 59 | spec: 60 | # This Recipe is eligible to run only when the checks succeed 61 | # 62 | # NOTE: 63 | # In this case, this Recipe will be eligible only after the 64 | # number of Recipes with matching labels equal the given count 65 | # 66 | # NOTE: 67 | # Eligibility check will get triggered after above think time 68 | # has elapsed 69 | eligible: 70 | checks: 71 | - labelSelector: 72 | matchLabels: 73 | i-create-50-configs: "true" 74 | i-am-tested-if-creation-of-configs-happen-in-time: "true" 75 | recipe.dope.mayadata.io/phase: Completed 76 | when: ListCountEquals 77 | count: 1 78 | resync: 79 | onNotEligibleResyncInSeconds: 5 80 | tasks: 81 | - name: assert-creation-of-fifty-configmaps-in-time 82 | assert: 83 | state: 84 | kind: Recipe 85 | apiVersion: dope.mayadata.io/v1 86 | metadata: 87 | name: create-fifty-configmaps-in-time 88 | namespace: d-testing 89 | pathCheck: 90 | path: status.executionTime.valueInSeconds 91 | pathCheckOperator: LTE 92 | # assert if the recipe to create 50 configmaps 93 | # completes within 30 seconds 94 | value: 30.0001 95 | dataType: float64 96 | --- -------------------------------------------------------------------------------- /test/declarative/experiments/create-assert-fifty-configmaps.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dope.mayadata.io/v1 2 | kind: Recipe 3 | metadata: 4 | name: create-and-assert-fifty-configmaps 5 | namespace: d-testing 6 | labels: 7 | d-testing.dope.mayadata.io/inference: "true" 8 | spec: 9 | tasks: 10 | - name: create-fifty-configmaps 11 | create: 12 | state: 13 | kind: ConfigMap 14 | apiVersion: v1 15 | metadata: 16 | name: create-cm 17 | namespace: d-testing 18 | replicas: 50 19 | - name: assert-presence-of-0th-configmap 20 | assert: 21 | state: 22 | kind: ConfigMap 23 | apiVersion: v1 24 | metadata: 25 | name: create-cm-0 26 | namespace: d-testing 27 | - name: assert-presence-of-49th-configmap 28 | assert: 29 | state: 30 | kind: ConfigMap 31 | apiVersion: v1 32 | metadata: 33 | name: create-cm-49 34 | namespace: d-testing 35 | --- -------------------------------------------------------------------------------- /test/declarative/experiments/crud-ops-on-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dope.mayadata.io/v1 2 | kind: Recipe 3 | metadata: 4 | name: crud-ops-on-pod 5 | namespace: d-testing 6 | labels: 7 | d-testing.dope.mayadata.io/inference: "true" 8 | spec: 9 | tasks: 10 | - name: apply-a-namespace 11 | apply: 12 | state: 13 | kind: Namespace 14 | apiVersion: v1 15 | metadata: 16 | name: my-ns 17 | - name: create-a-pod 18 | create: 19 | state: 20 | kind: Pod 21 | apiVersion: v1 22 | metadata: 23 | name: my-pod 24 | namespace: my-ns 25 | spec: 26 | containers: 27 | - name: web 28 | image: nginx 29 | - name: delete-the-pod 30 | delete: 31 | state: 32 | kind: Pod 33 | apiVersion: v1 34 | metadata: 35 | name: my-pod 36 | namespace: my-ns 37 | - name: delete-the-namespace 38 | delete: 39 | state: 40 | kind: Namespace 41 | apiVersion: v1 42 | metadata: 43 | name: my-ns 44 | --- -------------------------------------------------------------------------------- /test/declarative/inference.yaml: -------------------------------------------------------------------------------- 1 | # This is an internal recipe used by test suite runner 2 | # to evaluate if desired experiments (read Recipes) were 3 | # successful or not 4 | # 5 | # NOTE: 6 | # This Recipe evaluates number of successful, failed & 7 | # errored Recipes 8 | apiVersion: dope.mayadata.io/v1 9 | kind: Recipe 10 | metadata: 11 | name: inference 12 | namespace: d-testing 13 | labels: 14 | d-testing.dope.mayadata.io/internal: "true" 15 | spec: 16 | # This Recipe is eligible to run only when the checks succeed 17 | # 18 | # NOTE: 19 | # In this case, this Recipe will be eligible only after the 20 | # number of Recipes with matching labels equal the given count 21 | # 22 | # NOTE: 23 | # Eligibility check will get triggered after above think time 24 | # has elapsed 25 | eligible: 26 | checks: 27 | - labelSelector: 28 | matchLabels: 29 | d-testing.dope.mayadata.io/inference: "true" 30 | matchExpressions: 31 | - key: recipe.dope.mayadata.io/phase 32 | operator: Exists 33 | - key: recipe.dope.mayadata.io/phase 34 | operator: NotIn 35 | values: 36 | - NotEligible 37 | when: ListCountEquals 38 | count: 8 39 | resync: 40 | onNotEligibleResyncInSeconds: 5 41 | tasks: 42 | # Start with asserting the count of Recipes that should 43 | # complete successfully 44 | - name: assert-count-of-tests-that-completed 45 | assert: 46 | state: 47 | kind: Recipe 48 | apiVersion: dope.mayadata.io/v1 49 | metadata: 50 | namespace: d-testing 51 | labels: 52 | d-testing.dope.mayadata.io/inference: "true" 53 | recipe.dope.mayadata.io/phase: Completed 54 | stateCheck: 55 | stateCheckOperator: ListCountEquals 56 | # These many Recipes should succeed 57 | count: 8 58 | # Then assert the count of Recipes that should fail 59 | - name: assert-count-of-tests-that-failed 60 | assert: 61 | state: 62 | kind: Recipe 63 | apiVersion: dope.mayadata.io/v1 64 | metadata: 65 | namespace: d-testing 66 | labels: 67 | d-testing.dope.mayadata.io/inference: "true" 68 | recipe.dope.mayadata.io/phase: Failed 69 | stateCheck: 70 | stateCheckOperator: ListCountEquals 71 | # These many Recipes should fail 72 | count: 0 73 | # Then assert the count of Recipes that should error 74 | - name: assert-count-of-tests-that-errored 75 | assert: 76 | state: 77 | kind: Recipe 78 | apiVersion: dope.mayadata.io/v1 79 | metadata: 80 | namespace: d-testing 81 | labels: 82 | d-testing.dope.mayadata.io/inference: "true" 83 | recipe.dope.mayadata.io/phase: Error 84 | stateCheck: 85 | stateCheckOperator: ListCountEquals 86 | # These many Recipes should error out 87 | count: 0 88 | --- 89 | -------------------------------------------------------------------------------- /test/declarative/registries.yaml: -------------------------------------------------------------------------------- 1 | mirrors: 2 | docker.io: 3 | endpoint: 4 | - "http://localhost:5000" -------------------------------------------------------------------------------- /test/declarative/suite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cleanup() { 4 | set +e 5 | 6 | echo -e "\n Display $ctrlbin-0 logs" 7 | k3s kubectl logs -n $ctrlbin $ctrlbin-0 || true 8 | 9 | echo -e "\n Display status of experiment with name 'inference'" 10 | k3s kubectl -n $ns get $group inference -ojson | jq .status || true 11 | 12 | echo -e "\n Display all experiments" 13 | k3s kubectl -n $ns get $group || true 14 | 15 | echo "" 16 | echo "--------------------------" 17 | echo "++ Clean up started" 18 | echo "--------------------------" 19 | 20 | echo -e "\n Uninstall K3s" 21 | /usr/local/bin/k3s-uninstall.sh > uninstall-k3s.txt 2>&1 || true 22 | 23 | echo -e "\n Stop local docker registry container" 24 | docker container stop e2eregistry || true 25 | 26 | echo -e "\n Remove local docker registry container" 27 | docker container rm -v e2eregistry || true 28 | 29 | echo "" 30 | echo "--------------------------" 31 | echo "++ Clean up completed" 32 | echo "--------------------------" 33 | } 34 | 35 | # Comment below if you donot want to invoke cleanup 36 | # after executing this script 37 | # 38 | # This is helpful if you might want to do some checks manually 39 | # & verify the state of the Kubernetes cluster and resources 40 | trap cleanup EXIT 41 | 42 | # Uncomment below if debug / verbose execution is needed 43 | #set -ex 44 | 45 | echo "" 46 | echo "--------------------------" 47 | echo "++ E to E suite started" 48 | echo "--------------------------" 49 | 50 | # Name of the targeted controller binary under test 51 | ctrlbin="dope" 52 | 53 | # Name of the daction controller binary 54 | dactionctrlbin="daction" 55 | 56 | # group that defines the Recipe custom resource 57 | group="recipes.dope.mayadata.io" 58 | 59 | # Namespace used by inference Recipe custom resource 60 | ns="d-testing" 61 | 62 | echo -e "\n Remove locally cached image $ctrlbin:e2e" 63 | docker image remove $ctrlbin:e2e || true 64 | 65 | echo -e "\n Remove locally cached image localhost:5000/$ctrlbin" 66 | docker image remove localhost:5000/$ctrlbin || true 67 | 68 | echo -e "\n Remove locally cached image localhost:5000/$dactionctrlbin" 69 | docker image remove localhost:5000/$dactionctrlbin || true 70 | 71 | echo -e "\n Run local docker registry at port 5000" 72 | docker run -d -p 5000:5000 --restart=always --name e2eregistry registry:2 73 | 74 | echo -e "\n Build $ctrlbin image as $ctrlbin:e2e" 75 | docker build -t $ctrlbin:e2e ./../../ 76 | 77 | echo -e "\n Build $dactionctrlbin image as $dactionctrlbin:e2e" 78 | docker build -t $dactionctrlbin:e2e ./../../tools/d-action 79 | 80 | echo -e "\n Tag $ctrlbin:e2e image as localhost:5000/$ctrlbin" 81 | docker tag $ctrlbin:e2e localhost:5000/$ctrlbin 82 | 83 | echo -e "\n Tag $dactionctrlbin:e2e image as localhost:5000/$dactionctrlbin" 84 | docker tag $dactionctrlbin:e2e localhost:5000/$dactionctrlbin 85 | 86 | echo -e "\n Push the image to local registry running at localhost:5000" 87 | docker push localhost:5000/$ctrlbin 88 | docker push localhost:5000/$dactionctrlbin 89 | 90 | echo -e "\n Setup K3s registries path" 91 | mkdir -p "/etc/rancher/k3s/" 92 | 93 | echo -e "\n Copy registries.yaml to K3s registries path" 94 | cp registries.yaml /etc/rancher/k3s/ 95 | 96 | echo -e "\n Download K3s if not available" 97 | if true && k3s -v ; then 98 | echo "" 99 | else 100 | curl -sfL https://get.k3s.io | sh - 101 | fi 102 | 103 | echo -e "\n Verify if K3s is up and running" 104 | k3s kubectl get node 105 | 106 | echo -e "\n Apply d-operators based ci to K3s cluster" 107 | k3s kubectl apply -f ci.yaml 108 | 109 | echo -e "\n Apply test experiments to K3s cluster" 110 | k3s kubectl apply -f ./experiments/ 111 | 112 | echo -e "\n Apply ci inference to K3s cluster" 113 | k3s kubectl apply -f inference.yaml 114 | 115 | echo -e "\n List configmaps if any in namespace $ns" 116 | k3s kubectl get configmaps -n $ns 117 | 118 | echo -e "\n Retry 50 times until inference experiment gets executed" 119 | date 120 | phase="" 121 | for i in {1..50} 122 | do 123 | phase=$(k3s kubectl -n $ns get $group inference -o=jsonpath='{.status.phase}') 124 | echo -e "Attempt $i: Inference status: status.phase='$phase'" 125 | if [[ "$phase" == "" ]] || [[ "$phase" == "NotEligible" ]]; then 126 | sleep 5 # Sleep & retry since experiment is in-progress 127 | else 128 | break # Abandon this loop since phase is set 129 | fi 130 | done 131 | date 132 | 133 | if [[ "$phase" != "Completed" ]]; then 134 | echo "" 135 | echo "--------------------------" 136 | echo -e "++ E to E suite failed: status.phase='$phase'" 137 | echo "--------------------------" 138 | exit 1 # error since inference experiment did not succeed 139 | fi 140 | 141 | echo "" 142 | echo "--------------------------" 143 | echo "++ E to E suite passed" 144 | echo "--------------------------" 145 | -------------------------------------------------------------------------------- /test/integration/it.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: dit 6 | --- 7 | apiVersion: v1 8 | kind: ServiceAccount 9 | metadata: 10 | name: dit 11 | namespace: dit 12 | --- 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: ClusterRole 15 | metadata: 16 | name: dit 17 | rules: 18 | - apiGroups: 19 | - "*" 20 | resources: 21 | - "*" 22 | verbs: 23 | - "*" 24 | --- 25 | apiVersion: rbac.authorization.k8s.io/v1 26 | kind: ClusterRoleBinding 27 | metadata: 28 | name: dit 29 | subjects: 30 | - kind: ServiceAccount 31 | name: dit 32 | namespace: dit 33 | roleRef: 34 | kind: ClusterRole 35 | name: dit 36 | apiGroup: rbac.authorization.k8s.io 37 | --- 38 | apiVersion: batch/v1 39 | kind: Job 40 | metadata: 41 | name: inference 42 | namespace: dit 43 | spec: 44 | # pod should not be recreated on failure 45 | # single pod only job 46 | backoffLimit: 0 47 | ttlSecondsAfterFinished: 10 48 | template: 49 | spec: 50 | restartPolicy: Never 51 | serviceAccountName: dit 52 | containers: 53 | - command: ["make"] 54 | args: 55 | - integration-test 56 | image: localhost:5000/dopeit 57 | imagePullPolicy: Always 58 | name: ditc -------------------------------------------------------------------------------- /test/integration/registries.yaml: -------------------------------------------------------------------------------- 1 | mirrors: 2 | docker.io: 3 | endpoint: 4 | - "http://localhost:5000" -------------------------------------------------------------------------------- /test/integration/suite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cleanup() { 4 | set +e 5 | 6 | echo "" 7 | echo "--------------------------" 8 | echo "++ Clean up started" 9 | echo "--------------------------" 10 | 11 | echo -e "\n Uninstall K3s" 12 | /usr/local/bin/k3s-uninstall.sh > uninstall-k3s.txt 2>&1 || true 13 | 14 | echo -e "\n Stop local docker registry container" 15 | docker container stop e2eregistry || true 16 | 17 | echo -e "\n Remove local docker registry container" 18 | docker container rm -v e2eregistry || true 19 | 20 | echo "" 21 | echo "--------------------------" 22 | echo "++ Clean up completed" 23 | echo "--------------------------" 24 | } 25 | 26 | # Comment below if you donot want to invoke cleanup 27 | # after executing this script 28 | # 29 | # This is helpful if you might want to do some checks manually 30 | # & verify the state of the Kubernetes cluster and resources 31 | trap cleanup EXIT 32 | 33 | # Uncomment below if debug / verbose execution is needed 34 | #set -ex 35 | 36 | echo "" 37 | echo "--------------------------" 38 | echo "++ Integration test suite started" 39 | echo "--------------------------" 40 | 41 | # Name of the targeted controller binary suitable for 42 | # running integration tests 43 | ctrlbinary="dopeit" 44 | 45 | echo -e "\n Delete previous integration test manifests if available" 46 | k3s kubectl delete -f it.yaml || true 47 | 48 | echo -e "\n Remove locally cached image $ctrlbinary:it" 49 | docker image remove $ctrlbinary:it || true 50 | 51 | echo -e "\n Remove locally cached image localhost:5000/$ctrlbinary" 52 | docker image remove localhost:5000/$ctrlbinary || true 53 | 54 | echo -e "\n Run local docker registry at port 5000" 55 | docker run -d -p 5000:5000 --restart=always --name e2eregistry registry:2 56 | 57 | echo -e "\n Build $ctrlbinary image as $ctrlbinary:it" 58 | docker build -t $ctrlbinary:it ./../../ -f ./../../Dockerfile.testing 59 | 60 | echo -e "\n Tag $ctrlbinary:it image as localhost:5000/$ctrlbinary" 61 | docker tag $ctrlbinary:it localhost:5000/$ctrlbinary 62 | 63 | echo -e "\n Push $ctrlbinary:it image to local registry running at localhost:5000" 64 | docker push localhost:5000/$ctrlbinary 65 | 66 | echo -e "\n Setup K3s registries path" 67 | mkdir -p "/etc/rancher/k3s/" 68 | 69 | echo -e "\n Copy registries.yaml to K3s registries path" 70 | cp registries.yaml /etc/rancher/k3s/ 71 | 72 | echo -e "\n Download K3s if not available" 73 | if true && k3s -v ; then 74 | echo "" 75 | else 76 | curl -sfL https://get.k3s.io | sh - 77 | fi 78 | 79 | echo -e "\n Verify if K3s is up and running" 80 | k3s kubectl get node 81 | 82 | echo -e "\n Apply integration manifests to K3s cluster" 83 | k3s kubectl apply -f it.yaml 84 | 85 | echo -e "\n Will retry 50 times until integration test job gets completed" 86 | 87 | echo -e "\n Start Time" 88 | date 89 | echo -e "\n" 90 | 91 | phase="" 92 | for i in {1..50} 93 | do 94 | succeeded=$(k3s kubectl get job inference -n dit -o=jsonpath='{.status.succeeded}') 95 | failed=$(k3s kubectl get job inference -n dit -o=jsonpath='{.status.failed}') 96 | 97 | echo -e "Attempt $i: status.succeeded='$succeeded' status.failed='$failed'" 98 | 99 | if [[ "$failed" == "1" ]]; then 100 | break # Abandon this loop since job has failed 101 | fi 102 | 103 | if [[ "$succeeded" != "1" ]]; then 104 | sleep 15 # Sleep & retry since experiment is in-progress 105 | else 106 | break # Abandon this loop since succeeded is set 107 | fi 108 | done 109 | 110 | echo -e "\n End Time" 111 | date 112 | echo -e "\n" 113 | 114 | echo -e "\n Display status of inference job" 115 | k3s kubectl get job inference -n dit -ojson | jq .status || true 116 | 117 | echo -e "\n Display test logs & coverage" 118 | k3s kubectl -n dit logs -ljob-name=inference --tail=-1 || true 119 | 120 | if [ "$succeeded" != "1" ] || [ "$failed" == "1" ]; then 121 | echo "" 122 | echo "--------------------------" 123 | echo -e "++ Integration test suite failed:" 124 | echo -e "+++ status.succeeded='$succeeded'" 125 | echo -e "+++ status.failed='$failed'" 126 | echo "--------------------------" 127 | exit 1 # error since inference experiment did not succeed 128 | fi 129 | 130 | echo "" 131 | echo "--------------------------" 132 | echo "++ Integration test suite passed" 133 | echo "--------------------------" 134 | -------------------------------------------------------------------------------- /tools/d-action/Dockerfile: -------------------------------------------------------------------------------- 1 | # -------------------------- 2 | # Build d-operators binary 3 | # -------------------------- 4 | FROM golang:1.13.5 as builder 5 | 6 | WORKDIR /tools 7 | 8 | # copy go modules manifests 9 | COPY go.mod go.mod 10 | COPY go.sum go.sum 11 | 12 | # copy build manifests 13 | COPY Makefile Makefile 14 | 15 | # copy source files 16 | COPY pkg/ pkg/ 17 | COPY main.go main.go 18 | 19 | # build binary 20 | RUN make 21 | 22 | # --------------------------- 23 | # Use alpine as minimal base image to package the final binary 24 | # --------------------------- 25 | FROM alpine:latest 26 | 27 | WORKDIR / 28 | 29 | COPY --from=builder /tools/daction /usr/bin/ 30 | 31 | CMD ["/usr/bin/daction"] -------------------------------------------------------------------------------- /tools/d-action/Makefile: -------------------------------------------------------------------------------- 1 | # Fetch the latest tags & then set the package version 2 | PACKAGE_VERSION ?= $(shell git fetch --all --tags | echo "" | git describe --always --tags) 3 | ALL_SRC = $(shell find . -name "*.go" | grep -v -e "vendor") 4 | 5 | # We are using docker hub as the default registry 6 | IMG_NAME ?= daction 7 | IMG_REPO ?= mayadataio/daction 8 | 9 | all: bins 10 | 11 | bins: vendor test $(IMG_NAME) 12 | 13 | $(IMG_NAME): $(ALL_SRC) 14 | @echo "+ Generating $(IMG_NAME) binary" 15 | @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on \ 16 | go build -o $@ ./main.go 17 | 18 | $(ALL_SRC): ; 19 | 20 | # go mod download modules to local cache 21 | # make vendored copy of dependencies 22 | # install other go binaries for code generation 23 | .PHONY: vendor 24 | vendor: go.mod go.sum 25 | @GO111MODULE=on go mod download 26 | @GO111MODULE=on go mod tidy 27 | @GO111MODULE=on go mod vendor 28 | 29 | .PHONY: test 30 | test: 31 | @go test ./... -cover 32 | 33 | .PHONY: testv 34 | testv: 35 | @go test ./... -cover -v -args --logtostderr -v=2 36 | 37 | .PHONY: e2e-test 38 | e2e-test: 39 | @cd test/e2e && ./suite.sh 40 | 41 | .PHONY: image 42 | image: 43 | docker build -t $(IMG_REPO):$(PACKAGE_VERSION) -t $(IMG_REPO):latest . 44 | 45 | .PHONY: push 46 | push: image 47 | docker push $(IMG_REPO):$(PACKAGE_VERSION) 48 | 49 | .PHONY: push-latest 50 | push-latest: image 51 | docker push $(IMG_REPO):latest 52 | -------------------------------------------------------------------------------- /tools/d-action/go.mod: -------------------------------------------------------------------------------- 1 | module mayadata.io/d-action 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-cmd/cmd v1.2.0 7 | github.com/pkg/errors v0.9.1 8 | k8s.io/apimachinery v0.17.3 9 | k8s.io/client-go v0.17.3 10 | k8s.io/klog/v2 v2.0.0 11 | mayadata.io/d-operators v1.13.0 12 | ) 13 | 14 | replace ( 15 | k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.17.3 16 | k8s.io/apimachinery => k8s.io/apimachinery v0.17.3 17 | k8s.io/client-go => k8s.io/client-go v0.17.3 18 | mayadata.io/d-operators => github.com/mayadata-io/d-operators v1.13.0 19 | openebs.io/metac => github.com/AmitKumarDas/metac v0.4.0 20 | ) 21 | -------------------------------------------------------------------------------- /tools/d-action/pkg/action/run_command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package action 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | 23 | types "mayadata.io/d-operators/types/command" 24 | ) 25 | 26 | // RunnableConfig helps constructing a new instance of Runnable 27 | type RunnableConfig struct { 28 | Command types.Command 29 | } 30 | 31 | // Runnable helps executing one or more commands 32 | // e.g. shell / script commands 33 | type Runnable struct { 34 | Command types.Command 35 | Status *types.CommandStatus 36 | 37 | // determines if Command resource can be reconciled more than once 38 | enabled types.EnabledWhen 39 | 40 | // err as value 41 | err error 42 | } 43 | 44 | func (r *Runnable) init() { 45 | // enabled defauls to Once i.e. Command can reconcile only once 46 | r.enabled = types.EnabledOnce 47 | // override with user specified value if set 48 | if r.Command.Spec.Enabled.When != "" { 49 | r.enabled = r.Command.Spec.Enabled.When 50 | } 51 | } 52 | 53 | // NewRunner returns a new instance of Runnable 54 | func NewRunner(config RunnableConfig) (*Runnable, error) { 55 | r := &Runnable{ 56 | Command: config.Command, 57 | Status: &types.CommandStatus{}, 58 | } 59 | r.init() 60 | return r, nil 61 | } 62 | 63 | func (r *Runnable) setStatus(out map[string]types.CommandOutput) { 64 | var totalTimetaken float64 65 | for _, op := range out { 66 | totalTimetaken = totalTimetaken + op.ExecutionTime.ValueInSeconds 67 | if op.Error != "" { 68 | r.Status.Counter.ErrorCount++ 69 | } 70 | if op.Warning != "" { 71 | r.Status.Counter.WarnCount++ 72 | } 73 | if op.Timedout { 74 | r.Status.Counter.TimeoutCount++ 75 | } 76 | } 77 | switch r.enabled { 78 | case types.EnabledOnce: 79 | // Command that is meant to run only once is initialised to 80 | // Completed phase 81 | r.Status.Phase = types.CommandPhaseCompleted 82 | default: 83 | // Command that is meant to be run periodically is initialised 84 | // to Running phase 85 | r.Status.Phase = types.CommandPhaseRunning 86 | } 87 | if r.Status.Counter.TimeoutCount > 0 { 88 | r.Status.Phase = types.CommandPhaseTimedOut 89 | r.Status.Timedout = true 90 | r.Status.Reason = fmt.Sprintf( 91 | "%d timeout(s) found", 92 | r.Status.Counter.TimeoutCount, 93 | ) 94 | } 95 | if r.Status.Counter.ErrorCount > 0 { 96 | r.Status.Phase = types.CommandPhaseError 97 | r.Status.Reason = fmt.Sprintf( 98 | "%d error(s) found", 99 | r.Status.Counter.ErrorCount, 100 | ) 101 | } 102 | totalTimeTakenSecs := time.Duration(totalTimetaken) * time.Second 103 | totalTimeTakenSecsFmt := totalTimeTakenSecs.Round(time.Millisecond).String() 104 | r.Status.ExecutionTime = types.ExecutionTime{ 105 | ValueInSeconds: totalTimeTakenSecs.Seconds() + 0.0001, 106 | ReadableValue: totalTimeTakenSecsFmt, 107 | } 108 | r.Status.Outputs = out 109 | } 110 | 111 | // Run executes the commands in a sequential order 112 | func (r *Runnable) Run() (status *types.CommandStatus, err error) { 113 | if r.enabled == types.EnabledNever { 114 | return &types.CommandStatus{ 115 | Phase: types.CommandPhaseSkipped, 116 | Message: "Resource is not enabled", 117 | }, nil 118 | } 119 | runcmdlist, err := NewShellListRunner(r.Command.Spec) 120 | if err != nil { 121 | return nil, err 122 | } 123 | out := runcmdlist.Run() 124 | r.setStatus(out) 125 | return r.Status, nil 126 | } 127 | -------------------------------------------------------------------------------- /tools/d-action/pkg/util/json.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "encoding/json" 21 | ) 22 | 23 | // JSONable holds any object that can be marshaled to json 24 | type JSONable struct { 25 | Obj interface{} 26 | } 27 | 28 | // New returns a new type of JSONable 29 | func NewJSON(obj interface{}) *JSONable { 30 | return &JSONable{obj} 31 | } 32 | 33 | // MustMarshal marshals the JSONable type 34 | func (j *JSONable) MustMarshal() string { 35 | raw, err := json.MarshalIndent(j.Obj, "", ".") 36 | if err != nil { 37 | panic(err) 38 | } 39 | return string(raw) 40 | } 41 | -------------------------------------------------------------------------------- /tools/d-action/pkg/util/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "reflect" 21 | 22 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 | ) 24 | 25 | // List is a custom datatype representing a list of 26 | // unstructured instances 27 | type List []*unstructured.Unstructured 28 | 29 | // ContainsByIdentity returns true if provided target is available 30 | // by its name, uid & other metadata fields 31 | func (s List) ContainsByIdentity(target *unstructured.Unstructured) bool { 32 | if target == nil || target.Object == nil { 33 | // we don't know how to compare against a nil 34 | return false 35 | } 36 | for _, obj := range s { 37 | if obj == nil || obj.Object == nil { 38 | continue 39 | } 40 | if obj.GetName() == target.GetName() && 41 | obj.GetNamespace() == target.GetNamespace() && 42 | obj.GetUID() == target.GetUID() && 43 | obj.GetKind() == target.GetKind() && 44 | obj.GetAPIVersion() == target.GetAPIVersion() { 45 | return true 46 | } 47 | } 48 | return false 49 | } 50 | 51 | // IdentifiesAll returns true if each item in the provided 52 | // targets is available & match by their identity 53 | func (s List) IdentifiesAll(targets []*unstructured.Unstructured) bool { 54 | if len(s) == len(targets) && len(s) == 0 { 55 | return true 56 | } 57 | if len(s) != len(targets) { 58 | return false 59 | } 60 | for _, t := range targets { 61 | if !s.ContainsByIdentity(t) { 62 | // return false if any item does not match 63 | return false 64 | } 65 | } 66 | return true 67 | } 68 | 69 | // ContainsByEquality does a field to field match of provided target 70 | // against the corresponding object present in this list 71 | func (s List) ContainsByEquality(target *unstructured.Unstructured) bool { 72 | if target == nil || target.Object == nil { 73 | // we can't match a nil target 74 | return false 75 | } 76 | for _, src := range s { 77 | if src == nil || src.Object == nil { 78 | continue 79 | } 80 | // use meta fields as much as possible to verify 81 | // if target & src do not match 82 | if src.GetName() != target.GetName() || 83 | src.GetNamespace() != target.GetNamespace() || 84 | src.GetUID() != target.GetUID() || 85 | src.GetKind() != target.GetKind() || 86 | src.GetAPIVersion() != target.GetAPIVersion() || 87 | len(src.GetAnnotations()) != len(target.GetAnnotations()) || 88 | len(src.GetLabels()) != len(target.GetLabels()) || 89 | len(src.GetOwnerReferences()) != len(target.GetOwnerReferences()) || 90 | len(src.GetFinalizers()) != len(target.GetFinalizers()) { 91 | // continue since target does not match src 92 | continue 93 | } 94 | // Since target matches with this src based on meta 95 | // information we need to **verify further** by running 96 | // reflect based match 97 | return reflect.DeepEqual(target, src) 98 | } 99 | return false 100 | } 101 | 102 | // EqualsAll does a field to field match of each target against 103 | // the corresponding object present in this list 104 | func (s List) EqualsAll(targets []*unstructured.Unstructured) bool { 105 | if len(s) == len(targets) && len(s) == 0 { 106 | return true 107 | } 108 | if len(s) != len(targets) { 109 | return false 110 | } 111 | for _, t := range targets { 112 | if !s.ContainsByEquality(t) { 113 | return false 114 | } 115 | } 116 | return true 117 | } 118 | -------------------------------------------------------------------------------- /tools/d-action/pkg/util/pointer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | // Bool returns a pointer to the given bool 20 | func Bool(b bool) *bool { 21 | o := b 22 | return &o 23 | } 24 | 25 | // Int returns a pointer to the given int 26 | func Int(i int) *int { 27 | o := i 28 | return &o 29 | } 30 | 31 | // Int32 returns a pointer to the given int32 32 | func Int32(i int32) *int32 { 33 | o := i 34 | return &o 35 | } 36 | 37 | // Int64 returns a pointer to the given int64 38 | func Int64(i int64) *int64 { 39 | o := i 40 | return &o 41 | } 42 | 43 | // String returns a pointer to the given string 44 | func String(s string) *string { 45 | o := s 46 | return &o 47 | } 48 | -------------------------------------------------------------------------------- /tools/d-action/pkg/util/unstruct.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "github.com/pkg/errors" 21 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/util/json" 24 | ) 25 | 26 | // ToTyped transforms the provided unstruct instance 27 | // to target type 28 | func ToTyped(src *unstructured.Unstructured, target interface{}) error { 29 | if src == nil || src.Object == nil { 30 | return errors.Errorf( 31 | "Can't transform unstruct to typed: Nil unstruct content", 32 | ) 33 | } 34 | if target == nil { 35 | return errors.Errorf( 36 | "Can't transform unstruct to typed: Nil target", 37 | ) 38 | } 39 | return runtime.DefaultUnstructuredConverter.FromUnstructured( 40 | src.UnstructuredContent(), 41 | target, 42 | ) 43 | } 44 | 45 | // MarshalThenUnmarshal marshals the provided src and unmarshals 46 | // it back into the dest 47 | func MarshalThenUnmarshal(src interface{}, dest interface{}) error { 48 | data, err := json.Marshal(src) 49 | if err != nil { 50 | return err 51 | } 52 | return json.Unmarshal(data, dest) 53 | } 54 | 55 | // SetLabels updates the given labels with the ones 56 | // found in the provided unstructured instance 57 | func SetLabels(obj *unstructured.Unstructured, lbls map[string]string) { 58 | if len(lbls) == 0 { 59 | return 60 | } 61 | if obj == nil || obj.Object == nil { 62 | return 63 | } 64 | got := obj.GetLabels() 65 | if got == nil { 66 | got = make(map[string]string) 67 | } 68 | for k, v := range lbls { 69 | // update given label against existing 70 | got[k] = v 71 | } 72 | obj.SetLabels(got) 73 | } 74 | -------------------------------------------------------------------------------- /types/command/command_test.go: -------------------------------------------------------------------------------- 1 | // +build !integration 2 | 3 | package types 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/pkg/errors" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | ) 12 | 13 | // ToTyped transforms the provided unstruct instance 14 | // to target type 15 | func ToTyped(src *unstructured.Unstructured, target interface{}) error { 16 | if src == nil || src.Object == nil { 17 | return errors.Errorf( 18 | "Can't transform unstruct to typed: Nil unstruct content", 19 | ) 20 | } 21 | if target == nil { 22 | return errors.Errorf( 23 | "Can't transform unstruct to typed: Nil target", 24 | ) 25 | } 26 | return runtime.DefaultUnstructuredConverter.FromUnstructured( 27 | src.UnstructuredContent(), 28 | target, 29 | ) 30 | } 31 | 32 | // ToTyped transforms the provided unstruct instance 33 | func TestToTyped(t *testing.T) { 34 | var tests = map[string]struct { 35 | Given *unstructured.Unstructured 36 | }{ 37 | "unstruct to typed command": { 38 | Given: &unstructured.Unstructured{ 39 | Object: map[string]interface{}{ 40 | "kind": "Command", 41 | "apiVersion": "v1", 42 | "metadata": map[string]interface{}{ 43 | "namespace": "dope", 44 | "name": "one-1", 45 | }, 46 | "spec": map[string]interface{}{ 47 | "commands": []interface{}{ 48 | map[string]interface{}{ 49 | "cmd": []interface{}{ 50 | "kubectl", 51 | "get", 52 | "pods", 53 | "-n", 54 | "metal", 55 | }, 56 | "name": "get pods in metal ns", 57 | }, 58 | }, 59 | }, 60 | "status": map[string]interface{}{ 61 | "counter": map[string]interface{}{ 62 | "errorCount": 1, 63 | "timeoutCount": 0, 64 | "warnCount": 0, 65 | }, 66 | "outputs": map[string]interface{}{ 67 | "get pods in metal ns": map[string]interface{}{ 68 | "cmd": "kubectl", 69 | "completed": false, 70 | "error": "exec: \"kubectl\": executable file not found in $PATH", 71 | "executionTime": map[string]interface{}{ 72 | "readableValue": "0s", 73 | "valueInSeconds": float64(0), 74 | }, 75 | "exit": -1, 76 | "pid": 0, 77 | "stderr": "", 78 | "stdout": "", 79 | "timedout": false, 80 | }, 81 | }, 82 | "phase": "Error", 83 | "reason": "1 error(s) found", 84 | "timedout": false, 85 | "timetakenInSeconds": map[string]interface{}{ 86 | "readableValue": "0s", 87 | "valueInSeconds": float64(0), 88 | }, 89 | }, 90 | }, 91 | }, 92 | }, 93 | } 94 | for name, mock := range tests { 95 | name := name 96 | mock := mock 97 | t.Run(name, func(t *testing.T) { 98 | var finale Command 99 | err := ToTyped(mock.Given, &finale) 100 | if err != nil { 101 | t.Fatalf("Expected no error got %s", err.Error()) 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /types/doperator/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // DOperator is a kubernetes custom resource that defines 24 | // the specifications to manage DOperator needs 25 | // 26 | // rough draft 27 | // include exclude CRDs that DOperator defines 28 | // e.g. CStorPoolAuto 29 | // e.g. HTTP 30 | // e.g. HTTPDirector 31 | // e.g. BlockDeviceSet 32 | type DOperator struct { 33 | metav1.TypeMeta `json:",inline"` 34 | metav1.ObjectMeta `json:"metadata"` 35 | } 36 | -------------------------------------------------------------------------------- /types/git/git.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | metac "openebs.io/metac/apis/metacontroller/v1alpha1" 24 | ) 25 | 26 | // GitUpload is a kubernetes custom resource that defines 27 | // the specifications to upload kubernetes resources and 28 | // pod logs 29 | type GitUpload struct { 30 | metav1.TypeMeta `json:",inline"` 31 | metav1.ObjectMeta `json:"metadata"` 32 | 33 | Spec GitUploadSpec `json:"spec"` 34 | Status GitUploadStatus `json:"status"` 35 | } 36 | 37 | // GitUploadSpec defines the specifications to upload 38 | // kubernetes resources and pod logs 39 | type GitUploadSpec struct { 40 | ResourceSelector []metac.GenericControllerResource `json:"resourceSelector,omitempty"` 41 | } 42 | 43 | // GitUploadStatus holds the status of executing a GitUpload 44 | type GitUploadStatus struct { 45 | Phase string `json:"phase"` 46 | Reason string `json:"reason,omitempty"` 47 | Message string `json:"message,omitempty"` 48 | } 49 | 50 | // String implements the Stringer interface 51 | func (jr GitUploadStatus) String() string { 52 | raw, err := json.MarshalIndent( 53 | jr, 54 | " ", 55 | ".", 56 | ) 57 | if err != nil { 58 | panic(err) 59 | } 60 | return string(raw) 61 | } 62 | -------------------------------------------------------------------------------- /types/gvk/gvk.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package gvk 18 | 19 | const ( 20 | // KindHTTP represents HTTP custom resource 21 | KindHTTP string = "HTTP" 22 | 23 | // KindHTTPData represents HTTPData custom resource 24 | KindHTTPData string = "HTTPData" 25 | 26 | // KindDirectorHTTP represents DirectorHTTP custom resource 27 | KindDirectorHTTP string = "DirectorHTTP" 28 | ) 29 | 30 | const ( 31 | // KindRecipe represents Recipe custom resource 32 | KindRecipe string = "Recipe" 33 | 34 | // APIVersionRecipe represent Recipe custom resource's api version 35 | APIVersionRecipe string = "dope.mayadata.io/v1" 36 | ) 37 | 38 | const ( 39 | // APIExtensionsK8sIOV1Beta1 represents apiextensions.k8s.io 40 | // as group & v1beta1 as version 41 | APIExtensionsK8sIOV1Beta1 string = "apiextensions.k8s.io/v1beta1" 42 | 43 | // GroupDAOMayadataIO represents dao.mayadata.io as 44 | // group 45 | GroupDAOMayadataIO string = "dao.mayadata.io" 46 | 47 | // VersionV1Alpha1 represents v1alpha1 version 48 | VersionV1Alpha1 string = "v1alpha1" 49 | 50 | // DAOMayadataIOV1Alpha1 represents 51 | // dao.mayadata.io as group & v1alpha1 as version 52 | DAOMayadataIOV1Alpha1 string = GroupDAOMayadataIO + "/" + VersionV1Alpha1 53 | ) 54 | -------------------------------------------------------------------------------- /types/recipe/apply.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 | metac "openebs.io/metac/apis/metacontroller/v1alpha1" 24 | ) 25 | 26 | // Apply represents the desired state that needs to 27 | // be applied against the cluster 28 | type Apply struct { 29 | // Desired state that needs to be created or 30 | // updated or deleted. Resource gets created if 31 | // this state is not observed in the cluster. 32 | // However, if this state is found in the cluster, 33 | // then the corresponding resource gets updated 34 | // via a 3-way merge. 35 | State *unstructured.Unstructured `json:"state"` 36 | 37 | // Desired count that needs to be created 38 | // 39 | // NOTE: 40 | // If value is 0 then this state needs to be 41 | // deleted 42 | Replicas *int `json:"replicas,omitempty"` 43 | 44 | // Resources that needs to be **updated** with above 45 | // desired state 46 | // 47 | // NOTE: 48 | // Presence of Targets implies an update operation 49 | Targets metac.ResourceSelector `json:"targets,omitempty"` 50 | 51 | // IgnoreDiscovery if set to true will not retry till 52 | // resource gets discovered 53 | // 54 | // NOTE: 55 | // This is only applicable for kind: CustomResourceDefinition 56 | IgnoreDiscovery bool `json:"ignoreDiscovery"` 57 | } 58 | 59 | // String implements the Stringer interface 60 | func (a Apply) String() string { 61 | raw, err := json.MarshalIndent( 62 | a, 63 | " ", 64 | ".", 65 | ) 66 | if err != nil { 67 | panic(err) 68 | } 69 | return string(raw) 70 | } 71 | 72 | // ApplyStatusPhase is a typed definition to determine the 73 | // result of executing an apply 74 | type ApplyStatusPhase string 75 | 76 | const ( 77 | // ApplyStatusPassed defines a successful apply 78 | ApplyStatusPassed ApplyStatusPhase = "Passed" 79 | 80 | // ApplyStatusWarning defines an apply that resulted in warnings 81 | ApplyStatusWarning ApplyStatusPhase = "Warning" 82 | 83 | // ApplyStatusFailed defines a failed apply 84 | ApplyStatusFailed ApplyStatusPhase = "Failed" 85 | ) 86 | 87 | // ToTaskStatusPhase transforms ApplyStatusPhase to TestResultPhase 88 | func (phase ApplyStatusPhase) ToTaskStatusPhase() TaskStatusPhase { 89 | switch phase { 90 | case ApplyStatusPassed: 91 | return TaskStatusPassed 92 | case ApplyStatusFailed: 93 | return TaskStatusFailed 94 | case ApplyStatusWarning: 95 | return TaskStatusWarning 96 | default: 97 | return "" 98 | } 99 | } 100 | 101 | // ApplyResult holds the result of the apply operation 102 | type ApplyResult struct { 103 | Phase ApplyStatusPhase `json:"phase"` 104 | Message string `json:"message,omitempty"` 105 | Verbose string `json:"verbose,omitempty"` 106 | Warning string `json:"warning,omitempty"` 107 | } 108 | -------------------------------------------------------------------------------- /types/recipe/assert.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | 23 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 | ) 25 | 26 | // Assert handles assertion of desired state against 27 | // the observed state found in the cluster 28 | type Assert struct { 29 | // Desired state(s) that is asserted against the observed 30 | // state(s) 31 | State *unstructured.Unstructured `json:"state"` 32 | 33 | // StateCheck has assertions related to state of resources 34 | StateCheck *StateCheck `json:"stateCheck,omitempty"` 35 | 36 | // PathCheck has assertions related to resource paths 37 | PathCheck *PathCheck `json:"pathCheck,omitempty"` 38 | 39 | // ErrorOnAssertFailure when set to true will result in 40 | // error if assertion fails 41 | ErrorOnAssertFailure *bool `json:"errorOnAssertFailure,omitempty"` 42 | } 43 | 44 | // String implements the Stringer interface 45 | func (a Assert) String() string { 46 | raw, err := json.MarshalIndent( 47 | a, 48 | " ", 49 | ".", 50 | ) 51 | if err != nil { 52 | panic(err) 53 | } 54 | return string(raw) 55 | } 56 | 57 | // AssertStatusPhase defines the status of executing an assertion 58 | type AssertStatusPhase string 59 | 60 | const ( 61 | // AssertResultPassed defines a successful assertion 62 | AssertResultPassed AssertStatusPhase = "AssertPassed" 63 | 64 | // AssertResultWarning defines an assertion that resulted in warning 65 | AssertResultWarning AssertStatusPhase = "AssertWarning" 66 | 67 | // AssertResultFailed defines a failed assertion 68 | AssertResultFailed AssertStatusPhase = "AssertFailed" 69 | ) 70 | 71 | // ToTaskStatusPhase transforms AssertResultPhase to TaskResultPhase 72 | func (phase AssertStatusPhase) ToTaskStatusPhase() TaskStatusPhase { 73 | switch phase { 74 | case AssertResultPassed: 75 | return TaskStatusPassed 76 | case AssertResultFailed: 77 | return TaskStatusFailed 78 | case AssertResultWarning: 79 | return TaskStatusWarning 80 | default: 81 | return "" 82 | } 83 | } 84 | 85 | // AssertStatus holds the result of assertion 86 | type AssertStatus struct { 87 | Phase AssertStatusPhase `json:"phase"` 88 | Message string `json:"message,omitempty"` 89 | Verbose string `json:"verbose,omitempty"` 90 | Warning string `json:"warning,omitempty"` 91 | Timeout string `json:"timeout,omitempty"` 92 | } 93 | 94 | // String implements the Stringer interface 95 | func (a AssertStatus) String() string { 96 | return fmt.Sprintf( 97 | "Assert status: Phase %q: Message %q: Verbose %q: Warning %q: Timeout %q", 98 | a.Phase, 99 | a.Message, 100 | a.Verbose, 101 | a.Warning, 102 | a.Timeout, 103 | ) 104 | } 105 | 106 | // AssertCheckType defines the type of assert check 107 | type AssertCheckType int 108 | 109 | const ( 110 | // AssertCheckTypeState defines a state check based assertion 111 | AssertCheckTypeState AssertCheckType = iota 112 | 113 | // AssertCheckTypePath defines a path check based assertion 114 | AssertCheckTypePath 115 | ) 116 | -------------------------------------------------------------------------------- /types/recipe/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 | ) 24 | 25 | // Create creates the state found in the cluster 26 | type Create struct { 27 | // Desired state that needs to be created 28 | State *unstructured.Unstructured `json:"state"` 29 | 30 | // Desired count that needs to be created 31 | Replicas *int `json:"replicas,omitempty"` 32 | 33 | // IgnoreDiscovery if set to true will not retry till 34 | // resource gets discovered 35 | // 36 | // NOTE: 37 | // This is only applicable for kind: CustomResourceDefinition 38 | IgnoreDiscovery bool `json:"ignoreDiscovery"` 39 | } 40 | 41 | // String implements the Stringer interface 42 | func (a Create) String() string { 43 | raw, err := json.MarshalIndent( 44 | a, 45 | " ", 46 | ".", 47 | ) 48 | if err != nil { 49 | panic(err) 50 | } 51 | return string(raw) 52 | } 53 | 54 | // CreateStatusPhase is a typed definition to determine the 55 | // result of executing a create 56 | type CreateStatusPhase string 57 | 58 | const ( 59 | // CreateStatusPassed defines a successful create 60 | CreateStatusPassed CreateStatusPhase = "Passed" 61 | 62 | // CreateStatusWarning defines a create that resulted in warnings 63 | CreateStatusWarning CreateStatusPhase = "Warning" 64 | 65 | // CreateStatusFailed defines a failed create 66 | CreateStatusFailed CreateStatusPhase = "Failed" 67 | ) 68 | 69 | // ToTaskStatusPhase transforms CreateStatusPhase to TaskStatusPhase 70 | func (phase CreateStatusPhase) ToTaskStatusPhase() TaskStatusPhase { 71 | switch phase { 72 | case CreateStatusPassed: 73 | return TaskStatusPassed 74 | case CreateStatusFailed: 75 | return TaskStatusFailed 76 | case CreateStatusWarning: 77 | return TaskStatusWarning 78 | default: 79 | return "" 80 | } 81 | } 82 | 83 | // ToApplyStatusPhase transforms CreateStatusPhase to ApplyStatusPhase 84 | func (phase CreateStatusPhase) ToApplyStatusPhase() ApplyStatusPhase { 85 | switch phase { 86 | case CreateStatusPassed: 87 | return ApplyStatusPassed 88 | case CreateStatusFailed: 89 | return ApplyStatusFailed 90 | case CreateStatusWarning: 91 | return ApplyStatusWarning 92 | default: 93 | return "" 94 | } 95 | } 96 | 97 | // CreateResult holds the result of the create operation 98 | type CreateResult struct { 99 | Phase CreateStatusPhase `json:"phase"` 100 | Message string `json:"message,omitempty"` 101 | Verbose string `json:"verbose,omitempty"` 102 | Warning string `json:"warning,omitempty"` 103 | } 104 | -------------------------------------------------------------------------------- /types/recipe/delete.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 21 | ) 22 | 23 | // Delete deletes the state found in the cluster 24 | type Delete struct { 25 | // Desired state that needs to be deleted 26 | State *unstructured.Unstructured `json:"state"` 27 | } 28 | -------------------------------------------------------------------------------- /types/recipe/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 23 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 | ) 25 | 26 | // Get represents the desired state that needs to 27 | // be fetched from the cluster 28 | type Get struct { 29 | // Desired state that needs to be fetched from the 30 | // Kubernetes cluster 31 | State *unstructured.Unstructured `json:"state"` 32 | } 33 | 34 | // String implements the Stringer interface 35 | func (g Get) String() string { 36 | raw, err := json.MarshalIndent( 37 | g, 38 | " ", 39 | ".", 40 | ) 41 | if err != nil { 42 | panic(err) 43 | } 44 | return string(raw) 45 | } 46 | 47 | // GetStatusPhase is a typed definition to determine the 48 | // result of executing a get invocation 49 | type GetStatusPhase string 50 | 51 | const ( 52 | // GetStatusPassed defines a successful get 53 | GetStatusPassed GetStatusPhase = "Passed" 54 | 55 | // GetStatusWarning defines a get that resulted in warnings 56 | GetStatusWarning GetStatusPhase = "Warning" 57 | 58 | // GetStatusFailed defines a failed get 59 | GetStatusFailed GetStatusPhase = "Failed" 60 | ) 61 | 62 | // ToTaskStatusPhase transforms GetStatusPhase to TaskStatusPhase 63 | func (phase GetStatusPhase) ToTaskStatusPhase() TaskStatusPhase { 64 | switch phase { 65 | case GetStatusPassed: 66 | return TaskStatusPassed 67 | case GetStatusFailed: 68 | return TaskStatusFailed 69 | case GetStatusWarning: 70 | return TaskStatusWarning 71 | default: 72 | return "" 73 | } 74 | } 75 | 76 | // GetResult holds the result of the get operation 77 | type GetResult struct { 78 | Phase GetStatusPhase `json:"phase"` 79 | Message string `json:"message,omitempty"` 80 | Verbose string `json:"verbose,omitempty"` 81 | Warning string `json:"warning,omitempty"` 82 | V1Beta1CRD *v1beta1.CustomResourceDefinition `json:"v1b1CRD,omitempty"` 83 | Object *unstructured.Unstructured `json:"object,omitempty"` 84 | } 85 | -------------------------------------------------------------------------------- /types/recipe/label.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 | ) 24 | 25 | // Label represents the label apply operation against 26 | // one or more desired resources 27 | type Label struct { 28 | // Desired state i.e. resources that needs to be 29 | // labeled 30 | State *unstructured.Unstructured `json:"state"` 31 | 32 | // Include the resources by these names 33 | // 34 | // Optional 35 | IncludeByNames []string `json:"includeByNames,omitempty"` 36 | 37 | // ApplyLabels represents the labels that need to be 38 | // applied against the selected resources 39 | // 40 | // This is mandatory field 41 | ApplyLabels map[string]string `json:"applyLabels"` 42 | 43 | // AutoUnset removes the labels from the resources if 44 | // they were applied earlier and these resources are 45 | // no longer elgible to be applied with these labels 46 | // 47 | // Defaults to false 48 | AutoUnset bool `json:"autoUnset"` 49 | } 50 | 51 | // String implements the Stringer interface 52 | func (l Label) String() string { 53 | raw, err := json.MarshalIndent( 54 | l, 55 | " ", 56 | ".", 57 | ) 58 | if err != nil { 59 | panic(err) 60 | } 61 | return string(raw) 62 | } 63 | 64 | // LabelStatusPhase is a typed definition to determine the 65 | // result of executing the label operation 66 | type LabelStatusPhase string 67 | 68 | const ( 69 | // LabelStatusPassed defines a successful labeling 70 | LabelStatusPassed LabelStatusPhase = "Passed" 71 | 72 | // LabelStatusWarning defines the label operation 73 | // that resulted in warnings 74 | LabelStatusWarning LabelStatusPhase = "Warning" 75 | 76 | // LabelStatusFailed defines a failed labeling 77 | LabelStatusFailed LabelStatusPhase = "Failed" 78 | ) 79 | 80 | // ToTaskStatusPhase transforms ApplyStatusPhase to TestResultPhase 81 | func (phase LabelStatusPhase) ToTaskStatusPhase() TaskStatusPhase { 82 | switch phase { 83 | case LabelStatusPassed: 84 | return TaskStatusPassed 85 | case LabelStatusFailed: 86 | return TaskStatusFailed 87 | case LabelStatusWarning: 88 | return TaskStatusWarning 89 | default: 90 | return "" 91 | } 92 | } 93 | 94 | // LabelResult holds the result of labeling operation 95 | type LabelResult struct { 96 | Phase LabelStatusPhase `json:"phase"` 97 | Message string `json:"message,omitempty"` 98 | Verbose string `json:"verbose,omitempty"` 99 | Warning string `json:"warning,omitempty"` 100 | } 101 | -------------------------------------------------------------------------------- /types/recipe/labels.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | const ( 20 | // LblKeyIsRecipeLock is the label key to determine if the 21 | // resource is used to lock the reconciliation of Recipe 22 | // resource. 23 | // 24 | // NOTE: 25 | // This is used to execute reconciliation by only one 26 | // controller goroutine at a time. 27 | // 28 | // NOTE: 29 | // A ConfigMap is used as a lock to achieve above behaviour. 30 | // This ConfigMap will have its labels set with this label key. 31 | LblKeyIsRecipeLock string = "recipe.dope.mayadata.io/lock" 32 | 33 | // LblKeyRecipeName is the label key to determine the name 34 | // of the Recipe that the current resource is associated to 35 | LblKeyRecipeName string = "recipe.dope.mayadata.io/name" 36 | 37 | // LblKeyRecipePhase is the label key to determine the phase 38 | // of the Recipe. This offers an additional way to determine 39 | // the phase of the Recipe apart from Recipe's status.phase 40 | // field. 41 | LblKeyRecipePhase string = "recipe.dope.mayadata.io/phase" 42 | ) 43 | -------------------------------------------------------------------------------- /types/recipe/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 23 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 | ) 25 | 26 | // List represents the desired state that needs to 27 | // be listed from the cluster 28 | type List struct { 29 | // Desired state that needs to be listed from the 30 | // Kubernetes cluster 31 | State *unstructured.Unstructured `json:"state"` 32 | } 33 | 34 | // String implements the Stringer interface 35 | func (l List) String() string { 36 | raw, err := json.MarshalIndent( 37 | l, 38 | " ", 39 | ".", 40 | ) 41 | if err != nil { 42 | panic(err) 43 | } 44 | return string(raw) 45 | } 46 | 47 | // ListStatusPhase is a typed definition to determine the 48 | // result of executing a list invocation 49 | type ListStatusPhase string 50 | 51 | const ( 52 | // ListStatusPassed defines a successful list 53 | ListStatusPassed ListStatusPhase = "Passed" 54 | 55 | // ListStatusWarning defines a list that resulted in warnings 56 | ListStatusWarning ListStatusPhase = "Warning" 57 | 58 | // ListStatusFailed defines a failed list 59 | ListStatusFailed ListStatusPhase = "Failed" 60 | ) 61 | 62 | // ToTaskStatusPhase transforms ListStatusPhase to TaskStatusPhase 63 | func (phase ListStatusPhase) ToTaskStatusPhase() TaskStatusPhase { 64 | switch phase { 65 | case ListStatusPassed: 66 | return TaskStatusPassed 67 | case ListStatusFailed: 68 | return TaskStatusFailed 69 | case ListStatusWarning: 70 | return TaskStatusWarning 71 | default: 72 | return "" 73 | } 74 | } 75 | 76 | // ListResult holds the result of the list operation 77 | type ListResult struct { 78 | Phase ListStatusPhase `json:"phase"` 79 | Message string `json:"message,omitempty"` 80 | Verbose string `json:"verbose,omitempty"` 81 | Warning string `json:"warning,omitempty"` 82 | V1Beta1CRDItems *v1beta1.CustomResourceDefinitionList `json:"v1b1CRDs,omitempty"` 83 | Items *unstructured.UnstructuredList `json:"items,omitempty"` 84 | } 85 | 86 | // String implements the Stringer interface 87 | func (l ListResult) String() string { 88 | raw, err := json.MarshalIndent( 89 | l, 90 | " ", 91 | ".", 92 | ) 93 | if err != nil { 94 | panic(err) 95 | } 96 | return string(raw) 97 | } 98 | -------------------------------------------------------------------------------- /types/recipe/schema.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | // SupportedAbsolutePaths represent the nested field paths that 20 | // are supported by Recipe custom resource schema 21 | // 22 | // NOTE: 23 | // Nested path is represented by field name(s) joined 24 | // by dots i.e. '.' 25 | // 26 | // NOTE: 27 | // Field with list-of-map(s) datatype are appended with [*] 28 | // 29 | // NOTE: 30 | // Whenever the Recipe schema is updated its field path needs to 31 | // be updated at SupportedAbsolutePaths or UserAllowedPathPrefixes 32 | // 33 | // NOTE: 34 | // Each field path set here should represent its absolute field path 35 | var SupportedAbsolutePaths = []string{ 36 | "apiVersion", 37 | "kind", 38 | // spec 39 | "spec.teardown", 40 | "spec.resync.onNotEligibleResyncInSeconds", 41 | "spec.resync.onErrorResyncInSeconds", 42 | "spec.resync.intervalInSeconds", 43 | "spec.eligible.checks.[*].kind", 44 | "spec.eligible.checks.[*].apiVersion", 45 | "spec.eligible.checks.[*].count", 46 | "spec.eligible.checks.[*].when", 47 | "spec.eligible.checks.[*].labelSelector.matchExpressions.[*].key", 48 | "spec.eligible.checks.[*].labelSelector.matchExpressions.[*].operator", 49 | "spec.eligible.checks.[*].labelSelector.matchExpressions.[*].values", 50 | "spec.enabled.when", 51 | // spec.tasks[*] 52 | "spec.tasks.[*].name", 53 | "spec.tasks.[*].failFast.when", 54 | "spec.tasks.[*].ignoreError", 55 | // spec.tasks.[*].create 56 | "spec.tasks.[*].create.ignoreDiscovery", 57 | "spec.tasks.[*].create.replicas", 58 | // spec.tasks.[*].assert 59 | "spec.tasks.[*].assert.stateCheck.stateCheckOperator", 60 | "spec.tasks.[*].assert.stateCheck.count", 61 | "spec.tasks.[*].assert.pathCheck.path", 62 | "spec.tasks.[*].assert.pathCheck.pathCheckOperator", 63 | "spec.tasks.[*].assert.pathCheck.value", 64 | "spec.tasks.[*].assert.pathCheck.dataType", 65 | "spec.tasks.[*].assert.errorOnAssertFailure", 66 | // spec.tasks.[*].apply 67 | "spec.tasks.[*].apply.ignoreDiscovery", 68 | "spec.tasks.[*].apply.replicas", 69 | // spec.tasks.[*].label 70 | "spec.tasks.[*].label.includeByNames", 71 | "spec.tasks.[*].label.autoUnset", 72 | } 73 | 74 | // UserAllowedPathPrefixes represent the nested field paths 75 | // that can have further fields. These fields are not managed 76 | // by Recipe schema & should not be validated as per the schema 77 | // definition. 78 | // 79 | // NOTE: 80 | // 'metadata' is a native Kubernetes type. It is 81 | // dependent on Kubernetes versions and hence makes little sense 82 | // to validate the fields of metadata. UserAllowedPathPrefixes can 83 | // be used to skip such field path(s). 84 | // 85 | // NOTE: 86 | // Each prefix set here must end with a dot i.e. `.` 87 | var UserAllowedPathPrefixes = []string{ 88 | "metadata.", // K8s controlled 89 | "status.", // dope controlled 90 | "spec.tasks.[*].apply.state.", // can be any K8s resource 91 | "spec.tasks.[*].delete.state.", // can be any K8s resource 92 | "spec.tasks.[*].create.state.", // can be any K8s resource 93 | "spec.tasks.[*].assert.state.", // can be any K8s resource 94 | "spec.tasks.[*].label.state.", // can be any K8s resource 95 | "spec.tasks.[*].label.applyLabels.", // can be any K8s labels 96 | "spec.eligible.checks.[*].labelSelector.matchLabels.", // can be any label pairs 97 | } 98 | 99 | type SchemaStatus string 100 | 101 | const ( 102 | // SchemaStatusValid conveys a successful validation 103 | SchemaStatusValid SchemaStatus = "Valid" 104 | 105 | // SchemaStatusInvalid conveys a failed validation 106 | SchemaStatusInvalid SchemaStatus = "Invalid" 107 | ) 108 | 109 | type SchemaFailure struct { 110 | Error string `json:"error"` 111 | Remedy string `json:"remedy,omitempty"` 112 | } 113 | 114 | type SchemaResult struct { 115 | Phase SchemaStatus `json:"phase"` 116 | Failures []SchemaFailure `json:"failures,omitempty"` 117 | Verbose []string `json:"verbose,omitempty"` 118 | } 119 | -------------------------------------------------------------------------------- /types/recipe/state_check.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | // StateCheckOperator defines the check that needs to be 20 | // done against the resource's state 21 | type StateCheckOperator string 22 | 23 | const ( 24 | // StateCheckOperatorEquals verifies if expected state 25 | // matches the observed state found in the cluster 26 | StateCheckOperatorEquals StateCheckOperator = "Equals" 27 | 28 | // StateCheckOperatorNotEquals verifies if expected state 29 | // does not match the observed state found in the cluster 30 | StateCheckOperatorNotEquals StateCheckOperator = "NotEquals" 31 | 32 | // StateCheckOperatorNotFound verifies if expected state 33 | // is not found in the cluster 34 | StateCheckOperatorNotFound StateCheckOperator = "NotFound" 35 | 36 | // StateCheckOperatorListCountEquals verifies if expected 37 | // states matches the observed states found in the cluster 38 | StateCheckOperatorListCountEquals StateCheckOperator = "ListCountEquals" 39 | 40 | // StateCheckOperatorListCountNotEquals verifies if count of 41 | // expected states does not match the count of observed states 42 | // found in the cluster 43 | StateCheckOperatorListCountNotEquals StateCheckOperator = "ListCountNotEquals" 44 | ) 45 | 46 | // StateCheck verifies expected resource state against 47 | // the observed state found in the cluster 48 | type StateCheck struct { 49 | // Check operation performed between the expected state 50 | // and the observed state 51 | Operator StateCheckOperator `json:"stateCheckOperator,omitempty"` 52 | 53 | // Count defines the expected number of observed states 54 | Count *int `json:"count,omitempty"` 55 | } 56 | 57 | // StateCheckResultPhase defines the result of StateCheck operation 58 | type StateCheckResultPhase string 59 | 60 | const ( 61 | // StateCheckResultPassed defines a successful StateCheckResult 62 | StateCheckResultPassed StateCheckResultPhase = "StateCheckPassed" 63 | 64 | // StateCheckResultWarning defines a StateCheckResult that has warnings 65 | StateCheckResultWarning StateCheckResultPhase = "StateCheckWarning" 66 | 67 | // StateCheckResultFailed defines an un-successful StateCheckResult 68 | StateCheckResultFailed StateCheckResultPhase = "StateCheckFailed" 69 | ) 70 | 71 | // ToAssertResultPhase transforms StateCheckResultPhase to AssertResultPhase 72 | func (phase StateCheckResultPhase) ToAssertResultPhase() AssertStatusPhase { 73 | switch phase { 74 | case StateCheckResultPassed: 75 | return AssertResultPassed 76 | case StateCheckResultFailed: 77 | return AssertResultFailed 78 | case StateCheckResultWarning: 79 | return AssertResultWarning 80 | default: 81 | return "" 82 | } 83 | } 84 | 85 | // StateCheckResult holds the result of StateCheck operation 86 | type StateCheckResult struct { 87 | // status as a pre-defined key word 88 | Phase StateCheckResultPhase `json:"phase"` 89 | 90 | // short message 91 | Message string `json:"message,omitempty"` 92 | 93 | // detailed message 94 | Verbose string `json:"verbose,omitempty"` 95 | 96 | // warning details 97 | Warning string `json:"warning,omitempty"` 98 | 99 | // timeout details 100 | Timeout string `json:"timeout,omitempty"` 101 | } 102 | -------------------------------------------------------------------------------- /types/recipe/task.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The MayaData Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package types 18 | 19 | import "encoding/json" 20 | 21 | // FailFastRule defines the condition that leads to fail fast 22 | type FailFastRule string 23 | 24 | const ( 25 | // FailFastOnDiscoveryError defines a fail fast based on 26 | // DiscoveryError 27 | FailFastOnDiscoveryError FailFastRule = "OnDiscoveryError" 28 | ) 29 | 30 | // IgnoreErrorRule defines the rule to ignore an error 31 | type IgnoreErrorRule string 32 | 33 | const ( 34 | // IgnoreErrorAsPassed defines the rule to ignore error 35 | // and treat it as passed 36 | IgnoreErrorAsPassed IgnoreErrorRule = "AsPassed" 37 | 38 | // IgnoreErrorAsWarning defines the rule to ignore error 39 | // and treat it as a warning 40 | IgnoreErrorAsWarning IgnoreErrorRule = "AsWarning" 41 | ) 42 | 43 | // FailFast holds the condition that determines if an error 44 | // should not result in retries and instead be allowed to fail 45 | // immediately 46 | type FailFast struct { 47 | When FailFastRule `json:"when,omitempty"` 48 | } 49 | 50 | // Task that needs to be executed as part of a Recipe 51 | // 52 | // Task forms the fundamental unit of execution within a 53 | // Recipe 54 | type Task struct { 55 | Name string `json:"name"` 56 | Assert *Assert `json:"assert,omitempty"` 57 | Apply *Apply `json:"apply,omitempty"` 58 | Delete *Delete `json:"delete,omitempty"` 59 | Create *Create `json:"create,omitempty"` 60 | Label *Label `json:"label,omitempty"` 61 | IgnoreErrorRule IgnoreErrorRule `json:"ignoreError,omitempty"` 62 | FailFast *FailFast `json:"failFast,omitempty"` 63 | } 64 | 65 | // String implements the Stringer interface 66 | func (t Task) String() string { 67 | raw, err := json.MarshalIndent( 68 | t, 69 | " ", 70 | ".", 71 | ) 72 | if err != nil { 73 | panic(err) 74 | } 75 | return string(raw) 76 | } 77 | 78 | // TaskStatusPhase defines the task execution status 79 | type TaskStatusPhase string 80 | 81 | const ( 82 | // TaskStatusPassed implies a passed task 83 | TaskStatusPassed TaskStatusPhase = "Passed" 84 | 85 | // TaskStatusFailed implies a failed task 86 | TaskStatusFailed TaskStatusPhase = "Failed" 87 | 88 | // TaskStatusWarning implies a failed task 89 | TaskStatusWarning TaskStatusPhase = "Warning" 90 | ) 91 | 92 | // TaskCount holds various counts related to execution of tasks 93 | // specified in the Recipe 94 | type TaskCount struct { 95 | Failed int `json:"failed"` // Number of failed tasks 96 | Skipped int `json:"skipped"` // Number of skipped tasks 97 | Warning int `json:"warning"` // Number of tasks with warnings 98 | Total int `json:"total"` // Total number of tasks in the Recipe 99 | } 100 | 101 | // TaskResult holds task specific execution details 102 | type TaskResult struct { 103 | Step int `json:"step"` 104 | Phase TaskStatusPhase `json:"phase"` 105 | ExecutionTime *ExecutionTime `json:"executionTime,omitempty"` 106 | Internal *bool `json:"internal,omitempty"` 107 | Message string `json:"message,omitempty"` 108 | Verbose string `json:"verbose,omitempty"` 109 | Warning string `json:"warning,omitempty"` 110 | Timeout string `json:"timeout,omitempty"` 111 | } 112 | 113 | // String implements the Stringer interface 114 | func (t TaskResult) String() string { 115 | raw, err := json.MarshalIndent( 116 | t, 117 | " ", 118 | ".", 119 | ) 120 | if err != nil { 121 | panic(err) 122 | } 123 | return string(raw) 124 | } 125 | --------------------------------------------------------------------------------