Skip to content

CLOUDP-266544: Support local resource credentials #1782

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

Merged
merged 7 commits into from
Sep 2, 2024
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
12 changes: 12 additions & 0 deletions config/crd/bases/atlas.mongodb.com_atlasdatabaseusers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ spec:
- USER
- ROLE
type: string
connectionSecret:
description: A reference to an object in the same namespace as the
referent
properties:
name:
description: |-
Name of the resource being referred to
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
required:
- name
type: object
databaseName:
default: admin
description: DatabaseName is a Database against which Atlas authenticates
Expand Down
1 change: 1 addition & 0 deletions internal/translation/dbuser/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func (u *User) clearedSpecClone() *akov2.AtlasDatabaseUserSpec {
clone.Project.Name = ""
clone.Project.Namespace = ""
clone.PasswordSecret = nil
clone.ConnectionSecret = nil
return &clone
}

Expand Down
3 changes: 3 additions & 0 deletions internal/translation/dbuser/conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/timeutil"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/dbuser"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api"
akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common"
)
Expand Down Expand Up @@ -393,6 +394,7 @@ func TestDiffSpecs(t *testing.T) {
spec.Project.Name = "some-project"
spec.Project.Namespace = "some-namespace"
spec.PasswordSecret = &common.ResourceRef{Name: "some-secret-ref"}
spec.ConnectionSecret = &api.LocalObjectReference{Name: "some-local-secret-ref"}
return spec
}(),
},
Expand All @@ -408,6 +410,7 @@ func TestDiffSpecs(t *testing.T) {
spec.Project.Name = "another-project"
spec.Project.Namespace = "another-namespace"
spec.PasswordSecret = &common.ResourceRef{Name: "another-secret-ref"}
spec.ConnectionSecret = &api.LocalObjectReference{Name: "another-local-secret-ref"}
return spec
}(),
},
Expand Down
28 changes: 28 additions & 0 deletions pkg/api/credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package api

type LocalRef string

// +k8s:deepcopy-gen=false

// CredentialsProvider gives access to custom local credentials
type CredentialsProvider interface {
Credentials() *LocalObjectReference
}

// +k8s:deepcopy-gen=false

// ResourceWithCredentials is to be implemented by all CRDs using custom local credentials
type ResourceWithCredentials interface {
CredentialsProvider
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need this spread across 2 interfaces, rather than just having the LocalObjectReference in this one?

Copy link
Collaborator Author

@josvazg josvazg Aug 27, 2024

Choose a reason for hiding this comment

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

Because CredentialsProvider can be implemented directly by the simple struct object, which is applied to the Spec sub-struct.

The wider interface ResourceWithCredentials applies to the actual CRD as a whole, not just the extended Spec.

It is the simplest way I found to make this as reusable as possible. The consumers just need to embed the LocalCredentialHolder struct in the Spec and implement the delegation to the CredentialsProvider interface it implements from the CRD struct.

GetName() string
GetNamespace() string
}

// LocalCredentialHolder is to be embedded by Specs of CRDs using custom local credentials
type LocalCredentialHolder struct {
ConnectionSecret *LocalObjectReference `json:"connectionSecret,omitempty"`
}

func (ch *LocalCredentialHolder) Credentials() *LocalObjectReference {
return ch.ConnectionSecret
}
8 changes: 8 additions & 0 deletions pkg/api/localref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package api

// LocalObjectReference is a reference to an object in the same namespace as the referent
type LocalObjectReference struct {
// Name of the resource being referred to
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
Name string `json:"name"`
}
6 changes: 6 additions & 0 deletions pkg/api/v1/atlasdatabaseuser_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const (

// AtlasDatabaseUserSpec defines the desired state of Database User in Atlas
type AtlasDatabaseUserSpec struct {
api.LocalCredentialHolder `json:",inline"`

// Project is a reference to AtlasProject resource the user belongs to
Project common.ResourceRefNamespaced `json:"projectRef"`

Expand Down Expand Up @@ -296,6 +298,10 @@ func (p *AtlasDatabaseUser) WithDeleteAfterDate(date string) *AtlasDatabaseUser
return p
}

func (p *AtlasDatabaseUser) Credentials() *api.LocalObjectReference {
return p.Spec.Credentials()
}

func DefaultDBUser(namespace, username, projectName string) *AtlasDatabaseUser {
return NewDBUser(namespace, username, username, projectName).WithRole("clusterMonitor", "admin", "")
}
1 change: 1 addition & 0 deletions pkg/api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions pkg/api/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,16 @@ func (r *AtlasDatabaseUserReconciler) Reconcile(ctx context.Context, req ctrl.Re
return result.ReconcileResult(), nil
}

dus, err := dbuser.NewAtlasDatabaseUsersService(ctx, r.AtlasProvider, project.ConnectionSecretObjectKey(), log)
credentialsSecret, err := customresource.ComputeSecret(project, databaseUser)
if err != nil {
result = workflow.Terminate(workflow.Internal, err.Error())
workflowCtx.SetConditionFromResult(api.DatabaseUserReadyType, result)
log.Error(result.GetMessage())

return result.ReconcileResult(), nil
}

dus, err := dbuser.NewAtlasDatabaseUsersService(ctx, r.AtlasProvider, credentialsSecret, log)
if err != nil {
result = workflow.Terminate(workflow.AtlasAPIAccessNotConfigured, err.Error())
workflowCtx.SetConditionFromResult(api.DatabaseUserReadyType, result)
Expand Down Expand Up @@ -157,7 +166,7 @@ func (r *AtlasDatabaseUserReconciler) Reconcile(ctx context.Context, req ctrl.Re
return result.ReconcileResult(), nil
}

ds, err := deployment.NewAtlasDeploymentsService(ctx, r.AtlasProvider, project.ConnectionSecretObjectKey(), log, r.AtlasProvider.IsCloudGov())
ds, err := deployment.NewAtlasDeploymentsService(ctx, r.AtlasProvider, credentialsSecret, log, r.AtlasProvider.IsCloudGov())
if err != nil {
result = workflow.Terminate(workflow.AtlasAPIAccessNotConfigured, err.Error())
workflowCtx.SetConditionFromResult(api.DatabaseUserReadyType, result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import (
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/indexer"
)

func TestRenconcile(t *testing.T) {
func TestReconcile(t *testing.T) {
tests := map[string]struct {
atlasClientMocker func() *mongodbatlas.Client
atlasSDKMocker func() *admin.APIClient
Expand Down
20 changes: 18 additions & 2 deletions pkg/controller/customresource/customresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import (
"context"
"fmt"

"github.com/Masterminds/semver"
"go.uber.org/zap"
apiErrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/Masterminds/semver"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api"
akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow"
Expand Down Expand Up @@ -139,3 +138,20 @@ func SetAnnotation(resource api.AtlasCustomResource, key, value string) {
annot[key] = value
resource.SetAnnotations(annot)
}

func ComputeSecret(project *akov2.AtlasProject, resource api.ResourceWithCredentials) (*client.ObjectKey, error) {
if resource == nil {
return nil, fmt.Errorf("resource cannot be nil")
}
creds := resource.Credentials()
if creds != nil && creds.Name != "" {
return &client.ObjectKey{
Namespace: resource.GetNamespace(),
Name: creds.Name,
}, nil
}
if project == nil {
return nil, fmt.Errorf("project cannot be nil")
}
return project.ConnectionSecretObjectKey(), nil
}
102 changes: 102 additions & 0 deletions pkg/controller/customresource/customresource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import (

"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api"
akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/version"

"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func TestResourceShouldBeLeftInAtlas(t *testing.T) {
Expand Down Expand Up @@ -227,3 +230,102 @@ func TestResourceVersionIsValid(t *testing.T) {
})
}
}

func TestComputeSecret(t *testing.T) {
for _, tt := range []struct {
name string
project *akov2.AtlasProject
resource api.ResourceWithCredentials
wantRef *types.NamespacedName
wantErrorMsg string
}{
{
name: "nil inputs fails with resource cannot be nil",
wantErrorMsg: "resource cannot be nil",
},

{
name: "nil project ignored if resource is set",
resource: &akov2.AtlasDatabaseUser{
ObjectMeta: metav1.ObjectMeta{Namespace: "local"},
Spec: akov2.AtlasDatabaseUserSpec{
LocalCredentialHolder: api.LocalCredentialHolder{
ConnectionSecret: &api.LocalObjectReference{Name: "local-secret"},
},
},
},
wantRef: &client.ObjectKey{
Name: "local-secret",
Namespace: "local",
},
},

{
name: "nil resource and empty project fails",
project: &akov2.AtlasProject{},
wantErrorMsg: "resource cannot be nil",
},

{
name: "when both are set empty it renders nil",
project: &akov2.AtlasProject{},
resource: &akov2.AtlasDatabaseUser{},
},

{
name: "empty resource and proper project get creds from project",
project: &akov2.AtlasProject{
Spec: akov2.AtlasProjectSpec{
Name: "",
RegionUsageRestrictions: "",
ConnectionSecret: &common.ResourceRefNamespaced{
Name: "project-secret",
Namespace: "some-namespace",
},
},
},
resource: &akov2.AtlasDatabaseUser{},
wantRef: &client.ObjectKey{
Name: "project-secret",
Namespace: "some-namespace",
},
},

{
name: "when both are properly set the resource wins",
project: &akov2.AtlasProject{
Spec: akov2.AtlasProjectSpec{
Name: "",
RegionUsageRestrictions: "",
ConnectionSecret: &common.ResourceRefNamespaced{
Name: "project-secret",
Namespace: "some-namespace",
},
},
},
resource: &akov2.AtlasDatabaseUser{
ObjectMeta: metav1.ObjectMeta{Namespace: "local"},
Spec: akov2.AtlasDatabaseUserSpec{
LocalCredentialHolder: api.LocalCredentialHolder{
ConnectionSecret: &api.LocalObjectReference{Name: "local-secret"},
},
},
},
wantRef: &client.ObjectKey{
Name: "local-secret",
Namespace: "local",
},
},
} {
t.Run(tt.name, func(t *testing.T) {
result, err := ComputeSecret(tt.project, tt.resource)
if tt.wantErrorMsg != "" {
assert.Nil(t, result, nil)
assert.ErrorContains(t, err, tt.wantErrorMsg)
} else {
assert.Equal(t, result, tt.wantRef)
assert.NoError(t, err)
}
})
}
}
4 changes: 2 additions & 2 deletions pkg/operator/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,14 @@ func (b *Builder) Build(ctx context.Context) (manager.Manager, error) {
return nil, fmt.Errorf("unable to create controller AtlasFederatedAuth: %w", err)
}

streamsInstanceReconiler := atlasstream.NewAtlasStreamsInstanceReconciler(
streamsInstanceReconciler := atlasstream.NewAtlasStreamsInstanceReconciler(
mgr,
b.predicates,
b.atlasProvider,
b.deletionProtection,
b.logger,
)
if err = streamsInstanceReconiler.SetupWithManager(mgr, b.skipNameValidation); err != nil {
if err = streamsInstanceReconciler.SetupWithManager(mgr, b.skipNameValidation); err != nil {
return nil, fmt.Errorf("unable to create controller AtlasStreamsInstance: %w", err)
}

Expand Down
Loading
Loading