diff --git a/.drone.yml b/.drone.yml index 76b7e8d4..a934435c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -18,7 +18,6 @@ steps: - apk add --update alpine-sdk - make build - make fmt - - make lint - make test when: event: diff --git a/.gitignore b/.gitignore index 080339e4..f64d20dc 100644 --- a/.gitignore +++ b/.gitignore @@ -76,4 +76,6 @@ tags .history ### Code coverage coverage.txt -# End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode +# intelliJ +.idea +# End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36a6e6d1..96b86323 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ Please note we have a code of conduct, please follow it in all your interactions 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. 2. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and table of contents. 3. Ensure you have implemented proper unit tests. -4. Enhance the [e2e test suite](./test/e2e), [run them](./README.md#e2e-tests) and ensure they pass. +4. Enhance the [e2e test suite](./test/e2e), [run them](./docs/CONTRIBUTING.md#e2e-tests) and ensure they pass. ## Code of Conduct diff --git a/Makefile b/Makefile index b920421e..246756ff 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,8 @@ BUILD_DATE=$(shell date +%FT%T%z) CRDS=$(shell echo deploy/crds/*_crd.yaml | sed 's/ / -f /g') GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor) GO := GOARCH=amd64 CGO_ENABLED=0 GOOS=linux go +TEST_DIR?=./pkg/controller/... +VERBOSE?= ORGANIZATION_ID?=5c4a2a55553855344780cf5f @@ -36,7 +38,7 @@ api: operator-sdk add api --api-version=knappek.com/$(API_VERSION) --kind=$(KIND) controller: - operator-sdk add controller --api-version=knappek.com/$(API_VERSION) --kind=$(KIND) + ./code-generation/controller-gen.sh --api-version v1alpha1 -k $(KIND) && gofmt -w $(GOFMT_FILES) .PHONY: build build: @@ -77,9 +79,9 @@ cleanup: .PHONY: test test: - go test ./pkg/controller/... -v -coverprofile=coverage.out -covermode=atomic + go test $(TEST_DIR) $(VERBOSE) -coverprofile=coverage.out -covermode=atomic -e2etest: cleanup fmt lint +e2etest: cleanup fmt @if [ "$(ATLAS_PRIVATE_KEY)" = "" ]; then \ echo "ERROR: Export ATLAS_PRIVATE_KEY variable and then run init again. For example:"; \ echo " export ATLAS_PRIVATE_KEY=xxxx-xxxx-xxxx-xxxx"; \ diff --git a/README.md b/README.md index 754891fc..804e1593 100644 --- a/README.md +++ b/README.md @@ -17,20 +17,12 @@ This project was inspired from the [MongoDB Atlas Terraform Provider](https://gi * [Scope](#scope) * [Prerequisites](#prerequisites) * [Getting Started](#getting-started) - * [Init](#init) + * [Deploy Operator](#deploy-operator) * [Create a MongoDB Atlas Project](#create-a-mongodb-atlas-project) * [Create a Cluster](#create-a-cluster) * [List all MongoDB Atlas resources](#list-all-mongodb-atlas-resources) * [Cleanup](#cleanup) -* [Developers Build Guide](#developers-build-guide) -* [Testing](#testing) - * [Unit Tests](#unit-tests) - * [E2E Tests](#e2e-tests) * [Contributing](#contributing) - * [Create new API](#create-new-api) - * [Create new Controller for the API](#create-new-controller-for-the-api) - * [Create CRDs](#create-crds) - * [Create a new Release](#create-a-new-release) @@ -40,6 +32,7 @@ This project was inspired from the [MongoDB Atlas Terraform Provider](https://gi * Create/Delete MongoDB Atlas Projects * Create/Update/Delete MongoDB Atlas Clusters +* Create/Update/Delete MongoDB Atlas Database Users ## Prerequisites @@ -51,7 +44,7 @@ This project was inspired from the [MongoDB Atlas Terraform Provider](https://gi This example creates a MongoDB Atlas project and a cluster inside this project. -### Init +### Deploy Operator First, create the MongoDB Atlas project CRD and some RBAC: @@ -111,68 +104,19 @@ kubectl delete -f deploy/ kubectl delete -f deploy/crds/ ``` -## Developers Build Guide +## Environment Variables -Connect to a Kubernetes cluster +You can specify the following environment variables in the Operator's [operator.yaml](./deploy/operator.yaml): -```shell -export KUBECONFIG=/path/to/config -``` - -**Create all CRDs that are managed by the operator** - -Run this once: - -```shell -make init -``` - -**Run Operator locally** - -```shell -export ATLAS_PRIVATE_KEY=xxxx-xxxx-xxxx-xxxx -export ATLAS_PUBLIC_KEY=yyyyy -make dev -``` - -**Create MongoDB Atlas Project** - -```shell -make deploy-project -``` - -**Create MongoDB Atlas Cluster** - -```shell -make deploy-cluster -``` - -**Delete MongoDB Atlas Project and Cluster** - -```shell -make delete-cluster -make delete-project -``` - -## Testing - -### Unit Tests - -The following executes unit tests for the controllers in `./pkg/controller/` - -```shell -make test -``` -### E2E Tests - -Export the Programmatic API key pair and run the end-to-end tests with - -```shell -export ATLAS_PRIVATE_KEY=xxxx-xxxx-xxxx-xxxx -export ATLAS_PUBLIC_KEY=yyyyy -make e2etest ORGANIZATION_ID=123456789 -``` +| Name | Description | Default | Required | +|------|-------------|---------|----------| +| WATCH_NAMESPACE | The namespace which the operator should watch for MongoDBAtlas CRDs. | `metadata.namespace` | yes | +| POD_NAME | Operator pod name. | `metadata.name` | no | +| OPERATOR_NAME | Operator name. | n/a | no | +| ATLAS_PRIVATE_KEY | The private key of the Atlas API. | n/a | yes | +| ATLAS_PUBLIC_KEY | The private key of the Atlas API. | n/a | yes | +| RECONCILIATION_TIME | Time in seconds which should be used to periodically reconcile the actual status in MongoDB Atlas with the current status in the corresponding Kubernetes CRD. | `"120"` | no | ## Contributing @@ -180,45 +124,4 @@ I am working on this project in my spare time, hence feature development and rel Read through the [Contributing Guidelines and Code of Conduct](./CONTRIBUTING.md). -### Create new API - -This example creates a new MongoDBAtlasCluster API: - -```shell -make api KIND=MongoDBAtlasCluster -``` - -### Create new Controller for the API - -To create a controller for the recently created API, run: - -```shell -make controller KIND=MongoDBAtlasCluster -``` - -### Create CRDs - -```shell -make generate-openapi -``` - -### Create a new Release - -> You need to have Collaborator permissions to perform this step - -A new release will - -* create a new release on the Github [release page](https://github.com/Knappek/mongodbatlas-operator/releases) -* push a new tagged Docker image to [Dockerhub](https://cloud.docker.com/repository/docker/knappek/mongodbatlas-operator/tags) - -In order to do this, follow these steps: - -1. Change the version in [.drone.yml](./.drone.yml) and in [operator.yaml](./deploy/operator.yaml) according to [Semantic Versioning](http://semver.org/) -2. Commit your changes (don't push) -3. Create a new release using [SemVer](http://semver.org/) - - ```shell - make release VERSION= - ``` - -This will kick the CI pipeline and create a new Github Release with the version tag `v`. +More information how to contribute/develop can be found in the [docs](./docs/CONTRIBUTING.md). diff --git a/code-generation/controller-gen.sh b/code-generation/controller-gen.sh new file mode 100755 index 00000000..7096d7f3 --- /dev/null +++ b/code-generation/controller-gen.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +SCRIPT_DIR="$(cd "$(dirname "$0")" ; pwd -P)" +show_help() { + echo "Usage: " + echo + echo " ./`basename "$0"` --api-version v1alpha1 --kind KIND" +} +POSITIONAL=() +while [[ $# -gt 0 ]]; do + key="$1" + case $key in + --api-version) + API_VERSION="$2" + shift + shift + ;; + -k|--kind) + KIND="$2" + shift + shift + ;; + *) + show_help + exit 1 + ;; + esac +done + +check_input_vars() { + if [[ ${API_VERSION} == "" ]] || [[ ${KIND} == "" ]]; then + show_help + exit 1 + fi +} + +check_if_already_substitued() { + file=$1 + replace=y + # handle add_kind.gp.tmpl + if [[ -f ${file} ]]; then + read -p "[warn] file ${file} already exists. Do you want to replace it? (Y/n) " replace + else + replace=y + fi + case $replace in + [Yy]* ) echo "y";; + [Nn]* ) echo "n";; + * ) echo "n";; + esac +} + +substitute_values() { + input="./templates/${1}.go.tmpl" + while IFS= read -r line + do + sed 's/_KIND_LOWERCASE_/'${KIND_LOWERCASE}'/g' $line > kind_lower_replaced_line.tmp + sed 's/_KIND_SHORT_/'${KIND_SHORT}'/g' kind_lower_replaced_line.tmp > kind_short_replaced_line.tmp + sed 's/_KIND_/'${KIND}'/g' kind_short_replaced_line.tmp > kind_replaced_line.tmp + sed 's/_API_VERSION_/'${API_VERSION}'/g' kind_replaced_line.tmp > substitued_values.go + done < "$input" + rm kind_lower_replaced_line.tmp kind_replaced_line.tmp kind_short_replaced_line.tmp +} + + + +pushd "${SCRIPT_DIR}/../" > /dev/null +check_input_vars +KIND_LOWERCASE=$(echo $KIND | tr '[:upper:]' '[:lower:]') +KIND_SHORT=$(echo $KIND | sed 's/MongoDBAtlas//g') + +# handle add_kind.go.tmpl +return_add_kind=$(check_if_already_substitued pkg/controller/add_${KIND_LOWERCASE}.go) +if [[ ${return_add_kind} == "y" ]];then + substitute_values add_kind + mv substitued_values.go pkg/controller/add_${KIND_LOWERCASE}.go +fi +# handle kind_controller.go.tmpl +return_kind_controller=$(check_if_already_substitued pkg/controller/${KIND_LOWERCASE}/${KIND_LOWERCASE}_controller.go) +if [[ ${return_kind_controller} == "y" ]];then + substitute_values kind_controller + mv substitued_values.go pkg/controller/${KIND_LOWERCASE}/${KIND_LOWERCASE}_controller.go +fi +# handle kind_controller_test.go.tmpl +return_kind_controller_test=$(check_if_already_substitued pkg/controller/${KIND_LOWERCASE}/${KIND_LOWERCASE}_controller_test.go) +if [[ ${return_kind_controller_test} == "y" ]];then + substitute_values kind_controller_test + mv substitued_values.go pkg/controller/${KIND_LOWERCASE}/${KIND_LOWERCASE}_controller_test.go +fi +# handle e2e/kind_test.go.tmpl +return_kind_controller_test=$(check_if_already_substitued test/e2e/${KIND_LOWERCASE}_test.go) +if [[ ${return_kind_controller_test} == "y" ]];then + substitute_values e2e/kind_test + mv substitued_values.go test/e2e/${KIND_LOWERCASE}_test.go +fi +popd > /dev/null diff --git a/deploy/crds/knappek_v1alpha1_mongodbatlascluster_crd.yaml b/deploy/crds/knappek_v1alpha1_mongodbatlascluster_crd.yaml index 212d7c35..21c7f21f 100644 --- a/deploy/crds/knappek_v1alpha1_mongodbatlascluster_crd.yaml +++ b/deploy/crds/knappek_v1alpha1_mongodbatlascluster_crd.yaml @@ -215,6 +215,8 @@ spec: - readOnlyNodes - analyticsNodes type: object + description: 'TODO: ReplicationSpec is deprecated, update to ReplicationSpecs. + This needs to be done in the Go clinet library first: https://github.com/akshaykarle/go-mongodbatlas' type: object srvAddress: type: string diff --git a/deploy/crds/knappek_v1alpha1_mongodbatlasdatabaseuser_cr.yaml b/deploy/crds/knappek_v1alpha1_mongodbatlasdatabaseuser_cr.yaml new file mode 100644 index 00000000..99ea9cd9 --- /dev/null +++ b/deploy/crds/knappek_v1alpha1_mongodbatlasdatabaseuser_cr.yaml @@ -0,0 +1,12 @@ +apiVersion: knappek.com/v1alpha1 +kind: MongoDBAtlasDatabaseUser +metadata: + name: example-mongodbatlasdatabaseuser +spec: + projectName: "example-project" + password: "$up€rsecurep@s$word" + roles: + - databaseName: "testdb" + collectionName: "testcollection" + roleName: "readWrite" + diff --git a/deploy/crds/knappek_v1alpha1_mongodbatlasdatabaseuser_crd.yaml b/deploy/crds/knappek_v1alpha1_mongodbatlasdatabaseuser_crd.yaml new file mode 100644 index 00000000..5309ebea --- /dev/null +++ b/deploy/crds/knappek_v1alpha1_mongodbatlasdatabaseuser_crd.yaml @@ -0,0 +1,90 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: mongodbatlasdatabaseusers.knappek.com +spec: + additionalPrinterColumns: + - JSONPath: .spec.projectName + description: The MongoDB Atlas Project to which the database user has access to + name: Project Name + type: string + group: knappek.com + names: + categories: + - all + - mongodbatlas + kind: MongoDBAtlasDatabaseUser + listKind: MongoDBAtlasDatabaseUserList + plural: mongodbatlasdatabaseusers + shortNames: + - madbuser + singular: mongodbatlasdatabaseuser + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + databaseName: + type: string + deleteAfterDate: + type: string + password: + type: string + projectName: + type: string + roles: + items: + properties: + collectionName: + type: string + databaseName: + type: string + roleName: + type: string + type: object + type: array + required: + - projectName + type: object + status: + properties: + databaseName: + type: string + deleteAfterDate: + type: string + groupID: + type: string + roles: + items: + properties: + collectionName: + type: string + databaseName: + type: string + roleName: + type: string + type: object + type: array + username: + type: string + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true diff --git a/deploy/crds/knappek_v1alpha1_mongodbatlasproject_crd.yaml b/deploy/crds/knappek_v1alpha1_mongodbatlasproject_crd.yaml index 036053fb..fc6ea360 100644 --- a/deploy/crds/knappek_v1alpha1_mongodbatlasproject_crd.yaml +++ b/deploy/crds/knappek_v1alpha1_mongodbatlasproject_crd.yaml @@ -4,9 +4,9 @@ metadata: name: mongodbatlasprojects.knappek.com spec: additionalPrinterColumns: - - JSONPath: .spec.orgID - description: The MongoDB Atlas Organization ID - name: OrgID + - JSONPath: .status.id + description: The ID of the Project + name: GroupID type: string - JSONPath: .status.clusterCount description: The number of Clusters in the Project @@ -15,9 +15,9 @@ spec: - JSONPath: .metadata.creationTimestamp name: Age type: date - - JSONPath: .status.id - description: The ID of the Project - name: ProjectID + - JSONPath: .spec.orgID + description: The MongoDB Atlas Organization ID + name: OrgID priority: 1 type: string group: knappek.com diff --git a/deploy/operator-latest.yaml b/deploy/operator-latest.yaml index 873b17fe..a83d6a5e 100644 --- a/deploy/operator-latest.yaml +++ b/deploy/operator-latest.yaml @@ -38,3 +38,5 @@ spec: name: example-monogdb-atlas-project - name: ATLAS_PUBLIC_KEY value: toppaljd + - name: RECONCILIATION_TIME + value: "120" diff --git a/deploy/operator.yaml b/deploy/operator.yaml index d2d46490..220cd37a 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -38,3 +38,5 @@ spec: name: example-monogdb-atlas-project - name: ATLAS_PUBLIC_KEY value: toppaljd + - name: RECONCILIATION_TIME + value: "120" diff --git a/deploy/role.yaml b/deploy/role.yaml index ea15c8b5..64d99e76 100644 --- a/deploy/role.yaml +++ b/deploy/role.yaml @@ -51,5 +51,6 @@ rules: resources: - '*' - mongodbatlasclusters + - mongodbatlasdatabaseusers verbs: - '*' diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..a0211b3e --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,132 @@ +# Contributing + + + + +* [Develop Locally](#develop-locally) +* [Testing](#testing) + * [Unit Tests](#unit-tests) + * [E2E Tests](#e2e-tests) +* [Create new API](#create-new-api) +* [Create new Controller for the API](#create-new-controller-for-the-api) +* [Create CRDs](#create-crds) +* [Create a new Release](#create-a-new-release) + + + +## Develop Locally + +Connect to a Kubernetes cluster + +```shell +export KUBECONFIG=/path/to/config +``` + +Create all CRDs that are managed by the operator: + +```shell +make init +``` + +Run Operator locally: + +```shell +export ATLAS_PRIVATE_KEY=xxxx-xxxx-xxxx-xxxx +export ATLAS_PUBLIC_KEY=yyyyy +make +``` + +Create MongoDB Atlas Project + +```shell +make deploy-project +``` + +Create MongoDB Atlas Cluster + +```shell +make deploy-cluster +``` + +Delete MongoDB Atlas Project and Cluster + +```shell +make delete-cluster +make delete-project +``` + +## Testing + +### Unit Tests + +The following executes unit tests for the controllers in `./pkg/controller/` + +```shell +make test +# test only a subset +make test TEST_DIR=./pkg/controller/mongodbatlasdatabaseuser/... +# increase verbosity +make test TEST_DIR=./pkg/controller/mongodbatlasdatabaseuser/... VERBOSE="-v" +``` + +### E2E Tests + +In order to run the end-to-end tests, you first have to create a namespace and a secret containing the private key of the programmatic API key pair which is needed by the Operator to perform API call against the MongoDB Atlas API. + +The following command will execute the corresponding `kubectl` commands for you + +```shell +export ATLAS_PRIVATE_KEY=xxxx-xxxx-xxxx-xxxx +make inite2etest +``` + +Afterwards, you can run the end-to-end tests with + +```shell +export ATLAS_PUBLIC_KEY=yyyyy +make e2etest ORGANIZATION_ID=123456789 +``` + +## Create new API + +This example creates a new MongoDBAtlasCluster API: + +```shell +make api KIND=MongoDBAtlasCluster +``` + +## Create new Controller for the API + +To create a controller for the recently created API, run: + +```shell +make controller KIND=MongoDBAtlasCluster +``` + +## Create CRDs + +```shell +make generate-openapi +``` + +## Create a new Release + +> You need to have Collaborator permissions to perform this step + +A new release will + +* create a new release on the Github [release page](https://github.com/Knappek/mongodbatlas-operator/releases) +* push a new tagged Docker image to [Dockerhub](https://cloud.docker.com/repository/docker/knappek/mongodbatlas-operator/tags) + +In order to do this, follow these steps: + +1. Change the version in [.drone.yml](./.drone.yml) and in [operator.yaml](./deploy/operator.yaml) according to [Semantic Versioning](http://semver.org/) +2. Commit your changes (don't push) +3. Create a new release using [SemVer](http://semver.org/) + + ```shell + make release VERSION= + ``` + +This will kick the CI pipeline and create a new Github Release with the version tag `v`. + diff --git a/pkg/apis/knappek/group.go b/pkg/apis/knappek/group.go new file mode 100644 index 00000000..9e9f381c --- /dev/null +++ b/pkg/apis/knappek/group.go @@ -0,0 +1,6 @@ +// Package knappek contains knappek API versions. +// +// This file ensures Go source parsers acknowledge the knappek package +// and any child packages. It can be removed if any other Go source files are +// added to this package. +package knappek diff --git a/pkg/apis/knappek/v1alpha1/mongodbatlascluster_types.go b/pkg/apis/knappek/v1alpha1/mongodbatlascluster_types.go index 457e477a..9b8585b9 100644 --- a/pkg/apis/knappek/v1alpha1/mongodbatlascluster_types.go +++ b/pkg/apis/knappek/v1alpha1/mongodbatlascluster_types.go @@ -1,6 +1,7 @@ package v1alpha1 import ( + "github.com/Knappek/mongodbatlas-operator/pkg/util" ma "github.com/akshaykarle/go-mongodbatlas/mongodbatlas" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -61,102 +62,83 @@ type MongoDBAtlasClusterRequestBody struct { func IsMongoDBAtlasClusterToBeUpdated(m1 MongoDBAtlasClusterRequestBody, m2 MongoDBAtlasClusterRequestBody) bool { region := m1.ProviderSettings.RegionName if m1.MongoDBMajorVersion != m2.MongoDBMajorVersion { - if !isZeroValue(m1.MongoDBMajorVersion) { + if !util.IsZeroValue(m1.MongoDBMajorVersion) { return true } } if m1.DiskSizeGB != m2.DiskSizeGB { - if !isZeroValue(m1.DiskSizeGB) { + if !util.IsZeroValue(m1.DiskSizeGB) { return true } } if m1.BackupEnabled != m2.BackupEnabled { - if !isZeroValue(m1.BackupEnabled) { + if !util.IsZeroValue(m1.BackupEnabled) { return true } } if m1.ReplicationSpec[region].Priority != m2.ReplicationSpec[region].Priority { - if !isZeroValue(m1.ReplicationSpec[region].Priority) { + if !util.IsZeroValue(m1.ReplicationSpec[region].Priority) { return true } } if m1.ReplicationSpec[region].ElectableNodes != m2.ReplicationSpec[region].ElectableNodes { - if !isZeroValue(m1.ReplicationSpec[region].ElectableNodes) { + if !util.IsZeroValue(m1.ReplicationSpec[region].ElectableNodes) { return true } } if m1.ReplicationSpec[region].ReadOnlyNodes != m2.ReplicationSpec[region].ReadOnlyNodes { - if !isZeroValue(m1.ReplicationSpec[region].ReadOnlyNodes) { + if !util.IsZeroValue(m1.ReplicationSpec[region].ReadOnlyNodes) { return true } } if m1.ReplicationSpec[region].AnalyticsNodes != m2.ReplicationSpec[region].AnalyticsNodes { - if !isZeroValue(m1.ReplicationSpec[region].AnalyticsNodes) { + if !util.IsZeroValue(m1.ReplicationSpec[region].AnalyticsNodes) { return true } } if m1.NumShards != m2.NumShards { - if !isZeroValue(m1.NumShards) { + if !util.IsZeroValue(m1.NumShards) { return true } } if m1.AutoScaling.DiskGBEnabled != m2.AutoScaling.DiskGBEnabled { - if !isZeroValue(m1.AutoScaling.DiskGBEnabled) { + if !util.IsZeroValue(m1.AutoScaling.DiskGBEnabled) { return true } } if m1.ProviderSettings.ProviderName != m2.ProviderSettings.ProviderName { - if !isZeroValue(m1.ProviderSettings.ProviderName) { + if !util.IsZeroValue(m1.ProviderSettings.ProviderName) { return true } } if m1.ProviderSettings.BackingProviderName != m2.ProviderSettings.BackingProviderName { - if !isZeroValue(m1.ProviderSettings.BackingProviderName) { + if !util.IsZeroValue(m1.ProviderSettings.BackingProviderName) { return true } } if m1.ProviderSettings.RegionName != m2.ProviderSettings.RegionName { - if !isZeroValue(m1.ProviderSettings.RegionName) { + if !util.IsZeroValue(m1.ProviderSettings.RegionName) { return true } } if m1.ProviderSettings.InstanceSizeName != m2.ProviderSettings.InstanceSizeName { - if !isZeroValue(m1.ProviderSettings.InstanceSizeName) { + if !util.IsZeroValue(m1.ProviderSettings.InstanceSizeName) { return true } } if m1.ProviderSettings.DiskIOPS != m2.ProviderSettings.DiskIOPS { - if !isZeroValue(m1.ProviderSettings.DiskIOPS) { + if !util.IsZeroValue(m1.ProviderSettings.DiskIOPS) { return true } } if m1.ProviderSettings.EncryptEBSVolume != m2.ProviderSettings.EncryptEBSVolume { - if !isZeroValue(m1.ProviderSettings.EncryptEBSVolume) { + if !util.IsZeroValue(m1.ProviderSettings.EncryptEBSVolume) { return true } } return false } -func isZeroValue(i interface{}) bool { - if i == nil { - return true - } // nil interface - if i == "" { - return true - } // zero value of a string - if i == 0.0 { - return true - } // zero value of a float64 - if i == 0 { - return true - } // zero value of an int - if i == false { - return true - } // zero value of a boolean - return false -} - // MongoDBAtlasClusterSpec defines the desired state of MongoDBAtlasCluster // +k8s:openapi-gen=true type MongoDBAtlasClusterSpec struct { diff --git a/pkg/apis/knappek/v1alpha1/mongodbatlasdatabaseuser_types.go b/pkg/apis/knappek/v1alpha1/mongodbatlasdatabaseuser_types.go new file mode 100644 index 00000000..6b562a0f --- /dev/null +++ b/pkg/apis/knappek/v1alpha1/mongodbatlasdatabaseuser_types.go @@ -0,0 +1,90 @@ +package v1alpha1 + +import ( + "github.com/Knappek/mongodbatlas-operator/pkg/util" + ma "github.com/akshaykarle/go-mongodbatlas/mongodbatlas" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// MongoDBAtlasDatabaseUserRequestBody defines the Request Body Parameters when creating/updating a database user +type MongoDBAtlasDatabaseUserRequestBody struct { + Password string `json:"password,omitempty"` + DeleteAfterDate string `json:"deleteAfterDate,omitempty"` + DatabaseName string `json:"databaseName,omitempty"` + Roles []ma.Role `json:"roles,omitempty"` +} + +// MongoDBAtlasDatabaseUserSpec defines the desired state of MongoDBAtlasDatabaseUser +// +k8s:openapi-gen=true +type MongoDBAtlasDatabaseUserSpec struct { + ProjectName string `json:"projectName,project"` + MongoDBAtlasDatabaseUserRequestBody `json:",inline"` +} + +// MongoDBAtlasDatabaseUserStatus defines the observed state of MongoDBAtlasDatabaseUser +// +k8s:openapi-gen=true +type MongoDBAtlasDatabaseUserStatus struct { + GroupID string `json:"groupID,omitempty"` + Username string `json:"username,omitempty"` + DeleteAfterDate string `json:"deleteAfterDate,omitempty"` + DatabaseName string `json:"databaseName,omitempty"` + Roles []ma.Role `json:"roles,omitempty"` +} + +// IsMongoDBAtlasDatabaseUserToBeUpdated is used to compare spec.MongoDBAtlasDatabaseUserRequestBody with status +func IsMongoDBAtlasDatabaseUserToBeUpdated(m1 MongoDBAtlasDatabaseUserRequestBody, m2 MongoDBAtlasDatabaseUserStatus) bool { + if m1.DeleteAfterDate != m2.DeleteAfterDate { + if !util.IsZeroValue(m1.DeleteAfterDate) { + return true + } + } + for idx, role := range m1.Roles { + if role.DatabaseName != m2.Roles[idx].DatabaseName { + if !util.IsZeroValue(role.DatabaseName) { + return true + } + } + if role.CollectionName != m2.Roles[idx].CollectionName { + if !util.IsZeroValue(role.CollectionName) { + return true + } + } + if role.RoleName != m2.Roles[idx].RoleName { + if !util.IsZeroValue(role.RoleName) { + return true + } + } + } + return false +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// MongoDBAtlasDatabaseUser is the Schema for the mongodbatlasdatabaseusers API +// +k8s:openapi-gen=true +// +kubebuilder:printcolumn:name="Project Name",type="string",JSONPath=".spec.projectName",description="The MongoDB Atlas Project to which the database user has access to" +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=mongodbatlasdatabaseusers,shortName=madbuser +// +kubebuilder:categories=all,mongodbatlas +type MongoDBAtlasDatabaseUser struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MongoDBAtlasDatabaseUserSpec `json:"spec,omitempty"` + Status MongoDBAtlasDatabaseUserStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// MongoDBAtlasDatabaseUserList contains a list of MongoDBAtlasDatabaseUser +type MongoDBAtlasDatabaseUserList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MongoDBAtlasDatabaseUser `json:"items"` +} + +func init() { + SchemeBuilder.Register(&MongoDBAtlasDatabaseUser{}, &MongoDBAtlasDatabaseUserList{}) +} diff --git a/pkg/apis/knappek/v1alpha1/mongodbatlasproject_types.go b/pkg/apis/knappek/v1alpha1/mongodbatlasproject_types.go index ab4d3043..a82e91b2 100644 --- a/pkg/apis/knappek/v1alpha1/mongodbatlasproject_types.go +++ b/pkg/apis/knappek/v1alpha1/mongodbatlasproject_types.go @@ -22,10 +22,10 @@ type MongoDBAtlasProjectList struct { // MongoDBAtlasProject is the Schema for the mongodbatlasprojects API // +k8s:openapi-gen=true -// +kubebuilder:printcolumn:name="OrgID",type="string",JSONPath=".spec.orgID",description="The MongoDB Atlas Organization ID" +// +kubebuilder:printcolumn:name="GroupID",type="string",JSONPath=".status.id",description="The ID of the Project" // +kubebuilder:printcolumn:name="ClusterCount",type="integer",JSONPath=".status.clusterCount",description="The number of Clusters in the Project" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" -// +kubebuilder:printcolumn:name="ProjectID",type="string",JSONPath=".status.id",description="The ID of the Project",priority="1" +// +kubebuilder:printcolumn:name="OrgID",type="string",JSONPath=".spec.orgID",description="The MongoDB Atlas Organization ID",priority="1" // +kubebuilder:subresource:status // +kubebuilder:resource:path=mongodbatlasprojects,shortName=map // +kubebuilder:categories=all,mongodbatlas diff --git a/pkg/apis/knappek/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/knappek/v1alpha1/zz_generated.deepcopy.go index 7d7c1723..f9f1a3c9 100644 --- a/pkg/apis/knappek/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/knappek/v1alpha1/zz_generated.deepcopy.go @@ -129,6 +129,126 @@ func (in *MongoDBAtlasClusterStatus) DeepCopy() *MongoDBAtlasClusterStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MongoDBAtlasDatabaseUser) DeepCopyInto(out *MongoDBAtlasDatabaseUser) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBAtlasDatabaseUser. +func (in *MongoDBAtlasDatabaseUser) DeepCopy() *MongoDBAtlasDatabaseUser { + if in == nil { + return nil + } + out := new(MongoDBAtlasDatabaseUser) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MongoDBAtlasDatabaseUser) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MongoDBAtlasDatabaseUserList) DeepCopyInto(out *MongoDBAtlasDatabaseUserList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]MongoDBAtlasDatabaseUser, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBAtlasDatabaseUserList. +func (in *MongoDBAtlasDatabaseUserList) DeepCopy() *MongoDBAtlasDatabaseUserList { + if in == nil { + return nil + } + out := new(MongoDBAtlasDatabaseUserList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MongoDBAtlasDatabaseUserList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MongoDBAtlasDatabaseUserRequestBody) DeepCopyInto(out *MongoDBAtlasDatabaseUserRequestBody) { + *out = *in + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]mongodbatlas.Role, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBAtlasDatabaseUserRequestBody. +func (in *MongoDBAtlasDatabaseUserRequestBody) DeepCopy() *MongoDBAtlasDatabaseUserRequestBody { + if in == nil { + return nil + } + out := new(MongoDBAtlasDatabaseUserRequestBody) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MongoDBAtlasDatabaseUserSpec) DeepCopyInto(out *MongoDBAtlasDatabaseUserSpec) { + *out = *in + in.MongoDBAtlasDatabaseUserRequestBody.DeepCopyInto(&out.MongoDBAtlasDatabaseUserRequestBody) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBAtlasDatabaseUserSpec. +func (in *MongoDBAtlasDatabaseUserSpec) DeepCopy() *MongoDBAtlasDatabaseUserSpec { + if in == nil { + return nil + } + out := new(MongoDBAtlasDatabaseUserSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MongoDBAtlasDatabaseUserStatus) DeepCopyInto(out *MongoDBAtlasDatabaseUserStatus) { + *out = *in + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]mongodbatlas.Role, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MongoDBAtlasDatabaseUserStatus. +func (in *MongoDBAtlasDatabaseUserStatus) DeepCopy() *MongoDBAtlasDatabaseUserStatus { + if in == nil { + return nil + } + out := new(MongoDBAtlasDatabaseUserStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MongoDBAtlasProject) DeepCopyInto(out *MongoDBAtlasProject) { *out = *in diff --git a/pkg/apis/knappek/v1alpha1/zz_generated.openapi.go b/pkg/apis/knappek/v1alpha1/zz_generated.openapi.go index db79fcf9..1afa3044 100644 --- a/pkg/apis/knappek/v1alpha1/zz_generated.openapi.go +++ b/pkg/apis/knappek/v1alpha1/zz_generated.openapi.go @@ -1,7 +1,5 @@ // +build !ignore_autogenerated -// Code generated by openapi-gen. DO NOT EDIT. - // This file was autogenerated by openapi-gen. Do not edit it manually! package v1alpha1 @@ -13,12 +11,15 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasCluster": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasCluster(ref), - "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasClusterSpec": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasClusterSpec(ref), - "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasClusterStatus": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasClusterStatus(ref), - "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasProject": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasProject(ref), - "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasProjectSpec": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasProjectSpec(ref), - "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasProjectStatus": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasProjectStatus(ref), + "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasCluster": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasCluster(ref), + "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasClusterSpec": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasClusterSpec(ref), + "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasClusterStatus": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasClusterStatus(ref), + "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasDatabaseUser": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasDatabaseUser(ref), + "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasDatabaseUserSpec": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasDatabaseUserSpec(ref), + "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasDatabaseUserStatus": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasDatabaseUserStatus(ref), + "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasProject": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasProject(ref), + "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasProjectSpec": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasProjectSpec(ref), + "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasProjectStatus": schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasProjectStatus(ref), } } @@ -163,13 +164,60 @@ func schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasClusterStatus(ref common.Refer Format: "", }, }, - "mongoDBVersion": { + "mongoDBMajorVersion": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, Format: "", }, }, - "mongoDBMajorVersion": { + "diskSizeGB": { + SchemaProps: spec.SchemaProps{ + Type: []string{"number"}, + Format: "double", + }, + }, + "backupEnabled": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "providerBackupEnabled": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "replicationSpec": { + SchemaProps: spec.SchemaProps{ + Description: "This needs to be done in the Go clinet library first: https://github.com/akshaykarle/go-mongodbatlas", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/akshaykarle/go-mongodbatlas/mongodbatlas.ReplicationSpec"), + }, + }, + }, + }, + }, + "numShards": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int32", + }, + }, + "autoScaling": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/akshaykarle/go-mongodbatlas/mongodbatlas.AutoScaling"), + }, + }, + "providerSettings": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/akshaykarle/go-mongodbatlas/mongodbatlas.ProviderSettings"), + }, + }, + "mongoDBVersion": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, Format: "", @@ -199,70 +247,168 @@ func schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasClusterStatus(ref common.Refer Format: "", }, }, - "diskSizeGB": { + "stateName": { SchemaProps: spec.SchemaProps{ - Type: []string{"number"}, - Format: "double", + Type: []string{"string"}, + Format: "", }, }, - "backupEnabled": { + "paused": { SchemaProps: spec.SchemaProps{ Type: []string{"boolean"}, Format: "", }, }, - "providerBackupEnabled": { + }, + Required: []string{"backupEnabled", "providerBackupEnabled", "paused"}, + }, + }, + Dependencies: []string{ + "github.com/akshaykarle/go-mongodbatlas/mongodbatlas.AutoScaling", "github.com/akshaykarle/go-mongodbatlas/mongodbatlas.ProviderSettings", "github.com/akshaykarle/go-mongodbatlas/mongodbatlas.ReplicationSpec"}, + } +} + +func schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasDatabaseUser(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "MongoDBAtlasDatabaseUser is the Schema for the mongodbatlasdatabaseusers API", + Properties: map[string]spec.Schema{ + "kind": { SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasDatabaseUserSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasDatabaseUserStatus"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasDatabaseUserSpec", "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1.MongoDBAtlasDatabaseUserStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasDatabaseUserSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "MongoDBAtlasDatabaseUserSpec defines the desired state of MongoDBAtlasDatabaseUser", + Properties: map[string]spec.Schema{ + "projectName": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, Format: "", }, }, - "stateName": { + "password": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, Format: "", }, }, - "replicationSpec": { + "deleteAfterDate": { SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ + Type: []string{"string"}, + Format: "", + }, + }, + "databaseName": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "roles": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/akshaykarle/go-mongodbatlas/mongodbatlas.ReplicationSpec"), + Ref: ref("github.com/akshaykarle/go-mongodbatlas/mongodbatlas.Role"), }, }, }, }, }, - "numShards": { + }, + Required: []string{"projectName"}, + }, + }, + Dependencies: []string{ + "github.com/akshaykarle/go-mongodbatlas/mongodbatlas.Role"}, + } +} + +func schema_pkg_apis_knappek_v1alpha1_MongoDBAtlasDatabaseUserStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "MongoDBAtlasDatabaseUserStatus defines the observed state of MongoDBAtlasDatabaseUser", + Properties: map[string]spec.Schema{ + "groupID": { SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int32", + Type: []string{"string"}, + Format: "", }, }, - "paused": { + "username": { SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, + Type: []string{"string"}, Format: "", }, }, - "autoScaling": { + "deleteAfterDate": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/akshaykarle/go-mongodbatlas/mongodbatlas.AutoScaling"), + Type: []string{"string"}, + Format: "", }, }, - "providerSettings": { + "databaseName": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/akshaykarle/go-mongodbatlas/mongodbatlas.ProviderSettings"), + Type: []string{"string"}, + Format: "", + }, + }, + "roles": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/akshaykarle/go-mongodbatlas/mongodbatlas.Role"), + }, + }, + }, }, }, }, - Required: []string{"backupEnabled", "providerBackupEnabled", "paused"}, }, }, Dependencies: []string{ - "github.com/akshaykarle/go-mongodbatlas/mongodbatlas.AutoScaling", "github.com/akshaykarle/go-mongodbatlas/mongodbatlas.ProviderSettings", "github.com/akshaykarle/go-mongodbatlas/mongodbatlas.ReplicationSpec"}, + "github.com/akshaykarle/go-mongodbatlas/mongodbatlas.Role"}, } } diff --git a/pkg/config/config.go b/pkg/config/config.go index 6658d186..fbd73b72 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,6 +3,8 @@ package config import ( "net/http" "os" + "strconv" + "time" dac "github.com/akshaykarle/go-http-digest-auth-client" ma "github.com/akshaykarle/go-mongodbatlas/mongodbatlas" @@ -39,3 +41,27 @@ func GetAtlasClient() *ma.Client { } return atlasConfig.newMongoDBAtlasClient() } + +// ReconciliationConfig let us customize reconcilitation +type ReconciliationConfig struct { + Time time.Duration +} + +// GetReconcilitationConfig gives us default values +func GetReconcilitationConfig() *ReconciliationConfig { + // default reconciliation loop time is 2 minutes + timeString := getenv("RECONCILIATION_TIME", "120") + timeInt, _ := strconv.Atoi(timeString) + reconciliationTime := time.Second * time.Duration(timeInt) + return &ReconciliationConfig{ + Time: reconciliationTime, + } +} + +func getenv(key, fallback string) string { + value := os.Getenv(key) + if len(value) == 0 { + return fallback + } + return value +} diff --git a/pkg/controller/add_mongodbatlasdatabaseuser.go b/pkg/controller/add_mongodbatlasdatabaseuser.go new file mode 100644 index 00000000..7e965299 --- /dev/null +++ b/pkg/controller/add_mongodbatlasdatabaseuser.go @@ -0,0 +1,10 @@ +package controller + +import ( + "github.com/Knappek/mongodbatlas-operator/pkg/controller/mongodbatlasdatabaseuser" +) + +func init() { + // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. + AddToManagerFuncs = append(AddToManagerFuncs, mongodbatlasdatabaseuser.Add) +} diff --git a/pkg/controller/mongodbatlascluster/mongodbatlascluster_controller.go b/pkg/controller/mongodbatlascluster/mongodbatlascluster_controller.go index 8f743c61..721f41ae 100644 --- a/pkg/controller/mongodbatlascluster/mongodbatlascluster_controller.go +++ b/pkg/controller/mongodbatlascluster/mongodbatlascluster_controller.go @@ -39,7 +39,12 @@ func Add(mgr manager.Manager) error { // newReconciler returns a new reconcile.Reconciler func newReconciler(mgr manager.Manager) reconcile.Reconciler { - return &ReconcileMongoDBAtlasCluster{client: mgr.GetClient(), scheme: mgr.GetScheme(), atlasClient: config.GetAtlasClient()} + return &ReconcileMongoDBAtlasCluster{ + client: mgr.GetClient(), + scheme: mgr.GetScheme(), + atlasClient: config.GetAtlasClient(), + reconciliationConfig: config.GetReconcilitationConfig(), + } } // add adds a new Controller to mgr with r as the reconcile.Reconciler @@ -65,9 +70,10 @@ var _ reconcile.Reconciler = &ReconcileMongoDBAtlasCluster{} type ReconcileMongoDBAtlasCluster struct { // This client, initialized using mgr.Client() above, is a split client // that reads objects from the cache and writes to the apiserver - client client.Client - scheme *runtime.Scheme - atlasClient *ma.Client + client client.Client + scheme *runtime.Scheme + atlasClient *ma.Client + reconciliationConfig *config.ReconciliationConfig } // Reconcile reads that state of the cluster for a MongoDBAtlasCluster object and makes changes based on the state read @@ -122,7 +128,7 @@ func (r *ReconcileMongoDBAtlasCluster) Reconcile(request reconcile.Request) (rec if err != nil { return reconcile.Result{}, err } - reqLogger.Info("Wait until Cluster has been deleted.", "MongoDBAtlasCluster.GroupID", groupID) + reqLogger.Info("Wait until Cluster has been deleted.") // Requeue after 20 seconds and check again for the status until CR can be deleted return reconcile.Result{RequeueAfter: time.Second * 20}, nil } @@ -131,7 +137,7 @@ func (r *ReconcileMongoDBAtlasCluster) Reconcile(request reconcile.Request) (rec _, resp, err := r.atlasClient.Clusters.Get(groupID, atlasCluster.Name) if err != nil { if resp.StatusCode == 404 { - reqLogger.Info("Cluster deleted.", "MongoDBAtlasCluster.GroupID", groupID) + reqLogger.Info("Cluster deleted.") // Update finalizer to allow delete CR atlasCluster.SetFinalizers(nil) // Update CR @@ -166,8 +172,8 @@ func (r *ReconcileMongoDBAtlasCluster) Reconcile(request reconcile.Request) (rec if err := r.addFinalizer(reqLogger, atlasCluster); err != nil { return reconcile.Result{}, err } - // Requeue after 30 seconds and check again for the status until CR can be deleted - return reconcile.Result{RequeueAfter: time.Second * 30}, nil + // Requeue to periodically reconcile the CR MongoDBAtlasCluster in order to recreate a manually deleted Atlas cluster + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil } } @@ -185,8 +191,8 @@ func (r *ReconcileMongoDBAtlasCluster) Reconcile(request reconcile.Request) (rec if err != nil { return reconcile.Result{}, err } - // Requeue after 30 seconds and check again for the status until CR can be deleted - return reconcile.Result{RequeueAfter: time.Second * 30}, nil + // Requeue to periodically reconcile the CR MongoDBAtlasCluster in order to recreate a manually deleted Atlas cluster + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil } } @@ -206,7 +212,7 @@ func (r *ReconcileMongoDBAtlasCluster) Reconcile(request reconcile.Request) (rec } // Requeue to periodically reconcile the CR MongoDBAtlasCluster in order to recreate a manually deleted Atlas cluster - return reconcile.Result{RequeueAfter: time.Second * 30}, nil + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil } func createMongoDBAtlasCluster(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappekv1alpha1.MongoDBAtlasCluster, ap *knappekv1alpha1.MongoDBAtlasProject) error { @@ -217,19 +223,18 @@ func createMongoDBAtlasCluster(reqLogger logr.Logger, atlasClient *ma.Client, cr if err != nil { return fmt.Errorf("Error creating Cluster %v: %s", cr.Name, err) } - reqLogger.Info("Sent request to create Cluster.", "MongoDBAtlasCluster.GroupID", groupID) + reqLogger.Info("Sent request to create Cluster.") return updateCRStatus(reqLogger, cr, c) } func updateMongoDBAtlasCluster(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappekv1alpha1.MongoDBAtlasCluster, ap *knappekv1alpha1.MongoDBAtlasProject) error { groupID := ap.Status.ID params := getClusterParams(cr) - c, _, err := atlasClient.Clusters.Update(groupID, cr.Name, ¶ms) if err != nil { return fmt.Errorf("Error updating Cluster %v: %s", cr.Name, err) } - reqLogger.Info("Sent request to update Cluster.", "MongoDBAtlasCluster.GroupID", groupID) + reqLogger.Info("Sent request to update Cluster.") return updateCRStatus(reqLogger, cr, c) } @@ -240,13 +245,13 @@ func deleteMongoDBAtlasCluster(reqLogger logr.Logger, atlasClient *ma.Client, cr resp, err := atlasClient.Clusters.Delete(groupID, clusterName) if err != nil { if resp.StatusCode == http.StatusNotFound { - reqLogger.Info("Cluster does not exist in Atlas. Deleting CR.", "MongoDBAtlasCluster.GroupID", groupID) + reqLogger.Info("Cluster does not exist in Atlas. Deleting CR.") // CR can be deleted - Requeue return nil } return fmt.Errorf("(%v) Error deleting Cluster %s: %s", resp.StatusCode, clusterName, err) } - reqLogger.Info("Sent request to delete Cluster.", "MongoDBAtlasCluster.GroupID", groupID) + reqLogger.Info("Sent request to delete Cluster.") return nil } @@ -290,10 +295,10 @@ func updateCRStatus(reqLogger logr.Logger, cr *knappekv1alpha1.MongoDBAtlasClust newStateName := cr.Status.StateName if oldStateName != newStateName { if oldStateName == "CREATING" && newStateName == "IDLE" { - reqLogger.Info("Cluster created.", "MongoDBAtlasCluster.GroupID", cr.Status.ID) + reqLogger.Info("Cluster created.") } if oldStateName == "UPDATING" && newStateName == "IDLE" { - reqLogger.Info("Cluster updated successfully.", "MongoDBAtlasCluster.GroupID", cr.Status.ID) + reqLogger.Info("Cluster updated successfully.") } } return nil diff --git a/pkg/controller/mongodbatlascluster/mongodbatlascluster_controller_test.go b/pkg/controller/mongodbatlascluster/mongodbatlascluster_controller_test.go index 11766fc2..df51196b 100644 --- a/pkg/controller/mongodbatlascluster/mongodbatlascluster_controller_test.go +++ b/pkg/controller/mongodbatlascluster/mongodbatlascluster_controller_test.go @@ -13,6 +13,7 @@ import ( ma "github.com/akshaykarle/go-mongodbatlas/mongodbatlas" knappekv1alpha1 "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1" + "github.com/Knappek/mongodbatlas-operator/pkg/config" testutil "github.com/Knappek/mongodbatlas-operator/pkg/controller/test" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,7 +29,7 @@ var ( namespace = "mongodbatlas" organizationID = "testOrgID" projectName = "unittest-project" - projectID = "5a0a1e7e0f2912c554080ae6" + groupID = "5a0a1e7e0f2912c554080ae6" clusterName = "unittest-cluster" clusterID = "testClusterId" mongoDBVersion = "3.4" @@ -63,22 +64,7 @@ func TestCreateMongoDBAtlasCluster(t *testing.T) { logf.SetLogger(logf.ZapLogger(true)) // A MongoDBAtlasProject resource with metadata and spec. - mongodbatlasproject := &knappekv1alpha1.MongoDBAtlasProject{ - ObjectMeta: metav1.ObjectMeta{ - Name: projectName, - Namespace: namespace, - }, - Spec: knappekv1alpha1.MongoDBAtlasProjectSpec{ - OrgID: organizationID, - }, - Status: knappekv1alpha1.MongoDBAtlasProjectStatus{ - ID: projectID, - Name: projectName, - OrgID: organizationID, - Created: "2016-07-14T14:19:33Z", - ClusterCount: 0, - }, - } + mongodbatlasproject := testutil.CreateAtlasProject(projectName, groupID, namespace, organizationID) // A MongoDBAtlasCluster resource with metadata and spec. mongodbatlascluster := &knappekv1alpha1.MongoDBAtlasCluster{ @@ -120,43 +106,16 @@ func TestCreateMongoDBAtlasCluster(t *testing.T) { atlasClient := ma.NewClient(httpClient) // Post - mux.HandleFunc("/api/atlas/v1.0/groups/"+projectID+"/clusters", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/atlas/v1.0/groups/"+groupID+"/clusters", func(w http.ResponseWriter, r *http.Request) { testutil.AssertMethod(t, "POST", r) w.Header().Set("Content-Type", "application/json") - expectedBody := map[string]interface{}{ - "autoScaling": map[string]interface{}{ - "diskGBEnabled": autoscaling.DiskGBEnabled, - }, - "backupEnabled": backupEnabled, - "diskSizeGB": diskSizeGB, - "name": clusterName, - "mongoDBMajorVersion": mongoDBMajorVersion, - "numShards": float64(numShards), - "paused": paused, - "providerBackupEnabled": providerBackupEnabled, - "providerSettings": map[string]interface{}{ - "providerName": providerSettings.ProviderName, - "regionName": providerSettings.RegionName, - "instanceSizeName": providerSettings.InstanceSizeName, - "encryptEBSVolume": providerSettings.EncryptEBSVolume, - }, - "replicationSpec": map[string]interface{}{ - "US_EAST_1": map[string]interface{}{ - "priority": float64(7), - "electableNodes": float64(2), - "readOnlyNodes": float64(1), - "analyticsNodes": float64(1), - }, - }, - } - testutil.AssertReqJSON(t, expectedBody, r) fmt.Fprintf(w, `{ "autoScaling":{ "diskGBEnabled":`+strconv.FormatBool(autoscaling.DiskGBEnabled)+` }, "backupEnabled":`+strconv.FormatBool(backupEnabled)+`, "diskSizeGB":`+strconv.FormatFloat(diskSizeGB, 'f', 6, 64)+`, - "groupId": "`+projectID+`", + "groupId": "`+groupID+`", "id": "`+clusterID+`", "mongoDBVersion":"`+mongoDBVersion+`", "mongoDBMajorVersion":"`+mongoDBMajorVersion+`", @@ -183,7 +142,12 @@ func TestCreateMongoDBAtlasCluster(t *testing.T) { }) // Create a ReconcileMongoDBAtlasCluster object with the scheme and fake client. - r := &ReconcileMongoDBAtlasCluster{client: k8sClient, scheme: s, atlasClient: atlasClient} + r := &ReconcileMongoDBAtlasCluster{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } // Mock request to simulate Reconcile() being called on an event for a // watched resource . @@ -197,7 +161,7 @@ func TestCreateMongoDBAtlasCluster(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - assert.Equal(t, time.Second*30, res.RequeueAfter) + assert.Equal(t, time.Second*120, res.RequeueAfter) // Check if the CR has been created and has the correct status. cr := &knappekv1alpha1.MongoDBAtlasCluster{} @@ -208,7 +172,7 @@ func TestCreateMongoDBAtlasCluster(t *testing.T) { assert.Equal(t, "CREATING", cr.Status.StateName, "stateName not as expected") // GET: Simulate a new reconcile where stateName changed from CREATING to IDLE - mux.HandleFunc("/api/atlas/v1.0/groups/"+projectID+"/clusters/"+clusterName, func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/atlas/v1.0/groups/"+groupID+"/clusters/"+clusterName, func(w http.ResponseWriter, r *http.Request) { testutil.AssertMethod(t, "GET", r) fmt.Fprintf(w, `{ "autoScaling":{ @@ -216,7 +180,7 @@ func TestCreateMongoDBAtlasCluster(t *testing.T) { }, "backupEnabled":`+strconv.FormatBool(backupEnabled)+`, "diskSizeGB":`+strconv.FormatFloat(diskSizeGB, 'f', 6, 64)+`, - "groupId": "`+projectID+`", + "groupId": "`+groupID+`", "id": "`+clusterID+`", "mongoDBVersion":"`+mongoDBVersion+`", "mongoDBMajorVersion":"`+mongoDBMajorVersion+`", @@ -258,7 +222,7 @@ func TestCreateMongoDBAtlasCluster(t *testing.T) { assert.Equal(t, "finalizer.knappek.com", cr.ObjectMeta.GetFinalizers()[0], "Finalizer not as expected") assert.Equal(t, clusterID, cr.Status.ID, "clusterID not as expected") assert.Equal(t, clusterName, cr.Status.Name, "clusterName not as expected") - assert.Equal(t, projectID, cr.Status.GroupID, "projectID not as expected") + assert.Equal(t, groupID, cr.Status.GroupID, "groupID not as expected") assert.Equal(t, mongoDBVersion, cr.Status.MongoDBVersion, "mongoDBVersion not as expected") assert.Equal(t, mongoDBMajorVersion, cr.Status.MongoDBMajorVersion, "mongoDBMajorVersion not as expected") assert.Equal(t, diskSizeGB, cr.Status.DiskSizeGB, "diskSizeGB not as expected") @@ -277,22 +241,7 @@ func TestDeleteMongoDBAtlasCluster(t *testing.T) { logf.SetLogger(logf.ZapLogger(true)) // A MongoDBAtlasProject resource with metadata and spec. - mongodbatlasproject := &knappekv1alpha1.MongoDBAtlasProject{ - ObjectMeta: metav1.ObjectMeta{ - Name: projectName, - Namespace: namespace, - }, - Spec: knappekv1alpha1.MongoDBAtlasProjectSpec{ - OrgID: organizationID, - }, - Status: knappekv1alpha1.MongoDBAtlasProjectStatus{ - ID: projectID, - Name: projectName, - OrgID: organizationID, - Created: "2016-07-14T14:19:33Z", - ClusterCount: 0, - }, - } + mongodbatlasproject := testutil.CreateAtlasProject(projectName, groupID, namespace, organizationID) // A MongoDBAtlasCluster resource with metadata and spec. mongodbatlascluster := &knappekv1alpha1.MongoDBAtlasCluster{ @@ -316,7 +265,7 @@ func TestDeleteMongoDBAtlasCluster(t *testing.T) { }, }, Status: knappekv1alpha1.MongoDBAtlasClusterStatus{ - GroupID: projectID, + GroupID: groupID, Name: clusterName, StateName: "IDLE", }, @@ -341,13 +290,18 @@ func TestDeleteMongoDBAtlasCluster(t *testing.T) { atlasClient := ma.NewClient(httpClient) // Delete - mux.HandleFunc("/api/atlas/v1.0/groups/"+projectID+"/clusters/"+clusterName, func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/atlas/v1.0/groups/"+groupID+"/clusters/"+clusterName, func(w http.ResponseWriter, r *http.Request) { testutil.AssertMethod(t, "DELETE", r) fmt.Fprintf(w, `{}`) }) // Create a ReconcileMongoDBAtlasCluster object with the scheme and fake client. - r := &ReconcileMongoDBAtlasCluster{client: k8sClient, scheme: s, atlasClient: atlasClient} + r := &ReconcileMongoDBAtlasCluster{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } // Mock request to simulate Reconcile() being called on an event for a // watched resource . @@ -376,13 +330,18 @@ func TestDeleteMongoDBAtlasCluster(t *testing.T) { defer server2.Close() atlasClient2 := ma.NewClient(httpClient2) // GET: Simulate a new reconcile where cluster has been deleted successfully - mux2.HandleFunc("/api/atlas/v1.0/groups/"+projectID+"/clusters/"+clusterName, func(w http.ResponseWriter, r *http.Request) { + mux2.HandleFunc("/api/atlas/v1.0/groups/"+groupID+"/clusters/"+clusterName, func(w http.ResponseWriter, r *http.Request) { testutil.AssertMethod(t, "GET", r) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) }) // Create a ReconcileMongoDBAtlasCluster object with the scheme and fake client. - r2 := &ReconcileMongoDBAtlasCluster{client: k8sClient, scheme: s, atlasClient: atlasClient2} + r2 := &ReconcileMongoDBAtlasCluster{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient2, + reconciliationConfig: config.GetReconcilitationConfig(), + } res2, err := r2.Reconcile(req) if err != nil { @@ -403,22 +362,7 @@ func TestUpdateMongoDBAtlasCluster(t *testing.T) { logf.SetLogger(logf.ZapLogger(true)) // A MongoDBAtlasProject resource with metadata and spec. - mongodbatlasproject := &knappekv1alpha1.MongoDBAtlasProject{ - ObjectMeta: metav1.ObjectMeta{ - Name: projectName, - Namespace: namespace, - }, - Spec: knappekv1alpha1.MongoDBAtlasProjectSpec{ - OrgID: organizationID, - }, - Status: knappekv1alpha1.MongoDBAtlasProjectStatus{ - ID: projectID, - Name: projectName, - OrgID: organizationID, - Created: "2016-07-14T14:19:33Z", - ClusterCount: 1, - }, - } + mongodbatlasproject := testutil.CreateAtlasProject(projectName, groupID, namespace, organizationID) // updates updatedDiskSizeGB := diskSizeGB + 10 @@ -450,7 +394,7 @@ func TestUpdateMongoDBAtlasCluster(t *testing.T) { }, }, Status: knappekv1alpha1.MongoDBAtlasClusterStatus{ - GroupID: projectID, + GroupID: groupID, Name: clusterName, StateName: "IDLE", ID: clusterID, @@ -487,7 +431,7 @@ func TestUpdateMongoDBAtlasCluster(t *testing.T) { defer server.Close() atlasClient := ma.NewClient(httpClient) // Construct Update API call - mux.HandleFunc("/api/atlas/v1.0/groups/"+projectID+"/clusters/"+clusterName, func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/atlas/v1.0/groups/"+groupID+"/clusters/"+clusterName, func(w http.ResponseWriter, r *http.Request) { testutil.AssertMethod(t, "PATCH", r) w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{ @@ -496,7 +440,7 @@ func TestUpdateMongoDBAtlasCluster(t *testing.T) { }, "backupEnabled":`+strconv.FormatBool(!backupEnabled)+`, "diskSizeGB":`+strconv.FormatFloat(updatedDiskSizeGB, 'f', 6, 64)+`, - "groupId": "`+projectID+`", + "groupId": "`+groupID+`", "id": "`+clusterID+`", "mongoDBVersion":"`+mongoDBVersion+`", "mongoDBMajorVersion":"`+mongoDBMajorVersion+`", @@ -522,7 +466,12 @@ func TestUpdateMongoDBAtlasCluster(t *testing.T) { }`) }) // Create a ReconcileMongoDBAtlasCluster object with the scheme and fake client. - r := &ReconcileMongoDBAtlasCluster{client: k8sClient, scheme: s, atlasClient: atlasClient} + r := &ReconcileMongoDBAtlasCluster{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } // Mock request to simulate Reconcile() being called on an event for a // watched resource . @@ -536,7 +485,7 @@ func TestUpdateMongoDBAtlasCluster(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - assert.Equal(t, time.Second*30, res.RequeueAfter) + assert.Equal(t, time.Second*120, res.RequeueAfter) // Check if the CR has been created and has the correct status. cr := &knappekv1alpha1.MongoDBAtlasCluster{} @@ -556,22 +505,7 @@ func TestNoUpdateMongoDBAtlasCluster(t *testing.T) { logf.SetLogger(logf.ZapLogger(true)) // A MongoDBAtlasProject resource with metadata and spec. - mongodbatlasproject := &knappekv1alpha1.MongoDBAtlasProject{ - ObjectMeta: metav1.ObjectMeta{ - Name: projectName, - Namespace: namespace, - }, - Spec: knappekv1alpha1.MongoDBAtlasProjectSpec{ - OrgID: organizationID, - }, - Status: knappekv1alpha1.MongoDBAtlasProjectStatus{ - ID: projectID, - Name: projectName, - OrgID: organizationID, - Created: "2016-07-14T14:19:33Z", - ClusterCount: 1, - }, - } + mongodbatlasproject := testutil.CreateAtlasProject(projectName, groupID, namespace, organizationID) // A MongoDBAtlasCluster resource with metadata and spec. This Spec contains only the bare minimum, other values // will be filled with default values @@ -588,7 +522,7 @@ func TestNoUpdateMongoDBAtlasCluster(t *testing.T) { }, }, Status: knappekv1alpha1.MongoDBAtlasClusterStatus{ - GroupID: projectID, + GroupID: groupID, Name: clusterName, StateName: "IDLE", ID: clusterID, @@ -625,7 +559,7 @@ func TestNoUpdateMongoDBAtlasCluster(t *testing.T) { defer server.Close() atlasClient := ma.NewClient(httpClient) // Construct Update API call - mux.HandleFunc("/api/atlas/v1.0/groups/"+projectID+"/clusters/"+clusterName, func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/atlas/v1.0/groups/"+groupID+"/clusters/"+clusterName, func(w http.ResponseWriter, r *http.Request) { testutil.AssertMethod(t, "GET", r) w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{ @@ -634,7 +568,7 @@ func TestNoUpdateMongoDBAtlasCluster(t *testing.T) { }, "backupEnabled":`+strconv.FormatBool(backupEnabled)+`, "diskSizeGB":`+strconv.FormatFloat(diskSizeGB, 'f', 6, 64)+`, - "groupId": "`+projectID+`", + "groupId": "`+groupID+`", "id": "`+clusterID+`", "mongoDBVersion":"`+mongoDBVersion+`", "mongoDBMajorVersion":"`+mongoDBMajorVersion+`", @@ -660,7 +594,12 @@ func TestNoUpdateMongoDBAtlasCluster(t *testing.T) { }`) }) // Create a ReconcileMongoDBAtlasCluster object with the scheme and fake client. - r := &ReconcileMongoDBAtlasCluster{client: k8sClient, scheme: s, atlasClient: atlasClient} + r := &ReconcileMongoDBAtlasCluster{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } // Mock request to simulate Reconcile() being called on an event for a // watched resource . @@ -674,7 +613,7 @@ func TestNoUpdateMongoDBAtlasCluster(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - assert.Equal(t, time.Second*30, res.RequeueAfter) + assert.Equal(t, time.Second*120, res.RequeueAfter) // Check if the CR has been created and has the correct status. cr := &knappekv1alpha1.MongoDBAtlasCluster{} diff --git a/pkg/controller/mongodbatlasdatabaseuser/mongodbatlasdatabaseuser_controller.go b/pkg/controller/mongodbatlasdatabaseuser/mongodbatlasdatabaseuser_controller.go new file mode 100644 index 00000000..52c7318c --- /dev/null +++ b/pkg/controller/mongodbatlasdatabaseuser/mongodbatlasdatabaseuser_controller.go @@ -0,0 +1,275 @@ +package mongodbatlasdatabaseuser + +import ( + "context" + "fmt" + "net/http" + "reflect" + + knappekv1alpha1 "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1" + "github.com/Knappek/mongodbatlas-operator/pkg/config" + + ma "github.com/akshaykarle/go-mongodbatlas/mongodbatlas" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +var log = logf.Log.WithName("controller_mongodbatlasdatabaseuser") + +/** +* USER ACTION REQUIRED: This is a scaffold file intended for the user to modify with their own Controller +* business logic. Delete these comments after modifying this file.* + */ + +// Add creates a new MongoDBAtlasDatabaseUser Controller and adds it to the Manager. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(mgr manager.Manager) error { + return add(mgr, newReconciler(mgr)) +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(mgr manager.Manager) reconcile.Reconciler { + return &ReconcileMongoDBAtlasDatabaseUser{ + client: mgr.GetClient(), + scheme: mgr.GetScheme(), + atlasClient: config.GetAtlasClient(), + reconciliationConfig: config.GetReconcilitationConfig(), + } +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New("mongodbatlasdatabaseuser-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to primary resource MongoDBAtlasDatabaseUser + err = c.Watch(&source.Kind{Type: &knappekv1alpha1.MongoDBAtlasDatabaseUser{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + return nil +} + +var _ reconcile.Reconciler = &ReconcileMongoDBAtlasDatabaseUser{} + +// ReconcileMongoDBAtlasDatabaseUser reconciles a MongoDBAtlasDatabaseUser object +type ReconcileMongoDBAtlasDatabaseUser struct { + // This client, initialized using mgr.Client() above, is a split client + // that reads objects from the cache and writes to the apiserver + client client.Client + scheme *runtime.Scheme + atlasClient *ma.Client + reconciliationConfig *config.ReconciliationConfig +} + +// Reconcile reads that state of the MongoDBAtlasDatabaseUser object and makes changes based on the state read +// and what is in the MongoDBAtlasDatabaseUser.Spec +// TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates +// a Pod as an example +// Note: +// The Controller will requeue the Request to be processed again if the returned error is non-nil or +// Result.Requeue is true, otherwise upon completion it will remove the work from the queue. +func (r *ReconcileMongoDBAtlasDatabaseUser) Reconcile(request reconcile.Request) (reconcile.Result, error) { + // Fetch the MongoDBAtlasDatabaseUser atlasDatabaseUser + atlasDatabaseUser := &knappekv1alpha1.MongoDBAtlasDatabaseUser{} + err := r.client.Get(context.TODO(), request.NamespacedName, atlasDatabaseUser) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, err + } + + projectName := atlasDatabaseUser.Spec.ProjectName + atlasProject := &knappekv1alpha1.MongoDBAtlasProject{} + atlasProjectNamespacedName := types.NamespacedName{ + Name: projectName, + Namespace: atlasDatabaseUser.Namespace, + } + + err = r.client.Get(context.TODO(), atlasProjectNamespacedName, atlasProject) + if err != nil { + return reconcile.Result{}, err + } + + groupID := atlasProject.Status.ID + // Define default logger + reqLogger := log.WithValues("Request.Namespace", request.Namespace, "MongoDBAtlasDatabaseUser.Name", request.Name, "MongoDBAtlasDatabaseUser.GroupID", groupID) + + // Check if the MongoDBAtlasDatabaseUser CR was marked to be deleted + isMongoDBAtlasDatabaseUserToBeDeleted := atlasDatabaseUser.GetDeletionTimestamp() != nil + if isMongoDBAtlasDatabaseUserToBeDeleted { + err := deleteMongoDBAtlasDatabaseUser(reqLogger, r.atlasClient, atlasDatabaseUser) + if err != nil { + return reconcile.Result{}, err + } + err = r.client.Update(context.TODO(), atlasDatabaseUser) + if err != nil { + return reconcile.Result{}, err + } + // Requeue to periodically reconcile the CR MongoDBAtlasDatabaseUser in order to recreate a manually deleted Atlas DatabaseUser + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil + } + + // Create a new MongoDBAtlasDatabaseUser + isMongoDBAtlasDatabaseUserToBeCreated := reflect.DeepEqual(atlasDatabaseUser.Status, knappekv1alpha1.MongoDBAtlasDatabaseUserStatus{}) + if isMongoDBAtlasDatabaseUserToBeCreated { + err = createMongoDBAtlasDatabaseUser(reqLogger, r.atlasClient, atlasDatabaseUser, atlasProject) + if err != nil { + return reconcile.Result{}, err + } + err = r.client.Status().Update(context.TODO(), atlasDatabaseUser) + if err != nil { + return reconcile.Result{}, err + } + // Add finalizer for this CR + if err := r.addFinalizer(reqLogger, atlasDatabaseUser); err != nil { + return reconcile.Result{}, err + } + // Requeue to periodically reconcile the CR MongoDBAtlasDatabaseUser in order to recreate a manually deleted Atlas DatabaseUser + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil + } + + // update existing MongoDBAtlasDatabaseUser + isMongoDBAtlasDatabaseUserToBeUpdated := knappekv1alpha1.IsMongoDBAtlasDatabaseUserToBeUpdated(atlasDatabaseUser.Spec.MongoDBAtlasDatabaseUserRequestBody, atlasDatabaseUser.Status) + if isMongoDBAtlasDatabaseUserToBeUpdated { + err = updateMongoDBAtlasDatabaseUser(reqLogger, r.atlasClient, atlasDatabaseUser, atlasProject) + if err != nil { + return reconcile.Result{}, err + } + err = r.client.Status().Update(context.TODO(), atlasDatabaseUser) + if err != nil { + return reconcile.Result{}, err + } + // Requeue to periodically reconcile the CR MongoDBAtlasDatabaseUser in order to recreate a manually deleted Atlas DatabaseUser + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil + } + + // if no Create/Update/Delete command apply, then fetch the status + err = getMongoDBAtlasDatabaseUser(reqLogger, r.atlasClient, atlasDatabaseUser) + if err != nil { + return reconcile.Result{}, err + } + err = r.client.Status().Update(context.TODO(), atlasDatabaseUser) + if err != nil { + return reconcile.Result{}, err + } + + // Requeue to periodically reconcile the CR MongoDBAtlasDatabaseUser in order to recreate a manually deleted Atlas DatabaseUser + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil +} + +func createMongoDBAtlasDatabaseUser(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappekv1alpha1.MongoDBAtlasDatabaseUser, ap *knappekv1alpha1.MongoDBAtlasProject) error { + groupID := ap.Status.ID + name := cr.Name + params := getDatabaseUserParams(cr) + c, resp, err := atlasClient.DatabaseUsers.Create(groupID, ¶ms) + if err != nil { + return fmt.Errorf("(%v) Error creating DatabaseUser %v: %s", resp.StatusCode, name, err) + } + if resp.StatusCode == http.StatusCreated { + reqLogger.Info("DatabaseUser created.") + return updateCRStatus(reqLogger, cr, c) + } + return fmt.Errorf("(%v) Error creating DatabaseUser %s: %s", resp.StatusCode, name, err) +} + +func updateMongoDBAtlasDatabaseUser(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappekv1alpha1.MongoDBAtlasDatabaseUser, ap *knappekv1alpha1.MongoDBAtlasProject) error { + groupID := ap.Status.ID + name := cr.Name + params := getDatabaseUserParams(cr) + c, resp, err := atlasClient.DatabaseUsers.Update(groupID, name, ¶ms) + if err != nil { + return fmt.Errorf("Error updating DatabaseUser %v: %s", name, err) + } + if resp.StatusCode == http.StatusOK { + reqLogger.Info("DatabaseUser updated.") + return updateCRStatus(reqLogger, cr, c) + } + return fmt.Errorf("(%v) Error updating DatabaseUser %s: %s", resp.StatusCode, name, err) +} + +func deleteMongoDBAtlasDatabaseUser(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappekv1alpha1.MongoDBAtlasDatabaseUser) error { + groupID := cr.Status.GroupID + name := cr.Name + // cluster exists and can be deleted + resp, err := atlasClient.DatabaseUsers.Delete(groupID, name) + if err != nil { + if resp.StatusCode == http.StatusNotFound { + reqLogger.Info("DatabaseUser does not exist in Atlas. Deleting CR.") + // Update finalizer to allow delete CR + cr.SetFinalizers(nil) + // CR can be deleted - Requeue + return nil + } + return fmt.Errorf("(%v) Error deleting DatabaseUser %s: %s", resp.StatusCode, name, err) + } + // Update finalizer to allow delete CR + cr.SetFinalizers(nil) + reqLogger.Info("DatabaseUser deleted.") + return nil +} + +func getMongoDBAtlasDatabaseUser(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappekv1alpha1.MongoDBAtlasDatabaseUser) error { + groupID := cr.Status.GroupID + name := cr.Name + c, resp, err := atlasClient.DatabaseUsers.Get(groupID, name) + if err != nil { + return fmt.Errorf("(%v) Error fetching DatabaseUser information %s: %s", resp.StatusCode, name, err) + } + err = updateCRStatus(reqLogger, cr, c) + if err != nil { + return fmt.Errorf("Error updating DatabaseUser CR Status: %s", err) + } + return nil +} + +func getDatabaseUserParams(cr *knappekv1alpha1.MongoDBAtlasDatabaseUser) ma.DatabaseUser { + return ma.DatabaseUser{ + Username: cr.Name, + Password: cr.Spec.Password, + DatabaseName: "admin", + Roles: cr.Spec.Roles, + } +} + +func updateCRStatus(reqLogger logr.Logger, cr *knappekv1alpha1.MongoDBAtlasDatabaseUser, c *ma.DatabaseUser) error { + // update status field in CR + cr.Status.Username = c.Username + cr.Status.GroupID = c.GroupID + cr.Status.DatabaseName = c.DatabaseName + cr.Status.DeleteAfterDate = c.DeleteAfterDate + cr.Status.Roles = c.Roles + return nil +} + +func (r *ReconcileMongoDBAtlasDatabaseUser) addFinalizer(reqLogger logr.Logger, cr *knappekv1alpha1.MongoDBAtlasDatabaseUser) error { + if len(cr.GetFinalizers()) < 1 && cr.GetDeletionTimestamp() == nil { + cr.SetFinalizers([]string{"finalizer.knappek.com"}) + + // Update CR + err := r.client.Update(context.TODO(), cr) + if err != nil { + reqLogger.Error(err, "Failed to update DatabaseUser with finalizer") + return err + } + } + return nil +} diff --git a/pkg/controller/mongodbatlasdatabaseuser/mongodbatlasdatabaseuser_controller_test.go b/pkg/controller/mongodbatlasdatabaseuser/mongodbatlasdatabaseuser_controller_test.go new file mode 100644 index 00000000..2be7a769 --- /dev/null +++ b/pkg/controller/mongodbatlasdatabaseuser/mongodbatlasdatabaseuser_controller_test.go @@ -0,0 +1,328 @@ +package mongodbatlasdatabaseuser + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + ma "github.com/akshaykarle/go-mongodbatlas/mongodbatlas" + + knappekv1alpha1 "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1" + "github.com/Knappek/mongodbatlas-operator/pkg/config" + testutil "github.com/Knappek/mongodbatlas-operator/pkg/controller/test" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" +) + +var ( + namespace = "mongodbatlas" + organizationID = "testOrgID" + projectName = "unittest-project" + groupID = "5a0a1e7e0f2912c554080ae6" + resourceName = "testuser" + password = "testpassword" + databaseName = "testdb" + deleteAfterDate = "2100-01-01T00:00:00Z" + roles = []ma.Role{ma.Role{DatabaseName: databaseName, RoleName: "readWrite"}} +) + +func TestCreatemongodbatlasdatabaseuser(t *testing.T) { + // Set the logger to development mode for verbose logs. + logf.SetLogger(logf.ZapLogger(true)) + + // A MongoDBAtlasProject resource with metadata and spec. + mongodbatlasproject := testutil.CreateAtlasProject(projectName, groupID, namespace, organizationID) + + // A mongodbatlasdatabaseuser resource with metadata and spec. + mongodbatlasdatabaseuser := &knappekv1alpha1.MongoDBAtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Spec: knappekv1alpha1.MongoDBAtlasDatabaseUserSpec{ + ProjectName: projectName, + MongoDBAtlasDatabaseUserRequestBody: knappekv1alpha1.MongoDBAtlasDatabaseUserRequestBody{ + Password: password, + DeleteAfterDate: deleteAfterDate, + DatabaseName: "admin", + Roles: roles, + }, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{ + mongodbatlasdatabaseuser, + mongodbatlasproject, + } + + // Register operator types with the runtime scheme. + s := scheme.Scheme + s.AddKnownTypes(knappekv1alpha1.SchemeGroupVersion, mongodbatlasdatabaseuser, mongodbatlasproject) + + // Create a fake k8s client to mock API calls. + k8sClient := fake.NewFakeClient(objs...) + // Create a fake atlas client to mock API calls. + // atlasClient, server := test.NewAtlasFakeClient(t) + httpClient, mux, server := testutil.Server() + defer server.Close() + atlasClient := ma.NewClient(httpClient) + + // Post request for MongoDBAtlasDatabaseUser + mux.HandleFunc("/api/atlas/v1.0/groups/"+groupID+"/databaseUsers", func(w http.ResponseWriter, r *http.Request) { + testutil.AssertMethod(t, "POST", r) + w.WriteHeader(http.StatusCreated) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "groupId":"`+groupID+`", + "databaseName":"admin", + "username":"`+resourceName+`", + "deleteAfterDate":"`+deleteAfterDate+`", + "roles":[{"databaseName":"`+roles[0].DatabaseName+`","roleName":"`+roles[0].RoleName+`"}] + }`) + }) + + // Create a ReconcileMongoDBAtlasDatabaseUser object with the scheme and fake client. + r := &ReconcileMongoDBAtlasDatabaseUser{ + client: k8sClient, + scheme: s, atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: resourceName, + Namespace: namespace, + }, + } + res, err := r.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + assert.Equal(t, time.Second*120, res.RequeueAfter) + + // Check if the CR has been created and has the correct status. + cr := &knappekv1alpha1.MongoDBAtlasDatabaseUser{} + err = k8sClient.Get(context.TODO(), req.NamespacedName, cr) + if err != nil { + t.Fatalf("get mongodbatlasdatabaseuser: (%v)", err) + } + assert.Equal(t, groupID, cr.Status.GroupID) + assert.Equal(t, resourceName, cr.Status.Username) + assert.Equal(t, "admin", cr.Status.DatabaseName) + assert.Equal(t, deleteAfterDate, cr.Status.DeleteAfterDate) + assert.Equal(t, roles, cr.Status.Roles) +} + +func TestDeletemongodbatlasdatabaseuser(t *testing.T) { + // Set the logger to development mode for verbose logs. + logf.SetLogger(logf.ZapLogger(true)) + + // A MongoDBAtlasProject resource with metadata and spec. + mongodbatlasproject := testutil.CreateAtlasProject(projectName, groupID, namespace, organizationID) + + // A mongodbatlasdatabaseuser resource with metadata and spec. + mongodbatlasdatabaseuser := &knappekv1alpha1.MongoDBAtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + Finalizers: []string{"finalizer.knappek.com"}, + }, + Spec: knappekv1alpha1.MongoDBAtlasDatabaseUserSpec{ + ProjectName: projectName, + MongoDBAtlasDatabaseUserRequestBody: knappekv1alpha1.MongoDBAtlasDatabaseUserRequestBody{ + Password: password, + DeleteAfterDate: deleteAfterDate, + DatabaseName: "admin", + Roles: roles, + }, + }, + Status: knappekv1alpha1.MongoDBAtlasDatabaseUserStatus{ + GroupID: groupID, + Username: resourceName, + DeleteAfterDate: deleteAfterDate, + DatabaseName: "admin", + Roles: roles, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{ + mongodbatlasdatabaseuser, + mongodbatlasproject, + } + + // Register operator types with the runtime scheme. + s := scheme.Scheme + s.AddKnownTypes(knappekv1alpha1.SchemeGroupVersion, mongodbatlasdatabaseuser, mongodbatlasproject) + + // Create a fake k8s client to mock API calls. + k8sClient := fake.NewFakeClient(objs...) + // Create a fake atlas client to mock API calls. + // atlasClient, server := test.NewAtlasFakeClient(t) + httpClient, mux, server := testutil.Server() + defer server.Close() + atlasClient := ma.NewClient(httpClient) + + // Delete + mux.HandleFunc("/api/atlas/v1.0/groups/"+groupID+"/databaseUsers/admin/"+resourceName, func(w http.ResponseWriter, r *http.Request) { + testutil.AssertMethod(t, "DELETE", r) + fmt.Fprintf(w, `{}`) + }) + + // Create a ReconcileMongoDBAtlasDatabaseUser object with the scheme and fake client. + r := &ReconcileMongoDBAtlasDatabaseUser{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: resourceName, + Namespace: namespace, + }, + } + res, err := r.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + assert.Equal(t, time.Second*120, res.RequeueAfter) + + // Check if the CR has been created and has the correct status. + cr := &knappekv1alpha1.MongoDBAtlasDatabaseUser{} + err = k8sClient.Get(context.TODO(), req.NamespacedName, cr) + assert.Nil(t, err) + assert.Nil(t, cr.ObjectMeta.GetFinalizers()) +} + +func TestUpdatemongodbatlasdatabaseuser(t *testing.T) { + // Set the logger to development mode for verbose logs. + logf.SetLogger(logf.ZapLogger(true)) + + // A MongoDBAtlasProject resource with metadata and spec. + mongodbatlasproject := &knappekv1alpha1.MongoDBAtlasProject{ + ObjectMeta: metav1.ObjectMeta{ + Name: projectName, + Namespace: namespace, + }, + Spec: knappekv1alpha1.MongoDBAtlasProjectSpec{ + OrgID: organizationID, + }, + Status: knappekv1alpha1.MongoDBAtlasProjectStatus{ + ID: groupID, + Name: projectName, + OrgID: organizationID, + Created: "2016-07-14T14:19:33Z", + ClusterCount: 1, + }, + } + + updatedRoles := []ma.Role{ + ma.Role{DatabaseName: databaseName, RoleName: "readWrite"}, + ma.Role{DatabaseName: "testdbreadonly", RoleName: "read"}, + } + updatedDeleteAfterDate := "2100-02-01T00:00:00Z" + + // A mongodbatlasdatabaseuser resource with metadata and spec. + mongodbatlasdatabaseuser := &knappekv1alpha1.MongoDBAtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Spec: knappekv1alpha1.MongoDBAtlasDatabaseUserSpec{ + ProjectName: projectName, + MongoDBAtlasDatabaseUserRequestBody: knappekv1alpha1.MongoDBAtlasDatabaseUserRequestBody{ + Password: password, + DeleteAfterDate: deleteAfterDate, + DatabaseName: "admin", + Roles: updatedRoles, + }, + }, + Status: knappekv1alpha1.MongoDBAtlasDatabaseUserStatus{ + GroupID: groupID, + Username: resourceName, + DeleteAfterDate: updatedDeleteAfterDate, + DatabaseName: "admin", + Roles: roles, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{ + mongodbatlasdatabaseuser, + mongodbatlasproject, + } + + // Register operator types with the runtime scheme. + s := scheme.Scheme + s.AddKnownTypes(knappekv1alpha1.SchemeGroupVersion, mongodbatlasdatabaseuser, mongodbatlasproject) + + // Create a fake k8s client to mock API calls. + k8sClient := fake.NewFakeClient(objs...) + // Create a fake atlas client to mock API calls. + // atlasClient, server := test.NewAtlasFakeClient(t) + httpClient, mux, server := testutil.Server() + defer server.Close() + atlasClient := ma.NewClient(httpClient) + // Construct Update API call + mux.HandleFunc("/api/atlas/v1.0/groups/"+groupID+"/databaseUsers/admin/"+resourceName, func(w http.ResponseWriter, r *http.Request) { + testutil.AssertMethod(t, "PATCH", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "groupId":"`+groupID+`", + "databaseName":"admin", + "deleteAfterDate":"`+updatedDeleteAfterDate+`", + "username":"`+resourceName+`", + "roles":[ + {"databaseName":"`+updatedRoles[0].DatabaseName+`","roleName":"`+updatedRoles[0].RoleName+`"}, + {"databaseName":"`+updatedRoles[1].DatabaseName+`","roleName":"`+updatedRoles[1].RoleName+`"} + ] + }`) + }) + // Create a ReconcileMongoDBAtlasDatabaseUser object with the scheme and fake client. + r := &ReconcileMongoDBAtlasDatabaseUser{ + client: k8sClient, + scheme: s, atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: resourceName, + Namespace: namespace, + }, + } + res, err := r.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + assert.Equal(t, time.Second*120, res.RequeueAfter) + + // Check if the CR has been created and has the correct status. + cr := &knappekv1alpha1.MongoDBAtlasDatabaseUser{} + err = k8sClient.Get(context.TODO(), req.NamespacedName, cr) + if err != nil { + t.Fatalf("get mongodbatlasdatabaseuser: (%v)", err) + } + assert.Equal(t, updatedRoles, cr.Status.Roles) + assert.Equal(t, updatedDeleteAfterDate, cr.Status.DeleteAfterDate) +} diff --git a/pkg/controller/mongodbatlasproject/mongodbatlasproject_controller.go b/pkg/controller/mongodbatlasproject/mongodbatlasproject_controller.go index 3e7e7c04..396b4dfc 100644 --- a/pkg/controller/mongodbatlasproject/mongodbatlasproject_controller.go +++ b/pkg/controller/mongodbatlasproject/mongodbatlasproject_controller.go @@ -3,7 +3,6 @@ package mongodbatlasproject import ( "context" "fmt" - "time" knappekv1alpha1 "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1" "github.com/Knappek/mongodbatlas-operator/pkg/config" @@ -29,9 +28,13 @@ func Add(mgr manager.Manager) error { return add(mgr, newReconciler(mgr)) } -// newReconciler returns a new reconcile.Reconciler func newReconciler(mgr manager.Manager) reconcile.Reconciler { - return &ReconcileMongoDBAtlasProject{client: mgr.GetClient(), scheme: mgr.GetScheme(), atlasClient: config.GetAtlasClient()} + return &ReconcileMongoDBAtlasProject{ + client: mgr.GetClient(), + scheme: mgr.GetScheme(), + atlasClient: config.GetAtlasClient(), + reconciliationConfig: config.GetReconcilitationConfig(), + } } // add adds a new Controller to mgr with r as the reconcile.Reconciler @@ -57,9 +60,10 @@ var _ reconcile.Reconciler = &ReconcileMongoDBAtlasProject{} type ReconcileMongoDBAtlasProject struct { // This client, initialized using mgr.Client() above, is a split client // that reads objects from the cache and writes to the apiserver - client client.Client - scheme *runtime.Scheme - atlasClient *ma.Client + client client.Client + scheme *runtime.Scheme + atlasClient *ma.Client + reconciliationConfig *config.ReconciliationConfig } // Reconcile reads that state of the cluster for a MongoDBAtlasProject object and makes changes based on the state read @@ -104,16 +108,13 @@ func (r *ReconcileMongoDBAtlasProject) Reconcile(request reconcile.Request) (rec if err != nil { return reconcile.Result{}, err } - // Update finalizer to allow delete CR - atlasProject.SetFinalizers(nil) - // Update CR err = r.client.Update(context.TODO(), atlasProject) if err != nil { return reconcile.Result{}, err } - // MongoDB Atlas Project successfully deleted - return reconcile.Result{}, nil + // Requeue to periodically reconcile the CR MongoDBAtlasProject in order to recreate a manually deleted Atlas DatabaseUser + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil } // Add finalizer for this CR if err := r.addFinalizer(reqLogger, atlasProject); err != nil { @@ -121,7 +122,7 @@ func (r *ReconcileMongoDBAtlasProject) Reconcile(request reconcile.Request) (rec } // MongoDB Atlas Project successfully created // Requeue to periodically reconcile the CR MongoDBAtlasProject in order to recreate a manually deleted Atlas project - return reconcile.Result{RequeueAfter: time.Second * 30}, nil + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil } func createMongoDBAtlasProject(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappekv1alpha1.MongoDBAtlasProject) error { @@ -153,18 +154,22 @@ func deleteMongoDBAtlasProject(reqLogger logr.Logger, atlasClient *ma.Client, cr if err != nil { if resp.StatusCode == 404 { reqLogger.Info("Project does not exist in Atlas. Deleting CR.") + // Update finalizer to allow delete CR + cr.SetFinalizers(nil) return nil } return fmt.Errorf("Error getting MongoDB Project %s: %s", cr.Name, err) } // project exists and can be deleted - atlasProjectID := p.ID - resp, err = atlasClient.Projects.Delete(atlasProjectID) + atlasGroupID := p.ID + resp, err = atlasClient.Projects.Delete(atlasGroupID) if err != nil { - return fmt.Errorf("(%v) Error deleting MongoDB Project %s: %s", resp.StatusCode, atlasProjectID, err) + return fmt.Errorf("(%v) Error deleting MongoDB Project %s: %s", resp.StatusCode, atlasGroupID, err) } - reqLogger.Info("Project deleted.", "MongoDBAtlasProject.ID", atlasProjectID) + // Update finalizer to allow delete CR + cr.SetFinalizers(nil) + reqLogger.Info("Project deleted.", "MongoDBAtlasProject.ID", atlasGroupID) return nil } diff --git a/pkg/controller/mongodbatlasproject/mongodbatlasproject_controller_test.go b/pkg/controller/mongodbatlasproject/mongodbatlasproject_controller_test.go index ec8d913e..3194d46f 100644 --- a/pkg/controller/mongodbatlasproject/mongodbatlasproject_controller_test.go +++ b/pkg/controller/mongodbatlasproject/mongodbatlasproject_controller_test.go @@ -13,6 +13,7 @@ import ( ma "github.com/akshaykarle/go-mongodbatlas/mongodbatlas" knappekv1alpha1 "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1" + "github.com/Knappek/mongodbatlas-operator/pkg/config" testutil "github.com/Knappek/mongodbatlas-operator/pkg/controller/test" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -26,7 +27,7 @@ import ( var ( projectName = "unittest-project" - projectID = "5a0a1e7e0f2912c554080ae6" + groupID = "5a0a1e7e0f2912c554080ae6" namespace = "mongodbatlas" organizationID = "testOrgID" created = "2016-07-14T14:19:33Z" @@ -71,17 +72,17 @@ func TestNonExistingMongoDBAtlasProjectCR(t *testing.T) { mux.HandleFunc("/api/atlas/v1.0/groups/", func(w http.ResponseWriter, r *http.Request) { testutil.AssertMethod(t, "POST", r) w.Header().Set("Content-Type", "application/json") - expectedBody := map[string]interface{}{ - "orgId": organizationID, - "name": projectName, - } - testutil.AssertReqJSON(t, expectedBody, r) - fmt.Fprintf(w, `{"clusterCount": `+strconv.Itoa(clusterCount)+`, "created":"`+created+`", "id":"`+projectID+`", "links":[], "name":"`+projectName+`", "orgId":"`+organizationID+`"}`) + fmt.Fprintf(w, `{"clusterCount": `+strconv.Itoa(clusterCount)+`, "created":"`+created+`", "id":"`+groupID+`", "links":[], "name":"`+projectName+`", "orgId":"`+organizationID+`"}`) }) atlasClient := ma.NewClient(httpClient) // Create a ReconcileMongoDBAtlasProject object with the scheme and fake client. - r := &ReconcileMongoDBAtlasProject{client: k8sClient, scheme: s, atlasClient: atlasClient} + r := &ReconcileMongoDBAtlasProject{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } // Mock request with non-existing project req := reconcile.Request{ @@ -136,17 +137,17 @@ func TestCreateMongoDBAtlasProject(t *testing.T) { mux.HandleFunc("/api/atlas/v1.0/groups/", func(w http.ResponseWriter, r *http.Request) { testutil.AssertMethod(t, "POST", r) w.Header().Set("Content-Type", "application/json") - expectedBody := map[string]interface{}{ - "orgId": organizationID, - "name": projectName, - } - testutil.AssertReqJSON(t, expectedBody, r) - fmt.Fprintf(w, `{"clusterCount": `+strconv.Itoa(clusterCount)+`, "created":"`+created+`", "id":"`+projectID+`", "links":[], "name":"`+projectName+`", "orgId":"`+organizationID+`"}`) + fmt.Fprintf(w, `{"clusterCount": `+strconv.Itoa(clusterCount)+`, "created":"`+created+`", "id":"`+groupID+`", "links":[], "name":"`+projectName+`", "orgId":"`+organizationID+`"}`) }) atlasClient := ma.NewClient(httpClient) // Create a ReconcileMongoDBAtlasProject object with the scheme and fake client. - r := &ReconcileMongoDBAtlasProject{client: k8sClient, scheme: s, atlasClient: atlasClient} + r := &ReconcileMongoDBAtlasProject{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } // Mock request to simulate Reconcile() being called on an event for a // watched resource . @@ -160,7 +161,7 @@ func TestCreateMongoDBAtlasProject(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - assert.Equal(t, time.Second*30, res.RequeueAfter) + assert.Equal(t, time.Second*120, res.RequeueAfter) // Check if the CR has been created and has the correct status. cr := &knappekv1alpha1.MongoDBAtlasProject{} @@ -170,7 +171,7 @@ func TestCreateMongoDBAtlasProject(t *testing.T) { } assert.Equal(t, "finalizer.knappek.com", cr.ObjectMeta.GetFinalizers()[0], "The finalizer in the CR is not as expected") assert.Equal(t, organizationID, cr.Spec.OrgID, "The orgID in the Spec block is not as expected") - assert.Equal(t, projectID, cr.Status.ID, "The id in the Status block is not as expected") + assert.Equal(t, groupID, cr.Status.ID, "The id in the Status block is not as expected") assert.Equal(t, projectName, cr.Status.Name, "The name in the Status block is not as expected") assert.Equal(t, organizationID, cr.Status.OrgID, "The orgId in the Status block is not as expected") assert.Equal(t, created, cr.Status.Created, "The create in the Status block is not as expected") @@ -193,7 +194,7 @@ func TestDeleteMongoDBAtlasProject(t *testing.T) { OrgID: organizationID, }, Status: knappekv1alpha1.MongoDBAtlasProjectStatus{ - ID: projectID, + ID: groupID, OrgID: organizationID, Name: projectName, Created: created, @@ -219,17 +220,22 @@ func TestDeleteMongoDBAtlasProject(t *testing.T) { // getByName: assert that there is no existing project mux.HandleFunc("/api/atlas/v1.0/groups/byName/"+projectName, func(w http.ResponseWriter, r *http.Request) { testutil.AssertMethod(t, "GET", r) - fmt.Fprintf(w, `{"clusterCount": 0, "created":"`+created+`", "id":"`+projectID+`", "links":[], "name":"`+projectName+`", "orgId":"`+organizationID+`"}`) + fmt.Fprintf(w, `{"clusterCount": 0, "created":"`+created+`", "id":"`+groupID+`", "links":[], "name":"`+projectName+`", "orgId":"`+organizationID+`"}`) }) // delete - mux.HandleFunc("/api/atlas/v1.0/groups/"+projectID, func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/atlas/v1.0/groups/"+groupID, func(w http.ResponseWriter, r *http.Request) { testutil.AssertMethod(t, "DELETE", r) fmt.Fprintf(w, `{}`) }) atlasClient := ma.NewClient(httpClient) // Create a ReconcileMongoDBAtlasProject object with the scheme and fake client. - r := &ReconcileMongoDBAtlasProject{client: k8sClient, scheme: s, atlasClient: atlasClient} + r := &ReconcileMongoDBAtlasProject{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } // Mock request to simulate Reconcile() being called on an event for a // watched resource . @@ -243,11 +249,11 @@ func TestDeleteMongoDBAtlasProject(t *testing.T) { if err != nil { t.Fatalf("reconcile: (%v)", err) } - assert.Equal(t, reconcile.Result{}, res) - assert.Equal(t, false, res.Requeue) + assert.Equal(t, time.Second*120, res.RequeueAfter) // Check if the CR has been created and has the correct status. cr := &knappekv1alpha1.MongoDBAtlasProject{} err = k8sClient.Get(context.TODO(), req.NamespacedName, cr) assert.Nil(t, err) + assert.Nil(t, cr.ObjectMeta.GetFinalizers()) } diff --git a/pkg/controller/test/util.go b/pkg/controller/test/util.go index 05bcea3b..9699877a 100644 --- a/pkg/controller/test/util.go +++ b/pkg/controller/test/util.go @@ -7,7 +7,9 @@ import ( "net/url" "testing" + knappekv1alpha1 "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1" "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Server returns an http Client, ServeMux, and Server. The client proxies @@ -56,3 +58,23 @@ func AssertReqJSON(t *testing.T, expected map[string]interface{}, req *http.Requ } assert.Equal(t, expected, reqJSON) } + +// CreateAtlasProject returns a standard atlas project +func CreateAtlasProject(projectName string, groupID string, namespace string, organizationID string) *knappekv1alpha1.MongoDBAtlasProject { + return &knappekv1alpha1.MongoDBAtlasProject{ + ObjectMeta: metav1.ObjectMeta{ + Name: projectName, + Namespace: namespace, + }, + Spec: knappekv1alpha1.MongoDBAtlasProjectSpec{ + OrgID: organizationID, + }, + Status: knappekv1alpha1.MongoDBAtlasProjectStatus{ + ID: groupID, + Name: projectName, + OrgID: organizationID, + Created: "2016-07-14T14:19:33Z", + ClusterCount: 0, + }, + } +} diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100644 index 00000000..10ad01b3 --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,21 @@ +package util + +// IsZeroValue returns true if input interface is the corresponding zero value +func IsZeroValue(i interface{}) bool { + if i == nil { + return true + } // nil interface + if i == "" { + return true + } // zero value of a string + if i == 0.0 { + return true + } // zero value of a float64 + if i == 0 { + return true + } // zero value of an int + if i == false { + return true + } // zero value of a boolean + return false +} diff --git a/templates/add_kind.go.tmpl b/templates/add_kind.go.tmpl new file mode 100644 index 00000000..2c67ab3c --- /dev/null +++ b/templates/add_kind.go.tmpl @@ -0,0 +1,11 @@ + +package controller + +import ( + "github.com/Knappek/mongodbatlas-operator/pkg/controller/_KIND_LOWERCASE_" +) + +func init() { + // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. + AddToManagerFuncs = append(AddToManagerFuncs, _KIND_LOWERCASE_.Add) +} diff --git a/templates/e2e/kind_test.go.tmpl b/templates/e2e/kind_test.go.tmpl new file mode 100644 index 00000000..18cfeb13 --- /dev/null +++ b/templates/e2e/kind_test.go.tmpl @@ -0,0 +1,71 @@ + +package e2e + +import ( + goctx "context" + "fmt" + "testing" + "time" + + knappekv1alpha1 "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1" + ma "github.com/akshaykarle/go-mongodbatlas/mongodbatlas" + + framework "github.com/operator-framework/operator-sdk/pkg/test" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" +) + +func _KIND_(t *testing.T, ctx *framework.TestCtx, f *framework.Framework, namespace string) { + resourceName := "e2etest-test_KIND_SHORT_" + example_KIND_ := &knappek_API_VERSION_._KIND_{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Spec: knappek_API_VERSION_._KIND_Spec{ + ProjectName: atlasProjectName, + _KIND_RequestBody: knappek_API_VERSION_._KIND_RequestBody{ + // + // TODO + // + }, + }, + } + err := f.Client.Create(goctx.TODO(), example_KIND_, &framework.CleanupOptions{TestContext: ctx, Timeout: time.Second * 5, RetryInterval: time.Second * 1}) + if err != nil { + t.Fatal(err) + } + fmt.Printf("wait for creating _KIND_SHORT_: %v\n", example_KIND_.ObjectMeta.Name) + err = waitFor_KIND_(t, f, example_KIND_, "2100-01-01T00:00:00Z") + if err != nil { + t.Fatal(err) + } + fmt.Printf("_KIND_SHORT_ %v successfully created\n", example_KIND_.ObjectMeta.Name) + + // update resource + example_KIND_.Spec.DeleteAfterDate = "2100-02-01T00:00:00Z" + err = f.Client.Update(goctx.TODO(), example_KIND_) + if err != nil { + t.Fatal(err) + } + fmt.Printf("wait for updating _KIND_SHORT_: %v\n", example_KIND_.ObjectMeta.Name) + err = waitFor_KIND_(t, f, example_KIND_, "2100-02-01T00:00:00Z") + if err != nil { + t.Fatal(err) + } + fmt.Printf("_KIND_SHORT_ %v successfully updated\n", example_KIND_.ObjectMeta.Name) +} + +func waitFor_KIND_(t *testing.T, f *framework.Framework, p *knappek_API_VERSION_._KIND_, desiredState string) error { + retryInterval := time.Second * 5 + timeout := time.Second * 10 + err := wait.Poll(retryInterval, timeout, func() (done bool, err error) { + err = f.Client.Get(goctx.TODO(), types.NamespacedName{Name: p.Name, Namespace: p.Namespace}, p) + return isInDesiredState(t, err, p.Name, p.Kind, p.Status.DeleteAfterDate, desiredState) + }) + if err != nil { + return err + } + return nil +} diff --git a/templates/kind_controller.go.tmpl b/templates/kind_controller.go.tmpl new file mode 100644 index 00000000..6697cc4d --- /dev/null +++ b/templates/kind_controller.go.tmpl @@ -0,0 +1,277 @@ + +package _KIND_LOWERCASE_ + +import ( + "context" + "reflect" + "net/http" + "fmt" + + knappek_API_VERSION_ "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/_API_VERSION_" + "github.com/Knappek/mongodbatlas-operator/pkg/config" + + ma "github.com/akshaykarle/go-mongodbatlas/mongodbatlas" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +var log = logf.Log.WithName("controller__KIND_LOWERCASE_") + +/** +* USER ACTION REQUIRED: This is a scaffold file intended for the user to modify with their own Controller +* business logic. Delete these comments after modifying this file.* + */ + +// Add creates a new _KIND_ Controller and adds it to the Manager. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(mgr manager.Manager) error { + return add(mgr, newReconciler(mgr)) +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(mgr manager.Manager) reconcile.Reconciler { + return &Reconcile_KIND_{ + client: mgr.GetClient(), + scheme: mgr.GetScheme(), + atlasClient: config.GetAtlasClient(), + reconciliationConfig: config.GetReconcilitationConfig(), + } +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New("_KIND_LOWERCASE_-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to primary resource _KIND_ + err = c.Watch(&source.Kind{Type: &knappek_API_VERSION_._KIND_{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + return nil +} + +var _ reconcile.Reconciler = &Reconcile_KIND_{} + +// Reconcile_KIND_ reconciles a _KIND_ object +type Reconcile_KIND_ struct { + // This client, initialized using mgr.Client() above, is a split client + // that reads objects from the cache and writes to the apiserver + client client.Client + scheme *runtime.Scheme + atlasClient *ma.Client + reconciliationConfig *config.ReconciliationConfig +} + +// Reconcile reads that state of the _KIND_ object and makes changes based on the state read +// and what is in the _KIND_.Spec +// TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates +// a Pod as an example +// Note: +// The Controller will requeue the Request to be processed again if the returned error is non-nil or +// Result.Requeue is true, otherwise upon completion it will remove the work from the queue. +func (r *Reconcile_KIND_) Reconcile(request reconcile.Request) (reconcile.Result, error) { + // Fetch the _KIND_ atlas_KIND_SHORT_ + atlas_KIND_SHORT_ := &knappek_API_VERSION_._KIND_{} + err := r.client.Get(context.TODO(), request.NamespacedName, atlas_KIND_SHORT_) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, err + } + + projectName := atlas_KIND_SHORT_.Spec.ProjectName + atlasProject := &knappek_API_VERSION_.MongoDBAtlasProject{} + atlasProjectNamespacedName := types.NamespacedName{ + Name: projectName, + Namespace: atlas_KIND_SHORT_.Namespace, + } + + err = r.client.Get(context.TODO(), atlasProjectNamespacedName, atlasProject) + if err != nil { + return reconcile.Result{}, err + } + + groupID := atlasProject.Status.ID + // Define default logger + reqLogger := log.WithValues("Request.Namespace", request.Namespace, "_KIND_.Name", request.Name, "_KIND_.GroupID", groupID) + + // Check if the _KIND_ CR was marked to be deleted + is_KIND_ToBeDeleted := atlas_KIND_SHORT_.GetDeletionTimestamp() != nil + if is_KIND_ToBeDeleted { + err := deleteMongoDBAtlas_KIND_SHORT_(reqLogger, r.atlasClient, atlas_KIND_SHORT_) + if err != nil { + return reconcile.Result{}, err + } + err = r.client.Update(context.TODO(), atlas_KIND_SHORT_) + if err != nil { + return reconcile.Result{}, err + } + // Requeue to periodically reconcile the CR MongoDBAtlas_KIND_SHORT_ in order to recreate a manually deleted Atlas _KIND_SHORT_ + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil + } + + // Create a new _KIND_ + is_KIND_ToBeCreated := reflect.DeepEqual(atlas_KIND_SHORT_.Status, knappek_API_VERSION_._KIND_Status{}) + if is_KIND_ToBeCreated { + err = create_KIND_(reqLogger, r.atlasClient, atlas_KIND_SHORT_, atlasProject) + if err != nil { + return reconcile.Result{}, err + } + err = r.client.Status().Update(context.TODO(), atlas_KIND_SHORT_) + if err != nil { + return reconcile.Result{}, err + } + // Add finalizer for this CR + if err := r.addFinalizer(reqLogger, atlas_KIND_SHORT_); err != nil { + return reconcile.Result{}, err + } + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil + } + + // update existing _KIND_ + is_KIND_ToBeUpdated := knappek_API_VERSION_.Is_KIND_ToBeUpdated(atlas_KIND_SHORT_.Spec._KIND_RequestBody, atlas_KIND_SHORT_.Status._KIND_RequestBody) + if is_KIND_ToBeUpdated { + err = update_KIND_(reqLogger, r.atlasClient, atlas_KIND_SHORT_, atlasProject) + if err != nil { + return reconcile.Result{}, err + } + err = r.client.Status().Update(context.TODO(), atlas_KIND_SHORT_) + if err != nil { + return reconcile.Result{}, err + } + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil + } + + // if no Create/Update/Delete command apply, then fetch the status + err = get_KIND_(reqLogger, r.atlasClient, atlas_KIND_SHORT_) + if err != nil { + return reconcile.Result{}, err + } + err = r.client.Status().Update(context.TODO(), atlas_KIND_SHORT_) + if err != nil { + return reconcile.Result{}, err + } + + // Requeue to periodically reconcile the CR _KIND_ in order to recreate a manually deleted Atlas _KIND_SHORT_ + return reconcile.Result{RequeueAfter: r.reconciliationConfig.Time}, nil +} + +func create_KIND_(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappek_API_VERSION_._KIND_, ap *knappek_API_VERSION_.MongoDBAtlasProject) error { + groupID := ap.Status.ID + name := cr.Name + params := get_KIND_SHORT_Params(cr) + c, resp, err := atlasClient._KIND_SHORT_s.Create(groupID, ¶ms) + if err != nil { + return fmt.Errorf("Error creating _KIND_SHORT_ %v: %s", name, err) + } + if resp.StatusCode == http.StatusOK { + reqLogger.Info("_KIND_SHORT_ created.") + return updateCRStatus(reqLogger, cr, c) + } + return fmt.Errorf("(%v) Error creating _KIND_SHORT_ %s: %s", resp.StatusCode, name, err) +} + +func update_KIND_(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappek_API_VERSION_._KIND_, ap *knappek_API_VERSION_.MongoDBAtlasProject) error { + groupID := ap.Status.ID + name := cr.Name + params := get_KIND_SHORT_Params(cr) + c, resp, err := atlasClient._KIND_SHORT_s.Update(groupID, name, ¶ms) + if err != nil { + return fmt.Errorf("Error updating _KIND_SHORT_ %v: %s", name, err) + } + if resp.StatusCode == http.StatusOK { + reqLogger.Info("_KIND_SHORT_ updated.") + return updateCRStatus(reqLogger, cr, c) + } + return fmt.Errorf("(%v) Error updating _KIND_SHORT_ %s: %s", resp.StatusCode, name, err) +} + +func delete_KIND_(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappek_API_VERSION_._KIND_) error { + groupID := cr.Status.GroupID + name := cr.Name + // cluster exists and can be deleted + resp, err := atlasClient._KIND_SHORT_s.Delete(groupID, name) + if err != nil { + if resp.StatusCode == http.StatusNotFound { + reqLogger.Info("_KIND_SHORT_ does not exist in Atlas. Deleting CR.") + // Update finalizer to allow delete CR + cr.SetFinalizers(nil) + // CR can be deleted - Requeue + return nil + } + return fmt.Errorf("(%v) Error deleting _KIND_SHORT_ %s: %s", resp.StatusCode, name, err) + } + if resp.StatusCode == http.StatusOK { + // Update finalizer to allow delete CR + cr.SetFinalizers(nil) + reqLogger.Info("_KIND_SHORT_ deleted.") + return nil + } + return fmt.Errorf("(%v) Error deleting _KIND_SHORT_ %s: %s", resp.StatusCode, name, err) +} + +func get_KIND_(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappek_API_VERSION_._KIND_) error { + groupID := cr.Status.GroupID + name := cr.Name + c, resp, err := atlasClient._KIND_SHORT_s.Get(groupID, name) + if err != nil { + return fmt.Errorf("(%v) Error fetching _KIND_SHORT_ information %s: %s", resp.StatusCode, name, err) + } + err = updateCRStatus(reqLogger, cr, c) + if err != nil { + return fmt.Errorf("Error updating _KIND_SHORT_ CR Status: %s", err) + } + return nil +} + +func get_KIND_SHORT_Params(cr *knappek_API_VERSION_._KIND_) ma._KIND_SHORT_ { + return ma._KIND_SHORT_{ + // + // TODO + // + } +} + +func updateCRStatus(reqLogger logr.Logger, cr *knappek_API_VERSION_._KIND_, c *ma._KIND_SHORT_) error { + // update status field in CR + cr.Status.ID = c.ID + cr.Status.GroupID = c.GroupID + cr.Status.Name = c.Name + // + // TODO + // + return nil +} + +func (r *Reconcile_KIND_) addFinalizer(reqLogger logr.Logger, cr *knappek_API_VERSION_._KIND_) error { + if len(cr.GetFinalizers()) < 1 && cr.GetDeletionTimestamp() == nil { + cr.SetFinalizers([]string{"finalizer.knappek.com"}) + + // Update CR + err := r.client.Update(context.TODO(), cr) + if err != nil { + reqLogger.Error(err, "Failed to update _KIND_SHORT_ with finalizer") + return err + } + } + return nil +} diff --git a/templates/kind_controller_test.go.tmpl b/templates/kind_controller_test.go.tmpl new file mode 100644 index 00000000..74bc6fbb --- /dev/null +++ b/templates/kind_controller_test.go.tmpl @@ -0,0 +1,436 @@ + +package _KIND_LOWERCASE_ + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + ma "github.com/akshaykarle/go-mongodbatlas/mongodbatlas" + + knappek_API_VERSION_ "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/_API_VERSION_" + testutil "github.com/Knappek/mongodbatlas-operator/pkg/controller/test" + "github.com/Knappek/mongodbatlas-operator/pkg/config" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" +) + +var ( + namespace = "mongodbatlas" + organizationID = "testOrgID" + projectName = "unittest-project" + groupID = "5a0a1e7e0f2912c554080ae6" + resourceName = "" +) + +func TestCreate_KIND_LOWERCASE_(t *testing.T) { + // Set the logger to development mode for verbose logs. + logf.SetLogger(logf.ZapLogger(true)) + + // A MongoDBAtlasProject resource with metadata and spec. + mongodbatlasproject := testutil.CreateAtlasProject(projectName, groupID, namespace, organizationID) + + // A _KIND_LOWERCASE_ resource with metadata and spec. + _KIND_LOWERCASE_ := &knappek_API_VERSION_._KIND_{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Spec: knappek_API_VERSION_._KIND_Spec{ + ProjectName: projectName, + _KIND_RequestBody: knappek_API_VERSION_._KIND_RequestBody{ + // + // TODO + // + }, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{ + _KIND_LOWERCASE_, + mongodbatlasproject, + } + + // Register operator types with the runtime scheme. + s := scheme.Scheme + s.AddKnownTypes(knappek_API_VERSION_.SchemeGroupVersion, _KIND_LOWERCASE_, mongodbatlasproject) + + // Create a fake k8s client to mock API calls. + k8sClient := fake.NewFakeClient(objs...) + // Create a fake atlas client to mock API calls. + // atlasClient, server := test.NewAtlasFakeClient(t) + httpClient, mux, server := testutil.Server() + defer server.Close() + atlasClient := ma.NewClient(httpClient) + + // Post request for _KIND_ + mux.HandleFunc("/api/atlas/v1.0/", func(w http.ResponseWriter, r *http.Request) { + testutil.AssertMethod(t, "POST", r) + w.Header().Set("Content-Type", "application/json") + expectedBody := map[string]interface{}{ + // + // TODO + // + } + testutil.AssertReqJSON(t, expectedBody, r) + fmt.Fprintf(w, `{ + // + // TODO + // + }`) + }) + + // Create a Reconcile_KIND_ object with the scheme and fake client. + r := &Reconcile_KIND_{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: resourceName, + Namespace: namespace, + }, + } + res, err := r.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + assert.Equal(t, time.Second*120, res.RequeueAfter) + + // Check if the CR has been created and has the correct status. + cr := &knappek_API_VERSION_._KIND_{} + err = k8sClient.Get(context.TODO(), req.NamespacedName, cr) + if err != nil { + t.Fatalf("get _KIND_LOWERCASE_: (%v)", err) + } + // + // TODO ... + // +} + +func TestDelete_KIND_LOWERCASE_(t *testing.T) { + // Set the logger to development mode for verbose logs. + logf.SetLogger(logf.ZapLogger(true)) + + // A MongoDBAtlasProject resource with metadata and spec. + mongodbatlasproject := testutil.CreateAtlasProject(projectName, groupID, namespace, organizationID) + + // A _KIND_LOWERCASE_ resource with metadata and spec. + _KIND_LOWERCASE_ := &knappek_API_VERSION_._KIND_{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + Finalizers: []string{"finalizer.knappek.com"}, + }, + Spec: knappek_API_VERSION_._KIND_Spec{ + ProjectName: projectName, + _KIND_RequestBody: knappek_API_VERSION_._KIND_RequestBody{ + // + // TODO + // + }, + }, + Status: knappek_API_VERSION_._KIND_Status{ + // + // TODO: some other read only values + // + _KIND_RequestBody: knappek_API_VERSION_._KIND_RequestBody{ + // + // TODO + // + }, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{ + _KIND_LOWERCASE_, + mongodbatlasproject, + } + + // Register operator types with the runtime scheme. + s := scheme.Scheme + s.AddKnownTypes(knappek_API_VERSION_.SchemeGroupVersion, _KIND_LOWERCASE_, mongodbatlasproject) + + // Create a fake k8s client to mock API calls. + k8sClient := fake.NewFakeClient(objs...) + // Create a fake atlas client to mock API calls. + // atlasClient, server := test.NewAtlasFakeClient(t) + httpClient, mux, server := testutil.Server() + defer server.Close() + atlasClient := ma.NewClient(httpClient) + + // Delete + mux.HandleFunc("/api/atlas/v1.0/groups/", func(w http.ResponseWriter, r *http.Request) { + testutil.AssertMethod(t, "DELETE", r) + fmt.Fprintf(w, `{}`) + }) + + // Create a Reconcile_KIND_ object with the scheme and fake client. + r := &Reconcile_KIND_{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: resourceName, + Namespace: namespace, + }, + } + res, err := r.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + assert.Equal(t, time.Second*20, res.RequeueAfter) + + // Check if the CR has been updated and has the correct status. + cr := &knappek_API_VERSION_._KIND_{} + err = k8sClient.Get(context.TODO(), req.NamespacedName, cr) + if err != nil { + t.Fatalf("get _KIND_LOWERCASE_: (%v)", err) + } + + httpClient2, mux2, server2 := testutil.Server() + defer server2.Close() + atlasClient2 := ma.NewClient(httpClient2) + // GET: Simulate a new reconcile where cluster has been deleted successfully + mux2.HandleFunc("/api/atlas/v1.0/groups/", func(w http.ResponseWriter, r *http.Request) { + testutil.AssertMethod(t, "GET", r) + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + }) + + // Create a Reconcile_KIND_ object with the scheme and fake client. + r := &Reconcile_KIND_{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient2, + reconciliationConfig: config.GetReconcilitationConfig(), + } + + res2, err := r2.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + assert.Equal(t, reconcile.Result{}, res2) + cr = &knappek_API_VERSION_._KIND_{} + err = k8sClient.Get(context.TODO(), req.NamespacedName, cr) + if err != nil { + t.Fatalf("get _KIND_LOWERCASE_: (%v)", err) + } + // verify that Finalizer has been removed + assert.Nil(t, cr.ObjectMeta.GetFinalizers()) +} + +func TestUpdate_KIND_LOWERCASE_(t *testing.T) { + // Set the logger to development mode for verbose logs. + logf.SetLogger(logf.ZapLogger(true)) + + // A MongoDBAtlasProject resource with metadata and spec. + mongodbatlasproject := testutil.CreateAtlasProject(projectName, groupID, namespace, organizationID) + + // updates + // + // TODO: some updates + // + + // A _KIND_LOWERCASE_ resource with metadata and spec. + _KIND_LOWERCASE_ := &knappek_API_VERSION_._KIND_{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Spec: knappek_API_VERSION_._KIND_Spec{ + ProjectName: projectName, + _KIND_RequestBody: knappek_API_VERSION_._KIND_RequestBody{ + // + // TODO + // + }, + }, + Status: knappek_API_VERSION_._KIND_Status{ + // + // TODO: some other read only values + // + _KIND_RequestBody: knappek_API_VERSION_._KIND_RequestBody{ + // + // TODO + // + }, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{ + _KIND_LOWERCASE_, + mongodbatlasproject, + } + + // Register operator types with the runtime scheme. + s := scheme.Scheme + s.AddKnownTypes(knappek_API_VERSION_.SchemeGroupVersion, _KIND_LOWERCASE_, mongodbatlasproject) + + // Create a fake k8s client to mock API calls. + k8sClient := fake.NewFakeClient(objs...) + // Create a fake atlas client to mock API calls. + // atlasClient, server := test.NewAtlasFakeClient(t) + httpClient, mux, server := testutil.Server() + defer server.Close() + atlasClient := ma.NewClient(httpClient) + // Construct Update API call + mux.HandleFunc("/api/atlas/v1.0/groups/", func(w http.ResponseWriter, r *http.Request) { + testutil.AssertMethod(t, "PATCH", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + // + // TODO + // + }`) + }) + // Create a Reconcile_KIND_ object with the scheme and fake client. + r := &Reconcile_KIND_{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: resourceName, + Namespace: namespace, + }, + } + res, err := r.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + assert.Equal(t, time.Second*120, res.RequeueAfter) + + // Check if the CR has been created and has the correct status. + cr := &knappek_API_VERSION_._KIND_{} + err = k8sClient.Get(context.TODO(), req.NamespacedName, cr) + if err != nil { + t.Fatalf("get _KIND_LOWERCASE_: (%v)", err) + } + // + // TODO: some assertions + // +} + +func TestNoUpdate_KIND_LOWERCASE_(t *testing.T) { + // Set the logger to development mode for verbose logs. + logf.SetLogger(logf.ZapLogger(true)) + + // A MongoDBAtlasProject resource with metadata and spec. + mongodbatlasproject := testutil.CreateAtlasProject(projectName, groupID, namespace, organizationID) + + // A _KIND_LOWERCASE_ resource with metadata and spec. This Spec contains only the bare minimum, other values + // will be filled with default values + _KIND_LOWERCASE_ := &knappek_API_VERSION_._KIND_{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Spec: knappek_API_VERSION_._KIND_Spec{ + ProjectName: projectName, + _KIND_RequestBody: knappek_API_VERSION_._KIND_RequestBody{ + // + // TODO: minimum requirements for the spec + // + }, + }, + Status: knappek_API_VERSION_._KIND_Status{ + // + // TODO: some other read only values + // + _KIND_RequestBody: knappek_API_VERSION_._KIND_RequestBody{ + // + // TODO + // + }, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{ + _KIND_LOWERCASE_, + mongodbatlasproject, + } + + // Register operator types with the runtime scheme. + s := scheme.Scheme + s.AddKnownTypes(knappek_API_VERSION_.SchemeGroupVersion, _KIND_LOWERCASE_, mongodbatlasproject) + + // Create a fake k8s client to mock API calls. + k8sClient := fake.NewFakeClient(objs...) + // Create a fake atlas client to mock API calls. + // atlasClient, server := test.NewAtlasFakeClient(t) + httpClient, mux, server := testutil.Server() + defer server.Close() + atlasClient := ma.NewClient(httpClient) + // Construct Update API call + mux.HandleFunc("/api/atlas/v1.0/groups/", func(w http.ResponseWriter, r *http.Request) { + testutil.AssertMethod(t, "GET", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + // + // TODO + // + }`) + }) + // Create a Reconcile_KIND_ object with the scheme and fake client. + r := &Reconcile_KIND_{ + client: k8sClient, + scheme: s, + atlasClient: atlasClient, + reconciliationConfig: config.GetReconcilitationConfig(), + } + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: resourceName, + Namespace: namespace, + }, + } + res, err := r.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + assert.Equal(t, time.Second*120, res.RequeueAfter) + + // Check if the CR has been created and has the correct status. + cr := &knappek_API_VERSION_._KIND_{} + err = k8sClient.Get(context.TODO(), req.NamespacedName, cr) + if err != nil { + t.Fatalf("get _KIND_LOWERCASE_: (%v)", err) + } + // + // TODO: assert that resource has not been updated + // +} diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index 015cce90..0060fe29 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -48,6 +48,7 @@ func TestMongoDBAtlas(t *testing.T) { MongoDBAtlasProject(t, ctx, f, namespace) MongoDBAtlasCluster(t, ctx, f, namespace) + MongoDBAtlasDatabaseUser(t, ctx, f, namespace) fmt.Println("Cleanup resources...") } diff --git a/test/e2e/mongodbatlasdatabaseuser_test.go b/test/e2e/mongodbatlasdatabaseuser_test.go new file mode 100644 index 00000000..c17a8b97 --- /dev/null +++ b/test/e2e/mongodbatlasdatabaseuser_test.go @@ -0,0 +1,72 @@ +package e2e + +import ( + goctx "context" + "fmt" + "testing" + "time" + + knappekv1alpha1 "github.com/Knappek/mongodbatlas-operator/pkg/apis/knappek/v1alpha1" + ma "github.com/akshaykarle/go-mongodbatlas/mongodbatlas" + + framework "github.com/operator-framework/operator-sdk/pkg/test" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" +) + +func MongoDBAtlasDatabaseUser(t *testing.T, ctx *framework.TestCtx, f *framework.Framework, namespace string) { + // create MongoDBAtlasProject custom resource + username := "e2etest-testuser" + exampleMongoDBAtlasDatabaseUser := &knappekv1alpha1.MongoDBAtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: username, + Namespace: namespace, + }, + Spec: knappekv1alpha1.MongoDBAtlasDatabaseUserSpec{ + ProjectName: atlasProjectName, + MongoDBAtlasDatabaseUserRequestBody: knappekv1alpha1.MongoDBAtlasDatabaseUserRequestBody{ + Password: "$upers€curep@ssword!", + DeleteAfterDate: "2100-01-01T00:00:00Z", + DatabaseName: "admin", + Roles: []ma.Role{ma.Role{DatabaseName: "e2etestdatabase", RoleName: "readWrite"}}, + }, + }, + } + err := f.Client.Create(goctx.TODO(), exampleMongoDBAtlasDatabaseUser, &framework.CleanupOptions{TestContext: ctx, Timeout: time.Second * 5, RetryInterval: time.Second * 1}) + if err != nil { + t.Fatal(err) + } + fmt.Printf("wait for creating the databaseUser: %v\n", exampleMongoDBAtlasDatabaseUser.ObjectMeta.Name) + err = waitForMongoDBAtlasDatabaseUser(t, f, exampleMongoDBAtlasDatabaseUser, "readWrite") + if err != nil { + t.Fatal(err) + } + fmt.Printf("databaseUser %v successfully created\n", exampleMongoDBAtlasDatabaseUser.ObjectMeta.Name) + + // update databaseUser + exampleMongoDBAtlasDatabaseUser.Spec.Roles[0].RoleName = "read" + err = f.Client.Update(goctx.TODO(), exampleMongoDBAtlasDatabaseUser) + if err != nil { + t.Fatal(err) + } + fmt.Printf("wait for updating the databaseUser: %v\n", exampleMongoDBAtlasDatabaseUser.ObjectMeta.Name) + err = waitForMongoDBAtlasDatabaseUser(t, f, exampleMongoDBAtlasDatabaseUser, "read") + if err != nil { + t.Fatal(err) + } + fmt.Printf("databaseUser %v successfully updated\n", exampleMongoDBAtlasDatabaseUser.ObjectMeta.Name) +} + +func waitForMongoDBAtlasDatabaseUser(t *testing.T, f *framework.Framework, p *knappekv1alpha1.MongoDBAtlasDatabaseUser, desiredState string) error { + retryInterval := time.Second * 5 + timeout := time.Second * 10 + err := wait.Poll(retryInterval, timeout, func() (done bool, err error) { + err = f.Client.Get(goctx.TODO(), types.NamespacedName{Name: p.Name, Namespace: p.Namespace}, p) + return isInDesiredState(t, err, p.Name, p.Kind, p.Status.Roles[0].RoleName, desiredState) + }) + if err != nil { + return err + } + return nil +}