diff --git a/cmd/manager/main.go b/cmd/manager/main.go index da2a1e39d..026f9ea89 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -6,7 +6,6 @@ import ( "github.com/mongodb/mongodb-kubernetes-operator/pkg/controller" "go.uber.org/zap" "os" - "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/manager/signals" @@ -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 { @@ -49,7 +55,7 @@ func main() { mgr, err := manager.New(cfg, manager.Options{ Namespace: namespace, }) - + if err != nil { os.Exit(1) } diff --git a/deploy/crds/mongodb.com_v1_mongodb_cr.yaml b/deploy/crds/mongodb.com_v1_mongodb_cr.yaml index 7a8e0cb3e..3464de36a 100644 --- a/deploy/crds/mongodb.com_v1_mongodb_cr.yaml +++ b/deploy/crds/mongodb.com_v1_mongodb_cr.yaml @@ -4,5 +4,5 @@ metadata: name: example-mongodb spec: members: 3 - kind: ReplicaSet + type: ReplicaSet version: "4.0.6" diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 89c06576e..bfa3b3f85 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -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 diff --git a/pkg/controller/mongodb/mongodb_controller.go b/pkg/controller/mongodb/mongodb_controller.go index 37ef5fbde..3425ab80b 100644 --- a/pkg/controller/mongodb/mongodb_controller.go +++ b/pkg/controller/mongodb/mongodb_controller.go @@ -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" @@ -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 @@ -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 } @@ -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" diff --git a/pkg/controller/mongodb/replicaset_controller_test.go b/pkg/controller/mongodb/replicaset_controller_test.go index 74502b0ea..62852a3be 100644 --- a/pkg/controller/mongodb/replicaset_controller_test.go +++ b/pkg/controller/mongodb/replicaset_controller_test.go @@ -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) @@ -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) +} diff --git a/pkg/kube/resourcerequirements/resource_requirements.go b/pkg/kube/resourcerequirements/resource_requirements.go new file mode 100644 index 000000000..36558e871 --- /dev/null +++ b/pkg/kube/resourcerequirements/resource_requirements.go @@ -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 +} diff --git a/pkg/kube/resourcerequirements/resource_requirements_test.go b/pkg/kube/resourcerequirements/resource_requirements_test.go new file mode 100644 index 000000000..338b46073 --- /dev/null +++ b/pkg/kube/resourcerequirements/resource_requirements_test.go @@ -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") +}