-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
vpc_endpoint.go
210 lines (180 loc) · 6.72 KB
/
vpc_endpoint.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package builder
import (
"context"
"errors"
"fmt"
"strings"
"github.com/aws/smithy-go"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
"github.com/weaveworks/eksctl/pkg/awsapi"
gfnec2 "github.com/weaveworks/goformation/v4/cloudformation/ec2"
gfnt "github.com/weaveworks/goformation/v4/cloudformation/types"
)
// A VPCEndpointResourceSet holds the resources required for VPC endpoints.
type VPCEndpointResourceSet struct {
ec2API awsapi.EC2
region string
rs *resourceSet
vpc *gfnt.Value
clusterConfig *api.ClusterConfig
subnets []SubnetResource
clusterSharedSG *gfnt.Value
}
// NewVPCEndpointResourceSet creates a new VPCEndpointResourceSet.
func NewVPCEndpointResourceSet(ec2API awsapi.EC2, region string, rs *resourceSet, clusterConfig *api.ClusterConfig, vpc *gfnt.Value, subnets []SubnetResource, clusterSharedSG *gfnt.Value) *VPCEndpointResourceSet {
return &VPCEndpointResourceSet{
ec2API: ec2API,
region: region,
rs: rs,
clusterConfig: clusterConfig,
vpc: vpc,
subnets: subnets,
clusterSharedSG: clusterSharedSG,
}
}
// VPCEndpointServiceDetails holds the details for a VPC endpoint service.
type VPCEndpointServiceDetails struct {
ServiceName string
ServiceReadableName string
EndpointType string
AvailabilityZones []string
}
// AddResources adds resources for VPC endpoints.
func (e *VPCEndpointResourceSet) AddResources(ctx context.Context) error {
additionalServices, err := api.MapOptionalEndpointServices(e.clusterConfig.PrivateCluster.AdditionalEndpointServices, e.clusterConfig.HasClusterCloudWatchLogging())
if err != nil {
return err
}
endpointServices := append(api.RequiredEndpointServices(e.clusterConfig.IsControlPlaneOnOutposts()), additionalServices...)
endpointServiceDetails, err := e.buildVPCEndpointServices(ctx, endpointServices)
if err != nil {
return fmt.Errorf("error building endpoint service details: %w", err)
}
for _, endpointDetail := range endpointServiceDetails {
endpoint := &gfnec2.VPCEndpoint{
ServiceName: gfnt.NewString(endpointDetail.ServiceName),
VpcId: e.vpc,
VpcEndpointType: gfnt.NewString(endpointDetail.EndpointType),
}
if endpointDetail.EndpointType == string(ec2types.VpcEndpointTypeGateway) {
endpoint.RouteTableIds = gfnt.NewSlice(e.routeTableIDs()...)
} else {
endpoint.SubnetIds = gfnt.NewSlice(e.subnetsForAZs(endpointDetail.AvailabilityZones)...)
endpoint.PrivateDnsEnabled = gfnt.NewBoolean(true)
endpoint.SecurityGroupIds = gfnt.NewSlice(e.clusterSharedSG)
}
resourceName := fmt.Sprintf("VPCEndpoint%s", strings.ToUpper(
strings.ReplaceAll(endpointDetail.ServiceReadableName, ".", ""),
))
// TODO attach policy document
e.rs.newResource(resourceName, endpoint)
}
return nil
}
func (e *VPCEndpointResourceSet) subnetsForAZs(azs []string) []*gfnt.Value {
var subnetRefs []*gfnt.Value
for _, az := range azs {
for _, subnet := range e.subnets {
if subnet.AvailabilityZone == az {
subnetRefs = append(subnetRefs, subnet.Subnet)
}
}
}
return subnetRefs
}
func (e *VPCEndpointResourceSet) routeTableIDs() []*gfnt.Value {
var routeTableIDs []*gfnt.Value
m := make(map[string]bool)
for _, subnet := range e.subnets {
routeTableStr := subnet.RouteTable.String()
if !m[routeTableStr] {
m[routeTableStr] = true
routeTableIDs = append(routeTableIDs, subnet.RouteTable)
}
}
return routeTableIDs
}
// buildVPCEndpointServices builds a slice of VPCEndpointServiceDetails for the specified endpoint names.
func (e *VPCEndpointResourceSet) buildVPCEndpointServices(ctx context.Context, endpointServices []api.EndpointService) ([]VPCEndpointServiceDetails, error) {
serviceNames := make([]string, len(endpointServices))
for i, endpoint := range endpointServices {
serviceNames[i] = makeServiceName(endpoint, e.region)
}
var (
serviceDetails []ec2types.ServiceDetail
nextToken *string
)
for {
output, err := e.ec2API.DescribeVpcEndpointServices(ctx, &ec2.DescribeVpcEndpointServicesInput{
ServiceNames: serviceNames,
Filters: []ec2types.Filter{
{
Name: aws.String("service-name"),
Values: serviceNames,
},
},
NextToken: nextToken,
})
if err != nil {
var ae smithy.APIError
if errors.As(err, &ae) && ae.ErrorCode() == "InvalidServiceName" {
return nil, &api.UnsupportedFeatureError{
Message: fmt.Sprintf("fully-private clusters are not supported in region %q, please retry with a different region", e.region),
Err: err,
}
}
return nil, fmt.Errorf("error describing VPC endpoint services: %w", err)
}
serviceDetails = append(serviceDetails, output.ServiceDetails...)
if nextToken = output.NextToken; nextToken == nil {
break
}
}
var ret []VPCEndpointServiceDetails
s3ServiceName := makeServiceName(api.EndpointServiceS3, e.region)
for _, sd := range serviceDetails {
if len(sd.ServiceType) > 1 {
return nil, fmt.Errorf("endpoint service %q with multiple service types isn't supported", *sd.ServiceName)
}
if len(sd.ServiceType) == 0 {
return nil, fmt.Errorf("expected to find a service type for endpoint service %q", *sd.ServiceName)
}
endpointType := sd.ServiceType[0].ServiceType
if !serviceEndpointTypeExpected(*sd.ServiceName, endpointType, s3ServiceName) {
continue
}
readableName, err := makeReadableName(*sd.ServiceName, e.region)
if err != nil {
return nil, err
}
ret = append(ret, VPCEndpointServiceDetails{
ServiceName: *sd.ServiceName,
ServiceReadableName: readableName,
EndpointType: string(endpointType),
AvailabilityZones: sd.AvailabilityZones,
})
}
return ret, nil
}
func makeReadableName(serviceName, region string) (string, error) {
search := fmt.Sprintf(".%s.", region)
idx := strings.Index(serviceName, search)
if idx == -1 {
return "", fmt.Errorf("unexpected format for endpoint service name: %q", serviceName)
}
return serviceName[idx+len(search)-1:], nil
}
// serviceEndpointTypeExpected returns true if the endpoint service is expected to use the specified endpoint type.
func serviceEndpointTypeExpected(serviceName string, endpointType ec2types.ServiceType, s3ServiceName string) bool {
if serviceName == s3ServiceName {
return endpointType == ec2types.ServiceTypeGateway
}
return endpointType == ec2types.ServiceTypeInterface
}
func makeServiceName(endpointService api.EndpointService, region string) string {
serviceDomainPrefix := api.Partitions.GetEndpointServiceDomainPrefix(endpointService, region)
return fmt.Sprintf("%s.%s.%s", serviceDomainPrefix, region, endpointService.Name)
}