From 9965879570cdc0f1a23f423fb5943c0081b34e33 Mon Sep 17 00:00:00 2001 From: r0zbot Date: Sat, 9 Sep 2023 06:19:31 -0300 Subject: [PATCH 1/4] Add rancher nodes to atlas firewall --- api/v1alpha1/mongodbcluster_types.go | 3 + ...ock.cloud.rocket.chat_mongodbclusters.yaml | 5 + .../airlock_v1alpha1_mongodbcluster.yaml | 3 + controllers/mongodbcluster_controller.go | 122 ++++++++++++++++++ 4 files changed, 133 insertions(+) diff --git a/api/v1alpha1/mongodbcluster_types.go b/api/v1alpha1/mongodbcluster_types.go index eaadc09..f428e5f 100644 --- a/api/v1alpha1/mongodbcluster_types.go +++ b/api/v1alpha1/mongodbcluster_types.go @@ -48,6 +48,9 @@ type MongoDBClusterSpec struct { // If this is set, Atlas API will be used instead of the regular mongo auth path. UseAtlasApi bool `json:"useAtlasApi,omitempty"` + + // If this is set, along with useAtlasApi, all the kubernetes nodes on the cluster will be added to the Atlas firewall, using the rke.cattle.io/external-ip annotation. + AllowOnAtlasFirewall bool `json:"allowOnAtlasFirewall,omitempty"` } // MongoDBClusterStatus defines the observed state of MongoDBCluster diff --git a/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml b/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml index 1fe71b8..81a93e9 100644 --- a/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml +++ b/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml @@ -41,6 +41,11 @@ spec: type: object spec: properties: + allowOnAtlasFirewall: + description: If this is set, all the kubernetes nodes on the cluster + will be added to the Atlas firewall, using rke.cattle.io/external-ip + annotation. + type: boolean connectionSecret: description: Secret in which Airlock will look for a ConnectionString or Atlas credentials, that will be used to connect to the cluster. diff --git a/config/samples/airlock_v1alpha1_mongodbcluster.yaml b/config/samples/airlock_v1alpha1_mongodbcluster.yaml index 11a5965..05073cd 100644 --- a/config/samples/airlock_v1alpha1_mongodbcluster.yaml +++ b/config/samples/airlock_v1alpha1_mongodbcluster.yaml @@ -35,6 +35,9 @@ spec: # Optional. Append this prefix to all default/generated usernames for this cluster. Will be ignored if "username" is already set on the access request. userNamePrefix: test-use1- + # Optional. If this is set, along with useAtlasApi, all the kubernetes nodes on the cluster will be added to the Atlas firewall, using the rke.cattle.io/external-ip annotation. + allowOnAtlasFirewall: true + --- apiVersion: v1 kind: Secret diff --git a/controllers/mongodbcluster_controller.go b/controllers/mongodbcluster_controller.go index 8ab78bd..e2de9c5 100644 --- a/controllers/mongodbcluster_controller.go +++ b/controllers/mongodbcluster_controller.go @@ -19,9 +19,12 @@ package controllers import ( "context" "fmt" + "net/http" + "strings" "time" "github.com/go-logr/logr" + "go.mongodb.org/atlas/mongodbatlas" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" @@ -120,6 +123,11 @@ func (r *MongoDBClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, utilerrors.NewAggregate([]error{err, r.Status().Update(ctx, mongodbClusterCR)}) } + + // Add nodes to Atlas firewall + if mongodbClusterCR.Spec.AllowOnAtlasFirewall { + r.reconcileAtlasFirewall(ctx, mongodbClusterCR, secret) + } } else { err = testMongoConnection(ctx, mongodbClusterCR, secret) if err != nil { @@ -196,6 +204,35 @@ func (r *MongoDBClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { handler.EnqueueRequestsFromMapFunc(r.findObjectsForSecret), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). + Watches( + &source.Kind{Type: &corev1.Node{}}, + handler.EnqueueRequestsFromMapFunc(func(node client.Object) []reconcile.Request { + mongodbClusterCR := &airlockv1alpha1.MongoDBClusterList{} + listOps := &client.ListOptions{ + Namespace: "", + } + + err := r.List(context.TODO(), mongodbClusterCR, listOps) + if err != nil { + return []reconcile.Request{} + } + + requests := make([]reconcile.Request, 0) + for _, item := range mongodbClusterCR.Items { + if item.Spec.AllowOnAtlasFirewall { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: item.GetName(), + Namespace: item.GetNamespace(), + }, + }) + } + } + + return requests + }), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). Complete(r) } @@ -332,3 +369,88 @@ func canCreateUsers(logger logr.Logger, roles primitive.A) bool { return false } + +func (r *MongoDBClusterReconciler) reconcileAtlasFirewall(ctx context.Context, mongodbClusterCR *airlockv1alpha1.MongoDBCluster, secret *corev1.Secret) error { + logger := log.FromContext(ctx) + + AIRLOCK_PREFIX := "Airlock-" + IP_ANNOTATION := "rke.cattle.io/external-ip" + + logger.Info("Reconciling atlas firewall for " + mongodbClusterCR.Name) + + client, atlasGroupID, err := getAtlasClientFromSecret(secret) + if err != nil { + logger.Error(err, "Couldn't get a client for Atlas") + return err + } + + // Get all nodes in the cluster + nodeList := &corev1.NodeList{} + err = r.List(ctx, nodeList) + if err != nil { + logger.Error(err, "Couldn't get nodes in the cluster") + return err + } + + // Get all nodes in the Atlas firewall + firewallList, _, err := client.ProjectIPAccessList.List(context.Background(), atlasGroupID, nil) + if err != nil { + logger.Error(err, "Couldn't get nodes in the Atlas firewall") + return err + } + + // Look for nodes in atlas firewall that don't match the current nodes + for _, entry := range firewallList.Results { + found := false + for _, node := range nodeList.Items { + externalIP := node.Annotations[IP_ANNOTATION] + + if externalIP == entry.IPAddress && AIRLOCK_PREFIX+node.Name == entry.Comment { + found = true + break + } + } + + // If the node has the airlock prefix but wasn't found locally, remove it from the Atlas firewall + if strings.HasPrefix(entry.Comment, AIRLOCK_PREFIX) && !found { + logger.Info("Removing node " + entry.Comment + " from the Atlas firewall") + _, err := client.ProjectIPAccessList.Delete(context.Background(), atlasGroupID, entry.IPAddress) + if err != nil { + logger.Error(err, "Couldn't remove node "+entry.Comment+" from the Atlas firewall") + return err + } + } + } + + // Add missing nodes to the Atlas firewall + entriesToAdd := []*mongodbatlas.ProjectIPAccessList{} + + for _, node := range nodeList.Items { + externalIP := node.Annotations[IP_ANNOTATION] + + // Check if node already exists in the firewall + found := false + for _, entry := range firewallList.Results { + if externalIP == entry.IPAddress && AIRLOCK_PREFIX+node.Name == entry.Comment { + found = true + break + } + } + + // If not, add it + if !found && externalIP != "" { + entriesToAdd = append(entriesToAdd, &mongodbatlas.ProjectIPAccessList{ + IPAddress: externalIP, + Comment: AIRLOCK_PREFIX + node.Name, + }) + logger.Info("Adding node " + node.Name + " to the Atlas firewall") + } + } + + _, response, err := client.ProjectIPAccessList.Create(context.Background(), atlasGroupID, entriesToAdd) + if err != nil || response.StatusCode != http.StatusCreated { + logger.Error(err, "Couldn't add nodes to the Atlas firewall") + return err + } + return nil +} From 2e9468f1a6895f6300a513344dedf167bdebdf51 Mon Sep 17 00:00:00 2001 From: r0zbot Date: Sat, 9 Sep 2023 06:28:07 -0300 Subject: [PATCH 2/4] Forgot some error checking (and also fix linting) --- api/v1alpha1/mongodbcluster_types.go | 2 +- controllers/mongodbcluster_controller.go | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/mongodbcluster_types.go b/api/v1alpha1/mongodbcluster_types.go index f428e5f..ca8cfc3 100644 --- a/api/v1alpha1/mongodbcluster_types.go +++ b/api/v1alpha1/mongodbcluster_types.go @@ -43,7 +43,7 @@ type MongoDBClusterSpec struct { // +kubebuilder:default=mongodb PrefixTemplate string `json:"prefixTemplate,omitempty"` - // Append this prefix to all default/generated usernames for this cluster. Will be overriden if "username" is specified. + // Append this prefix to all default/generated usernames for this cluster. Will be overridden if "username" is specified. UserNamePrefix string `json:"userNamePrefix,omitempty"` // If this is set, Atlas API will be used instead of the regular mongo auth path. diff --git a/controllers/mongodbcluster_controller.go b/controllers/mongodbcluster_controller.go index e2de9c5..396f502 100644 --- a/controllers/mongodbcluster_controller.go +++ b/controllers/mongodbcluster_controller.go @@ -126,7 +126,19 @@ func (r *MongoDBClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque // Add nodes to Atlas firewall if mongodbClusterCR.Spec.AllowOnAtlasFirewall { - r.reconcileAtlasFirewall(ctx, mongodbClusterCR, secret) + err = r.reconcileAtlasFirewall(ctx, mongodbClusterCR, secret) + if err != nil { + meta.SetStatusCondition(&mongodbClusterCR.Status.Conditions, + metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionFalse, + Reason: "AtlasFirewallFailed", + LastTransitionTime: metav1.NewTime(time.Now()), + Message: fmt.Sprintf("Failed to add nodes to atlas firewall: %s", err.Error()), + }) + + return ctrl.Result{}, utilerrors.NewAggregate([]error{err, r.Status().Update(ctx, mongodbClusterCR)}) + } } } else { err = testMongoConnection(ctx, mongodbClusterCR, secret) @@ -386,6 +398,7 @@ func (r *MongoDBClusterReconciler) reconcileAtlasFirewall(ctx context.Context, m // Get all nodes in the cluster nodeList := &corev1.NodeList{} + err = r.List(ctx, nodeList) if err != nil { logger.Error(err, "Couldn't get nodes in the cluster") @@ -402,6 +415,7 @@ func (r *MongoDBClusterReconciler) reconcileAtlasFirewall(ctx context.Context, m // Look for nodes in atlas firewall that don't match the current nodes for _, entry := range firewallList.Results { found := false + for _, node := range nodeList.Items { externalIP := node.Annotations[IP_ANNOTATION] @@ -414,6 +428,7 @@ func (r *MongoDBClusterReconciler) reconcileAtlasFirewall(ctx context.Context, m // If the node has the airlock prefix but wasn't found locally, remove it from the Atlas firewall if strings.HasPrefix(entry.Comment, AIRLOCK_PREFIX) && !found { logger.Info("Removing node " + entry.Comment + " from the Atlas firewall") + _, err := client.ProjectIPAccessList.Delete(context.Background(), atlasGroupID, entry.IPAddress) if err != nil { logger.Error(err, "Couldn't remove node "+entry.Comment+" from the Atlas firewall") @@ -430,6 +445,7 @@ func (r *MongoDBClusterReconciler) reconcileAtlasFirewall(ctx context.Context, m // Check if node already exists in the firewall found := false + for _, entry := range firewallList.Results { if externalIP == entry.IPAddress && AIRLOCK_PREFIX+node.Name == entry.Comment { found = true @@ -452,5 +468,6 @@ func (r *MongoDBClusterReconciler) reconcileAtlasFirewall(ctx context.Context, m logger.Error(err, "Couldn't add nodes to the Atlas firewall") return err } + return nil } From 4483ec3c65d78fe58e3a52fdf9d5cdecc61f8149 Mon Sep 17 00:00:00 2001 From: r0zbot Date: Mon, 11 Sep 2023 13:16:15 -0300 Subject: [PATCH 3/4] Update property name to atlasNodeIPAccessStrategy --- api/v1alpha1/mongodbcluster_types.go | 4 ++-- .../airlock.cloud.rocket.chat_mongodbclusters.yaml | 13 +++++++------ config/samples/airlock_v1alpha1_mongodbcluster.yaml | 4 ++-- controllers/mongodbcluster_controller.go | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/api/v1alpha1/mongodbcluster_types.go b/api/v1alpha1/mongodbcluster_types.go index ca8cfc3..6aa95c0 100644 --- a/api/v1alpha1/mongodbcluster_types.go +++ b/api/v1alpha1/mongodbcluster_types.go @@ -49,8 +49,8 @@ type MongoDBClusterSpec struct { // If this is set, Atlas API will be used instead of the regular mongo auth path. UseAtlasApi bool `json:"useAtlasApi,omitempty"` - // If this is set, along with useAtlasApi, all the kubernetes nodes on the cluster will be added to the Atlas firewall, using the rke.cattle.io/external-ip annotation. - AllowOnAtlasFirewall bool `json:"allowOnAtlasFirewall,omitempty"` + // If this is set, along with useAtlasApi, all the kubernetes nodes on the cluster will be added to the Atlas firewall. The only available value right now is "rancher-annotation", which uses the rke.cattle.io/external-ip annotation. + AtlasNodeIPAccessStrategy string `json:"atlasNodeIPAccessStrategy,omitempty"` } // MongoDBClusterStatus defines the observed state of MongoDBCluster diff --git a/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml b/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml index 81a93e9..532d626 100644 --- a/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml +++ b/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml @@ -41,11 +41,12 @@ spec: type: object spec: properties: - allowOnAtlasFirewall: - description: If this is set, all the kubernetes nodes on the cluster - will be added to the Atlas firewall, using rke.cattle.io/external-ip - annotation. - type: boolean + atlasNodeIPAccessStrategy: + description: If this is set, along with useAtlasApi, all the kubernetes + nodes on the cluster will be added to the Atlas firewall. The only + available value right now is "rancher-annotation", which uses the + rke.cattle.io/external-ip annotation. + type: string connectionSecret: description: Secret in which Airlock will look for a ConnectionString or Atlas credentials, that will be used to connect to the cluster. @@ -75,7 +76,7 @@ spec: type: boolean userNamePrefix: description: Append this prefix to all default/generated usernames - for this cluster. Will be overriden if "username" is specified. + for this cluster. Will be overridden if "username" is specified. type: string required: - connectionSecret diff --git a/config/samples/airlock_v1alpha1_mongodbcluster.yaml b/config/samples/airlock_v1alpha1_mongodbcluster.yaml index 05073cd..8feb9d0 100644 --- a/config/samples/airlock_v1alpha1_mongodbcluster.yaml +++ b/config/samples/airlock_v1alpha1_mongodbcluster.yaml @@ -35,8 +35,8 @@ spec: # Optional. Append this prefix to all default/generated usernames for this cluster. Will be ignored if "username" is already set on the access request. userNamePrefix: test-use1- - # Optional. If this is set, along with useAtlasApi, all the kubernetes nodes on the cluster will be added to the Atlas firewall, using the rke.cattle.io/external-ip annotation. - allowOnAtlasFirewall: true + # Optional. If this is set, along with useAtlasApi, all the kubernetes nodes on the cluster will be added to the Atlas firewall. The only available value right now is "rancher-annotation", which uses the rke.cattle.io/external-ip annotation. + atlasNodeIPAccessStrategy: rancher-annotation --- apiVersion: v1 diff --git a/controllers/mongodbcluster_controller.go b/controllers/mongodbcluster_controller.go index 396f502..10661e9 100644 --- a/controllers/mongodbcluster_controller.go +++ b/controllers/mongodbcluster_controller.go @@ -125,7 +125,7 @@ func (r *MongoDBClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque } // Add nodes to Atlas firewall - if mongodbClusterCR.Spec.AllowOnAtlasFirewall { + if mongodbClusterCR.Spec.AtlasNodeIPAccessStrategy == "rancher-annotation" { err = r.reconcileAtlasFirewall(ctx, mongodbClusterCR, secret) if err != nil { meta.SetStatusCondition(&mongodbClusterCR.Status.Conditions, @@ -231,7 +231,7 @@ func (r *MongoDBClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { requests := make([]reconcile.Request, 0) for _, item := range mongodbClusterCR.Items { - if item.Spec.AllowOnAtlasFirewall { + if item.Spec.AtlasNodeIPAccessStrategy != "" { requests = append(requests, reconcile.Request{ NamespacedName: types.NamespacedName{ Name: item.GetName(), From 53f2e0f5b5ba9ee5846dea9ee9c28ffcb15ca562 Mon Sep 17 00:00:00 2001 From: r0zbot Date: Mon, 11 Sep 2023 14:33:48 -0300 Subject: [PATCH 4/4] linter happy? --- api/v1alpha1/mongodbcluster_types.go | 2 +- config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml | 2 +- config/samples/airlock_v1alpha1_mongodbcluster.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/mongodbcluster_types.go b/api/v1alpha1/mongodbcluster_types.go index 6aa95c0..944e7bd 100644 --- a/api/v1alpha1/mongodbcluster_types.go +++ b/api/v1alpha1/mongodbcluster_types.go @@ -50,7 +50,7 @@ type MongoDBClusterSpec struct { UseAtlasApi bool `json:"useAtlasApi,omitempty"` // If this is set, along with useAtlasApi, all the kubernetes nodes on the cluster will be added to the Atlas firewall. The only available value right now is "rancher-annotation", which uses the rke.cattle.io/external-ip annotation. - AtlasNodeIPAccessStrategy string `json:"atlasNodeIPAccessStrategy,omitempty"` + AtlasNodeIPAccessStrategy string `json:"atlasNodeIpAccessStrategy,omitempty"` } // MongoDBClusterStatus defines the observed state of MongoDBCluster diff --git a/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml b/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml index 532d626..5a89405 100644 --- a/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml +++ b/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml @@ -41,7 +41,7 @@ spec: type: object spec: properties: - atlasNodeIPAccessStrategy: + atlasNodeIpAccessStrategy: description: If this is set, along with useAtlasApi, all the kubernetes nodes on the cluster will be added to the Atlas firewall. The only available value right now is "rancher-annotation", which uses the diff --git a/config/samples/airlock_v1alpha1_mongodbcluster.yaml b/config/samples/airlock_v1alpha1_mongodbcluster.yaml index 8feb9d0..7333503 100644 --- a/config/samples/airlock_v1alpha1_mongodbcluster.yaml +++ b/config/samples/airlock_v1alpha1_mongodbcluster.yaml @@ -36,7 +36,7 @@ spec: userNamePrefix: test-use1- # Optional. If this is set, along with useAtlasApi, all the kubernetes nodes on the cluster will be added to the Atlas firewall. The only available value right now is "rancher-annotation", which uses the rke.cattle.io/external-ip annotation. - atlasNodeIPAccessStrategy: rancher-annotation + atlasNodeIpAccessStrategy: rancher-annotation --- apiVersion: v1