Skip to content
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
18 changes: 4 additions & 14 deletions pkg/api/v1/atlascluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ limitations under the License.
package v1

import (
"encoding/json"

"go.mongodb.org/atlas/mongodbatlas"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/compat"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/kube"
)

Expand Down Expand Up @@ -240,18 +239,9 @@ var _ = RegionsConfig(mongodbatlas.RegionsConfig{})

// Cluster converts the Spec to native Atlas client format.
func (spec *AtlasClusterSpec) Cluster() (*mongodbatlas.Cluster, error) {
result := mongodbatlas.Cluster{}
b, err := json.Marshal(spec)
if err != nil {
return nil, err
}

err = json.Unmarshal(b, &result)
if err != nil {
return nil, err
}

return &result, nil
result := &mongodbatlas.Cluster{}
err := compat.JSONCopy(result, spec)
return result, err
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
Expand Down
15 changes: 15 additions & 0 deletions pkg/api/v1/atlasproject_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ limitations under the License.
package v1

import (
"go.mongodb.org/atlas/mongodbatlas"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/compat"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/kube"
)

Expand Down Expand Up @@ -96,6 +98,19 @@ type ProjectIPAccessList struct {
IPAddress string `json:"ipAddress,omitempty"`
}

// ToAtlas converts the ProjectIPAccessList to native Atlas client format.
func (i ProjectIPAccessList) ToAtlas() (*mongodbatlas.ProjectIPAccessList, error) {
result := &mongodbatlas.ProjectIPAccessList{}
err := compat.JSONCopy(result, i)
return result, err
}

// Identifier returns the "id" of the ProjectIPAccessList. Note, that it's an error to specify more than one of these
// fields - the business layer must validate this beforehand
func (i ProjectIPAccessList) Identifier() interface{} {
return i.CIDRBlock + i.AwsSecurityGroup + i.IPAddress
}

func (p *AtlasProject) ConnectionSecretObjectKey() *client.ObjectKey {
if p.Spec.ConnectionSecret != nil {
key := kube.ObjectKey(p.Namespace, p.Spec.ConnectionSecret.Name)
Expand Down
7 changes: 5 additions & 2 deletions pkg/controller/atlasproject/atlasproject_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,12 @@ func (r *AtlasProjectReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error

// Updating the status with "projectReady = true" and "IPAccessListReady = false" (not as separate updates!)
ctx.SetConditionTrue(status.ProjectReadyType)
statushandler.Update(ctx.SetConditionFalse(status.IPAccessListReadyType), r.Client, project)

// TODO projectAccessList
if result = r.ensureIPAccessList(ctx, connection, projectID, project); !result.IsOk() {
ctx.SetConditionFromResult(status.IPAccessListReadyType, result)
return result.ReconcileResult(), nil
}
ctx.SetConditionTrue(status.IPAccessListReadyType)
ctx.SetConditionTrue(status.ReadyType)
return ctrl.Result{}, nil
}
Expand Down
129 changes: 129 additions & 0 deletions pkg/controller/atlasproject/ipaccess_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package atlasproject

import (
"context"
"errors"
"time"

"go.mongodb.org/atlas/mongodbatlas"
"go.uber.org/zap"

mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/atlas"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/set"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/timeutil"
)

// atlasProjectIPAccessList is a synonym of Atlas object as we need to implement 'Identifier' (and we cannot modify
// their object)
type atlasProjectIPAccessList mongodbatlas.ProjectIPAccessList

func (i atlasProjectIPAccessList) Identifier() interface{} {
return i.CIDRBlock + i.AwsSecurityGroup + i.IPAddress
}

func (r *AtlasProjectReconciler) ensureIPAccessList(ctx *workflow.Context, connection atlas.Connection, projectID string, project *mdbv1.AtlasProject) workflow.Result {
if err := validateIPAccessLists(project.Spec.ProjectIPAccessList); err != nil {
return workflow.Terminate(workflow.ProjectIPAccessInvalid, err.Error())
}
active, _ := filterActiveIPAccessLists(project)

client, err := atlas.Client(r.AtlasDomain, connection, ctx.Log)
if err != nil {
return workflow.Terminate(workflow.Internal, err.Error())
}
if result := createOrDeleteInAtlas(client, projectID, active, ctx.Log); !result.IsOk() {
return result
}
// TODO update status - add the expired project IP access list there
return workflow.OK()
}

func validateIPAccessLists(ipAccessList []mdbv1.ProjectIPAccessList) error {
for _, list := range ipAccessList {
if list.DeleteAfterDate != "" {
_, err := timeutil.ParseISO8601(list.DeleteAfterDate)
if err != nil {
return err
}
}
// Go doesn't support XOR, but uses '!=' instead: https://stackoverflow.com/a/23025720/614239
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] but != literally is XOR for booleans 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes! :)

onlyOneSpecified := isNotEmpty(list.AwsSecurityGroup) != isNotEmpty(list.CIDRBlock) != isNotEmpty(list.IPAddress)
if !onlyOneSpecified {
return errors.New("only one of the 'awsSecurityGroup', 'cidrBlock' or 'ipAddress' is required be specified")
}
}
return nil
}

func createOrDeleteInAtlas(client *mongodbatlas.Client, projectID string, operatorIPAccessLists []mdbv1.ProjectIPAccessList, log *zap.SugaredLogger) workflow.Result {
atlasAccess, _, err := client.ProjectIPAccessList.List(context.Background(), projectID, &mongodbatlas.ListOptions{})
if err != nil {
return workflow.Terminate(workflow.ProjectIPNotCreatedInAtlas, err.Error())
}
// Making a new slice with synonyms as Atlas IP Access list to enable usage of 'Identifiable'
atlasAccessLists := make([]atlasProjectIPAccessList, len(atlasAccess.Results))
for i, r := range atlasAccess.Results {
atlasAccessLists[i] = atlasProjectIPAccessList(r)
}

difference := set.Difference(atlasAccessLists, operatorIPAccessLists)

if err := deleteIPAccessFromAtlas(client, projectID, difference, log); err != nil {
return workflow.Terminate(workflow.ProjectIPNotCreatedInAtlas, err.Error())
}

if result := createIPAccessListsInAtlas(client, projectID, operatorIPAccessLists); !result.IsOk() {
return result
}
return workflow.OK()
}

func createIPAccessListsInAtlas(client *mongodbatlas.Client, projectID string, ipAccessLists []mdbv1.ProjectIPAccessList) workflow.Result {
operatorAccessLists := make([]*mongodbatlas.ProjectIPAccessList, len(ipAccessLists))
for i, list := range ipAccessLists {
atlasFormat, err := list.ToAtlas()
if err != nil {
return workflow.Terminate(workflow.Internal, err.Error())
}
operatorAccessLists[i] = atlasFormat
}

if _, _, err := client.ProjectIPAccessList.Create(context.Background(), projectID, operatorAccessLists); err != nil {
return workflow.Terminate(workflow.ProjectIPNotCreatedInAtlas, err.Error())
}
return workflow.OK()
}

func deleteIPAccessFromAtlas(client *mongodbatlas.Client, projectID string, listsToRemove []set.Identifiable, log *zap.SugaredLogger) error {
for _, l := range listsToRemove {
if _, err := client.ProjectIPAccessList.Delete(context.Background(), projectID, l.Identifier().(string)); err != nil {
return err
}
log.Debugw("Removed IPAccessList from Atlas as it's not specified in current AtlasProject", "id", l.Identifier())
}
return nil
}

func filterActiveIPAccessLists(project *mdbv1.AtlasProject) ([]mdbv1.ProjectIPAccessList, []mdbv1.ProjectIPAccessList) {
active := make([]mdbv1.ProjectIPAccessList, 0)
expired := make([]mdbv1.ProjectIPAccessList, 0)
for _, list := range project.Spec.ProjectIPAccessList {
if list.DeleteAfterDate != "" {
// We are ignoring the error as it will never happen due to validation check before
iso8601, _ := timeutil.ParseISO8601(list.DeleteAfterDate)
if iso8601.Before(time.Now()) {
expired = append(expired, list)
continue
}
}
// Either 'deleteAfterDate' field is not specified or it's higher than the current time
active = append(active, list)
}
return active, expired
}

func isNotEmpty(s string) bool {
return s != ""
}
4 changes: 3 additions & 1 deletion pkg/controller/workflow/reason.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const (

// Atlas Project reasons
const (
ProjectNotCreatedInAtlas ConditionReason = "ProjectNotCreatedInAtlas"
ProjectNotCreatedInAtlas ConditionReason = "ProjectNotCreatedInAtlas"
ProjectIPAccessInvalid ConditionReason = "ProjectIPAccessListInvalid"
ProjectIPNotCreatedInAtlas ConditionReason = "ProjectIPAccessListNotCreatedInAtlas"
)

// Atlas Cluster reasons
Expand Down
7 changes: 7 additions & 0 deletions pkg/util/timeutil/timeutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package timeutil

import "time"

func ParseISO8601(dateTime string) (time.Time, error) {
return time.Parse(dateTime, "2006-01-02T15:04:05-0700")
}
15 changes: 14 additions & 1 deletion test/int/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ var _ = Describe("AtlasProject", func() {
Expect(createdProject.Status.ID).NotTo(BeNil())
expectedConditionsMatchers := testutil.MatchConditions(
status.TrueCondition(status.ProjectReadyType),
status.FalseCondition(status.IPAccessListReadyType),
status.TrueCondition(status.IPAccessListReadyType),
status.TrueCondition(status.ReadyType),
)
Expect(createdProject.Status.Conditions).To(ConsistOf(expectedConditionsMatchers))
Expand Down Expand Up @@ -181,6 +181,19 @@ var _ = Describe("AtlasProject", func() {
})
})
})

Describe("Creating the project IP access list", func() {
It("Should Succeed", func() {
expectedProject := testAtlasProject(namespace.Name, "test-project", namespace.Name, connectionSecret.Name)
expectedProject.Spec.ProjectIPAccessList = []mdbv1.ProjectIPAccessList{{Comment: "bla", IPAddress: "192.0.2.15"}}
createdProject.ObjectMeta = expectedProject.ObjectMeta
Expect(k8sClient.Create(context.Background(), expectedProject)).ToNot(HaveOccurred())

Eventually(testutil.WaitFor(k8sClient, createdProject, status.TrueCondition(status.ReadyType)),
20, interval).Should(BeTrue())
})
})

})

// TODO builders
Expand Down