Skip to content

Commit cc2c751

Browse files
committed
Support apiKey with ip access list on install cmd
1 parent 1aaeb94 commit cc2c751

File tree

13 files changed

+257
-23
lines changed

13 files changed

+257
-23
lines changed

docs/command/atlas-kubernetes-operator-install.txt

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ Options
5656
-
5757
- false
5858
- Flag that indicates whether to import existing Atlas resources into the cluster for the operator to manage.
59+
* - --ipAccessList
60+
- string
61+
- false
62+
- A comma-separated list of IP or CIDR block to allowlist for Operator to communicate with Atlas APIs. Read more: https://www.mongodb.com/docs/atlas/configure-api-access-project/
5963
* - --kubeContext
6064
- string
6165
- false
@@ -123,46 +127,39 @@ Examples
123127
:copyable: false
124128

125129
# Install the latest version of the operator targeting Atlas for Government instead of regular commercial Atlas:
126-
atlas kubernetes operator install --atlasGov
130+
atlas kubernetes operator install --atlasGov --ipAccessList=<IP_ADDRESS_OR_CIDR>
127131

128132

129133
.. code-block::
130134
:copyable: false
131135

132136
# Install a specific version of the operator:
133-
atlas kubernetes operator install --operatorVersion=1.7.0
137+
atlas kubernetes operator install --ipAccessList=<IP_ADDRESS_OR_CIDR> --operatorVersion=1.7.0
134138

135139

136140
.. code-block::
137141
:copyable: false
138142

139143
# Install a specific version of the operator to a namespace and watch only this namespace and a second one:
140-
atlas kubernetes operator install --operatorVersion=1.7.0 --targetNamespace=<namespace> --watchNamespace=<namespace>,<secondNamespace>
144+
atlas kubernetes operator install --ipAccessList=<IP_ADDRESS_OR_CIDR> --operatorVersion=1.7.0 --targetNamespace=<namespace> --watchNamespace=<namespace>,<secondNamespace>
141145

142146

143147
.. code-block::
144148
:copyable: false
145149

146150
# Install and import all objects from an organization:
147-
atlas kubernetes operator install --targetNamespace=<namespace> --orgID <orgID> --import
151+
atlas kubernetes operator install --ipAccessList=<IP_ADDRESS_OR_CIDR> --targetNamespace=<namespace> --orgID <orgID> --import
148152

149153

150154
.. code-block::
151155
:copyable: false
152156

153157
# Install and import objects from a specific project:
154-
atlas kubernetes operator install --targetNamespace=<namespace> --orgID <orgID> --projectName <project> --import
158+
atlas kubernetes operator install --ipAccessList=<IP_ADDRESS_OR_CIDR> --targetNamespace=<namespace> --orgID <orgID> --projectName <project> --import
155159

156160

157161
.. code-block::
158162
:copyable: false
159163

160164
# Install the operator and disable deletion protection:
161-
atlas kubernetes operator install --resourceDeletionProtection=false
162-
163-
164-
.. code-block::
165-
:copyable: false
166-
167-
# Install the operator and disable deletion protection for sub-resources (Atlas project integrations, private endpoints, etc.):
168-
atlas kubernetes operator install --subresourceDeletionProtection=false
165+
atlas kubernetes operator install --ipAccessList=<IP_ADDRESS_OR_CIDR> --resourceDeletionProtection=false

internal/cli/kubernetes/operator/install.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ package operator
1616

1717
import (
1818
"context"
19+
"errors"
1920
"fmt"
21+
"net"
22+
"strings"
2023

2124
"github.com/google/go-github/v61/github"
2225
"github.com/mongodb/atlas-cli-core/config"
@@ -53,6 +56,7 @@ type InstallOpts struct {
5356
featureDeletionProtection bool
5457
featureSubDeletionProtection bool
5558
configOnly bool
59+
ipAccessList string
5660
}
5761

5862
func (opts *InstallOpts) defaults() error {
@@ -103,6 +107,23 @@ func (opts *InstallOpts) ValidateWatchNamespace() error {
103107
return nil
104108
}
105109

110+
func (opts *InstallOpts) ValidateIpAccessList() error {
111+
if opts.ipAccessList == "" {
112+
return errors.New("IP access list cannot be empty")
113+
}
114+
115+
list := strings.Split(opts.ipAccessList, ",")
116+
for _, entry := range list {
117+
if _, _, err := net.ParseCIDR(entry); err != nil {
118+
if net.ParseIP(entry) == nil {
119+
return fmt.Errorf("IP access list \"%s\" must be a valid IP address or CIDR", entry)
120+
}
121+
}
122+
}
123+
124+
return nil
125+
}
126+
106127
func (opts *InstallOpts) Run(ctx context.Context) error {
107128
kubeCtl, err := kubernetes.NewKubeCtl(opts.KubeConfig, opts.KubeContext)
108129
if err != nil {
@@ -129,7 +150,7 @@ func (opts *InstallOpts) Run(ctx context.Context) error {
129150
return err
130151
}
131152

132-
err = operator.NewInstall(installer, atlasStore, credStore, featureValidator, kubeCtl, opts.operatorVersion).
153+
err = operator.NewInstall(installer, atlasStore, credStore, featureValidator, kubeCtl, opts.operatorVersion, opts.ipAccessList).
133154
WithNamespace(opts.targetNamespace).
134155
WithWatchNamespaces(opts.watchNamespace).
135156
WithWatchProjectName(opts.projectName).
@@ -164,25 +185,22 @@ The key is scoped to the project when you specify the --projectName option and t
164185
atlas kubernetes operator install
165186
166187
# Install the latest version of the operator targeting Atlas for Government instead of regular commercial Atlas:
167-
atlas kubernetes operator install --atlasGov
188+
atlas kubernetes operator install --atlasGov --ipAccessList=<IP_ADDRESS_OR_CIDR>
168189
169190
# Install a specific version of the operator:
170-
atlas kubernetes operator install --operatorVersion=1.7.0
191+
atlas kubernetes operator install --ipAccessList=<IP_ADDRESS_OR_CIDR> --operatorVersion=1.7.0
171192
172193
# Install a specific version of the operator to a namespace and watch only this namespace and a second one:
173-
atlas kubernetes operator install --operatorVersion=1.7.0 --targetNamespace=<namespace> --watchNamespace=<namespace>,<secondNamespace>
194+
atlas kubernetes operator install --ipAccessList=<IP_ADDRESS_OR_CIDR> --operatorVersion=1.7.0 --targetNamespace=<namespace> --watchNamespace=<namespace>,<secondNamespace>
174195
175196
# Install and import all objects from an organization:
176-
atlas kubernetes operator install --targetNamespace=<namespace> --orgID <orgID> --import
197+
atlas kubernetes operator install --ipAccessList=<IP_ADDRESS_OR_CIDR> --targetNamespace=<namespace> --orgID <orgID> --import
177198
178199
# Install and import objects from a specific project:
179-
atlas kubernetes operator install --targetNamespace=<namespace> --orgID <orgID> --projectName <project> --import
200+
atlas kubernetes operator install --ipAccessList=<IP_ADDRESS_OR_CIDR> --targetNamespace=<namespace> --orgID <orgID> --projectName <project> --import
180201
181202
# Install the operator and disable deletion protection:
182-
atlas kubernetes operator install --resourceDeletionProtection=false
183-
184-
# Install the operator and disable deletion protection for sub-resources (Atlas project integrations, private endpoints, etc.):
185-
atlas kubernetes operator install --subresourceDeletionProtection=false`,
203+
atlas kubernetes operator install --ipAccessList=<IP_ADDRESS_OR_CIDR> --resourceDeletionProtection=false`,
186204
PreRunE: func(_ *cobra.Command, _ []string) error {
187205
opts.versionProvider = version.NewOperatorVersion(github.NewClient(nil))
188206

@@ -192,6 +210,7 @@ The key is scoped to the project when you specify the --projectName option and t
192210
opts.ValidateOperatorVersion,
193211
opts.ValidateTargetNamespace,
194212
opts.ValidateWatchNamespace,
213+
opts.ValidateIpAccessList,
195214
)
196215
},
197216
RunE: func(cmd *cobra.Command, _ []string) error {
@@ -213,6 +232,7 @@ The key is scoped to the project when you specify the --projectName option and t
213232
flags.BoolVar(&opts.featureDeletionProtection, flag.OperatorResourceDeletionProtection, true, usage.OperatorResourceDeletionProtection)
214233
flags.BoolVar(&opts.featureSubDeletionProtection, flag.OperatorSubResourceDeletionProtection, true, usage.OperatorSubResourceDeletionProtection)
215234
flags.BoolVar(&opts.configOnly, flag.OperatorConfigOnly, false, usage.OperatorConfigOnly)
235+
flags.StringVar(&opts.ipAccessList, flag.IPAccessList, "", usage.IPAccessList)
216236

217237
return cmd
218238
}

internal/cli/kubernetes/operator/install_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,48 @@
1515
//go:build unit
1616

1717
package operator
18+
19+
import (
20+
"errors"
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
)
25+
26+
func TestInstallOptsValidateIpAccessList(t *testing.T) {
27+
tests := map[string]struct {
28+
ipAccessList string
29+
err error
30+
}{
31+
"valid single IP": {
32+
ipAccessList: "104.30.164.5",
33+
},
34+
"valid CIDR block": {
35+
ipAccessList: "192.168.100.177/24",
36+
},
37+
"valid list of entries": {
38+
ipAccessList: "104.30.164.5,192.168.100.177/24",
39+
},
40+
"empty string": {
41+
ipAccessList: "",
42+
err: errors.New("IP access list cannot be empty"),
43+
},
44+
"invalid IP": {
45+
ipAccessList: "256.256.256.256",
46+
err: errors.New("IP access list \"256.256.256.256\" must be a valid IP address or CIDR"),
47+
},
48+
"invalid CIDR block": {
49+
ipAccessList: "192.168.100.177/33",
50+
err: errors.New("IP access list \"192.168.100.177/33\" must be a valid IP address or CIDR"),
51+
},
52+
}
53+
for name, tt := range tests {
54+
t.Run(name, func(t *testing.T) {
55+
opts := &InstallOpts{
56+
ipAccessList: tt.ipAccessList,
57+
}
58+
err := opts.ValidateIpAccessList()
59+
assert.Equal(t, tt.err, err)
60+
})
61+
}
62+
}

internal/flag/flags.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ const (
3939
KubernetesClusterContext = "kubeContext" // KubeContext flag
4040
DataFederationName = "dataFederationName" // DataFederationName flag
4141
IndependentResources = "independentResources" // IndependentResources flag
42+
IPAccessList = "ipAccessList" // IPAccessList flag
4243
)

internal/kubernetes/operator/install.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"errors"
2020
"fmt"
21+
"strings"
2122

2223
"github.com/mongodb/atlas-cli-plugin-kubernetes/internal/kubernetes"
2324
"github.com/mongodb/atlas-cli-plugin-kubernetes/internal/kubernetes/operator/features"
@@ -53,6 +54,7 @@ type Install struct {
5354
importResources bool
5455
atlasGov bool
5556
configOnly bool
57+
ipAccessList string
5658
}
5759

5860
func (i *Install) WithConfigOnly(configOnly bool) *Install {
@@ -109,6 +111,11 @@ func (i *Install) Run(ctx context.Context, orgID string) error {
109111
return err
110112
}
111113

114+
err = i.addAPIKeyIPAccessList(orgID, keys.GetId())
115+
if err != nil {
116+
return err
117+
}
118+
112119
if err = i.installResources.InstallCRDs(ctx, i.version, len(i.watch) > 0); err != nil {
113120
return err
114121
}
@@ -204,6 +211,34 @@ func (i *Install) generateKeys(orgID string) (*admin.ApiKeyUserDetails, error) {
204211
return keys, nil
205212
}
206213

214+
func (i *Install) addAPIKeyIPAccessList(orgID, apiKeyID string) error {
215+
list := strings.Split(i.ipAccessList, ",")
216+
entries := make([]admin.UserAccessListRequest, 0, len(list))
217+
218+
for _, entry := range list {
219+
if strings.Contains(entry, "/") {
220+
entries = append(entries, admin.UserAccessListRequest{
221+
CidrBlock: &entry,
222+
})
223+
} else {
224+
entries = append(entries, admin.UserAccessListRequest{
225+
IpAddress: &entry,
226+
})
227+
}
228+
}
229+
230+
err := i.atlasStore.AddIPAccessList(
231+
orgID,
232+
apiKeyID,
233+
&entries,
234+
)
235+
if err != nil {
236+
return fmt.Errorf("failed to add IP access list to API key: %w", err)
237+
}
238+
239+
return nil
240+
}
241+
207242
func (i *Install) importAtlasResources(orgID, apiKeyID string) error {
208243
projectsIDs := make([]string, 0)
209244

@@ -326,6 +361,7 @@ func NewInstall(
326361
featureValidator features.FeatureValidator,
327362
kubectl *kubernetes.KubeCtl,
328363
version string,
364+
ipAccessList string,
329365
) *Install {
330366
return &Install{
331367
installResources: installer,
@@ -334,5 +370,6 @@ func NewInstall(
334370
featureValidator: featureValidator,
335371
kubectl: kubectl,
336372
version: version,
373+
ipAccessList: ipAccessList,
337374
}
338375
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2024 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 operator
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"testing"
23+
24+
"github.com/golang/mock/gomock"
25+
"github.com/mongodb/atlas-cli-plugin-kubernetes/internal/mocks"
26+
"github.com/stretchr/testify/assert"
27+
"go.mongodb.org/atlas-sdk/v20250312006/admin"
28+
)
29+
30+
func TestInstall_addAPIKeyIPAccessList(t *testing.T) {
31+
tests := map[string]struct {
32+
ipAccessList string
33+
expectedErr error
34+
}{
35+
"An IP is provided": {
36+
ipAccessList: "104.30.164.5",
37+
expectedErr: nil,
38+
},
39+
"An CIDR is provided": {
40+
ipAccessList: "192.168.100.177/24",
41+
expectedErr: nil,
42+
},
43+
"Multiple entries are provided": {
44+
ipAccessList: "104.30.164.5,192.168.100.177/24",
45+
expectedErr: nil,
46+
},
47+
"API failed to add ip access list": {
48+
ipAccessList: "104.30.164.5,192.168.100.177/24",
49+
expectedErr: fmt.Errorf("failed to add IP access list to API key: %w", errors.New("failed to add IP access list")),
50+
},
51+
}
52+
for name, tt := range tests {
53+
storeMock := mocks.NewMockOperatorGenericStore(gomock.NewController(t))
54+
storeMock.EXPECT().AddIPAccessList("orgID", "apiKeyID", gomock.Any()).
55+
DoAndReturn(func(string, string, *[]admin.UserAccessListRequest) error {
56+
if tt.expectedErr != nil {
57+
return errors.New("failed to add IP access list")
58+
}
59+
60+
return nil
61+
}).
62+
Times(1)
63+
64+
t.Run(name, func(t *testing.T) {
65+
i := &Install{
66+
ipAccessList: tt.ipAccessList,
67+
atlasStore: storeMock,
68+
}
69+
err := i.addAPIKeyIPAccessList("orgID", "apiKeyID")
70+
assert.Equal(t, tt.expectedErr, err)
71+
})
72+
}
73+
}

internal/mocks/mock_api_keys.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)