Skip to content

Commit 8990244

Browse files
committed
Draft test drive testing flex
1 parent 73d74cc commit 8990244

File tree

6 files changed

+376
-1
lines changed

6 files changed

+376
-1
lines changed

internal/crapi/translator.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ type translator struct {
3838
}
3939

4040
func (tr *translator) ToAPI(target any, source client.Object, objs ...client.Object) error {
41+
if source == nil {
42+
return fmt.Errorf("unexpected nil source")
43+
}
4144
unstructuredSrc, err := unstructured.ToUnstructured(source)
4245
if err != nil {
4346
return fmt.Errorf("failed to convert k8s source value to unstructured: %w", err)
@@ -69,6 +72,9 @@ func (tr *translator) ToAPI(target any, source client.Object, objs ...client.Obj
6972
}
7073

7174
func (tr *translator) FromAPI(target client.Object, source any, objs ...client.Object) ([]client.Object, error) {
75+
if source == nil {
76+
return nil, fmt.Errorf("unexpected nil source")
77+
}
7278
sourceUnstructured, err := unstructured.ToUnstructured(source)
7379
if err != nil {
7480
return nil, fmt.Errorf("failed to convert API source value to unstructured: %w", err)

internal/fakeatlas/builder.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package fakeatlas
16+
17+
import (
18+
v20250312sdk "go.mongodb.org/atlas-sdk/v20250312009/admin"
19+
)
20+
21+
// AtlasClientBuilder is a builder for creating fake Atlas API clients.
22+
type AtlasClientBuilder struct {
23+
client *v20250312sdk.APIClient
24+
}
25+
26+
// NewAtlasClientBuilder creates a new AtlasClientBuilder.
27+
func NewAtlasClientBuilder() *AtlasClientBuilder {
28+
return &AtlasClientBuilder{
29+
client: &v20250312sdk.APIClient{},
30+
}
31+
}
32+
33+
// WithFakeFlexClusterClient configures the builder with a fake FlexCluster client.
34+
func (b *AtlasClientBuilder) WithFakeFlexClusterClient() *AtlasClientBuilder {
35+
b.client.FlexClustersApi = &FakeFlexClustersApi{}
36+
return b
37+
}
38+
39+
// Build returns the configured Atlas API client.
40+
func (b *AtlasClientBuilder) Build() *v20250312sdk.APIClient {
41+
return b.client
42+
}

internal/fakeatlas/fakeflex.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package fakeatlas
16+
17+
import (
18+
"context"
19+
"net/http"
20+
21+
v20250312sdk "go.mongodb.org/atlas-sdk/v20250312009/admin"
22+
)
23+
24+
type FakeFlexClustersApi struct {
25+
v20250312sdk.FlexClustersApi
26+
CreateFlexClusterWithParamsFunc func(ctx context.Context, params *v20250312sdk.CreateFlexClusterApiParams) (*v20250312sdk.FlexClusterDescription20241113, *http.Response, error)
27+
createFlexClusterFunc func(ctx context.Context, groupId string, flexClusterDescriptionCreate20241113 *v20250312sdk.FlexClusterDescriptionCreate20241113) (*v20250312sdk.FlexClusterDescription20241113, *http.Response, error)
28+
// Store the last context and params for Execute to use
29+
lastCtx context.Context
30+
lastParams *v20250312sdk.CreateFlexClusterApiParams
31+
}
32+
33+
func (f *FakeFlexClustersApi) CreateFlexCluster(ctx context.Context, groupId string, flexClusterDescriptionCreate20241113 *v20250312sdk.FlexClusterDescriptionCreate20241113) v20250312sdk.CreateFlexClusterApiRequest {
34+
req := v20250312sdk.CreateFlexClusterApiRequest{
35+
ApiService: f,
36+
}
37+
return req
38+
}
39+
40+
func (f *FakeFlexClustersApi) CreateFlexClusterWithParams(ctx context.Context, params *v20250312sdk.CreateFlexClusterApiParams) v20250312sdk.CreateFlexClusterApiRequest {
41+
// Store context and params for Execute to use
42+
f.lastCtx = ctx
43+
f.lastParams = params
44+
req := v20250312sdk.CreateFlexClusterApiRequest{
45+
ApiService: f,
46+
}
47+
return req
48+
}
49+
50+
// CreateFlexClusterExecute is called by the SDK request's Execute method
51+
func (f *FakeFlexClustersApi) CreateFlexClusterExecute(request v20250312sdk.CreateFlexClusterApiRequest) (*v20250312sdk.FlexClusterDescription20241113, *http.Response, error) {
52+
if f.CreateFlexClusterWithParamsFunc == nil || f.lastParams == nil {
53+
return nil, nil, nil
54+
}
55+
return f.CreateFlexClusterWithParamsFunc(f.lastCtx, f.lastParams)
56+
}

internal/generated/controller/flexcluster/handler_v20250312.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func (h *Handlerv20250312) getDependencies(ctx context.Context, flexcluster *ako
6262
}, group)
6363

6464
if err != nil {
65-
return nil, fmt.Errorf("failed to get group %w", err)
65+
return nil, fmt.Errorf("failed to get group %w", err)
6666
}
6767

6868
result = append(result, group)
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package flexcluster
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"net/http"
21+
"testing"
22+
23+
"github.com/crd2go/crd2go/k8s"
24+
ctrlstate "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/state"
25+
result "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/result"
26+
state "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/state"
27+
"github.com/stretchr/testify/assert"
28+
"github.com/stretchr/testify/require"
29+
corev1 "k8s.io/api/core/v1"
30+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/runtime"
32+
"sigs.k8s.io/controller-runtime/pkg/client"
33+
reconcile "sigs.k8s.io/controller-runtime/pkg/reconcile"
34+
35+
v20250312sdk "go.mongodb.org/atlas-sdk/v20250312009/admin"
36+
37+
crapi "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/crapi"
38+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/fakeatlas"
39+
crds "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/crds"
40+
akov2generated "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1"
41+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer"
42+
)
43+
44+
func TestHandleInitial(t *testing.T) {
45+
testCases := []struct {
46+
title string
47+
flexCluster *akov2generated.FlexCluster
48+
kubeObjects []client.Object
49+
want ctrlstate.Result
50+
wantErr string
51+
atlasCreateClusterFunc func(ctx context.Context, params *v20250312sdk.CreateFlexClusterApiParams) (*v20250312sdk.FlexClusterDescription20241113, *http.Response, error)
52+
}{
53+
{
54+
title: "successful initial creation",
55+
flexCluster: setGroupRef(defaultTestFlexCluster("test-cluster", "default"), "test-group"),
56+
kubeObjects: []client.Object{
57+
defaultTestGroup("test-group", "default", pointer.MakePtr("62b6e34b3d91647abb20e7b8")),
58+
},
59+
want: ReenqueueResult(state.StateCreating, "Creating Flex Cluster."),
60+
},
61+
{
62+
title: "missing group",
63+
flexCluster: setGroupRef(defaultTestFlexCluster("test-cluster", "default"), "not-found"),
64+
want: ctrlstate.Result{NextState: state.StateInitial},
65+
wantErr: "failed to get dependencies: failed to get group",
66+
},
67+
{
68+
title: "group without id",
69+
flexCluster: setGroupRef(defaultTestFlexCluster("test-cluster", "default"), "test-group"),
70+
kubeObjects: []client.Object{
71+
defaultTestGroup("test-group", "default", nil),
72+
},
73+
want: ctrlstate.Result{NextState: state.StateInitial},
74+
wantErr: "failed to fetch referenced value groupId",
75+
},
76+
{
77+
title: "atlas API error",
78+
flexCluster: setGroupRef(defaultTestFlexCluster("test-cluster", "default"), "test-group"),
79+
kubeObjects: []client.Object{
80+
defaultTestGroup("test-group", "default", pointer.MakePtr("62b6e34b3d91647abb20e7b8")),
81+
},
82+
atlasCreateClusterFunc: func(ctx context.Context, params *v20250312sdk.CreateFlexClusterApiParams) (*v20250312sdk.FlexClusterDescription20241113, *http.Response, error) {
83+
return nil, nil, fmt.Errorf("atlas API error: cluster creation failed")
84+
},
85+
want: ctrlstate.Result{NextState: state.StateInitial},
86+
wantErr: "failed to create flex cluster",
87+
},
88+
{
89+
title: "fromAPI translation error",
90+
flexCluster: setGroupRef(defaultTestFlexCluster("test-cluster", "default"), "test-group"),
91+
kubeObjects: []client.Object{
92+
defaultTestGroup("test-group", "default", pointer.MakePtr("62b6e34b3d91647abb20e7b8")),
93+
},
94+
atlasCreateClusterFunc: func(ctx context.Context, params *v20250312sdk.CreateFlexClusterApiParams) (*v20250312sdk.FlexClusterDescription20241113, *http.Response, error) {
95+
// Return nil response to cause ToUnstructured to fail
96+
return nil, nil, nil
97+
},
98+
want: ctrlstate.Result{NextState: state.StateInitial},
99+
wantErr: "failed to translate flex create response",
100+
},
101+
}
102+
103+
for _, tc := range testCases {
104+
t.Run(tc.title, func(t *testing.T) {
105+
ctx := context.Background()
106+
scheme := createTestScheme(t)
107+
108+
allObjects := append([]client.Object{tc.flexCluster}, tc.kubeObjects...)
109+
kubeClient := newFakeClientWithGVK(scheme, allObjects, tc.flexCluster)
110+
111+
crd, err := crds.EmbeddedCRD("FlexCluster")
112+
require.NoError(t, err)
113+
tr, err := crapi.NewTranslator(crd, "v1", "v20250312")
114+
require.NoError(t, err)
115+
116+
atlasClientBuilder := fakeatlas.NewAtlasClientBuilder().
117+
WithFakeFlexClusterClient()
118+
119+
atlasClient := atlasClientBuilder.Build()
120+
121+
// Configure fake client with custom function if provided
122+
if tc.atlasCreateClusterFunc != nil {
123+
if fakeFlexClient, ok := atlasClient.FlexClustersApi.(*fakeatlas.FakeFlexClustersApi); ok {
124+
fakeFlexClient.CreateFlexClusterWithParamsFunc = tc.atlasCreateClusterFunc
125+
}
126+
}
127+
128+
deletionProtection := false
129+
130+
handler := NewHandlerv20250312(kubeClient, atlasClient, tr, deletionProtection)
131+
132+
result, err := handler.HandleInitial(ctx, tc.flexCluster)
133+
134+
if tc.wantErr != "" {
135+
assert.ErrorContains(t, err, tc.wantErr)
136+
} else {
137+
require.NoError(t, err)
138+
}
139+
assert.Equal(t, tc.want, result)
140+
})
141+
}
142+
}
143+
144+
// createTestScheme creates a runtime scheme for testing
145+
func createTestScheme(t *testing.T) *runtime.Scheme {
146+
scheme := runtime.NewScheme()
147+
require.NoError(t, akov2generated.AddToScheme(scheme))
148+
require.NoError(t, corev1.AddToScheme(scheme))
149+
return scheme
150+
}
151+
152+
// defaultTestFlexCluster creates a basic FlexCluster for testing
153+
func defaultTestFlexCluster(name, namespace string) *akov2generated.FlexCluster {
154+
return &akov2generated.FlexCluster{
155+
ObjectMeta: metav1.ObjectMeta{
156+
Name: name,
157+
Namespace: namespace,
158+
},
159+
Spec: akov2generated.FlexClusterSpec{
160+
V20250312: &akov2generated.FlexClusterSpecV20250312{
161+
Entry: &akov2generated.FlexClusterSpecV20250312Entry{
162+
Name: name,
163+
ProviderSettings: akov2generated.ProviderSettings{
164+
BackingProviderName: "AWS",
165+
RegionName: "us-east-1",
166+
},
167+
},
168+
},
169+
},
170+
}
171+
}
172+
173+
func setGroupRef(flexCluster *akov2generated.FlexCluster, groupRef string) *akov2generated.FlexCluster {
174+
flexCluster.Spec.V20250312.GroupRef = &k8s.LocalReference{Name: groupRef}
175+
return flexCluster
176+
}
177+
178+
// defaultTestGroup creates a basic Group for testing
179+
func defaultTestGroup(name, namespace string, id *string) *akov2generated.Group {
180+
return &akov2generated.Group{
181+
TypeMeta: metav1.TypeMeta{
182+
Kind: "Group",
183+
APIVersion: "atlas.generated.mongodb.com/v1",
184+
},
185+
ObjectMeta: metav1.ObjectMeta{
186+
Name: name,
187+
Namespace: namespace,
188+
},
189+
Status: akov2generated.GroupStatus{
190+
V20250312: &akov2generated.GroupStatusV20250312{
191+
Id: id,
192+
},
193+
},
194+
}
195+
}
196+
197+
func ReenqueueResult(state state.ResourceState, msg string) ctrlstate.Result {
198+
return ctrlstate.Result{
199+
Result: reconcile.Result{RequeueAfter: result.DefaultRequeueTIme},
200+
NextState: state,
201+
StateMsg: msg,
202+
}
203+
}

0 commit comments

Comments
 (0)