Skip to content

Commit 6fedb28

Browse files
authored
CLOUDP-278492: Avoid panic when changing type of existing cluster (#1869)
1 parent 88322ac commit 6fedb28

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

internal/translation/deployment/conversion.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type Deployment interface {
2424
GetMongoDBVersion() string
2525
GetConnection() *status.ConnectionStrings
2626
GetReplicaSet() []status.ReplicaSet
27+
IsServerless() bool
2728
}
2829

2930
type Cluster struct {
@@ -69,6 +70,10 @@ func (c *Cluster) GetCustomResource() *akov2.AtlasDeployment {
6970
return c.customResource
7071
}
7172

73+
func (c *Cluster) IsServerless() bool {
74+
return false
75+
}
76+
7277
func (c *Cluster) IsTenant() bool {
7378
return c.isTenant
7479
}
@@ -111,6 +116,10 @@ func (s *Serverless) GetCustomResource() *akov2.AtlasDeployment {
111116
return s.customResource
112117
}
113118

119+
func (s *Serverless) IsServerless() bool {
120+
return true
121+
}
122+
114123
type Connection struct {
115124
Name string
116125
ConnURL string

pkg/controller/atlasdeployment/atlasdeployment_controller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ func (r *AtlasDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Requ
162162
wasDeleted := !atlasDeployment.GetDeletionTimestamp().IsZero()
163163
existsInAtlas := deploymentInAtlas != nil
164164

165+
if existsInAtlas && deploymentInAKO.IsServerless() != deploymentInAtlas.IsServerless() {
166+
return r.terminate(workflowCtx, workflow.Internal, errors.New("regular deployment cannot be converted into a serverless deployment and vice-versa"))
167+
}
168+
165169
switch {
166170
case existsInAtlas && wasDeleted:
167171
return r.delete(workflowCtx, deploymentInAKO)

pkg/controller/atlasdeployment/atlasdeployment_controller_test.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"reflect"
2525
"testing"
2626

27+
"github.com/google/go-cmp/cmp"
28+
"github.com/google/go-cmp/cmp/cmpopts"
2729
"github.com/stretchr/testify/assert"
2830
"github.com/stretchr/testify/mock"
2931
"github.com/stretchr/testify/require"
@@ -1422,3 +1424,175 @@ func TestGetProjectFromKube(t *testing.T) {
14221424
})
14231425
}
14241426
}
1427+
1428+
func TestChangeDeploymentType(t *testing.T) {
1429+
tests := map[string]struct {
1430+
deployment *akov2.AtlasDeployment
1431+
}{
1432+
"should fail when existing cluster is regular but manifest defines a serverless instance": {
1433+
deployment: &akov2.AtlasDeployment{
1434+
ObjectMeta: metav1.ObjectMeta{
1435+
Name: "cluster0",
1436+
Namespace: "default",
1437+
},
1438+
Spec: akov2.AtlasDeploymentSpec{
1439+
Project: &common.ResourceRefNamespaced{
1440+
Name: "my-project",
1441+
Namespace: "default",
1442+
},
1443+
ServerlessSpec: &akov2.ServerlessSpec{
1444+
Name: "cluster0",
1445+
ProviderSettings: &akov2.ServerlessProviderSettingsSpec{
1446+
ProviderName: "SERVERLESS",
1447+
BackingProviderName: "AWS",
1448+
},
1449+
},
1450+
},
1451+
Status: status.AtlasDeploymentStatus{
1452+
StateName: "IDLE",
1453+
},
1454+
},
1455+
},
1456+
"should fail when existing cluster is serverless instance but manifest defines a regular deployment": {
1457+
deployment: &akov2.AtlasDeployment{
1458+
ObjectMeta: metav1.ObjectMeta{
1459+
Name: "cluster0",
1460+
Namespace: "default",
1461+
},
1462+
Spec: akov2.AtlasDeploymentSpec{
1463+
Project: &common.ResourceRefNamespaced{
1464+
Name: "my-project",
1465+
Namespace: "default",
1466+
},
1467+
DeploymentSpec: &akov2.AdvancedDeploymentSpec{
1468+
Name: "cluster0",
1469+
},
1470+
},
1471+
Status: status.AtlasDeploymentStatus{
1472+
StateName: "IDLE",
1473+
},
1474+
},
1475+
},
1476+
}
1477+
1478+
for name, tt := range tests {
1479+
t.Run(name, func(t *testing.T) {
1480+
secret := &corev1.Secret{
1481+
ObjectMeta: metav1.ObjectMeta{
1482+
Name: "api-secret",
1483+
Namespace: "default",
1484+
Labels: map[string]string{
1485+
"atlas.mongodb.com/type": "credentials",
1486+
},
1487+
},
1488+
Data: map[string][]byte{
1489+
"orgId": []byte("1234567890"),
1490+
"publicApiKey": []byte("a1b2c3"),
1491+
"privateApiKey": []byte("abcdef123456"),
1492+
},
1493+
Type: "Opaque",
1494+
}
1495+
project := &akov2.AtlasProject{
1496+
ObjectMeta: metav1.ObjectMeta{
1497+
Name: "my-project",
1498+
Namespace: "default",
1499+
},
1500+
Spec: akov2.AtlasProjectSpec{
1501+
Name: "MyProject",
1502+
ConnectionSecret: &common.ResourceRefNamespaced{
1503+
Name: secret.Name,
1504+
Namespace: secret.Namespace,
1505+
},
1506+
},
1507+
Status: status.AtlasProjectStatus{ID: "abc123"},
1508+
}
1509+
1510+
ctx := context.Background()
1511+
logger := zaptest.NewLogger(t)
1512+
1513+
sch := runtime.NewScheme()
1514+
require.NoError(t, akov2.AddToScheme(sch))
1515+
require.NoError(t, corev1.AddToScheme(sch))
1516+
dbUserProjectIndexer := indexer.NewAtlasDatabaseUserByProjectIndexer(ctx, nil, logger)
1517+
k8sClient := fake.NewClientBuilder().
1518+
WithScheme(sch).
1519+
WithObjects(secret, project, tt.deployment).
1520+
WithStatusSubresource(project, tt.deployment).
1521+
WithIndex(dbUserProjectIndexer.Object(), dbUserProjectIndexer.Name(), dbUserProjectIndexer.Keys).
1522+
Build()
1523+
1524+
atlasProvider := &atlasmock.TestProvider{
1525+
IsCloudGovFunc: func() bool {
1526+
return false
1527+
},
1528+
IsSupportedFunc: func() bool {
1529+
return true
1530+
},
1531+
ClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*mongodbatlas.Client, string, error) {
1532+
return &mongodbatlas.Client{}, "org-id", nil
1533+
},
1534+
SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) {
1535+
clusterAPI := mockadmin.NewClustersApi(t)
1536+
clusterAPI.EXPECT().GetCluster(ctx, "abc123", "cluster0").
1537+
Return(admin.GetClusterApiRequest{ApiService: clusterAPI})
1538+
clusterAPI.EXPECT().GetClusterExecute(mock.AnythingOfType("admin.GetClusterApiRequest")).
1539+
RunAndReturn(
1540+
func(request admin.GetClusterApiRequest) (*admin.AdvancedClusterDescription, *http.Response, error) {
1541+
if !tt.deployment.IsServerless() {
1542+
err := &admin.GenericOpenAPIError{}
1543+
err.SetModel(admin.ApiError{ErrorCode: pointer.MakePtr(atlas.ServerlessInstanceFromClusterAPI)})
1544+
return nil, nil, err
1545+
}
1546+
return &admin.AdvancedClusterDescription{Name: pointer.MakePtr("cluster0")}, nil, nil
1547+
},
1548+
)
1549+
1550+
serverlessAPI := mockadmin.NewServerlessInstancesApi(t)
1551+
if !tt.deployment.IsServerless() {
1552+
serverlessAPI.EXPECT().GetServerlessInstance(ctx, "abc123", "cluster0").
1553+
Return(admin.GetServerlessInstanceApiRequest{ApiService: serverlessAPI})
1554+
serverlessAPI.EXPECT().GetServerlessInstanceExecute(mock.AnythingOfType("admin.GetServerlessInstanceApiRequest")).
1555+
Return(&admin.ServerlessInstanceDescription{Name: pointer.MakePtr("cluster0")}, nil, nil)
1556+
}
1557+
1558+
return &admin.APIClient{ClustersApi: clusterAPI, ServerlessInstancesApi: serverlessAPI}, "org-id", nil
1559+
},
1560+
}
1561+
1562+
r := &AtlasDeploymentReconciler{
1563+
Client: k8sClient,
1564+
AtlasProvider: atlasProvider,
1565+
Log: logger.Sugar(),
1566+
EventRecorder: record.NewFakeRecorder(1),
1567+
}
1568+
result, err := r.Reconcile(
1569+
ctx,
1570+
ctrl.Request{
1571+
NamespacedName: types.NamespacedName{
1572+
Namespace: tt.deployment.Namespace,
1573+
Name: tt.deployment.Name,
1574+
},
1575+
},
1576+
)
1577+
1578+
assert.NoError(t, err)
1579+
assert.Equal(t, ctrl.Result{Requeue: false, RequeueAfter: workflow.DefaultRetry}, result)
1580+
assert.NoError(t, k8sClient.Get(ctx, client.ObjectKeyFromObject(tt.deployment), tt.deployment))
1581+
assert.True(
1582+
t,
1583+
cmp.Equal(
1584+
[]api.Condition{
1585+
api.FalseCondition(api.ReadyType),
1586+
api.TrueCondition(api.ResourceVersionStatus),
1587+
api.TrueCondition(api.ValidationSucceeded),
1588+
api.FalseCondition(api.DeploymentReadyType).
1589+
WithReason(string(workflow.Internal)).
1590+
WithMessageRegexp("regular deployment cannot be converted into a serverless deployment and vice-versa"),
1591+
},
1592+
tt.deployment.Status.Conditions,
1593+
cmpopts.IgnoreFields(api.Condition{}, "LastTransitionTime"),
1594+
),
1595+
)
1596+
})
1597+
}
1598+
}

0 commit comments

Comments
 (0)