├── .dockerignore ├── .github └── workflows │ ├── pull_request.yml │ ├── push.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── .vscode └── launch.json ├── Dockerfile ├── Makefile ├── PROJECT ├── README.md ├── api └── v1alpha1 │ ├── customer_types.go │ ├── customer_webhook.go │ ├── groupversion_info.go │ ├── project_types.go │ ├── project_webhook.go │ └── zz_generated.deepcopy.go ├── bundle.Dockerfile ├── bundle ├── manifests │ ├── jira-service-desk-operator-controller-manager-metrics-service_v1_service.yaml │ ├── jira-service-desk-operator-manager-config_v1_configmap.yaml │ ├── jira-service-desk-operator-manager-metrics_v1_service.yaml │ ├── jira-service-desk-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml │ ├── jira-service-desk-operator-webhook-service_v1_service.yaml │ ├── jira-service-desk-operator.clusterserviceversion.yaml │ ├── jiraservicedesk.stakater.com_customers.yaml │ └── jiraservicedesk.stakater.com_projects.yaml ├── metadata │ └── annotations.yaml └── tests │ └── scorecard │ └── config.yaml ├── charts └── jira-service-desk-operator │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── crds │ ├── jiraservicedesk.stakater.com_customers.yaml │ └── jiraservicedesk.stakater.com_projects.yaml │ ├── templates │ ├── _helpers.tpl │ ├── certificate.yaml │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── deployment.yaml │ ├── mutating-webhook.yaml │ ├── role.yaml │ ├── rolebinding.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ ├── servicemonitor.yaml │ ├── tests │ │ └── test-connection.yaml │ └── validating-webhook.yaml │ └── values.yaml ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── bases │ │ ├── jiraservicedesk.stakater.com_customers.yaml │ │ └── jiraservicedesk.stakater.com_projects.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_customers.yaml │ │ ├── cainjection_in_projects.yaml │ │ ├── webhook_in_customers.yaml │ │ └── webhook_in_projects.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_config_patch.yaml │ ├── manager_webhook_patch.yaml │ └── webhookcainjection_patch.yaml ├── manager │ ├── controller_manager_config.yaml │ ├── kustomization.yaml │ └── manager.yaml ├── manifests │ ├── bases │ │ └── jira-service-desk-operator.clusterserviceversion.yaml │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── customer_editor_role.yaml │ ├── customer_viewer_role.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── project_editor_role.yaml │ ├── project_viewer_role.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── samples │ ├── jiraservicedesk_v1alpha1_customer.yaml │ ├── jiraservicedesk_v1alpha1_project.yaml │ └── kustomization.yaml ├── scorecard │ ├── bases │ │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ │ ├── basic.config.yaml │ │ └── olm.config.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ ├── manifests.yaml │ └── service.yaml ├── controllers ├── customer_controller.go ├── customer_controller_test.go ├── project_controller.go ├── project_controller_test.go ├── suite_test.go └── util │ └── testUtil.go ├── examples ├── customer │ ├── customer.yaml │ └── legacy-customer.yaml └── project │ ├── classic-project.yaml │ └── next-gen-project.yaml ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── main.go ├── mock └── data.go └── pkg └── jiraservicedesk ├── client ├── client.go ├── client_customer.go ├── client_customer_mappers.go ├── client_customer_test.go ├── client_project.go ├── client_project_mappers.go └── client_project_test.go ├── config └── config.go └── jiraservicedesk.go /.dockerignore: -------------------------------------------------------------------------------- 1 | ### Git ### 2 | .git* 3 | 4 | ### GitHub Actions ### 5 | /.github/ 6 | 7 | # Dependency directories 8 | .local 9 | /bin/ 10 | /config/ -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | env: 9 | DOCKER_FILE_PATH: Dockerfile 10 | GOLANG_VERSION: 1.17 11 | KUBERNETES_VERSION: "1.23.0" 12 | KIND_VERSION: "0.10.0" 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | name: Build 18 | if: "! contains(toJSON(github.event.commits.*.message), '[skip-ci]')" 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@v2 22 | with: 23 | ref: ${{github.event.pull_request.head.sha}} 24 | 25 | - name: Set up Go 26 | id: go 27 | uses: actions/setup-go@v2 28 | with: 29 | go-version: ${{ env.GOLANG_VERSION }} 30 | 31 | - name: Lint 32 | uses: golangci/golangci-lint-action@v2.3.0 33 | with: 34 | version: v1.45.2 35 | only-new-issues: false 36 | args: --timeout 10m 37 | 38 | - name: Install kubectl 39 | run: | 40 | curl -LO "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl" 41 | sudo install ./kubectl /usr/local/bin/ && rm kubectl 42 | kubectl version --short --client 43 | kubectl version --short --client | grep -q ${KUBERNETES_VERSION} 44 | 45 | - name: Install Kind 46 | run: | 47 | curl -L -o kind https://github.com/kubernetes-sigs/kind/releases/download/v${KIND_VERSION}/kind-linux-amd64 48 | sudo install ./kind /usr/local/bin && rm kind 49 | kind version 50 | kind version | grep -q ${KIND_VERSION} 51 | 52 | - name: Create Kind Cluster 53 | run: | 54 | kind create cluster 55 | 56 | - name: Set up Cluster 57 | run: | 58 | kubectl cluster-info 59 | kubectl create namespace test 60 | echo "${{ secrets.JIRA_SERVICE_DESK_CONFIG }}"> kube.yaml 61 | kubectl apply -f kube.yaml 62 | rm -f kube.yaml 63 | 64 | - name: Test 65 | run: make test OPERATOR_NAMESPACE=test USE_EXISTING_CLUSTER=true 66 | 67 | - name: Generate Tag 68 | id: generate_tag 69 | run: | 70 | sha=${{ github.event.pull_request.head.sha }} 71 | tag="SNAPSHOT-PR-${{ github.event.pull_request.number }}-${sha:0:8}" 72 | echo "##[set-output name=GIT_TAG;]$(echo ${tag})" 73 | 74 | - name: Set up QEMU 75 | uses: docker/setup-qemu-action@v1 76 | 77 | - name: Set up Docker Buildx 78 | uses: docker/setup-buildx-action@v1 79 | 80 | - name: Login to Registry 81 | uses: docker/login-action@v1 82 | with: 83 | username: ${{ secrets.STAKATER_DOCKERHUB_USERNAME }} 84 | password: ${{ secrets.STAKATER_DOCKERHUB_PASSWORD }} 85 | 86 | - name: Generate image repository path 87 | run: | 88 | echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV 89 | 90 | - name: Build and Push Docker Image 91 | uses: docker/build-push-action@v2 92 | with: 93 | context: . 94 | file: ${{ env.DOCKER_FILE_PATH }} 95 | pull: true 96 | push: true 97 | build-args: BUILD_PARAMETERS=${{ env.BUILD_PARAMETERS }} 98 | cache-to: type=inline 99 | tags: | 100 | ${{ env.IMAGE_REPOSITORY }}:${{ steps.generate_tag.outputs.GIT_TAG }} 101 | labels: | 102 | org.opencontainers.image.source=${{ github.event.repository.clone_url }} 103 | org.opencontainers.image.created=${{ steps.prep.outputs.created }} 104 | org.opencontainers.image.revision=${{ github.sha }} 105 | 106 | - name: Comment on PR 107 | uses: mshick/add-pr-comment@v1 108 | env: 109 | GITHUB_TOKEN: ${{ secrets.STAKATER_GITHUB_TOKEN }} 110 | with: 111 | message: '@${{ github.actor }} Image is available for testing. `docker pull ${{ github.repository }}:${{ steps.generate_tag.outputs.GIT_TAG }}`' 112 | allow-repeats: false 113 | 114 | - name: Notify Failure 115 | if: failure() 116 | uses: mshick/add-pr-comment@v1 117 | env: 118 | GITHUB_TOKEN: ${{ secrets.STAKATER_GITHUB_TOKEN }} 119 | with: 120 | message: '@${{ github.actor }} Yikes! You better fix it before anyone else finds out! [Build](https://github.com/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha }}/checks) has Failed!' 121 | allow-repeats: false 122 | 123 | - name: Notify Slack 124 | uses: 8398a7/action-slack@v3 125 | if: always() # Pick up events even if the job fails or is canceled. 126 | with: 127 | status: ${{ job.status }} 128 | fields: repo,author,action,eventName,ref,workflow 129 | env: 130 | GITHUB_TOKEN: ${{ secrets.STAKATER_GITHUB_TOKEN }} 131 | SLACK_WEBHOOK_URL: ${{ secrets.STAKATER_DELIVERY_SLACK_WEBHOOK }} 132 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | env: 9 | DOCKER_FILE_PATH: Dockerfile 10 | GOLANG_VERSION: 1.17 11 | OPERATOR_SDK_VERSION: "1.20.0" 12 | KUSTOMIZE_VERSION: "3.5.4" 13 | KUBERNETES_VERSION: "1.23.0" 14 | KIND_VERSION: "0.10.0" 15 | 16 | jobs: 17 | build: 18 | name: Build 19 | if: "! contains(toJSON(github.event.commits.*.message), '[skip-ci]')" 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Check out code 24 | uses: actions/checkout@v2 25 | with: 26 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token 27 | fetch-depth: 0 # otherwise, you will fail to push refs to dest repo 28 | 29 | - name: Set up Go 30 | id: go 31 | uses: actions/setup-go@v2 32 | with: 33 | go-version: ${{ env.GOLANG_VERSION }} 34 | 35 | - name: Lint 36 | uses: golangci/golangci-lint-action@v2.3.0 37 | with: 38 | version: v1.45.2 39 | only-new-issues: false 40 | args: --timeout 10m 41 | 42 | - name: Install kubectl 43 | run: | 44 | curl -LO "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl" 45 | sudo install ./kubectl /usr/local/bin/ && rm kubectl 46 | kubectl version --short --client 47 | kubectl version --short --client | grep -q ${KUBERNETES_VERSION} 48 | 49 | - name: Install Kind 50 | run: | 51 | curl -L -o kind https://github.com/kubernetes-sigs/kind/releases/download/v${KIND_VERSION}/kind-linux-amd64 52 | sudo install ./kind /usr/local/bin && rm kind 53 | kind version 54 | kind version | grep -q ${KIND_VERSION} 55 | 56 | - name: Create Kind Cluster 57 | run: | 58 | kind create cluster 59 | 60 | - name: Set up Cluster 61 | run: | 62 | kubectl cluster-info 63 | kubectl create namespace test 64 | echo "${{ secrets.JIRA_SERVICE_DESK_CONFIG }}"> kube.yaml 65 | kubectl apply -f kube.yaml 66 | rm -f kube.yaml 67 | 68 | - name: Test 69 | run: make test OPERATOR_NAMESPACE=test USE_EXISTING_CLUSTER=true 70 | 71 | - name: Generate Tag 72 | id: generate_tag 73 | uses: anothrNick/github-tag-action@1.36.0 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.STAKATER_GITHUB_TOKEN }} 76 | WITH_V: true 77 | DEFAULT_BUMP: patch 78 | DRY_RUN: true 79 | 80 | - name: Set up QEMU 81 | uses: docker/setup-qemu-action@v1 82 | 83 | - name: Set up Docker Buildx 84 | uses: docker/setup-buildx-action@v1 85 | 86 | - name: Login to Registry 87 | uses: docker/login-action@v1 88 | with: 89 | username: ${{ secrets.STAKATER_DOCKERHUB_USERNAME }} 90 | password: ${{ secrets.STAKATER_DOCKERHUB_PASSWORD }} 91 | 92 | - name: Generate image repository path 93 | run: | 94 | echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV 95 | 96 | - name: Build and push 97 | uses: docker/build-push-action@v2 98 | with: 99 | context: . 100 | file: ${{ env.DOCKER_FILE_PATH }} 101 | pull: true 102 | push: true 103 | build-args: BUILD_PARAMETERS=${{ env.BUILD_PARAMETERS }} 104 | cache-to: type=inline 105 | tags: | 106 | ${{ env.IMAGE_REPOSITORY }}:${{ steps.generate_tag.outputs.new_tag }} 107 | labels: | 108 | org.opencontainers.image.source=${{ github.event.repository.clone_url }} 109 | org.opencontainers.image.created=${{ steps.prep.outputs.created }} 110 | org.opencontainers.image.revision=${{ github.sha }} 111 | 112 | ############################## 113 | ## Add steps to generate required artifacts for a release here(helm chart, operator manifest etc.) 114 | ############################## 115 | 116 | # Generate tag for operator without "v" 117 | - name: Generate Operator Tag 118 | id: generate_operator_tag 119 | uses: anothrNick/github-tag-action@1.36.0 120 | env: 121 | GITHUB_TOKEN: ${{ secrets.STAKATER_GITHUB_TOKEN }} 122 | WITH_V: false 123 | DEFAULT_BUMP: patch 124 | DRY_RUN: true 125 | 126 | # Install operator-sdk 127 | - name: Install operator-sdk 128 | env: 129 | OPERATOR_SDK_VERSION: ${{ env.OPERATOR_SDK_VERSION }} 130 | run: | 131 | curl -fL -o /tmp/operator-sdk "https://github.com/operator-framework/operator-sdk/releases/download/v${OPERATOR_SDK_VERSION}/operator-sdk_linux_amd64" 132 | sudo install /tmp/operator-sdk /usr/local/bin && rm -f /tmp/operator-sdk 133 | operator-sdk version 134 | operator-sdk version | grep -q "${OPERATOR_SDK_VERSION}" 135 | 136 | # Install kustomize 137 | - uses: imranismail/setup-kustomize@v1 138 | with: 139 | kustomize-version: ${{ env.KUSTOMIZE_VERSION }} 140 | 141 | - name: Generate Bundle 142 | env: 143 | VERSION: ${{ steps.generate_operator_tag.outputs.new_tag }} 144 | run: make bundle 145 | 146 | - name: Update Chart CRDs 147 | run: make generate-crds 148 | 149 | # Update chart tag to the latest semver tag 150 | - name: Update Chart Version 151 | env: 152 | VERSION: ${{ steps.generate_operator_tag.outputs.new_tag }} 153 | run: make bump-chart 154 | 155 | # Publish helm chart 156 | - name: Publish Helm chart 157 | uses: stefanprodan/helm-gh-pages@master 158 | with: 159 | branch: master 160 | repository: stakater-charts 161 | target_dir: docs 162 | token: ${{ secrets.STAKATER_GITHUB_TOKEN }} 163 | charts_dir: charts 164 | charts_url: https://stakater.github.io/stakater-charts 165 | owner: stakater 166 | linting: off 167 | commit_username: stakater-user 168 | commit_email: stakater@gmail.com 169 | 170 | # Commit back changes 171 | - name: Commit files 172 | run: | 173 | git config --local user.email "stakater@gmail.com" 174 | git config --local user.name "stakater-user" 175 | git status 176 | git add . 177 | git commit -m "[skip-ci] Update artifacts" -a 178 | 179 | - name: Push changes 180 | uses: ad-m/github-push-action@master 181 | with: 182 | github_token: ${{ secrets.STAKATER_GITHUB_TOKEN }} 183 | 184 | - name: Push Latest Tag 185 | uses: anothrNick/github-tag-action@1.36.0 186 | env: 187 | GITHUB_TOKEN: ${{ secrets.STAKATER_GITHUB_TOKEN }} 188 | WITH_V: true 189 | DEFAULT_BUMP: patch 190 | 191 | - name: Notify Slack 192 | uses: 8398a7/action-slack@v3 193 | if: always() # Pick up events even if the job fails or is canceled. 194 | with: 195 | status: ${{ job.status }} 196 | fields: repo,author,action,eventName,ref,workflow 197 | env: 198 | GITHUB_TOKEN: ${{ secrets.STAKATER_GITHUB_TOKEN }} 199 | SLACK_WEBHOOK_URL: ${{ secrets.STAKATER_DELIVERY_SLACK_WEBHOOK }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Go project 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | env: 9 | GOLANG_VERSION: 1.17 10 | 11 | jobs: 12 | build: 13 | name: GoReleaser build 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 # See: https://goreleaser.com/ci/actions/ 21 | 22 | - name: Set up Go 1.x 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: ${{ env.GOLANG_VERSION }} 26 | id: go 27 | 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@master 30 | with: 31 | version: latest 32 | args: release --rm-dist 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.STAKATER_GITHUB_TOKEN }} 35 | 36 | - name: Notify Slack 37 | uses: 8398a7/action-slack@v3 38 | if: always() 39 | with: 40 | status: ${{ job.status }} 41 | fields: repo,author,action,eventName,ref,workflow 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.STAKATER_GITHUB_TOKEN }} 44 | SLACK_WEBHOOK_URL: ${{ secrets.STAKATER_DELIVERY_SLACK_WEBHOOK }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Kubernetes Generated files - skip generated files, except for vendored files 17 | 18 | !vendor/**/zz_generated.* 19 | 20 | # editor and IDE paraphernalia 21 | .idea 22 | *.swp 23 | *.swo 24 | *~ 25 | /_output/ 26 | /jira-service-desk-operator 27 | /.local/ 28 | testbin/ 29 | kube* -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | - go generate ./... 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - windows 11 | - darwin 12 | archives: 13 | - replacements: 14 | darwin: Darwin 15 | linux: Linux 16 | windows: Windows 17 | 386: i386 18 | amd64: x86_64 19 | checksum: 20 | name_template: 'checksums.txt' 21 | snapshot: 22 | name_template: "{{ .Tag }}-next" 23 | changelog: 24 | sort: asc 25 | filters: 26 | exclude: 27 | - '^docs:' 28 | - '^test:' 29 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}/main.go", 13 | "env": { 14 | "OPERATOR_NAMESPACE":"stakater-slack", 15 | "ENABLE_WEBHOOKS":"false" 16 | } 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.17 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | COPY api/ api/ 15 | COPY controllers/ controllers/ 16 | COPY pkg/ pkg/ 17 | 18 | # Build 19 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -mod=mod -a -o manager main.go 20 | 21 | # Use distroless as minimal base image to package the manager binary 22 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 23 | FROM gcr.io/distroless/static:nonroot 24 | WORKDIR / 25 | COPY --from=builder /workspace/manager . 26 | USER 65532:65532 27 | 28 | ENTRYPOINT ["/manager"] 29 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: stakater.com 2 | layout: 3 | - go.kubebuilder.io/v3 4 | plugins: 5 | manifests.sdk.operatorframework.io/v2: {} 6 | scorecard.sdk.operatorframework.io/v2: {} 7 | projectName: jira-service-desk-operator 8 | repo: github.com/stakater/jira-service-desk-operator 9 | resources: 10 | - api: 11 | crdVersion: v1 12 | namespaced: true 13 | controller: true 14 | domain: stakater.com 15 | group: jiraservicedesk 16 | kind: Customer 17 | path: github.com/stakater/jira-service-desk-operator/api/v1alpha1 18 | version: v1alpha1 19 | webhooks: 20 | defaulting: true 21 | validation: true 22 | webhookVersion: v1 23 | - api: 24 | crdVersion: v1 25 | namespaced: true 26 | controller: true 27 | domain: stakater.com 28 | group: jiraservicedesk 29 | kind: Project 30 | path: github.com/stakater/jira-service-desk-operator/api/v1alpha1 31 | version: v1alpha1 32 | webhooks: 33 | defaulting: true 34 | validation: true 35 | webhookVersion: v1 36 | version: "3" 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jira-service-desk-operator 2 | 3 | Kubernetes operator for Jira Service Desk 4 | 5 | ## About 6 | 7 | Jira service desk(JSD) operator is used to automate the process of setting up JSD for alertmanager in a k8s native way. By using CRDs it lets you: 8 | 9 | 1. Manage Projects 10 | 2. Manage customer/organization for projects 11 | 3. Configure Issues 12 | 13 | It uses [Jira REST API](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/) in it's underlying layer and can be extended to perform other tasks that are supported via the REST API. 14 | 15 | ### Project 16 | 17 | We support the following CRUD operation on project via our Jira Service Desk Operator: 18 | 19 | * Create - Creates a new projects with the provided fields 20 | * Update - Updates an existing project with the updated fields 21 | * Delete - Removes and deletes the project 22 | 23 | Examples for Project Custom Resource can be found [here](https://github.com/stakater/jira-service-desk-operator/tree/master/examples/project). 24 | 25 | #### Limitations 26 | 27 | * We only support creating three types of JSD projects via our operator i.e Business, ServiceDesk, Software. The details and differences between these project types can be viewed [here](https://confluence.atlassian.com/adminjiraserver/jira-applications-and-project-types-overview-938846805.html). 28 | * Following are the immutable fields that cannot be updated: 29 | * ProjectTemplateKey 30 | * ProjectTypeKey 31 | * LeadAccountId 32 | * CategoryId 33 | * NotificationScheme 34 | * PermissionScheme 35 | * IssueSecurityScheme 36 | * OpenAccess 37 | 38 | You can read more about these fields on [Jira Service Desk api docs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-post). 39 | 40 | 41 | ### Customer 42 | 43 | We support the following CRUD operations on customer via our Jira Service Desk Operator 44 | * Create - Create a new customer and assign the projects mentioned in the CR 45 | * Update - Only updates(add/remove) the associated projects mentioned in the CR 46 | * Delete - Remove all the project associations and deletes the customer 47 | 48 | Examples for Customer Custom Resource can be found [here](https://github.com/stakater/jira-service-desk-operator/tree/master/examples/customer). 49 | 50 | #### Limitations 51 | 52 | * Jira Service Desk Operator can access only those customers which are created through it. Customers that are manually created and added in the projects can’t be accessed later with the Jira Service Desk Operator. 53 | * Each custom resource is associated to a single customer. 54 | * You can not update **customer name and email**. 55 | * Once a customer is created, no signup link is sent to the customer email. The customer then has to signup on the help center manually with his provided email to access the projects associated with him on the customer portal. 56 | 57 | To resolve the sign up link limitation during customer creation, we have introduced the legacy customer flag in customer CR. When the flag is true, customer is created using the Jira legacy API and a signup link is sent to his email. However, customer name can't be set while creating a legacy customer. The customer name is set equivalent to customer email by default. Once the customer signs up using the signup link, the customer name is updated to the new provided value during the signup. 58 | 59 | 60 | ## Usage 61 | 62 | ### Prerequisites 63 | 64 | - Atlassian account 65 | - API Token to access Jira REST API (https://id.atlassian.com/manage-profile/security/api-tokens) 66 | 67 | ### Create secret 68 | 69 | Create the following secret which is required for jira-service-desk-operator: 70 | 71 | ```yaml 72 | kind: Secret 73 | apiVersion: v1 74 | metadata: 75 | name: jira-service-desk-config 76 | namespace: default 77 | data: 78 | JIRA_SERVICE_DESK_API_TOKEN: 79 | #Example: https://stakater-cloud.atlassian.net/ 80 | JIRA_SERVICE_DESK_API_BASE_URL: 81 | JIRA_SERVICE_DESK_EMAIL: 82 | type: Opaque 83 | ``` 84 | 85 | ### Deploy operator 86 | 87 | - Make sure that [certman](https://cert-manager.io/) is deployed in your cluster since webhooks require certman to generate valid certs since webhooks serve using HTTPS 88 | - To install certman 89 | ```terminal 90 | $ kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.16.1/cert-manager.yaml 91 | ``` 92 | - Deploy operator 93 | ```terminal 94 | $ oc apply -f bundle/manifests 95 | ``` 96 | 97 | ## Local Development 98 | 99 | - [Operator-sdk v1.20.0](https://github.com/operator-framework/operator-sdk/releases/tag/v1.20.0) is required for local development. 100 | 101 | 1. Create `jira-service-desk-config` secret 102 | 2. Run `make run ENABLE_WEBHOOKS=false WATCH_NAMESPACE=default OPERATOR_NAMESPACE=default` where `WATCH_NAMESPACE` denotes the namespaces that the operator is supposed to watch and `OPERATOR_NAMESPACE` is the namespace in which it's supposed to be deployed. 103 | 104 | 3. Before committing your changes run the following to ensure that everything is verified and up-to-date: 105 | 106 | - `make verify` 107 | - `make bundle` 108 | - `make packagemanifests` 109 | 110 | ## Running Tests 111 | 112 | ### Pre-requisites 113 | 114 | 1. Create a namespace with the name `test` 115 | 2. Create `jira-service-desk-config` secret in test namespace 116 | 117 | ### To run tests 118 | 119 | Use the following command to run tests: 120 | `make test OPERATOR_NAMESPACE=test USE_EXISTING_CLUSTER=true` 121 | -------------------------------------------------------------------------------- /api/v1alpha1/customer_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "strings" 23 | 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | ) 26 | 27 | const ( 28 | invalidUpdateErrorMsg string = " is an immutable field and can not be modified." 29 | duplicateKeysErr string = "Duplicate Project Keys are not allowed" 30 | ) 31 | 32 | // CustomerSpec defines the desired state of Customer 33 | type CustomerSpec struct { 34 | // Name of the customer 35 | // +required 36 | Name string `json:"name"` 37 | 38 | // Email of the customer 39 | // +kubebuilder:validation:Pattern=\S+@\S+\.\S+ 40 | // +required 41 | Email string `json:"email"` 42 | 43 | // LegacyCustomer is a boolean flag that represents whether a customer is created using legacy API or not 44 | // In case of a legacy Customer, a signup link is sent to the customer email which he can than use to signup 45 | // In case of a normal Customer, no signup link is sent to the customer. The customer than has to signup manually using the portal 46 | // If not given, default behaviour is false i.e. normal customer 47 | // +optional 48 | LegacyCustomer bool `json:"legacyCustomer,omitempty"` 49 | 50 | // List of ProjectKeys in which customer will be added 51 | // +kubebuilder:validation:MinItems=1 52 | // +required 53 | Projects []string `json:"projects"` 54 | } 55 | 56 | // CustomerStatus defines the observed state of Customer 57 | type CustomerStatus struct { 58 | // Jira Service Desk Customer Account Id 59 | CustomerId string `json:"customerId"` 60 | 61 | // List of ProjectKeys in which customer has bee added 62 | AssociatedProjects []string `json:"associatedProjects,omitempty"` 63 | 64 | // Status conditions 65 | Conditions []metav1.Condition `json:"conditions,omitempty"` 66 | } 67 | 68 | //+kubebuilder:object:root=true 69 | //+kubebuilder:subresource:status 70 | 71 | // Customer is the Schema for the customers API 72 | type Customer struct { 73 | metav1.TypeMeta `json:",inline"` 74 | metav1.ObjectMeta `json:"metadata,omitempty"` 75 | 76 | Spec CustomerSpec `json:"spec,omitempty"` 77 | Status CustomerStatus `json:"status,omitempty"` 78 | } 79 | 80 | //+kubebuilder:object:root=true 81 | 82 | // CustomerList contains a list of Customer 83 | type CustomerList struct { 84 | metav1.TypeMeta `json:",inline"` 85 | metav1.ListMeta `json:"metadata,omitempty"` 86 | Items []Customer `json:"items"` 87 | } 88 | 89 | func init() { 90 | SchemeBuilder.Register(&Customer{}, &CustomerList{}) 91 | } 92 | 93 | func (customer *Customer) GetReconcileStatus() []metav1.Condition { 94 | return customer.Status.Conditions 95 | } 96 | 97 | func (customer *Customer) SetReconcileStatus(reconcileStatus []metav1.Condition) { 98 | customer.Status.Conditions = reconcileStatus 99 | } 100 | 101 | func (customer *Customer) IsValid() (bool, error) { 102 | 103 | if duplicateKeysExist(customer.Spec.Projects) { 104 | return false, errors.New(duplicateKeysErr) 105 | } 106 | 107 | return true, nil 108 | } 109 | 110 | func (customer *Customer) IsValidUpdate(existingCustomer Customer) (bool, error) { 111 | 112 | if !strings.EqualFold(customer.Spec.Email, existingCustomer.Spec.Email) { 113 | return false, fmt.Errorf("%s %s", "CustomerEmail", invalidUpdateErrorMsg) 114 | } 115 | if customer.Spec.Name != existingCustomer.Spec.Name { 116 | return false, fmt.Errorf("%s %s", "CustomerName", invalidUpdateErrorMsg) 117 | } 118 | 119 | if customer.Spec.LegacyCustomer != existingCustomer.Spec.LegacyCustomer { 120 | return false, fmt.Errorf("%s %s", "LegacyCustomer", invalidUpdateErrorMsg) 121 | } 122 | 123 | if duplicateKeysExist(customer.Spec.Projects) { 124 | return false, errors.New(duplicateKeysErr) 125 | } 126 | 127 | return true, nil 128 | } 129 | 130 | func (customer *Customer) IsValidCustomerUpdate(existingCustomer Customer) (bool, error) { 131 | if !strings.EqualFold(customer.Spec.Email, existingCustomer.Spec.Email) { 132 | // It takes a few seconds for customers to be persisted at JSD. Check if it's pending with known values are return. 133 | if existingCustomer.Spec.Name == "User " && existingCustomer.Spec.Email == "?" { 134 | return true, nil 135 | } 136 | return false, fmt.Errorf("%s %s", "Customer email", invalidUpdateErrorMsg) 137 | } 138 | 139 | return true, nil 140 | } 141 | 142 | func duplicateKeysExist(projectKeys []string) bool { 143 | keys := make(map[string]bool) 144 | 145 | for _, entry := range projectKeys { 146 | if _, value := keys[entry]; !value { 147 | keys[entry] = true 148 | } else { 149 | return true 150 | } 151 | } 152 | return false 153 | } 154 | -------------------------------------------------------------------------------- /api/v1alpha1/customer_webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "fmt" 21 | 22 | "k8s.io/apimachinery/pkg/runtime" 23 | ctrl "sigs.k8s.io/controller-runtime" 24 | logf "sigs.k8s.io/controller-runtime/pkg/log" 25 | "sigs.k8s.io/controller-runtime/pkg/webhook" 26 | ) 27 | 28 | // log is for logging in this package. 29 | var customerlog = logf.Log.WithName("customer-resource") 30 | 31 | func (r *Customer) SetupWebhookWithManager(mgr ctrl.Manager) error { 32 | return ctrl.NewWebhookManagedBy(mgr). 33 | For(r). 34 | Complete() 35 | } 36 | 37 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 38 | 39 | //+kubebuilder:webhook:path=/mutate-jiraservicedesk-stakater-com-v1alpha1-customer,mutating=true,failurePolicy=fail,sideEffects=None,groups=jiraservicedesk.stakater.com,resources=customers,verbs=create;update,versions=v1alpha1,name=mcustomer.kb.io,admissionReviewVersions=v1 40 | 41 | var _ webhook.Defaulter = &Customer{} 42 | 43 | // Default implements webhook.Defaulter so a webhook will be registered for the type 44 | func (r *Customer) Default() { 45 | customerlog.Info("default", "name", r.Name) 46 | 47 | // TODO(user): fill in your defaulting logic. 48 | } 49 | 50 | // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. 51 | //+kubebuilder:webhook:path=/validate-jiraservicedesk-stakater-com-v1alpha1-customer,mutating=false,failurePolicy=fail,sideEffects=None,groups=jiraservicedesk.stakater.com,resources=customers,verbs=create;update,versions=v1alpha1,name=vcustomer.kb.io,admissionReviewVersions=v1 52 | 53 | var _ webhook.Validator = &Customer{} 54 | 55 | // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 56 | func (r *Customer) ValidateCreate() error { 57 | customerlog.Info("validate create", "name", r.Name) 58 | 59 | _, err := r.IsValid() 60 | return err 61 | } 62 | 63 | // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 64 | func (r *Customer) ValidateUpdate(old runtime.Object) error { 65 | customerlog.Info("validate update", "name", r.Name) 66 | 67 | oldCustomer, ok := old.(*Customer) 68 | if !ok { 69 | return fmt.Errorf("Error casting old runtime object to %T from %T", oldCustomer, old) 70 | } 71 | 72 | _, err := r.IsValid() 73 | if err != nil { 74 | return err 75 | } 76 | _, err = r.IsValidUpdate(*oldCustomer) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 85 | func (r *Customer) ValidateDelete() error { 86 | customerlog.Info("validate delete", "name", r.Name) 87 | 88 | // TODO(user): fill in your validation logic upon object deletion. 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the jiraservicedesk v1alpha1 API group 18 | //+kubebuilder:object:generate=true 19 | //+groupName=jiraservicedesk.stakater.com 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "jiraservicedesk.stakater.com", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1alpha1/project_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "fmt" 21 | 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | const ( 26 | errorImmutableFieldMsg string = "is an immutable field, can't be changed while updating" 27 | ) 28 | 29 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 30 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 31 | 32 | // ProjectSpec defines the desired state of Project 33 | type ProjectSpec struct { 34 | 35 | // Name of the project 36 | // +required 37 | Name string `json:"name"` 38 | 39 | // The project key is used as the prefix of your project's issue keys 40 | // +kubebuilder:validation:MaxLength=10 41 | // +kubebuilder:validation:Pattern=^[A-Z][A-Z0-9]+$ 42 | // +required 43 | Key string `json:"key"` 44 | 45 | // The project type, which dictates the application-specific feature set 46 | // +kubebuilder:validation:Enum=business;service_desk;software 47 | // +required 48 | ProjectTypeKey string `json:"projectTypeKey"` 49 | 50 | // A prebuilt configuration for a project 51 | // +required 52 | ProjectTemplateKey string `json:"projectTemplateKey"` 53 | 54 | // Description for project 55 | // +required 56 | Description string `json:"description"` 57 | 58 | // Task assignee type 59 | // +kubebuilder:validation:Enum=PROJECT_LEAD;UNASSIGNED 60 | // +required 61 | AssigneeType string `json:"assigneeType"` 62 | 63 | // ID of project lead 64 | // +kubebuilder:validation:MaxLength=128 65 | // +required 66 | LeadAccountId string `json:"leadAccountId"` 67 | 68 | // A link to information about this project, such as project documentation 69 | // +kubebuilder:validation:Pattern="(http|ftp|https)://([a-zA-Z0-9~!@#$%^&*()_=+/?.:;',-]*)?" 70 | // +optional 71 | URL string `json:"url,omitempty"` 72 | 73 | // An integer value for the project's avatar. 74 | // +optional 75 | AvatarId int `json:"avatarId,omitempty"` 76 | 77 | // The ID of the issue security scheme for the project, which enables you to control who can and cannot view issues 78 | // +optional 79 | IssueSecurityScheme int `json:"issueSecurityScheme,omitempty"` 80 | 81 | // The ID of the permission scheme for the project 82 | // +optional 83 | PermissionScheme int `json:"permissionScheme,omitempty"` 84 | 85 | // The ID of the notification scheme for the project 86 | // +optional 87 | NotificationScheme int `json:"notificationScheme,omitempty"` 88 | 89 | // The ID of the project's category 90 | // +optional 91 | CategoryId int `json:"categoryId,omitempty"` 92 | 93 | // The Open Access status, which dictates who can access the project. If set to true all customers can access the project. If false, only customers added to project can access the project. 94 | // +optional, if not provided default behaviour is False 95 | OpenAccess bool `json:"openAccess,omitempty"` 96 | } 97 | 98 | // ProjectStatus defines the observed state of Project 99 | type ProjectStatus struct { 100 | // Jira service desk project ID 101 | ID string `json:"id"` 102 | 103 | // Status conditions 104 | Conditions []metav1.Condition `json:"conditions,omitempty"` 105 | } 106 | 107 | //+kubebuilder:object:root=true 108 | //+kubebuilder:subresource:status 109 | 110 | // Project is the Schema for the projects API 111 | type Project struct { 112 | metav1.TypeMeta `json:",inline"` 113 | metav1.ObjectMeta `json:"metadata,omitempty"` 114 | 115 | Spec ProjectSpec `json:"spec,omitempty"` 116 | Status ProjectStatus `json:"status,omitempty"` 117 | } 118 | 119 | //+kubebuilder:object:root=true 120 | 121 | // ProjectList contains a list of Project 122 | type ProjectList struct { 123 | metav1.TypeMeta `json:",inline"` 124 | metav1.ListMeta `json:"metadata,omitempty"` 125 | Items []Project `json:"items"` 126 | } 127 | 128 | func init() { 129 | SchemeBuilder.Register(&Project{}, &ProjectList{}) 130 | } 131 | 132 | func (project *Project) GetReconcileStatus() []metav1.Condition { 133 | return project.Status.Conditions 134 | } 135 | 136 | func (project *Project) SetReconcileStatus(reconcileStatus []metav1.Condition) { 137 | project.Status.Conditions = reconcileStatus 138 | } 139 | 140 | func (project *Project) IsValid() (bool, error) { 141 | // TODO: Add logic for additional validation here 142 | return true, nil 143 | } 144 | 145 | func (project *Project) IsValidUpdate(existingProject Project) (bool, error) { 146 | 147 | if project.Spec.ProjectTemplateKey != existingProject.Spec.ProjectTemplateKey { 148 | return false, fmt.Errorf("%s %s", "ProjectTemplateKey", errorImmutableFieldMsg) 149 | } 150 | if project.Spec.ProjectTypeKey != existingProject.Spec.ProjectTypeKey { 151 | return false, fmt.Errorf("%s %s", "ProjectTypeKey", errorImmutableFieldMsg) 152 | } 153 | if project.Spec.LeadAccountId != existingProject.Spec.LeadAccountId { 154 | return false, fmt.Errorf("%s %s", "LeadAccountId", errorImmutableFieldMsg) 155 | } 156 | if project.Spec.CategoryId != existingProject.Spec.CategoryId { 157 | return false, fmt.Errorf("%s %s", "CategoryId", errorImmutableFieldMsg) 158 | } 159 | if project.Spec.NotificationScheme != existingProject.Spec.NotificationScheme { 160 | return false, fmt.Errorf("%s %s", "NotificationScheme", errorImmutableFieldMsg) 161 | } 162 | if project.Spec.PermissionScheme != existingProject.Spec.PermissionScheme { 163 | return false, fmt.Errorf("%s %s", "PermissionScheme", errorImmutableFieldMsg) 164 | } 165 | if project.Spec.IssueSecurityScheme != existingProject.Spec.IssueSecurityScheme { 166 | return false, fmt.Errorf("%s %s", "IssueSecurityScheme", errorImmutableFieldMsg) 167 | } 168 | if project.Spec.OpenAccess != existingProject.Spec.OpenAccess { 169 | return false, fmt.Errorf("%s %s", "OpenAccess", errorImmutableFieldMsg) 170 | } 171 | 172 | return true, nil 173 | } 174 | -------------------------------------------------------------------------------- /api/v1alpha1/project_webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "fmt" 21 | 22 | "k8s.io/apimachinery/pkg/runtime" 23 | ctrl "sigs.k8s.io/controller-runtime" 24 | logf "sigs.k8s.io/controller-runtime/pkg/log" 25 | "sigs.k8s.io/controller-runtime/pkg/webhook" 26 | ) 27 | 28 | // log is for logging in this package. 29 | var projectlog = logf.Log.WithName("project-resource") 30 | 31 | func (r *Project) SetupWebhookWithManager(mgr ctrl.Manager) error { 32 | return ctrl.NewWebhookManagedBy(mgr). 33 | For(r). 34 | Complete() 35 | } 36 | 37 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 38 | 39 | //+kubebuilder:webhook:path=/mutate-jiraservicedesk-stakater-com-v1alpha1-project,mutating=true,failurePolicy=fail,sideEffects=None,groups=jiraservicedesk.stakater.com,resources=projects,verbs=create;update,versions=v1alpha1,name=mproject.kb.io,admissionReviewVersions=v1 40 | 41 | var _ webhook.Defaulter = &Project{} 42 | 43 | // Default implements webhook.Defaulter so a webhook will be registered for the type 44 | func (r *Project) Default() { 45 | projectlog.Info("default", "name", r.Name) 46 | 47 | // TODO(user): fill in your defaulting logic. 48 | } 49 | 50 | // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. 51 | //+kubebuilder:webhook:path=/validate-jiraservicedesk-stakater-com-v1alpha1-project,mutating=false,failurePolicy=fail,sideEffects=None,groups=jiraservicedesk.stakater.com,resources=projects,verbs=create;update,versions=v1alpha1,name=vproject.kb.io,admissionReviewVersions=v1 52 | 53 | var _ webhook.Validator = &Project{} 54 | 55 | // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 56 | func (r *Project) ValidateCreate() error { 57 | projectlog.Info("validate create", "name", r.Name) 58 | 59 | _, err := r.IsValid() 60 | return err 61 | } 62 | 63 | // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 64 | func (r *Project) ValidateUpdate(old runtime.Object) error { 65 | projectlog.Info("validate update", "name", r.Name) 66 | 67 | oldProject, ok := old.(*Project) 68 | if !ok { 69 | return fmt.Errorf("Error casting old runtime object to %T from %T", oldProject, old) 70 | } 71 | _, err := r.IsValidUpdate(*oldProject) 72 | return err 73 | } 74 | 75 | // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 76 | func (r *Project) ValidateDelete() error { 77 | projectlog.Info("validate delete", "name", r.Name) 78 | 79 | // TODO(user): fill in your validation logic upon object deletion. 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2021. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by controller-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | ) 28 | 29 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 30 | func (in *Customer) DeepCopyInto(out *Customer) { 31 | *out = *in 32 | out.TypeMeta = in.TypeMeta 33 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 34 | in.Spec.DeepCopyInto(&out.Spec) 35 | in.Status.DeepCopyInto(&out.Status) 36 | } 37 | 38 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Customer. 39 | func (in *Customer) DeepCopy() *Customer { 40 | if in == nil { 41 | return nil 42 | } 43 | out := new(Customer) 44 | in.DeepCopyInto(out) 45 | return out 46 | } 47 | 48 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 49 | func (in *Customer) DeepCopyObject() runtime.Object { 50 | if c := in.DeepCopy(); c != nil { 51 | return c 52 | } 53 | return nil 54 | } 55 | 56 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 57 | func (in *CustomerList) DeepCopyInto(out *CustomerList) { 58 | *out = *in 59 | out.TypeMeta = in.TypeMeta 60 | in.ListMeta.DeepCopyInto(&out.ListMeta) 61 | if in.Items != nil { 62 | in, out := &in.Items, &out.Items 63 | *out = make([]Customer, len(*in)) 64 | for i := range *in { 65 | (*in)[i].DeepCopyInto(&(*out)[i]) 66 | } 67 | } 68 | } 69 | 70 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomerList. 71 | func (in *CustomerList) DeepCopy() *CustomerList { 72 | if in == nil { 73 | return nil 74 | } 75 | out := new(CustomerList) 76 | in.DeepCopyInto(out) 77 | return out 78 | } 79 | 80 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 81 | func (in *CustomerList) DeepCopyObject() runtime.Object { 82 | if c := in.DeepCopy(); c != nil { 83 | return c 84 | } 85 | return nil 86 | } 87 | 88 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 89 | func (in *CustomerSpec) DeepCopyInto(out *CustomerSpec) { 90 | *out = *in 91 | if in.Projects != nil { 92 | in, out := &in.Projects, &out.Projects 93 | *out = make([]string, len(*in)) 94 | copy(*out, *in) 95 | } 96 | } 97 | 98 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomerSpec. 99 | func (in *CustomerSpec) DeepCopy() *CustomerSpec { 100 | if in == nil { 101 | return nil 102 | } 103 | out := new(CustomerSpec) 104 | in.DeepCopyInto(out) 105 | return out 106 | } 107 | 108 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 109 | func (in *CustomerStatus) DeepCopyInto(out *CustomerStatus) { 110 | *out = *in 111 | if in.AssociatedProjects != nil { 112 | in, out := &in.AssociatedProjects, &out.AssociatedProjects 113 | *out = make([]string, len(*in)) 114 | copy(*out, *in) 115 | } 116 | if in.Conditions != nil { 117 | in, out := &in.Conditions, &out.Conditions 118 | *out = make([]v1.Condition, len(*in)) 119 | for i := range *in { 120 | (*in)[i].DeepCopyInto(&(*out)[i]) 121 | } 122 | } 123 | } 124 | 125 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomerStatus. 126 | func (in *CustomerStatus) DeepCopy() *CustomerStatus { 127 | if in == nil { 128 | return nil 129 | } 130 | out := new(CustomerStatus) 131 | in.DeepCopyInto(out) 132 | return out 133 | } 134 | 135 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 136 | func (in *Project) DeepCopyInto(out *Project) { 137 | *out = *in 138 | out.TypeMeta = in.TypeMeta 139 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 140 | out.Spec = in.Spec 141 | in.Status.DeepCopyInto(&out.Status) 142 | } 143 | 144 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Project. 145 | func (in *Project) DeepCopy() *Project { 146 | if in == nil { 147 | return nil 148 | } 149 | out := new(Project) 150 | in.DeepCopyInto(out) 151 | return out 152 | } 153 | 154 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 155 | func (in *Project) DeepCopyObject() runtime.Object { 156 | if c := in.DeepCopy(); c != nil { 157 | return c 158 | } 159 | return nil 160 | } 161 | 162 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 163 | func (in *ProjectList) DeepCopyInto(out *ProjectList) { 164 | *out = *in 165 | out.TypeMeta = in.TypeMeta 166 | in.ListMeta.DeepCopyInto(&out.ListMeta) 167 | if in.Items != nil { 168 | in, out := &in.Items, &out.Items 169 | *out = make([]Project, len(*in)) 170 | for i := range *in { 171 | (*in)[i].DeepCopyInto(&(*out)[i]) 172 | } 173 | } 174 | } 175 | 176 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectList. 177 | func (in *ProjectList) DeepCopy() *ProjectList { 178 | if in == nil { 179 | return nil 180 | } 181 | out := new(ProjectList) 182 | in.DeepCopyInto(out) 183 | return out 184 | } 185 | 186 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 187 | func (in *ProjectList) DeepCopyObject() runtime.Object { 188 | if c := in.DeepCopy(); c != nil { 189 | return c 190 | } 191 | return nil 192 | } 193 | 194 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 195 | func (in *ProjectSpec) DeepCopyInto(out *ProjectSpec) { 196 | *out = *in 197 | } 198 | 199 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectSpec. 200 | func (in *ProjectSpec) DeepCopy() *ProjectSpec { 201 | if in == nil { 202 | return nil 203 | } 204 | out := new(ProjectSpec) 205 | in.DeepCopyInto(out) 206 | return out 207 | } 208 | 209 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 210 | func (in *ProjectStatus) DeepCopyInto(out *ProjectStatus) { 211 | *out = *in 212 | if in.Conditions != nil { 213 | in, out := &in.Conditions, &out.Conditions 214 | *out = make([]v1.Condition, len(*in)) 215 | for i := range *in { 216 | (*in)[i].DeepCopyInto(&(*out)[i]) 217 | } 218 | } 219 | } 220 | 221 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectStatus. 222 | func (in *ProjectStatus) DeepCopy() *ProjectStatus { 223 | if in == nil { 224 | return nil 225 | } 226 | out := new(ProjectStatus) 227 | in.DeepCopyInto(out) 228 | return out 229 | } 230 | -------------------------------------------------------------------------------- /bundle.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=jira-service-desk-operator 8 | LABEL operators.operatorframework.io.bundle.channels.v1=alpha 9 | LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.19.0+git 10 | LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 11 | LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v3 12 | 13 | # Labels for testing. 14 | LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 15 | LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ 16 | 17 | # Copy files to locations specified by labels. 18 | COPY bundle/manifests /manifests/ 19 | COPY bundle/metadata /metadata/ 20 | COPY bundle/tests/scorecard /tests/scorecard/ 21 | -------------------------------------------------------------------------------- /bundle/manifests/jira-service-desk-operator-controller-manager-metrics-service_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | control-plane: controller-manager 7 | name: jira-service-desk-operator-controller-manager-metrics-service 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | status: 16 | loadBalancer: {} 17 | -------------------------------------------------------------------------------- /bundle/manifests/jira-service-desk-operator-manager-config_v1_configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | controller_manager_config.yaml: | 4 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 5 | kind: ControllerManagerConfig 6 | health: 7 | healthProbeBindAddress: :8081 8 | metrics: 9 | bindAddress: 127.0.0.1:8080 10 | webhook: 11 | port: 9443 12 | leaderElection: 13 | leaderElect: true 14 | resourceName: 48610e2b.stakater.com 15 | kind: ConfigMap 16 | metadata: 17 | name: jira-service-desk-operator-manager-config 18 | -------------------------------------------------------------------------------- /bundle/manifests/jira-service-desk-operator-manager-metrics_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | control-plane: controller-manager 7 | name: jira-service-desk-operator-manager-metrics 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | status: 16 | loadBalancer: {} 17 | -------------------------------------------------------------------------------- /bundle/manifests/jira-service-desk-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | name: jira-service-desk-operator-metrics-reader 6 | rules: 7 | - nonResourceURLs: 8 | - /metrics 9 | verbs: 10 | - get 11 | -------------------------------------------------------------------------------- /bundle/manifests/jira-service-desk-operator-webhook-service_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | name: jira-service-desk-operator-webhook-service 6 | spec: 7 | ports: 8 | - port: 443 9 | protocol: TCP 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | status: 14 | loadBalancer: {} 15 | -------------------------------------------------------------------------------- /bundle/manifests/jiraservicedesk.stakater.com_customers.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.8.0 6 | creationTimestamp: null 7 | name: customers.jiraservicedesk.stakater.com 8 | spec: 9 | group: jiraservicedesk.stakater.com 10 | names: 11 | kind: Customer 12 | listKind: CustomerList 13 | plural: customers 14 | singular: customer 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: Customer is the Schema for the customers API 21 | properties: 22 | apiVersion: 23 | description: 'APIVersion defines the versioned schema of this representation 24 | of an object. Servers should convert recognized schemas to the latest 25 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 26 | type: string 27 | kind: 28 | description: 'Kind is a string value representing the REST resource this 29 | object represents. Servers may infer this from the endpoint the client 30 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 31 | type: string 32 | metadata: 33 | type: object 34 | spec: 35 | description: CustomerSpec defines the desired state of Customer 36 | properties: 37 | email: 38 | description: Email of the customer 39 | pattern: \S+@\S+\.\S+ 40 | type: string 41 | legacyCustomer: 42 | description: LegacyCustomer is a boolean flag that represents whether 43 | a customer is created using legacy API or not In case of a legacy 44 | Customer, a signup link is sent to the customer email which he can 45 | than use to signup In case of a normal Customer, no signup link 46 | is sent to the customer. The customer than has to signup manually 47 | using the portal If not given, default behaviour is false i.e. normal 48 | customer 49 | type: boolean 50 | name: 51 | description: Name of the customer 52 | type: string 53 | projects: 54 | description: List of ProjectKeys in which customer will be added 55 | items: 56 | type: string 57 | minItems: 1 58 | type: array 59 | required: 60 | - email 61 | - name 62 | - projects 63 | type: object 64 | status: 65 | description: CustomerStatus defines the observed state of Customer 66 | properties: 67 | associatedProjects: 68 | description: List of ProjectKeys in which customer has bee added 69 | items: 70 | type: string 71 | type: array 72 | conditions: 73 | description: Status conditions 74 | items: 75 | description: "Condition contains details for one aspect of the current 76 | state of this API Resource. --- This struct is intended for direct 77 | use as an array at the field path .status.conditions. For example, 78 | type FooStatus struct{ // Represents the observations of a foo's 79 | current state. // Known .status.conditions.type are: \"Available\", 80 | \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge 81 | // +listType=map // +listMapKey=type Conditions []metav1.Condition 82 | `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" 83 | protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" 84 | properties: 85 | lastTransitionTime: 86 | description: lastTransitionTime is the last time the condition 87 | transitioned from one status to another. This should be when 88 | the underlying condition changed. If that is not known, then 89 | using the time when the API field changed is acceptable. 90 | format: date-time 91 | type: string 92 | message: 93 | description: message is a human readable message indicating 94 | details about the transition. This may be an empty string. 95 | maxLength: 32768 96 | type: string 97 | observedGeneration: 98 | description: observedGeneration represents the .metadata.generation 99 | that the condition was set based upon. For instance, if .metadata.generation 100 | is currently 12, but the .status.conditions[x].observedGeneration 101 | is 9, the condition is out of date with respect to the current 102 | state of the instance. 103 | format: int64 104 | minimum: 0 105 | type: integer 106 | reason: 107 | description: reason contains a programmatic identifier indicating 108 | the reason for the condition's last transition. Producers 109 | of specific condition types may define expected values and 110 | meanings for this field, and whether the values are considered 111 | a guaranteed API. The value should be a CamelCase string. 112 | This field may not be empty. 113 | maxLength: 1024 114 | minLength: 1 115 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 116 | type: string 117 | status: 118 | description: status of the condition, one of True, False, Unknown. 119 | enum: 120 | - "True" 121 | - "False" 122 | - Unknown 123 | type: string 124 | type: 125 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 126 | --- Many .condition.type values are consistent across resources 127 | like Available, but because arbitrary conditions can be useful 128 | (see .node.status.conditions), the ability to deconflict is 129 | important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 130 | maxLength: 316 131 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 132 | type: string 133 | required: 134 | - lastTransitionTime 135 | - message 136 | - reason 137 | - status 138 | - type 139 | type: object 140 | type: array 141 | customerId: 142 | description: Jira Service Desk Customer Account Id 143 | type: string 144 | required: 145 | - customerId 146 | type: object 147 | type: object 148 | served: true 149 | storage: true 150 | subresources: 151 | status: {} 152 | status: 153 | acceptedNames: 154 | kind: "" 155 | plural: "" 156 | conditions: [] 157 | storedVersions: [] 158 | -------------------------------------------------------------------------------- /bundle/metadata/annotations.yaml: -------------------------------------------------------------------------------- 1 | annotations: 2 | # Core bundle annotations. 3 | operators.operatorframework.io.bundle.mediatype.v1: registry+v1 4 | operators.operatorframework.io.bundle.manifests.v1: manifests/ 5 | operators.operatorframework.io.bundle.metadata.v1: metadata/ 6 | operators.operatorframework.io.bundle.package.v1: jira-service-desk-operator 7 | operators.operatorframework.io.bundle.channels.v1: alpha 8 | operators.operatorframework.io.metrics.builder: operator-sdk-v1.19.0+git 9 | operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 10 | operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v3 11 | 12 | # Annotations for testing. 13 | operators.operatorframework.io.test.mediatype.v1: scorecard+v1 14 | operators.operatorframework.io.test.config.v1: tests/scorecard/ 15 | -------------------------------------------------------------------------------- /bundle/tests/scorecard/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: 8 | - entrypoint: 9 | - scorecard-test 10 | - basic-check-spec 11 | image: quay.io/operator-framework/scorecard-test:v1.20.0 12 | labels: 13 | suite: basic 14 | test: basic-check-spec-test 15 | storage: 16 | spec: 17 | mountPath: {} 18 | - entrypoint: 19 | - scorecard-test 20 | - olm-bundle-validation 21 | image: quay.io/operator-framework/scorecard-test:v1.20.0 22 | labels: 23 | suite: olm 24 | test: olm-bundle-validation-test 25 | storage: 26 | spec: 27 | mountPath: {} 28 | - entrypoint: 29 | - scorecard-test 30 | - olm-crds-have-validation 31 | image: quay.io/operator-framework/scorecard-test:v1.20.0 32 | labels: 33 | suite: olm 34 | test: olm-crds-have-validation-test 35 | storage: 36 | spec: 37 | mountPath: {} 38 | - entrypoint: 39 | - scorecard-test 40 | - olm-crds-have-resources 41 | image: quay.io/operator-framework/scorecard-test:v1.20.0 42 | labels: 43 | suite: olm 44 | test: olm-crds-have-resources-test 45 | storage: 46 | spec: 47 | mountPath: {} 48 | - entrypoint: 49 | - scorecard-test 50 | - olm-spec-descriptors 51 | image: quay.io/operator-framework/scorecard-test:v1.20.0 52 | labels: 53 | suite: olm 54 | test: olm-spec-descriptors-test 55 | storage: 56 | spec: 57 | mountPath: {} 58 | - entrypoint: 59 | - scorecard-test 60 | - olm-status-descriptors 61 | image: quay.io/operator-framework/scorecard-test:v1.20.0 62 | labels: 63 | suite: olm 64 | test: olm-status-descriptors-test 65 | storage: 66 | spec: 67 | mountPath: {} 68 | storage: 69 | spec: 70 | mountPath: {} 71 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: jira-service-desk-operator 3 | description: Helm Chart for jira-service-desk-operator 4 | 5 | type: application 6 | 7 | sources: 8 | - https://github.com/stakater/jira-service-desk-operator 9 | 10 | # Helm chart Version 11 | version: 0.0.50 12 | 13 | # Application version to be deployed 14 | appVersion: 0.0.50 15 | 16 | keywords: 17 | - stakater 18 | - sro 19 | - jira-service-desk-operator 20 | - jira 21 | - service desk 22 | 23 | # Maintainers 24 | maintainers: 25 | - name: stakater 26 | email: hello@stakater.com 27 | 28 | icon: https://github.com/stakater/ForecastleIcons/blob/master/stakater.png 29 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/README.md: -------------------------------------------------------------------------------- 1 | # jira-service-desk-operator 2 | 3 | A Helm chart to deploy jira-service-desk-operator 4 | 5 | ## Pre-requisites 6 | 7 | - Make sure that [certman](https://cert-manager.io/) is deployed in your cluster since webhooks require certman to generate valid certs since webhooks serve using HTTPS 8 | 9 | ```terminal 10 | $ kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.0.1/cert-manager.yaml 11 | ``` 12 | 13 | ## Installing the chart 14 | 15 | ```sh 16 | helm repo add stakater https://stakater.github.io/stakater-charts/ 17 | helm repo update 18 | helm install stakater/jira-service-desk-operator --namespace jira-service-desk-operator 19 | ``` -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/crds/jiraservicedesk.stakater.com_customers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.8.0 7 | creationTimestamp: null 8 | name: customers.jiraservicedesk.stakater.com 9 | spec: 10 | group: jiraservicedesk.stakater.com 11 | names: 12 | kind: Customer 13 | listKind: CustomerList 14 | plural: customers 15 | singular: customer 16 | scope: Namespaced 17 | versions: 18 | - name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | description: Customer is the Schema for the customers API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | description: CustomerSpec defines the desired state of Customer 37 | properties: 38 | email: 39 | description: Email of the customer 40 | pattern: \S+@\S+\.\S+ 41 | type: string 42 | legacyCustomer: 43 | description: LegacyCustomer is a boolean flag that represents whether 44 | a customer is created using legacy API or not In case of a legacy 45 | Customer, a signup link is sent to the customer email which he can 46 | than use to signup In case of a normal Customer, no signup link 47 | is sent to the customer. The customer than has to signup manually 48 | using the portal If not given, default behaviour is false i.e. normal 49 | customer 50 | type: boolean 51 | name: 52 | description: Name of the customer 53 | type: string 54 | projects: 55 | description: List of ProjectKeys in which customer will be added 56 | items: 57 | type: string 58 | minItems: 1 59 | type: array 60 | required: 61 | - email 62 | - name 63 | - projects 64 | type: object 65 | status: 66 | description: CustomerStatus defines the observed state of Customer 67 | properties: 68 | associatedProjects: 69 | description: List of ProjectKeys in which customer has bee added 70 | items: 71 | type: string 72 | type: array 73 | conditions: 74 | description: Status conditions 75 | items: 76 | description: "Condition contains details for one aspect of the current 77 | state of this API Resource. --- This struct is intended for direct 78 | use as an array at the field path .status.conditions. For example, 79 | type FooStatus struct{ // Represents the observations of a foo's 80 | current state. // Known .status.conditions.type are: \"Available\", 81 | \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge 82 | // +listType=map // +listMapKey=type Conditions []metav1.Condition 83 | `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" 84 | protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" 85 | properties: 86 | lastTransitionTime: 87 | description: lastTransitionTime is the last time the condition 88 | transitioned from one status to another. This should be when 89 | the underlying condition changed. If that is not known, then 90 | using the time when the API field changed is acceptable. 91 | format: date-time 92 | type: string 93 | message: 94 | description: message is a human readable message indicating 95 | details about the transition. This may be an empty string. 96 | maxLength: 32768 97 | type: string 98 | observedGeneration: 99 | description: observedGeneration represents the .metadata.generation 100 | that the condition was set based upon. For instance, if .metadata.generation 101 | is currently 12, but the .status.conditions[x].observedGeneration 102 | is 9, the condition is out of date with respect to the current 103 | state of the instance. 104 | format: int64 105 | minimum: 0 106 | type: integer 107 | reason: 108 | description: reason contains a programmatic identifier indicating 109 | the reason for the condition's last transition. Producers 110 | of specific condition types may define expected values and 111 | meanings for this field, and whether the values are considered 112 | a guaranteed API. The value should be a CamelCase string. 113 | This field may not be empty. 114 | maxLength: 1024 115 | minLength: 1 116 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 117 | type: string 118 | status: 119 | description: status of the condition, one of True, False, Unknown. 120 | enum: 121 | - "True" 122 | - "False" 123 | - Unknown 124 | type: string 125 | type: 126 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 127 | --- Many .condition.type values are consistent across resources 128 | like Available, but because arbitrary conditions can be useful 129 | (see .node.status.conditions), the ability to deconflict is 130 | important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 131 | maxLength: 316 132 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 133 | type: string 134 | required: 135 | - lastTransitionTime 136 | - message 137 | - reason 138 | - status 139 | - type 140 | type: object 141 | type: array 142 | customerId: 143 | description: Jira Service Desk Customer Account Id 144 | type: string 145 | required: 146 | - customerId 147 | type: object 148 | type: object 149 | served: true 150 | storage: true 151 | subresources: 152 | status: {} 153 | status: 154 | acceptedNames: 155 | kind: "" 156 | plural: "" 157 | conditions: [] 158 | storedVersions: [] 159 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "jira-service-desk-operator.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "jira-service-desk-operator.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "jira-service-desk-operator.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "jira-service-desk-operator.labels" -}} 37 | helm.sh/chart: {{ include "jira-service-desk-operator.chart" . }} 38 | {{ include "jira-service-desk-operator.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "jira-service-desk-operator.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "jira-service-desk-operator.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "jira-service-desk-operator.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "jira-service-desk-operator.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/certificate.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.enabled }} 2 | 3 | {{- if not (.Capabilities.APIVersions.Has "cert-manager.io/v1") -}} 4 | {{- fail "cert-manager/v1 CRDs not installed" }} 5 | {{ end }} 6 | --- 7 | apiVersion: cert-manager.io/v1 8 | kind: Issuer 9 | metadata: 10 | name: {{ include "jira-service-desk-operator.fullname" . }}-selfsigned-issuer 11 | namespace: {{ .Release.Namespace }} 12 | spec: 13 | selfSigned: {} 14 | --- 15 | apiVersion: cert-manager.io/v1 16 | kind: Certificate 17 | metadata: 18 | name: {{ include "jira-service-desk-operator.fullname" . }}-serving-cert 19 | namespace: {{ .Release.Namespace }} 20 | spec: 21 | dnsNames: 22 | - "{{ include "jira-service-desk-operator.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc" 23 | - "{{ include "jira-service-desk-operator.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc.cluster.local" 24 | issuerRef: 25 | kind: Issuer 26 | name: {{ include "jira-service-desk-operator.fullname" . }}-selfsigned-issuer 27 | secretName: webhook-server-cert 28 | {{- end -}} 29 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{- if .Values.rbac.enabled -}} 4 | --- 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: ClusterRole 7 | metadata: 8 | name: {{ include "jira-service-desk-operator.fullname" . }}-manager-role 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - secrets 14 | verbs: 15 | - get 16 | - list 17 | - apiGroups: 18 | - jiraservicedesk.stakater.com 19 | resources: 20 | - customers 21 | verbs: 22 | - create 23 | - delete 24 | - get 25 | - list 26 | - patch 27 | - update 28 | - watch 29 | - apiGroups: 30 | - jiraservicedesk.stakater.com 31 | resources: 32 | - customers/status 33 | verbs: 34 | - get 35 | - patch 36 | - update 37 | - apiGroups: 38 | - jiraservicedesk.stakater.com 39 | resources: 40 | - projects 41 | verbs: 42 | - create 43 | - delete 44 | - get 45 | - list 46 | - patch 47 | - update 48 | - watch 49 | - apiGroups: 50 | - jiraservicedesk.stakater.com 51 | resources: 52 | - projects/status 53 | verbs: 54 | - get 55 | - patch 56 | - update 57 | --- 58 | {{- if .Values.rbac.allowProxyRole }} 59 | apiVersion: rbac.authorization.k8s.io/v1 60 | kind: ClusterRole 61 | metadata: 62 | name: {{ include "jira-service-desk-operator.fullname" . }}-proxy-role 63 | rules: 64 | - apiGroups: 65 | - authentication.k8s.io 66 | resources: 67 | - tokenreviews 68 | verbs: 69 | - create 70 | - apiGroups: 71 | - authorization.k8s.io 72 | resources: 73 | - subjectaccessreviews 74 | verbs: 75 | - create 76 | {{- end }} 77 | 78 | --- 79 | {{- if .Values.rbac.allowMetricsReaderRole }} 80 | apiVersion: rbac.authorization.k8s.io/v1 81 | kind: ClusterRole 82 | metadata: 83 | name: {{ include "jira-service-desk-operator.fullname" . }}-metrics-reader 84 | rules: 85 | - nonResourceURLs: 86 | - /metrics 87 | verbs: 88 | - get 89 | {{- end }} 90 | 91 | {{- end }} 92 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.enabled -}} 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRoleBinding 5 | metadata: 6 | name: {{ include "jira-service-desk-operator.fullname" . }}-manager-rolebinding 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: {{ include "jira-service-desk-operator.fullname" . }}-manager-role 11 | subjects: 12 | - kind: ServiceAccount 13 | name: {{ include "jira-service-desk-operator.serviceAccountName" . }} 14 | namespace: {{ .Release.Namespace }} 15 | 16 | --- 17 | {{- if .Values.rbac.allowProxyRole }} 18 | apiVersion: rbac.authorization.k8s.io/v1 19 | kind: ClusterRoleBinding 20 | metadata: 21 | name: {{ include "jira-service-desk-operator.fullname" . }}-proxy-rolebinding 22 | roleRef: 23 | apiGroup: rbac.authorization.k8s.io 24 | kind: ClusterRole 25 | name: {{ include "jira-service-desk-operator.fullname" . }}-proxy-role 26 | subjects: 27 | - kind: ServiceAccount 28 | name: {{ include "jira-service-desk-operator.serviceAccountName" . }} 29 | namespace: {{ .Release.Namespace }} 30 | {{- end }} 31 | 32 | --- 33 | {{- if .Values.rbac.allowMetricsReaderRole }} 34 | apiVersion: rbac.authorization.k8s.io/v1 35 | kind: ClusterRoleBinding 36 | metadata: 37 | name: {{ include "jira-service-desk-operator.fullname" . }}-metrics-reader-rolebbinding 38 | roleRef: 39 | apiGroup: rbac.authorization.k8s.io 40 | kind: ClusterRole 41 | name: {{ include "jira-service-desk-operator.fullname" . }}-metrics-reader 42 | subjects: 43 | - kind: ServiceAccount 44 | name: {{ include "jira-service-desk-operator.serviceAccountName" . }} 45 | namespace: {{ .Release.Namespace }} 46 | {{- end }} 47 | 48 | {{- end }} 49 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "jira-service-desk-operator.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "jira-service-desk-operator.labels" . | nindent 4 }} 8 | control-plane: controller-manager 9 | spec: 10 | replicas: {{ .Values.replicaCount }} 11 | selector: 12 | matchLabels: 13 | {{- include "jira-service-desk-operator.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | kubectl.kubernetes.io/default-container: manager 19 | {{- end }} 20 | labels: 21 | {{- include "jira-service-desk-operator.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "jira-service-desk-operator.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - args: 32 | - --secure-listen-address=0.0.0.0:8443 33 | - --upstream=http://127.0.0.1:8080/ 34 | - --logtostderr=true 35 | - --v=0 36 | # Issue: https://github.com/operator-framework/operator-sdk/issues/4813 37 | image: registry.redhat.io/openshift4/ose-kube-rbac-proxy:v4.7.0 38 | imagePullPolicy: {{ .Values.image.pullPolicy }} 39 | name: kube-rbac-proxy 40 | ports: 41 | - protocol: TCP 42 | containerPort: 8443 43 | name: https 44 | - args: 45 | - --health-probe-bind-address=:8081 46 | - --metrics-bind-address=127.0.0.1:8080 47 | - --leader-elect 48 | command: 49 | - /manager 50 | env: 51 | - name: WATCH_NAMESPACE 52 | value: {{ .Values.watchNamespaces | join "," | quote }} 53 | - name: CONFIG_SECRET_NAME 54 | value: "{{ default "jira-service-desk-config" .Values.configSecretName }}" 55 | - name: ENABLE_WEBHOOKS 56 | value: "{{ default true .Values.webhook.enabled }}" 57 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 58 | imagePullPolicy: {{ .Values.image.pullPolicy }} 59 | securityContext: 60 | allowPrivilegeEscalation: false 61 | name: manager 62 | livenessProbe: 63 | httpGet: 64 | path: /healthz 65 | port: 8081 66 | initialDelaySeconds: 15 67 | periodSeconds: 20 68 | name: manager 69 | readinessProbe: 70 | httpGet: 71 | path: /readyz 72 | port: 8081 73 | initialDelaySeconds: 5 74 | periodSeconds: 10 75 | ports: 76 | - containerPort: 9443 77 | name: webhook-server 78 | resources: 79 | {{- toYaml .Values.resources | nindent 12 }} 80 | volumeMounts: 81 | - mountPath: /tmp/k8s-webhook-server/serving-certs 82 | name: cert 83 | readOnly: true 84 | terminationGracePeriodSeconds: 10 85 | volumes: 86 | - name: cert 87 | secret: 88 | defaultMode: 420 89 | secretName: webhook-server-cert 90 | {{- with .Values.nodeSelector }} 91 | nodeSelector: 92 | {{- toYaml . | nindent 8 }} 93 | {{- end }} 94 | {{- with .Values.affinity }} 95 | affinity: 96 | {{- toYaml . | nindent 8 }} 97 | {{- end }} 98 | {{- with .Values.tolerations }} 99 | tolerations: 100 | {{- toYaml . | nindent 8 }} 101 | {{- end }} 102 | --- -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/mutating-webhook.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.enabled -}} 2 | --- 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "jira-service-desk-operator.fullname" . }}-serving-cert 8 | creationTimestamp: null 9 | name: {{ include "jira-service-desk-operator.fullname" . }}-mutating-webhook-configuration 10 | webhooks: 11 | - admissionReviewVersions: 12 | - v1 13 | clientConfig: 14 | service: 15 | name: {{ include "jira-service-desk-operator.fullname" . }}-webhook-service 16 | namespace: {{ .Release.Namespace }} 17 | path: /mutate-jiraservicedesk-stakater-com-v1alpha1-customer 18 | failurePolicy: Fail 19 | name: mcustomer.kb.io 20 | rules: 21 | - apiGroups: 22 | - jiraservicedesk.stakater.com 23 | apiVersions: 24 | - v1alpha1 25 | operations: 26 | - CREATE 27 | - UPDATE 28 | resources: 29 | - customers 30 | sideEffects: None 31 | - admissionReviewVersions: 32 | - v1 33 | clientConfig: 34 | service: 35 | name: {{ include "jira-service-desk-operator.fullname" . }}-webhook-service 36 | namespace: {{ .Release.Namespace }} 37 | path: /mutate-jiraservicedesk-stakater-com-v1alpha1-project 38 | failurePolicy: Fail 39 | name: mproject.kb.io 40 | rules: 41 | - apiGroups: 42 | - jiraservicedesk.stakater.com 43 | apiVersions: 44 | - v1alpha1 45 | operations: 46 | - CREATE 47 | - UPDATE 48 | resources: 49 | - projects 50 | sideEffects: None 51 | {{- end -}} 52 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/role.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.enabled -}} 2 | {{- if .Values.rbac.allowLeaderElectionRole }} 3 | --- 4 | apiVersion: rbac.authorization.k8s.io/v1 5 | kind: Role 6 | metadata: 7 | name: {{ include "jira-service-desk-operator.fullname" . }}-leader-election-role 8 | namespace: {{ .Release.Namespace }} 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - configmaps 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - create 19 | - update 20 | - patch 21 | - delete 22 | - apiGroups: 23 | - coordination.k8s.io 24 | resources: 25 | - leases 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - create 31 | - update 32 | - patch 33 | - delete 34 | - apiGroups: 35 | - "" 36 | resources: 37 | - events 38 | verbs: 39 | - create 40 | - patch 41 | {{- end }} 42 | 43 | {{- end }} 44 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.enabled -}} 2 | {{- if .Values.rbac.allowLeaderElectionRole }} 3 | --- 4 | apiVersion: rbac.authorization.k8s.io/v1 5 | kind: RoleBinding 6 | metadata: 7 | name: {{ include "jira-service-desk-operator.fullname" . }}-leader-election-rolebinding 8 | namespace: {{ .Release.Namespace }} 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: Role 12 | name: {{ include "jira-service-desk-operator.fullname" . }}-leader-election-role 13 | subjects: 14 | - kind: ServiceAccount 15 | name: {{ include "jira-service-desk-operator.serviceAccountName" . }} 16 | namespace: {{ .Release.Namespace }} 17 | {{- end }} 18 | 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "jira-service-desk-operator.fullname" . }}-webhook-service 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "jira-service-desk-operator.labels" . | nindent 4 }} 9 | spec: 10 | type: {{ .Values.service.type }} 11 | ports: 12 | - port: {{ .Values.service.port }} 13 | targetPort: 9443 14 | protocol: TCP 15 | name: http 16 | selector: 17 | {{- include "jira-service-desk-operator.selectorLabels" . | nindent 4 }} 18 | --- 19 | apiVersion: v1 20 | kind: Service 21 | metadata: 22 | name: {{ include "jira-service-desk-operator.fullname" . }}-metrics-service 23 | namespace: {{ .Release.Namespace }} 24 | labels: 25 | {{- include "jira-service-desk-operator.labels" . | nindent 4 }} 26 | spec: 27 | ports: 28 | - name: https 29 | port: 8443 30 | targetPort: https 31 | selector: 32 | {{- include "jira-service-desk-operator.selectorLabels" . | nindent 4 }} -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | --- 3 | apiVersion: v1 4 | kind: ServiceAccount 5 | metadata: 6 | name: {{ include "jira-service-desk-operator.serviceAccountName" . }} 7 | namespace: {{ .Release.Namespace }} 8 | labels: 9 | {{- include "jira-service-desk-operator.labels" . | nindent 4 }} 10 | {{- with .Values.serviceAccount.annotations }} 11 | annotations: 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.enabled -}} 2 | --- 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | name: '{{ .Release.Name }}' 7 | namespace: '{{ .Release.Namespace }}' 8 | labels: 9 | app: '{{ .Chart.Name }}' 10 | app.kubernetes.io/name: '{{ .Chart.Name }}' 11 | app.kubernetes.io/instance: '{{ .Release.Name }}' 12 | spec: 13 | selector: 14 | matchLabels: 15 | app: '{{ .Chart.Name }}' 16 | app.kubernetes.io/name: '{{ .Chart.Name }}' 17 | app.kubernetes.io/instance: '{{ .Release.Name }}' 18 | endpoints: 19 | - port: metrics 20 | path: /metrics 21 | namespaceSelector: 22 | matchNames: 23 | - '{{ .Release.Namespace }}' 24 | {{- end }} -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "jira-service-desk-operator.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "jira-service-desk-operator.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "jira-service-desk-operator.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/templates/validating-webhook.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.enabled -}} 2 | --- 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: ValidatingWebhookConfiguration 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "jira-service-desk-operator.fullname" . }}-serving-cert 8 | creationTimestamp: null 9 | name: {{ include "jira-service-desk-operator.fullname" . }}-validating-webhook-configuration 10 | webhooks: 11 | - admissionReviewVersions: 12 | - v1 13 | clientConfig: 14 | service: 15 | name: {{ include "jira-service-desk-operator.fullname" . }}-webhook-service 16 | namespace: {{ .Release.Namespace }} 17 | path: /validate-jiraservicedesk-stakater-com-v1alpha1-customer 18 | failurePolicy: Fail 19 | name: vcustomer.kb.io 20 | rules: 21 | - apiGroups: 22 | - jiraservicedesk.stakater.com 23 | apiVersions: 24 | - v1alpha1 25 | operations: 26 | - CREATE 27 | - UPDATE 28 | resources: 29 | - customers 30 | sideEffects: None 31 | - admissionReviewVersions: 32 | - v1 33 | clientConfig: 34 | service: 35 | name: {{ include "jira-service-desk-operator.fullname" . }}-webhook-service 36 | namespace: {{ .Release.Namespace }} 37 | path: /validate-jiraservicedesk-stakater-com-v1alpha1-project 38 | failurePolicy: Fail 39 | name: vproject.kb.io 40 | rules: 41 | - apiGroups: 42 | - jiraservicedesk.stakater.com 43 | apiVersions: 44 | - v1alpha1 45 | operations: 46 | - CREATE 47 | - UPDATE 48 | resources: 49 | - projects 50 | sideEffects: None 51 | {{- end -}} 52 | 53 | 54 | -------------------------------------------------------------------------------- /charts/jira-service-desk-operator/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | 3 | image: 4 | repository: stakater/jira-service-desk-operator 5 | tag: v0.0.50 6 | pullPolicy: IfNotPresent 7 | imagePullSecrets: [] 8 | 9 | nameOverride: "" 10 | fullnameOverride: "" 11 | 12 | watchNamespaces: [] 13 | configSecretName: "jira-service-desk-config" 14 | 15 | # Webhook Configuration 16 | webhook: 17 | enabled: true 18 | 19 | service: 20 | type: ClusterIP 21 | port: 443 22 | protocol: TCP 23 | 24 | # Monitoring Configuration 25 | serviceMonitor: 26 | enabled: false 27 | 28 | rbac: 29 | enabled: true 30 | allowProxyRole: true 31 | allowMetricsReaderRole: true 32 | allowLeaderElectionRole: true 33 | 34 | serviceAccount: 35 | create: true 36 | annotations: {} 37 | # If not set and create is true, a name is generated using the fullname template 38 | name: "" 39 | 40 | resources: 41 | {} 42 | # limits: 43 | # cpu: 100m 44 | # memory: 128Mi 45 | # requests: 46 | # cpu: 100m 47 | # memory: 128Mi 48 | 49 | podAnnotations: {} 50 | 51 | podSecurityContext: 52 | runAsNonRoot: true 53 | 54 | securityContext: 55 | {} 56 | # capabilities: 57 | # drop: 58 | # - ALL 59 | # readOnlyRootFilesystem: true 60 | # runAsNonRoot: true 61 | # runAsUser: 1000 62 | 63 | nodeSelector: {} 64 | 65 | tolerations: [] 66 | 67 | affinity: {} 68 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. 4 | apiVersion: cert-manager.io/v1 5 | kind: Issuer 6 | metadata: 7 | name: selfsigned-issuer 8 | namespace: system 9 | spec: 10 | selfSigned: {} 11 | --- 12 | apiVersion: cert-manager.io/v1 13 | kind: Certificate 14 | metadata: 15 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 16 | namespace: system 17 | spec: 18 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 19 | dnsNames: 20 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 22 | issuerRef: 23 | kind: Issuer 24 | name: selfsigned-issuer 25 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 26 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/crd/bases/jiraservicedesk.stakater.com_customers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.8.0 7 | creationTimestamp: null 8 | name: customers.jiraservicedesk.stakater.com 9 | spec: 10 | group: jiraservicedesk.stakater.com 11 | names: 12 | kind: Customer 13 | listKind: CustomerList 14 | plural: customers 15 | singular: customer 16 | scope: Namespaced 17 | versions: 18 | - name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | description: Customer is the Schema for the customers API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | description: CustomerSpec defines the desired state of Customer 37 | properties: 38 | email: 39 | description: Email of the customer 40 | pattern: \S+@\S+\.\S+ 41 | type: string 42 | legacyCustomer: 43 | description: LegacyCustomer is a boolean flag that represents whether 44 | a customer is created using legacy API or not In case of a legacy 45 | Customer, a signup link is sent to the customer email which he can 46 | than use to signup In case of a normal Customer, no signup link 47 | is sent to the customer. The customer than has to signup manually 48 | using the portal If not given, default behaviour is false i.e. normal 49 | customer 50 | type: boolean 51 | name: 52 | description: Name of the customer 53 | type: string 54 | projects: 55 | description: List of ProjectKeys in which customer will be added 56 | items: 57 | type: string 58 | minItems: 1 59 | type: array 60 | required: 61 | - email 62 | - name 63 | - projects 64 | type: object 65 | status: 66 | description: CustomerStatus defines the observed state of Customer 67 | properties: 68 | associatedProjects: 69 | description: List of ProjectKeys in which customer has bee added 70 | items: 71 | type: string 72 | type: array 73 | conditions: 74 | description: Status conditions 75 | items: 76 | description: "Condition contains details for one aspect of the current 77 | state of this API Resource. --- This struct is intended for direct 78 | use as an array at the field path .status.conditions. For example, 79 | type FooStatus struct{ // Represents the observations of a foo's 80 | current state. // Known .status.conditions.type are: \"Available\", 81 | \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge 82 | // +listType=map // +listMapKey=type Conditions []metav1.Condition 83 | `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" 84 | protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" 85 | properties: 86 | lastTransitionTime: 87 | description: lastTransitionTime is the last time the condition 88 | transitioned from one status to another. This should be when 89 | the underlying condition changed. If that is not known, then 90 | using the time when the API field changed is acceptable. 91 | format: date-time 92 | type: string 93 | message: 94 | description: message is a human readable message indicating 95 | details about the transition. This may be an empty string. 96 | maxLength: 32768 97 | type: string 98 | observedGeneration: 99 | description: observedGeneration represents the .metadata.generation 100 | that the condition was set based upon. For instance, if .metadata.generation 101 | is currently 12, but the .status.conditions[x].observedGeneration 102 | is 9, the condition is out of date with respect to the current 103 | state of the instance. 104 | format: int64 105 | minimum: 0 106 | type: integer 107 | reason: 108 | description: reason contains a programmatic identifier indicating 109 | the reason for the condition's last transition. Producers 110 | of specific condition types may define expected values and 111 | meanings for this field, and whether the values are considered 112 | a guaranteed API. The value should be a CamelCase string. 113 | This field may not be empty. 114 | maxLength: 1024 115 | minLength: 1 116 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 117 | type: string 118 | status: 119 | description: status of the condition, one of True, False, Unknown. 120 | enum: 121 | - "True" 122 | - "False" 123 | - Unknown 124 | type: string 125 | type: 126 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 127 | --- Many .condition.type values are consistent across resources 128 | like Available, but because arbitrary conditions can be useful 129 | (see .node.status.conditions), the ability to deconflict is 130 | important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 131 | maxLength: 316 132 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 133 | type: string 134 | required: 135 | - lastTransitionTime 136 | - message 137 | - reason 138 | - status 139 | - type 140 | type: object 141 | type: array 142 | customerId: 143 | description: Jira Service Desk Customer Account Id 144 | type: string 145 | required: 146 | - customerId 147 | type: object 148 | type: object 149 | served: true 150 | storage: true 151 | subresources: 152 | status: {} 153 | status: 154 | acceptedNames: 155 | kind: "" 156 | plural: "" 157 | conditions: [] 158 | storedVersions: [] 159 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/jiraservicedesk.stakater.com_customers.yaml 6 | - bases/jiraservicedesk.stakater.com_projects.yaml 7 | #+kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patchesStrategicMerge: 10 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 | # patches here are for enabling the conversion webhook for each CRD 12 | #- patches/webhook_in_customers.yaml 13 | #- patches/webhook_in_projects.yaml 14 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 15 | 16 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 17 | # patches here are for enabling the CA injection for each CRD 18 | #- patches/cainjection_in_customers.yaml 19 | #- patches/cainjection_in_projects.yaml 20 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 21 | 22 | # the following config is for teaching kustomize how to do kustomization for CRDs. 23 | configurations: 24 | - kustomizeconfig.yaml 25 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_customers.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: customers.jiraservicedesk.stakater.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_projects.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: projects.jiraservicedesk.stakater.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_customers.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: customers.jiraservicedesk.stakater.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_projects.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: projects.jiraservicedesk.stakater.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: jira-service-desk-operator-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: jira-service-desk-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | - ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | - ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | # Mount the controller config file for loading manager configurations 34 | # through a ComponentConfig type 35 | #- manager_config_patch.yaml 36 | 37 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 38 | # crd/kustomization.yaml 39 | - manager_webhook_patch.yaml 40 | 41 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 42 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 43 | # 'CERTMANAGER' needs to be enabled to use ca injection 44 | - webhookcainjection_patch.yaml 45 | 46 | # the following config is for teaching kustomize how to do var substitution 47 | vars: 48 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 49 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 50 | # objref: 51 | # kind: Certificate 52 | # group: cert-manager.io 53 | # version: v1 54 | # name: serving-cert # this name should match the one in certificate.yaml 55 | # fieldref: 56 | # fieldpath: metadata.namespace 57 | #- name: CERTIFICATE_NAME 58 | # objref: 59 | # kind: Certificate 60 | # group: cert-manager.io 61 | # version: v1 62 | # name: serving-cert # this name should match the one in certificate.yaml 63 | #- name: SERVICE_NAMESPACE # namespace of the service 64 | # objref: 65 | # kind: Service 66 | # version: v1 67 | # name: webhook-service 68 | # fieldref: 69 | # fieldpath: metadata.namespace 70 | #- name: SERVICE_NAME 71 | # objref: 72 | # kind: Service 73 | # version: v1 74 | # name: webhook-service 75 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 14 | resources: 15 | limits: 16 | cpu: 500m 17 | memory: 128Mi 18 | requests: 19 | cpu: 5m 20 | memory: 64Mi 21 | args: 22 | - "--secure-listen-address=0.0.0.0:8443" 23 | - "--upstream=http://127.0.0.1:8080/" 24 | - "--logtostderr=true" 25 | - "--v=0" 26 | ports: 27 | - containerPort: 8443 28 | protocol: TCP 29 | name: https 30 | - name: manager 31 | args: 32 | - "--health-probe-bind-address=:8081" 33 | - "--metrics-bind-address=127.0.0.1:8080" 34 | - "--leader-elect" 35 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | args: 12 | - "--config=controller_manager_config.yaml" 13 | volumeMounts: 14 | - name: manager-config 15 | mountPath: /controller_manager_config.yaml 16 | subPath: controller_manager_config.yaml 17 | volumes: 18 | - name: manager-config 19 | configMap: 20 | name: manager-config 21 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /config/manager/controller_manager_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 2 | kind: ControllerManagerConfig 3 | health: 4 | healthProbeBindAddress: :8081 5 | metrics: 6 | bindAddress: 127.0.0.1:8080 7 | webhook: 8 | port: 9443 9 | leaderElection: 10 | leaderElect: true 11 | resourceName: 48610e2b.stakater.com 12 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | 7 | configMapGenerator: 8 | - files: 9 | - controller_manager_config.yaml 10 | name: manager-config 11 | apiVersion: kustomize.config.k8s.io/v1beta1 12 | kind: Kustomization 13 | images: 14 | - name: controller 15 | newName: stakater/jira-service-desk-operator 16 | newTag: v0.0.50 17 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: controller-manager 24 | annotations: 25 | kubectl.kubernetes.io/default-container: manager 26 | spec: 27 | securityContext: 28 | runAsNonRoot: true 29 | containers: 30 | - command: 31 | - /manager 32 | args: 33 | - --leader-elect 34 | image: controller:latest 35 | name: manager 36 | securityContext: 37 | allowPrivilegeEscalation: false 38 | livenessProbe: 39 | httpGet: 40 | path: /healthz 41 | port: 8081 42 | initialDelaySeconds: 15 43 | periodSeconds: 20 44 | readinessProbe: 45 | httpGet: 46 | path: /readyz 47 | port: 8081 48 | initialDelaySeconds: 5 49 | periodSeconds: 10 50 | resources: 51 | limits: 52 | cpu: 500m 53 | memory: 768Mi 54 | requests: 55 | cpu: 10m 56 | memory: 256Mi 57 | serviceAccountName: controller-manager 58 | terminationGracePeriodSeconds: 10 59 | -------------------------------------------------------------------------------- /config/manifests/bases/jira-service-desk-operator.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | alm-examples: '[]' 6 | capabilities: Basic Install 7 | name: jira-service-desk-operator.v0.0.0 8 | namespace: placeholder 9 | spec: 10 | apiservicedefinitions: {} 11 | customresourcedefinitions: 12 | owned: 13 | - description: Customer is the Schema for the customers API 14 | displayName: Customer 15 | kind: Customer 16 | name: customers.jiraservicedesk.stakater.com 17 | version: v1alpha1 18 | - description: Project is the Schema for the projects API 19 | displayName: Project 20 | kind: Project 21 | name: projects.jiraservicedesk.stakater.com 22 | version: v1alpha1 23 | description: Kubernetes operator for Jira Service Desk 24 | displayName: jira-service-desk-operator 25 | icon: 26 | - base64data: "" 27 | mediatype: "" 28 | install: 29 | spec: 30 | deployments: null 31 | strategy: "" 32 | installModes: 33 | - supported: false 34 | type: OwnNamespace 35 | - supported: false 36 | type: SingleNamespace 37 | - supported: false 38 | type: MultiNamespace 39 | - supported: true 40 | type: AllNamespaces 41 | keywords: 42 | - jira 43 | - service desk 44 | - operator 45 | - openshift 46 | - kubernetes 47 | links: 48 | - name: Jira Service Desk Operator 49 | url: https://jira-service-desk-operator.domain 50 | maintainers: 51 | - email: hello@stakater.com 52 | name: stakater 53 | - email: ahmedwaleedmalik@gmail.com 54 | name: ahmedwaleedmalik 55 | maturity: alpha 56 | provider: 57 | name: stakater 58 | url: https://stakater.com 59 | version: 0.0.0 60 | -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | resources: 4 | - bases/jira-service-desk-operator.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | 9 | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 | #patchesJson6902: 13 | #- target: 14 | # group: apps 15 | # version: v1 16 | # kind: Deployment 17 | # name: controller-manager 18 | # namespace: system 19 | # patch: |- 20 | # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 | # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 | # - op: remove 23 | # path: /spec/template/spec/containers/1/volumeMounts/0 24 | # # Remove the "cert" volume, since OLM will create and mount a set of certs. 25 | # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 26 | # - op: remove 27 | # path: /spec/template/spec/volumes/0 28 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | scheme: https 15 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 16 | tlsConfig: 17 | insecureSkipVerify: true 18 | selector: 19 | matchLabels: 20 | control-plane: controller-manager 21 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: manager-metrics 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /config/rbac/customer_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit customers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: customer-editor-role 6 | rules: 7 | - apiGroups: 8 | - jiraservicedesk.stakater.com 9 | resources: 10 | - customers 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - jiraservicedesk.stakater.com 21 | resources: 22 | - customers/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/customer_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view customers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: customer-viewer-role 6 | rules: 7 | - apiGroups: 8 | - jiraservicedesk.stakater.com 9 | resources: 10 | - customers 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - jiraservicedesk.stakater.com 17 | resources: 18 | - customers/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - coordination.k8s.io 21 | resources: 22 | - leases 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - events 35 | verbs: 36 | - create 37 | - patch 38 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/project_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit projects. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: project-editor-role 6 | rules: 7 | - apiGroups: 8 | - jiraservicedesk.stakater.com 9 | resources: 10 | - projects 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - jiraservicedesk.stakater.com 21 | resources: 22 | - projects/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/project_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view projects. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: project-viewer-role 6 | rules: 7 | - apiGroups: 8 | - jiraservicedesk.stakater.com 9 | resources: 10 | - projects 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - jiraservicedesk.stakater.com 17 | resources: 18 | - projects/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: manager-role 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - secrets 12 | verbs: 13 | - get 14 | - list 15 | - apiGroups: 16 | - jiraservicedesk.stakater.com 17 | resources: 18 | - customers 19 | verbs: 20 | - create 21 | - delete 22 | - get 23 | - list 24 | - patch 25 | - update 26 | - watch 27 | - apiGroups: 28 | - jiraservicedesk.stakater.com 29 | resources: 30 | - customers/status 31 | verbs: 32 | - get 33 | - patch 34 | - update 35 | - apiGroups: 36 | - jiraservicedesk.stakater.com 37 | resources: 38 | - projects 39 | verbs: 40 | - create 41 | - delete 42 | - get 43 | - list 44 | - patch 45 | - update 46 | - watch 47 | - apiGroups: 48 | - jiraservicedesk.stakater.com 49 | resources: 50 | - projects/status 51 | verbs: 52 | - get 53 | - patch 54 | - update 55 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | -------------------------------------------------------------------------------- /config/samples/jiraservicedesk_v1alpha1_customer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: jiraservicedesk.stakater.com/v1alpha1 2 | kind: Customer 3 | metadata: 4 | name: customer 5 | spec: 6 | name: sample 7 | email: samplecustomer@sample.com 8 | projects: 9 | - TEST1 10 | - TEST2 -------------------------------------------------------------------------------- /config/samples/jiraservicedesk_v1alpha1_project.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: jiraservicedesk.stakater.com/v1alpha1 2 | kind: Project 3 | metadata: 4 | name: stakater 5 | spec: 6 | name: stakater 7 | key: STK 8 | projectTypeKey: service_desk 9 | projectTemplateKey: com.atlassian.servicedesk:itil-v2-service-desk-project 10 | description: "Sample project for jira-service-desk-operator" 11 | assigneeType: PROJECT_LEAD 12 | leadAccountId: 5ebfbc3ead226b0ba46c3590 13 | url: https://stakater.com -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - jiraservicedesk_v1alpha1_customer.yaml 4 | - jiraservicedesk_v1alpha1_project.yaml 5 | #+kubebuilder:scaffold:manifestskustomizesamples 6 | -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | #+kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.20.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.20.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.20.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.20.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.20.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.20.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | creationTimestamp: null 6 | name: mutating-webhook-configuration 7 | webhooks: 8 | - admissionReviewVersions: 9 | - v1 10 | clientConfig: 11 | service: 12 | name: webhook-service 13 | namespace: system 14 | path: /mutate-jiraservicedesk-stakater-com-v1alpha1-customer 15 | failurePolicy: Fail 16 | name: mcustomer.kb.io 17 | rules: 18 | - apiGroups: 19 | - jiraservicedesk.stakater.com 20 | apiVersions: 21 | - v1alpha1 22 | operations: 23 | - CREATE 24 | - UPDATE 25 | resources: 26 | - customers 27 | sideEffects: None 28 | - admissionReviewVersions: 29 | - v1 30 | clientConfig: 31 | service: 32 | name: webhook-service 33 | namespace: system 34 | path: /mutate-jiraservicedesk-stakater-com-v1alpha1-project 35 | failurePolicy: Fail 36 | name: mproject.kb.io 37 | rules: 38 | - apiGroups: 39 | - jiraservicedesk.stakater.com 40 | apiVersions: 41 | - v1alpha1 42 | operations: 43 | - CREATE 44 | - UPDATE 45 | resources: 46 | - projects 47 | sideEffects: None 48 | --- 49 | apiVersion: admissionregistration.k8s.io/v1 50 | kind: ValidatingWebhookConfiguration 51 | metadata: 52 | creationTimestamp: null 53 | name: validating-webhook-configuration 54 | webhooks: 55 | - admissionReviewVersions: 56 | - v1 57 | clientConfig: 58 | service: 59 | name: webhook-service 60 | namespace: system 61 | path: /validate-jiraservicedesk-stakater-com-v1alpha1-customer 62 | failurePolicy: Fail 63 | name: vcustomer.kb.io 64 | rules: 65 | - apiGroups: 66 | - jiraservicedesk.stakater.com 67 | apiVersions: 68 | - v1alpha1 69 | operations: 70 | - CREATE 71 | - UPDATE 72 | resources: 73 | - customers 74 | sideEffects: None 75 | - admissionReviewVersions: 76 | - v1 77 | clientConfig: 78 | service: 79 | name: webhook-service 80 | namespace: system 81 | path: /validate-jiraservicedesk-stakater-com-v1alpha1-project 82 | failurePolicy: Fail 83 | name: vproject.kb.io 84 | rules: 85 | - apiGroups: 86 | - jiraservicedesk.stakater.com 87 | apiVersions: 88 | - v1alpha1 89 | operations: 90 | - CREATE 91 | - UPDATE 92 | resources: 93 | - projects 94 | sideEffects: None 95 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | protocol: TCP 11 | targetPort: 9443 12 | selector: 13 | control-plane: controller-manager 14 | -------------------------------------------------------------------------------- /controllers/customer_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "time" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | 11 | "k8s.io/apimachinery/pkg/types" 12 | 13 | "github.com/stakater/jira-service-desk-operator/api/v1alpha1" 14 | mockData "github.com/stakater/jira-service-desk-operator/mock" 15 | ) 16 | 17 | var _ = Describe("Customer Controller", func() { 18 | 19 | ns, _ = os.LookupEnv("OPERATOR_NAMESPACE") 20 | 21 | customerInput := mockData.SampleCustomer 22 | // Randomize customer name and email 23 | str := cUtil.RandSeqString(3) 24 | customerInput.Spec.Name += str 25 | customerInput.Spec.Email = "customer" + str + "@sample.com" 26 | 27 | AfterEach(func() { 28 | cUtil.TryDeleteCustomer(customerInput.Spec.Name, ns) 29 | }) 30 | 31 | Describe("Create new Jira Service Desk customer", func() { 32 | Context("With valid fields", func() { 33 | It("should create a new customer", func() { 34 | _ = cUtil.CreateCustomer(customerInput, ns) 35 | customer := cUtil.GetCustomer(customerInput.Spec.Name, ns) 36 | 37 | Expect(customer.Status.CustomerId).ToNot(Equal("")) 38 | }) 39 | }) 40 | }) 41 | 42 | Describe("Modifying customer associations", func() { 43 | Describe("Add Jira Service Desk customer to project", func() { 44 | Context("With Valid Project Id", func() { 45 | It("Should add the customer in the project", func() { 46 | project := util.GetProject(mockData.CustomerTestProjectInput.Spec.Name, ns) 47 | Expect(project.Status.ID).ToNot(Equal("")) 48 | 49 | _ = cUtil.CreateCustomer(customerInput, ns) 50 | time.Sleep(5 * time.Second) 51 | 52 | customer := cUtil.GetCustomer(customerInput.Spec.Name, ns) 53 | 54 | Expect(customer.Status.CustomerId).ToNot(Equal("")) 55 | 56 | customer.Spec.Projects = []string{strings.ToUpper(customerKey)} 57 | 58 | _ = cUtil.UpdateCustomer(customer, ns) 59 | updatedCustomer := cUtil.GetCustomer(customer.Spec.Name, ns) 60 | 61 | Expect(customer.Spec.Projects).To(Equal(updatedCustomer.Status.AssociatedProjects)) 62 | }) 63 | }) 64 | }) 65 | 66 | Describe("Remove Jira Service Desk customer from project", func() { 67 | Context("With Valid Project Id", func() { 68 | It("Should remove the customer from that project", func() { 69 | project := util.GetProject(mockData.CustomerTestProjectInput.Spec.Name, ns) 70 | Expect(project.Status.ID).ToNot(Equal("")) 71 | 72 | mockData.SampleUpdatedCustomer.Spec.Name = customerInput.Spec.Name 73 | mockData.SampleUpdatedCustomer.Spec.Email = customerInput.Spec.Email 74 | // Assigning Customer -> CustomerTestproject Key 75 | mockData.SampleUpdatedCustomer.Spec.Projects = []string{strings.ToUpper(customerKey)} 76 | 77 | _ = cUtil.CreateCustomer(mockData.SampleUpdatedCustomer, ns) 78 | time.Sleep(5 * time.Second) 79 | 80 | customer := cUtil.GetCustomer(mockData.SampleUpdatedCustomer.Spec.Name, ns) 81 | 82 | Expect(customer.Status.CustomerId).ToNot(Equal("")) 83 | 84 | customer.Spec.Projects = []string{strings.ToUpper(projectKey)} 85 | 86 | _ = cUtil.UpdateCustomer(customer, ns) 87 | updatedCustomer := cUtil.GetCustomer(customer.Spec.Name, ns) 88 | 89 | Expect(customer.Spec.Projects).To(Equal(updatedCustomer.Status.AssociatedProjects)) 90 | }) 91 | }) 92 | }) 93 | }) 94 | 95 | Describe("Delete Jira Service Desk customer", func() { 96 | Context("With valid Customer AccountId", func() { 97 | It("should delete the customer", func() { 98 | 99 | _ = cUtil.CreateCustomer(customerInput, ns) 100 | 101 | customer := cUtil.GetCustomer(customerInput.Spec.Name, ns) 102 | Expect(customer.Status.CustomerId).NotTo(BeEmpty()) 103 | 104 | cUtil.DeleteCustomer(customer.Name, ns) 105 | 106 | customerObject := &v1alpha1.Customer{} 107 | err := k8sClient.Get(ctx, types.NamespacedName{Name: customerInput.Spec.Name, Namespace: ns}, customerObject) 108 | 109 | Expect(err).To(HaveOccurred()) 110 | }) 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /controllers/project_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "strings" 22 | 23 | "github.com/go-logr/logr" 24 | "k8s.io/apimachinery/pkg/api/errors" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | 29 | jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" 30 | jiraservicedeskclient "github.com/stakater/jira-service-desk-operator/pkg/jiraservicedesk/client" 31 | finalizerUtil "github.com/stakater/operator-utils/util/finalizer" 32 | reconcilerUtil "github.com/stakater/operator-utils/util/reconciler" 33 | ) 34 | 35 | const ( 36 | // TODO: Check if this is required in our case 37 | // defaultRequeueTime = 60 * time.Second 38 | ProjectFinalizer string = "jiraservicedesk.stakater.com/project" 39 | ProjectAlreadyExistsErr string = "A project with that name already exists." 40 | ) 41 | 42 | // ProjectReconciler reconciles a Project object 43 | type ProjectReconciler struct { 44 | client.Client 45 | Scheme *runtime.Scheme 46 | Log logr.Logger 47 | JiraServiceDeskClient jiraservicedeskclient.Client 48 | } 49 | 50 | // +kubebuilder:rbac:groups=jiraservicedesk.stakater.com,resources=projects,verbs=get;list;watch;create;update;patch;delete 51 | // +kubebuilder:rbac:groups=jiraservicedesk.stakater.com,resources=projects/status,verbs=get;update;patch 52 | // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list 53 | 54 | func (r *ProjectReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 55 | _ = context.Background() 56 | log := r.Log.WithValues("project", req.NamespacedName) 57 | 58 | log.Info("Reconciling Project") 59 | 60 | // Fetch the Project instance 61 | instance := &jiraservicedeskv1alpha1.Project{} 62 | 63 | err := r.Get(context.TODO(), req.NamespacedName, instance) 64 | if err != nil { 65 | if errors.IsNotFound(err) { 66 | // Request object not found, could have been deleted after reconcile request. 67 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 68 | // Return and don't requeue 69 | return reconcilerUtil.DoNotRequeue() 70 | } 71 | // Error reading the object - requeue the request. 72 | return reconcilerUtil.RequeueWithError(err) 73 | } 74 | 75 | // Validate Custom Resource 76 | if ok, err := instance.IsValid(); !ok { 77 | return reconcilerUtil.ManageError(r.Client, instance, err, false) 78 | } 79 | 80 | // Resource is marked for deletion 81 | if instance.DeletionTimestamp != nil { 82 | log.Info("Deletion timestamp found for instance " + req.Name) 83 | if finalizerUtil.HasFinalizer(instance, ProjectFinalizer) { 84 | return r.handleDelete(req, instance) 85 | } 86 | // Finalizer doesn't exist so clean up is already done 87 | return reconcilerUtil.DoNotRequeue() 88 | } 89 | 90 | // Add finalizer if it doesn't exist 91 | if !finalizerUtil.HasFinalizer(instance, ProjectFinalizer) { 92 | log.Info("Adding finalizer for instance " + req.Name) 93 | 94 | finalizerUtil.AddFinalizer(instance, ProjectFinalizer) 95 | 96 | err := r.Client.Update(context.TODO(), instance) 97 | if err != nil { 98 | return reconcilerUtil.ManageError(r.Client, instance, err, false) 99 | } 100 | } 101 | 102 | // Check if the Project already exists 103 | if len(instance.Status.ID) > 0 { 104 | existingProject, err := r.JiraServiceDeskClient.GetProjectByIdentifier(instance.Status.ID) 105 | if err != nil { 106 | return reconcilerUtil.ManageError(r.Client, instance, err, false) 107 | } 108 | // Project already exists 109 | if len(existingProject.Id) > 0 { 110 | updatedProject := r.JiraServiceDeskClient.GetProjectFromProjectCR(instance) 111 | // Compare retrieved project with current spec 112 | if !r.JiraServiceDeskClient.ProjectEqual(existingProject, updatedProject) { 113 | // Update if there are changes in the declared spec 114 | return r.handleUpdate(req, existingProject, instance) 115 | } else { 116 | log.Info("Skipping update. No changes found") 117 | return reconcilerUtil.DoNotRequeue() 118 | } 119 | } 120 | } 121 | 122 | return r.handleCreate(req, instance) 123 | } 124 | 125 | func (r *ProjectReconciler) SetupWithManager(mgr ctrl.Manager) error { 126 | return ctrl.NewControllerManagedBy(mgr). 127 | For(&jiraservicedeskv1alpha1.Project{}). 128 | Complete(r) 129 | } 130 | 131 | func (r *ProjectReconciler) handleCreate(req ctrl.Request, instance *jiraservicedeskv1alpha1.Project) (ctrl.Result, error) { 132 | log := r.Log.WithValues("project", req.NamespacedName) 133 | 134 | log.Info("Creating Jira Service Desk Project: " + instance.Spec.Name) 135 | 136 | project := r.JiraServiceDeskClient.GetProjectFromProjectCR(instance) 137 | projectId, err := r.JiraServiceDeskClient.CreateProject(project) 138 | 139 | // If project already exists then reconstruct status 140 | if err != nil && strings.Contains(err.Error(), ProjectAlreadyExistsErr) { 141 | existingProject, err := r.JiraServiceDeskClient.GetProjectByIdentifier(instance.Spec.Key) 142 | if err != nil { 143 | return reconcilerUtil.ManageError(r.Client, instance, err, false) 144 | } 145 | log.Info("Successfully reconstructed status for Jira Service Desk Project " + instance.Spec.Name) 146 | 147 | projectId = existingProject.Id 148 | } else if err != nil { 149 | return reconcilerUtil.ManageError(r.Client, instance, err, false) 150 | } 151 | 152 | log.Info("Successfully created Jira Service Desk Project: " + instance.Spec.Name) 153 | 154 | if !instance.Spec.OpenAccess { 155 | err = r.JiraServiceDeskClient.UpdateProjectAccessPermissions(instance.Spec.OpenAccess, project.Key) 156 | if err != nil { 157 | return reconcilerUtil.ManageError(r.Client, instance, err, false) 158 | } 159 | 160 | log.Info("Successfully updated the Access Permissions to customer") 161 | } 162 | 163 | instance.Status.ID = projectId 164 | return reconcilerUtil.ManageSuccess(r.Client, instance) 165 | } 166 | 167 | func (r *ProjectReconciler) handleDelete(req ctrl.Request, instance *jiraservicedeskv1alpha1.Project) (ctrl.Result, error) { 168 | log := r.Log.WithValues("project", req.NamespacedName) 169 | 170 | if instance == nil { 171 | // Instance not found, nothing to do 172 | return reconcilerUtil.DoNotRequeue() 173 | } 174 | 175 | log.Info("Deleting Jira Service Desk Project: " + instance.Spec.Name) 176 | 177 | // Check if the project was created 178 | if instance.Status.ID != "" { 179 | err := r.JiraServiceDeskClient.DeleteProject(instance.Status.ID) 180 | if err != nil { 181 | return reconcilerUtil.ManageError(r.Client, instance, err, false) 182 | } 183 | } else { 184 | log.Info("Project '" + instance.Spec.Name + "' do not exists on JSD. So skipping deletion") 185 | } 186 | 187 | // Delete finalizer 188 | finalizerUtil.DeleteFinalizer(instance, ProjectFinalizer) 189 | 190 | log.Info("Finalizer removed for project: " + instance.Spec.Name) 191 | 192 | // Update instance 193 | err := r.Client.Update(context.TODO(), instance) 194 | if err != nil { 195 | return reconcilerUtil.ManageError(r.Client, instance, err, false) 196 | } 197 | 198 | return reconcilerUtil.DoNotRequeue() 199 | } 200 | 201 | func (r *ProjectReconciler) handleUpdate(req ctrl.Request, existingProject jiraservicedeskclient.Project, instance *jiraservicedeskv1alpha1.Project) (ctrl.Result, error) { 202 | log := r.Log.WithValues("project", req.NamespacedName) 203 | 204 | log.Info("Updating Jira Service Desk Project: " + instance.Spec.Name) 205 | 206 | existingProjectInstance := r.JiraServiceDeskClient.GetProjectCRFromProject(existingProject) 207 | if ok, err := instance.IsValidUpdate(existingProjectInstance); !ok { 208 | return reconcilerUtil.ManageError(r.Client, instance, err, false) 209 | } 210 | 211 | updatedProject := r.JiraServiceDeskClient.GetProjectForUpdateRequest(existingProject, instance) 212 | err := r.JiraServiceDeskClient.UpdateProject(updatedProject, existingProject.Id) 213 | if err != nil { 214 | log.Error(err, "Failed to update status of Project") 215 | return reconcilerUtil.ManageError(r.Client, instance, err, false) 216 | } 217 | 218 | return reconcilerUtil.ManageSuccess(r.Client, instance) 219 | } 220 | -------------------------------------------------------------------------------- /controllers/project_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strings" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" 11 | mockData "github.com/stakater/jira-service-desk-operator/mock" 12 | "k8s.io/apimachinery/pkg/types" 13 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 14 | ) 15 | 16 | var _ = Describe("Project Controller", func() { 17 | 18 | ns, _ = os.LookupEnv("OPERATOR_NAMESPACE") 19 | 20 | Describe("Positive test cases", func() { 21 | 22 | projectInput := mockData.CreateProjectInput 23 | 24 | // Generation of 3 char long random string 25 | key := cUtil.RandSeqString(3) 26 | 27 | projectInput.Spec.Name += key 28 | projectInput.Spec.Key = strings.ToUpper(key) 29 | 30 | AfterEach(func() { 31 | util.TryDeleteProject(projectInput.Spec.Name, ns) 32 | }) 33 | 34 | Describe("Create new Jira service desk project resource", func() { 35 | Context("With valid fields", func() { 36 | It("should create a new project", func() { 37 | _ = util.CreateProject(projectInput, ns) 38 | project := util.GetProject(projectInput.Spec.Name, ns) 39 | 40 | Expect(project.Status.ID).ToNot(Equal("")) 41 | }) 42 | }) 43 | }) 44 | 45 | Describe("Deleting jira service desk project resource", func() { 46 | Context("With valid project Id", func() { 47 | It("should remove resource and delete project ", func() { 48 | _ = util.CreateProject(projectInput, ns) 49 | 50 | project := util.GetProject(projectInput.Spec.Name, ns) 51 | Expect(project.Status.ID).NotTo(BeEmpty()) 52 | 53 | util.DeleteProject(project.Name, ns) 54 | 55 | projectObject := &jiraservicedeskv1alpha1.Project{} 56 | err := k8sClient.Get(ctx, types.NamespacedName{Name: projectInput.Spec.Name, Namespace: ns}, projectObject) 57 | 58 | Expect(err).To(HaveOccurred()) 59 | }) 60 | }) 61 | }) 62 | 63 | Describe("Updating jira service desk resource", func() { 64 | Context("With mutable fields ", func() { 65 | 66 | It("should assign changed field values to Project", func() { 67 | _ = util.CreateProject(projectInput, ns) 68 | project := util.GetProject(projectInput.Spec.Name, ns) 69 | 70 | project.Spec.Name = mockData.UpdateMutableProjectFields.Name 71 | project.Spec.Key = mockData.UpdateMutableProjectFields.Key 72 | 73 | err := k8sClient.Update(ctx, project) 74 | if err != nil { 75 | Fail(err.Error()) 76 | } 77 | 78 | req := reconcile.Request{NamespacedName: types.NamespacedName{Name: projectInput.Spec.Name, Namespace: ns}} 79 | _, err = r.Reconcile(context.Background(), req) 80 | if err != nil { 81 | Fail(err.Error()) 82 | } 83 | 84 | updatedProject := util.GetProject(projectInput.Spec.Name, ns) 85 | 86 | Expect(updatedProject.Spec.Name).To(Equal(mockData.UpdateMutableProjectFields.Name)) 87 | Expect(updatedProject.Spec.Key).To(Equal(mockData.UpdateMutableProjectFields.Key)) 88 | }) 89 | 90 | }) 91 | Context("With immutable fields ", func() { 92 | 93 | It("should not assign changed field values to Project", func() { 94 | _ = util.CreateProject(projectInput, ns) 95 | project := util.GetProject(projectInput.Spec.Name, ns) 96 | 97 | oldTypeKey := project.Spec.ProjectTypeKey 98 | project.Spec.ProjectTemplateKey = mockData.UpdateImmutableProjectFields.ProjectTypeKey 99 | 100 | err := k8sClient.Update(ctx, project) 101 | if err != nil { 102 | Fail(err.Error()) 103 | } 104 | 105 | req := reconcile.Request{NamespacedName: types.NamespacedName{Name: projectInput.Spec.Name, Namespace: ns}} 106 | _, err = r.Reconcile(context.Background(), req) 107 | if err != nil { 108 | Fail(err.Error()) 109 | } 110 | 111 | updatedProject := util.GetProject(projectInput.Spec.Name, ns) 112 | 113 | Expect(updatedProject.Spec.ProjectTypeKey).To(Equal(oldTypeKey)) 114 | }) 115 | }) 116 | }) 117 | }) 118 | 119 | Describe("Negative test cases", func() { 120 | 121 | projectInvalidInput := mockData.CreateProjectInvalidInput 122 | 123 | Describe("Create new Jira servie desk project resource", func() { 124 | Context("with invalid fields", func() { 125 | It("should not create a new project", func() { 126 | key := cUtil.RandSeqString(9) 127 | projectInvalidInput.Spec.Key = strings.ToUpper(key) 128 | projectInvalidInput.Spec.Name += key[:3] 129 | 130 | _ = util.CreateProject(projectInvalidInput, ns) 131 | project := util.GetProject(projectInvalidInput.Spec.Name, ns) 132 | 133 | Expect(project.Status.ID).To(Equal("")) 134 | 135 | util.TryDeleteProject(projectInvalidInput.Spec.Name, ns) 136 | }) 137 | }) 138 | }) 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | "testing" 25 | 26 | . "github.com/onsi/ginkgo" 27 | . "github.com/onsi/gomega" 28 | mockData "github.com/stakater/jira-service-desk-operator/mock" 29 | "k8s.io/client-go/kubernetes/scheme" 30 | "k8s.io/client-go/rest" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | "sigs.k8s.io/controller-runtime/pkg/envtest" 33 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 34 | logf "sigs.k8s.io/controller-runtime/pkg/log" 35 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 36 | 37 | jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" 38 | controllerUtil "github.com/stakater/jira-service-desk-operator/controllers/util" 39 | c "github.com/stakater/jira-service-desk-operator/pkg/jiraservicedesk/client" 40 | "github.com/stakater/jira-service-desk-operator/pkg/jiraservicedesk/config" 41 | secretsUtil "github.com/stakater/operator-utils/util/secrets" 42 | // +kubebuilder:scaffold:imports 43 | ) 44 | 45 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 46 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 47 | 48 | var cfg *rest.Config 49 | var k8sClient client.Client 50 | var testEnv *envtest.Environment 51 | 52 | var ctx context.Context 53 | var r *ProjectReconciler 54 | var util *controllerUtil.TestUtil 55 | var ns string 56 | 57 | var cr *CustomerReconciler 58 | var cUtil *controllerUtil.TestUtil 59 | 60 | var log = logf.Log.WithName("config") 61 | var customerKey = cUtil.RandSeqString(3) 62 | var projectKey = cUtil.RandSeqString(3) 63 | 64 | func TestAPIs(t *testing.T) { 65 | RegisterFailHandler(Fail) 66 | 67 | RunSpecsWithDefaultAndCustomReporters(t, 68 | "Controller Suite", 69 | []Reporter{printer.NewlineReporter{}}) 70 | } 71 | 72 | var _ = BeforeSuite(func(done Done) { 73 | 74 | logger := zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)) 75 | logf.SetLogger(logger) 76 | 77 | By("bootstrapping test environment") 78 | testEnv = &envtest.Environment{ 79 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 80 | } 81 | 82 | var err error 83 | cfg, err = testEnv.Start() 84 | Expect(err).ToNot(HaveOccurred()) 85 | Expect(cfg).ToNot(BeNil()) 86 | 87 | err = jiraservicedeskv1alpha1.AddToScheme(scheme.Scheme) 88 | Expect(err).NotTo(HaveOccurred()) 89 | 90 | // +kubebuilder:scaffold:scheme 91 | 92 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 93 | 94 | Expect(err).ToNot(HaveOccurred()) 95 | Expect(k8sClient).ToNot(BeNil()) 96 | 97 | ctx = context.Background() 98 | 99 | // Retrieve operator namespace 100 | ns, _ := os.LookupEnv("OPERATOR_NAMESPACE") 101 | 102 | apiToken, err := secretsUtil.LoadSecretDataUsingClient(k8sClient, config.JiraServiceDeskSecretName, ns, config.JiraServiceDeskAPITokenSecretKey) 103 | Expect(err).ToNot(HaveOccurred()) 104 | Expect(apiToken).ToNot(BeNil()) 105 | 106 | apiBaseUrl, err := secretsUtil.LoadSecretDataUsingClient(k8sClient, config.JiraServiceDeskSecretName, ns, config.JiraServiceDeskAPIBaseURLSecretKey) 107 | Expect(err).ToNot(HaveOccurred()) 108 | Expect(apiBaseUrl).ToNot(BeNil()) 109 | 110 | email, err := secretsUtil.LoadSecretDataUsingClient(k8sClient, config.JiraServiceDeskSecretName, ns, config.JiraServiceDeskEmailSecretKey) 111 | Expect(err).ToNot(HaveOccurred()) 112 | Expect(email).ToNot(BeNil()) 113 | 114 | r = &ProjectReconciler{ 115 | Client: k8sClient, 116 | Scheme: scheme.Scheme, 117 | Log: log.WithName("Reconciler"), 118 | JiraServiceDeskClient: c.NewClient(apiToken, apiBaseUrl, email), 119 | } 120 | Expect(r).ToNot((BeNil())) 121 | 122 | util = controllerUtil.New(ctx, k8sClient, r) 123 | Expect(util).ToNot(BeNil()) 124 | 125 | cr = &CustomerReconciler{ 126 | Client: k8sClient, 127 | Scheme: scheme.Scheme, 128 | Log: log.WithName("Reconciler"), 129 | JiraServiceDeskClient: c.NewClient(apiToken, apiBaseUrl, email), 130 | } 131 | Expect(cr).ToNot((BeNil())) 132 | 133 | cUtil = controllerUtil.New(ctx, k8sClient, cr) 134 | Expect(util).ToNot(BeNil()) 135 | 136 | mockData.CustomerTestProjectInput.Spec.Name += customerKey 137 | mockData.CustomerTestProjectInput.Spec.Key = strings.ToUpper(customerKey) 138 | 139 | mockData.SampleProjectInput.Spec.Name += projectKey 140 | mockData.SampleProjectInput.Spec.Key = strings.ToUpper(projectKey) 141 | 142 | // Creating projects for customer tests 143 | _ = util.CreateProject(mockData.CustomerTestProjectInput, ns) 144 | _ = util.CreateProject(mockData.SampleProjectInput, ns) 145 | 146 | close(done) 147 | }, 60) 148 | 149 | var _ = AfterSuite(func() { 150 | // Delete the projects created for customer tests 151 | util.DeleteProject(mockData.CustomerTestProjectInput.Spec.Name, ns) 152 | util.DeleteProject(mockData.SampleProjectInput.Spec.Name, ns) 153 | 154 | // Cleanup - Delete all remnent resources 155 | util.DeleteAllProjects(ns) 156 | util.DeleteAllCustomers(ns) 157 | 158 | By("tearing down the test environment") 159 | err := testEnv.Stop() 160 | Expect(err).ToNot(HaveOccurred()) 161 | }) 162 | -------------------------------------------------------------------------------- /examples/customer/customer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: jiraservicedesk.stakater.com/v1alpha1 2 | kind: Customer 3 | metadata: 4 | name: customer 5 | spec: 6 | name: sample 7 | email: samplecustomer@sample.com 8 | projects: 9 | - TEST1 10 | - TEST2 -------------------------------------------------------------------------------- /examples/customer/legacy-customer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: jiraservicedesk.stakater.com/v1alpha1 2 | kind: Customer 3 | metadata: 4 | name: legacy-customer 5 | spec: 6 | name: Legacy Customer 7 | email: legacycustomer@sample.com 8 | legacyCustomer: true 9 | projects: 10 | - TEST1 11 | - TEST2 -------------------------------------------------------------------------------- /examples/project/classic-project.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: jiraservicedesk.stakater.com/v1alpha1 2 | kind: Project 3 | metadata: 4 | name: stakater 5 | spec: 6 | name: stakater 7 | key: STK 8 | projectTypeKey: service_desk 9 | projectTemplateKey: com.atlassian.servicedesk:itil-v2-service-desk-project 10 | description: "Sample project for jira-service-desk-operator" 11 | assigneeType: PROJECT_LEAD 12 | leadAccountId: 5ebfbc3ead226b0ba46c3590 13 | url: https://stakater.com 14 | -------------------------------------------------------------------------------- /examples/project/next-gen-project.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: jiraservicedesk.stakater.com/v1alpha1 2 | kind: Project 3 | metadata: 4 | name: stakater 5 | spec: 6 | name: stakater 7 | key: STK 8 | projectTypeKey: service_desk 9 | projectTemplateKey: com.atlassian.servicedesk:next-gen-it-service-desk 10 | description: "Sample project for jira-service-desk-operator" 11 | assigneeType: PROJECT_LEAD 12 | leadAccountId: 5ebfbc3ead226b0ba46c3590 13 | url: https://stakater.com 14 | 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stakater/jira-service-desk-operator 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-logr/logr v1.2.0 7 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 8 | github.com/onsi/ginkgo v1.16.5 9 | github.com/onsi/gomega v1.17.0 10 | github.com/stakater/operator-utils v0.1.13 11 | go.uber.org/zap v1.19.1 12 | gopkg.in/h2non/gock.v1 v1.0.16 13 | k8s.io/api v0.23.0 14 | k8s.io/apimachinery v0.23.0 15 | k8s.io/client-go v0.23.0 16 | sigs.k8s.io/controller-runtime v0.11.0 17 | ) 18 | 19 | require ( 20 | cloud.google.com/go v0.81.0 // indirect 21 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 22 | github.com/Azure/go-autorest/autorest v0.11.18 // indirect 23 | github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect 24 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 25 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 26 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 27 | github.com/beorn7/perks v1.0.1 // indirect 28 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 31 | github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect 32 | github.com/fsnotify/fsnotify v1.5.1 // indirect 33 | github.com/go-logr/zapr v1.2.0 // indirect 34 | github.com/gogo/protobuf v1.3.2 // indirect 35 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 36 | github.com/golang/protobuf v1.5.2 // indirect 37 | github.com/google/go-cmp v0.5.5 // indirect 38 | github.com/google/gofuzz v1.1.0 // indirect 39 | github.com/google/uuid v1.1.2 // indirect 40 | github.com/googleapis/gnostic v0.5.5 // indirect 41 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect 42 | github.com/imdario/mergo v0.3.12 // indirect 43 | github.com/json-iterator/go v1.1.12 // indirect 44 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/nxadm/tail v1.4.8 // indirect 48 | github.com/pkg/errors v0.9.1 // indirect 49 | github.com/prometheus/client_golang v1.11.0 // indirect 50 | github.com/prometheus/client_model v0.2.0 // indirect 51 | github.com/prometheus/common v0.28.0 // indirect 52 | github.com/prometheus/procfs v0.6.0 // indirect 53 | github.com/spf13/pflag v1.0.5 // indirect 54 | go.uber.org/atomic v1.7.0 // indirect 55 | go.uber.org/multierr v1.6.0 // indirect 56 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect 57 | golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect 58 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect 59 | golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect 60 | golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect 61 | golang.org/x/text v0.3.7 // indirect 62 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 63 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 64 | google.golang.org/appengine v1.6.7 // indirect 65 | google.golang.org/protobuf v1.27.1 // indirect 66 | gopkg.in/inf.v0 v0.9.1 // indirect 67 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 68 | gopkg.in/yaml.v2 v2.4.0 // indirect 69 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 70 | k8s.io/apiextensions-apiserver v0.23.0 // indirect 71 | k8s.io/component-base v0.23.0 // indirect 72 | k8s.io/klog/v2 v2.30.0 // indirect 73 | k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect 74 | k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect 75 | sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect 76 | sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect 77 | sigs.k8s.io/yaml v1.3.0 // indirect 78 | ) 79 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "os" 23 | "strings" 24 | 25 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 26 | // to ensure that exec-entrypoint and run can make use of them. 27 | "go.uber.org/zap/zapcore" 28 | _ "k8s.io/client-go/plugin/pkg/client/auth" 29 | 30 | "k8s.io/apimachinery/pkg/runtime" 31 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 32 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 33 | ctrl "sigs.k8s.io/controller-runtime" 34 | "sigs.k8s.io/controller-runtime/pkg/cache" 35 | "sigs.k8s.io/controller-runtime/pkg/healthz" 36 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 37 | 38 | jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" 39 | "github.com/stakater/jira-service-desk-operator/controllers" 40 | jiraservicedeskclient "github.com/stakater/jira-service-desk-operator/pkg/jiraservicedesk/client" 41 | jiraservicedeskconfig "github.com/stakater/jira-service-desk-operator/pkg/jiraservicedesk/config" 42 | // +kubebuilder:scaffold:imports 43 | ) 44 | 45 | var ( 46 | scheme = runtime.NewScheme() 47 | setupLog = ctrl.Log.WithName("setup") 48 | ) 49 | 50 | func init() { 51 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 52 | 53 | utilruntime.Must(jiraservicedeskv1alpha1.AddToScheme(scheme)) 54 | // +kubebuilder:scaffold:scheme 55 | } 56 | 57 | func main() { 58 | var metricsAddr string 59 | var enableLeaderElection bool 60 | var probeAddr string 61 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 62 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 63 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 64 | "Enable leader election for controller manager. "+ 65 | "Enabling this will ensure there is only one active controller manager.") 66 | opts := zap.Options{ 67 | Development: true, 68 | TimeEncoder: zapcore.ISO8601TimeEncoder, 69 | } 70 | opts.BindFlags(flag.CommandLine) 71 | flag.Parse() 72 | 73 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 74 | 75 | watchNamespace, err := getWatchNamespace() 76 | if err != nil { 77 | setupLog.Error(err, "unable to get WatchNamespace, "+ 78 | "the manager will watch and manage resources in all Namespaces") 79 | } 80 | 81 | // Manager options 82 | options := ctrl.Options{ 83 | Scheme: scheme, 84 | MetricsBindAddress: metricsAddr, 85 | Port: 9443, 86 | HealthProbeBindAddress: probeAddr, 87 | LeaderElection: enableLeaderElection, 88 | LeaderElectionID: "48610e2b.stakater.com", 89 | Namespace: watchNamespace, // namespaced-scope when the value is not an empty string 90 | } 91 | 92 | // Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2) 93 | // More Info: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/cache#MultiNamespacedCacheBuilder 94 | if strings.Contains(watchNamespace, ",") { 95 | setupLog.Info("manager will be watching namespace %q", watchNamespace) 96 | // configure cluster-scoped with MultiNamespacedCacheBuilder 97 | options.Namespace = "" 98 | options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(watchNamespace, ",")) 99 | } 100 | 101 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options) 102 | 103 | if err != nil { 104 | setupLog.Error(err, "unable to start manager") 105 | os.Exit(1) 106 | } 107 | 108 | // Load config for controller from secret 109 | controllerConfig, err := jiraservicedeskconfig.LoadControllerConfig(mgr.GetAPIReader()) 110 | if err != nil { 111 | setupLog.Error(err, "unable to load controller config") 112 | os.Exit(1) 113 | } 114 | 115 | if err = (&controllers.ProjectReconciler{ 116 | Client: mgr.GetClient(), 117 | Scheme: mgr.GetScheme(), 118 | Log: ctrl.Log.WithName("controllers").WithName("Project"), 119 | JiraServiceDeskClient: jiraservicedeskclient.NewClient(controllerConfig.ApiToken, controllerConfig.ApiBaseUrl, controllerConfig.Email), 120 | }).SetupWithManager(mgr); err != nil { 121 | setupLog.Error(err, "unable to create controller", "controller", "Project") 122 | os.Exit(1) 123 | } 124 | 125 | if err = (&controllers.CustomerReconciler{ 126 | Client: mgr.GetClient(), 127 | Log: ctrl.Log.WithName("controllers").WithName("Customer"), 128 | Scheme: mgr.GetScheme(), 129 | JiraServiceDeskClient: jiraservicedeskclient.NewClient(controllerConfig.ApiToken, controllerConfig.ApiBaseUrl, controllerConfig.Email), 130 | }).SetupWithManager(mgr); err != nil { 131 | setupLog.Error(err, "unable to create controller", "controller", "Customer") 132 | os.Exit(1) 133 | } 134 | 135 | if os.Getenv("ENABLE_WEBHOOKS") != "false" { 136 | if err = (&jiraservicedeskv1alpha1.Project{}).SetupWebhookWithManager(mgr); err != nil { 137 | setupLog.Error(err, "unable to create webhook", "webhook", "Project") 138 | os.Exit(1) 139 | } 140 | if err = (&jiraservicedeskv1alpha1.Customer{}).SetupWebhookWithManager(mgr); err != nil { 141 | setupLog.Error(err, "unable to create webhook", "webhook", "Customer") 142 | os.Exit(1) 143 | } 144 | } 145 | 146 | // Add health endpoints 147 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 148 | setupLog.Error(err, "unable to set up health check") 149 | os.Exit(1) 150 | } 151 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 152 | setupLog.Error(err, "unable to set up ready check") 153 | os.Exit(1) 154 | } 155 | // +kubebuilder:scaffold:builder 156 | 157 | setupLog.Info("starting manager") 158 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 159 | setupLog.Error(err, "problem running manager") 160 | os.Exit(1) 161 | } 162 | } 163 | 164 | // getWatchNamespace returns the Namespace the operator should be watching for changes 165 | func getWatchNamespace() (string, error) { 166 | // WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE 167 | // which specifies the Namespace to watch. 168 | // An empty value means the operator is running with cluster scope. 169 | var watchNamespaceEnvVar = "WATCH_NAMESPACE" 170 | 171 | ns, found := os.LookupEnv(watchNamespaceEnvVar) 172 | if !found { 173 | return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar) 174 | } 175 | return ns, nil 176 | } 177 | -------------------------------------------------------------------------------- /mock/data.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "strconv" 5 | 6 | jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" 7 | ) 8 | 9 | const BaseURL = "https://sample.atlassian.net" 10 | 11 | var ProjectID = "10003" 12 | var ProjectIDInt, _ = strconv.Atoi(ProjectID) 13 | var InvalidPermissionScheme = "4000" 14 | 15 | var GetProjectFailedErrorMsg = "Rest request to get Project failed with status: 404" 16 | var CreateProjectFailedErrorMsg = "Rest request to create Project failed with status: 400 and response: " 17 | var UpdateProjectFailedErrorMsg = "Rest request to update Project failed with status: 404 and response: " 18 | var DeleteProjectFailedErrorMsg = "Rest request to delete Project failed with status: 404" 19 | 20 | var GetCustomerFailedErrorMsg = "Rest request to get customer failed with status: 400" 21 | var CreateCustomerFailedErrorMsg = "Rest request to create customer failed with status: 400 and response: " 22 | var AddCustomerFailedErrorMsg = "Rest request to add Customer failed with status: 400" 23 | var RemoveCustomerFailedErrorMsg = "Rest request to remove Customer failed with status: 400" 24 | var DeleteCustomerFailedErrorMsg = "Rest request to delete Customer failed with status: 400" 25 | 26 | var ProjectObjectModifiedError = "Operation cannot be fulfilled on projects.jiraservicedesk.stakater.com \"%s\": the object has been modified; please apply your changes to the latest version and try again" 27 | var CustomerObjectModifiedError = "Operation cannot be fulfilled on customers.jiraservicedesk.stakater.com \"%s\": the object has been modified; please apply your changes to the latest version and try again" 28 | 29 | var AddCustomerSuccessResponse = map[string]interface{}{ 30 | "accountIds": []string{CustomerAccountId}, 31 | } 32 | 33 | var AddedProjectsList = []string{"CTP"} 34 | 35 | var SampleCustomer = jiraservicedeskv1alpha1.Customer{ 36 | Spec: jiraservicedeskv1alpha1.CustomerSpec{ 37 | Name: "customer", 38 | Email: "customer@sample.com", 39 | Projects: []string{ 40 | "SAMPLE", 41 | }, 42 | }, 43 | } 44 | 45 | var SampleUpdatedCustomer = jiraservicedeskv1alpha1.Customer{ 46 | Spec: jiraservicedeskv1alpha1.CustomerSpec{ 47 | Name: "customer", 48 | Email: "customer@sample.com", 49 | Projects: []string{ 50 | "TEST", 51 | }, 52 | }, 53 | } 54 | 55 | var CreateProjectInputJSON = map[string]string{ 56 | "name": "testproject", 57 | "key": "TEST", 58 | "projectTypeKey": "service_desk", 59 | "projectTemplateKey": "com.atlassian.servicedesk:itil-v2-service-desk-project", 60 | "description": "Sample project for jira-service-desk-operator", 61 | "assigneeType": "PROJECT_LEAD", 62 | "leadAccountId": "5ebfbc3ead226b0ba46c3590", 63 | "url": "https://test.com", 64 | } 65 | 66 | var CreateProjectResponseJSON = map[string]interface{}{ 67 | "self": BaseURL + "/rest/api/3/project/10003", 68 | "id": ProjectIDInt, 69 | "key": "KEY", 70 | } 71 | 72 | var CreateProjectInput = jiraservicedeskv1alpha1.Project{ 73 | Spec: jiraservicedeskv1alpha1.ProjectSpec{ 74 | Name: "testproject", 75 | Key: "TEST", 76 | ProjectTypeKey: "service_desk", 77 | ProjectTemplateKey: "com.atlassian.servicedesk:itil-v2-service-desk-project", 78 | Description: "Sample project for jira-service-desk-operator", 79 | AssigneeType: "PROJECT_LEAD", 80 | LeadAccountId: "5ebfbc3ead226b0ba46c3590", 81 | URL: "https://test.com", 82 | }, 83 | } 84 | 85 | var CustomerTestProjectInput = jiraservicedeskv1alpha1.Project{ 86 | Spec: jiraservicedeskv1alpha1.ProjectSpec{ 87 | Name: "customertestproject", 88 | Key: "CTP", 89 | ProjectTypeKey: "service_desk", 90 | ProjectTemplateKey: "com.atlassian.servicedesk:itil-v2-service-desk-project", 91 | Description: "Sample project for jira-service-desk-operator", 92 | AssigneeType: "PROJECT_LEAD", 93 | LeadAccountId: "5ebfbc3ead226b0ba46c3590", 94 | URL: "https://test.com", 95 | }, 96 | } 97 | 98 | var CreateProjectInvalidInput = jiraservicedeskv1alpha1.Project{ 99 | Spec: jiraservicedeskv1alpha1.ProjectSpec{ 100 | Name: "test", 101 | Key: "TEST20000", 102 | ProjectTypeKey: "service_desk", 103 | ProjectTemplateKey: "com.atlassian.servicedesk:itil-v2-service-desk-project", 104 | Description: "Sample project for jira-service-desk-operator", 105 | AssigneeType: "PROJECT_LEAD", 106 | LeadAccountId: "5ebfbc3ead226b0ba46c3590", 107 | URL: "https://test.com", 108 | AvatarId: 10200, 109 | IssueSecurityScheme: 10001, 110 | PermissionScheme: 10011, 111 | NotificationScheme: 10021, 112 | CategoryId: 10120, 113 | }, 114 | } 115 | 116 | var SampleProjectInput = jiraservicedeskv1alpha1.Project{ 117 | Spec: jiraservicedeskv1alpha1.ProjectSpec{ 118 | Name: "sampleproject", 119 | Key: "SAMPLE", 120 | ProjectTypeKey: "service_desk", 121 | ProjectTemplateKey: "com.atlassian.servicedesk:itil-v2-service-desk-project", 122 | Description: "Sample project for jira-service-desk-operator", 123 | AssigneeType: "PROJECT_LEAD", 124 | LeadAccountId: "5ebfbc3ead226b0ba46c3590", 125 | URL: "https://test.com", 126 | }, 127 | } 128 | 129 | var UpdateMutableProjectFields = struct { 130 | Name string 131 | Key string 132 | }{ 133 | "testupdated", 134 | "TEST2", 135 | } 136 | 137 | var UpdateImmutableProjectFields = struct { 138 | ProjectTypeKey string 139 | }{ 140 | "business", 141 | } 142 | 143 | var GetProjectByIdResponseJSON = map[string]string{ 144 | "description": "Sample Project", 145 | "name": "Sample", 146 | "assigneeType": "UNASSIGNED", 147 | "projectTypeKey": "business", 148 | "key": "KEY", 149 | "url": "https://www.sample.com", 150 | } 151 | 152 | var GetProjectByIdExpectedResponse = struct { 153 | Description string 154 | Name string 155 | AssigneeType string 156 | ProjectTypeKey string 157 | Key string 158 | URL string 159 | }{ 160 | "Sample Project", 161 | "Sample", 162 | "UNASSIGNED", 163 | "business", 164 | "KEY", 165 | "https://www.sample.com", 166 | } 167 | 168 | var UpdateProjectInput = struct { 169 | Id string 170 | Name string 171 | Key string 172 | }{ 173 | Id: "99999", 174 | Name: "stakater2", 175 | Key: "WEE", 176 | } 177 | 178 | var UpdateProjectRequestJSON = map[string]string{ 179 | "name": "stakater2", 180 | "key": "WEE", 181 | } 182 | 183 | var UpdateProjectResponseJSON = map[string]interface{}{ 184 | "self": BaseURL + "/rest/api/3/project/" + ProjectID, 185 | "id": ProjectIDInt, 186 | "key": "STK", 187 | } 188 | 189 | var GetCustomerResponse = struct { 190 | AccountId string 191 | DisplayName string 192 | Email string 193 | }{ 194 | AccountId: "sample12345", 195 | DisplayName: "Sample Customer", 196 | Email: "sample@test.com", 197 | } 198 | 199 | var GetCustomerResponseJSON = map[string]string{ 200 | "self": "https://sample.net/user?accountId=sample12345", 201 | "accountId": "sample12345", 202 | "emailAddress": "sample@test.com", 203 | "displayName": "Sample Customer", 204 | "accountType": "customer", 205 | } 206 | 207 | var CustomerAccountId = "sample12345" 208 | 209 | var CreateCustomerInputJSON = map[string]string{ 210 | "displayName": "Sample Customer", 211 | "email": "sample@test.com", 212 | } 213 | 214 | var CreateCustomerResponseJSON = map[string]string{ 215 | "accountId": "sample12345", 216 | "displayName": "Sample Customer", 217 | "emailAddress": "sample@test.com", 218 | } 219 | 220 | var AddProjectKey string = "ADD" 221 | var CustomerEndPoint string = "/customer" 222 | 223 | var RemoveProjectKey string = "REMOVE" 224 | -------------------------------------------------------------------------------- /pkg/jiraservicedesk/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | 11 | jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" 12 | logf "sigs.k8s.io/controller-runtime/pkg/log" 13 | ) 14 | 15 | var Log = logf.Log.WithName("jiraServiceDeskClient") 16 | 17 | type Client interface { 18 | // Methods for Project 19 | GetProjectByIdentifier(identifier string) (Project, error) 20 | GetProjectFromProjectCR(project *jiraservicedeskv1alpha1.Project) Project 21 | GetProjectCRFromProject(project Project) jiraservicedeskv1alpha1.Project 22 | CreateProject(project Project) (string, error) 23 | DeleteProject(id string) error 24 | UpdateProject(updatedProject Project, id string) error 25 | ProjectEqual(oldProject Project, newProject Project) bool 26 | GetProjectForUpdateRequest(existingProject Project, newProject *jiraservicedeskv1alpha1.Project) Project 27 | UpdateProjectAccessPermissions(status bool, key string) error 28 | GetCustomerById(customerAccountId string) (Customer, error) 29 | GetCustomerIdByEmail(emailAddress string) (string, error) 30 | CreateCustomer(customer Customer) (string, error) 31 | CreateLegacyCustomer(email string, projectKey string) (string, error) 32 | IsCustomerUpdated(customer *jiraservicedeskv1alpha1.Customer, existingCustomer Customer) bool 33 | AddCustomerToProject(customerAccountId string, projectKey string) error 34 | RemoveCustomerFromProject(customerAccountId string, projectKey string) error 35 | DeleteCustomer(customerAccountId string) error 36 | GetCustomerCRFromCustomer(customer Customer) jiraservicedeskv1alpha1.Customer 37 | GetCustomerFromCustomerCRForCreateCustomer(customer *jiraservicedeskv1alpha1.Customer) Customer 38 | } 39 | 40 | // Client wraps http client 41 | type jiraServiceDeskClient struct { 42 | apiToken string 43 | baseURL string 44 | email string 45 | httpClient *http.Client 46 | } 47 | 48 | // NewClient creates an API client 49 | func NewClient(apiToken string, baseURL string, email string) Client { 50 | return &jiraServiceDeskClient{ 51 | apiToken: apiToken, 52 | baseURL: baseURL, 53 | email: email, 54 | httpClient: http.DefaultClient, 55 | } 56 | } 57 | 58 | func (c *jiraServiceDeskClient) newRequest(method, path string, body interface{}, experimental bool) (*http.Request, error) { 59 | endpoint := c.baseURL + path 60 | url, err := url.Parse(endpoint) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | var buf io.ReadWriter 66 | 67 | if body != nil { 68 | buf = new(bytes.Buffer) 69 | err := json.NewEncoder(buf).Encode(body) 70 | if err != nil { 71 | return nil, err 72 | } 73 | } 74 | req, err := http.NewRequest(method, url.String(), buf) 75 | if err != nil { 76 | return nil, err 77 | } 78 | if body != nil { 79 | req.Header.Set("Content-Type", "application/json") 80 | } 81 | req.Header.Set("Accept", "application/json") 82 | req.Header.Set("User-Agent", "golang httpClient") 83 | 84 | if experimental { 85 | req.Header.Set("X-ExperimentalApi", "opt-in") 86 | } 87 | 88 | req.SetBasicAuth(c.email, c.apiToken) 89 | return req, nil 90 | } 91 | 92 | func (c *jiraServiceDeskClient) do(req *http.Request) (*http.Response, error) { 93 | resp, err := c.httpClient.Do(req) 94 | if err != nil { 95 | return resp, fmt.Errorf("Error calling the API endpoint: %v", err) 96 | } 97 | 98 | return resp, nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/jiraservicedesk/client/client_customer.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "reflect" 8 | "strconv" 9 | 10 | jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" 11 | ) 12 | 13 | const ( 14 | // Endpoints 15 | CreateCustomerApiPath = "/rest/servicedeskapi/customer" 16 | AddCustomerApiPath = "/rest/servicedeskapi/servicedesk/" 17 | EndpointUser = "/rest/api/3/user?accountId=" 18 | LegacyCustomerApiPath = "/rest/servicedesk/1/pages/people/customers/pagination/" 19 | LegacyCustomerCreateEndpoint = "/invite" 20 | SearchUserEndpoint = "/rest/api/3/user/search?query=" 21 | ) 22 | 23 | type Customer struct { 24 | AccountId string `json:"accountId,omitempty"` 25 | DisplayName string `json:"displayName,omitempty"` 26 | Email string `json:"email,omitempty"` 27 | ProjectKeys []string `json:"projectKeys,omitempty"` 28 | } 29 | 30 | type CustomerCreateResponse struct { 31 | AccountId string `json:"accountId,omitempty"` 32 | EmailAddress string `json:"emailAddress,omitempty"` 33 | DisplayName string `json:"displayName,omitempty"` 34 | } 35 | 36 | type CustomerAddResponse struct { 37 | AccountIds []string `json:"accountIds,omitempty"` 38 | } 39 | 40 | type CustomerGetResponse struct { 41 | Self string `json:"self,omitempty"` 42 | AccountId string `json:"accountId,omitempty"` 43 | EmailAddress string `json:"emailAddress,omitempty"` 44 | DisplayName string `json:"displayName,omitempty"` 45 | AccountType string `json:"accountType,omitempty"` 46 | } 47 | 48 | type CustomerGetByEmailResponse []CustomerGetResponse 49 | 50 | type LegacyCustomerRequestBody struct { 51 | Emails []string `json:"emails,omitempty"` 52 | } 53 | 54 | type LegacyCustomerCreateResponse struct { 55 | Success []LegacyCustomerSuccessResponse `json:"success,omitempty"` 56 | } 57 | 58 | type LegacyCustomerSuccessResponse struct { 59 | Key string `json:"key,omitempty"` 60 | EmailAddress string `json:"emailAddress,omitempty"` 61 | DisplayName string `json:"displayName,omitempty"` 62 | AccoundId string `json:"accountId,omitempty"` 63 | } 64 | 65 | // GetCustomerById gets a customer by ID from JSD 66 | func (c *jiraServiceDeskClient) GetCustomerById(customerAccountId string) (Customer, error) { 67 | var customer Customer 68 | 69 | request, err := c.newRequest("GET", EndpointUser+customerAccountId, nil, false) 70 | if err != nil { 71 | return customer, err 72 | } 73 | 74 | response, err := c.do(request) 75 | if err != nil { 76 | return customer, err 77 | } 78 | defer response.Body.Close() 79 | 80 | if response.StatusCode < 200 || response.StatusCode > 299 { 81 | err := errors.New("Rest request to get customer failed with status: " + strconv.Itoa(response.StatusCode)) 82 | return customer, err 83 | } 84 | 85 | var responseObject CustomerGetResponse 86 | err = json.NewDecoder(response.Body).Decode(&responseObject) 87 | if err != nil { 88 | return customer, err 89 | } 90 | 91 | customer = customerGetResponseToCustomerMapper(responseObject) 92 | 93 | return customer, err 94 | } 95 | 96 | // GetCustomerById gets a customer by ID from JSD 97 | func (c *jiraServiceDeskClient) GetCustomerIdByEmail(emailAddress string) (string, error) { 98 | request, err := c.newRequest("GET", SearchUserEndpoint+emailAddress, nil, false) 99 | if err != nil { 100 | return "", err 101 | } 102 | 103 | response, err := c.do(request) 104 | if err != nil { 105 | return "", err 106 | } 107 | defer response.Body.Close() 108 | 109 | if response.StatusCode < 200 || response.StatusCode > 299 { 110 | err := errors.New("Rest request to get customer failed with status: " + strconv.Itoa(response.StatusCode)) 111 | return "", err 112 | } 113 | 114 | var responseObject CustomerGetByEmailResponse 115 | err = json.NewDecoder(response.Body).Decode(&responseObject) 116 | if err != nil { 117 | return "", err 118 | } 119 | 120 | if len(responseObject) > 0 { 121 | return responseObject[0].AccountId, err 122 | } 123 | 124 | return "", err 125 | } 126 | 127 | // CreateCustomer create a new customer on JSD 128 | func (c *jiraServiceDeskClient) CreateCustomer(customer Customer) (string, error) { 129 | request, err := c.newRequest("POST", CreateCustomerApiPath, customer, false) 130 | if err != nil { 131 | return "", err 132 | } 133 | 134 | response, err := c.do(request) 135 | if err != nil { 136 | return "", err 137 | } 138 | 139 | defer response.Body.Close() 140 | responseData, _ := ioutil.ReadAll(response.Body) 141 | 142 | if response.StatusCode < 200 || response.StatusCode > 299 { 143 | err = errors.New("Rest request to create customer failed with status: " + strconv.Itoa(response.StatusCode) + 144 | " and response: " + string(responseData)) 145 | return "", err 146 | } 147 | 148 | var responseObject CustomerCreateResponse 149 | err = json.Unmarshal(responseData, &responseObject) 150 | if err != nil { 151 | return "", err 152 | } 153 | 154 | return responseObject.AccountId, err 155 | } 156 | 157 | // CreateLegacyCustomer create a customer on JSD using the legacy api endpoint 158 | func (c *jiraServiceDeskClient) CreateLegacyCustomer(customerEmail string, projectKey string) (string, error) { 159 | legacyCustomerRequestBody := LegacyCustomerRequestBody{ 160 | Emails: []string{customerEmail}, 161 | } 162 | 163 | request, err := c.newRequest("POST", LegacyCustomerApiPath+projectKey+LegacyCustomerCreateEndpoint, legacyCustomerRequestBody, false) 164 | if err != nil { 165 | return "", err 166 | } 167 | 168 | response, err := c.do(request) 169 | if err != nil { 170 | return "", err 171 | } 172 | defer response.Body.Close() 173 | responseData, _ := ioutil.ReadAll(response.Body) 174 | 175 | if response.StatusCode < 200 || response.StatusCode > 299 { 176 | err = errors.New("Rest request to create legacy customer failed with status: " + strconv.Itoa(response.StatusCode) + 177 | " and response: " + string(responseData)) 178 | return "", err 179 | } 180 | 181 | var responseObject LegacyCustomerCreateResponse 182 | err = json.Unmarshal(responseData, &responseObject) 183 | if err != nil { 184 | return "", err 185 | } 186 | 187 | return responseObject.Success[0].AccoundId, nil 188 | } 189 | 190 | // AddCustomerToProject adds a customer to a JSD project 191 | func (c *jiraServiceDeskClient) AddCustomerToProject(customerAccountId string, projectKey string) error { 192 | addCustomerBody := CustomerAddResponse{ 193 | AccountIds: []string{customerAccountId}, 194 | } 195 | 196 | request, err := c.newRequest("POST", AddCustomerApiPath+projectKey+"/customer", addCustomerBody, false) 197 | if err != nil { 198 | return err 199 | } 200 | 201 | response, err := c.do(request) 202 | if err != nil { 203 | return err 204 | } 205 | 206 | defer response.Body.Close() 207 | 208 | if response.StatusCode < 200 || response.StatusCode > 299 { 209 | err = errors.New("Rest request to add Customer failed with status: " + strconv.Itoa(response.StatusCode)) 210 | return err 211 | } 212 | 213 | return nil 214 | } 215 | 216 | func (c *jiraServiceDeskClient) IsCustomerUpdated(customer *jiraservicedeskv1alpha1.Customer, existingCustomer Customer) bool { 217 | if reflect.DeepEqual(customer.Spec.Projects, customer.Status.AssociatedProjects) && customer.Spec.Email == existingCustomer.Email { 218 | return false 219 | } else { 220 | return true 221 | } 222 | } 223 | 224 | // RemoveCustomerFromProject removes a customer from JSD project 225 | func (c *jiraServiceDeskClient) RemoveCustomerFromProject(customerAccountId string, projectKey string) error { 226 | removeCustomerBody := CustomerAddResponse{ 227 | AccountIds: []string{customerAccountId}, 228 | } 229 | 230 | request, err := c.newRequest("DELETE", AddCustomerApiPath+projectKey+"/customer", removeCustomerBody, true) 231 | if err != nil { 232 | return err 233 | } 234 | 235 | response, err := c.do(request) 236 | if err != nil { 237 | return err 238 | } 239 | 240 | defer response.Body.Close() 241 | 242 | if response.StatusCode < 200 || response.StatusCode > 299 { 243 | err = errors.New("Rest request to remove Customer failed with status: " + strconv.Itoa(response.StatusCode)) 244 | return err 245 | } 246 | 247 | return nil 248 | } 249 | 250 | // Delete customer deletes a customer from JSD 251 | func (c *jiraServiceDeskClient) DeleteCustomer(customerAccountId string) error { 252 | request, err := c.newRequest("DELETE", EndpointUser+customerAccountId, nil, false) 253 | if err != nil { 254 | return err 255 | } 256 | 257 | response, err := c.do(request) 258 | if err != nil { 259 | return err 260 | } 261 | 262 | defer response.Body.Close() 263 | 264 | if response.StatusCode < 200 || response.StatusCode > 299 { 265 | err = errors.New("Rest request to delete Customer failed with status: " + strconv.Itoa(response.StatusCode)) 266 | return err 267 | } 268 | 269 | return nil 270 | } 271 | 272 | func (c *jiraServiceDeskClient) GetCustomerCRFromCustomer(customer Customer) jiraservicedeskv1alpha1.Customer { 273 | return customerToCustomerCRMapper(customer) 274 | } 275 | 276 | func (c *jiraServiceDeskClient) GetCustomerFromCustomerCRForCreateCustomer(customer *jiraservicedeskv1alpha1.Customer) Customer { 277 | return customerCRToCustomerMapperForCreateCustomer(customer) 278 | } 279 | -------------------------------------------------------------------------------- /pkg/jiraservicedesk/client/client_customer_mappers.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" 4 | 5 | func customerToCustomerCRMapper(customer Customer) jiraservicedeskv1alpha1.Customer { 6 | var customerObject jiraservicedeskv1alpha1.Customer 7 | 8 | customerObject.Spec.Email = customer.Email 9 | customerObject.Spec.Name = customer.DisplayName 10 | customerObject.Spec.Projects = customer.ProjectKeys 11 | 12 | return customerObject 13 | } 14 | 15 | func customerCRToCustomerMapperForCreateCustomer(customer *jiraservicedeskv1alpha1.Customer) Customer { 16 | customerObject := Customer{ 17 | DisplayName: customer.Spec.Name, 18 | Email: customer.Spec.Email, 19 | } 20 | return customerObject 21 | } 22 | 23 | func customerGetResponseToCustomerMapper(response CustomerGetResponse) Customer { 24 | return Customer{ 25 | AccountId: response.AccountId, 26 | DisplayName: response.DisplayName, 27 | Email: response.EmailAddress, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/jiraservicedesk/client/client_customer_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/nbio/st" 8 | mockData "github.com/stakater/jira-service-desk-operator/mock" 9 | "gopkg.in/h2non/gock.v1" 10 | ) 11 | 12 | func TestJiraClient_GetCustomerById_shouldGetCustomer_whenValidCustomerAccountIdIsGiven(t *testing.T) { 13 | defer gock.Off() 14 | 15 | gock.New(mockData.BaseURL+EndpointUser). 16 | Get("/"). 17 | MatchParam("accountId", mockData.CustomerAccountId). 18 | Reply(200). 19 | JSON(mockData.GetCustomerResponseJSON) 20 | 21 | jiraClient := NewClient("", mockData.BaseURL, "") 22 | customer, err := jiraClient.GetCustomerById(mockData.CustomerAccountId) 23 | 24 | st.Expect(t, customer.AccountId, mockData.GetCustomerResponse.AccountId) 25 | st.Expect(t, customer.DisplayName, mockData.GetCustomerResponse.DisplayName) 26 | st.Expect(t, customer.Email, mockData.GetCustomerResponse.Email) 27 | st.Expect(t, err, nil) 28 | 29 | st.Expect(t, gock.IsDone(), true) 30 | } 31 | 32 | func TestJiraClient_GetCustomerById_shouldNotGetCustomer_whenInValidCustomerAccountIdIsGiven(t *testing.T) { 33 | defer gock.Off() 34 | 35 | gock.New(mockData.BaseURL + EndpointUser). 36 | Get("/"). 37 | Reply(400) 38 | 39 | jiraClient := NewClient("", mockData.BaseURL, "") 40 | customer, err := jiraClient.GetCustomerById(mockData.CustomerAccountId) 41 | 42 | st.Expect(t, customer.AccountId, "") 43 | st.Expect(t, customer.DisplayName, "") 44 | st.Expect(t, customer.Email, "") 45 | 46 | st.Expect(t, err, errors.New(mockData.GetCustomerFailedErrorMsg)) 47 | st.Expect(t, gock.IsDone(), true) 48 | } 49 | 50 | func TestJiraClient_CreateCustomer_shouldCreateCustomer_whenValidCustomerDataIsGiven(t *testing.T) { 51 | defer gock.Off() 52 | 53 | gock.New(mockData.BaseURL + CreateCustomerApiPath). 54 | Post("/"). 55 | MatchType("json"). 56 | JSON(mockData.CreateCustomerInputJSON). 57 | Reply(201). 58 | JSON(mockData.CreateCustomerResponseJSON) 59 | 60 | sampleCustomer := Customer{ 61 | Email: mockData.GetCustomerResponse.Email, 62 | DisplayName: mockData.GetCustomerResponse.DisplayName, 63 | } 64 | 65 | jiraClient := NewClient("", mockData.BaseURL, "") 66 | id, err := jiraClient.CreateCustomer(sampleCustomer) 67 | 68 | st.Expect(t, id, mockData.CustomerAccountId) 69 | st.Expect(t, err, nil) 70 | 71 | st.Expect(t, gock.IsDone(), true) 72 | } 73 | 74 | func TestJiraClient_CreateCustomer_shouldNotCreateCustomer_whenInValidCustomerDataIsGiven(t *testing.T) { 75 | defer gock.Off() 76 | gock.New(mockData.BaseURL + CreateCustomerApiPath). 77 | Post("/"). 78 | Reply(400) 79 | 80 | sampleCustomer := Customer{ 81 | DisplayName: mockData.GetCustomerResponse.DisplayName, 82 | } 83 | 84 | jiraClient := NewClient("", mockData.BaseURL, "") 85 | id, err := jiraClient.CreateCustomer(sampleCustomer) 86 | 87 | st.Expect(t, id, "") 88 | st.Expect(t, err, errors.New(mockData.CreateCustomerFailedErrorMsg)) 89 | 90 | st.Expect(t, gock.IsDone(), true) 91 | } 92 | 93 | func TestJiraClient_AddCustomerToProject_shouldAddCustomerToProject_whenValidProjectIsGiven(t *testing.T) { 94 | defer gock.Off() 95 | gock.New(mockData.BaseURL + AddCustomerApiPath + mockData.AddProjectKey). 96 | Post(mockData.CustomerEndPoint). 97 | MatchType("json"). 98 | JSON(mockData.AddCustomerSuccessResponse). 99 | Reply(201) 100 | 101 | jiraClient := NewClient("", mockData.BaseURL, "") 102 | err := jiraClient.AddCustomerToProject(mockData.CustomerAccountId, mockData.AddProjectKey) 103 | 104 | st.Expect(t, err, nil) 105 | 106 | st.Expect(t, gock.IsDone(), true) 107 | } 108 | 109 | func TestJiraClient_AddCustomerToProject_shouldNotAddCustomerToProject_whenInValidProjectIsGiven(t *testing.T) { 110 | defer gock.Off() 111 | gock.New(mockData.BaseURL + AddCustomerApiPath + mockData.AddProjectKey). 112 | Post(mockData.CustomerEndPoint). 113 | Reply(400) 114 | 115 | jiraClient := NewClient("", mockData.BaseURL, "") 116 | err := jiraClient.AddCustomerToProject(mockData.CustomerAccountId, mockData.AddProjectKey) 117 | 118 | st.Expect(t, err, errors.New(mockData.AddCustomerFailedErrorMsg)) 119 | 120 | st.Expect(t, gock.IsDone(), true) 121 | } 122 | 123 | func TestJiraClient_RemoveCustomerFromProject_shouldRemoveCustomerFromProject_whenValidProjectIsGiven(t *testing.T) { 124 | defer gock.Off() 125 | gock.New(mockData.BaseURL + AddCustomerApiPath + mockData.RemoveProjectKey). 126 | Delete(mockData.CustomerEndPoint). 127 | Reply(201) 128 | 129 | jiraClient := NewClient("", mockData.BaseURL, "") 130 | err := jiraClient.RemoveCustomerFromProject(mockData.CustomerAccountId, mockData.RemoveProjectKey) 131 | 132 | st.Expect(t, err, nil) 133 | 134 | st.Expect(t, gock.IsDone(), true) 135 | } 136 | 137 | func TestJiraClient_RemoveCustomerFromProject_shouldNotRemoveCustomerFromProject_whenInvalidProjectIsGiven(t *testing.T) { 138 | defer gock.Off() 139 | gock.New(mockData.BaseURL + AddCustomerApiPath + mockData.RemoveProjectKey). 140 | Delete(mockData.CustomerEndPoint). 141 | Reply(400) 142 | 143 | jiraClient := NewClient("", mockData.BaseURL, "") 144 | err := jiraClient.RemoveCustomerFromProject(mockData.CustomerAccountId, mockData.RemoveProjectKey) 145 | 146 | st.Expect(t, err, errors.New(mockData.RemoveCustomerFailedErrorMsg)) 147 | 148 | st.Expect(t, gock.IsDone(), true) 149 | } 150 | 151 | func TestJiraClient_DeleteCustomer_shouldDeleteCustomer_whenValidCustomerIsGiven(t *testing.T) { 152 | defer gock.Off() 153 | gock.New(mockData.BaseURL+EndpointUser). 154 | Delete("/"). 155 | MatchParam("accountId", mockData.CustomerAccountId). 156 | Reply(200) 157 | 158 | jiraClient := NewClient("", mockData.BaseURL, "") 159 | err := jiraClient.DeleteCustomer(mockData.CustomerAccountId) 160 | 161 | st.Expect(t, err, nil) 162 | 163 | st.Expect(t, gock.IsDone(), true) 164 | } 165 | 166 | func TestJiraClient_DeleteCustomer_shouldNotDeleteCustomer_whenInvalidCustomerIsGiven(t *testing.T) { 167 | defer gock.Off() 168 | gock.New(mockData.BaseURL + EndpointUser). 169 | Delete("/"). 170 | Reply(400) 171 | 172 | jiraClient := NewClient("", mockData.BaseURL, "") 173 | err := jiraClient.DeleteCustomer(mockData.CustomerAccountId) 174 | 175 | st.Expect(t, err, errors.New(mockData.DeleteCustomerFailedErrorMsg)) 176 | 177 | st.Expect(t, gock.IsDone(), true) 178 | } 179 | -------------------------------------------------------------------------------- /pkg/jiraservicedesk/client/client_project.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "strconv" 8 | 9 | jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" 10 | ) 11 | 12 | const ( 13 | // Endpoints 14 | EndpointApiVersion3Project = "/rest/api/3/project" 15 | ServiceDeskV1ApiPath = "/rest/servicedesk/1/servicedesk/" 16 | RequestSecurityPath = "/settings/requestsecurity" 17 | 18 | // Project Template Types 19 | ClassicProjectTemplateKey = "com.atlassian.servicedesk:itil-v2-service-desk-project" 20 | NextGenProjectTemplateKey = "com.atlassian.servicedesk:next-gen-it-service-desk" 21 | ) 22 | 23 | type Project struct { 24 | Id string `json:"id,omitempty"` 25 | Name string `json:"name,omitempty"` 26 | Key string `json:"key,omitempty"` 27 | ProjectTypeKey string `json:"projectTypeKey,omitempty"` 28 | ProjectTemplateKey string `json:"projectTemplateKey,omitempty"` 29 | Description string `json:"description,omitempty"` 30 | AssigneeType string `json:"assigneeType,omitempty"` 31 | LeadAccountId string `json:"leadAccountId,omitempty"` 32 | URL string `json:"url,omitempty"` 33 | AvatarId int `json:"avatarId,omitempty"` 34 | IssueSecurityScheme int `json:"issueSecurityScheme,omitempty"` 35 | PermissionScheme int `json:"permissionScheme,omitempty"` 36 | NotificationScheme int `json:"notificationScheme,omitempty"` 37 | CategoryId int `json:"categoryId,omitempty"` 38 | } 39 | 40 | type ProjectGetResponse struct { 41 | Self string `json:"self,omitempty"` 42 | Id string `json:"id,omitempty"` 43 | Name string `json:"name,omitempty"` 44 | Key string `json:"key,omitempty"` 45 | Description string `json:"description,omitempty"` 46 | Lead ProjectLead `json:"lead,omitempty"` 47 | ProjectTypeKey string `json:"projectTypeKey,omitempty"` 48 | Style string `json:"style,omitempty"` 49 | AssigneeType string `json:"assigneeType,omitempty"` 50 | URL string `json:"url,omitempty"` 51 | } 52 | 53 | type ProjectLead struct { 54 | Self string `json:"self,omitempty"` 55 | AccountId string `json:"accountId,omitempty"` 56 | } 57 | 58 | type ProjectCreateResponse struct { 59 | Self string `json:"self"` 60 | Id int `json:"id"` 61 | Key string `json:"key"` 62 | } 63 | 64 | type CustomerAccessRequestBody struct { 65 | autocompleteEnabled bool 66 | manageEnabled bool 67 | serviceDeskOpenAccess bool 68 | serviceDeskPublicSignup bool 69 | } 70 | 71 | func (c *jiraServiceDeskClient) GetProjectByIdentifier(id string) (Project, error) { 72 | var project Project 73 | 74 | request, err := c.newRequest("GET", EndpointApiVersion3Project+"/"+id, nil, false) 75 | if err != nil { 76 | return project, err 77 | } 78 | 79 | response, err := c.do(request) 80 | if err != nil { 81 | return project, err 82 | } 83 | defer response.Body.Close() 84 | 85 | if response.StatusCode < 200 || response.StatusCode > 299 { 86 | err := errors.New("Rest request to get Project failed with status: " + strconv.Itoa(response.StatusCode)) 87 | return project, err 88 | } 89 | 90 | var responseObject ProjectGetResponse 91 | err = json.NewDecoder(response.Body).Decode(&responseObject) 92 | if err != nil { 93 | return project, err 94 | } 95 | 96 | project = projectGetResponseToProjectMapper(responseObject) 97 | return project, err 98 | } 99 | 100 | func (c *jiraServiceDeskClient) CreateProject(project Project) (string, error) { 101 | request, err := c.newRequest("POST", EndpointApiVersion3Project, project, false) 102 | if err != nil { 103 | return "", err 104 | } 105 | 106 | response, err := c.do(request) 107 | if err != nil { 108 | return "", err 109 | } 110 | 111 | defer response.Body.Close() 112 | responseData, _ := ioutil.ReadAll(response.Body) 113 | 114 | if response.StatusCode < 200 || response.StatusCode > 299 { 115 | err := errors.New("Rest request to create Project failed with status: " + strconv.Itoa(response.StatusCode) + 116 | " and response: " + string(responseData)) 117 | return "", err 118 | } 119 | 120 | var responseObject ProjectCreateResponse 121 | err = json.Unmarshal(responseData, &responseObject) 122 | if err != nil { 123 | return "", err 124 | } 125 | projectId := strconv.Itoa(responseObject.Id) 126 | 127 | return projectId, err 128 | } 129 | 130 | func (c *jiraServiceDeskClient) UpdateProject(updatedProject Project, id string) error { 131 | request, err := c.newRequest("PUT", EndpointApiVersion3Project+"/"+id, updatedProject, false) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | response, err := c.do(request) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | defer response.Body.Close() 142 | responseData, _ := ioutil.ReadAll(response.Body) 143 | 144 | if response.StatusCode < 200 || response.StatusCode > 299 { 145 | err := errors.New("Rest request to update Project failed with status: " + strconv.Itoa(response.StatusCode) + 146 | " and response: " + string(responseData)) 147 | return err 148 | } 149 | 150 | return nil 151 | } 152 | 153 | func (c *jiraServiceDeskClient) DeleteProject(id string) error { 154 | request, err := c.newRequest("DELETE", EndpointApiVersion3Project+"/"+id, nil, false) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | response, err := c.do(request) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | if response.StatusCode < 200 || response.StatusCode > 299 { 165 | return errors.New("Rest request to delete Project failed with status: " + strconv.Itoa(response.StatusCode)) 166 | } 167 | 168 | return err 169 | } 170 | 171 | func (c *jiraServiceDeskClient) UpdateProjectAccessPermissions(status bool, key string) error { 172 | body := CustomerAccessRequestBody{ 173 | autocompleteEnabled: false, 174 | manageEnabled: false, 175 | serviceDeskOpenAccess: status, 176 | serviceDeskPublicSignup: status, 177 | } 178 | 179 | request, err := c.newRequest("POST", ServiceDeskV1ApiPath+key+RequestSecurityPath, body, false) 180 | if err != nil { 181 | return err 182 | } 183 | 184 | response, err := c.do(request) 185 | if err != nil { 186 | return err 187 | } 188 | 189 | defer response.Body.Close() 190 | 191 | if response.StatusCode < 200 || response.StatusCode > 299 { 192 | err := errors.New("Rest request to update project permissions failed with status: " + strconv.Itoa(response.StatusCode)) 193 | return err 194 | } 195 | 196 | return err 197 | } 198 | 199 | func (c *jiraServiceDeskClient) ProjectEqual(oldProject Project, newProject Project) bool { 200 | // The fields AvatarId, IssueSecurityScheme, NotificationScheme, PermissionScheme, CategoryId are not retrieved 201 | // through get project REST API call so they cannot be used in project comparison 202 | return oldProject.Id == newProject.Id && 203 | oldProject.Name == newProject.Name && 204 | oldProject.Key == newProject.Key && 205 | oldProject.ProjectTypeKey == newProject.ProjectTypeKey && 206 | oldProject.ProjectTemplateKey == newProject.ProjectTemplateKey && 207 | oldProject.Description == newProject.Description && 208 | oldProject.AssigneeType == newProject.AssigneeType && 209 | oldProject.LeadAccountId == newProject.LeadAccountId && 210 | oldProject.URL == newProject.URL 211 | } 212 | 213 | func (c *jiraServiceDeskClient) GetProjectFromProjectCR(project *jiraservicedeskv1alpha1.Project) Project { 214 | return projectCRToProjectMapper(project) 215 | } 216 | 217 | func (c *jiraServiceDeskClient) GetProjectCRFromProject(project Project) jiraservicedeskv1alpha1.Project { 218 | return projectToProjectCRMapper(project) 219 | } 220 | 221 | func (c *jiraServiceDeskClient) GetProjectForUpdateRequest(existingProject Project, newProject *jiraservicedeskv1alpha1.Project) Project { 222 | var updatedProject Project 223 | if existingProject.Name != newProject.Spec.Name { 224 | updatedProject.Name = newProject.Spec.Name 225 | } 226 | if existingProject.Key != newProject.Spec.Key { 227 | updatedProject.Key = newProject.Spec.Key 228 | } 229 | if existingProject.AvatarId != newProject.Spec.AvatarId { 230 | updatedProject.AvatarId = newProject.Spec.AvatarId 231 | } 232 | if existingProject.Description != newProject.Spec.Description { 233 | updatedProject.Description = newProject.Spec.Description 234 | } 235 | if existingProject.AssigneeType != newProject.Spec.AssigneeType { 236 | updatedProject.AssigneeType = newProject.Spec.AssigneeType 237 | } 238 | if existingProject.URL != newProject.Spec.URL { 239 | updatedProject.URL = newProject.Spec.URL 240 | } 241 | return updatedProject 242 | 243 | } 244 | -------------------------------------------------------------------------------- /pkg/jiraservicedesk/client/client_project_mappers.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" 5 | ) 6 | 7 | func projectCRToProjectMapper(project *jiraservicedeskv1alpha1.Project) Project { 8 | 9 | projectObject := Project{ 10 | Name: project.Spec.Name, 11 | Key: project.Spec.Key, 12 | ProjectTypeKey: project.Spec.ProjectTypeKey, 13 | ProjectTemplateKey: project.Spec.ProjectTemplateKey, 14 | Description: project.Spec.Description, 15 | AssigneeType: project.Spec.AssigneeType, 16 | LeadAccountId: project.Spec.LeadAccountId, 17 | URL: project.Spec.URL, 18 | AvatarId: project.Spec.AvatarId, 19 | IssueSecurityScheme: project.Spec.IssueSecurityScheme, 20 | PermissionScheme: project.Spec.PermissionScheme, 21 | NotificationScheme: project.Spec.NotificationScheme, 22 | CategoryId: project.Spec.CategoryId, 23 | } 24 | 25 | if len(project.Status.ID) > 0 { 26 | projectObject.Id = project.Status.ID 27 | } 28 | 29 | return projectObject 30 | } 31 | 32 | func projectGetResponseToProjectMapper(response ProjectGetResponse) Project { 33 | var projectTemplateKey string 34 | if len(response.Style) > 0 { 35 | if response.Style == "classic" { 36 | projectTemplateKey = ClassicProjectTemplateKey 37 | } else if response.Style == "next-gen" { 38 | projectTemplateKey = NextGenProjectTemplateKey 39 | } 40 | } 41 | 42 | return Project{ 43 | Id: response.Id, 44 | Name: response.Name, 45 | Key: response.Key, 46 | ProjectTypeKey: response.ProjectTypeKey, 47 | ProjectTemplateKey: projectTemplateKey, 48 | Description: response.Description, 49 | AssigneeType: response.AssigneeType, 50 | LeadAccountId: response.Lead.AccountId, 51 | URL: response.URL, 52 | } 53 | } 54 | 55 | func projectToProjectCRMapper(project Project) jiraservicedeskv1alpha1.Project { 56 | 57 | var projectObject jiraservicedeskv1alpha1.Project 58 | 59 | projectObject.Status.ID = project.Id 60 | projectObject.Spec.Name = project.Name 61 | projectObject.Spec.Key = project.Key 62 | projectObject.Spec.ProjectTypeKey = project.ProjectTypeKey 63 | projectObject.Spec.ProjectTemplateKey = project.ProjectTemplateKey 64 | projectObject.Spec.Description = project.Description 65 | projectObject.Spec.AssigneeType = project.AssigneeType 66 | projectObject.Spec.LeadAccountId = project.LeadAccountId 67 | projectObject.Spec.URL = project.URL 68 | projectObject.Spec.AvatarId = project.AvatarId 69 | projectObject.Spec.IssueSecurityScheme = project.IssueSecurityScheme 70 | projectObject.Spec.PermissionScheme = project.PermissionScheme 71 | projectObject.Spec.NotificationScheme = project.NotificationScheme 72 | projectObject.Spec.CategoryId = project.CategoryId 73 | 74 | return projectObject 75 | } 76 | -------------------------------------------------------------------------------- /pkg/jiraservicedesk/client/client_project_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/nbio/st" 8 | "gopkg.in/h2non/gock.v1" 9 | 10 | "github.com/stakater/jira-service-desk-operator/mock" 11 | mockData "github.com/stakater/jira-service-desk-operator/mock" 12 | ) 13 | 14 | func TestJiraService_GetProject_shouldGetProject_whenValidProjectIdIsGiven(t *testing.T) { 15 | defer gock.Off() 16 | 17 | gock.New(mockData.BaseURL + EndpointApiVersion3Project). 18 | Get("/" + mockData.ProjectID). 19 | Reply(200). 20 | JSON(mockData.GetProjectByIdResponseJSON) 21 | 22 | jiraClient := NewClient("", mockData.BaseURL, "") 23 | project, err := jiraClient.GetProjectByIdentifier("/" + mockData.ProjectID) 24 | 25 | st.Expect(t, project.Description, mockData.GetProjectByIdExpectedResponse.Description) 26 | st.Expect(t, project.Name, mockData.GetProjectByIdExpectedResponse.Name) 27 | st.Expect(t, project.AssigneeType, mockData.GetProjectByIdExpectedResponse.AssigneeType) 28 | st.Expect(t, project.ProjectTypeKey, mockData.GetProjectByIdExpectedResponse.ProjectTypeKey) 29 | st.Expect(t, project.Key, mockData.GetProjectByIdExpectedResponse.Key) 30 | st.Expect(t, project.URL, mockData.GetProjectByIdExpectedResponse.URL) 31 | st.Expect(t, err, nil) 32 | 33 | st.Expect(t, gock.IsDone(), true) 34 | } 35 | 36 | func TestJiraService_GetProject_shouldNotGetProject_whenInValidProjectIdIsGiven(t *testing.T) { 37 | defer gock.Off() 38 | 39 | gock.New(mockData.BaseURL + EndpointApiVersion3Project). 40 | Get("/"). 41 | Reply(404) 42 | 43 | jiraClient := NewClient("", mockData.BaseURL, "") 44 | _, err := jiraClient.GetProjectByIdentifier("/" + mockData.ProjectID) 45 | 46 | st.Expect(t, err, errors.New(mockData.GetProjectFailedErrorMsg)) 47 | st.Expect(t, gock.IsDone(), true) 48 | } 49 | 50 | func TestJiraService_CreateProject_shouldCreateProject_whenValidProjectDataIsGiven(t *testing.T) { 51 | defer gock.Off() 52 | 53 | gock.New(mockData.BaseURL + EndpointApiVersion3Project). 54 | Post("/"). 55 | MatchType("json"). 56 | JSON(mockData.CreateProjectInputJSON). 57 | Reply(200). 58 | JSON(mockData.CreateProjectResponseJSON) 59 | 60 | sampleProject := Project{ 61 | Name: mockData.CreateProjectInput.Spec.Name, 62 | Key: mockData.CreateProjectInput.Spec.Key, 63 | ProjectTypeKey: mockData.CreateProjectInput.Spec.ProjectTypeKey, 64 | ProjectTemplateKey: mockData.CreateProjectInput.Spec.ProjectTemplateKey, 65 | Description: mockData.CreateProjectInput.Spec.Description, 66 | AssigneeType: mockData.CreateProjectInput.Spec.AssigneeType, 67 | LeadAccountId: mockData.CreateProjectInput.Spec.LeadAccountId, 68 | URL: mockData.CreateProjectInput.Spec.URL, 69 | } 70 | 71 | jiraClient := NewClient("", mockData.BaseURL, "") 72 | id, err := jiraClient.CreateProject(sampleProject) 73 | 74 | st.Expect(t, id, mockData.ProjectID) 75 | st.Expect(t, err, nil) 76 | 77 | st.Expect(t, gock.IsDone(), true) 78 | } 79 | 80 | func TestJiraService_CreateProject_shouldNotCreateProject_whenInValidProjectDataIsGiven(t *testing.T) { 81 | defer gock.Off() 82 | 83 | gock.New(mockData.BaseURL + EndpointApiVersion3Project). 84 | Post("/"). 85 | Reply(400) 86 | 87 | sampleProject := Project{ 88 | Name: mockData.CreateProjectInput.Spec.Name, 89 | } 90 | 91 | jiraClient := NewClient("", mockData.BaseURL, "") 92 | _, err := jiraClient.CreateProject(sampleProject) 93 | 94 | st.Expect(t, err, errors.New(mockData.CreateProjectFailedErrorMsg)) 95 | st.Expect(t, gock.IsDone(), true) 96 | } 97 | 98 | func TestJiraService_UpdateProject_shouldUpdateProject_whenValidProjectIdIsGiven(t *testing.T) { 99 | defer gock.Off() 100 | 101 | gock.New(mockData.BaseURL + EndpointApiVersion3Project). 102 | Put("/" + mockData.ProjectID). 103 | JSON(mock.UpdateProjectRequestJSON). 104 | Reply(204). 105 | JSON(mock.UpdateProjectResponseJSON) 106 | 107 | var updateProject = Project{ 108 | Key: mockData.UpdateProjectInput.Key, 109 | Name: mockData.UpdateProjectInput.Name, 110 | } 111 | 112 | client := NewClient("", mock.BaseURL, "") 113 | err := client.UpdateProject(updateProject, mock.ProjectID) 114 | st.Expect(t, err, nil) 115 | 116 | st.Expect(t, gock.IsDone(), true) 117 | } 118 | 119 | func TestJiraServiceDesk_UpdateProject_shouldNotUpdateProject_whenInvalidProjectDataIsGiven(t *testing.T) { 120 | defer gock.Off() 121 | 122 | gock.New(mock.BaseURL + EndpointApiVersion3Project). 123 | Put("/"). 124 | JSON(mock.UpdateProjectRequestJSON). 125 | Reply(404) 126 | 127 | var updateProject = Project{ 128 | Key: mock.UpdateProjectInput.Key, 129 | Name: mock.UpdateProjectInput.Name, 130 | } 131 | 132 | client := NewClient("", mock.BaseURL, "") 133 | err := client.UpdateProject(updateProject, mock.ProjectID) 134 | 135 | st.Expect(t, err, errors.New(mockData.UpdateProjectFailedErrorMsg)) 136 | st.Expect(t, gock.IsDone(), true) 137 | } 138 | 139 | func TestJiraServiceDesk_UpdateProject_shouldNotUpdateProject_whenImmutableFieldIsGiven(t *testing.T) { 140 | defer gock.Off() 141 | 142 | gock.New(mock.BaseURL + EndpointApiVersion3Project). 143 | Put("/" + mockData.ProjectID). 144 | Reply(404) 145 | 146 | var updateProject = Project{ 147 | Id: mock.UpdateProjectInput.Id, 148 | } 149 | 150 | client := NewClient("", mock.BaseURL, "") 151 | err := client.UpdateProject(updateProject, mock.ProjectID) 152 | 153 | st.Expect(t, err, errors.New(mockData.UpdateProjectFailedErrorMsg)) 154 | st.Expect(t, gock.IsDone(), true) 155 | } 156 | 157 | func TestJiraService_DeleteProject_shouldDeleteProject_whenValidProjectIdIsGiven(t *testing.T) { 158 | defer gock.Off() 159 | 160 | gock.New(mockData.BaseURL + EndpointApiVersion3Project). 161 | Delete("/" + mockData.ProjectID). 162 | Reply(204) 163 | 164 | jiraClient := NewClient("", mockData.BaseURL, "") 165 | 166 | err := jiraClient.DeleteProject(mockData.ProjectID) 167 | st.Expect(t, err, nil) 168 | 169 | st.Expect(t, gock.IsDone(), true) 170 | } 171 | 172 | func TestJiraService_DeleteProject_shouldNotDeleteProject_whenInValidProjectIdIsGiven(t *testing.T) { 173 | defer gock.Off() 174 | 175 | gock.New(mockData.BaseURL + EndpointApiVersion3Project). 176 | Delete("/"). 177 | Reply(404) 178 | 179 | jiraClient := NewClient("", mockData.BaseURL, "") 180 | err := jiraClient.DeleteProject(mockData.ProjectID) 181 | 182 | st.Expect(t, err, errors.New(mockData.DeleteProjectFailedErrorMsg)) 183 | st.Expect(t, gock.IsDone(), true) 184 | } 185 | -------------------------------------------------------------------------------- /pkg/jiraservicedesk/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | util "github.com/stakater/operator-utils/util" 7 | secretsUtil "github.com/stakater/operator-utils/util/secrets" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | logf "sigs.k8s.io/controller-runtime/pkg/log" 10 | ) 11 | 12 | var log = logf.Log.WithName("config") 13 | 14 | const ( 15 | JiraServiceDeskDefaultSecretName string = "jira-service-desk-config" 16 | JiraServiceDeskAPITokenSecretKey string = "JIRA_SERVICE_DESK_API_TOKEN" 17 | JiraServiceDeskAPIBaseURLSecretKey string = "JIRA_SERVICE_DESK_API_BASE_URL" 18 | JiraServiceDeskEmailSecretKey string = "JIRA_SERVICE_DESK_EMAIL" 19 | ) 20 | 21 | var ( 22 | JiraServiceDeskSecretName = getConfigSecretName() 23 | ) 24 | 25 | type ControllerConfig struct { 26 | ApiToken string 27 | ApiBaseUrl string 28 | Email string 29 | } 30 | 31 | func getConfigSecretName() string { 32 | configSecretName, _ := os.LookupEnv("CONFIG_SECRET_NAME") 33 | if len(configSecretName) == 0 { 34 | configSecretName = JiraServiceDeskDefaultSecretName 35 | log.Info("CONFIG_SECRET_NAME is unset, using default value: " + JiraServiceDeskDefaultSecretName) 36 | } 37 | return configSecretName 38 | } 39 | 40 | func LoadControllerConfig(apiReader client.Reader) (ControllerConfig, error) { 41 | log.Info("Loading Configuration from secret") 42 | 43 | // Retrieve operator namespace 44 | operatorNamespace, _ := os.LookupEnv("OPERATOR_NAMESPACE") 45 | if len(operatorNamespace) == 0 { 46 | operatorNamespaceTemp, err := util.GetOperatorNamespace() 47 | if err != nil { 48 | if err.Error() == "namespace not found for current environment" { 49 | log.Info("Skipping leader election; not running in a cluster.") 50 | } 51 | log.Error(err, "Unable to get operator namespace") 52 | } 53 | operatorNamespace = operatorNamespaceTemp 54 | } 55 | 56 | apiToken, err := secretsUtil.LoadSecretData(apiReader, JiraServiceDeskSecretName, operatorNamespace, JiraServiceDeskAPITokenSecretKey) 57 | if err != nil { 58 | log.Error(err, "Unable to fetch apiToken from secret") 59 | } 60 | 61 | apiBaseUrl, err := secretsUtil.LoadSecretData(apiReader, JiraServiceDeskSecretName, operatorNamespace, JiraServiceDeskAPIBaseURLSecretKey) 62 | if err != nil { 63 | log.Error(err, "Unable to fetch apiBaseUrl from secret") 64 | } 65 | 66 | email, err := secretsUtil.LoadSecretData(apiReader, JiraServiceDeskSecretName, operatorNamespace, JiraServiceDeskEmailSecretKey) 67 | if err != nil { 68 | log.Error(err, "Unable to fetch email from secret") 69 | } 70 | 71 | controllerConfig := ControllerConfig{ApiToken: apiToken, ApiBaseUrl: apiBaseUrl, Email: email} 72 | 73 | return controllerConfig, err 74 | } 75 | -------------------------------------------------------------------------------- /pkg/jiraservicedesk/jiraservicedesk.go: -------------------------------------------------------------------------------- 1 | package jiraservicedesk 2 | --------------------------------------------------------------------------------