diff --git a/api/v1alpha1/mongodbcluster_types.go b/api/v1alpha1/mongodbcluster_types.go index eaadc09..944e7bd 100644 --- a/api/v1alpha1/mongodbcluster_types.go +++ b/api/v1alpha1/mongodbcluster_types.go @@ -43,11 +43,14 @@ 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. 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"` } // 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..5a89405 100644 --- a/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml +++ b/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml @@ -41,6 +41,12 @@ spec: type: object spec: properties: + 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. @@ -70,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 11a5965..7333503 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. The only available value right now is "rancher-annotation", which uses the rke.cattle.io/external-ip annotation. + atlasNodeIpAccessStrategy: rancher-annotation + --- apiVersion: v1 kind: Secret diff --git a/controllers/mongodbcluster_controller.go b/controllers/mongodbcluster_controller.go index 8ab78bd..10661e9 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,23 @@ 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.AtlasNodeIPAccessStrategy == "rancher-annotation" { + 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) if err != nil { @@ -196,6 +216,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.AtlasNodeIPAccessStrategy != "" { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: item.GetName(), + Namespace: item.GetNamespace(), + }, + }) + } + } + + return requests + }), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). Complete(r) } @@ -332,3 +381,93 @@ 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 +}