Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rancher nodes to atlas firewall #15

Merged
merged 4 commits into from
Sep 11, 2023
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
5 changes: 4 additions & 1 deletion api/v1alpha1/mongodbcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions config/samples/airlock_v1alpha1_mongodbcluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

r0zbot marked this conversation as resolved.
Show resolved Hide resolved
---
apiVersion: v1
kind: Secret
Expand Down
139 changes: 139 additions & 0 deletions controllers/mongodbcluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}