diff --git a/config/crd/bases/atlas.mongodb.com_atlasprojects.yaml b/config/crd/bases/atlas.mongodb.com_atlasprojects.yaml index 3bc74e2c40..d792ceda33 100644 --- a/config/crd/bases/atlas.mongodb.com_atlasprojects.yaml +++ b/config/crd/bases/atlas.mongodb.com_atlasprojects.yaml @@ -90,6 +90,8 @@ spec: type: object channelName: type: string + enabled: + type: boolean flowName: type: string licenseKeyRef: @@ -106,8 +108,26 @@ spec: required: - name type: object + microsoftTeamsWebhookUrl: + type: string + name: + type: string orgName: type: string + passwordRef: + description: ResourceRefNamespaced is a reference to a Kubernetes + Resource that allows to configure the namespace + properties: + name: + description: Name is the name of the Kubernetes Resource + type: string + namespace: + description: Namespace is the namespace of the Kubernetes + Resource + type: string + required: + - name + type: object readTokenRef: description: ResourceRefNamespaced is a reference to a Kubernetes Resource that allows to configure the namespace @@ -138,7 +158,9 @@ spec: required: - name type: object - secret: + scheme: + type: string + secretRef: description: ResourceRefNamespaced is a reference to a Kubernetes Resource that allows to configure the namespace properties: @@ -152,6 +174,8 @@ spec: required: - name type: object + serviceDiscovery: + type: string serviceKeyRef: description: ResourceRefNamespaced is a reference to a Kubernetes Resource that allows to configure the namespace @@ -181,9 +205,12 @@ spec: - FLOWDOCK - WEBHOOK - MICROSOFT_TEAMS + - PROMETHEUS type: string url: type: string + username: + type: string writeTokenRef: description: ResourceRefNamespaced is a reference to a Kubernetes Resource that allows to configure the namespace @@ -306,7 +333,7 @@ spec: status: description: AtlasProjectStatus defines the observed state of AtlasProject properties: - AuthModes: + authModes: description: AuthModes contains a list of configured authentication modes "SCRAM" is default authentication method and requires a password for each user "X509" signifies that self-managed X.509 authentication @@ -432,6 +459,15 @@ spec: - region type: object type: array + prometheus: + description: Prometheus contains the status for Prometheus integration + including the prometheusDiscoveryURL + properties: + prometheusDiscoveryURL: + type: string + scheme: + type: string + type: object required: - conditions type: object diff --git a/go.mod b/go.mod index 05f327d825..3e5949b1be 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/pborman/uuid v1.2.1 github.com/sethvargo/go-password v0.2.0 github.com/stretchr/testify v1.7.0 - go.mongodb.org/atlas v0.15.0 + go.mongodb.org/atlas v0.16.0 go.mongodb.org/mongo-driver v1.8.3 go.uber.org/zap v1.21.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b diff --git a/go.sum b/go.sum index 6530031be1..50f90c11ec 100644 --- a/go.sum +++ b/go.sum @@ -99,6 +99,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.44.15 h1:z02BVeV6k7hZMfWEQmKh3X23s3F9PBHFCcIVfNlut7A= github.com/aws/aws-sdk-go v1.44.15/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.43.23 h1:/YmZzPMK6Xzi0B/W9O/Pq7nyIXpBv6mTiJdDDFC7u94= +github.com/aws/aws-sdk-go v1.43.23/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -577,8 +579,8 @@ go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lL go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= -go.mongodb.org/atlas v0.15.0 h1:YyOBdBIuI//krRITf4r7PSirJ3YDNNUfNmapxwSyDow= -go.mongodb.org/atlas v0.15.0/go.mod h1:lQhRHIxc6jQHEK3/q9WLu/SdBkPj2fQYhjLGUF6Z3U8= +go.mongodb.org/atlas v0.16.0 h1:IqnDuK3XAZUgJ5lPHc4v4z4B8F6mvsS37O4ck7tOYVc= +go.mongodb.org/atlas v0.16.0/go.mod h1:lQhRHIxc6jQHEK3/q9WLu/SdBkPj2fQYhjLGUF6Z3U8= go.mongodb.org/mongo-driver v1.8.3 h1:TDKlTkGDKm9kkJVUOAXDK5/fkqKHJVwYQSpoRfB43R4= go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/pkg/api/v1/project/integration.go b/pkg/api/v1/project/integration.go index bd7b7d5071..fb62d9e6ef 100644 --- a/pkg/api/v1/project/integration.go +++ b/pkg/api/v1/project/integration.go @@ -10,7 +10,7 @@ import ( type Integration struct { // Third Party Integration type such as Slack, New Relic, etc - // +kubebuilder:validation:Enum=PAGER_DUTY;SLACK;DATADOG;NEW_RELIC;OPS_GENIE;VICTOR_OPS;FLOWDOCK;WEBHOOK;MICROSOFT_TEAMS + // +kubebuilder:validation:Enum=PAGER_DUTY;SLACK;DATADOG;NEW_RELIC;OPS_GENIE;VICTOR_OPS;FLOWDOCK;WEBHOOK;MICROSOFT_TEAMS;PROMETHEUS // +optional Type string `json:"type,omitempty"` // +optional @@ -43,29 +43,74 @@ type Integration struct { URL string `json:"url,omitempty"` // +optional SecretRef common.ResourceRefNamespaced `json:"secretRef,omitempty"` + // +optional + Name string `json:"name,omitempty"` + // +optional + MicrosoftTeamsWebhookURL string `json:"microsoftTeamsWebhookUrl,omitempty"` + // +optional + UserName string `json:"username,omitempty"` + // +optional + PasswordRef common.ResourceRefNamespaced `json:"passwordRef,omitempty"` + // +optional + ServiceDiscovery string `json:"serviceDiscovery,omitempty"` + // +optional + Scheme string `json:"scheme,omitempty"` + // +optional + Enabled bool `json:"enabled,omitempty"` } -func (i Integration) ToAtlas(defaultNS string, c client.Client) *mongodbatlas.ThirdPartyIntegration { - result := mongodbatlas.ThirdPartyIntegration{} - result.Type = i.Type - result.LicenseKey, _ = i.LicenseKeyRef.ReadPassword(c, defaultNS) - result.AccountID = i.AccountID - result.WriteToken, _ = i.WriteTokenRef.ReadPassword(c, defaultNS) - result.ReadToken, _ = i.ReadTokenRef.ReadPassword(c, defaultNS) - result.APIKey, _ = i.APIKeyRef.ReadPassword(c, defaultNS) - result.Region = i.Region - result.ServiceKey, _ = i.ServiceKeyRef.ReadPassword(c, defaultNS) - result.APIToken, _ = i.APITokenRef.ReadPassword(c, defaultNS) - result.TeamName = i.TeamName - result.ChannelName = i.ChannelName - result.RoutingKey, _ = i.RoutingKeyRef.ReadPassword(c, defaultNS) - result.FlowName = i.FlowName - result.OrgName = i.OrgName - result.URL = i.URL - result.Secret, _ = i.SecretRef.ReadPassword(c, defaultNS) - return &result +func (i Integration) ToAtlas(c client.Client, defaultNS string) (result *mongodbatlas.ThirdPartyIntegration, err error) { + result = &mongodbatlas.ThirdPartyIntegration{ + Type: i.Type, + AccountID: i.AccountID, + Region: i.Region, + TeamName: i.TeamName, + ChannelName: i.ChannelName, + FlowName: i.FlowName, + OrgName: i.OrgName, + URL: i.URL, + Name: i.Name, + MicrosoftTeamsWebhookURL: i.MicrosoftTeamsWebhookURL, + UserName: i.UserName, + ServiceDiscovery: i.ServiceDiscovery, + Scheme: i.Scheme, + Enabled: i.Enabled, + } + + readPassword := func(passwordField common.ResourceRefNamespaced, target *string, errors *[]error) { + if passwordField.Name == "" { + return + } + + *target, err = passwordField.ReadPassword(c, defaultNS) + storeError(err, errors) + } + + errorList := make([]error, 0) + readPassword(i.LicenseKeyRef, &result.LicenseKey, &errorList) + readPassword(i.WriteTokenRef, &result.WriteToken, &errorList) + readPassword(i.ReadTokenRef, &result.ReadToken, &errorList) + readPassword(i.APIKeyRef, &result.APIKey, &errorList) + readPassword(i.ServiceKeyRef, &result.ServiceKey, &errorList) + readPassword(i.APITokenRef, &result.APIToken, &errorList) + readPassword(i.RoutingKeyRef, &result.RoutingKey, &errorList) + readPassword(i.SecretRef, &result.Secret, &errorList) + readPassword(i.PasswordRef, &result.Password, &errorList) + + if len(errorList) != 0 { + firstError := (errorList)[0] + return nil, firstError + } + + return result, nil } func (i Integration) Identifier() interface{} { return i.Type } + +func storeError(err error, errors *[]error) { + if err != nil { + *errors = append(*errors, err) + } +} diff --git a/pkg/api/v1/status/atlasproject.go b/pkg/api/v1/status/atlasproject.go index cd984e1d1f..cfafaf8f4a 100644 --- a/pkg/api/v1/status/atlasproject.go +++ b/pkg/api/v1/status/atlasproject.go @@ -86,5 +86,10 @@ type AtlasProjectStatus struct { // AuthModes contains a list of configured authentication modes // "SCRAM" is default authentication method and requires a password for each user // "X509" signifies that self-managed X.509 authentication is configured - AuthModes authmode.AuthModes `json:"AuthModes,omitempty"` + AuthModes authmode.AuthModes `json:"authModes,omitempty"` + + // Prometheus contains the status for Prometheus integration + // including the prometheusDiscoveryURL + // +optional + Prometheus Prometheus `json:"prometheus,omitempty"` } diff --git a/pkg/api/v1/status/prometheus.go b/pkg/api/v1/status/prometheus.go new file mode 100644 index 0000000000..8cf3fa1848 --- /dev/null +++ b/pkg/api/v1/status/prometheus.go @@ -0,0 +1,8 @@ +package status + +type Prometheus struct { + // +optional + Scheme string `json:"scheme,omitempty"` + // +optional + DiscoveryURL string `json:"prometheusDiscoveryURL,omitempty"` +} diff --git a/pkg/api/v1/status/zz_generated.deepcopy.go b/pkg/api/v1/status/zz_generated.deepcopy.go index 3626bf84c0..1b0e4caee1 100644 --- a/pkg/api/v1/status/zz_generated.deepcopy.go +++ b/pkg/api/v1/status/zz_generated.deepcopy.go @@ -1,4 +1,3 @@ -//go:build !ignore_autogenerated // +build !ignore_autogenerated /* @@ -76,6 +75,7 @@ func (in *AtlasProjectStatus) DeepCopyInto(out *AtlasProjectStatus) { *out = make(authmode.AuthModes, len(*in)) copy(*out, *in) } + out.Prometheus = in.Prometheus } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AtlasProjectStatus. @@ -217,3 +217,18 @@ func (in *ProjectPrivateEndpoint) DeepCopy() *ProjectPrivateEndpoint { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Prometheus) DeepCopyInto(out *Prometheus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Prometheus. +func (in *Prometheus) DeepCopy() *Prometheus { + if in == nil { + return nil + } + out := new(Prometheus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/v1/zz_generated.deepcopy.go b/pkg/api/v1/zz_generated.deepcopy.go index 11caa2b306..ec2c1a0500 100644 --- a/pkg/api/v1/zz_generated.deepcopy.go +++ b/pkg/api/v1/zz_generated.deepcopy.go @@ -1,4 +1,3 @@ -//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/controller/atlasproject/atlasproject_controller.go b/pkg/controller/atlasproject/atlasproject_controller.go index 9603fc79ca..2fa5954e67 100644 --- a/pkg/controller/atlasproject/atlasproject_controller.go +++ b/pkg/controller/atlasproject/atlasproject_controller.go @@ -101,7 +101,7 @@ func (r *AtlasProjectReconciler) Reconcile(context context.Context, req ctrl.Req if err := validate.Project(project); err != nil { result := workflow.Terminate(workflow.Internal, err.Error()) - ctx.SetConditionFromResult(status.ValidationSucceeded, result) + setCondition(ctx, status.ValidationSucceeded, result) return result.ReconcileResult(), nil } ctx.SetConditionTrue(status.ValidationSucceeded) @@ -110,31 +110,32 @@ func (r *AtlasProjectReconciler) Reconcile(context context.Context, req ctrl.Req if err != nil { if errRm := r.removeDeletionFinalizer(context, project); errRm != nil { result = workflow.Terminate(workflow.Internal, errRm.Error()) - ctx.SetConditionFromResult(status.ClusterReadyType, result) + setCondition(ctx, status.ClusterReadyType, result) } result = workflow.Terminate(workflow.AtlasCredentialsNotProvided, err.Error()) - ctx.SetConditionFromResult(status.ProjectReadyType, result) + setCondition(ctx, status.ProjectReadyType, result) return result.ReconcileResult(), nil } ctx.Connection = connection atlasClient, err := atlas.Client(r.AtlasDomain, connection, log) if err != nil { - ctx.SetConditionFromResult(status.ClusterReadyType, workflow.Terminate(workflow.Internal, err.Error())) + result := workflow.Terminate(workflow.Internal, err.Error()) + setCondition(ctx, status.ClusterReadyType, result) return result.ReconcileResult(), nil } ctx.Client = atlasClient var projectID string if projectID, result = r.ensureProjectExists(ctx, project); !result.IsOk() { - ctx.SetConditionFromResult(status.ProjectReadyType, result) + setCondition(ctx, status.ProjectReadyType, result) return result.ReconcileResult(), nil } ctx.EnsureStatusOption(status.AtlasProjectIDOption(projectID)) var authModes authmode.AuthModes if authModes, result = r.ensureX509(ctx, projectID, project); !result.IsOk() { - ctx.SetConditionFromResult(status.ProjectReadyType, result) + setCondition(ctx, status.ProjectReadyType, result) return result.ReconcileResult(), nil } authModes.AddAuthMode(authmode.Scram) // add the default auth method @@ -145,7 +146,7 @@ func (r *AtlasProjectReconciler) Reconcile(context context.Context, req ctrl.Req log.Debugw("Add deletion finalizer", "name", getFinalizerName()) if err := r.addDeletionFinalizer(context, project); err != nil { result = workflow.Terminate(workflow.Internal, err.Error()) - ctx.SetConditionFromResult(status.ClusterReadyType, result) + setCondition(ctx, status.ClusterReadyType, result) return result.ReconcileResult(), nil } } @@ -157,20 +158,20 @@ func (r *AtlasProjectReconciler) Reconcile(context context.Context, req ctrl.Req log.Infof("Not removing the Atlas Project from Atlas as the '%s' annotation is set", customresource.ResourcePolicyAnnotation) } else { if result = DeleteAllPrivateEndpoints(ctx, atlasClient, projectID, project.Status.PrivateEndpoints, log); !result.IsOk() { - ctx.SetConditionFromResult(status.PrivateEndpointReadyType, result) + setCondition(ctx, status.PrivateEndpointReadyType, result) return result.ReconcileResult(), nil } if err = r.deleteAtlasProject(context, atlasClient, project); err != nil { result = workflow.Terminate(workflow.Internal, err.Error()) - ctx.SetConditionFromResult(status.ClusterReadyType, result) + setCondition(ctx, status.ClusterReadyType, result) return result.ReconcileResult(), nil } } if err = r.removeDeletionFinalizer(context, project); err != nil { result = workflow.Terminate(workflow.Internal, err.Error()) - ctx.SetConditionFromResult(status.ClusterReadyType, result) + setCondition(ctx, status.ClusterReadyType, result) return result.ReconcileResult(), nil } } @@ -182,7 +183,7 @@ func (r *AtlasProjectReconciler) Reconcile(context context.Context, req ctrl.Req r.EventRecorder.Event(project, "Normal", string(status.ProjectReadyType), "") if result = ensureIPAccessList(ctx, projectID, project); !result.IsOk() { - ctx.SetConditionFromResult(status.IPAccessListReadyType, result) + setCondition(ctx, status.IPAccessListReadyType, result) return result.ReconcileResult(), nil } @@ -201,17 +202,19 @@ func (r *AtlasProjectReconciler) Reconcile(context context.Context, req ctrl.Req } if result = r.ensurePrivateEndpoint(ctx, projectID, project); !result.IsOk() { + setCondition(ctx, status.PrivateEndpointReadyType, result) return result.ReconcileResult(), nil } r.EventRecorder.Event(project, "Normal", string(status.PrivateEndpointReadyType), "") if result = r.ensureIntegration(ctx, projectID, project); !result.IsOk() { + setCondition(ctx, status.IntegrationReadyType, result) return result.ReconcileResult(), nil } r.EventRecorder.Event(project, "Normal", string(status.IntegrationReadyType), "") ctx.SetConditionTrue(status.ReadyType) - return ctrl.Result{}, nil + return workflow.OK().ReconcileResult(), nil } // allIPAccessListsAreReady returns true if all ipAccessLists are in the ACTIVE state. @@ -305,3 +308,11 @@ func removeString(slice []string, s string) (result []string) { } return result } + +// setCondition sets the condition from the result and logs the warnings +func setCondition(ctx *workflow.Context, condition status.ConditionType, result workflow.Result) { + ctx.SetConditionFromResult(condition, result) + if result.IsWarning() { + ctx.Log.Warnw(result.GetMessage()) + } +} diff --git a/pkg/controller/atlasproject/integrations.go b/pkg/controller/atlasproject/integrations.go index 93c8eaaad1..3f49c5991a 100644 --- a/pkg/controller/atlasproject/integrations.go +++ b/pkg/controller/atlasproject/integrations.go @@ -2,10 +2,11 @@ package atlasproject import ( "context" + "fmt" + "net/http" "reflect" "go.mongodb.org/atlas/mongodbatlas" - "sigs.k8s.io/controller-runtime/pkg/client" mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/project" @@ -15,75 +16,63 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/set" ) -type integrations struct { - list []project.Integration - projectNamespace string -} - func (r *AtlasProjectReconciler) ensureIntegration(ctx *workflow.Context, projectID string, project *mdbv1.AtlasProject) workflow.Result { - integrationList := integrations{ - list: project.Spec.Integrations, - projectNamespace: project.Namespace, - } - if result := createOrDeleteIntegrationsInAtlas(ctx, r.Client, projectID, integrationList); !result.IsOk() { - return result - } - return workflow.OK() -} - -func createOrDeleteIntegrationsInAtlas(ctx *workflow.Context, c client.Client, projectID string, requestedIntegrations integrations) workflow.Result { - integrationsInAtlas, _, err := ctx.Client.Integrations.List(context.Background(), projectID) + integrationsInAtlas, err := fetchIntegrations(ctx, projectID) if err != nil { - return workflow.Terminate(workflow.ProjectIntegrationInAtlasInternal, err.Error()) + return workflow.Terminate(workflow.ProjectIntegrationInternal, err.Error()) } - ctx.Log.Debugf("Got Integrations From Atlas: %v", *integrationsInAtlas) integrationsInAtlasAlias := toAliasThirdPartyIntegration(integrationsInAtlas.Results) - indentificatorsForDelete := set.Difference(integrationsInAtlasAlias, requestedIntegrations.list) + indentificatorsForDelete := set.Difference(integrationsInAtlasAlias, project.Spec.Integrations) ctx.Log.Debugf("indentificatorsForDelete: %v", indentificatorsForDelete) if err := deleteIntegrationsFromAtlas(ctx, projectID, indentificatorsForDelete); err != nil { - return workflow.Terminate(workflow.ProjectIntegrationInAtlasInternal, err.Error()) + return workflow.Terminate(workflow.ProjectIntegrationInternal, err.Error()) } - integrationsToUpdate := set.Intersection(integrationsInAtlasAlias, requestedIntegrations.list) + integrationsToUpdate := set.Intersection(integrationsInAtlasAlias, project.Spec.Integrations) ctx.Log.Debugf("integrationsToUpdate: %v", integrationsToUpdate) - if result := updateIntegrationsAtlas(ctx, c, projectID, integrationsToUpdate, requestedIntegrations.projectNamespace); !result.IsOk() { + if result := r.updateIntegrationsAtlas(ctx, projectID, integrationsToUpdate, project.Namespace); !result.IsOk() { return result } - indentificatorsForCreate := set.Difference(requestedIntegrations.list, integrationsInAtlasAlias) + indentificatorsForCreate := set.Difference(project.Spec.Integrations, integrationsInAtlasAlias) ctx.Log.Debugf("indentificatorsForCreate: %v", indentificatorsForCreate) - if result := createIntegrationsInAtlas(ctx, c, projectID, indentificatorsForCreate, requestedIntegrations.projectNamespace); !result.IsOk() { + if result := r.createIntegrationsInAtlas(ctx, projectID, indentificatorsForCreate, project.Namespace); !result.IsOk() { return result } - ready, err := checkIntegrationsReady(ctx, c, projectID, requestedIntegrations) - if err != nil { - return workflow.Terminate(workflow.ProjectIntegrationInAtlasInternal, err.Error()) - } - if !ready { - ctx.SetConditionFalse(status.IntegrationReadyType) - return workflow.InProgress(workflow.ProjectIntegrationInAtlasInternal, "in progress") + setPrometheusStatus(project, integrationsInAtlas) + if ready := r.checkIntegrationsReady(ctx, project.Namespace, integrationsToUpdate, project.Spec.Integrations); !ready { + return workflow.InProgress(workflow.ProjectIntegrationReady, "in progress") } - if len(requestedIntegrations.list) > 0 { + if len(project.Spec.Integrations) > 0 { ctx.SetConditionTrue(status.IntegrationReadyType) } return workflow.OK() } -func updateIntegrationsAtlas(ctx *workflow.Context, c client.Client, projectID string, integrationsToUpdate [][]set.Identifiable, defaultNS string) workflow.Result { +func fetchIntegrations(ctx *workflow.Context, projectID string) (*mongodbatlas.ThirdPartyIntegrations, error) { + integrationsInAtlas, _, err := ctx.Client.Integrations.List(context.Background(), projectID) + if err != nil { + return nil, err + } + ctx.Log.Debugf("Got Integrations From Atlas: %v", *integrationsInAtlas) + return integrationsInAtlas, nil +} + +func (r *AtlasProjectReconciler) updateIntegrationsAtlas(ctx *workflow.Context, projectID string, integrationsToUpdate [][]set.Identifiable, namespace string) workflow.Result { for _, item := range integrationsToUpdate { atlasIntegration := item[0].(aliasThirdPartyIntegration) - kubeIntegration := item[1].(project.Integration).ToAtlas(defaultNS, c) + kubeIntegration, err := item[1].(project.Integration).ToAtlas(r.Client, namespace) if kubeIntegration == nil { - ctx.Log.Warn("Update Integrations: Can not convert kube integration") - return workflow.Terminate(workflow.ProjectIntegrationInAtlasInternal, "Update Integrations: Can not convert kube integration") + ctx.Log.Warnw("Update Integrations", "Can not convert kube integration", err) + return workflow.Terminate(workflow.ProjectIntegrationInternal, "Update Integrations: Can not convert kube integration") } t := mongodbatlas.ThirdPartyIntegration(atlasIntegration) if &t != kubeIntegration { ctx.Log.Debugf("Try to update integration: %s", kubeIntegration.Type) if _, _, err := ctx.Client.Integrations.Replace(context.Background(), projectID, kubeIntegration.Type, kubeIntegration); err != nil { - return workflow.Terminate(workflow.ProjectIntegrationInAtlasRequest, "Can not convert integration") + return workflow.Terminate(workflow.ProjectIntegrationRequest, "Can not convert integration") } } } @@ -100,40 +89,48 @@ func deleteIntegrationsFromAtlas(ctx *workflow.Context, projectID string, integr return nil } -func createIntegrationsInAtlas(ctx *workflow.Context, c client.Client, projectID string, integrations []set.Identifiable, defaultNS string) workflow.Result { +func (r *AtlasProjectReconciler) createIntegrationsInAtlas(ctx *workflow.Context, projectID string, integrations []set.Identifiable, namespace string) workflow.Result { for _, item := range integrations { - integration := item.(project.Integration).ToAtlas(defaultNS, c) - if integration == nil { - return workflow.Terminate(workflow.ProjectIntegrationInAtlasInternal, "Can not convert integration") + integration, err := item.(project.Integration).ToAtlas(r.Client, namespace) + if err != nil || integration == nil { + return workflow.Terminate(workflow.ProjectIntegrationInternal, fmt.Sprintf("cannot convert integration: %s", err.Error())) } - _, _, err := ctx.Client.Integrations.Create(context.Background(), projectID, integration.Type, integration) + _, resp, err := ctx.Client.Integrations.Create(context.Background(), projectID, integration.Type, integration) + if resp.StatusCode != http.StatusOK { + ctx.Log.Debugw("Create request failed", "Status", resp.Status, "Integration", integration) + } if err != nil { - return workflow.Terminate(workflow.ProjectIntegrationInAtlasRequest, err.Error()) + return workflow.Terminate(workflow.ProjectIntegrationRequest, err.Error()) } } return workflow.OK() } -func checkIntegrationsReady(ctx *workflow.Context, c client.Client, projectID string, requestedIntegrations integrations) (bool, error) { - integrationsInAtlas, _, err := ctx.Client.Integrations.List(context.Background(), projectID) - if err != nil { - return false, err +func (r *AtlasProjectReconciler) checkIntegrationsReady(ctx *workflow.Context, namespace string, integrationsIntersection [][]set.Identifiable, requestedIntegrations []project.Integration) bool { + if len(integrationsIntersection) != len(requestedIntegrations) { + return false } - requestedIntegrationsConverted := convertToAtlasIntegrationList(requestedIntegrations, c) - if reflect.DeepEqual(integrationsInAtlas.Results, requestedIntegrationsConverted) { - return true, nil - } - return false, err -} + for _, integrationPair := range integrationsIntersection { + atlas := integrationPair[0].(aliasThirdPartyIntegration) + spec := integrationPair[1].(project.Integration) + + var areEqual bool + if isPrometheusType(atlas.Type) { + areEqual = arePrometheusesEqual(atlas, spec) + } else { + specAsAtlas, _ := spec.ToAtlas(r.Client, namespace) + areEqual = reflect.DeepEqual(atlas, aliasThirdPartyIntegration(*specAsAtlas)) + } + ctx.Log.Debugw("checkIntegrationsReady", "atlas", atlas, "spec", spec, "areEqual", areEqual) -func convertToAtlasIntegrationList(list integrations, c client.Client) []*mongodbatlas.ThirdPartyIntegration { - result := make([]*mongodbatlas.ThirdPartyIntegration, len(list.list)) - for i, item := range list.list { - result[i] = item.ToAtlas(list.projectNamespace, c) + if !areEqual { + return false + } } - return result + + return true } type aliasThirdPartyIntegration mongodbatlas.ThirdPartyIntegration @@ -149,3 +146,30 @@ func toAliasThirdPartyIntegration(list []*mongodbatlas.ThirdPartyIntegration) [] } return result } + +func setPrometheusStatus(project *mdbv1.AtlasProject, atlasIntegrations *mongodbatlas.ThirdPartyIntegrations) { + for _, atlasIntegration := range atlasIntegrations.Results { + if isPrometheusType(atlasIntegration.Type) { + project.Status.Prometheus = status.Prometheus{ + Scheme: atlasIntegration.Scheme, + DiscoveryURL: buildPrometheusDiscoveryURL(project.ID()), + } + } + } +} + +func arePrometheusesEqual(atlas aliasThirdPartyIntegration, spec project.Integration) bool { + return atlas.Type == spec.Type && + atlas.UserName == spec.UserName && + atlas.ServiceDiscovery == spec.ServiceDiscovery && + atlas.Enabled == spec.Enabled +} + +func isPrometheusType(typeName string) bool { + return typeName == "PROMETHEUS" +} + +func buildPrometheusDiscoveryURL(projectID string) string { + api := "https://cloud.mongodb.com/api/atlas/v1.0" + return fmt.Sprintf("%s/groups/%s/discovery", api, projectID) +} diff --git a/pkg/controller/workflow/context.go b/pkg/controller/workflow/context.go index 8453c91dba..5978aea412 100644 --- a/pkg/controller/workflow/context.go +++ b/pkg/controller/workflow/context.go @@ -70,12 +70,16 @@ func (c *Context) EnsureCondition(condition status.Condition) *Context { } func (c *Context) SetConditionFromResult(conditionType status.ConditionType, result Result) *Context { - c.EnsureCondition(status.Condition{ + condition := status.Condition{ Type: conditionType, Status: corev1.ConditionFalse, Reason: string(result.reason), Message: result.message, - }) + } + if result.IsOk() { + condition.Status = corev1.ConditionTrue + } + c.EnsureCondition(condition) c.lastConditionWarn = result.warning return c } diff --git a/pkg/controller/workflow/reason.go b/pkg/controller/workflow/reason.go index 795809a757..3472731d0e 100644 --- a/pkg/controller/workflow/reason.go +++ b/pkg/controller/workflow/reason.go @@ -18,9 +18,9 @@ const ( ProjectPEServiceIsNotReadyInAtlas ConditionReason = "ProjectPrivateEndpointServiceIsNotReadyInAtlas" ProjectPrivateEndpointIsNotReadyInAtlas ConditionReason = "ProjectPrivateEndpointIsNotReadyInAtlas" ProjectIPAccessListNotActive ConditionReason = "ProjectIPAccessListNotActive" - ProjectIntegrationInAtlasInternal ConditionReason = "ProjectIntegrationInternalError" - ProjectIntegrationInAtlasRequest ConditionReason = "ProjectIntegrationRequestError" - ProjectIntegrationInAtlasReady ConditionReason = "ProjectIntegrationReady" + ProjectIntegrationInternal ConditionReason = "ProjectIntegrationInternalError" + ProjectIntegrationRequest ConditionReason = "ProjectIntegrationRequestError" + ProjectIntegrationReady ConditionReason = "ProjectIntegrationReady" ) // Atlas Cluster reasons diff --git a/pkg/controller/workflow/result.go b/pkg/controller/workflow/result.go index 817628bd74..c7cb15d152 100644 --- a/pkg/controller/workflow/result.go +++ b/pkg/controller/workflow/result.go @@ -78,6 +78,14 @@ func (r Result) IsOk() bool { return !r.terminated } +func (r Result) IsWarning() bool { + return r.warning +} + +func (r Result) GetMessage() string { + return r.message +} + func (r Result) ReconcileResult() reconcile.Result { if r.requeueAfter < 0 { return reconcile.Result{}