From 7b11f5360f756d430af389334fad9e4535382bb5 Mon Sep 17 00:00:00 2001 From: josvaz Date: Fri, 22 Nov 2024 15:10:54 +0100 Subject: [PATCH] CLOUDP-280230: Network peering translation layer (#1884) * Fix indirect go mod ref and update licenses * CLOUDP-280230: Network peering translation layer * Improve contract test helper Signed-off-by: jose.vazquez * Add network peering contract tests Signed-off-by: jose.vazquez --------- Signed-off-by: jose.vazquez --- .github/workflows/test-contract.yml | 9 + .licenses-gomod.sha256 | 2 +- Makefile | 18 +- go.mod | 2 +- .../translation/networkpeering/conversion.go | 180 ++++++++++++ .../networkpeering/conversion_test.go | 111 ++++++++ .../networkpeering/networkpeering.go | 147 ++++++++++ licenses.csv | 2 +- pkg/api/v1/atlasnetworkpeering_types.go | 75 +++++ pkg/api/v1/zz_generated.deepcopy.go | 90 ++++++ test/contract/audit/audit_test.go | 36 +-- .../networkpeering/networkpeering_test.go | 261 ++++++++++++++++++ test/helper/cloud/aws/aws.go | 20 ++ test/helper/cloud/aws/vpc.go | 54 ++++ test/helper/cloud/azure/azure.go | 55 ++++ test/helper/cloud/azure/vpc.go | 73 +++++ test/helper/cloud/google/google.go | 52 ++++ test/helper/cloud/google/vpc.go | 57 ++++ test/helper/contract/ako.go | 49 ---- test/helper/contract/contract.go | 105 ++++--- test/helper/contract/contract_test.go | 20 +- 21 files changed, 1294 insertions(+), 124 deletions(-) create mode 100644 internal/translation/networkpeering/conversion.go create mode 100644 internal/translation/networkpeering/conversion_test.go create mode 100644 internal/translation/networkpeering/networkpeering.go create mode 100644 pkg/api/v1/atlasnetworkpeering_types.go create mode 100644 test/contract/networkpeering/networkpeering_test.go create mode 100644 test/helper/cloud/aws/aws.go create mode 100644 test/helper/cloud/aws/vpc.go create mode 100644 test/helper/cloud/azure/azure.go create mode 100644 test/helper/cloud/azure/vpc.go create mode 100644 test/helper/cloud/google/google.go create mode 100644 test/helper/cloud/google/vpc.go diff --git a/.github/workflows/test-contract.yml b/.github/workflows/test-contract.yml index f277d99bc8..9f0a3d96d7 100644 --- a/.github/workflows/test-contract.yml +++ b/.github/workflows/test-contract.yml @@ -32,5 +32,14 @@ jobs: MCLI_ORG_ID: ${{ secrets.ATLAS_ORG_ID }} MCLI_PUBLIC_API_KEY: ${{ secrets.ATLAS_PUBLIC_KEY }} MCLI_PRIVATE_API_KEY: ${{ secrets.ATLAS_PRIVATE_KEY }} + AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets. AZURE_CLIENT_SECRET }} + GOOGLE_PROJECT_ID: ${{ secrets.GOOGLE_PROJECT_ID }} + GCP_SA_CRED: ${{ secrets.GCP_SA_CRED}} USE_KIND: "false" # Avoid launching a kind cluster yet again run: devbox run -- 'make contract-tests' diff --git a/.licenses-gomod.sha256 b/.licenses-gomod.sha256 index 8c76749c68..0f05cf48f0 100644 --- a/.licenses-gomod.sha256 +++ b/.licenses-gomod.sha256 @@ -1 +1 @@ -100644 8b729a99c24b7dbc21e36509be5899392215190f go.mod +100644 c22a2d48ac68bce318aa0942dfd8a5366a68e291 go.mod diff --git a/Makefile b/Makefile index 55bb0593f0..8a4fc85400 100644 --- a/Makefile +++ b/Makefile @@ -132,6 +132,10 @@ CONTAINER_SPEC=.spec.template.spec.containers[0] SILK_ASSET_GROUP="atlas-kubernetes-operator" +HELM_REPO_URL = "https://mongodb.github.io/helm-charts" +HELM_AKO_INSTALL_NAME = local-ako-install +HELM_AKO_NAMESPACE = test-ako + .DEFAULT_GOAL := help .PHONY: help help: ## Show this help screen @@ -567,7 +571,17 @@ download-from-silk: ## Download the latest augmented SBOM for a given architectu store-silk-sboms: download-from-silk ## Download & Store the latest augmented SBOM for a given version & architecture SILK_ASSET_GROUP=$(SILK_ASSET_GROUP) ./scripts/store-sbom-in-s3.sh $(VERSION) $(TARGET_ARCH) +.PHONY: install-ako-helm +install-ako-helm: + helm repo add mongodb $(HELM_REPO_URL) + helm upgrade $(HELM_AKO_INSTALL_NAME) mongodb/mongodb-atlas-operator --atomic --install \ + --set-string atlasURI=$(MCLI_OPS_MANAGER_URL) \ + --set objectDeletionProtection=false \ + --set subobjectDeletionProtection=false \ + --namespace=$(HELM_AKO_NAMESPACE) --create-namespace + kubectl get crds + .PHONY: contract-tests -contract-tests: run-kind ## Run contract tests +contract-tests: run-kind install-ako-helm ## Run contract tests with support by a k8s cluster and AKO go clean -testcache - AKO_CONTRACT_TEST=1 go test -v -race -cover ./test/contract/... + AKO_CONTRACT_TEST=1 HELM_AKO_NAMESPACE=$(HELM_AKO_NAMESPACE) go test -v -race -cover ./test/contract/... diff --git a/go.mod b/go.mod index 8b729a99c2..c22a2d48ac 100644 --- a/go.mod +++ b/go.mod @@ -121,7 +121,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect diff --git a/internal/translation/networkpeering/conversion.go b/internal/translation/networkpeering/conversion.go new file mode 100644 index 0000000000..39872b2828 --- /dev/null +++ b/internal/translation/networkpeering/conversion.go @@ -0,0 +1,180 @@ +package networkpeering + +import ( + "fmt" + + "go.mongodb.org/atlas-sdk/v20231115008/admin" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/provider" +) + +type NetworkPeer struct { + akov2.AtlasNetworkPeeringConfig + ID string +} + +func NewNetworkPeer(id string, cfg *akov2.AtlasNetworkPeeringConfig) *NetworkPeer { + return &NetworkPeer{ + AtlasNetworkPeeringConfig: *cfg, + ID: id, + } +} + +type ProviderContainer struct { + akov2.AtlasProviderContainerConfig + ID string + Provider string +} + +func NewProviderContainer(id string, provider string, cfg *akov2.AtlasProviderContainerConfig) *ProviderContainer { + return &ProviderContainer{ + AtlasProviderContainerConfig: *cfg, + ID: id, + Provider: provider, + } +} + +func toAtlasConnection(peer *NetworkPeer) (*admin.BaseNetworkPeeringConnectionSettings, error) { + switch peer.Provider { + case string(provider.ProviderAWS): + if peer.AWSConfiguration == nil { + return nil, fmt.Errorf("unsupported AWS peer with AWSConfiguration unset") + } + return &admin.BaseNetworkPeeringConnectionSettings{ + ContainerId: peer.ContainerID, + ProviderName: pointer.SetOrNil(peer.Provider, ""), + AccepterRegionName: pointer.SetOrNil(peer.AWSConfiguration.AccepterRegionName, ""), + AwsAccountId: pointer.SetOrNil(peer.AWSConfiguration.AWSAccountID, ""), + RouteTableCidrBlock: pointer.SetOrNil(peer.AWSConfiguration.RouteTableCIDRBlock, ""), + VpcId: pointer.SetOrNil(peer.AWSConfiguration.VpcID, ""), + }, nil + case string(provider.ProviderGCP): + if peer.GCPConfiguration == nil { + return nil, fmt.Errorf("unsupported Google peer with GCPConfiguration unset") + } + return &admin.BaseNetworkPeeringConnectionSettings{ + ContainerId: peer.ContainerID, + ProviderName: pointer.SetOrNil(peer.Provider, ""), + GcpProjectId: pointer.SetOrNil(peer.GCPConfiguration.GCPProjectID, ""), + NetworkName: pointer.SetOrNil(peer.GCPConfiguration.NetworkName, ""), + }, nil + case string(provider.ProviderAzure): + if peer.AzureConfiguration == nil { + return nil, fmt.Errorf("unsupported Azure peer with AzureConfiguration unset") + } + return &admin.BaseNetworkPeeringConnectionSettings{ + ContainerId: peer.ContainerID, + ProviderName: pointer.SetOrNil(peer.Provider, ""), + AzureDirectoryId: pointer.SetOrNil(peer.AzureConfiguration.AzureDirectoryID, ""), + AzureSubscriptionId: pointer.SetOrNil(peer.AzureConfiguration.AzureSubscriptionID, ""), + ResourceGroupName: pointer.SetOrNil(peer.AzureConfiguration.ResourceGroupName, ""), + VnetName: pointer.SetOrNil(peer.AzureConfiguration.VNetName, ""), + }, nil + default: + return nil, fmt.Errorf("unsupported provider %q", peer.Provider) + } +} + +func fromAtlasConnection(conn *admin.BaseNetworkPeeringConnectionSettings) (*NetworkPeer, error) { + switch provider.ProviderName(conn.GetProviderName()) { + case provider.ProviderAWS: + return &NetworkPeer{ + ID: conn.GetId(), + AtlasNetworkPeeringConfig: akov2.AtlasNetworkPeeringConfig{ + ContainerID: conn.GetContainerId(), + Provider: conn.GetProviderName(), + AWSConfiguration: &akov2.AWSNetworkPeeringConfiguration{ + AccepterRegionName: conn.GetAccepterRegionName(), + AWSAccountID: conn.GetAwsAccountId(), + RouteTableCIDRBlock: conn.GetRouteTableCidrBlock(), + VpcID: conn.GetVpcId(), + }, + }, + }, nil + case provider.ProviderGCP: + return &NetworkPeer{ + ID: conn.GetId(), + AtlasNetworkPeeringConfig: akov2.AtlasNetworkPeeringConfig{ + ContainerID: conn.GetContainerId(), + Provider: conn.GetProviderName(), + GCPConfiguration: &akov2.GCPNetworkPeeringConfiguration{ + GCPProjectID: conn.GetGcpProjectId(), + NetworkName: conn.GetNetworkName(), + }, + }, + }, nil + case provider.ProviderAzure: + return &NetworkPeer{ + ID: conn.GetId(), + AtlasNetworkPeeringConfig: akov2.AtlasNetworkPeeringConfig{ + ContainerID: conn.GetContainerId(), + Provider: conn.GetProviderName(), + AzureConfiguration: &akov2.AzureNetworkPeeringConfiguration{ + AzureDirectoryID: conn.GetAzureDirectoryId(), + AzureSubscriptionID: conn.GetAzureSubscriptionId(), + ResourceGroupName: conn.GetResourceGroupName(), + VNetName: conn.GetVnetName(), + }, + }, + }, nil + default: + return nil, fmt.Errorf("unsupported provider %q", conn.GetProviderName()) + } +} + +func fromAtlasConnectionList(list []admin.BaseNetworkPeeringConnectionSettings) ([]NetworkPeer, error) { + if list == nil { + return nil, nil + } + peers := make([]NetworkPeer, 0, len(list)) + for i, conn := range list { + c, err := fromAtlasConnection(&conn) + if err != nil { + return nil, fmt.Errorf("failed to convert connection list item %d: %w", i, err) + } + peers = append(peers, *c) + } + return peers, nil +} + +func toAtlasContainer(container *ProviderContainer) *admin.CloudProviderContainer { + cpc := &admin.CloudProviderContainer{ + Id: pointer.SetOrNil(container.ID, ""), + ProviderName: pointer.SetOrNil(container.Provider, ""), + AtlasCidrBlock: pointer.SetOrNil(container.AtlasCIDRBlock, ""), + } + if cpc.GetProviderName() == string(provider.ProviderAWS) { + cpc.RegionName = pointer.SetOrNil(container.ContainerRegion, "") + } else { + cpc.Region = pointer.SetOrNil(container.ContainerRegion, "") + } + return cpc +} + +func fromAtlasContainer(container *admin.CloudProviderContainer) *ProviderContainer { + region := container.GetRegion() + if container.GetProviderName() == string(provider.ProviderAWS) { + region = container.GetRegionName() + } + return &ProviderContainer{ + ID: container.GetId(), + Provider: container.GetProviderName(), + AtlasProviderContainerConfig: akov2.AtlasProviderContainerConfig{ + AtlasCIDRBlock: container.GetAtlasCidrBlock(), + ContainerRegion: region, + }, + } +} + +func fromAtlasContainerList(list []admin.CloudProviderContainer) []ProviderContainer { + if list == nil { + return nil + } + containers := make([]ProviderContainer, 0, len(list)) + for _, container := range list { + containers = append(containers, *fromAtlasContainer(&container)) + } + return containers +} diff --git a/internal/translation/networkpeering/conversion_test.go b/internal/translation/networkpeering/conversion_test.go new file mode 100644 index 0000000000..bca570a54c --- /dev/null +++ b/internal/translation/networkpeering/conversion_test.go @@ -0,0 +1,111 @@ +package networkpeering + +import ( + "fmt" + "testing" + + gofuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/atlas-sdk/v20231115008/admin" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/provider" +) + +const fuzzIterations = 100 + +var providerNames = []string{ + string(provider.ProviderAWS), + string(provider.ProviderAzure), + string(provider.ProviderGCP), +} + +func FuzzConvertConnection(f *testing.F) { + for i := uint(0); i < fuzzIterations; i++ { + f.Add(([]byte)(fmt.Sprintf("seed sample %x", i)), i) + } + f.Fuzz(func(t *testing.T, data []byte, index uint) { + peerData := NetworkPeer{} + gofuzz.NewFromGoFuzz(data).Fuzz(&peerData) + peerData.Provider = providerNames[index%3] + cleanupPeer(&peerData) + atlasConn, err := toAtlasConnection(&peerData) + require.NoError(t, err) + result, err := fromAtlasConnection(atlasConn) + require.NoError(t, err) + assert.Equal(t, &peerData, result, "failed for index=%d", index) + }) +} + +func FuzzConvertListOfConnections(f *testing.F) { + for i := uint(0); i < fuzzIterations; i++ { + f.Add(([]byte)(fmt.Sprintf("seed sample %x", i)), i, (i % 5)) + } + f.Fuzz(func(t *testing.T, data []byte, index uint, size uint) { + conns := []admin.BaseNetworkPeeringConnectionSettings{} + expected := []NetworkPeer{} + for i := uint(0); i < size; i++ { + peerData := NetworkPeer{} + gofuzz.NewFromGoFuzz(data).Fuzz(&peerData) + peerData.Provider = providerNames[index%3] + cleanupPeer(&peerData) + atlasConn, err := toAtlasConnection(&peerData) + require.NoError(t, err) + expectedConn, err := fromAtlasConnection(atlasConn) + require.NoError(t, err) + expected = append(expected, *expectedConn) + atlasConnItem, err := toAtlasConnection(&peerData) + require.NoError(t, err) + conns = append(conns, *atlasConnItem) + } + result, err := fromAtlasConnectionList(conns) + require.NoError(t, err) + assert.Equal(t, expected, result) + }) +} + +func FuzzConvertContainer(f *testing.F) { + for i := uint(0); i < fuzzIterations; i++ { + f.Add(([]byte)(fmt.Sprintf("seed sample %x", i)), i) + } + f.Fuzz(func(t *testing.T, data []byte, index uint) { + containerData := ProviderContainer{} + gofuzz.NewFromGoFuzz(data).Fuzz(&containerData) + containerData.Provider = providerNames[index%3] + result := fromAtlasContainer(toAtlasContainer(&containerData)) + assert.Equal(t, &containerData, result, "failed for index=%d", index) + }) +} + +func FuzzConvertListOfContainers(f *testing.F) { + for i := uint(0); i < fuzzIterations; i++ { + f.Add(([]byte)(fmt.Sprintf("seed sample %x", i)), i, (i % 5)) + } + f.Fuzz(func(t *testing.T, data []byte, index uint, size uint) { + containers := []admin.CloudProviderContainer{} + expected := []ProviderContainer{} + for i := uint(0); i < size; i++ { + containerData := ProviderContainer{} + gofuzz.NewFromGoFuzz(data).Fuzz(&containerData) + containerData.Provider = providerNames[index%3] + expectedContainer := fromAtlasContainer(toAtlasContainer(&containerData)) + expected = append(expected, *expectedContainer) + containers = append(containers, *toAtlasContainer(&containerData)) + } + result := fromAtlasContainerList(containers) + assert.Equal(t, expected, result) + }) +} + +func cleanupPeer(peer *NetworkPeer) { + peer.ID = "" + if peer.Provider != string(provider.ProviderAWS) { + peer.AWSConfiguration = nil + } + if peer.Provider != string(provider.ProviderGCP) { + peer.GCPConfiguration = nil + } + if peer.Provider != string(provider.ProviderAzure) { + peer.AzureConfiguration = nil + } +} diff --git a/internal/translation/networkpeering/networkpeering.go b/internal/translation/networkpeering/networkpeering.go new file mode 100644 index 0000000000..6a2ba1c65c --- /dev/null +++ b/internal/translation/networkpeering/networkpeering.go @@ -0,0 +1,147 @@ +package networkpeering + +import ( + "context" + "fmt" + + "go.mongodb.org/atlas-sdk/v20231115008/admin" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/provider" +) + +type PeerConnectionsService interface { + CreatePeer(ctx context.Context, projectID string, conn *NetworkPeer) (*NetworkPeer, error) + ListPeers(ctx context.Context, projectID string) ([]NetworkPeer, error) + DeletePeer(ctx context.Context, projectID, containerID string) error +} + +type PeeringContainerService interface { + CreateContainer(ctx context.Context, projectID string, container *ProviderContainer) (*ProviderContainer, error) + GetContainer(ctx context.Context, projectID, containerID string) (*ProviderContainer, error) + ListContainers(ctx context.Context, projectID, providerName string) ([]ProviderContainer, error) + DeleteContainer(ctx context.Context, projectID, containerID string) error +} + +type NetworkPeeringService interface { + PeerConnectionsService + PeeringContainerService +} + +type networkPeeringService struct { + peeringAPI admin.NetworkPeeringApi +} + +func NewNetworkPeeringService(peeringAPI admin.NetworkPeeringApi) NetworkPeeringService { + return &networkPeeringService{peeringAPI: peeringAPI} +} + +func (np *networkPeeringService) CreatePeer(ctx context.Context, projectID string, conn *NetworkPeer) (*NetworkPeer, error) { + atlasConnRequest, err := toAtlasConnection(conn) + if err != nil { + return nil, fmt.Errorf("failed to convert peer to Atlas: %w", err) + } + newAtlasConn, _, err := np.peeringAPI.CreatePeeringConnection(ctx, projectID, atlasConnRequest).Execute() + if err != nil { + return nil, fmt.Errorf("failed to create network peer %v: %w", conn, err) + } + newConn, err := fromAtlasConnection(newAtlasConn) + if err != nil { + return nil, fmt.Errorf("failed to convert peer from Atlas: %w", err) + } + return newConn, nil +} + +func (np *networkPeeringService) ListPeers(ctx context.Context, projectID string) ([]NetworkPeer, error) { + var peersList []NetworkPeer + providers := []provider.ProviderName{provider.ProviderAWS, provider.ProviderAzure, provider.ProviderGCP} + for _, providerName := range providers { + peers, err := np.listPeersForProvider(ctx, projectID, providerName) + if err != nil { + return nil, fmt.Errorf("failed to list network peers for %s: %w", string(providerName), err) + } + peersList = append(peersList, peers...) + } + return peersList, nil +} + +func (np *networkPeeringService) listPeersForProvider(ctx context.Context, projectID string, providerName provider.ProviderName) ([]NetworkPeer, error) { + results := []NetworkPeer{} + pageNum := 1 + listOpts := &admin.ListPeeringConnectionsApiParams{ + GroupId: projectID, + ProviderName: admin.PtrString(string(providerName)), + PageNum: pointer.MakePtr(pageNum), + } + for { + page, _, err := np.peeringAPI.ListPeeringConnectionsWithParams(ctx, listOpts).Execute() + if err != nil { + return nil, fmt.Errorf("failed to list network peers: %w", err) + } + list, err := fromAtlasConnectionList(page.GetResults()) + if err != nil { + return nil, fmt.Errorf("failed to convert results to peer list: %w", err) + } + results = append(results, list...) + if len(results) >= page.GetTotalCount() { + return results, nil + } + pageNum += 1 + } +} + +func (np *networkPeeringService) DeletePeer(ctx context.Context, projectID, peerID string) error { + _, _, err := np.peeringAPI.DeletePeeringConnection(ctx, projectID, peerID).Execute() + if err != nil { + return fmt.Errorf("failed to delete peering connection for peer %s: %w", peerID, err) + } + return nil +} + +func (np *networkPeeringService) CreateContainer(ctx context.Context, projectID string, container *ProviderContainer) (*ProviderContainer, error) { + newContainer, _, err := np.peeringAPI.CreatePeeringContainer(ctx, projectID, toAtlasContainer(container)).Execute() + if err != nil { + return nil, fmt.Errorf("failed to create peering container %s: %w", container.ID, err) + } + return fromAtlasContainer(newContainer), nil +} + +func (np *networkPeeringService) GetContainer(ctx context.Context, projectID, containerID string) (*ProviderContainer, error) { + container, _, err := np.peeringAPI.GetPeeringContainer(ctx, projectID, containerID).Execute() + if err != nil { + return nil, fmt.Errorf("failed to get container for gcp status %s: %w", containerID, err) + } + return fromAtlasContainer(container), nil +} + +func (np *networkPeeringService) ListContainers(ctx context.Context, projectID, providerName string) ([]ProviderContainer, error) { + results := []ProviderContainer{} + pageNum := 1 + listOpts := &admin.ListPeeringContainerByCloudProviderApiParams{ + GroupId: projectID, + ProviderName: pointer.SetOrNil(providerName, ""), + PageNum: pointer.MakePtr(pageNum), + } + for { + page, _, err := np.peeringAPI.ListPeeringContainerByCloudProviderWithParams(ctx, listOpts).Execute() + if err != nil { + return nil, fmt.Errorf("failed to list containers: %w", err) + } + results = append(results, fromAtlasContainerList(page.GetResults())...) + if len(results) >= page.GetTotalCount() { + return results, nil + } + pageNum += 1 + } +} + +func (np *networkPeeringService) DeleteContainer(ctx context.Context, projectID, containerID string) error { + _, _, err := np.peeringAPI.DeletePeeringContainer(ctx, projectID, containerID).Execute() + if admin.IsErrorCode(err, "CLOUD_PROVIDER_CONTAINER_NOT_FOUND") { + return nil + } + if err != nil { + return fmt.Errorf("failed to delete container: %w", err) + } + return nil +} diff --git a/licenses.csv b/licenses.csv index c62103db02..a94888236e 100644 --- a/licenses.csv +++ b/licenses.csv @@ -113,7 +113,7 @@ golang.org/x/exp,https://cs.opensource.google/go/x/exp/+/8a7402ab:LICENSE,BSD-3- golang.org/x/net,https://cs.opensource.google/go/x/net/+/v0.31.0:LICENSE,BSD-3-Clause golang.org/x/oauth2,https://cs.opensource.google/go/x/oauth2/+/v0.24.0:LICENSE,BSD-3-Clause golang.org/x/sync,https://cs.opensource.google/go/x/sync/+/v0.9.0:LICENSE,BSD-3-Clause -golang.org/x/sys,https://cs.opensource.google/go/x/sys/+/v0.27.0:LICENSE,BSD-3-Clause +golang.org/x/sys/unix,https://cs.opensource.google/go/x/sys/+/v0.27.0:LICENSE,BSD-3-Clause golang.org/x/term,https://cs.opensource.google/go/x/term/+/v0.26.0:LICENSE,BSD-3-Clause golang.org/x/text,https://cs.opensource.google/go/x/text/+/v0.20.0:LICENSE,BSD-3-Clause golang.org/x/time/rate,https://cs.opensource.google/go/x/time/+/v0.8.0:LICENSE,BSD-3-Clause diff --git a/pkg/api/v1/atlasnetworkpeering_types.go b/pkg/api/v1/atlasnetworkpeering_types.go new file mode 100644 index 0000000000..1829befba8 --- /dev/null +++ b/pkg/api/v1/atlasnetworkpeering_types.go @@ -0,0 +1,75 @@ +/* +Copyright 2024 MongoDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +type AtlasNetworkPeeringConfig struct { + // Name of the cloud service provider for which you want to create the network peering service. + // +kubebuilder:validation:Enum=AWS;GCP;AZURE + // +kubebuilder:validation:Required + Provider string `json:"provider"` + // ID of the network peer container. If not set, operator will create a new container with ContainerRegion and AtlasCIDRBlock input. + // +optional + ContainerID string `json:"containerId"` + + // AWSConfiguration is the specific AWS settings for network peering + // +kubebuilder:validation:Optional + AWSConfiguration *AWSNetworkPeeringConfiguration `json:"awsConfiguration,omitempty"` + // AzureConfiguration is the specific Azure settings for network peering + // +kubebuilder:validation:Optional + AzureConfiguration *AzureNetworkPeeringConfiguration `json:"azureConfiguration,omitempty"` + // GCPConfiguration is the specific Google Cloud settings for network peering + // +kubebuilder:validation:Optional + GCPConfiguration *GCPNetworkPeeringConfiguration `json:"gcpConfiguration,omitempty"` +} + +type AtlasProviderContainerConfig struct { + // ContainerRegion is the provider region name of Atlas network peer container. If not set, AccepterRegionName is used. + // +optional + ContainerRegion string `json:"containerRegion"` + // Atlas CIDR. It needs to be set if ContainerID is not set. + // +optional + AtlasCIDRBlock string `json:"atlasCidrBlock"` +} + +type AWSNetworkPeeringConfiguration struct { + // AccepterRegionName is the provider region name of user's vpc. + AccepterRegionName string `json:"accepterRegionName"` + // AccountID of the user's vpc. + AWSAccountID string `json:"awsAccountId,omitempty"` + // User VPC CIDR. + RouteTableCIDRBlock string `json:"routeTableCidrBlock,omitempty"` + // AWS VPC ID. + VpcID string `json:"vpcId,omitempty"` +} + +type AzureNetworkPeeringConfiguration struct { + //AzureDirectoryID is the unique identifier for an Azure AD directory. + AzureDirectoryID string `json:"azureDirectoryId,omitempty"` + // AzureSubscriptionID is the unique identifier of the Azure subscription in which the VNet resides. + AzureSubscriptionID string `json:"azureSubscriptionId,omitempty"` + //ResourceGroupName is the name of your Azure resource group. + ResourceGroupName string `json:"resourceGroupName,omitempty"` + // VNetName is name of your Azure VNet. Its applicable only for Azure. + VNetName string `json:"vnetName,omitempty"` +} + +type GCPNetworkPeeringConfiguration struct { + // User GCP Project ID. Its applicable only for GCP. + GCPProjectID string `json:"gcpProjectId,omitempty"` + // GCP Network Peer Name. Its applicable only for GCP. + NetworkName string `json:"networkName,omitempty"` +} diff --git a/pkg/api/v1/zz_generated.deepcopy.go b/pkg/api/v1/zz_generated.deepcopy.go index aa17f088a0..0fa84cd6a4 100644 --- a/pkg/api/v1/zz_generated.deepcopy.go +++ b/pkg/api/v1/zz_generated.deepcopy.go @@ -20,6 +20,21 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/project" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSNetworkPeeringConfiguration) DeepCopyInto(out *AWSNetworkPeeringConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSNetworkPeeringConfiguration. +func (in *AWSNetworkPeeringConfiguration) DeepCopy() *AWSNetworkPeeringConfiguration { + if in == nil { + return nil + } + out := new(AWSNetworkPeeringConfiguration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AWSPrivateEndpointConfiguration) DeepCopyInto(out *AWSPrivateEndpointConfiguration) { *out = *in @@ -1024,6 +1039,36 @@ func (in *AtlasFederatedAuthSpec) DeepCopy() *AtlasFederatedAuthSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AtlasNetworkPeeringConfig) DeepCopyInto(out *AtlasNetworkPeeringConfig) { + *out = *in + if in.AWSConfiguration != nil { + in, out := &in.AWSConfiguration, &out.AWSConfiguration + *out = new(AWSNetworkPeeringConfiguration) + **out = **in + } + if in.AzureConfiguration != nil { + in, out := &in.AzureConfiguration, &out.AzureConfiguration + *out = new(AzureNetworkPeeringConfiguration) + **out = **in + } + if in.GCPConfiguration != nil { + in, out := &in.GCPConfiguration, &out.GCPConfiguration + *out = new(GCPNetworkPeeringConfiguration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AtlasNetworkPeeringConfig. +func (in *AtlasNetworkPeeringConfig) DeepCopy() *AtlasNetworkPeeringConfig { + if in == nil { + return nil + } + out := new(AtlasNetworkPeeringConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AtlasOnDemandPolicy) DeepCopyInto(out *AtlasOnDemandPolicy) { *out = *in @@ -1299,6 +1344,21 @@ func (in *AtlasProjectSpec) DeepCopy() *AtlasProjectSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AtlasProviderContainerConfig) DeepCopyInto(out *AtlasProviderContainerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AtlasProviderContainerConfig. +func (in *AtlasProviderContainerConfig) DeepCopy() *AtlasProviderContainerConfig { + if in == nil { + return nil + } + out := new(AtlasProviderContainerConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AtlasSearchIndexAnalyzer) DeepCopyInto(out *AtlasSearchIndexAnalyzer) { *out = *in @@ -1741,6 +1801,21 @@ func (in *AzureKeyVault) DeepCopy() *AzureKeyVault { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzureNetworkPeeringConfiguration) DeepCopyInto(out *AzureNetworkPeeringConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureNetworkPeeringConfiguration. +func (in *AzureNetworkPeeringConfiguration) DeepCopy() *AzureNetworkPeeringConfiguration { + if in == nil { + return nil + } + out := new(AzureNetworkPeeringConfiguration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzurePrivateEndpointConfiguration) DeepCopyInto(out *AzurePrivateEndpointConfiguration) { *out = *in @@ -2279,6 +2354,21 @@ func (in GCPEndpoints) DeepCopy() GCPEndpoints { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPNetworkPeeringConfiguration) DeepCopyInto(out *GCPNetworkPeeringConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPNetworkPeeringConfiguration. +func (in *GCPNetworkPeeringConfiguration) DeepCopy() *GCPNetworkPeeringConfiguration { + if in == nil { + return nil + } + out := new(GCPNetworkPeeringConfiguration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GCPPrivateEndpoint) DeepCopyInto(out *GCPPrivateEndpoint) { *out = *in diff --git a/test/contract/audit/audit_test.go b/test/contract/audit/audit_test.go index 86440cd52c..192aba66ec 100644 --- a/test/contract/audit/audit_test.go +++ b/test/contract/audit/audit_test.go @@ -8,8 +8,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/audit" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" @@ -17,19 +15,22 @@ import ( ) func TestDefaultAuditingGet(t *testing.T) { - contract.RunContractTest(t, "get default auditing", func(ct *contract.ContractTest) { + ctx := context.Background() + contract.RunGoContractTest(ctx, t, "get default auditing", func(ch contract.ContractHelper) { projectName := "default-auditing-project" - ct.AddResources(time.Minute, contract.DefaultAtlasProject(projectName)) - testProjectID := mustReadProjectID(t, ct.Ctx, ct.K8sClient, ct.Namespace(), projectName) - as := audit.NewAuditLog(ct.AtlasClient.AuditingApi) + require.NoError(t, ch.AddResources(ctx, time.Minute, contract.DefaultAtlasProject(projectName))) + testProjectID, err := ch.ProjectID(ctx, projectName) + require.NoError(t, err) + as := audit.NewAuditLog(ch.AtlasClient().AuditingApi) - result, err := as.Get(ct.Ctx, testProjectID) + result, err := as.Get(ctx, testProjectID) require.NoError(t, err) assert.Equal(t, audit.NewAuditConfig(nil), result) }) } func TestSyncs(t *testing.T) { + ctx := context.Background() testCases := []struct { title string auditing *audit.AuditConfig @@ -86,12 +87,12 @@ func TestSyncs(t *testing.T) { ), }, } - contract.RunContractTest(t, "test syncs", func(ct *contract.ContractTest) { + contract.RunGoContractTest(ctx, t, "test syncs", func(ch contract.ContractHelper) { projectName := "audit-syncs-project" - ct.AddResources(time.Minute, contract.DefaultAtlasProject(projectName)) - testProjectID := mustReadProjectID(t, ct.Ctx, ct.K8sClient, ct.Namespace(), projectName) - ctx := context.Background() - as := audit.NewAuditLog(ct.AtlasClient.AuditingApi) + require.NoError(t, ch.AddResources(ctx, time.Minute, contract.DefaultAtlasProject(projectName))) + testProjectID, err := ch.ProjectID(ctx, projectName) + require.NoError(t, err) + as := audit.NewAuditLog(ch.AtlasClient().AuditingApi) for _, tc := range testCases { t.Run(tc.title, func(t *testing.T) { @@ -105,14 +106,3 @@ func TestSyncs(t *testing.T) { } }) } - -func mustReadProjectID(t *testing.T, ctx context.Context, k8sClient client.Client, ns, projectName string) string { - t.Helper() - project := akov2.AtlasProject{} - key := types.NamespacedName{ - Namespace: ns, - Name: projectName, - } - require.NoError(t, k8sClient.Get(ctx, key, &project)) - return project.Status.ID -} diff --git a/test/contract/networkpeering/networkpeering_test.go b/test/contract/networkpeering/networkpeering_test.go new file mode 100644 index 0000000000..2fbb6b28e6 --- /dev/null +++ b/test/contract/networkpeering/networkpeering_test.go @@ -0,0 +1,261 @@ +package networkpeering + +import ( + "context" + _ "embed" + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/atlas-sdk/v20231115004/admin" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/networkpeering" + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/cloud/aws" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/cloud/azure" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/cloud/google" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/contract" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e/utils" +) + +const ( + testVPCName = "ako-test-network-peering-vpc" +) + +func TestPeerContainerServiceCRUD(t *testing.T) { + ctx := context.Background() + contract.RunGoContractTest(ctx, t, "test container CRUD", func(ch contract.ContractHelper) { + projectName := "peer-container-crud-project" + require.NoError(t, ch.AddResources(ctx, time.Minute, contract.DefaultAtlasProject(projectName))) + testProjectID, err := ch.ProjectID(ctx, projectName) + require.NoError(t, err) + nps := networkpeering.NewNetworkPeeringService(ch.AtlasClient().NetworkPeeringApi) + cs := nps.(networkpeering.PeeringContainerService) + for _, tc := range []struct { + provider string + container *networkpeering.ProviderContainer + }{ + { + provider: "AWS", + container: testAWSPeeringContainer("10.1.0.0/21"), + }, + { + provider: "Azure", + container: testAzurePeeringContainer("10.2.0.0/21"), + }, + { + provider: "Google", + container: testGooglePeeringContainer("10.3.0.0/18"), // .../21 is not allowed in GCP + }, + } { + createdContainer := &networkpeering.ProviderContainer{} + t.Run(fmt.Sprintf("create %s container", tc.provider), func(t *testing.T) { + newContainer, err := cs.CreateContainer(ctx, testProjectID, tc.container) + require.NoError(t, err) + assert.NotEmpty(t, newContainer.ID) + createdContainer = newContainer + }) + + t.Run(fmt.Sprintf("list %s containers", tc.provider), func(t *testing.T) { + containers, err := cs.ListContainers(ctx, testProjectID, tc.container.Provider) + require.NoError(t, err) + assert.NotEmpty(t, containers) + assert.Len(t, containers, 1) + }) + + t.Run(fmt.Sprintf("get %s container", tc.provider), func(t *testing.T) { + container, err := cs.GetContainer(ctx, testProjectID, createdContainer.ID) + require.NoError(t, err) + assert.NotEmpty(t, container) + assert.Equal(t, createdContainer.ID, container.ID) + assert.Equal(t, tc.container.ContainerRegion, container.ContainerRegion) + assert.Equal(t, tc.container.AtlasCIDRBlock, container.AtlasCIDRBlock) + }) + + t.Run(fmt.Sprintf("delete %s container", tc.provider), func(t *testing.T) { + time.Sleep(time.Second) // Atlas may reject removal if it happened within a second of creation + assert.NoErrorf(t, ignoreRemoved(cs.DeleteContainer(ctx, testProjectID, createdContainer.ID)), + "failed cleanup for provider %s Atlas project ID %s and container id %s", + tc.provider, testProjectID, createdContainer.ID) + }) + } + }) +} + +func TestPeerServiceCRUD(t *testing.T) { + ctx := context.Background() + contract.RunGoContractTest(ctx, t, "test container CRUD", func(ch contract.ContractHelper) { + projectName := "peer-connection-crud-project" + require.NoError(t, ch.AddResources(ctx, time.Minute, contract.DefaultAtlasProject(projectName))) + testProjectID, err := ch.ProjectID(ctx, projectName) + require.NoError(t, err) + nps := networkpeering.NewNetworkPeeringService(ch.AtlasClient().NetworkPeeringApi) + ps := nps.(networkpeering.PeerConnectionsService) + createdPeer := &networkpeering.NetworkPeer{} + + for _, tc := range []struct { + provider string + preparedCloudTest func(func(peerRequest *networkpeering.NetworkPeer)) + }{ + { + provider: "AWS", + preparedCloudTest: func(performTest func(*networkpeering.NetworkPeer)) { + testContainer := testAWSPeeringContainer("10.10.0.0/21") + awsRegionName := aws.RegionCode(testContainer.ContainerRegion) + vpcCIDR := "10.11.0.0/21" + awsVPCid, err := aws.CreateVPC(utils.RandomName(testVPCName), vpcCIDR, awsRegionName) + require.NoError(t, err) + newContainer, err := nps.CreateContainer(ctx, testProjectID, testContainer) + require.NoError(t, err) + assert.NotEmpty(t, newContainer.ID) + defer func() { + require.NoError(t, aws.DeleteVPC(awsVPCid, awsRegionName)) + }() + performTest(testAWSPeerConnection(t, newContainer.ID, vpcCIDR, awsVPCid)) + }, + }, + { + provider: "AZURE", + preparedCloudTest: func(performTest func(*networkpeering.NetworkPeer)) { + testContainer := testAzurePeeringContainer("10.20.0.0/21") + azureRegionName := azure.RegionCode(testContainer.ContainerRegion) + vpcCIDR := "10.21.0.0/21" + azureVPC, err := azure.CreateVPC(ctx, utils.RandomName(testVPCName), vpcCIDR, azureRegionName) + require.NoError(t, err) + newContainer, err := nps.CreateContainer(ctx, testProjectID, testContainer) + require.NoError(t, err) + assert.NotEmpty(t, newContainer.ID) + defer func() { + require.NoError(t, azure.DeleteVPC(ctx, azureVPC)) + }() + performTest(testAzurePeerConnection(t, newContainer.ID, azureVPC)) + }, + }, + { + provider: "GOOGLE", + preparedCloudTest: func(performTest func(*networkpeering.NetworkPeer)) { + testContainer := testGooglePeeringContainer("10.30.0.0/18") + vpcName := utils.RandomName(testVPCName) + require.NoError(t, google.CreateVPC(ctx, vpcName)) + newContainer, err := nps.CreateContainer(ctx, testProjectID, testContainer) + require.NoError(t, err) + assert.NotEmpty(t, newContainer.ID) + defer func() { + require.NoError(t, google.DeleteVPC(ctx, vpcName)) + }() + performTest(testGooglePeerConnection(t, newContainer.ID, vpcName)) + }, + }, + } { + tc.preparedCloudTest(func(peerRequest *networkpeering.NetworkPeer) { + t.Run(fmt.Sprintf("create %s peer connection", tc.provider), func(t *testing.T) { + newPeer, err := ps.CreatePeer(ctx, testProjectID, peerRequest) + require.NoError(t, err) + assert.NotEmpty(t, newPeer) + createdPeer = newPeer + }) + + t.Run(fmt.Sprintf("list %s peer connections", tc.provider), func(t *testing.T) { + containers, err := ps.ListPeers(ctx, testProjectID) + require.NoError(t, err) + assert.NotEmpty(t, containers) + assert.Len(t, containers, 1) + }) + + t.Run(fmt.Sprintf("delete %s peer connection", tc.provider), func(t *testing.T) { + assert.NoError(t, ps.DeletePeer(ctx, testProjectID, createdPeer.ID)) + }) + }) + } + }) +} + +func testAWSPeeringContainer(cidr string) *networkpeering.ProviderContainer { + return &networkpeering.ProviderContainer{ + Provider: "AWS", + AtlasProviderContainerConfig: akov2.AtlasProviderContainerConfig{ + ContainerRegion: "US_EAST_1", + AtlasCIDRBlock: cidr, + }, + } +} + +func testAzurePeeringContainer(cidr string) *networkpeering.ProviderContainer { + return &networkpeering.ProviderContainer{ + Provider: "AZURE", + AtlasProviderContainerConfig: akov2.AtlasProviderContainerConfig{ + ContainerRegion: "US_EAST_2", + AtlasCIDRBlock: cidr, + }, + } +} + +func testGooglePeeringContainer(cidr string) *networkpeering.ProviderContainer { + return &networkpeering.ProviderContainer{ + Provider: "GCP", + AtlasProviderContainerConfig: akov2.AtlasProviderContainerConfig{ + AtlasCIDRBlock: cidr, + }, + } +} + +func testAWSPeerConnection(t *testing.T, containerID string, vpcCIDR, vpcID string) *networkpeering.NetworkPeer { + return &networkpeering.NetworkPeer{ + AtlasNetworkPeeringConfig: akov2.AtlasNetworkPeeringConfig{ + Provider: "AWS", + ContainerID: containerID, + AWSConfiguration: &akov2.AWSNetworkPeeringConfiguration{ + AWSAccountID: mustHaveEnvVar(t, "AWS_ACCOUNT_ID"), + AccepterRegionName: "us-east-1", + RouteTableCIDRBlock: vpcCIDR, + VpcID: vpcID, + }, + }, + } +} + +func testAzurePeerConnection(t *testing.T, containerID string, vpcName string) *networkpeering.NetworkPeer { + return &networkpeering.NetworkPeer{ + AtlasNetworkPeeringConfig: akov2.AtlasNetworkPeeringConfig{ + Provider: "AZURE", + ContainerID: containerID, + AzureConfiguration: &akov2.AzureNetworkPeeringConfiguration{ + AzureDirectoryID: mustHaveEnvVar(t, "AZURE_TENANT_ID"), + AzureSubscriptionID: mustHaveEnvVar(t, "AZURE_SUBSCRIPTION_ID"), + ResourceGroupName: azure.TestResourceGroupName(), + VNetName: vpcName, + }, + }, + } +} + +func testGooglePeerConnection(t *testing.T, containerID string, vpcName string) *networkpeering.NetworkPeer { + return &networkpeering.NetworkPeer{ + AtlasNetworkPeeringConfig: akov2.AtlasNetworkPeeringConfig{ + Provider: "GCP", + ContainerID: containerID, + GCPConfiguration: &akov2.GCPNetworkPeeringConfiguration{ + GCPProjectID: mustHaveEnvVar(t, "GOOGLE_PROJECT_ID"), + NetworkName: vpcName, + }, + }, + } +} + +func mustHaveEnvVar(t *testing.T, name string) string { + value, ok := os.LookupEnv(name) + if !ok { + t.Fatalf("Unexpected unset env var %q", name) + } + return value +} + +func ignoreRemoved(err error) error { + if admin.IsErrorCode(err, "CLOUD_PROVIDER_CONTAINER_NOT_FOUND") { + return nil + } + return err +} diff --git a/test/helper/cloud/aws/aws.go b/test/helper/cloud/aws/aws.go new file mode 100644 index 0000000000..8bcc142484 --- /dev/null +++ b/test/helper/cloud/aws/aws.go @@ -0,0 +1,20 @@ +package aws + +import ( + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" +) + +func RegionCode(region string) string { + return strings.ReplaceAll(strings.ToLower(region), "_", "-") +} + +func newSession(region string) (*session.Session, error) { + awsSession, err := session.NewSession(aws.NewConfig().WithRegion(region)) + if err != nil { + return nil, err + } + return awsSession, nil +} diff --git a/test/helper/cloud/aws/vpc.go b/test/helper/cloud/aws/vpc.go new file mode 100644 index 0000000000..6cb2b254e6 --- /dev/null +++ b/test/helper/cloud/aws/vpc.go @@ -0,0 +1,54 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" +) + +func CreateVPC(name, cidr, region string) (string, error) { + awsSession, err := newSession(region) + if err != nil { + return "", fmt.Errorf("failed to create an AWS session: %w", err) + } + ec2Client := ec2.New(awsSession) + result, err := ec2Client.CreateVpc(&ec2.CreateVpcInput{ + AmazonProvidedIpv6CidrBlock: aws.Bool(false), + CidrBlock: aws.String(cidr), + TagSpecifications: []*ec2.TagSpecification{{ + ResourceType: aws.String(ec2.ResourceTypeVpc), + Tags: []*ec2.Tag{ + {Key: aws.String("Name"), Value: aws.String(name)}, + }, + }}, + }) + if err != nil { + return "", fmt.Errorf("failed to create an AWS VPC: %w", err) + } + + _, err = ec2Client.ModifyVpcAttribute(&ec2.ModifyVpcAttributeInput{ + EnableDnsHostnames: &ec2.AttributeBooleanValue{ + Value: aws.Bool(true), + }, + VpcId: result.Vpc.VpcId, + }) + if err != nil { + return "", fmt.Errorf("failed to configure AWS VPC: %w", err) + } + + return *result.Vpc.VpcId, nil +} + +func DeleteVPC(vpcID, region string) error { + awsSession, err := newSession(region) + if err != nil { + return fmt.Errorf("failed to create an AWS session: %w", err) + } + ec2Client := ec2.New(awsSession) + _, err = ec2Client.DeleteVpc(&ec2.DeleteVpcInput{ + DryRun: aws.Bool(false), + VpcId: aws.String(vpcID), + }) + return err +} diff --git a/test/helper/cloud/azure/azure.go b/test/helper/cloud/azure/azure.go new file mode 100644 index 0000000000..6398e13bea --- /dev/null +++ b/test/helper/cloud/azure/azure.go @@ -0,0 +1,55 @@ +package azure + +import ( + "fmt" + "os" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v2" +) + +const ( + defaultTestResourceGroupName = "svet-test" +) + +type azureConnection struct { + resourceGroupName string + credentials *azidentity.DefaultAzureCredential + networkResourceFactory *armnetwork.ClientFactory +} + +func newClient(resourceGroupName string) (*azureConnection, error) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return nil, err + } + + networkFactory, err := armnetwork.NewClientFactory(subscriptionID, cred, nil) + if err != nil { + return nil, err + } + + return &azureConnection{ + resourceGroupName: resourceGroupName, + networkResourceFactory: networkFactory, + credentials: cred, + }, err +} + +func RegionCode(region string) string { + region2azure := map[string]string{ + "US_CENTRAL": "us_central", + "US_EAST": "eastus", + "US_EAST_2": "eastus2", + } + azureRegion, ok := region2azure[region] + if !ok { + return fmt.Sprintf("unsupported region %q", region) + } + return azureRegion +} + +func TestResourceGroupName() string { + return defaultTestResourceGroupName +} diff --git a/test/helper/cloud/azure/vpc.go b/test/helper/cloud/azure/vpc.go new file mode 100644 index 0000000000..c62f36f553 --- /dev/null +++ b/test/helper/cloud/azure/vpc.go @@ -0,0 +1,73 @@ +package azure + +import ( + "context" + "errors" + "fmt" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v2" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" +) + +func CreateVPC(ctx context.Context, vpcName, cidr, region string) (string, error) { + azr, err := newClient(TestResourceGroupName()) + if err != nil { + return "", fmt.Errorf("failed to create azure client: %w", err) + } + vpcClient := azr.networkResourceFactory.NewVirtualNetworksClient() + + op, err := vpcClient.BeginCreateOrUpdate( + ctx, + azr.resourceGroupName, + vpcName, + armnetwork.VirtualNetwork{ + Location: pointer.MakePtr(region), + Properties: &armnetwork.VirtualNetworkPropertiesFormat{ + AddressSpace: &armnetwork.AddressSpace{ + AddressPrefixes: []*string{ + pointer.MakePtr(cidr), + }, + }, + }, + Tags: map[string]*string{ + "Name": pointer.MakePtr(vpcName), + }, + }, + nil, + ) + if err != nil { + return "", fmt.Errorf("failed to begin create azure VPC: %w", err) + } + + vpc, err := op.PollUntilDone(ctx, nil) + if err != nil { + return "", fmt.Errorf("creation process of VPC failed: %w", err) + } + if vpc.Name == nil { + return "", errors.New("VPC created without a name") + } + return *vpc.Name, nil +} + +func DeleteVPC(ctx context.Context, vpcName string) error { + azr, err := newClient(TestResourceGroupName()) + if err != nil { + return fmt.Errorf("failed to create azure client: %w", err) + } + vpcClient := azr.networkResourceFactory.NewVirtualNetworksClient() + + op, err := vpcClient.BeginDelete( + ctx, + azr.resourceGroupName, + vpcName, + nil, + ) + if err != nil { + return err + } + + _, err = op.PollUntilDone(ctx, nil) + + return err +} diff --git a/test/helper/cloud/google/google.go b/test/helper/cloud/google/google.go new file mode 100644 index 0000000000..16d4339013 --- /dev/null +++ b/test/helper/cloud/google/google.go @@ -0,0 +1,52 @@ +package google + +import ( + "context" + "errors" + "fmt" + "os" + + compute "cloud.google.com/go/compute/apiv1" +) + +const ( + googleSAFilename = ".googleServiceAccount.json" +) + +type googleConnection struct { + projectID string + + networkClient *compute.NetworksClient +} + +func newConnection(ctx context.Context, projectID string) (*googleConnection, error) { + if err := ensureCredentials(); err != nil { + return nil, fmt.Errorf("failed to prepare credentials") + } + + networkClient, err := compute.NewNetworksRESTClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to setup network rest client") + } + + return &googleConnection{ + projectID: projectID, + networkClient: networkClient, + }, nil +} + +func ensureCredentials() error { + if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") != "" { + return nil + } + credentials := os.Getenv("GCP_SA_CRED") + if credentials == "" { + return errors.New("GOOGLE_APPLICATION_CREDENTIALS and GCP_SA_CRED are unset, cant setup Google credentials") + } + if err := os.WriteFile(googleSAFilename, ([]byte)(credentials), 0600); err != nil { + return fmt.Errorf("failed to save credentials contents GCP_SA_CRED to %s: %w", + googleSAFilename, err) + } + os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", googleSAFilename) + return nil +} diff --git a/test/helper/cloud/google/vpc.go b/test/helper/cloud/google/vpc.go new file mode 100644 index 0000000000..024b49d16f --- /dev/null +++ b/test/helper/cloud/google/vpc.go @@ -0,0 +1,57 @@ +package google + +import ( + "context" + "fmt" + "os" + + "cloud.google.com/go/compute/apiv1/computepb" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" +) + +func CreateVPC(ctx context.Context, vpcName string) error { + gce, err := newConnection(ctx, os.Getenv("GOOGLE_PROJECT_ID")) + if err != nil { + return fmt.Errorf("failed to get Google Cloud connection: %w", err) + } + + op, err := gce.networkClient.Insert(ctx, &computepb.InsertNetworkRequest{ + Project: gce.projectID, + NetworkResource: &computepb.Network{ + Name: pointer.MakePtr(vpcName), + Description: pointer.MakePtr("Atlas Kubernetes Operator E2E Tests VPC"), + AutoCreateSubnetworks: pointer.MakePtr(false), + }, + }) + if err != nil { + return fmt.Errorf("failed to request creation of Google VPC: %w", err) + } + + err = op.Wait(ctx) + if err != nil { + return fmt.Errorf("failed to create Google VPC: %w", err) + } + + return nil +} + +func DeleteVPC(ctx context.Context, vpcName string) error { + gce, err := newConnection(ctx, os.Getenv("GOOGLE_PROJECT_ID")) + if err != nil { + return fmt.Errorf("failed to get Google Cloud connection: %w", err) + } + op, err := gce.networkClient.Delete(ctx, &computepb.DeleteNetworkRequest{ + Project: gce.projectID, + Network: vpcName, + }) + if err != nil { + return fmt.Errorf("failed to request deletion of Google VPC: %w", err) + } + err = op.Wait(ctx) + if err != nil { + return fmt.Errorf("failed to delete Google VPC: %w", err) + } + + return nil +} diff --git a/test/helper/contract/ako.go b/test/helper/contract/ako.go index bcc614e38c..0225fd76d6 100644 --- a/test/helper/contract/ako.go +++ b/test/helper/contract/ako.go @@ -3,10 +3,7 @@ package contract import ( "context" "fmt" - "log" "os" - "os/exec" - "strings" "go.mongodb.org/atlas-sdk/v20231115008/admin" corev1 "k8s.io/api/core/v1" @@ -17,10 +14,6 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas" ) -const ( - mongodbRepoURL = "https://mongodb.github.io/helm-charts" -) - func DefaultAtlasProject(name string) client.Object { return &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{Name: name}, @@ -51,41 +44,6 @@ func mustCreateVersionedAtlasClient(ctx context.Context) *admin.APIClient { return client } -func ensureTestAtlasOperator(namespace string) error { - if !isTestAtlasOperatorInstalled(namespace) { - return installTestAtlasOperator(namespace) - } - return nil -} - -func isTestAtlasOperatorInstalled(namespace string) bool { - output, err := run("helm", "ls", "-n", namespace) - if err != nil { - return false - } - return strings.Contains(string(output), operatorInstallName) -} - -func installTestAtlasOperator(namespace string) error { - if _, err := run("helm", "repo", "add", "mongodb", mongodbRepoURL); err != nil { - return fmt.Errorf("failed to set mongodb repo to URL %q: %w", mongodbRepoURL, err) - } - domain := os.Getenv("MCLI_OPS_MANAGER_URL") - args := []string{ - "install", - operatorInstallName, - "mongodb/mongodb-atlas-operator", - "--atomic", - "--set-string", fmt.Sprintf("atlasURI=%s", domain), - "--set", "objectDeletionProtection=false", - "--set", "subobjectDeletionProtection=false", - "--namespace=" + namespace, - "--create-namespace", - } - _, err := run("helm", args...) - return err -} - func globalSecret(namespace string) client.Object { return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -102,10 +60,3 @@ func globalSecret(namespace string) client.Object { }, } } - -func run(cmd string, args ...string) ([]byte, error) { - log.Printf("Running:\n%s %s", cmd, strings.Join(args, " ")) - output, err := exec.Command(cmd, args...).CombinedOutput() - log.Printf("%s", string(output)) - return output, err -} diff --git a/test/helper/contract/contract.go b/test/helper/contract/contract.go index 44422928aa..febc16486f 100644 --- a/test/helper/contract/contract.go +++ b/test/helper/contract/contract.go @@ -2,84 +2,111 @@ package contract import ( "context" + "fmt" + "os" "testing" "time" "go.mongodb.org/atlas-sdk/v20231115008/admin" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/stretchr/testify/require" + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/control" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e/utils" ) -const ( - operatorInstallName = "test-atlas-operator" - akoTestNamespace = "ako-test" -) - -type ContractTest struct { - t *testing.T - akoInstalled bool - namespace string - resources []client.Object +type ContractHelper interface { + AtlasClient() *admin.APIClient + AddResources(ctx context.Context, timeout time.Duration, resources ...client.Object) error + ProjectID(ctx context.Context, projectName string) (string, error) +} - Ctx context.Context - AtlasClient *admin.APIClient - K8sClient client.Client +type contractTest struct { + credentials bool + namespace string + resources []client.Object + k8sClient client.Client + atlasClient *admin.APIClient } -func (ct *ContractTest) cleanup() { +func (ct *contractTest) cleanup(ctx context.Context) error { for i := len(ct.resources) - 1; i >= 0; i-- { resource := ct.resources[i] - require.NoError(ct.t, ct.K8sClient.Delete(ct.Ctx, resource)) + if err := ct.k8sClient.Delete(ctx, resource); err != nil { + return fmt.Errorf("failed to delete contract test pre-set resource: %w", err) + } } ct.resources = []client.Object{} if ct.namespace != "" { - require.NoError(ct.t, ct.K8sClient.Delete(ct.Ctx, defaultNamespace(ct.namespace))) + if err := ct.k8sClient.Delete(ctx, defaultNamespace(ct.namespace)); err != nil { + return fmt.Errorf("failed to delete namespace %q: %w", ct.namespace, err) + } } + return nil } -func RunContractTest(t *testing.T, name string, ctFn func(ct *ContractTest)) { - t.Helper() +func RunGoContractTest(ctx context.Context, t *testing.T, name string, contractTest func(ch ContractHelper)) { if !control.Enabled("AKO_CONTRACT_TEST") { t.Skip("Skipping contract test as AKO_CONTRACT_TEST is unset") return } - ctx := context.Background() - ct := &ContractTest{ - t: t, - Ctx: ctx, - K8sClient: mustCreateK8sClient(), - akoInstalled: false, - resources: []client.Object{}, - AtlasClient: mustCreateVersionedAtlasClient(ctx), - } - defer ct.cleanup() + ct := newContractTest(ctx) + defer func() { + require.NoError(t, ct.cleanup(ctx)) + }() t.Run(name, func(t *testing.T) { - ctFn(ct) + contractTest(ct) }) } -func (ct *ContractTest) AddResources(timeout time.Duration, resources ...client.Object) { - if !ct.akoInstalled { - require.NoError(ct.t, ensureTestAtlasOperator(akoTestNamespace)) - require.NoError(ct.t, k8sRecreate(ct.Ctx, ct.K8sClient, globalSecret(akoTestNamespace))) - ct.akoInstalled = true +func newContractTest(ctx context.Context) *contractTest { + return &contractTest{ + k8sClient: mustCreateK8sClient(), + credentials: false, + resources: []client.Object{}, + atlasClient: mustCreateVersionedAtlasClient(ctx), + } +} + +func (ct *contractTest) AtlasClient() *admin.APIClient { + return ct.atlasClient +} + +func (ct *contractTest) AddResources(ctx context.Context, timeout time.Duration, resources ...client.Object) error { + if !ct.credentials { + akoTestNamespace := os.Getenv("HELM_AKO_NAMESPACE") + if err := k8sRecreate(ctx, ct.k8sClient, globalSecret(akoTestNamespace)); err != nil { + return fmt.Errorf("failed to set AKO namespace: %w", err) + } + ct.credentials = true } if ct.namespace == "" { ct.namespace = utils.RandomName("test-ns") - require.NoError(ct.t, ct.K8sClient.Create(ct.Ctx, defaultNamespace(ct.namespace))) + if err := ct.k8sClient.Create(ctx, defaultNamespace(ct.namespace)); err != nil { + return fmt.Errorf("failed to create test namespace: %w", err) + } } for _, resource := range resources { resource.SetNamespace(ct.namespace) - require.NoError(ct.t, ct.K8sClient.Create(ct.Ctx, resource)) + if err := ct.k8sClient.Create(ctx, resource); err != nil { + return fmt.Errorf("failed to create resource: %w", err) + } } ct.resources = append(ct.resources, resources...) - require.NoError(ct.t, waitForReadyStatus(ct.K8sClient, resources, timeout)) + if err := waitForReadyStatus(ct.k8sClient, resources, timeout); err != nil { + return fmt.Errorf("failed to reach READY status: %w", err) + } + return nil } -func (ct *ContractTest) Namespace() string { - return ct.namespace +func (ct *contractTest) ProjectID(ctx context.Context, projectName string) (string, error) { + project := akov2.AtlasProject{} + key := types.NamespacedName{Namespace: ct.namespace, Name: projectName} + if err := ct.k8sClient.Get(ctx, key, &project); err != nil { + return "", fmt.Errorf("failed to get project ID: %w", err) + } + return project.Status.ID, nil } diff --git a/test/helper/contract/contract_test.go b/test/helper/contract/contract_test.go index 6d263dcc63..d74efd1c4a 100644 --- a/test/helper/contract/contract_test.go +++ b/test/helper/contract/contract_test.go @@ -1,6 +1,7 @@ package contract_test import ( + "context" "fmt" "os" "strings" @@ -19,17 +20,19 @@ const ( ) func TestContractTestSkip(t *testing.T) { + ctx := context.Background() testWithEnv(func() { - contract.RunContractTest(t, "Skip contract test", func(_ *contract.ContractTest) { + contract.RunGoContractTest(ctx, t, "Skip contract test", func(_ contract.ContractHelper) { panic("should not have got here!") }) }, "-AKO_CONTRACT_TEST") } func TestContractTestClientSetFails(t *testing.T) { + ctx := context.Background() testWithEnv(func() { assert.Panics(t, func() { - contract.RunContractTest(t, "bad client settings panics", func(_ *contract.ContractTest) {}) + contract.RunGoContractTest(ctx, t, "bad client settings panics", func(_ contract.ContractHelper) {}) }) }, "AKO_CONTRACT_TEST=1", @@ -39,14 +42,15 @@ func TestContractTestClientSetFails(t *testing.T) { } func TestContractsWithResources(t *testing.T) { - contract.RunContractTest(t, "run contract test list projects", func(ct *contract.ContractTest) { - ct.AddResources(time.Minute, contract.DefaultAtlasProject("contract-tests-list-projects")) - _, _, err := ct.AtlasClient.ProjectsApi.ListProjects(ct.Ctx).Execute() + ctx := context.Background() + contract.RunGoContractTest(ctx, t, "run contract test list projects", func(ch contract.ContractHelper) { + ch.AddResources(ctx, time.Minute, contract.DefaultAtlasProject("contract-tests-list-projects")) + _, _, err := ch.AtlasClient().ProjectsApi.ListProjects(ctx).Execute() assert.NoError(t, err) }) - contract.RunContractTest(t, "run contract test list orgs", func(ct *contract.ContractTest) { - ct.AddResources(time.Minute, contract.DefaultAtlasProject("contract-tests-list-orgs")) - _, _, err := ct.AtlasClient.OrganizationsApi.ListOrganizations(ct.Ctx).Execute() + contract.RunGoContractTest(ctx, t, "run contract test list orgs", func(ch contract.ContractHelper) { + ch.AddResources(ctx, time.Minute, contract.DefaultAtlasProject("contract-tests-list-orgs")) + _, _, err := ch.AtlasClient().OrganizationsApi.ListOrganizations(ctx).Execute() assert.NoError(t, err) }) }