Skip to content

Commit 0a53e05

Browse files
committed
Export IP Access List resource
1 parent 368055f commit 0a53e05

File tree

8 files changed

+499
-22
lines changed

8 files changed

+499
-22
lines changed

internal/kubernetes/operator/config_exporter.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,28 @@ func (e *ConfigExporter) exportProject() ([]runtime.Object, string, error) {
276276
}
277277
}
278278

279+
if e.featureValidator.IsResourceSupported(features.ResourceAtlasIPAccessList) {
280+
ipAccessList, isEmpty, err := project.BuildIPAccessList(
281+
e.dataProvider,
282+
project.IPAccessListRequest{
283+
ProjectName: projectData.Project.Name,
284+
ProjectID: e.projectID,
285+
TargetNamespace: e.targetNamespace,
286+
Version: e.operatorVersion,
287+
Credentials: credentialsName,
288+
IndependentResource: e.independentResources,
289+
Dictionary: e.dictionaryForAtlasNames,
290+
},
291+
)
292+
if err != nil {
293+
return nil, "", err
294+
}
295+
296+
if !isEmpty {
297+
r = append(r, ipAccessList)
298+
}
299+
}
300+
279301
// DB users
280302
usersData, relatedSecrets, err := dbusers.BuildDBUsers(
281303
e.dataProvider,

internal/kubernetes/operator/project/customroles_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
//go:build unit
16+
1517
//nolint:all
1618
package project
1719

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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 project
16+
17+
import (
18+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/features"
19+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/resources"
20+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/store"
21+
akoapi "github.com/mongodb/mongodb-atlas-kubernetes/v2/api"
22+
akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1"
23+
akov2common "github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1/common"
24+
akov2status "github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1/status"
25+
"go.mongodb.org/atlas-sdk/v20241113004/admin"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
)
28+
29+
type IPAccessListRequest struct {
30+
ProjectName string
31+
ProjectID string
32+
TargetNamespace string
33+
Version string
34+
Credentials string
35+
IndependentResource bool
36+
Dictionary map[string]string
37+
}
38+
39+
func BuildIPAccessList(
40+
provider store.ProjectIPAccessListLister,
41+
request IPAccessListRequest,
42+
) (*akov2.AtlasIPAccessList, bool, error) {
43+
ipAccessLists, err := provider.ProjectIPAccessLists(request.ProjectID, &store.ListOptions{ItemsPerPage: MaxItems})
44+
if err != nil {
45+
return nil, false, err
46+
}
47+
48+
if len(ipAccessLists.GetResults()) == 0 {
49+
return nil, true, nil
50+
}
51+
52+
entries := make([]akov2.IPAccessEntry, 0, len(ipAccessLists.GetResults()))
53+
for _, ipAccessList := range ipAccessLists.GetResults() {
54+
entries = append(entries, fromAtlas(ipAccessList))
55+
}
56+
57+
resource := akov2.AtlasIPAccessList{
58+
TypeMeta: metav1.TypeMeta{
59+
Kind: "AtlasIPAccessList",
60+
APIVersion: "atlas.mongodb.com/v1",
61+
},
62+
ObjectMeta: metav1.ObjectMeta{
63+
Name: resources.NormalizeAtlasName(request.ProjectName+"-ip-access-list", request.Dictionary),
64+
Namespace: request.TargetNamespace,
65+
Labels: map[string]string{
66+
features.ResourceVersion: request.Version,
67+
},
68+
},
69+
Spec: akov2.AtlasIPAccessListSpec{
70+
Entries: entries,
71+
},
72+
Status: akov2status.AtlasIPAccessListStatus{
73+
Common: akoapi.Common{
74+
Conditions: []akoapi.Condition{},
75+
},
76+
},
77+
}
78+
79+
if request.IndependentResource {
80+
resource.Spec.ProjectDualReference = akov2.ProjectDualReference{
81+
ExternalProjectRef: &akov2.ExternalProjectReference{
82+
ID: request.ProjectID,
83+
},
84+
ConnectionSecret: &akoapi.LocalObjectReference{
85+
Name: resources.NormalizeAtlasName(request.Credentials, request.Dictionary),
86+
},
87+
}
88+
} else {
89+
resource.Spec.ProjectDualReference = akov2.ProjectDualReference{
90+
ProjectRef: &akov2common.ResourceRefNamespaced{
91+
Name: request.ProjectName,
92+
Namespace: request.TargetNamespace,
93+
},
94+
}
95+
}
96+
97+
return &resource, false, nil
98+
}
99+
100+
func fromAtlas(entry admin.NetworkPermissionEntry) akov2.IPAccessEntry {
101+
result := akov2.IPAccessEntry{
102+
AwsSecurityGroup: entry.GetAwsSecurityGroup(),
103+
Comment: entry.GetComment(),
104+
}
105+
106+
if _, ok := entry.GetDeleteAfterDateOk(); ok {
107+
deleteAfter := metav1.NewTime(entry.GetDeleteAfterDate())
108+
result.DeleteAfterDate = &deleteAfter
109+
}
110+
111+
if _, ok := entry.GetIpAddressOk(); ok {
112+
result.IPAddress = entry.GetIpAddress()
113+
} else if _, ok := entry.GetCidrBlockOk(); ok {
114+
result.CIDRBlock = entry.GetCidrBlock()
115+
}
116+
117+
return result
118+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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+
//go:build unit
16+
17+
package project
18+
19+
import (
20+
"testing"
21+
"time"
22+
23+
"github.com/golang/mock/gomock"
24+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/features"
25+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/kubernetes/operator/resources"
26+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/mocks"
27+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/pointer"
28+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/store"
29+
akoapi "github.com/mongodb/mongodb-atlas-kubernetes/v2/api"
30+
akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1"
31+
akov2common "github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1/common"
32+
akov2status "github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1/status"
33+
"github.com/stretchr/testify/assert"
34+
"github.com/stretchr/testify/require"
35+
"go.mongodb.org/atlas-sdk/v20241113004/admin"
36+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37+
)
38+
39+
func TestBuildIPAccessList(t *testing.T) {
40+
projectID := "project-ial-id"
41+
projectName := "projectName-ial"
42+
targetNamespace := "ialNamespace"
43+
credentialName := "ial-creds"
44+
deleteAfter := metav1.NewTime(time.Now().Add(time.Hour))
45+
46+
tests := map[string]struct {
47+
ipAccessList []admin.NetworkPermissionEntry
48+
independentResource bool
49+
expectedEmpty bool
50+
expectedResource *akov2.AtlasIPAccessList
51+
}{
52+
"ip access list is empty": {
53+
ipAccessList: []admin.NetworkPermissionEntry{},
54+
expectedEmpty: true,
55+
},
56+
"generate ip access list with kubernetes project reference": {
57+
ipAccessList: []admin.NetworkPermissionEntry{
58+
{
59+
IpAddress: pointer.Get("192.168.100.233"),
60+
Comment: pointer.Get("My private access"),
61+
},
62+
{
63+
CidrBlock: pointer.Get("10.1.1.0/24"),
64+
Comment: pointer.Get("Company network"),
65+
},
66+
{
67+
AwsSecurityGroup: pointer.Get("sg-123456"),
68+
Comment: pointer.Get("Cloud network"),
69+
},
70+
{
71+
IpAddress: pointer.Get("172.16.100.10"),
72+
DeleteAfterDate: pointer.Get(deleteAfter.Time),
73+
Comment: pointer.Get("Third party temporary access"),
74+
},
75+
},
76+
expectedResource: &akov2.AtlasIPAccessList{
77+
TypeMeta: metav1.TypeMeta{
78+
Kind: "AtlasIPAccessList",
79+
APIVersion: "atlas.mongodb.com/v1",
80+
},
81+
ObjectMeta: metav1.ObjectMeta{
82+
Name: "projectname-ial-ip-access-list",
83+
Namespace: targetNamespace,
84+
Labels: map[string]string{
85+
features.ResourceVersion: "2.7.0",
86+
},
87+
},
88+
Spec: akov2.AtlasIPAccessListSpec{
89+
ProjectDualReference: akov2.ProjectDualReference{
90+
ProjectRef: &akov2common.ResourceRefNamespaced{
91+
Name: projectName,
92+
Namespace: targetNamespace,
93+
},
94+
},
95+
Entries: []akov2.IPAccessEntry{
96+
{
97+
IPAddress: "192.168.100.233",
98+
Comment: "My private access",
99+
},
100+
{
101+
CIDRBlock: "10.1.1.0/24",
102+
Comment: "Company network",
103+
},
104+
{
105+
AwsSecurityGroup: "sg-123456",
106+
Comment: "Cloud network",
107+
},
108+
{
109+
IPAddress: "172.16.100.10",
110+
DeleteAfterDate: pointer.Get(deleteAfter),
111+
Comment: "Third party temporary access",
112+
},
113+
},
114+
},
115+
Status: akov2status.AtlasIPAccessListStatus{
116+
Common: akoapi.Common{
117+
Conditions: []akoapi.Condition{},
118+
},
119+
},
120+
},
121+
},
122+
"generate ip access list with external project reference": {
123+
ipAccessList: []admin.NetworkPermissionEntry{
124+
{
125+
IpAddress: pointer.Get("192.168.100.233"),
126+
Comment: pointer.Get("My private access"),
127+
},
128+
{
129+
CidrBlock: pointer.Get("10.1.1.0/24"),
130+
Comment: pointer.Get("Company network"),
131+
},
132+
},
133+
independentResource: true,
134+
expectedResource: &akov2.AtlasIPAccessList{
135+
TypeMeta: metav1.TypeMeta{
136+
Kind: "AtlasIPAccessList",
137+
APIVersion: "atlas.mongodb.com/v1",
138+
},
139+
ObjectMeta: metav1.ObjectMeta{
140+
Name: "projectname-ial-ip-access-list",
141+
Namespace: targetNamespace,
142+
Labels: map[string]string{
143+
features.ResourceVersion: "2.7.0",
144+
},
145+
},
146+
Spec: akov2.AtlasIPAccessListSpec{
147+
ProjectDualReference: akov2.ProjectDualReference{
148+
ExternalProjectRef: &akov2.ExternalProjectReference{
149+
ID: projectID,
150+
},
151+
ConnectionSecret: &akoapi.LocalObjectReference{
152+
Name: credentialName,
153+
},
154+
},
155+
Entries: []akov2.IPAccessEntry{
156+
{
157+
IPAddress: "192.168.100.233",
158+
Comment: "My private access",
159+
},
160+
{
161+
CIDRBlock: "10.1.1.0/24",
162+
Comment: "Company network",
163+
},
164+
},
165+
},
166+
Status: akov2status.AtlasIPAccessListStatus{
167+
Common: akoapi.Common{
168+
Conditions: []akoapi.Condition{},
169+
},
170+
},
171+
},
172+
},
173+
}
174+
for name, tt := range tests {
175+
t.Run(name, func(t *testing.T) {
176+
ctl := gomock.NewController(t)
177+
ialStore := mocks.NewMockProjectIPAccessListLister(ctl)
178+
dictionary := resources.AtlasNameToKubernetesName()
179+
180+
ialStore.EXPECT().ProjectIPAccessLists(projectID, &store.ListOptions{ItemsPerPage: MaxItems}).
181+
Return(&admin.PaginatedNetworkAccess{Results: &tt.ipAccessList}, nil)
182+
183+
atlasIPAccessList, isEmpty, err := BuildIPAccessList(
184+
ialStore,
185+
IPAccessListRequest{
186+
ProjectName: projectName,
187+
ProjectID: projectID,
188+
TargetNamespace: targetNamespace,
189+
Version: "2.7.0",
190+
Credentials: credentialName,
191+
IndependentResource: tt.independentResource,
192+
Dictionary: dictionary,
193+
},
194+
)
195+
require.NoError(t, err)
196+
assert.Equal(t, tt.expectedEmpty, isEmpty)
197+
assert.Equal(t, tt.expectedResource, atlasIPAccessList)
198+
})
199+
}
200+
}

internal/kubernetes/operator/project/privateendpoints_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
//go:build unit
16+
1517
package project
1618

1719
import (

internal/kubernetes/operator/project/project.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func BuildAtlasProject(br *AtlasProjectBuildRequest) (*AtlasProjectResult, error
9494
Teams: nil,
9595
}
9696

97-
if br.Validator.FeatureExist(features.ResourceAtlasProject, featureAccessLists) {
97+
if br.Validator.FeatureExist(features.ResourceAtlasProject, featureAccessLists) && !br.Validator.IsResourceSupported(features.ResourceAtlasIPAccessList) {
9898
ipAccessList, ferr := buildAccessLists(br.ProjectStore, br.ProjectID)
9999
if ferr != nil {
100100
return nil, ferr

0 commit comments

Comments
 (0)