Skip to content

Commit

Permalink
Implement database users api (#20)
Browse files Browse the repository at this point in the history
* implement creating/updating/deleting database users
* introduce codegen script that creates boilerplates for controller, controller unit test and e2e test
* improve code reusability
* improve readme visibility
* make it possible to configure the default reconciliation time
Knappek authored Oct 15, 2019
1 parent 30f6279 commit 091d458
Showing 36 changed files with 2,454 additions and 362 deletions.
1 change: 0 additions & 1 deletion .drone.yml
Original file line number Diff line number Diff line change
@@ -18,7 +18,6 @@ steps:
- apk add --update alpine-sdk
- make build
- make fmt
- make lint
- make test
when:
event:
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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

8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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"; \
125 changes: 14 additions & 111 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

<!-- vim-markdown-toc -->

@@ -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,114 +104,24 @@ 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

I am working on this project in my spare time, hence feature development and release cycles could be improved ;). Contributors are welcome!

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=<major.minor.patch>
```

This will kick the CI pipeline and create a new Github Release with the version tag `v<major.minor.patch>`.
More information how to contribute/develop can be found in the [docs](./docs/CONTRIBUTING.md).
96 changes: 96 additions & 0 deletions code-generation/controller-gen.sh
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions deploy/crds/knappek_v1alpha1_mongodbatlascluster_crd.yaml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions deploy/crds/knappek_v1alpha1_mongodbatlasdatabaseuser_cr.yaml
Original file line number Diff line number Diff line change
@@ -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"

90 changes: 90 additions & 0 deletions deploy/crds/knappek_v1alpha1_mongodbatlasdatabaseuser_crd.yaml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 6 additions & 6 deletions deploy/crds/knappek_v1alpha1_mongodbatlasproject_crd.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions deploy/operator-latest.yaml
Original file line number Diff line number Diff line change
@@ -38,3 +38,5 @@ spec:
name: example-monogdb-atlas-project
- name: ATLAS_PUBLIC_KEY
value: toppaljd
- name: RECONCILIATION_TIME
value: "120"
2 changes: 2 additions & 0 deletions deploy/operator.yaml
Original file line number Diff line number Diff line change
@@ -38,3 +38,5 @@ spec:
name: example-monogdb-atlas-project
- name: ATLAS_PUBLIC_KEY
value: toppaljd
- name: RECONCILIATION_TIME
value: "120"
1 change: 1 addition & 0 deletions deploy/role.yaml
Original file line number Diff line number Diff line change
@@ -51,5 +51,6 @@ rules:
resources:
- '*'
- mongodbatlasclusters
- mongodbatlasdatabaseusers
verbs:
- '*'
132 changes: 132 additions & 0 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Contributing


<!-- vim-markdown-toc GFM -->

* [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)

<!-- vim-markdown-toc -->

## 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=<major.minor.patch>
```

This will kick the CI pipeline and create a new Github Release with the version tag `v<major.minor.patch>`.

6 changes: 6 additions & 0 deletions pkg/apis/knappek/group.go
Original file line number Diff line number Diff line change
@@ -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
50 changes: 16 additions & 34 deletions pkg/apis/knappek/v1alpha1/mongodbatlascluster_types.go
Original file line number Diff line number Diff line change
@@ -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 {
90 changes: 90 additions & 0 deletions pkg/apis/knappek/v1alpha1/mongodbatlasdatabaseuser_types.go
Original file line number Diff line number Diff line change
@@ -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{})
}
4 changes: 2 additions & 2 deletions pkg/apis/knappek/v1alpha1/mongodbatlasproject_types.go
Original file line number Diff line number Diff line change
@@ -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
120 changes: 120 additions & 0 deletions pkg/apis/knappek/v1alpha1/zz_generated.deepcopy.go
210 changes: 178 additions & 32 deletions pkg/apis/knappek/v1alpha1/zz_generated.openapi.go
Original file line number Diff line number Diff line change
@@ -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"},
}
}

26 changes: 26 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 10 additions & 0 deletions pkg/controller/add_mongodbatlasdatabaseuser.go
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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, &params)
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
Original file line number Diff line number Diff line change
@@ -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,15 +172,15 @@ 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":{
"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+`",
@@ -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{}
Original file line number Diff line number Diff line change
@@ -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, &params)
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, &params)
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
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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,24 +108,21 @@ 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 {
return reconcile.Result{}, err
}
// 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
}

Original file line number Diff line number Diff line change
@@ -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())
}
22 changes: 22 additions & 0 deletions pkg/controller/test/util.go
Original file line number Diff line number Diff line change
@@ -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,
},
}
}
21 changes: 21 additions & 0 deletions pkg/util/util.go
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 11 additions & 0 deletions templates/add_kind.go.tmpl
Original file line number Diff line number Diff line change
@@ -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)
}
71 changes: 71 additions & 0 deletions templates/e2e/kind_test.go.tmpl
Original file line number Diff line number Diff line change
@@ -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
}
277 changes: 277 additions & 0 deletions templates/kind_controller.go.tmpl
Original file line number Diff line number Diff line change
@@ -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, &params)
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, &params)
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
}
Loading

0 comments on commit 091d458

Please sign in to comment.