Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"github.com/mongodb/mongodb-kubernetes-operator/pkg/controller"
"go.uber.org/zap"
"os"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when are we getting the commit hook for gofmt and goimports?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rodrigovalin is working on it now I believe!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am, doing it right now

"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
Expand All @@ -31,6 +30,13 @@ func main() {
if err != nil {
os.Exit(1)
}

// TODO: implement mechanism to specify required/optional environment variables
if _, agentImageSpecified := os.LookupEnv("AGENT_IMAGE"); !agentImageSpecified {
log.Error("required environment variable AGENT_IMAGE not found")
os.Exit(1)
}

// get watch namespace from environment variable
namespace, nsSpecified := os.LookupEnv("WATCH_NAMESPACE")
if !nsSpecified {
Expand All @@ -49,7 +55,7 @@ func main() {
mgr, err := manager.New(cfg, manager.Options{
Namespace: namespace,
})

if err != nil {
os.Exit(1)
}
Expand Down
2 changes: 1 addition & 1 deletion deploy/crds/mongodb.com_v1_mongodb_cr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ metadata:
name: example-mongodb
spec:
members: 3
kind: ReplicaSet
type: ReplicaSet
version: "4.0.6"
2 changes: 2 additions & 0 deletions deploy/operator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ spec:
fieldPath: metadata.name
- name: OPERATOR_NAME
value: "mongodb-kubernetes-operator"
- name: AGENT_IMAGE # The MongoDB Agent the operator will deploy to manage MongoDB deployments
value: REPLACE_IMAGE
66 changes: 47 additions & 19 deletions pkg/controller/mongodb/mongodb_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import (
"context"
"encoding/json"
"fmt"
"os"

mdbv1 "github.com/mongodb/mongodb-kubernetes-operator/pkg/apis/mongodb/v1"
"github.com/mongodb/mongodb-kubernetes-operator/pkg/automationconfig"
mdbClient "github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/client"
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/configmap"
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/resourcerequirements"
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/statefulset"
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -22,7 +26,9 @@ import (
)

const (
automationConfigKey = "automation-config"
automationConfigKey = "automation-config"
agentName = "mongodb-agent"
agentImageEnvVariable = "AGENT_IMAGE"
)

// Add creates a new MongoDB Controller and adds it to the Manager. The Manager will set fields on the Controller
Expand Down Expand Up @@ -92,32 +98,20 @@ func (r *ReplicaSetReconciler) Reconcile(request reconcile.Request) (reconcile.R
// TODO: Read current automation config version from config map

if err := r.ensureAutomationConfig(mdb); err != nil {
log.Errorf("failed creating config map: %s", err)
log.Warnf("failed creating config map: %s", err)
return reconcile.Result{}, err
}

// TODO: Create the service for the MDB resource

labels := map[string]string{
"dummy": "label",
sts, err := buildStatefulSet(mdb)
if err != nil {
log.Warnf("error building StatefulSet: %s", err)
return reconcile.Result{}, nil
}

sts, err := statefulset.NewBuilder().
SetPodTemplateSpec(corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{},
}).
SetNamespace(request.NamespacedName.Namespace).
SetName(request.NamespacedName.Name).
SetReplicas(mdb.Spec.Members).
SetLabels(labels).
SetMatchLabels(labels).
Build()

if err = r.client.CreateOrUpdate(&sts); err != nil {
log.Errorf("error creating/updating StatefulSet: %s", err)
log.Warnf("error creating/updating StatefulSet: %s", err)
return reconcile.Result{}, err
}

Expand Down Expand Up @@ -162,6 +156,40 @@ func buildAutomationConfigConfigMap(mdb mdbv1.MongoDB) (corev1.ConfigMap, error)
Build(), nil
}

// buildStatefulSet takes a MongoDB resource and converts it into
// the corresponding stateful set
func buildStatefulSet(mdb mdbv1.MongoDB) (appsv1.StatefulSet, error) {
labels := map[string]string{
"dummy": "label",
}
agentContainer := corev1.Container{
Name: agentName,
Image: os.Getenv(agentImageEnvVariable),
Resources: resourcerequirements.Defaults(),
Command: []string{"agent/mongodb-agent", "-cluster=/var/lib/automation/config/automation-config.json"},
}

podSpecTemplate := corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
agentContainer,
},
},
}

return statefulset.NewBuilder().
SetPodTemplateSpec(podSpecTemplate).
SetNamespace(mdb.Namespace).
SetName(mdb.Name).
SetReplicas(mdb.Spec.Members).
SetLabels(labels).
SetMatchLabels(labels).
Build()
}

func getDomain(service, namespace, clusterName string) string {
if clusterName == "" {
clusterName = "cluster.local"
Expand Down
43 changes: 37 additions & 6 deletions pkg/controller/mongodb/replicaset_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,39 @@ package mongodb

import (
"context"
"os"
"testing"

mdbv1 "github.com/mongodb/mongodb-kubernetes-operator/pkg/apis/mongodb/v1"
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/client"
"github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/resourcerequirements"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"testing"
)

func TestKubernetesResources_AreCreated(t *testing.T) {
// TODO: Create builder/yaml fixture of some type to construct MDB objects for unit tests
mdb := mdbv1.MongoDB{
func init() {
os.Setenv("AGENT_IMAGE", "agent-image")
}

func newTestReplicaSet() mdbv1.MongoDB {
return mdbv1.MongoDB{
ObjectMeta: metav1.ObjectMeta{
Name: "my-rs",
Namespace: "my-ns",
},
Spec: mdbv1.MongoDBSpec{},
Status: mdbv1.MongoDBStatus{},
Spec: mdbv1.MongoDBSpec{
Members: 3,
},
}
}

func TestKubernetesResources_AreCreated(t *testing.T) {
// TODO: Create builder/yaml fixture of some type to construct MDB objects for unit tests
mdb := newTestReplicaSet()

mgr := client.NewManager(&mdb)
r := newReconciler(mgr)
Expand All @@ -37,3 +50,21 @@ func TestKubernetesResources_AreCreated(t *testing.T) {
assert.Contains(t, cm.Data, automationConfigKey)
assert.NotEmpty(t, cm.Data[automationConfigKey])
}

func TestStatefulSet_IsCorrectlyConfigured(t *testing.T) {
mdb := newTestReplicaSet()
mgr := client.NewManager(&mdb)
r := newReconciler(mgr)
res, err := r.Reconcile(reconcile.Request{NamespacedName: types.NamespacedName{Namespace: mdb.Namespace, Name: mdb.Name}})
assertReconciliationSuccessful(t, res, err)

sts := appsv1.StatefulSet{}
err = mgr.GetClient().Get(context.TODO(), types.NamespacedName{Name: mdb.Name, Namespace: mdb.Namespace}, &sts)
assert.NoError(t, err)

agentContainer := sts.Spec.Template.Spec.Containers[0]
assert.Equal(t, agentName, agentContainer.Name)
assert.Equal(t, os.Getenv(agentImageEnvVariable), agentContainer.Image)

assert.Equal(t, resourcerequirements.Defaults(), agentContainer.Resources)
}
55 changes: 55 additions & 0 deletions pkg/kube/resourcerequirements/resource_requirements.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package resourcerequirements

import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)

const (
resourceMemory = "memory"
resourceCpu = "cpu"
)

// Defaults returns the default resource requirements for a container
func Defaults() corev1.ResourceRequirements {
// we can safely ignore the error as we are passing all valid values
req, _ := newDefaultRequirements()
return req
}

func newDefaultRequirements() (corev1.ResourceRequirements, error) {
return newRequirements("1.0", "500M", "0.5", "400M")
}

// newRequirements returns a new corev1.ResourceRequirements with the specified arguments, and an error
// which indicates if there was a problem parsing the input
func newRequirements(limitsCpu, limitsMemory, requestsCpu, requestsMemory string) (corev1.ResourceRequirements, error) {
limits, err := buildResourceList(limitsCpu, limitsMemory)
if err != nil {
return corev1.ResourceRequirements{}, err
}

requests, err := buildResourceList(requestsCpu, requestsMemory)
if err != nil {
return corev1.ResourceRequirements{}, err
}
return corev1.ResourceRequirements{
Limits: limits,
Requests: requests,
}, nil
}

func buildResourceList(cpu, memory string) (corev1.ResourceList, error) {
cpuQuantity, err := resource.ParseQuantity(cpu)
if err != nil {
return nil, err
}
memoryQuantity, err := resource.ParseQuantity(memory)
if err != nil {
return nil, err
}
return corev1.ResourceList{
resourceCpu: cpuQuantity,
resourceMemory: memoryQuantity,
}, nil
}
27 changes: 27 additions & 0 deletions pkg/kube/resourcerequirements/resource_requirements_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package resourcerequirements

import (
"testing"

"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/api/resource"
)

func TestNewResourceRequirements_GetsConstructedCorrectly(t *testing.T) {
requirements, err := newRequirements("0.5", "2.0", "500", "1000")
assert.NoError(t, err)
assert.Equal(t, resource.MustParse("0.5"), *requirements.Limits.Cpu())
assert.Equal(t, resource.MustParse("2.0"), *requirements.Limits.Memory())
assert.Equal(t, resource.MustParse("500"), *requirements.Requests.Cpu())
assert.Equal(t, resource.MustParse("1000"), *requirements.Requests.Memory())
}

func TestBadInput_ReturnsError(t *testing.T) {
_, err := newRequirements("BAD_INPUT", "2.0", "500", "1000")
assert.Error(t, err)
}

func TestDefaultValues_DontReturnError(t *testing.T) {
_, err := newDefaultRequirements()
assert.NoError(t, err, "default requirements should never result in an error")
}