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/test/contract/networkpeering/aws.go b/test/contract/networkpeering/aws.go new file mode 100644 index 0000000000..0965981d2e --- /dev/null +++ b/test/contract/networkpeering/aws.go @@ -0,0 +1,68 @@ +package networkpeering + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" +) + +func createAWSTestVPC(name, cidr, region string) (string, error) { + awsSession, err := newAWSSession(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 deleteAWSTestVPC(vpcID, region string) error { + awsSession, err := newAWSSession(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 +} + +func newAWSSession(region string) (*session.Session, error) { + awsSession, err := session.NewSession(aws.NewConfig().WithRegion(region)) + if err != nil { + return nil, err + } + return awsSession, nil +} + +func awsRegionCode(region string) string { + return strings.ReplaceAll(strings.ToLower(region), "_", "-") +} diff --git a/test/contract/networkpeering/azure.go b/test/contract/networkpeering/azure.go new file mode 100644 index 0000000000..5f715bbdde --- /dev/null +++ b/test/contract/networkpeering/azure.go @@ -0,0 +1,117 @@ +package networkpeering + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v2" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" +) + +const ( + azureTestResourceGroupName = "svet-test" +) + +type azureConnection struct { + resourceGroupName string + credentials *azidentity.DefaultAzureCredential + networkResourceFactory *armnetwork.ClientFactory +} + +func createAzureTestVpc(ctx context.Context, vpcName, cidr, region string) (string, error) { + azr, err := newAzureClient(azureTestResourceGroupName) + 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 deleteAzureTestVpc(ctx context.Context, vpcName string) error { + azr, err := newAzureClient(azureTestResourceGroupName) + 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 +} + +func newAzureClient(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 azureRegionCode(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 +} diff --git a/test/contract/networkpeering/google.go b/test/contract/networkpeering/google.go new file mode 100644 index 0000000000..e23be9c9c4 --- /dev/null +++ b/test/contract/networkpeering/google.go @@ -0,0 +1,76 @@ +package networkpeering + +import ( + "context" + "fmt" + "os" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" +) + +type googleConnection struct { + projectID string + + networkClient *compute.NetworksClient +} + +func createGoogleTestVPC(ctx context.Context, vpcName string) error { + gce, err := newGoogleConnection(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 deleteGoogleTestVPC(ctx context.Context, vpcName string) error { + gce, err := newGoogleConnection(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 +} + +func newGoogleConnection(ctx context.Context, projectID string) (*googleConnection, error) { + networkClient, err := compute.NewNetworksRESTClient(ctx) + if err != nil { + return nil, err + } + + return &googleConnection{ + projectID: projectID, + networkClient: networkClient, + }, nil +} diff --git a/test/contract/networkpeering/networkpeering_test.go b/test/contract/networkpeering/networkpeering_test.go new file mode 100644 index 0000000000..0d69927349 --- /dev/null +++ b/test/contract/networkpeering/networkpeering_test.go @@ -0,0 +1,237 @@ +package networkpeering + +import ( + "context" + _ "embed" + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "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/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(), + }, + { + provider: "Azure", + container: testAzurePeeringContainer(), + }, + { + provider: "Google", + container: testGooglePeeringContainer(), + }, + } { + 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, string(tc.container.ProviderName)) + 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.RegionName, container.RegionName) + 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.NoError(t, cs.DeleteContainer(ctx, 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() + awsRegionName := awsRegionCode(testContainer.RegionName) + vpcCIDR := "10.9.0.0/21" + awsVPCid, err := createAWSTestVPC(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, deleteAWSTestVPC(awsVPCid, awsRegionName)) + }() + performTest(testAWSPeerConnection(t, newContainer.ID, vpcCIDR, awsVPCid)) + }, + }, + { + provider: "AZURE", + preparedCloudTest: func(performTest func(*networkpeering.NetworkPeer)) { + testContainer := testAzurePeeringContainer() + azureRegionName := azureRegionCode(testContainer.Region) + vpcCIDR := "10.9.0.0/21" + azureVPC, err := createAzureTestVpc(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, deleteAzureTestVpc(ctx, azureVPC)) + }() + performTest(testAzurePeerConnection(t, newContainer.ID, azureVPC)) + }, + }, + { + provider: "GOOGLE", + preparedCloudTest: func(performTest func(*networkpeering.NetworkPeer)) { + mustHaveEnvVar(t, "GCP_SA_CRED") + testContainer := testGooglePeeringContainer() + vpcName := utils.RandomName(testVPCName) + require.NoError(t, createGoogleTestVPC(ctx, vpcName)) + newContainer, err := nps.CreateContainer(ctx, testProjectID, testContainer) + require.NoError(t, err) + assert.NotEmpty(t, newContainer.ID) + defer func() { + require.NoError(t, deleteGoogleTestVPC(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() *networkpeering.ProviderContainer { + return &networkpeering.ProviderContainer{ + ProviderName: "AWS", + RegionName: "US_EAST_1", + AtlasCIDRBlock: "10.8.0.0/21", + } +} + +func testAzurePeeringContainer() *networkpeering.ProviderContainer { + return &networkpeering.ProviderContainer{ + ProviderName: "AZURE", + Region: "US_EAST_2", + AtlasCIDRBlock: "10.8.0.0/21", + } +} + +func testGooglePeeringContainer() *networkpeering.ProviderContainer { + return &networkpeering.ProviderContainer{ + ProviderName: "GCP", + AtlasCIDRBlock: "10.8.0.0/18", // .../21 is not allowed in GCP + } +} + +func testAWSPeerConnection(t *testing.T, containerID string, vpcCIDR, vpcID string) *networkpeering.NetworkPeer { + return &networkpeering.NetworkPeer{ + NetworkPeer: akov2.NetworkPeer{ + ProviderName: "AWS", + ContainerID: containerID, + 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{ + NetworkPeer: akov2.NetworkPeer{ + ProviderName: "AZURE", + AzureDirectoryID: mustHaveEnvVar(t, "AZURE_TENANT_ID"), + AzureSubscriptionID: mustHaveEnvVar(t, "AZURE_SUBSCRIPTION_ID"), + ContainerID: containerID, + ResourceGroupName: azureTestResourceGroupName, + VNetName: vpcName, + }, + } +} + +func testGooglePeerConnection(t *testing.T, containerID string, vpcName string) *networkpeering.NetworkPeer { + return &networkpeering.NetworkPeer{ + NetworkPeer: akov2.NetworkPeer{ + ProviderName: "GCP", + ContainerID: containerID, + 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 +}