Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automated backport of 1007 upstream release 0.16 #1012

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 135 additions & 15 deletions pkg/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ package aws

import (
"context"
"fmt"
"regexp"
"strings"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/pkg/errors"
"github.com/submariner-io/admiral/pkg/reporter"
"github.com/submariner-io/cloud-prepare/pkg/api"
Expand All @@ -37,34 +41,84 @@ const (
messageValidatedPrerequisites = "Validated pre-requisites"
)

type CloudOption func(*awsCloud)

const (
ControlPlaneSecurityGroupIDKey = "controlPlaneSecurityGroupID"
WorkerSecurityGroupIDKey = "workerSecurityGroupID"
PublicSubnetListKey = "PublicSubnetList"
VPCIDKey = "VPCID"
)

func WithControlPlaneSecurityGroup(id string) CloudOption {
return func(cloud *awsCloud) {
cloud.cloudConfig[ControlPlaneSecurityGroupIDKey] = id
}
}

func WithWorkerSecurityGroup(id string) CloudOption {
return func(cloud *awsCloud) {
cloud.cloudConfig[WorkerSecurityGroupIDKey] = id
}
}

func WithPublicSubnetList(id []string) CloudOption {
return func(cloud *awsCloud) {
cloud.cloudConfig[PublicSubnetListKey] = id
}
}

func WithVPCName(name string) CloudOption {
return func(cloud *awsCloud) {
cloud.cloudConfig[VPCIDKey] = name
}
}

type awsCloud struct {
client awsClient.Interface
infraID string
region string
client awsClient.Interface
infraID string
region string
nodeSGSuffix string
controlPlaneSGSuffix string
cloudConfig map[string]interface{}
}

// NewCloud creates a new api.Cloud instance which can prepare AWS for Submariner to be deployed on it.
func NewCloud(client awsClient.Interface, infraID, region string) api.Cloud {
return &awsCloud{
client: client,
infraID: infraID,
region: region,
func NewCloud(client awsClient.Interface, infraID, region string, opts ...CloudOption) api.Cloud {
cloud := &awsCloud{
client: client,
infraID: infraID,
region: region,
cloudConfig: make(map[string]interface{}),
}

for _, opt := range opts {
opt(cloud)
}

return cloud
}

// NewCloudFromConfig creates a new api.Cloud instance based on an AWS configuration
// which can prepare AWS for Submariner to be deployed on it.
func NewCloudFromConfig(cfg *aws.Config, infraID, region string) api.Cloud {
return &awsCloud{
client: ec2.NewFromConfig(*cfg),
infraID: infraID,
region: region,
func NewCloudFromConfig(cfg *aws.Config, infraID, region string, opts ...CloudOption) api.Cloud {
cloud := &awsCloud{
client: ec2.NewFromConfig(*cfg),
infraID: infraID,
region: region,
cloudConfig: make(map[string]interface{}),
}

for _, opt := range opts {
opt(cloud)
}

return cloud
}

// NewCloudFromSettings creates a new api.Cloud instance using the given credentials file and profile
// which can prepare AWS for Submariner to be deployed on it.
func NewCloudFromSettings(credentialsFile, profile, infraID, region string) (api.Cloud, error) {
func NewCloudFromSettings(credentialsFile, profile, infraID, region string, opts ...CloudOption) (api.Cloud, error) {
options := []func(*config.LoadOptions) error{config.WithRegion(region), config.WithSharedConfigProfile(profile)}
if credentialsFile != DefaultCredentialsFile() {
options = append(options, config.WithSharedCredentialsFiles([]string{credentialsFile}))
Expand All @@ -75,7 +129,7 @@ func NewCloudFromSettings(credentialsFile, profile, infraID, region string) (api
return nil, errors.Wrap(err, "error loading default config")
}

return NewCloudFromConfig(&cfg, infraID, region), nil
return NewCloudFromConfig(&cfg, infraID, region, opts...), nil
}

// DefaultCredentialsFile returns the default credentials file name.
Expand All @@ -88,6 +142,58 @@ func DefaultProfile() string {
return "default"
}

func (ac *awsCloud) setSuffixes(vpcID string) error {
if ac.nodeSGSuffix != "" {
return nil
}

var publicSubnets []types.Subnet

if subnets, exists := ac.cloudConfig[PublicSubnetListKey]; exists {
if subnetIDs, ok := subnets.([]string); ok && len(subnetIDs) > 0 {
for _, id := range subnetIDs {
subnet, err := ac.getSubnetByID(id)
if err != nil {
return errors.Wrapf(err, "unable to find subnet with ID %s", id)
}

publicSubnets = append(publicSubnets, *subnet)
}
} else {
return errors.New("Subnet IDs must be a valid non-empty slice of strings")
}
} else {
publicSubnets, err := ac.findPublicSubnets(vpcID, ac.filterByName("{infraID}*-public-{region}*"))
if err != nil {
return errors.Wrapf(err, "unable to find the public subnet")
}

if len(publicSubnets) == 0 {
return errors.New("no public subnet found")
}
}

pattern := fmt.Sprintf(`%s.*-subnet-public-%s.*`, regexp.QuoteMeta(ac.infraID), regexp.QuoteMeta(ac.region))
re := regexp.MustCompile(pattern)

for i := range publicSubnets {
tags := publicSubnets[i].Tags
for i := range tags {
if strings.Contains(*tags[i].Key, "Name") && re.MatchString(*tags[i].Value) {
ac.nodeSGSuffix = "-node"
ac.controlPlaneSGSuffix = "-controlplane"

return nil
}
}
}

ac.nodeSGSuffix = "-worker-sg"
ac.controlPlaneSGSuffix = "-master-sg"

return nil
}

func (ac *awsCloud) OpenPorts(ports []api.PortSpec, status reporter.Interface) error {
status.Start(messageRetrieveVPCID)
defer status.End()
Expand All @@ -97,6 +203,13 @@ func (ac *awsCloud) OpenPorts(ports []api.PortSpec, status reporter.Interface) e
return status.Error(err, "unable to retrieve the VPC ID")
}

if _, found := ac.cloudConfig[VPCIDKey]; !found {
err = ac.setSuffixes(vpcID)
if err != nil {
return status.Error(err, "unable to retrieve the security group names")
}
}

status.Success(messageRetrievedVPCID, vpcID)

status.Start(messageValidatePrerequisites)
Expand Down Expand Up @@ -135,6 +248,13 @@ func (ac *awsCloud) ClosePorts(status reporter.Interface) error {
return status.Error(err, "unable to retrieve the VPC ID")
}

if _, found := ac.cloudConfig[VPCIDKey]; !found {
err = ac.setSuffixes(vpcID)
if err != nil {
return status.Error(err, "unable to retrieve the security group names")
}
}

status.Success(messageRetrievedVPCID, vpcID)

status.Start(messageValidatePrerequisites)
Expand Down
3 changes: 3 additions & 0 deletions pkg/aws/aws_cloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func testOpenPorts() {

JustBeforeEach(func() {
t.expectDescribeVpcs(t.vpcID)
t.expectDescribePublicSubnets(t.subnets...)

retError = t.cloud.OpenPorts([]api.PortSpec{
{
Expand Down Expand Up @@ -114,6 +115,8 @@ func testClosePorts() {

JustBeforeEach(func() {
t.expectDescribeVpcs(t.vpcID)
t.expectDescribePublicSubnets(t.subnets...)
t.expectDescribePublicSubnetsSigs(t.subnets...)

retError = t.cloud.ClosePorts(reporter.Stdout())
})
Expand Down
69 changes: 46 additions & 23 deletions pkg/aws/aws_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,28 @@ import (
. "github.com/onsi/gomega"
"github.com/submariner-io/admiral/pkg/mock"
"github.com/submariner-io/cloud-prepare/pkg/aws/client/fake"
"k8s.io/utils/ptr"
)

const (
infraID = "test-infra"
region = "test-region"
vpcID = "test-vpc"
workerGroupID = "worker-group"
masterGroupID = "master-group"
gatewayGroupID = "gateway-group"
internalTraffic = "Internal Submariner traffic"
availabilityZone1 = "availability-zone-1"
availabilityZone2 = "availability-zone-2"
subnetID1 = "subnet-1"
subnetID2 = "subnet-2"
instanceImageID = "test-image"
infraID = "test-infra"
region = "test-region"
vpcID = "test-vpc"
workerGroupID = "worker-group"
masterGroupID = "master-group"
gatewayGroupID = "gateway-group"
internalTraffic = "Internal Submariner traffic"
availabilityZone1 = "availability-zone-1"
availabilityZone2 = "availability-zone-2"
subnetID1 = "subnet-1"
subnetID2 = "subnet-2"
instanceImageID = "test-image"
masterSGName = infraID + "-master-sg"
workerSGName = infraID + "-worker-sg"
gatewaySGName = infraID + "-submariner-gw-sg"
providerAWSTagPrefix = "tag:sigs.k8s.io/cluster-api-provider-aws/cluster/"
clusterFilterTagName = "tag:kubernetes.io/cluster/" + infraID
clusterFilterTagNameSigs = providerAWSTagPrefix + infraID
)

var internalTrafficDesc = fmt.Sprintf("Should contain %q", internalTraffic)
Expand All @@ -60,6 +67,7 @@ type fakeAWSClientBase struct {
awsClient *fake.MockInterface
mockCtrl *gomock.Controller
vpcID string
subnets []types.Subnet
describeSubnetsErr error
authorizeSecurityGroupIngressErr error
createTagsErr error
Expand All @@ -70,6 +78,7 @@ func (f *fakeAWSClientBase) beforeEach() {
f.mockCtrl = gomock.NewController(GinkgoT())
f.awsClient = fake.NewMockInterface(f.mockCtrl)
f.vpcID = vpcID
f.subnets = []types.Subnet{newSubnet(availabilityZone1, subnetID1), newSubnet(availabilityZone2, subnetID2)}
f.describeSubnetsErr = nil
f.authorizeSecurityGroupIngressErr = nil
f.createTagsErr = nil
Expand Down Expand Up @@ -110,15 +119,19 @@ func (f *fakeAWSClientBase) expectDescribeVpcs(vpcID string) {
}

func (f *fakeAWSClientBase) expectValidateAuthorizeSecurityGroupIngress(authErr error) *gomock.Call {
return f.awsClient.EXPECT().AuthorizeSecurityGroupIngress(gomock.Any(), mock.Eq(&ec2.AuthorizeSecurityGroupIngressInput{
DryRun: awssdk.Bool(true),
GroupId: awssdk.String(workerGroupID),
})).Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, authErr)
return f.awsClient.EXPECT().AuthorizeSecurityGroupIngress(gomock.Any(),
eqAuthorizeSecurityGroupIngressInput(ec2.AuthorizeSecurityGroupIngressInput{
DryRun: ptr.To(true),
GroupId: ptr.To(workerGroupID),
})).Return(&ec2.AuthorizeSecurityGroupIngressOutput{}, authErr)
}

func (f *fakeAWSClientBase) expectAuthorizeSecurityGroupIngress(srcGroup string, ipPerm *types.IpPermission) {
f.awsClient.EXPECT().AuthorizeSecurityGroupIngress(gomock.Any(),
eqAuthorizeSecurityGroupIngressInput(srcGroup, ipPerm)).Return(&ec2.AuthorizeSecurityGroupIngressOutput{},
eqAuthorizeSecurityGroupIngressInput(ec2.AuthorizeSecurityGroupIngressInput{
GroupId: awssdk.String(srcGroup),
IpPermissions: []types.IpPermission{*ipPerm},
})).Return(&ec2.AuthorizeSecurityGroupIngressOutput{},
f.authorizeSecurityGroupIngressErr)
}

Expand All @@ -139,7 +152,7 @@ func (f *fakeAWSClientBase) expectValidateRevokeSecurityGroupIngress(retErr erro
func (f *fakeAWSClientBase) expectDescribePublicSubnets(retSubnets ...types.Subnet) {
f.awsClient.EXPECT().DescribeSubnets(gomock.Any(), eqFilters(types.Filter{
Name: awssdk.String("tag:Name"),
Values: []string{infraID + "-public-" + region + "*"},
Values: []string{infraID + "*-public-" + region + "*"},
}, types.Filter{
Name: awssdk.String("vpc-id"),
Values: []string{f.vpcID},
Expand All @@ -149,6 +162,19 @@ func (f *fakeAWSClientBase) expectDescribePublicSubnets(retSubnets ...types.Subn
})).Return(&ec2.DescribeSubnetsOutput{Subnets: retSubnets}, f.describeSubnetsErr).AnyTimes()
}

func (f *fakeAWSClientBase) expectDescribePublicSubnetsSigs(retSubnets ...types.Subnet) {
f.awsClient.EXPECT().DescribeSubnets(gomock.Any(), eqFilters(types.Filter{
Name: awssdk.String("tag:Name"),
Values: []string{infraID + "*-public-" + region + "*"},
}, types.Filter{
Name: awssdk.String("vpc-id"),
Values: []string{f.vpcID},
}, types.Filter{
Name: awssdk.String(clusterFilterTagNameSigs),
Values: []string{"owned"},
})).Return(&ec2.DescribeSubnetsOutput{Subnets: retSubnets}, f.describeSubnetsErr).AnyTimes()
}

func (f *fakeAWSClientBase) expectDescribeGatewaySubnets(retSubnets ...types.Subnet) {
f.awsClient.EXPECT().DescribeSubnets(gomock.Any(), eqFilters(types.Filter{
Name: awssdk.String("tag:submariner.io/gateway"),
Expand Down Expand Up @@ -423,12 +449,9 @@ func (m *authorizeSecurityGroupIngressInputMatcher) String() string {
return "matches " + mock.FormatToYAML(&m.AuthorizeSecurityGroupIngressInput)
}

func eqAuthorizeSecurityGroupIngressInput(srcGroup string, ipPerm *types.IpPermission) gomock.Matcher {
func eqAuthorizeSecurityGroupIngressInput(input ec2.AuthorizeSecurityGroupIngressInput) gomock.Matcher {
m := &authorizeSecurityGroupIngressInputMatcher{
AuthorizeSecurityGroupIngressInput: ec2.AuthorizeSecurityGroupIngressInput{
GroupId: awssdk.String(srcGroup),
IpPermissions: []types.IpPermission{*ipPerm},
},
AuthorizeSecurityGroupIngressInput: input,
}

return mock.FormattingMatcher(&m.AuthorizeSecurityGroupIngressInput, m)
Expand Down
Loading
Loading