diff --git a/api/v1alpha1/mongodbcluster_types.go b/api/v1alpha1/mongodbcluster_types.go index a818230..e6d95bd 100644 --- a/api/v1alpha1/mongodbcluster_types.go +++ b/api/v1alpha1/mongodbcluster_types.go @@ -32,11 +32,13 @@ type MongoDBClusterSpec struct { ConnectionSecretNamespace string `json:"connectionSecretNamespace,omitempty"` // The host with port that clients will receive when requesting credentials. - // +kubebuilder:validation:Required - HostTemplate string `json:"hostTemplate"` // Obs: no omitempty here to make it required. (the annotation above refuses to work on this particular field for some reason) + // If not provided, useAtlasApi and atlasClusterName must be provided. + // +kubebuilder:validation:Optional + HostTemplate string `json:"hostTemplate,omitempty"` // Extra connection string parameters that will be added to the connection string. - // +kubebuilder:default=?replicaSet=rs01 + // If useAtlasApi and atlasClusterName is provided, this will be dynamically populated/updated + // +kubebuilder:validation:Optional OptionsTemplate string `json:"optionsTemplate,omitempty"` // The prefix used when building the connection string. Defaults to "mongodb" @@ -49,6 +51,11 @@ type MongoDBClusterSpec struct { // If this is set, Atlas API will be used instead of the regular mongo auth path. UseAtlasApi bool `json:"useAtlasApi,omitempty"` + // The name of the Atlas cluster. + // If this is provided, PrefixTemplate, HostTemplate CAN be omitted. Airlock will use the Atlas API to get the details and update the CR. + // +kubebuilder:validation:Optional + AtlasClusterName string `json:"atlasClusterName,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"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 56179a6..a5fb57c 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1,5 +1,4 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* Copyright 2022. diff --git a/config/crd/bases/airlock.cloud.rocket.chat_mongodbaccessrequests.yaml b/config/crd/bases/airlock.cloud.rocket.chat_mongodbaccessrequests.yaml index 4145e26..7446320 100644 --- a/config/crd/bases/airlock.cloud.rocket.chat_mongodbaccessrequests.yaml +++ b/config/crd/bases/airlock.cloud.rocket.chat_mongodbaccessrequests.yaml @@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.19.0 name: mongodbaccessrequests.airlock.cloud.rocket.chat spec: group: airlock.cloud.rocket.chat @@ -32,14 +31,19 @@ spec: API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -69,43 +73,35 @@ spec: conditions: description: Conditions is the list of status condition updates items: - description: "Condition contains details for one aspect of the current - state of this API Resource. --- This struct is intended for direct - use as an array at the field path .status.conditions. For example, - \n type FooStatus struct{ // Represents the observations of a - foo's current state. // Known .status.conditions.type are: \"Available\", - \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge - // +listType=map // +listMapKey=type Conditions []metav1.Condition - `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" - protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + description: Condition contains details for one aspect of the current + state of this API Resource. properties: lastTransitionTime: - description: lastTransitionTime is the last time the condition - transitioned from one status to another. This should be when - the underlying condition changed. If that is not known, then - using the time when the API field changed is acceptable. + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: - description: message is a human readable message indicating - details about the transition. This may be an empty string. + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. maxLength: 32768 type: string observedGeneration: - description: observedGeneration represents the .metadata.generation - that the condition was set based upon. For instance, if .metadata.generation - is currently 12, but the .status.conditions[x].observedGeneration - is 9, the condition is out of date with respect to the current - state of the instance. + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: - description: reason contains a programmatic identifier indicating - the reason for the condition's last transition. Producers - of specific condition types may define expected values and - meanings for this field, and whether the values are considered - a guaranteed API. The value should be a CamelCase string. + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 @@ -120,10 +116,6 @@ spec: type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. - --- Many .condition.type values are consistent across resources - like Available, but because arbitrary conditions can be useful - (see .node.status.conditions), the ability to deconflict is - important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string diff --git a/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml b/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml index 2df808b..f9de651 100644 --- a/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml +++ b/config/crd/bases/airlock.cloud.rocket.chat_mongodbclusters.yaml @@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.19.0 name: mongodbclusters.airlock.cloud.rocket.chat spec: group: airlock.cloud.rocket.chat @@ -28,19 +27,29 @@ spec: description: MongoDBCluster is the Schema for the mongodbclusters API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: properties: + atlasClusterName: + description: |- + The name of the Atlas cluster. + If this is provided, PrefixTemplate, HostTemplate CAN be omitted. Airlock will use the Atlas API to get the details and update the CR. + type: string 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 @@ -51,10 +60,9 @@ spec: properties: enabled: default: false - description: If this is set, the cluster will be enabled for scheduled - autoscaling. The way it works is that the cluster will be scaled - up to the high tier at the specified time, and scaled down to - the lowTier at the specified time. + description: |- + If this is set, the cluster will be enabled for scheduled autoscaling. + The way it works is that the cluster will be scaled up to the high tier at the specified time, and scaled down to the lowTier at the specified time. type: boolean highTier: default: M50 @@ -105,13 +113,14 @@ spec: default: airlock-system type: string hostTemplate: - description: The host with port that clients will receive when requesting - credentials. + description: |- + The host with port that clients will receive when requesting credentials. + If not provided, useAtlasApi and atlasClusterName must be provided. type: string optionsTemplate: - default: ?replicaSet=rs01 - description: Extra connection string parameters that will be added - to the connection string. + description: |- + Extra connection string parameters that will be added to the connection string. + If useAtlasApi and atlasClusterName is provided, this will be dynamically populated/updated type: string prefixTemplate: default: mongodb @@ -128,50 +137,41 @@ spec: type: string required: - connectionSecret - - hostTemplate type: object status: description: MongoDBClusterStatus defines the observed state of MongoDBCluster properties: conditions: items: - description: "Condition contains details for one aspect of the current - state of this API Resource. --- This struct is intended for direct - use as an array at the field path .status.conditions. For example, - \n type FooStatus struct{ // Represents the observations of a - foo's current state. // Known .status.conditions.type are: \"Available\", - \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge - // +listType=map // +listMapKey=type Conditions []metav1.Condition - `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" - protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + description: Condition contains details for one aspect of the current + state of this API Resource. properties: lastTransitionTime: - description: lastTransitionTime is the last time the condition - transitioned from one status to another. This should be when - the underlying condition changed. If that is not known, then - using the time when the API field changed is acceptable. + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: - description: message is a human readable message indicating - details about the transition. This may be an empty string. + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. maxLength: 32768 type: string observedGeneration: - description: observedGeneration represents the .metadata.generation - that the condition was set based upon. For instance, if .metadata.generation - is currently 12, but the .status.conditions[x].observedGeneration - is 9, the condition is out of date with respect to the current - state of the instance. + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: - description: reason contains a programmatic identifier indicating - the reason for the condition's last transition. Producers - of specific condition types may define expected values and - meanings for this field, and whether the values are considered - a guaranteed API. The value should be a CamelCase string. + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 @@ -186,10 +186,6 @@ spec: type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. - --- Many .condition.type values are consistent across resources - like Available, but because arbitrary conditions can be useful - (see .node.status.conditions), the ability to deconflict is - important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index e50801c..af1eda7 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -2,7 +2,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: manager-role rules: - apiGroups: @@ -38,31 +37,6 @@ rules: - airlock.cloud.rocket.chat resources: - mongodbaccessrequests - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - airlock.cloud.rocket.chat - resources: - - mongodbaccessrequests/finalizers - verbs: - - update -- apiGroups: - - airlock.cloud.rocket.chat - resources: - - mongodbaccessrequests/status - verbs: - - get - - patch - - update -- apiGroups: - - airlock.cloud.rocket.chat - resources: - mongodbclusters verbs: - create @@ -75,12 +49,14 @@ rules: - apiGroups: - airlock.cloud.rocket.chat resources: + - mongodbaccessrequests/finalizers - mongodbclusters/finalizers verbs: - update - apiGroups: - airlock.cloud.rocket.chat resources: + - mongodbaccessrequests/status - mongodbclusters/status verbs: - get diff --git a/controllers/atlas_connection.go b/controllers/atlas_connection.go new file mode 100644 index 0000000..5c04aa3 --- /dev/null +++ b/controllers/atlas_connection.go @@ -0,0 +1,240 @@ +package controllers + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/go-logr/logr" + "go.mongodb.org/mongo-driver/x/mongo/driver/connstring" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/log" + + airlockv1alpha1 "github.com/RocketChat/airlock/api/v1alpha1" +) + +type connectionStringParts struct { + Prefix string + Hosts string + Options string + ReplicaSet string +} + +func parseConnectionString(uri string) (connectionStringParts, error) { + cs, err := connstring.Parse(uri) + if err != nil { + return connectionStringParts{}, fmt.Errorf("parse connection string: %w", err) + } + + parts := connectionStringParts{ + Prefix: cs.Scheme, + Hosts: strings.Join(cs.Hosts, ","), + ReplicaSet: cs.ReplicaSet, + } + + if parsed, err := url.Parse(uri); err == nil && parsed.RawQuery != "" { + parts.Options = "?" + parsed.RawQuery + } + + return parts, nil +} + +func effectivePrefix(prefixTemplate string) string { + if prefixTemplate == "" { + return connstring.SchemeMongoDB + } + + return prefixTemplate +} + +func selectedConnectionParts(srvParts, stdParts connectionStringParts, prefix string) (connectionStringParts, error) { + switch prefix { + case connstring.SchemeMongoDBSRV: + if srvParts.Hosts == "" { + return connectionStringParts{}, fmt.Errorf("Atlas cluster has no SRV connection string") + } + + return srvParts, nil + case connstring.SchemeMongoDB: + if stdParts.Hosts == "" { + return connectionStringParts{}, fmt.Errorf("Atlas cluster has no standard connection string") + } + + return stdParts, nil + default: + return connectionStringParts{}, fmt.Errorf("invalid prefix %q", prefix) + } +} + +func needsAtlasConnectionPopulate(spec airlockv1alpha1.MongoDBClusterSpec) bool { + if spec.AtlasClusterName == "" { + return false + } + + return spec.HostTemplate == "" || spec.OptionsTemplate == "" || spec.PrefixTemplate == "" +} + +func hostTemplateMatches(hostTemplate, atlasHosts string) bool { + if hostTemplate == atlasHosts { + return true + } + + return strings.Contains(atlasHosts, hostTemplate) +} + +func parseQueryParams(options string) map[string]string { + params := map[string]string{} + options = strings.TrimPrefix(options, "?") + if options == "" { + return params + } + + for _, pair := range strings.Split(options, "&") { + if pair == "" { + continue + } + + key, value, _ := strings.Cut(pair, "=") + params[key] = value + } + + return params +} + +func logOptionsDiff(logger logr.Logger, clusterName, userOptions, atlasOptions string) { + userParams := parseQueryParams(userOptions) + atlasParams := parseQueryParams(atlasOptions) + + for key, atlasValue := range atlasParams { + userValue, ok := userParams[key] + if !ok { + logger.Info("optionsTemplate differs from Atlas: parameter missing in CR", + "cluster", clusterName, "parameter", key, "atlasValue", atlasValue) + continue + } + + if userValue != atlasValue { + logger.Info("optionsTemplate differs from Atlas: parameter value mismatch", + "cluster", clusterName, "parameter", key, "crValue", userValue, "atlasValue", atlasValue) + } + } + + for key, userValue := range userParams { + if _, ok := atlasParams[key]; !ok { + logger.Info("optionsTemplate differs from Atlas: extra parameter in CR", + "cluster", clusterName, "parameter", key, "crValue", userValue) + } + } +} + +func (r *MongoDBClusterReconciler) reconcileAtlasClusterConnectionDetails(ctx context.Context, cr *airlockv1alpha1.MongoDBCluster, secret *corev1.Secret) (bool, error) { + logger := log.FromContext(ctx) + + legacyClient, groupID, err := getAtlasClientFromSecret(secret) + if err != nil { + return false, err + } + + clusterName, err := resolveAtlasClusterName(ctx, cr.Spec, legacyClient, groupID) + if err != nil { + return false, err + } + + adminClient, _, err := getAtlasAdminClientFromSecret(secret) + if err != nil { + return false, err + } + + cluster, resp, err := adminClient.ClustersApi.GetCluster(ctx, groupID, clusterName).Execute() + if err != nil { + return false, fmt.Errorf("fetch Atlas cluster %q: %w", clusterName, err) + } + + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("fetch Atlas cluster %q: HTTP %s", clusterName, resp.Status) + } + + var srvParts, stdParts connectionStringParts + + if cluster.ConnectionStrings != nil { + if cluster.ConnectionStrings.StandardSrv != nil && *cluster.ConnectionStrings.StandardSrv != "" { + srvParts, err = parseConnectionString(*cluster.ConnectionStrings.StandardSrv) + if err != nil { + return false, fmt.Errorf("parse SRV connection string: %w", err) + } + } + + if cluster.ConnectionStrings.Standard != nil && *cluster.ConnectionStrings.Standard != "" { + stdParts, err = parseConnectionString(*cluster.ConnectionStrings.Standard) + if err != nil { + return false, fmt.Errorf("parse standard connection string: %w", err) + } + } + } + + if needsAtlasConnectionPopulate(cr.Spec) { + prefix := cr.Spec.PrefixTemplate + if prefix == "" { + prefix = connstring.SchemeMongoDBSRV + } + + parts, err := selectedConnectionParts(srvParts, stdParts, prefix) + if err != nil { + return false, err + } + + updated := false + + if cr.Spec.HostTemplate == "" { + cr.Spec.HostTemplate = parts.Hosts + updated = true + } + + if cr.Spec.OptionsTemplate == "" { + cr.Spec.OptionsTemplate = parts.Options + updated = true + } + + if cr.Spec.PrefixTemplate == "" { + cr.Spec.PrefixTemplate = parts.Prefix + updated = true + } + + if updated { + if err := r.Client.Update(ctx, cr); err != nil { + return false, fmt.Errorf("update MongoDBCluster spec: %w", err) + } + + logger.Info("Populated Atlas connection details from cluster", "cluster", clusterName) + + return true, nil + } + } + + prefix := effectivePrefix(cr.Spec.PrefixTemplate) + if prefix != connstring.SchemeMongoDB && prefix != connstring.SchemeMongoDBSRV { + return false, errors.NewBadRequest(fmt.Sprintf("invalid prefixTemplate %q", prefix)) + } + + parts, err := selectedConnectionParts(srvParts, stdParts, prefix) + if err != nil { + return false, err + } + + if parts.Prefix != prefix { + return false, errors.NewBadRequest(fmt.Sprintf("prefixTemplate %q does not match Atlas connection string prefix %q", prefix, parts.Prefix)) + } + + if cr.Spec.HostTemplate != "" && !hostTemplateMatches(cr.Spec.HostTemplate, parts.Hosts) { + return false, errors.NewBadRequest(fmt.Sprintf("hostTemplate %q does not match Atlas host(s) %q for %s connection", cr.Spec.HostTemplate, parts.Hosts, prefix)) + } + + if cr.Spec.OptionsTemplate != "" { + logOptionsDiff(logger, cr.Name, cr.Spec.OptionsTemplate, parts.Options) + } + + return false, nil +} diff --git a/controllers/common.go b/controllers/common.go index 33294bf..b936457 100644 --- a/controllers/common.go +++ b/controllers/common.go @@ -5,9 +5,12 @@ import ( "strings" "github.com/mongodb-forks/digest" + "go.mongodb.org/atlas-sdk/v20250312020/admin" "go.mongodb.org/atlas/mongodbatlas" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + + airlockv1alpha1 "github.com/RocketChat/airlock/api/v1alpha1" ) func getSecretProperty(secret *corev1.Secret, property string) (string, error) { @@ -48,6 +51,42 @@ func getAtlasClientFromSecret(secret *corev1.Secret) (*mongodbatlas.Client, stri return client, atlasGroupID, nil } +func getAtlasAdminClientFromSecret(secret *corev1.Secret) (*admin.APIClient, string, error) { + atlasPublicKey, err := getSecretProperty(secret, "atlasPublicKey") + if err != nil { + return nil, "", err + } + + atlasPrivateKey, err := getSecretProperty(secret, "atlasPrivateKey") + if err != nil { + return nil, "", err + } + + atlasGroupID, err := getSecretProperty(secret, "atlasGroupID") + if err != nil { + return nil, "", err + } + + client, err := admin.NewClient(admin.UseDigestAuth(atlasPublicKey, atlasPrivateKey)) + if err != nil { + return nil, "", err + } + + return client, atlasGroupID, nil +} + +func resolveAtlasClusterName(ctx context.Context, spec airlockv1alpha1.MongoDBClusterSpec, client *mongodbatlas.Client, groupID string) (string, error) { + if spec.AtlasClusterName != "" { + return spec.AtlasClusterName, nil + } + + if spec.HostTemplate != "" { + return getClusterNameFromHostTemplate(ctx, client, groupID, spec.HostTemplate) + } + + return "", errors.NewBadRequest("atlasClusterName or hostTemplate is required") +} + func getClusterNameFromHostTemplate(ctx context.Context, client *mongodbatlas.Client, groupID, hostTemplate string) (string, error) { clusters, _, err := client.Clusters.List(ctx, groupID, &mongodbatlas.ListOptions{}) if err != nil { diff --git a/controllers/mongodbcluster_controller.go b/controllers/mongodbcluster_controller.go index 7daf19f..6dd12d8 100644 --- a/controllers/mongodbcluster_controller.go +++ b/controllers/mongodbcluster_controller.go @@ -133,10 +133,34 @@ func (r *MongoDBClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, utilerrors.NewAggregate([]error{err, r.Status().Update(ctx, mongodbClusterCR)}) } + // FIXME: this is wrong, secret is not owned by us unless we create it. _ = ctrl.SetControllerReference(mongodbClusterCR, secret, r.Scheme) // Test connection and user permissions if mongodbClusterCR.Spec.UseAtlasApi { + if mongodbClusterCR.Spec.AtlasClusterName == "" && mongodbClusterCR.Spec.HostTemplate == "" { + err = errors.NewBadRequest("atlasClusterName is required when useAtlasApi is true") + return ctrl.Result{}, utilerrors.NewAggregate([]error{err, r.Status().Update(ctx, mongodbClusterCR)}) + } + + updated, err := r.reconcileAtlasClusterConnectionDetails(ctx, mongodbClusterCR, secret) + if err != nil { + meta.SetStatusCondition(&mongodbClusterCR.Status.Conditions, + metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionFalse, + Reason: "AtlasConnectionDetailsInvalid", + LastTransitionTime: metav1.NewTime(time.Now()), + Message: fmt.Sprintf("Atlas connection details invalid: %s", err.Error()), + }) + + return ctrl.Result{}, utilerrors.NewAggregate([]error{err, r.Status().Update(ctx, mongodbClusterCR)}) + } + + if updated { + return ctrl.Result{Requeue: true}, nil + } + err = testAtlasConnection(ctx, mongodbClusterCR, secret) if err != nil { meta.SetStatusCondition(&mongodbClusterCR.Status.Conditions, @@ -564,10 +588,14 @@ func (r *MongoDBClusterReconciler) reconcileAtlasScheduledAutoscaling(ctx contex return err } - clusterName, err := getClusterNameFromHostTemplate(ctx, client, atlasGroupID, mongodbClusterCR.Spec.HostTemplate) - if err != nil { - logger.Error(err, "Couldn't find cluster in Atlas") - return err + clusterName := mongodbClusterCR.Spec.AtlasClusterName + if clusterName == "" { + var err error + clusterName, err = getClusterNameFromHostTemplate(ctx, client, atlasGroupID, mongodbClusterCR.Spec.HostTemplate) + if err != nil { + logger.Error(err, "Couldn't find cluster in Atlas") + return err + } } clusterDetails, response, err := client.Clusters.Get(ctx, atlasGroupID, clusterName) diff --git a/go.mod b/go.mod index 5ed03f8..6a8ecaa 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/RocketChat/airlock -go 1.21 +go 1.25.0 require ( github.com/davecgh/go-spew v1.1.1 @@ -8,6 +8,7 @@ require ( github.com/onsi/ginkgo/v2 v2.1.4 github.com/onsi/gomega v1.19.0 github.com/thanhpk/randstr v1.0.4 + go.mongodb.org/atlas-sdk/v20250312020 v20250312020.0.0 go.mongodb.org/mongo-driver v1.11.1 k8s.io/api v0.25.0 k8s.io/apimachinery v0.25.0 @@ -59,7 +60,7 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mongodb-forks/digest v1.0.4 + github.com/mongodb-forks/digest v1.1.0 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -78,14 +79,13 @@ require ( go.uber.org/zap v1.21.0 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect - golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect - google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index de9d068..3ba92ce 100644 --- a/go.sum +++ b/go.sum @@ -109,6 +109,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= @@ -141,7 +142,8 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -282,8 +284,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mongodb-forks/digest v1.0.4 h1:9FrGTc7MGAchgaQBcXBnEwUM/Oo8obW7OGWxnsSvZ64= -github.com/mongodb-forks/digest v1.0.4/go.mod h1:eHRfgovT+dvSFfltrOa27hy1oR/rcwyDdp5H1ZQxEMA= +github.com/mongodb-forks/digest v1.1.0 h1:7eUdsR1BtqLv0mdNm4OXs6ddWvR4X2/OsLwdKksrOoc= +github.com/mongodb-forks/digest v1.1.0/go.mod h1:rb+EX8zotClD5Dj4NdgxnJXG9nwrlx3NWKJ8xttz1Dg= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -292,7 +294,9 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= @@ -345,7 +349,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo= github.com/thanhpk/randstr v1.0.4/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= @@ -365,6 +370,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.mongodb.org/atlas v0.24.0 h1:n9ibqbxrtFOInLD6MhNupK9GKFmaH08rLwU5qnr4mpA= go.mongodb.org/atlas v0.24.0/go.mod h1:L4BKwVx/OeEhOVjCSdgo90KJm4469iv7ZLzQms/EPTg= +go.mongodb.org/atlas-sdk/v20250312020 v20250312020.0.0 h1:UHEpyrxeLy45IUcL2G30567pVyfOEtEOw+vAftYEkGY= +go.mongodb.org/atlas-sdk/v20250312020 v20250312020.0.0/go.mod h1:8OGHqMpalr/OTRHA9kjSSg2EKfig5lrNEeNaVYphYYo= go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8= go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -380,6 +387,7 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= @@ -489,8 +497,8 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -674,7 +682,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -779,10 +786,12 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=