-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
36 changed files
with
2,454 additions
and
362 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
deploy/crds/knappek_v1alpha1_mongodbatlasdatabaseuser_cr.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
90
deploy/crds/knappek_v1alpha1_mongodbatlasdatabaseuser_crd.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,5 +51,6 @@ rules: | |
resources: | ||
- '*' | ||
- mongodbatlasclusters | ||
- mongodbatlasdatabaseusers | ||
verbs: | ||
- '*' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>`. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
pkg/apis/knappek/v1alpha1/mongodbatlasdatabaseuser_types.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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{}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
275 changes: 275 additions & 0 deletions
275
pkg/controller/mongodbatlasdatabaseuser/mongodbatlasdatabaseuser_controller.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, ¶ms) | ||
if err != nil { | ||
return fmt.Errorf("(%v) Error creating DatabaseUser %v: %s", resp.StatusCode, name, err) | ||
} | ||
if resp.StatusCode == http.StatusCreated { | ||
reqLogger.Info("DatabaseUser created.") | ||
return updateCRStatus(reqLogger, cr, c) | ||
} | ||
return fmt.Errorf("(%v) Error creating DatabaseUser %s: %s", resp.StatusCode, name, err) | ||
} | ||
|
||
func updateMongoDBAtlasDatabaseUser(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappekv1alpha1.MongoDBAtlasDatabaseUser, ap *knappekv1alpha1.MongoDBAtlasProject) error { | ||
groupID := ap.Status.ID | ||
name := cr.Name | ||
params := getDatabaseUserParams(cr) | ||
c, resp, err := atlasClient.DatabaseUsers.Update(groupID, name, ¶ms) | ||
if err != nil { | ||
return fmt.Errorf("Error updating DatabaseUser %v: %s", name, err) | ||
} | ||
if resp.StatusCode == http.StatusOK { | ||
reqLogger.Info("DatabaseUser updated.") | ||
return updateCRStatus(reqLogger, cr, c) | ||
} | ||
return fmt.Errorf("(%v) Error updating DatabaseUser %s: %s", resp.StatusCode, name, err) | ||
} | ||
|
||
func deleteMongoDBAtlasDatabaseUser(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappekv1alpha1.MongoDBAtlasDatabaseUser) error { | ||
groupID := cr.Status.GroupID | ||
name := cr.Name | ||
// cluster exists and can be deleted | ||
resp, err := atlasClient.DatabaseUsers.Delete(groupID, name) | ||
if err != nil { | ||
if resp.StatusCode == http.StatusNotFound { | ||
reqLogger.Info("DatabaseUser does not exist in Atlas. Deleting CR.") | ||
// Update finalizer to allow delete CR | ||
cr.SetFinalizers(nil) | ||
// CR can be deleted - Requeue | ||
return nil | ||
} | ||
return fmt.Errorf("(%v) Error deleting DatabaseUser %s: %s", resp.StatusCode, name, err) | ||
} | ||
// Update finalizer to allow delete CR | ||
cr.SetFinalizers(nil) | ||
reqLogger.Info("DatabaseUser deleted.") | ||
return nil | ||
} | ||
|
||
func getMongoDBAtlasDatabaseUser(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappekv1alpha1.MongoDBAtlasDatabaseUser) error { | ||
groupID := cr.Status.GroupID | ||
name := cr.Name | ||
c, resp, err := atlasClient.DatabaseUsers.Get(groupID, name) | ||
if err != nil { | ||
return fmt.Errorf("(%v) Error fetching DatabaseUser information %s: %s", resp.StatusCode, name, err) | ||
} | ||
err = updateCRStatus(reqLogger, cr, c) | ||
if err != nil { | ||
return fmt.Errorf("Error updating DatabaseUser CR Status: %s", err) | ||
} | ||
return nil | ||
} | ||
|
||
func getDatabaseUserParams(cr *knappekv1alpha1.MongoDBAtlasDatabaseUser) ma.DatabaseUser { | ||
return ma.DatabaseUser{ | ||
Username: cr.Name, | ||
Password: cr.Spec.Password, | ||
DatabaseName: "admin", | ||
Roles: cr.Spec.Roles, | ||
} | ||
} | ||
|
||
func updateCRStatus(reqLogger logr.Logger, cr *knappekv1alpha1.MongoDBAtlasDatabaseUser, c *ma.DatabaseUser) error { | ||
// update status field in CR | ||
cr.Status.Username = c.Username | ||
cr.Status.GroupID = c.GroupID | ||
cr.Status.DatabaseName = c.DatabaseName | ||
cr.Status.DeleteAfterDate = c.DeleteAfterDate | ||
cr.Status.Roles = c.Roles | ||
return nil | ||
} | ||
|
||
func (r *ReconcileMongoDBAtlasDatabaseUser) addFinalizer(reqLogger logr.Logger, cr *knappekv1alpha1.MongoDBAtlasDatabaseUser) error { | ||
if len(cr.GetFinalizers()) < 1 && cr.GetDeletionTimestamp() == nil { | ||
cr.SetFinalizers([]string{"finalizer.knappek.com"}) | ||
|
||
// Update CR | ||
err := r.client.Update(context.TODO(), cr) | ||
if err != nil { | ||
reqLogger.Error(err, "Failed to update DatabaseUser with finalizer") | ||
return err | ||
} | ||
} | ||
return nil | ||
} |
328 changes: 328 additions & 0 deletions
328
pkg/controller/mongodbatlasdatabaseuser/mongodbatlasdatabaseuser_controller_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, ¶ms) | ||
if err != nil { | ||
return fmt.Errorf("Error creating _KIND_SHORT_ %v: %s", name, err) | ||
} | ||
if resp.StatusCode == http.StatusOK { | ||
reqLogger.Info("_KIND_SHORT_ created.") | ||
return updateCRStatus(reqLogger, cr, c) | ||
} | ||
return fmt.Errorf("(%v) Error creating _KIND_SHORT_ %s: %s", resp.StatusCode, name, err) | ||
} | ||
|
||
func update_KIND_(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappek_API_VERSION_._KIND_, ap *knappek_API_VERSION_.MongoDBAtlasProject) error { | ||
groupID := ap.Status.ID | ||
name := cr.Name | ||
params := get_KIND_SHORT_Params(cr) | ||
c, resp, err := atlasClient._KIND_SHORT_s.Update(groupID, name, ¶ms) | ||
if err != nil { | ||
return fmt.Errorf("Error updating _KIND_SHORT_ %v: %s", name, err) | ||
} | ||
if resp.StatusCode == http.StatusOK { | ||
reqLogger.Info("_KIND_SHORT_ updated.") | ||
return updateCRStatus(reqLogger, cr, c) | ||
} | ||
return fmt.Errorf("(%v) Error updating _KIND_SHORT_ %s: %s", resp.StatusCode, name, err) | ||
} | ||
|
||
func delete_KIND_(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappek_API_VERSION_._KIND_) error { | ||
groupID := cr.Status.GroupID | ||
name := cr.Name | ||
// cluster exists and can be deleted | ||
resp, err := atlasClient._KIND_SHORT_s.Delete(groupID, name) | ||
if err != nil { | ||
if resp.StatusCode == http.StatusNotFound { | ||
reqLogger.Info("_KIND_SHORT_ does not exist in Atlas. Deleting CR.") | ||
// Update finalizer to allow delete CR | ||
cr.SetFinalizers(nil) | ||
// CR can be deleted - Requeue | ||
return nil | ||
} | ||
return fmt.Errorf("(%v) Error deleting _KIND_SHORT_ %s: %s", resp.StatusCode, name, err) | ||
} | ||
if resp.StatusCode == http.StatusOK { | ||
// Update finalizer to allow delete CR | ||
cr.SetFinalizers(nil) | ||
reqLogger.Info("_KIND_SHORT_ deleted.") | ||
return nil | ||
} | ||
return fmt.Errorf("(%v) Error deleting _KIND_SHORT_ %s: %s", resp.StatusCode, name, err) | ||
} | ||
|
||
func get_KIND_(reqLogger logr.Logger, atlasClient *ma.Client, cr *knappek_API_VERSION_._KIND_) error { | ||
groupID := cr.Status.GroupID | ||
name := cr.Name | ||
c, resp, err := atlasClient._KIND_SHORT_s.Get(groupID, name) | ||
if err != nil { | ||
return fmt.Errorf("(%v) Error fetching _KIND_SHORT_ information %s: %s", resp.StatusCode, name, err) | ||
} | ||
err = updateCRStatus(reqLogger, cr, c) | ||
if err != nil { | ||
return fmt.Errorf("Error updating _KIND_SHORT_ CR Status: %s", err) | ||
} | ||
return nil | ||
} | ||
|
||
func get_KIND_SHORT_Params(cr *knappek_API_VERSION_._KIND_) ma._KIND_SHORT_ { | ||
return ma._KIND_SHORT_{ | ||
// | ||
// TODO | ||
// | ||
} | ||
} | ||
|
||
func updateCRStatus(reqLogger logr.Logger, cr *knappek_API_VERSION_._KIND_, c *ma._KIND_SHORT_) error { | ||
// update status field in CR | ||
cr.Status.ID = c.ID | ||
cr.Status.GroupID = c.GroupID | ||
cr.Status.Name = c.Name | ||
// | ||
// TODO | ||
// | ||
return nil | ||
} | ||
|
||
func (r *Reconcile_KIND_) addFinalizer(reqLogger logr.Logger, cr *knappek_API_VERSION_._KIND_) error { | ||
if len(cr.GetFinalizers()) < 1 && cr.GetDeletionTimestamp() == nil { | ||
cr.SetFinalizers([]string{"finalizer.knappek.com"}) | ||
|
||
// Update CR | ||
err := r.client.Update(context.TODO(), cr) | ||
if err != nil { | ||
reqLogger.Error(err, "Failed to update _KIND_SHORT_ with finalizer") | ||
return err | ||
} | ||
} | ||
return nil | ||
} |
Oops, something went wrong.