diff --git a/controllers/ingress/group_controller.go b/controllers/ingress/group_controller.go
index 175bbb6906..19eb9e17f3 100644
--- a/controllers/ingress/group_controller.go
+++ b/controllers/ingress/group_controller.go
@@ -45,6 +45,7 @@ const (
// NewGroupReconciler constructs new GroupReconciler
func NewGroupReconciler(cloud aws.Cloud, k8sClient client.Client, eventRecorder record.EventRecorder,
finalizerManager k8s.FinalizerManager, networkingSGManager networkingpkg.SecurityGroupManager,
+ vpcEndpointServiceManager networkingpkg.VPCEndpointServiceManager,
networkingSGReconciler networkingpkg.SecurityGroupReconciler, subnetsResolver networkingpkg.SubnetsResolver,
elbv2TaggingManager elbv2deploy.TaggingManager, controllerConfig config.ControllerConfig, backendSGProvider networkingpkg.BackendSGProvider,
sgResolver networkingpkg.SecurityGroupResolver, logger logr.Logger) *groupReconciler {
@@ -62,8 +63,8 @@ func NewGroupReconciler(cloud aws.Cloud, k8sClient client.Client, eventRecorder
controllerConfig.DefaultSSLPolicy, controllerConfig.DefaultTargetType, backendSGProvider, sgResolver,
controllerConfig.EnableBackendSecurityGroup, controllerConfig.DisableRestrictedSGRules, controllerConfig.IngressConfig.AllowedCertificateAuthorityARNs, controllerConfig.FeatureGates.Enabled(config.EnableIPTargetType), logger)
stackMarshaller := deploy.NewDefaultStackMarshaller()
- stackDeployer := deploy.NewDefaultStackDeployer(cloud, k8sClient, networkingSGManager, networkingSGReconciler, elbv2TaggingManager,
- controllerConfig, ingressTagPrefix, logger)
+ stackDeployer := deploy.NewDefaultStackDeployer(cloud, k8sClient, networkingSGManager, networkingSGReconciler, vpcEndpointServiceManager,
+ elbv2TaggingManager, controllerConfig, ingressTagPrefix, logger)
classLoader := ingress.NewDefaultClassLoader(k8sClient, true)
classAnnotationMatcher := ingress.NewDefaultClassAnnotationMatcher(controllerConfig.IngressConfig.IngressClass)
manageIngressesWithoutIngressClass := controllerConfig.IngressConfig.IngressClass == ""
diff --git a/controllers/service/service_controller.go b/controllers/service/service_controller.go
index 2ed7612b01..4a15aa89a8 100644
--- a/controllers/service/service_controller.go
+++ b/controllers/service/service_controller.go
@@ -36,6 +36,7 @@ const (
func NewServiceReconciler(cloud aws.Cloud, k8sClient client.Client, eventRecorder record.EventRecorder,
finalizerManager k8s.FinalizerManager, networkingSGManager networking.SecurityGroupManager,
+ vpcEndpointServiceManager networking.VPCEndpointServiceManager,
networkingSGReconciler networking.SecurityGroupReconciler, subnetsResolver networking.SubnetsResolver,
vpcInfoProvider networking.VPCInfoProvider, elbv2TaggingManager elbv2deploy.TaggingManager, controllerConfig config.ControllerConfig,
backendSGProvider networking.BackendSGProvider, sgResolver networking.SecurityGroupResolver, logger logr.Logger) *serviceReconciler {
@@ -48,7 +49,7 @@ func NewServiceReconciler(cloud aws.Cloud, k8sClient client.Client, eventRecorde
controllerConfig.DefaultSSLPolicy, controllerConfig.DefaultTargetType, controllerConfig.FeatureGates.Enabled(config.EnableIPTargetType), serviceUtils,
backendSGProvider, sgResolver, controllerConfig.EnableBackendSecurityGroup, controllerConfig.DisableRestrictedSGRules, logger)
stackMarshaller := deploy.NewDefaultStackMarshaller()
- stackDeployer := deploy.NewDefaultStackDeployer(cloud, k8sClient, networkingSGManager, networkingSGReconciler, elbv2TaggingManager, controllerConfig, serviceTagPrefix, logger)
+ stackDeployer := deploy.NewDefaultStackDeployer(cloud, k8sClient, networkingSGManager, networkingSGReconciler, vpcEndpointServiceManager, elbv2TaggingManager, controllerConfig, serviceTagPrefix, logger)
return &serviceReconciler{
k8sClient: k8sClient,
eventRecorder: eventRecorder,
diff --git a/docs/guide/ingress/annotations.md b/docs/guide/ingress/annotations.md
index b310785473..d1aa4ea780 100644
--- a/docs/guide/ingress/annotations.md
+++ b/docs/guide/ingress/annotations.md
@@ -60,6 +60,58 @@ You can add annotations to kubernetes Ingress and Service objects to customize t
| [alb.ingress.kubernetes.io/target-node-labels](#target-node-labels) | stringMap |N/A| Ingress,Service | N/A |
| [alb.ingress.kubernetes.io/mutual-authentication](#mutual-authentication) | json |N/A| Ingress |Exclusive|
+## Annotations
+| Name | Type |Default| Location | MergeBehavior |
+|-------------------------------------------------------------------------------------------------------|-----------------------------|------|-----------------|-----------|
+| [alb.ingress.kubernetes.io/load-balancer-name](#load-balancer-name) | string |N/A| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/group.name](#group.name) | string |N/A| Ingress | N/A |
+| [alb.ingress.kubernetes.io/group.order](#group.order) | integer |0| Ingress | N/A |
+| [alb.ingress.kubernetes.io/tags](#tags) | stringMap |N/A| Ingress,Service | Merge |
+| [alb.ingress.kubernetes.io/ip-address-type](#ip-address-type) | ipv4 \| dualstack \| dualstack-without-public-ipv4 |ipv4| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/scheme](#scheme) | internal \| internet-facing |internal| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/subnets](#subnets) | stringList |N/A| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/security-groups](#security-groups) | stringList |N/A| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/manage-backend-security-group-rules](#manage-backend-security-group-rules) | boolean |N/A| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/customer-owned-ipv4-pool](#customer-owned-ipv4-pool) | string |N/A| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/load-balancer-attributes](#load-balancer-attributes) | stringMap |N/A| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/wafv2-acl-arn](#wafv2-acl-arn) | string |N/A| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/waf-acl-id](#waf-acl-id) | string |N/A| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/shield-advanced-protection](#shield-advanced-protection) | boolean |N/A| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/listen-ports](#listen-ports) | json |'[{"HTTP": 80}]' \| '[{"HTTPS": 443}]'| Ingress | Merge |
+| [alb.ingress.kubernetes.io/ssl-redirect](#ssl-redirect) | integer |N/A| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/inbound-cidrs](#inbound-cidrs) | stringList |0.0.0.0/0, ::/0| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/security-group-prefix-lists](#security-group-prefix-lists) | stringList |pl-00000000, pl-1111111| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/certificate-arn](#certificate-arn) | stringList |N/A| Ingress | Merge |
+| [alb.ingress.kubernetes.io/ssl-policy](#ssl-policy) | string |ELBSecurityPolicy-2016-08| Ingress | Exclusive |
+| [alb.ingress.kubernetes.io/target-type](#target-type) | instance \| ip |instance| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/backend-protocol](#backend-protocol) | HTTP \| HTTPS |HTTP| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/backend-protocol-version](#backend-protocol-version) | string | HTTP1 | Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/target-group-attributes](#target-group-attributes) | stringMap |N/A| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/healthcheck-port](#healthcheck-port) | integer \| traffic-port |traffic-port| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/healthcheck-protocol](#healthcheck-protocol) | HTTP \| HTTPS |HTTP| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/healthcheck-path](#healthcheck-path) | string |/ \| /AWS.ALB/healthcheck | Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/healthcheck-interval-seconds](#healthcheck-interval-seconds) | integer |'15'| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/healthcheck-timeout-seconds](#healthcheck-timeout-seconds) | integer |'5'| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/healthy-threshold-count](#healthy-threshold-count) | integer |'2'| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/unhealthy-threshold-count](#unhealthy-threshold-count) | integer |'2'| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/success-codes](#success-codes) | string |'200' \| '12' | Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/auth-type](#auth-type) | none\|oidc\|cognito |none| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/auth-idp-cognito](#auth-idp-cognito) | json |N/A| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/auth-idp-oidc](#auth-idp-oidc) | json |N/A| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/auth-on-unauthenticated-request](#auth-on-unauthenticated-request) | authenticate\|allow\|deny |authenticate| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/auth-scope](#auth-scope) | string |openid| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/auth-session-cookie](#auth-session-cookie) | string |AWSELBAuthSessionCookie| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/auth-session-timeout](#auth-session-timeout) | integer |'604800'| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/actions.${action-name}](#actions) | json |N/A| Ingress | N/A |
+| [alb.ingress.kubernetes.io/conditions.${conditions-name}](#conditions) | json |N/A| Ingress | N/A |
+| [alb.ingress.kubernetes.io/target-node-labels](#target-node-labels) | stringMap |N/A| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/mutual-authentication](#mutual-authentication) | json |N/A| Ingress |Exclusive|
+| [alb.ingress.kubernetes.io/aws-load-balancer-endpoint-service-enabled](#endpoint-service-enable) | boolean |false| Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/aws-load-balancer-endpoint-service-acceptance-required](#endpoint-service-acceptance) | boolean |true | Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/aws-load-balancer-endpoint-service-allowed-principals](#endpoint-allowed-principals) | stringList |[] | Ingress,Service | N/A |
+| [alb.ingress.kubernetes.io/aws-load-balancer-endpoint-service-private-dns-name](#endpoint-private-dns) | string |"" | Ingress,Service | N/A |
+
+
## IngressGroup
IngressGroup feature enables you to group multiple Ingress resources together.
The controller will automatically merge Ingress rules for all Ingresses within IngressGroup and support them with a single ALB.
@@ -76,7 +128,7 @@ By default, Ingresses don't belong to any IngressGroup, and we treat it as a "im
!!!warning "Security Risk"
IngressGroup feature should only be used when all Kubernetes users with RBAC permission to create/modify Ingress resources are within trust boundary.
-
+
If you turn your Ingress to belong a "explicit IngressGroup" by adding `group.name` annotation,
other Kubernetes users may create/modify their Ingresses to belong to the same IngressGroup, and can thus add more rules or overwrite existing rules with higher priority to the ALB for your Ingress.
@@ -95,7 +147,7 @@ By default, Ingresses don't belong to any IngressGroup, and we treat it as a "im
```
- `alb.ingress.kubernetes.io/group.order` specifies the order across all Ingresses within IngressGroup.
-
+
!!!note ""
- You can explicitly denote the order using a number between -1000 and 1000
- The smaller the order, the rule will be evaluated first. All Ingresses without an explicit order setting get order value as 0
@@ -110,26 +162,26 @@ By default, Ingresses don't belong to any IngressGroup, and we treat it as a "im
Traffic Listening can be controlled with the following annotations:
- `alb.ingress.kubernetes.io/listen-ports` specifies the ports that ALB listens on.
-
+
!!!note "Merge Behavior"
`listen-ports` is merged across all Ingresses in IngressGroup.
-
+
- You can define different listen-ports per Ingress, Ingress rules will only impact the ports defined for that Ingress.
- If same listen-port is defined by multiple Ingress within IngressGroup, Ingress rules will be merged with respect to their group order within IngressGroup.
!!!note "Default"
- defaults to `'[{"HTTP": 80}]'` or `'[{"HTTPS": 443}]'` depending on whether `certificate-arn` is specified.
- !!!warning ""
+ !!!warning ""
You may not have duplicate load balancer ports defined.
-
+
!!!example
```
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}, {"HTTP": 8080}, {"HTTPS": 8443}]'
```
-
+
- `alb.ingress.kubernetes.io/ssl-redirect` enables SSLRedirect and specifies the SSL port that redirects to.
-
+
!!!note "Merge Behavior"
`ssl-redirect` is exclusive across all Ingresses in IngressGroup.
@@ -152,7 +204,7 @@ Traffic Listening can be controlled with the following annotations:
```
- `alb.ingress.kubernetes.io/customer-owned-ipv4-pool` specifies the customer-owned IPv4 address pool for ALB on Outpost.
-
+
!!!warning ""
This annotation should be treated as immutable. To remove or change coIPv4Pool, you need to recreate Ingress.
@@ -212,7 +264,7 @@ Traffic Routing can be controlled with following annotations:
alb.ingress.kubernetes.io/backend-protocol: HTTPS
```
-- `alb.ingress.kubernetes.io/backend-protocol-version` specifies the application protocol used to route traffic to pods. Only valid when HTTP or HTTPS is used as the backend protocol.
+- `alb.ingress.kubernetes.io/backend-protocol-version` specifies the application protocol used to route traffic to pods. Only valid when HTTP or HTTPS is used as the backend protocol.
!!!example
- HTTP2
@@ -248,7 +300,7 @@ Traffic Routing can be controlled with following annotations:
ARN can be used in forward action(both simplified schema and advanced schema), it must be an targetGroup created outside of k8s, typically an targetGroup for legacy application.
!!!note "use ServiceName/ServicePort in forward Action"
ServiceName/ServicePort can be used in forward action(advanced schema only).
-
+
!!!warning ""
[Auth related annotations](#authentication) on Service object will only be respected if a single TargetGroup in is used.
@@ -309,24 +361,24 @@ Traffic Routing can be controlled with following annotations:
name: use-annotation
```
-- `alb.ingress.kubernetes.io/conditions.${conditions-name}` Provides a method for specifying routing conditions **in addition to original host/path condition on Ingress spec**.
-
- The `conditions-name` in the annotation must match the serviceName in the Ingress rules.
+- `alb.ingress.kubernetes.io/conditions.${conditions-name}` Provides a method for specifying routing conditions **in addition to original host/path condition on Ingress spec**.
+
+ The `conditions-name` in the annotation must match the serviceName in the Ingress rules.
It can be a either real serviceName or an annotation based action name when servicePort is `use-annotation`.
-
+
!!!warning "limitations"
General ALB limitations applies:
1. Each rule can optionally include up to one of each of the following conditions: host-header, http-request-method, path-pattern, and source-ip. Each rule can also optionally include one or more of each of the following conditions: http-header and query-string.
-
+
2. You can specify up to three match evaluations per condition.
-
+
3. You can specify up to five match evaluations per rule.
-
+
Refer [ALB documentation](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#rule-condition-types) for more details.
!!!example
- - rule-path1:
+ - rule-path1:
- Host is www.example.com OR anno.example.com
- Path is /path1
- rule-path2:
@@ -519,7 +571,7 @@ Access control for LoadBalancer can be controlled with following annotations:
- if same listen-port is defined by multiple Ingress within IngressGroup, `inbound-cidrs` should only be defined on one of the Ingress.
!!!note "Default"
-
+
- `0.0.0.0/0` will be used if the IPAddressType is "ipv4"
- `0.0.0.0/0` and `::/0` will be used if the IPAddressType is "dualstack"
@@ -592,7 +644,7 @@ ALB supports authentication with Cognito or OIDC. See [Authenticate Users Using
```
alb.ingress.kubernetes.io/auth-type: cognito
```
-
+
- `alb.ingress.kubernetes.io/auth-idp-cognito` specifies the cognito idp configuration.
!!!tip ""
@@ -604,7 +656,7 @@ ALB supports authentication with Cognito or OIDC. See [Authenticate Users Using
```
- `alb.ingress.kubernetes.io/auth-idp-oidc` specifies the oidc idp configuration.
-
+
!!!tip ""
You need to create an [secret](https://kubernetes.io/docs/concepts/configuration/secret/) within the same namespace as Ingress to hold your OIDC clientID and clientSecret. The format of secret is as below:
```yaml
@@ -624,12 +676,12 @@ ALB supports authentication with Cognito or OIDC. See [Authenticate Users Using
```
- `alb.ingress.kubernetes.io/auth-on-unauthenticated-request` specifies the behavior if the user is not authenticated.
-
+
!!!info "options:"
* **authenticate**: try authenticate with configured IDP.
* **deny**: return an HTTP 401 Unauthorized error.
* **allow**: allow the request to be forwarded to the target.
-
+
!!!example
```
alb.ingress.kubernetes.io/auth-on-unauthenticated-request: authenticate
@@ -643,7 +695,7 @@ ALB supports authentication with Cognito or OIDC. See [Authenticate Users Using
* **profile**
* **openid**
* **aws.cognito.signin.user.admin**
-
+
!!!example
```
alb.ingress.kubernetes.io/auth-scope: 'email openid'
@@ -655,7 +707,7 @@ ALB supports authentication with Cognito or OIDC. See [Authenticate Users Using
```
alb.ingress.kubernetes.io/auth-session-cookie: custom-cookie
```
-
+
- `alb.ingress.kubernetes.io/auth-session-timeout` specifies the maximum duration of the authentication session, in seconds
!!!example
@@ -767,7 +819,7 @@ TLS support can be controlled with the following annotations:
!!!tip ""
The first certificate in the list will be added as default certificate. And remaining certificate will be added to the optional certificate list.
See [SSL Certificates](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html#https-listener-certificates) for more details.
-
+
!!!tip "Certificate Discovery"
TLS certificates for ALB Listeners can be automatically discovered with hostnames from Ingress resources. See [Certificate Discovery](cert_discovery.md) for instructions.
@@ -780,7 +832,7 @@ TLS support can be controlled with the following annotations:
```
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:xxxxx:certificate/cert1,arn:aws:acm:us-west-2:xxxxx:certificate/cert2,arn:aws:acm:us-west-2:xxxxx:certificate/cert3
```
-
+
- `alb.ingress.kubernetes.io/ssl-policy` specifies the [Security Policy](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html#describe-ssl-policies) that should be assigned to the ALB, allowing you to control the protocol and ciphers.
!!!example
@@ -832,7 +884,7 @@ Custom attributes to LoadBalancers and TargetGroups can be controlled with follo
!!!note ""
- If `deletion_protection.enabled=true` is in annotation, the controller will not be able to delete the ALB during reconciliation. Once the attribute gets edited to `deletion_protection.enabled=false` during reconciliation, the deployer will force delete the resource.
- Please note, if the deletion protection is not enabled via annotation (e.g. via AWS console), the controller still deletes the underlying resource.
-
+
!!!example
- enable access log to s3
```
@@ -897,7 +949,7 @@ The AWS Load Balancer Controller automatically applies following tags to the AWS
In addition, you can use annotations to specify additional tags
- `alb.ingress.kubernetes.io/tags` specifies additional tags that will be applied to AWS resources created.
- In case of target group, the controller will merge the tags from the ingress and the backend service giving precedence
+ In case of target group, the controller will merge the tags from the ingress and the backend service giving precedence
to the values specified on the service when there is conflict.
!!!example
@@ -939,3 +991,10 @@ In addition, you can use annotations to specify additional tags
```alb.ingress.kubernetes.io/shield-advanced-protection: 'true'
```
+## VPC Endpoint Service
+A VPC Endpoint Service can be attached to a controlled loadbalancer via the following annotations:
+
+- ``alb.ingress.kubernetes.io/aws-load-balancer-endpoint-service-enabled` specifies whether to create a VPC Endpoint Service or not. The `--enable-endpoint-service` flag must also be set.
+- ``alb.ingress.kubernetes.io/aws-load-balancer-endpoint-service-acceptance-required` specifies whether requests to attach an Endpoint to the Endpoint Service require manual acceptance.
+- ``alb.ingress.kubernetes.io/aws-load-balancer-endpoint-service-allowed-principals` is a list of principals from which an Endpoint can be attached to this Endpoint Service.
+- ``alb.ingress.kubernetes.io/aws-load-balancer-endpoint-service-private-dns-name` is the private DNS name given to the Endpoint Service. This will need to be verifies through a valid DNS record.
\ No newline at end of file
diff --git a/docs/guide/service/annotations.md b/docs/guide/service/annotations.md
index f56d0419c8..bcf0352281 100644
--- a/docs/guide/service/annotations.md
+++ b/docs/guide/service/annotations.md
@@ -52,6 +52,10 @@
| [service.beta.kubernetes.io/aws-load-balancer-security-groups](#security-groups) | stringList | | |
| [service.beta.kubernetes.io/aws-load-balancer-manage-backend-security-group-rules](#manage-backend-sg-rules) | boolean | true | If `service.beta.kubernetes.io/aws-load-balancer-security-groups` is specified, this must also be explicitly specified otherwise it defaults to `false`. |
| [service.beta.kubernetes.io/aws-load-balancer-inbound-sg-rules-on-private-link-traffic](#update-security-settings) | string | |
+| [service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-enabled](#endpoint-service-enable)| boolean | false | |
+| [service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-acceptance-required](#endpoint-service-acceptance)| boolean| | |
+| [service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-allowed-principals](#endpoint-allowed-principals)|stringList| | |
+| [service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-private-dns-name](#endpoint-private-dns)| string | | |
## Traffic Routing
Traffic Routing can be controlled with following annotations:
@@ -513,6 +517,16 @@ Load balancer access can be controlled via following annotations:
service.beta.kubernetes.io/aws-load-balancer-inbound-sg-rules-on-private-link-traffic: "off"
```
+## VPC Endpoint Service
+A VPC Endpoint Service can be attached to a controlled loadbalancer via the following annotations:
+
+- `service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-enabled` specifies whether to create a VPC Endpoint Service or not. The `--enable-endpoint-service` flag must also be set.
+
+- `service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-acceptance-required` specifies whether requests to attach an Endpoint to the Endpoint Service require manual acceptance.
+
+- `service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-allowed-principals` is a list of principals from which an Endpoint can be attached to this Endpoint Service.
+
+- `service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-private-dns-name` is the private DNS name given to the Endpoint Service. This will need to be verified through a valid DNS record.
## Legacy Cloud Provider
The AWS Load Balancer Controller manages Kubernetes Services in a compatible way with the AWS cloud provider's legacy service controller.
diff --git a/docs/install/iam_policy.json b/docs/install/iam_policy.json
index e8a05f8e64..bd7c47db65 100644
--- a/docs/install/iam_policy.json
+++ b/docs/install/iam_policy.json
@@ -124,6 +124,66 @@
}
}
},
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:CreateVpcEndpointServiceConfiguration"
+ ],
+ "Resource": "*"
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:CreateTags"
+ ],
+ "Resource": "arn:aws:ec2:*:*:vpc-endpoint-service/*",
+ "Condition": {
+ "Null": {
+ "aws:RequestTag/elbv2.k8s.aws/cluster": "false"
+ },
+ "StringEquals": {
+ "ec2:CreateAction": "CreateVpcEndpointServiceConfiguration"
+ }
+ }
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:CreateTags",
+ "ec2:DeleteTags"
+ ],
+ "Resource": "arn:aws:ec2:*:*:vpc-endpoint-service/*",
+ "Condition": {
+ "Null": {
+ "aws:RequestTag/elbv2.k8s.aws/cluster": "true",
+ "aws:ResourceTag/elbv2.k8s.aws/cluster": "false"
+ }
+ }
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:DeleteVpcEndpointServiceConfigurations",
+ "ec2:ModifyVpcEndpointServiceConfiguration",
+ "ec2:ModifyVpcEndpointServicePermissions",
+ "ec2:StartVpcEndpointServicePrivateDnsVerification"
+ ],
+ "Resource": "*",
+ "Condition": {
+ "Null": {
+ "aws:ResourceTag/elbv2.k8s.aws/cluster": "false"
+ }
+ }
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:DescribeVpcEndpointServiceConfigurations",
+ "ec2:DescribeVpcEndpointServicePermissions",
+ "ec2:DescribeVpcEndpointServices"
+ ],
+ "Resource": "*"
+ },
{
"Effect": "Allow",
"Action": [
diff --git a/docs/install/iam_policy_cn.json b/docs/install/iam_policy_cn.json
index cb8bc040e3..d71122cebf 100644
--- a/docs/install/iam_policy_cn.json
+++ b/docs/install/iam_policy_cn.json
@@ -124,6 +124,66 @@
}
}
},
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:CreateVpcEndpointServiceConfiguration"
+ ],
+ "Resource": "*"
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:CreateTags"
+ ],
+ "Resource": "arn:aws-cn:ec2:*:*:vpc-endpoint-service/*",
+ "Condition": {
+ "Null": {
+ "aws:RequestTag/elbv2.k8s.aws/cluster": "false"
+ },
+ "StringEquals": {
+ "ec2:CreateAction": "CreateVpcEndpointServiceConfiguration"
+ }
+ }
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:CreateTags",
+ "ec2:DeleteTags"
+ ],
+ "Resource": "arn:aws-cn:ec2:*:*:vpc-endpoint-service/*",
+ "Condition": {
+ "Null": {
+ "aws:RequestTag/elbv2.k8s.aws/cluster": "true",
+ "aws:ResourceTag/elbv2.k8s.aws/cluster": "false"
+ }
+ }
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:DeleteVpcEndpointServiceConfigurations",
+ "ec2:ModifyVpcEndpointServiceConfiguration",
+ "ec2:ModifyVpcEndpointServicePermissions",
+ "ec2:StartVpcEndpointServicePrivateDnsVerification"
+ ],
+ "Resource": "*",
+ "Condition": {
+ "Null": {
+ "aws:ResourceTag/elbv2.k8s.aws/cluster": "false"
+ }
+ }
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:DescribeVpcEndpointServiceConfigurations",
+ "ec2:DescribeVpcEndpointServicePermissions",
+ "ec2:DescribeVpcEndpointServices"
+ ],
+ "Resource": "*"
+ },
{
"Effect": "Allow",
"Action": [
diff --git a/docs/install/iam_policy_us-gov.json b/docs/install/iam_policy_us-gov.json
index 97ccf2ebbf..9e7f2df56d 100644
--- a/docs/install/iam_policy_us-gov.json
+++ b/docs/install/iam_policy_us-gov.json
@@ -124,6 +124,66 @@
}
}
},
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:CreateVpcEndpointServiceConfiguration"
+ ],
+ "Resource": "*"
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:CreateTags"
+ ],
+ "Resource": "arn:aws-us-gov:ec2:*:*:vpc-endpoint-service/*",
+ "Condition": {
+ "Null": {
+ "aws:RequestTag/elbv2.k8s.aws/cluster": "false"
+ },
+ "StringEquals": {
+ "ec2:CreateAction": "CreateVpcEndpointServiceConfiguration"
+ }
+ }
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:CreateTags",
+ "ec2:DeleteTags"
+ ],
+ "Resource": "arn:aws-us-gov:ec2:*:*:vpc-endpoint-service/*",
+ "Condition": {
+ "Null": {
+ "aws:RequestTag/elbv2.k8s.aws/cluster": "true",
+ "aws:ResourceTag/elbv2.k8s.aws/cluster": "false"
+ }
+ }
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:DeleteVpcEndpointServiceConfigurations",
+ "ec2:ModifyVpcEndpointServiceConfiguration",
+ "ec2:ModifyVpcEndpointServicePermissions",
+ "ec2:StartVpcEndpointServicePrivateDnsVerification"
+ ],
+ "Resource": "*",
+ "Condition": {
+ "Null": {
+ "aws:ResourceTag/elbv2.k8s.aws/cluster": "false"
+ }
+ }
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:DescribeVpcEndpointServiceConfigurations",
+ "ec2:DescribeVpcEndpointServicePermissions",
+ "ec2:DescribeVpcEndpointServices"
+ ],
+ "Resource": "*"
+ },
{
"Effect": "Allow",
"Action": [
diff --git a/main.go b/main.go
index d9435e1755..8239a8d2e1 100644
--- a/main.go
+++ b/main.go
@@ -103,6 +103,7 @@ func main() {
podInfoRepo := k8s.NewDefaultPodInfoRepo(clientSet.CoreV1().RESTClient(), controllerCFG.RuntimeConfig.WatchNamespace, ctrl.Log)
finalizerManager := k8s.NewDefaultFinalizerManager(mgr.GetClient(), ctrl.Log)
sgManager := networking.NewDefaultSecurityGroupManager(cloud.EC2(), ctrl.Log)
+ esManager := networking.NewDefaultVPCEndpointServiceManager(cloud.EC2(), ctrl.Log)
sgReconciler := networking.NewDefaultSecurityGroupReconciler(sgManager, ctrl.Log)
azInfoProvider := networking.NewDefaultAZInfoProvider(cloud.EC2(), ctrl.Log.WithName("az-info-provider"))
vpcInfoProvider := networking.NewDefaultVPCInfoProvider(cloud.EC2(), ctrl.Log.WithName("vpc-info-provider"))
@@ -116,10 +117,10 @@ func main() {
sgResolver := networking.NewDefaultSecurityGroupResolver(cloud.EC2(), cloud.VpcID())
elbv2TaggingManager := elbv2deploy.NewDefaultTaggingManager(cloud.ELBV2(), cloud.VpcID(), controllerCFG.FeatureGates, cloud.RGT(), ctrl.Log)
ingGroupReconciler := ingress.NewGroupReconciler(cloud, mgr.GetClient(), mgr.GetEventRecorderFor("ingress"),
- finalizerManager, sgManager, sgReconciler, subnetResolver, elbv2TaggingManager,
+ finalizerManager, sgManager, esManager, sgReconciler, subnetResolver, elbv2TaggingManager,
controllerCFG, backendSGProvider, sgResolver, ctrl.Log.WithName("controllers").WithName("ingress"))
svcReconciler := service.NewServiceReconciler(cloud, mgr.GetClient(), mgr.GetEventRecorderFor("service"),
- finalizerManager, sgManager, sgReconciler, subnetResolver, vpcInfoProvider, elbv2TaggingManager,
+ finalizerManager, sgManager, esManager, sgReconciler, subnetResolver, vpcInfoProvider, elbv2TaggingManager,
controllerCFG, backendSGProvider, sgResolver, ctrl.Log.WithName("controllers").WithName("service"))
tgbReconciler := elbv2controller.NewTargetGroupBindingReconciler(mgr.GetClient(), mgr.GetEventRecorderFor("targetGroupBinding"),
finalizerManager, tgbResManager,
diff --git a/pkg/algorithm/strings.go b/pkg/algorithm/strings.go
index 46b6a8c43e..cfecbde08a 100644
--- a/pkg/algorithm/strings.go
+++ b/pkg/algorithm/strings.go
@@ -1,5 +1,7 @@
package algorithm
+import "k8s.io/apimachinery/pkg/util/sets"
+
// ChunkStrings will split slice of String into chunks
func ChunkStrings(targets []string, chunkSize int) [][]string {
var chunks [][]string
@@ -12,3 +14,29 @@ func ChunkStrings(targets []string, chunkSize int) [][]string {
}
return chunks
}
+
+// DiffStringSlice returns three lists these consist of :-
+// - all the elements in the first argument but not the second
+// - all the elements in both arguments
+// - all the elements in the second argument but not in the first
+func DiffStringSlice(first, second []string) ([]*string, []*string, []*string) {
+ firstSet := sets.NewString(first...)
+ secondSet := sets.NewString(second...)
+
+ matchFirst := make([]*string, 0)
+ matchBoth := make([]*string, 0)
+ matchSecond := make([]*string, 0)
+ for _, elem := range firstSet.Difference(secondSet).List() {
+ elem := elem
+ matchFirst = append(matchFirst, &elem)
+ }
+ for _, elem := range secondSet.Difference(firstSet).List() {
+ elem := elem
+ matchSecond = append(matchSecond, &elem)
+ }
+ for _, elem := range firstSet.Intersection(secondSet).List() {
+ elem := elem
+ matchBoth = append(matchBoth, &elem)
+ }
+ return matchFirst, matchBoth, matchSecond
+}
diff --git a/pkg/algorithm/strings_test.go b/pkg/algorithm/strings_test.go
index 5dbf3c0960..39d0877213 100644
--- a/pkg/algorithm/strings_test.go
+++ b/pkg/algorithm/strings_test.go
@@ -81,3 +81,94 @@ func TestChunkStrings(t *testing.T) {
})
}
}
+
+func TestDiffStringSlice(t *testing.T) {
+ a := "a"
+ b := "b"
+ c := "c"
+
+ type args struct {
+ first []string
+ second []string
+ }
+ type want struct {
+ matchFirst []*string
+ matchBoth []*string
+ matchSecond []*string
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "when only first has values",
+ args: args{
+ first: []string{"a", "b"},
+ second: []string{},
+ },
+ want: want{
+ matchFirst: []*string{&a, &b},
+ matchBoth: []*string{},
+ matchSecond: []*string{},
+ },
+ },
+ {
+ name: "when only second has values",
+ args: args{
+ first: []string{},
+ second: []string{"a", "b"},
+ },
+ want: want{
+ matchFirst: []*string{},
+ matchBoth: []*string{},
+ matchSecond: []*string{&a, &b},
+ },
+ },
+ {
+ name: "when first and second are identical",
+ args: args{
+ first: []string{"a", "b"},
+ second: []string{"a", "b"},
+ },
+ want: want{
+ matchFirst: []*string{},
+ matchBoth: []*string{&a, &b},
+ matchSecond: []*string{},
+ },
+ },
+ {
+ name: "when no values are used",
+ args: args{
+ first: []string{},
+ second: []string{},
+ },
+ want: want{
+ matchFirst: []*string{},
+ matchBoth: []*string{},
+ matchSecond: []*string{},
+ },
+ },
+ {
+ name: "when all return values are required",
+ args: args{
+ first: []string{"a", "b"},
+ second: []string{"b", "c"},
+ },
+ want: want{
+ matchFirst: []*string{&a},
+ matchBoth: []*string{&b},
+ matchSecond: []*string{&c},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // t.Parallel()
+ matchFirst, matchBoth, matchSecond := DiffStringSlice(tt.args.first, tt.args.second)
+ assert.Equal(t, tt.want.matchFirst, matchFirst)
+ assert.Equal(t, tt.want.matchBoth, matchBoth)
+ assert.Equal(t, tt.want.matchSecond, matchSecond)
+ })
+ }
+}
diff --git a/pkg/annotations/constants.go b/pkg/annotations/constants.go
index 493e0427ef..c3f8c6474a 100644
--- a/pkg/annotations/constants.go
+++ b/pkg/annotations/constants.go
@@ -88,4 +88,10 @@ const (
SvcLBSuffixManageSGRules = "aws-load-balancer-manage-backend-security-group-rules"
SvcLBSuffixEnforceSGInboundRulesOnPrivateLinkTraffic = "aws-load-balancer-inbound-sg-rules-on-private-link-traffic"
SvcLBSuffixSecurityGroupPrefixLists = "aws-load-balancer-security-group-prefix-lists"
+
+ // prefixes service.alpha.kubernetes.io, service.kubernetes.io
+ SvcLBSuffixEndpointServiceEnabled = "aws-load-balancer-endpoint-service-enabled"
+ SvcLBSuffixEndpointServiceAcceptanceRequired = "aws-load-balancer-endpoint-service-acceptance-required"
+ SvcLBSuffixEndpointServiceAllowedPrincipals = "aws-load-balancer-endpoint-service-allowed-principals"
+ SvcLBSuffixEndpointServicePrivateDNSName = "aws-load-balancer-endpoint-service-private-dns-name"
)
diff --git a/pkg/aws/services/ec2.go b/pkg/aws/services/ec2.go
index 80230f96f3..fa43f1657c 100644
--- a/pkg/aws/services/ec2.go
+++ b/pkg/aws/services/ec2.go
@@ -23,6 +23,9 @@ type EC2 interface {
// DescribeSubnetsAsList wraps the DescribeSubnetsPagesWithContext API, which aggregates paged results into list.
DescribeSubnetsAsList(ctx context.Context, input *ec2.DescribeSubnetsInput) ([]*ec2.Subnet, error)
+ // wrapper to DescribeVpcEndpointServiceConfigurationsPagesWithContext API, which aggregates paged results into list.
+ DescribeVpcEndpointServicesAsList(ctx context.Context, input *ec2.DescribeVpcEndpointServiceConfigurationsInput) ([]*ec2.ServiceConfiguration, error)
+
// DescribeVPCsAsList wraps the DescribeVpcsPagesWithContext API, which aggregates paged results into list.
DescribeVPCsAsList(ctx context.Context, input *ec2.DescribeVpcsInput) ([]*ec2.Vpc, error)
}
@@ -84,6 +87,17 @@ func (c *defaultEC2) DescribeSubnetsAsList(ctx context.Context, input *ec2.Descr
return result, nil
}
+func (c *defaultEC2) DescribeVpcEndpointServicesAsList(ctx context.Context, input *ec2.DescribeVpcEndpointServiceConfigurationsInput) ([]*ec2.ServiceConfiguration, error) {
+ var result []*ec2.ServiceConfiguration
+ if err := c.DescribeVpcEndpointServiceConfigurationsPagesWithContext(ctx, input, func(output *ec2.DescribeVpcEndpointServiceConfigurationsOutput, _ bool) bool {
+ result = append(result, output.ServiceConfigurations...)
+ return true
+ }); err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
func (c *defaultEC2) DescribeVPCsAsList(ctx context.Context, input *ec2.DescribeVpcsInput) ([]*ec2.Vpc, error) {
var result []*ec2.Vpc
if err := c.DescribeVpcsPagesWithContext(ctx, input, func(output *ec2.DescribeVpcsOutput, _ bool) bool {
diff --git a/pkg/aws/services/ec2_mocks.go b/pkg/aws/services/ec2_mocks.go
index e3ffd34e3d..00abf9e789 100644
--- a/pkg/aws/services/ec2_mocks.go
+++ b/pkg/aws/services/ec2_mocks.go
@@ -21856,6 +21856,21 @@ func (mr *MockEC2MockRecorder) DescribeVpcEndpointServices(arg0 interface{}) *go
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeVpcEndpointServices", reflect.TypeOf((*MockEC2)(nil).DescribeVpcEndpointServices), arg0)
}
+// DescribeVpcEndpointServicesAsList mocks base method.
+func (m *MockEC2) DescribeVpcEndpointServicesAsList(arg0 context.Context, arg1 *ec2.DescribeVpcEndpointServiceConfigurationsInput) ([]*ec2.ServiceConfiguration, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DescribeVpcEndpointServicesAsList", arg0, arg1)
+ ret0, _ := ret[0].([]*ec2.ServiceConfiguration)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// DescribeVpcEndpointServicesAsList indicates an expected call of DescribeVpcEndpointServicesAsList.
+func (mr *MockEC2MockRecorder) DescribeVpcEndpointServicesAsList(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeVpcEndpointServicesAsList", reflect.TypeOf((*MockEC2)(nil).DescribeVpcEndpointServicesAsList), arg0, arg1)
+}
+
// DescribeVpcEndpointServicesRequest mocks base method.
func (m *MockEC2) DescribeVpcEndpointServicesRequest(arg0 *ec2.DescribeVpcEndpointServicesInput) (*request.Request, *ec2.DescribeVpcEndpointServicesOutput) {
m.ctrl.T.Helper()
diff --git a/pkg/config/addons_config.go b/pkg/config/addons_config.go
index 2fca79c018..bdf49eb49c 100644
--- a/pkg/config/addons_config.go
+++ b/pkg/config/addons_config.go
@@ -3,10 +3,11 @@ package config
import "github.com/spf13/pflag"
const (
- flagWAFEnabled = "enable-waf"
- flagWAFV2Enabled = "enable-wafv2"
- flagShieldEnabled = "enable-shield"
- defaultEnabled = true
+ flagWAFEnabled = "enable-waf"
+ flagWAFV2Enabled = "enable-wafv2"
+ flagShieldEnabled = "enable-shield"
+ flagEndpointServiceEnabled = "enable-endpoint-service"
+ defaultEnabled = true
)
// AddonsConfig contains configuration for the addon features
@@ -17,6 +18,8 @@ type AddonsConfig struct {
WAFV2Enabled bool
// Shield addon for ALB
ShieldEnabled bool
+ // Endpoint Service addon for NLB
+ EndpointServiceEnabled bool
}
// BindFlags binds the command line flags to the fields in the config object
@@ -24,4 +27,5 @@ func (f *AddonsConfig) BindFlags(fs *pflag.FlagSet) {
fs.BoolVar(&f.WAFEnabled, flagWAFEnabled, defaultEnabled, "Enable WAF addon for ALB")
fs.BoolVar(&f.WAFV2Enabled, flagWAFV2Enabled, defaultEnabled, "Enable WAF V2 addon for ALB")
fs.BoolVar(&f.ShieldEnabled, flagShieldEnabled, defaultEnabled, "Enable Shield addon for ALB")
+ fs.BoolVar(&f.EndpointServiceEnabled, flagEndpointServiceEnabled, defaultEnabled, "Enable VPC Endpoint Service addon for NLB")
}
diff --git a/pkg/deploy/ec2/endpoint_service_manager.go b/pkg/deploy/ec2/endpoint_service_manager.go
new file mode 100644
index 0000000000..44d1e8ee24
--- /dev/null
+++ b/pkg/deploy/ec2/endpoint_service_manager.go
@@ -0,0 +1,275 @@
+package ec2
+
+import (
+ "context"
+ "fmt"
+
+ awssdk "github.com/aws/aws-sdk-go/aws"
+ ec2sdk "github.com/aws/aws-sdk-go/service/ec2"
+ "github.com/go-logr/logr"
+ "github.com/pkg/errors"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/algorithm"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/aws/services"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/tracking"
+ ec2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/ec2"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/networking"
+)
+
+// abstraction around endpoint service operations for EC2.
+type EndpointServiceManager interface {
+ Create(ctx context.Context, resES *ec2model.VPCEndpointService) (ec2model.VPCEndpointServiceStatus, error)
+
+ Update(ctx context.Context, resES *ec2model.VPCEndpointService, sdkES networking.VPCEndpointServiceInfo) (ec2model.VPCEndpointServiceStatus, error)
+
+ Delete(ctx context.Context, sdkES networking.VPCEndpointServiceInfo) error
+
+ ReconcilePermissions(ctx context.Context, permissions *ec2model.VPCEndpointServicePermissions) error
+}
+
+// NewDefaultEndpointServiceManager constructs new defaultEndpointServiceManager.
+func NewDefaultEndpointServiceManager(ec2Client services.EC2, vpcID string, logger logr.Logger, trackingProvider tracking.Provider, taggingManager TaggingManager, externalManagedTags []string) *defaultEndpointServiceManager {
+ return &defaultEndpointServiceManager{
+ ec2Client: ec2Client,
+ vpcID: vpcID,
+ logger: logger,
+ taggingManager: taggingManager,
+ trackingProvider: trackingProvider,
+ externalManagedTags: externalManagedTags,
+ }
+}
+
+var _ EndpointServiceManager = &defaultEndpointServiceManager{}
+
+// default implementation for EndpointServiceManager.
+type defaultEndpointServiceManager struct {
+ ec2Client services.EC2
+ vpcID string
+ logger logr.Logger
+ taggingManager TaggingManager
+ trackingProvider tracking.Provider
+ externalManagedTags []string
+}
+
+func (m *defaultEndpointServiceManager) Create(ctx context.Context, resES *ec2model.VPCEndpointService) (ec2model.VPCEndpointServiceStatus, error) {
+ esTags := m.trackingProvider.ResourceTags(resES.Stack(), resES, resES.Spec.Tags)
+ sdkTags := convertTagsToSDKTags(esTags)
+
+ var resolvedLoadBalancerArns []string
+ for _, unresolved := range resES.Spec.NetworkLoadBalancerArns {
+ arn, err := unresolved.Resolve(ctx)
+ if err != nil {
+ return ec2model.VPCEndpointServiceStatus{}, err
+ }
+ resolvedLoadBalancerArns = append(resolvedLoadBalancerArns, arn)
+ }
+
+ var privateDnsName *string
+ if resES.Spec.PrivateDNSName != nil {
+ privateDnsName = awssdk.String(*resES.Spec.PrivateDNSName)
+ }
+
+ req := ec2sdk.CreateVpcEndpointServiceConfigurationInput{
+ AcceptanceRequired: awssdk.Bool(*resES.Spec.AcceptanceRequired),
+ PrivateDnsName: privateDnsName,
+ NetworkLoadBalancerArns: awssdk.StringSlice(resolvedLoadBalancerArns),
+ TagSpecifications: []*ec2sdk.TagSpecification{
+ {
+ ResourceType: awssdk.String("vpc-endpoint-service"),
+ Tags: sdkTags,
+ },
+ },
+ }
+ m.logger.Info("creating VpcEndpointService",
+ "resourceID", resES.ID())
+ resp, err := m.ec2Client.CreateVpcEndpointServiceConfigurationWithContext(ctx, &req)
+ if err != nil {
+ return ec2model.VPCEndpointServiceStatus{}, err
+ }
+ serviceID := awssdk.StringValue(resp.ServiceConfiguration.ServiceId)
+ m.logger.Info("created VpcEndpointService",
+ "resourceID", resES.ID(),
+ "serviceID", serviceID)
+
+ return ec2model.VPCEndpointServiceStatus{
+ ServiceID: serviceID,
+ }, nil
+}
+
+func (m *defaultEndpointServiceManager) Update(ctx context.Context, resES *ec2model.VPCEndpointService, sdkES networking.VPCEndpointServiceInfo) (ec2model.VPCEndpointServiceStatus, error) {
+
+ m.logger.Info("Updating", "resES", resES, "sdkES", sdkES)
+
+ var resLBArnsRaw []string
+ for _, lb := range resES.Spec.NetworkLoadBalancerArns {
+ arn, err := lb.Resolve(ctx)
+ if err != nil {
+ return ec2model.VPCEndpointServiceStatus{}, err
+ }
+ resLBArnsRaw = append(resLBArnsRaw, arn)
+ }
+
+ if err := m.updateSDKVPCEndpointServiceWithTags(ctx, resES, sdkES); err != nil {
+ return ec2model.VPCEndpointServiceStatus{}, err
+ }
+
+ addLBArns, _, removeLBArns := algorithm.DiffStringSlice(resLBArnsRaw, sdkES.NetworkLoadBalancerArns)
+ // The API call expects these to be nil if no changes are required. An empty list returns an error
+ if len(addLBArns) == 0 {
+ addLBArns = nil
+ }
+ if len(removeLBArns) == 0 {
+ removeLBArns = nil
+ }
+
+ var acceptanceRequired *bool
+ if resES.Spec.AcceptanceRequired != nil && *resES.Spec.AcceptanceRequired != sdkES.AcceptanceRequired {
+ acceptanceRequired = resES.Spec.AcceptanceRequired
+ }
+
+ var privateDNSName *string
+ var removePrivateDNSName *bool
+ if resES.Spec.PrivateDNSName == nil && sdkES.PrivateDNSName != nil {
+ removePrivateDNSName = awssdk.Bool(true)
+ } else if resES.Spec.PrivateDNSName != sdkES.PrivateDNSName {
+ privateDNSName = resES.Spec.PrivateDNSName
+ }
+
+ if len(addLBArns) > 0 || len(removeLBArns) > 0 || acceptanceRequired != nil || privateDNSName != nil || removePrivateDNSName != nil {
+
+ serviceId := &sdkES.ServiceID
+
+ m.logger.Info(
+ "Updating VPCEndpointService",
+ "addLBArns", addLBArns,
+ "removeLBArns", removeLBArns,
+ "acceptanceRequired", acceptanceRequired,
+ "privateDNSName", privateDNSName,
+ "removePrivateDNSName", removePrivateDNSName,
+ "serviceId", serviceId,
+ )
+
+ req := ec2sdk.ModifyVpcEndpointServiceConfigurationInput{
+ AcceptanceRequired: acceptanceRequired,
+ AddNetworkLoadBalancerArns: addLBArns,
+ RemoveNetworkLoadBalancerArns: removeLBArns,
+ PrivateDnsName: privateDNSName,
+ RemovePrivateDnsName: removePrivateDNSName,
+ ServiceId: serviceId,
+ }
+
+ _, err := m.ec2Client.ModifyVpcEndpointServiceConfigurationWithContext(ctx, &req)
+ if err != nil {
+ return ec2model.VPCEndpointServiceStatus{}, err
+ }
+ } else {
+ m.logger.Info(
+ "Not updating VPCEndpointService",
+ )
+ }
+
+ return ec2model.VPCEndpointServiceStatus{
+ ServiceID: sdkES.ServiceID,
+ }, nil
+}
+
+func (m *defaultEndpointServiceManager) Delete(ctx context.Context, sdkES networking.VPCEndpointServiceInfo) error {
+ req := &ec2sdk.DeleteVpcEndpointServiceConfigurationsInput{
+ ServiceIds: awssdk.StringSlice(
+ []string{sdkES.ServiceID},
+ ),
+ }
+
+ m.logger.Info("deleting VPCEndpointService",
+ "serviceId", sdkES.ServiceID)
+ unsuccessful, err := m.ec2Client.DeleteVpcEndpointServiceConfigurationsWithContext(ctx, req)
+ if err != nil {
+ return errors.Wrap(err, "failed to delete VPCEndpointService")
+ }
+ if unsuccessful != nil {
+ for _, endpoint := range unsuccessful.Unsuccessful {
+ // We return the first error found.
+ // We shouldn't be deleteing more than one endpoint with this call so this
+ // slice should never have more than one element.
+ return fmt.Errorf("failed to delete VPCEndpointService '%s'. Reason: '%s'", *endpoint.ResourceId, *endpoint.Error.Message)
+ }
+ }
+ m.logger.Info("deleted VPCEndpointService",
+ "serviceId", sdkES.ServiceID)
+
+ return nil
+}
+
+func (m *defaultEndpointServiceManager) ReconcilePermissions(ctx context.Context, permissions *ec2model.VPCEndpointServicePermissions) error {
+ m.logger.Info("reconciling permissions")
+
+ serviceId, err := permissions.Spec.ServiceId.Resolve(ctx)
+ if err != nil {
+ return errors.Wrap(err, "failed to resolve VPCEndpointServicePermissions serviceID")
+ }
+ req := &ec2sdk.DescribeVpcEndpointServicePermissionsInput{
+ ServiceId: &serviceId,
+ }
+
+ m.logger.Info("reconciling permissions for service", "serviceId", serviceId)
+
+ permissionsInfo, err := m.fetchESPermissionInfosFromAWS(ctx, req)
+ if err != nil {
+ m.logger.Info("error while fetching existing VPC endpoint service permissions")
+ return errors.Wrap(err, "failed to fetch existing VPCEndpointServicePermissions")
+ }
+
+ addPrincipals, _, removePrincipals := algorithm.DiffStringSlice(permissions.Spec.AllowedPrincipals, permissionsInfo.AllowedPrincipals)
+ // The API call expects these to be nil if no changes are required. An empty list returns an error
+ if len(addPrincipals) == 0 {
+ addPrincipals = nil
+ }
+ if len(removePrincipals) == 0 {
+ removePrincipals = nil
+ }
+ modReq := &ec2sdk.ModifyVpcEndpointServicePermissionsInput{
+ AddAllowedPrincipals: addPrincipals,
+ RemoveAllowedPrincipals: removePrincipals,
+ ServiceId: &serviceId,
+ }
+
+ m.logger.Info("Build priciples",
+ "AddPrincipals", addPrincipals,
+ "RemovePrincipals", removePrincipals,
+ )
+
+ if len(addPrincipals) > 0 || len(removePrincipals) > 0 {
+
+ m.logger.Info("modifying VpcEndpointService permissions",
+ "serviceID", serviceId,
+ "addPrincipals", addPrincipals,
+ "removePrincipals", removePrincipals,
+ )
+
+ _, err := m.ec2Client.ModifyVpcEndpointServicePermissionsWithContext(ctx, modReq)
+ if err != nil {
+ return errors.Wrap(err, "failed to modify VPCEndpointServicePermissions")
+ }
+
+ m.logger.Info("modified VpcEndpointService permissions",
+ "serviceID", serviceId)
+ }
+
+ return nil
+}
+
+func (m *defaultEndpointServiceManager) fetchESPermissionInfosFromAWS(ctx context.Context, req *ec2sdk.DescribeVpcEndpointServicePermissionsInput) (networking.VPCEndpointServicePermissionsInfo, error) {
+ endpointServicePermissions, err := m.ec2Client.DescribeVpcEndpointServicePermissionsWithContext(ctx, req)
+ if err != nil {
+ return networking.VPCEndpointServicePermissionsInfo{}, errors.Wrap(err, "Failed to fetch VPCEndpointPermissions from AWS")
+ }
+ return networking.NewRawVPCEndpointServicePermissionsInfo(endpointServicePermissions), nil
+}
+
+func (m *defaultEndpointServiceManager) updateSDKVPCEndpointServiceWithTags(ctx context.Context, resVPCES *ec2model.VPCEndpointService, sdkVPCES networking.VPCEndpointServiceInfo) error {
+ desiredVPCESTags := m.trackingProvider.ResourceTags(resVPCES.Stack(), resVPCES, resVPCES.Spec.Tags)
+ return m.taggingManager.ReconcileTags(ctx, sdkVPCES.ServiceID, desiredVPCESTags,
+ WithCurrentTags(sdkVPCES.Tags),
+ WithIgnoredTagKeys(m.trackingProvider.LegacyTagKeys()),
+ WithIgnoredTagKeys(m.externalManagedTags),
+ )
+}
diff --git a/pkg/deploy/ec2/endpoint_service_manager_mocks.go b/pkg/deploy/ec2/endpoint_service_manager_mocks.go
new file mode 100644
index 0000000000..fe166c2d51
--- /dev/null
+++ b/pkg/deploy/ec2/endpoint_service_manager_mocks.go
@@ -0,0 +1,95 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/ec2 (interfaces: EndpointServiceManager)
+
+// Package ec2 is a generated GoMock package.
+package ec2
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ ec2 "sigs.k8s.io/aws-load-balancer-controller/pkg/model/ec2"
+ networking "sigs.k8s.io/aws-load-balancer-controller/pkg/networking"
+)
+
+// MockEndpointServiceManager is a mock of EndpointServiceManager interface.
+type MockEndpointServiceManager struct {
+ ctrl *gomock.Controller
+ recorder *MockEndpointServiceManagerMockRecorder
+}
+
+// MockEndpointServiceManagerMockRecorder is the mock recorder for MockEndpointServiceManager.
+type MockEndpointServiceManagerMockRecorder struct {
+ mock *MockEndpointServiceManager
+}
+
+// NewMockEndpointServiceManager creates a new mock instance.
+func NewMockEndpointServiceManager(ctrl *gomock.Controller) *MockEndpointServiceManager {
+ mock := &MockEndpointServiceManager{ctrl: ctrl}
+ mock.recorder = &MockEndpointServiceManagerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockEndpointServiceManager) EXPECT() *MockEndpointServiceManagerMockRecorder {
+ return m.recorder
+}
+
+// Create mocks base method.
+func (m *MockEndpointServiceManager) Create(arg0 context.Context, arg1 *ec2.VPCEndpointService) (ec2.VPCEndpointServiceStatus, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Create", arg0, arg1)
+ ret0, _ := ret[0].(ec2.VPCEndpointServiceStatus)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Create indicates an expected call of Create.
+func (mr *MockEndpointServiceManagerMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockEndpointServiceManager)(nil).Create), arg0, arg1)
+}
+
+// Delete mocks base method.
+func (m *MockEndpointServiceManager) Delete(arg0 context.Context, arg1 networking.VPCEndpointServiceInfo) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Delete", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Delete indicates an expected call of Delete.
+func (mr *MockEndpointServiceManagerMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockEndpointServiceManager)(nil).Delete), arg0, arg1)
+}
+
+// ReconcilePermissions mocks base method.
+func (m *MockEndpointServiceManager) ReconcilePermissions(arg0 context.Context, arg1 *ec2.VPCEndpointServicePermissions) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ReconcilePermissions", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ReconcilePermissions indicates an expected call of ReconcilePermissions.
+func (mr *MockEndpointServiceManagerMockRecorder) ReconcilePermissions(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReconcilePermissions", reflect.TypeOf((*MockEndpointServiceManager)(nil).ReconcilePermissions), arg0, arg1)
+}
+
+// Update mocks base method.
+func (m *MockEndpointServiceManager) Update(arg0 context.Context, arg1 *ec2.VPCEndpointService, arg2 networking.VPCEndpointServiceInfo) (ec2.VPCEndpointServiceStatus, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2)
+ ret0, _ := ret[0].(ec2.VPCEndpointServiceStatus)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Update indicates an expected call of Update.
+func (mr *MockEndpointServiceManagerMockRecorder) Update(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockEndpointServiceManager)(nil).Update), arg0, arg1, arg2)
+}
diff --git a/pkg/deploy/ec2/endpoint_service_manager_test.go b/pkg/deploy/ec2/endpoint_service_manager_test.go
new file mode 100644
index 0000000000..03f690c2bd
--- /dev/null
+++ b/pkg/deploy/ec2/endpoint_service_manager_test.go
@@ -0,0 +1,797 @@
+package ec2
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ awssdk "github.com/aws/aws-sdk-go/aws"
+ ec2sdk "github.com/aws/aws-sdk-go/service/ec2"
+ "github.com/go-logr/logr"
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/aws/services"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/tracking"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/model/ec2"
+ ec2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/ec2"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/networking"
+)
+
+type testStringToken struct {
+ core.Token
+ value string
+ err error
+}
+
+func (t testStringToken) Resolve(ctx context.Context) (string, error) {
+ return t.value, t.err
+}
+
+type DescribeVpcEndpointServicePermissionsWithContextResponse struct {
+ response *ec2sdk.DescribeVpcEndpointServicePermissionsOutput
+ err error
+}
+
+func Test_Create(t *testing.T) {
+ lbArn := "lbArn"
+ privateDNSName := "http://example.com"
+ serviceID := "serviceID"
+ tags := map[string]string{
+ "key": "value",
+ }
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ nlbResolveError error
+ createAPICallError error
+ shouldError bool
+ }{
+ {
+ name: "returns an error when the service id can't be resolved",
+ nlbResolveError: errors.New("test_error"),
+ createAPICallError: nil,
+ shouldError: true,
+ },
+ {
+ name: "returns an error when the API call returns an error",
+ nlbResolveError: nil,
+ createAPICallError: errors.New("test_error"),
+ shouldError: true,
+ },
+ {
+ name: "returns correctly with no errors",
+ nlbResolveError: nil,
+ createAPICallError: nil,
+ shouldError: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ stack := core.NewDefaultStack(core.StackID{Namespace: "namespace", Name: "name"})
+ res := &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointService"),
+ Spec: ec2model.VPCEndpointServiceSpec{
+ AcceptanceRequired: awssdk.Bool(false),
+ NetworkLoadBalancerArns: []core.StringToken{
+ testStringToken{
+ value: lbArn,
+ err: tt.nlbResolveError,
+ },
+ },
+ PrivateDNSName: &privateDNSName,
+ Tags: tags,
+ },
+ }
+
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ mockEC2 := services.NewMockEC2(mockCtrl)
+ if tt.nlbResolveError == nil {
+ req := &ec2sdk.CreateVpcEndpointServiceConfigurationInput{
+ AcceptanceRequired: awssdk.Bool(false),
+ PrivateDnsName: &privateDNSName,
+ NetworkLoadBalancerArns: []*string{&lbArn},
+ TagSpecifications: []*ec2sdk.TagSpecification{
+ {
+ ResourceType: awssdk.String("vpc-endpoint-service"),
+ Tags: []*ec2sdk.Tag{
+ {
+ Key: awssdk.String("key"),
+ Value: awssdk.String("value"),
+ },
+ },
+ },
+ },
+ }
+ mockEC2.EXPECT().CreateVpcEndpointServiceConfigurationWithContext(
+ ctx,
+ gomock.Eq(req),
+ ).Return(
+ &ec2sdk.CreateVpcEndpointServiceConfigurationOutput{
+ ServiceConfiguration: &ec2sdk.ServiceConfiguration{
+ ServiceId: &serviceID,
+ },
+ },
+ tt.createAPICallError,
+ ).Times(1)
+ }
+
+ mockTaggingManager := NewMockTaggingManager(mockCtrl)
+
+ mockProvider := tracking.NewMockProvider(mockCtrl)
+
+ mockProvider.EXPECT().ResourceTags(gomock.Any(), gomock.Any(), gomock.Any()).Return(
+ map[string]string{
+ "key": "value",
+ },
+ ).AnyTimes()
+
+ manager := NewDefaultEndpointServiceManager(
+ mockEC2,
+ "vpcID",
+ logr.Discard(),
+ mockProvider,
+ mockTaggingManager,
+ []string{},
+ )
+
+ resp, err := manager.Create(ctx, res)
+
+ if tt.shouldError {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, resp.ServiceID, serviceID)
+ assert.Equal(t, resp.ServiceID, serviceID)
+ }
+ })
+ }
+}
+
+func Test_Update_responses(t *testing.T) {
+ lbArn := "lbArn"
+ privateDNSName := "http://example.com"
+ serviceID := "serviceID"
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ nlbResolveError error
+ reconcileTagsError error
+ modifyAPICallError error
+ shouldError bool
+ }{
+ {
+ name: "returns an error when the service id can't be resolved",
+ nlbResolveError: errors.New("test_error"),
+ reconcileTagsError: nil,
+ modifyAPICallError: nil,
+ shouldError: true,
+ },
+ {
+ name: "returns an error when tag reconciliation returns an error",
+ nlbResolveError: nil,
+ reconcileTagsError: errors.New("test_error"),
+ modifyAPICallError: nil,
+ shouldError: true,
+ },
+ {
+ name: "returns an error when the API call returns an error",
+ nlbResolveError: nil,
+ reconcileTagsError: nil,
+ modifyAPICallError: errors.New("test_error"),
+ shouldError: true,
+ },
+ {
+ name: "returns correctly with no errors",
+ nlbResolveError: nil,
+ reconcileTagsError: nil,
+ modifyAPICallError: nil,
+ shouldError: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ stack := core.NewDefaultStack(core.StackID{Namespace: "namespace", Name: "name"})
+ res := &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointService"),
+ Spec: ec2model.VPCEndpointServiceSpec{
+ AcceptanceRequired: awssdk.Bool(false),
+ NetworkLoadBalancerArns: []core.StringToken{
+ testStringToken{
+ value: lbArn,
+ err: tt.nlbResolveError,
+ },
+ },
+ PrivateDNSName: &privateDNSName,
+ },
+ }
+ sdk := networking.VPCEndpointServiceInfo{
+ ServiceID: serviceID,
+ }
+
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ mockTaggingManager := NewMockTaggingManager(mockCtrl)
+ if tt.nlbResolveError == nil {
+ mockTaggingManager.EXPECT().ReconcileTags(
+ ctx,
+ serviceID,
+ map[string]string{
+ "service.k8s.aws/resource": "VPCEndpointService",
+ "service.k8s.aws/stack": "namespace/name",
+ "elbv2.k8s.aws/cluster": "clusterName",
+ },
+ gomock.Any(),
+ gomock.Any(),
+ gomock.Any(),
+ ).Return(tt.reconcileTagsError).Times(1)
+ }
+
+ mockEC2 := services.NewMockEC2(mockCtrl)
+ if tt.nlbResolveError == nil && tt.reconcileTagsError == nil {
+ mockEC2.EXPECT().ModifyVpcEndpointServiceConfigurationWithContext(ctx, gomock.Any()).Return(
+ // We don't use this value
+ nil,
+ tt.modifyAPICallError,
+ ).Times(1)
+ }
+
+ manager := NewDefaultEndpointServiceManager(
+ mockEC2,
+ "vpcID",
+ logr.Discard(),
+ tracking.NewDefaultProvider("service.k8s.aws", "clusterName"),
+ mockTaggingManager,
+ []string{},
+ )
+
+ resp, err := manager.Update(ctx, res, sdk)
+
+ if tt.shouldError {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, resp.ServiceID, serviceID)
+ }
+ })
+ }
+}
+
+func Test_Update_modifyVPCEndpointServiceConfigurationInput(t *testing.T) {
+ lbArn := "lbArn"
+ privateDNSName := "http://example.com"
+ serviceID := "serviceID"
+ ctx := context.Background()
+
+ stack := core.NewDefaultStack(core.StackID{Namespace: "namespace", Name: "name"})
+ tests := []struct {
+ name string
+ res *ec2model.VPCEndpointService
+ sdk networking.VPCEndpointServiceInfo
+ req *ec2sdk.ModifyVpcEndpointServiceConfigurationInput
+ }{
+ {
+ name: "AcceptanceRequired gets set in input",
+ res: &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointService"),
+ Spec: ec2model.VPCEndpointServiceSpec{
+ AcceptanceRequired: awssdk.Bool(true),
+ },
+ },
+ sdk: networking.VPCEndpointServiceInfo{
+ AcceptanceRequired: false,
+ ServiceID: serviceID,
+ },
+ req: &ec2sdk.ModifyVpcEndpointServiceConfigurationInput{
+ AcceptanceRequired: awssdk.Bool(true),
+ AddNetworkLoadBalancerArns: nil,
+ RemoveNetworkLoadBalancerArns: nil,
+ PrivateDnsName: nil,
+ RemovePrivateDnsName: nil,
+ ServiceId: &serviceID,
+ },
+ },
+ {
+ name: "AddNetworkLoadBalancerArns gets set in input",
+ res: &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointService"),
+ Spec: ec2model.VPCEndpointServiceSpec{
+ NetworkLoadBalancerArns: []core.StringToken{
+ testStringToken{
+ value: lbArn,
+ err: nil,
+ },
+ },
+ },
+ },
+ sdk: networking.VPCEndpointServiceInfo{
+ ServiceID: serviceID,
+ },
+ req: &ec2sdk.ModifyVpcEndpointServiceConfigurationInput{
+ AcceptanceRequired: nil,
+ AddNetworkLoadBalancerArns: []*string{&lbArn},
+ RemoveNetworkLoadBalancerArns: nil,
+ PrivateDnsName: nil,
+ RemovePrivateDnsName: nil,
+ ServiceId: &serviceID,
+ },
+ },
+ {
+ name: "RemoveNetworkLoadBalancerArns gets set in input",
+ res: &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointService"),
+ Spec: ec2model.VPCEndpointServiceSpec{},
+ },
+ sdk: networking.VPCEndpointServiceInfo{
+ NetworkLoadBalancerArns: []string{lbArn},
+ ServiceID: serviceID,
+ },
+ req: &ec2sdk.ModifyVpcEndpointServiceConfigurationInput{
+ AcceptanceRequired: nil,
+ AddNetworkLoadBalancerArns: nil,
+ RemoveNetworkLoadBalancerArns: []*string{&lbArn},
+ PrivateDnsName: nil,
+ RemovePrivateDnsName: nil,
+ ServiceId: &serviceID,
+ },
+ },
+ {
+ name: "PrivateDnsName gets set in input",
+ res: &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointService"),
+ Spec: ec2model.VPCEndpointServiceSpec{
+ PrivateDNSName: &privateDNSName,
+ },
+ },
+ sdk: networking.VPCEndpointServiceInfo{
+ ServiceID: serviceID,
+ },
+ req: &ec2sdk.ModifyVpcEndpointServiceConfigurationInput{
+ AcceptanceRequired: nil,
+ AddNetworkLoadBalancerArns: nil,
+ RemoveNetworkLoadBalancerArns: nil,
+ PrivateDnsName: &privateDNSName,
+ RemovePrivateDnsName: nil,
+ ServiceId: &serviceID,
+ },
+ },
+ {
+ name: "RemovePrivateDnsName gets set in input",
+ res: &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointService"),
+ Spec: ec2model.VPCEndpointServiceSpec{},
+ },
+ sdk: networking.VPCEndpointServiceInfo{
+ PrivateDNSName: &privateDNSName,
+ ServiceID: serviceID,
+ },
+ req: &ec2sdk.ModifyVpcEndpointServiceConfigurationInput{
+ AcceptanceRequired: nil,
+ AddNetworkLoadBalancerArns: nil,
+ RemoveNetworkLoadBalancerArns: nil,
+ PrivateDnsName: nil,
+ RemovePrivateDnsName: awssdk.Bool(true),
+ ServiceId: &serviceID,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ mockEC2 := services.NewMockEC2(mockCtrl)
+ mockEC2.EXPECT().ModifyVpcEndpointServiceConfigurationWithContext(ctx, gomock.Eq(tt.req)).Return(
+ // We don't use this value
+ nil,
+ nil,
+ ).Times(1)
+
+ mockTaggingManager := NewMockTaggingManager(mockCtrl)
+ mockTaggingManager.EXPECT().ReconcileTags(
+ ctx,
+ serviceID,
+ map[string]string{
+ "service.k8s.aws/resource": "VPCEndpointService",
+ "service.k8s.aws/stack": "namespace/name",
+ "elbv2.k8s.aws/cluster": "clusterName",
+ },
+ gomock.Any(),
+ gomock.Any(),
+ gomock.Any(),
+ ).Return(nil).Times(1)
+
+ manager := NewDefaultEndpointServiceManager(
+ mockEC2,
+ "vpcID",
+ logr.Discard(),
+ tracking.NewDefaultProvider("service.k8s.aws", "clusterName"),
+ mockTaggingManager,
+ []string{},
+ )
+
+ resp, err := manager.Update(ctx, tt.res, tt.sdk)
+
+ assert.NoError(t, err)
+ assert.Equal(t, resp.ServiceID, serviceID)
+ })
+ }
+}
+
+func Test_Delete(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ serviceID := "serviceID"
+ sdkES := networking.VPCEndpointServiceInfo{
+ ServiceID: serviceID,
+ }
+
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ deleteResponseError error
+ waitESDeletionPollInterval time.Duration
+ waitESDeletionTimeout time.Duration
+ }{
+ {
+ name: "calls delete with expected arguments",
+ deleteResponseError: nil,
+ },
+ {
+ name: "returns an error if the delete call returns an error",
+ deleteResponseError: errors.New("test_error"),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockEC2 := services.NewMockEC2(mockCtrl)
+ mockTaggingManager := NewMockTaggingManager(mockCtrl)
+ manager := NewDefaultEndpointServiceManager(
+ mockEC2,
+ "vpcID",
+ logr.Discard(),
+ tracking.NewDefaultProvider("", ""),
+ mockTaggingManager,
+ []string{},
+ )
+ req := &ec2sdk.DeleteVpcEndpointServiceConfigurationsInput{
+ ServiceIds: awssdk.StringSlice(
+ []string{serviceID},
+ ),
+ }
+
+ mockEC2.EXPECT().DeleteVpcEndpointServiceConfigurationsWithContext(ctx, gomock.Eq(req)).Return(
+ // We never use this return value
+ nil,
+ tt.deleteResponseError,
+ ).Times(1)
+
+ err := manager.Delete(ctx, sdkES)
+
+ if tt.deleteResponseError != nil {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func Test_ReconcilePermissions(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ principleName := "principle"
+ serviceID := "serviceID"
+
+ describeVpcEndpointServicePermissionsWithContextReq := &ec2sdk.DescribeVpcEndpointServicePermissionsInput{
+ ServiceId: &serviceID,
+ }
+
+ ctx := context.Background()
+ tests := []struct {
+ name string
+ desiredAllowedPrincipals []string
+ describePermissionsResponse DescribeVpcEndpointServicePermissionsWithContextResponse
+ ModifyVpcEndpointServicePermissionsWithContextRequest *ec2sdk.ModifyVpcEndpointServicePermissionsInput
+ ModifyVpcEndpointServicePermissionsWithContextError error
+ expectError bool
+ }{
+ {
+ name: "returns an error when describe permissions AWS call returns an error",
+ desiredAllowedPrincipals: []string{},
+ describePermissionsResponse: DescribeVpcEndpointServicePermissionsWithContextResponse{
+ response: &ec2sdk.DescribeVpcEndpointServicePermissionsOutput{},
+ err: errors.New("test_error"),
+ },
+ ModifyVpcEndpointServicePermissionsWithContextRequest: nil,
+ ModifyVpcEndpointServicePermissionsWithContextError: nil,
+ expectError: true,
+ },
+ {
+ name: "does not call update when there are no principals to be changed",
+ desiredAllowedPrincipals: []string{principleName},
+ describePermissionsResponse: DescribeVpcEndpointServicePermissionsWithContextResponse{
+ response: &ec2sdk.DescribeVpcEndpointServicePermissionsOutput{
+ AllowedPrincipals: []*ec2sdk.AllowedPrincipal{
+ {
+ Principal: &principleName,
+ },
+ },
+ },
+ err: nil,
+ },
+ ModifyVpcEndpointServicePermissionsWithContextRequest: nil,
+ ModifyVpcEndpointServicePermissionsWithContextError: nil,
+ expectError: false,
+ },
+ {
+ name: "returns and error when update call returns an error",
+ desiredAllowedPrincipals: []string{principleName},
+ describePermissionsResponse: DescribeVpcEndpointServicePermissionsWithContextResponse{
+ response: &ec2sdk.DescribeVpcEndpointServicePermissionsOutput{
+ AllowedPrincipals: []*ec2sdk.AllowedPrincipal{},
+ },
+ err: nil,
+ },
+ ModifyVpcEndpointServicePermissionsWithContextRequest: &ec2sdk.ModifyVpcEndpointServicePermissionsInput{
+ AddAllowedPrincipals: []*string{&principleName},
+ RemoveAllowedPrincipals: nil,
+ ServiceId: &serviceID,
+ },
+ ModifyVpcEndpointServicePermissionsWithContextError: errors.New("test_error"),
+ expectError: true,
+ },
+ {
+ name: "calls update when a principle need to be added",
+ desiredAllowedPrincipals: []string{principleName},
+ describePermissionsResponse: DescribeVpcEndpointServicePermissionsWithContextResponse{
+ response: &ec2sdk.DescribeVpcEndpointServicePermissionsOutput{
+ AllowedPrincipals: []*ec2sdk.AllowedPrincipal{},
+ },
+ err: nil,
+ },
+ ModifyVpcEndpointServicePermissionsWithContextRequest: &ec2sdk.ModifyVpcEndpointServicePermissionsInput{
+ AddAllowedPrincipals: []*string{&principleName},
+ RemoveAllowedPrincipals: nil,
+ ServiceId: &serviceID,
+ },
+ ModifyVpcEndpointServicePermissionsWithContextError: nil,
+ expectError: false,
+ },
+ {
+ name: "calls update when a principle need to be removed",
+ desiredAllowedPrincipals: []string{},
+ describePermissionsResponse: DescribeVpcEndpointServicePermissionsWithContextResponse{
+ response: &ec2sdk.DescribeVpcEndpointServicePermissionsOutput{
+ AllowedPrincipals: []*ec2sdk.AllowedPrincipal{
+ {
+ Principal: &principleName,
+ },
+ },
+ },
+ err: nil,
+ },
+ ModifyVpcEndpointServicePermissionsWithContextRequest: &ec2sdk.ModifyVpcEndpointServicePermissionsInput{
+ AddAllowedPrincipals: nil,
+ RemoveAllowedPrincipals: []*string{&principleName},
+ ServiceId: &serviceID,
+ },
+ ModifyVpcEndpointServicePermissionsWithContextError: nil,
+ expectError: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockEC2 := services.NewMockEC2(mockCtrl)
+ mockTaggingManager := NewMockTaggingManager(mockCtrl)
+ manager := NewDefaultEndpointServiceManager(
+ mockEC2,
+ "vpcID",
+ logr.Discard(),
+ tracking.NewDefaultProvider("", ""),
+ mockTaggingManager,
+ []string{},
+ )
+
+ permissions := &ec2.VPCEndpointServicePermissions{
+ Spec: ec2.VPCEndpointServicePermissionsSpec{
+ AllowedPrincipals: tt.desiredAllowedPrincipals,
+ ServiceId: testStringToken{
+ value: serviceID,
+ },
+ },
+ }
+
+ // Set up mocks
+ mockEC2.EXPECT().DescribeVpcEndpointServicePermissionsWithContext(ctx, gomock.Eq(describeVpcEndpointServicePermissionsWithContextReq)).Return(
+ tt.describePermissionsResponse.response,
+ tt.describePermissionsResponse.err,
+ ).Times(1)
+ if tt.ModifyVpcEndpointServicePermissionsWithContextRequest != nil {
+ mockEC2.EXPECT().ModifyVpcEndpointServicePermissionsWithContext(
+ ctx,
+ gomock.Eq(tt.ModifyVpcEndpointServicePermissionsWithContextRequest),
+ ).Return(
+ // We never use this response value
+ nil,
+ tt.ModifyVpcEndpointServicePermissionsWithContextError,
+ ).Times(1)
+ } else {
+ mockEC2.EXPECT().ModifyVpcEndpointServicePermissionsWithContext(gomock.Any(), gomock.Any()).Times(0)
+ }
+
+ err := manager.ReconcilePermissions(ctx, permissions)
+ if tt.expectError {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func Test_fetchESPermissionInfosFromAWS(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ ctx := context.Background()
+ pricipalNames := []string{"principle1", "principle2"}
+ req := &ec2sdk.DescribeVpcEndpointServicePermissionsInput{}
+
+ tests := []struct {
+ name string
+ mockResponse DescribeVpcEndpointServicePermissionsWithContextResponse
+ expected networking.VPCEndpointServicePermissionsInfo
+ err bool
+ }{
+ {
+ name: "returns valid output on valid request",
+ mockResponse: DescribeVpcEndpointServicePermissionsWithContextResponse{
+ response: &ec2sdk.DescribeVpcEndpointServicePermissionsOutput{
+ AllowedPrincipals: []*ec2sdk.AllowedPrincipal{
+ {Principal: &pricipalNames[0]},
+ {Principal: &pricipalNames[1]},
+ },
+ },
+ err: nil,
+ },
+ expected: networking.VPCEndpointServicePermissionsInfo{
+ AllowedPrincipals: pricipalNames,
+ ServiceId: "",
+ },
+ err: false,
+ },
+ {
+ name: "returns an error on an SDK error",
+ mockResponse: DescribeVpcEndpointServicePermissionsWithContextResponse{
+ response: &ec2sdk.DescribeVpcEndpointServicePermissionsOutput{},
+ err: errors.New("test_error"),
+ },
+ expected: networking.VPCEndpointServicePermissionsInfo{},
+ err: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockEC2 := services.NewMockEC2(mockCtrl)
+ mockTaggingManager := NewMockTaggingManager(mockCtrl)
+ manager := NewDefaultEndpointServiceManager(
+ mockEC2,
+ "vpcID",
+ logr.Discard(),
+ tracking.NewDefaultProvider("", ""),
+ mockTaggingManager,
+ []string{},
+ )
+ mockEC2.EXPECT().DescribeVpcEndpointServicePermissionsWithContext(ctx, req).Return(
+ tt.mockResponse.response,
+ tt.mockResponse.err,
+ ).Times(1)
+ actual, err := manager.fetchESPermissionInfosFromAWS(ctx, req)
+ assert.Equal(t, tt.expected, actual)
+ if tt.err {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func Test_DeleteWithFailure(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ serviceID := "serviceID"
+ sdkES := networking.VPCEndpointServiceInfo{
+ ServiceID: serviceID,
+ }
+
+ ctx := context.Background()
+
+ resourceId := "1"
+ errorCode := "error code"
+ errorMessage := "error message"
+
+ tests := []struct {
+ name string
+ deleteResponse *ec2sdk.DeleteVpcEndpointServiceConfigurationsOutput
+ waitESDeletionPollInterval time.Duration
+ waitESDeletionTimeout time.Duration
+ }{
+ {
+ name: "delete response is nil",
+ deleteResponse: nil,
+ },
+ {
+ name: "delete response contains no errors",
+ deleteResponse: &ec2sdk.DeleteVpcEndpointServiceConfigurationsOutput{
+ Unsuccessful: []*ec2sdk.UnsuccessfulItem{},
+ },
+ },
+ {
+ name: "delete response contains one error",
+ deleteResponse: &ec2sdk.DeleteVpcEndpointServiceConfigurationsOutput{
+ Unsuccessful: []*ec2sdk.UnsuccessfulItem{
+ {
+ ResourceId: &resourceId,
+ Error: &ec2sdk.UnsuccessfulItemError{
+ Code: &errorCode,
+ Message: &errorMessage,
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockEC2 := services.NewMockEC2(mockCtrl)
+ mockTaggingManager := NewMockTaggingManager(mockCtrl)
+ manager := NewDefaultEndpointServiceManager(
+ mockEC2,
+ "vpcID",
+ logr.Discard(),
+ tracking.NewDefaultProvider("", ""),
+ mockTaggingManager,
+ []string{},
+ )
+ req := &ec2sdk.DeleteVpcEndpointServiceConfigurationsInput{
+ ServiceIds: awssdk.StringSlice(
+ []string{serviceID},
+ ),
+ }
+
+ mockEC2.EXPECT().DeleteVpcEndpointServiceConfigurationsWithContext(ctx, gomock.Eq(req)).Return(
+ // We never use this return value
+ tt.deleteResponse,
+ nil,
+ ).Times(1)
+
+ err := manager.Delete(ctx, sdkES)
+
+ if tt.deleteResponse != nil && len(tt.deleteResponse.Unsuccessful) > 0 {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
diff --git a/pkg/deploy/ec2/endpoint_service_synthesizer.go b/pkg/deploy/ec2/endpoint_service_synthesizer.go
new file mode 100644
index 0000000000..4eb76b8d47
--- /dev/null
+++ b/pkg/deploy/ec2/endpoint_service_synthesizer.go
@@ -0,0 +1,204 @@
+package ec2
+
+import (
+ "context"
+
+ "github.com/go-logr/logr"
+ "github.com/pkg/errors"
+ "k8s.io/apimachinery/pkg/util/sets"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/aws/services"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/tracking"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"
+ ec2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/ec2"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/networking"
+)
+
+// NewEndpointServiceSynthesizer constructs new endpointServiceSynthesizer.
+func NewEndpointServiceSynthesizer(ec2Client services.EC2, trackingProvider tracking.Provider, taggingManager TaggingManager,
+ esManager EndpointServiceManager, vpcID string, logger logr.Logger, stack core.Stack) *endpointServiceSynthesizer {
+ return &endpointServiceSynthesizer{
+ ec2Client: ec2Client,
+ trackingProvider: trackingProvider,
+ taggingManager: taggingManager,
+ esManager: esManager,
+ vpcID: vpcID,
+ logger: logger,
+ stack: stack,
+ }
+}
+
+type endpointServiceSynthesizer struct {
+ ec2Client services.EC2
+ trackingProvider tracking.Provider
+ taggingManager TaggingManager
+ esManager EndpointServiceManager
+ vpcID string
+ logger logr.Logger
+
+ stack core.Stack
+ unmatchedSDKESs []networking.VPCEndpointServiceInfo
+}
+
+func (s *endpointServiceSynthesizer) Synthesize(ctx context.Context) error {
+ // The load balancer synthesizer creates and deletes in its synthesize
+ // loop. We need to make sure that we delete any VPC endpoint services
+ // before a load balancer deletion is attempted so we also need to delete
+ // in our synthesize loop and we need to make sure that our synthesize
+ // loop is called before the load balancers.
+ var resESs []*ec2model.VPCEndpointService
+ s.stack.ListResources(&resESs)
+ sdkESs, err := s.findSDKEndpointServices(ctx)
+ if err != nil {
+ return err
+ }
+
+ _, _, unmatchedSDKESs, err := matchResAndSDKEndpointServices(resESs, sdkESs, s.trackingProvider.ResourceIDTagKey())
+ if err != nil {
+ return err
+ }
+
+ // We delete before we create as we can only have a single VPC end point per LB
+ for _, sdkES := range unmatchedSDKESs {
+ if err := s.esManager.Delete(ctx, sdkES); err != nil {
+ return errors.Wrap(err, "failed to delete VPCEndpointService")
+ }
+ }
+
+ return nil
+}
+
+func (s *endpointServiceSynthesizer) PostSynthesize(ctx context.Context) error {
+ // We need the load balancer to be created before we attempt to create
+ // our VPC endpoint services. The load balancer synthesizer creates
+ // load balancers in its synthesize loop so we can safely create in ours
+ // in our post synthesize loop.
+ // We can't create in our synthesize loop as we must synthesize before the
+ // load balancer synthesizer.
+
+ var resESs []*ec2model.VPCEndpointService
+ s.stack.ListResources(&resESs)
+ sdkESs, err := s.findSDKEndpointServices(ctx)
+ if err != nil {
+ return err
+ }
+
+ matchedResAndSDKESs, unmatchedResESs, _, err := matchResAndSDKEndpointServices(resESs, sdkESs, s.trackingProvider.ResourceIDTagKey())
+ if err != nil {
+ return err
+ }
+
+ for _, resES := range unmatchedResESs {
+ esStatus, err := s.esManager.Create(ctx, resES)
+ if err != nil {
+ return errors.Wrap(err, "failed to create VPCEndpointService")
+ }
+ resES.SetStatus(esStatus)
+ }
+
+ for _, pair := range matchedResAndSDKESs {
+ esStatus, err := s.esManager.Update(ctx, pair.res, pair.sdk)
+ if err != nil {
+ return errors.Wrap(err, "failed to update VPCEndpointService")
+ }
+ pair.res.SetStatus(esStatus)
+ }
+
+ var resESPs []*ec2model.VPCEndpointServicePermissions
+ err = s.stack.ListResources(&resESPs)
+ if err != nil {
+ return err
+ }
+ s.logger.Info("Permission to reconcile", "permission", resESPs)
+ for _, permission := range resESPs {
+ err = s.esManager.ReconcilePermissions(ctx, permission)
+ if err != nil {
+ return errors.Wrap(err, "failed to reconcile VPCEndpointServicePermissions")
+ }
+ }
+
+ return nil
+}
+
+func (s *endpointServiceSynthesizer) findSDKEndpointServices(ctx context.Context) ([]networking.VPCEndpointServiceInfo, error) {
+ stackTags := s.trackingProvider.StackTags(s.stack)
+ stackTagsLegacy := s.trackingProvider.StackTagsLegacy(s.stack)
+
+ return s.taggingManager.ListVPCEndpointServices(ctx,
+ tracking.TagsAsTagFilter(stackTags),
+ tracking.TagsAsTagFilter(stackTagsLegacy),
+ )
+}
+
+type resAndSDKEndpointServicePair struct {
+ res *ec2model.VPCEndpointService
+ sdk networking.VPCEndpointServiceInfo
+}
+
+func matchResAndSDKEndpointServices(resESs []*ec2model.VPCEndpointService, sdkESs []networking.VPCEndpointServiceInfo,
+ resourceIDTagKey string) ([]resAndSDKEndpointServicePair, []*ec2model.VPCEndpointService, []networking.VPCEndpointServiceInfo, error) {
+
+ var matchedResAndSDKESs []resAndSDKEndpointServicePair
+
+ var unmatchedResESs []*ec2model.VPCEndpointService
+
+ var unmatchedSDKESs []networking.VPCEndpointServiceInfo
+
+ resESsByID := mapResEndpointServiceByResourceID(resESs)
+
+ sdkESsByID, err := mapSDKEndpointServiceByResourceID(sdkESs, resourceIDTagKey)
+ if err != nil {
+ return nil, nil, nil, errors.Wrap(err, "failed to map VPCEndpointServices by ID")
+ }
+
+ resESIDs := sets.StringKeySet(resESsByID)
+ sdkESIDs := sets.StringKeySet(sdkESsByID)
+
+ for _, resID := range resESIDs.Intersection(sdkESIDs).List() {
+ resES := resESsByID[resID]
+ sdkESs := sdkESsByID[resID]
+
+ matchedResAndSDKESs = append(matchedResAndSDKESs, resAndSDKEndpointServicePair{
+ res: resES,
+ sdk: sdkESs[0],
+ })
+
+ for _, sdkES := range sdkESs[1:] {
+ unmatchedSDKESs = append(unmatchedSDKESs, sdkES)
+ }
+ }
+
+ for _, resID := range resESIDs.Difference(sdkESIDs).List() {
+ unmatchedResESs = append(unmatchedResESs, resESsByID[resID])
+ }
+
+ for _, resID := range sdkESIDs.Difference(resESIDs).List() {
+ unmatchedSDKESs = append(unmatchedSDKESs, sdkESsByID[resID]...)
+ }
+
+ return matchedResAndSDKESs, unmatchedResESs, unmatchedSDKESs, nil
+}
+
+func mapResEndpointServiceByResourceID(resESs []*ec2model.VPCEndpointService) map[string]*ec2model.VPCEndpointService {
+ resESsByID := make(map[string]*ec2model.VPCEndpointService, len(resESs))
+ for _, resES := range resESs {
+ resESsByID[resES.ID()] = resES
+ }
+
+ return resESsByID
+}
+
+func mapSDKEndpointServiceByResourceID(sdkESs []networking.VPCEndpointServiceInfo,
+ resourceIDTagKey string) (map[string][]networking.VPCEndpointServiceInfo, error) {
+ sdkESsByID := make(map[string][]networking.VPCEndpointServiceInfo, len(sdkESs))
+
+ for _, sdkES := range sdkESs {
+ resourceID, ok := sdkES.Tags[resourceIDTagKey]
+ if !ok {
+ return nil, errors.Errorf("unexpected VPCEndpointService with no resourceID: %v", sdkES.ServiceID)
+ }
+
+ sdkESsByID[resourceID] = append(sdkESsByID[resourceID], sdkES)
+ }
+
+ return sdkESsByID, nil
+}
diff --git a/pkg/deploy/ec2/endpoint_service_synthesizer_test.go b/pkg/deploy/ec2/endpoint_service_synthesizer_test.go
new file mode 100644
index 0000000000..d49aa927e6
--- /dev/null
+++ b/pkg/deploy/ec2/endpoint_service_synthesizer_test.go
@@ -0,0 +1,316 @@
+package ec2
+
+import (
+ "context"
+ "testing"
+
+ "github.com/go-logr/logr"
+ "github.com/golang/mock/gomock"
+ "github.com/pkg/errors"
+ "github.com/stretchr/testify/assert"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/aws/services"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/tracking"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"
+ ec2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/ec2"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/networking"
+)
+
+func Test_synthesize_happyPath(t *testing.T) {
+ serviceID := "serviceID"
+ t.Parallel()
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ sdkVPCESEnabled bool
+ resVPCESEnabled bool
+ createCalls int
+ deleteCalls int
+ updateCalls int
+ callSynthesize bool
+ callPostSynthesize bool
+ }{
+ {
+ name: "create with synthesize",
+ sdkVPCESEnabled: false,
+ resVPCESEnabled: true,
+ createCalls: 0,
+ deleteCalls: 0,
+ updateCalls: 0,
+ callSynthesize: true,
+ callPostSynthesize: false,
+ },
+ {
+ name: "delete with synthesize",
+ sdkVPCESEnabled: true,
+ resVPCESEnabled: false,
+ createCalls: 0,
+ deleteCalls: 1,
+ updateCalls: 0,
+ callSynthesize: true,
+ callPostSynthesize: false,
+ },
+ {
+ name: "update with synthesize",
+ sdkVPCESEnabled: true,
+ resVPCESEnabled: true,
+ createCalls: 0,
+ deleteCalls: 0,
+ updateCalls: 0,
+ callSynthesize: true,
+ callPostSynthesize: false,
+ },
+ {
+ name: "create with post synthesize",
+ sdkVPCESEnabled: false,
+ resVPCESEnabled: true,
+ createCalls: 1,
+ deleteCalls: 0,
+ updateCalls: 0,
+ callSynthesize: false,
+ callPostSynthesize: true,
+ },
+ {
+ name: "delete with post synthesize",
+ sdkVPCESEnabled: true,
+ resVPCESEnabled: false,
+ createCalls: 0,
+ deleteCalls: 0,
+ updateCalls: 0,
+ callSynthesize: false,
+ callPostSynthesize: true,
+ },
+ {
+ name: "update with post synthesize",
+ sdkVPCESEnabled: true,
+ resVPCESEnabled: true,
+ createCalls: 0,
+ deleteCalls: 0,
+ updateCalls: 1,
+ callSynthesize: false,
+ callPostSynthesize: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockEC2 := services.NewMockEC2(mockCtrl)
+ mockTaggingManager := NewMockTaggingManager(mockCtrl)
+ mockEndpointServiceManager := NewMockEndpointServiceManager(mockCtrl)
+
+ stack := core.NewDefaultStack(core.StackID{})
+
+ var resVPCES *ec2model.VPCEndpointService
+ if tt.resVPCESEnabled {
+ resVPCES = &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointService"),
+ }
+ err := stack.AddResource(resVPCES)
+ assert.NoError(t, err)
+ }
+
+ sdkVPCES := []networking.VPCEndpointServiceInfo{}
+ vpcesInfo := networking.VPCEndpointServiceInfo{
+ ServiceID: serviceID,
+ Tags: map[string]string{
+ "prefix/resource": "VPCEndpointService",
+ },
+ }
+ if tt.sdkVPCESEnabled {
+ sdkVPCES = []networking.VPCEndpointServiceInfo{vpcesInfo}
+ }
+
+ synthesizer := NewEndpointServiceSynthesizer(
+ mockEC2,
+ tracking.NewDefaultProvider("prefix", "clusterName"),
+ mockTaggingManager,
+ mockEndpointServiceManager,
+ "vpcIP",
+ logr.Discard(),
+ stack,
+ )
+
+ mockTaggingManager.EXPECT().ListVPCEndpointServices(ctx, gomock.Any(), gomock.Any()).Return(sdkVPCES, nil)
+ if tt.resVPCESEnabled && !tt.sdkVPCESEnabled && tt.callPostSynthesize {
+ mockEndpointServiceManager.EXPECT().Create(ctx, resVPCES).Times(tt.createCalls)
+ } else {
+ mockEndpointServiceManager.EXPECT().Create(ctx, gomock.Any()).Times(tt.createCalls)
+ }
+ if !tt.resVPCESEnabled && tt.sdkVPCESEnabled && tt.callSynthesize {
+ mockEndpointServiceManager.EXPECT().Delete(ctx, vpcesInfo).Times(tt.deleteCalls)
+ } else {
+ mockEndpointServiceManager.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(tt.deleteCalls)
+ }
+ if tt.resVPCESEnabled && tt.sdkVPCESEnabled && tt.callSynthesize {
+ mockEndpointServiceManager.EXPECT().Update(ctx, resVPCES, vpcesInfo).Times(tt.updateCalls)
+ } else {
+ mockEndpointServiceManager.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).Times(tt.updateCalls)
+ }
+
+ if tt.callSynthesize {
+ err := synthesizer.Synthesize(ctx)
+ assert.NoError(t, err)
+ }
+
+ if tt.callPostSynthesize {
+ err := synthesizer.PostSynthesize(ctx)
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func Test_Synthesize_errorPath(t *testing.T) {
+ t.Parallel()
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ ctx := context.Background()
+
+ mockEC2 := services.NewMockEC2(mockCtrl)
+ mockTaggingManager := NewMockTaggingManager(mockCtrl)
+ mockEndpointServiceManager := NewMockEndpointServiceManager(mockCtrl)
+
+ stack := core.NewDefaultStack(core.StackID{})
+ updateVPCES := &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointServiceUpdate"),
+ }
+ createVPCES := &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointServiceCreate"),
+ }
+ permissions := &ec2model.VPCEndpointServicePermissions{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointServicePermissions"),
+ }
+ _ = stack.AddResource(updateVPCES)
+ _ = stack.AddResource(createVPCES)
+ _ = stack.AddResource(permissions)
+
+ updateVPCESInfo := networking.VPCEndpointServiceInfo{
+ AcceptanceRequired: true,
+ ServiceID: "serviceID",
+ Tags: map[string]string{
+ "prefix/resource": "VPCEndpointServiceUpdate",
+ },
+ }
+ deleteVPCESInfo := networking.VPCEndpointServiceInfo{
+ AcceptanceRequired: true,
+ ServiceID: "serviceID",
+ Tags: map[string]string{
+ "prefix/resource": "VPCEndpointServiceDelete",
+ },
+ }
+ sdkVPCES := []networking.VPCEndpointServiceInfo{updateVPCESInfo, deleteVPCESInfo}
+
+ synthesizer := NewEndpointServiceSynthesizer(
+ mockEC2,
+ tracking.NewDefaultProvider("prefix", "clusterName"),
+ mockTaggingManager,
+ mockEndpointServiceManager,
+ "vpcIP",
+ logr.Discard(),
+ stack,
+ )
+
+ mockTaggingManager.EXPECT().ListVPCEndpointServices(ctx, gomock.Any(), gomock.Any()).Return(sdkVPCES, nil)
+
+ mockEndpointServiceManager.EXPECT().Delete(ctx, deleteVPCESInfo).Return(errors.New("test_error")).Times(1)
+
+ err := synthesizer.Synthesize(ctx)
+ assert.Error(t, err)
+}
+
+func Test_postSynthesize_errorPath(t *testing.T) {
+ t.Parallel()
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ createError error
+ updateError error
+ reconcilePermissionsError error
+ }{
+ {
+ name: "create endpoint returns an error",
+ createError: errors.New("test_error"),
+ updateError: nil,
+ reconcilePermissionsError: nil,
+ },
+ {
+ name: "update endpoint returns an error",
+ createError: nil,
+ updateError: errors.New("test_error"),
+ reconcilePermissionsError: nil,
+ },
+ {
+ name: "reconcile endpoint returns an error",
+ createError: nil,
+ updateError: nil,
+ reconcilePermissionsError: errors.New("test_error"),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockEC2 := services.NewMockEC2(mockCtrl)
+ mockTaggingManager := NewMockTaggingManager(mockCtrl)
+ mockEndpointServiceManager := NewMockEndpointServiceManager(mockCtrl)
+
+ stack := core.NewDefaultStack(core.StackID{})
+ updateVPCES := &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointServiceUpdate"),
+ }
+ createVPCES := &ec2model.VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointServiceCreate"),
+ }
+ permissions := &ec2model.VPCEndpointServicePermissions{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", "VPCEndpointServicePermissions"),
+ }
+ _ = stack.AddResource(updateVPCES)
+ _ = stack.AddResource(createVPCES)
+ _ = stack.AddResource(permissions)
+
+ updateVPCESInfo := networking.VPCEndpointServiceInfo{
+ AcceptanceRequired: true,
+ ServiceID: "serviceID",
+ Tags: map[string]string{
+ "prefix/resource": "VPCEndpointServiceUpdate",
+ },
+ }
+ sdkVPCES := []networking.VPCEndpointServiceInfo{updateVPCESInfo}
+
+ synthesizer := NewEndpointServiceSynthesizer(
+ mockEC2,
+ tracking.NewDefaultProvider("prefix", "clusterName"),
+ mockTaggingManager,
+ mockEndpointServiceManager,
+ "vpcIP",
+ logr.Discard(),
+ stack,
+ )
+
+ mockTaggingManager.EXPECT().ListVPCEndpointServices(ctx, gomock.Any(), gomock.Any()).Return(sdkVPCES, nil)
+
+ endpointStatus := ec2model.VPCEndpointServiceStatus{ServiceID: "serviceID"}
+ mockEndpointServiceManager.EXPECT().Create(ctx, createVPCES).Return(endpointStatus, tt.createError).Times(1)
+ if tt.createError == nil {
+ mockEndpointServiceManager.EXPECT().Update(ctx, updateVPCES, updateVPCESInfo).Return(endpointStatus, tt.updateError).Times(1)
+ } else {
+ mockEndpointServiceManager.EXPECT().Update(ctx, gomock.Any(), gomock.Any()).Times(0)
+ }
+ if tt.createError == nil && tt.updateError == nil {
+ mockEndpointServiceManager.EXPECT().ReconcilePermissions(ctx, permissions).Return(tt.reconcilePermissionsError).Times(1)
+ } else {
+ mockEndpointServiceManager.EXPECT().ReconcilePermissions(ctx, gomock.Any()).Times(0)
+ }
+
+ err := synthesizer.PostSynthesize(ctx)
+ assert.Error(t, err)
+ })
+ }
+}
diff --git a/pkg/deploy/ec2/tagging_manager.go b/pkg/deploy/ec2/tagging_manager.go
index e972c2029b..322d8bf772 100644
--- a/pkg/deploy/ec2/tagging_manager.go
+++ b/pkg/deploy/ec2/tagging_manager.go
@@ -54,15 +54,25 @@ type TaggingManager interface {
// ListSecurityGroups returns SecurityGroups that matches any of the tagging requirements.
ListSecurityGroups(ctx context.Context, tagFilters ...tracking.TagFilter) ([]networking.SecurityGroupInfo, error)
+
+ // ListVPCEndpointServices returns VPCEndpointServices that match any of the tagging requirements.
+ ListVPCEndpointServices(ctx context.Context, tagFilters ...tracking.TagFilter) ([]networking.VPCEndpointServiceInfo, error)
}
// NewDefaultTaggingManager constructs new defaultTaggingManager.
-func NewDefaultTaggingManager(ec2Client services.EC2, networkingSGManager networking.SecurityGroupManager, vpcID string, logger logr.Logger) *defaultTaggingManager {
+func NewDefaultTaggingManager(
+ ec2Client services.EC2,
+ networkingSGManager networking.SecurityGroupManager,
+ vpcEndpointServiceManager networking.VPCEndpointServiceManager,
+ vpcID string,
+ logger logr.Logger,
+) *defaultTaggingManager {
return &defaultTaggingManager{
- ec2Client: ec2Client,
- networkingSGManager: networkingSGManager,
- vpcID: vpcID,
- logger: logger,
+ ec2Client: ec2Client,
+ networkingSGManager: networkingSGManager,
+ vpcEndpointServiceManager: vpcEndpointServiceManager,
+ vpcID: vpcID,
+ logger: logger,
}
}
@@ -70,10 +80,11 @@ var _ TaggingManager = &defaultTaggingManager{}
// default implementation for TaggingManager.
type defaultTaggingManager struct {
- ec2Client services.EC2
- networkingSGManager networking.SecurityGroupManager
- vpcID string
- logger logr.Logger
+ ec2Client services.EC2
+ networkingSGManager networking.SecurityGroupManager
+ vpcEndpointServiceManager networking.VPCEndpointServiceManager
+ vpcID string
+ logger logr.Logger
}
func (m *defaultTaggingManager) ReconcileTags(ctx context.Context, resID string, desiredTags map[string]string, opts ...ReconcileTagsOption) error {
@@ -175,6 +186,48 @@ func (m *defaultTaggingManager) listSecurityGroupsWithTagFilter(ctx context.Cont
return m.networkingSGManager.FetchSGInfosByRequest(ctx, req)
}
+func (m *defaultTaggingManager) ListVPCEndpointServices(ctx context.Context, tagFilters ...tracking.TagFilter) ([]networking.VPCEndpointServiceInfo, error) {
+ esInfoByID := make(map[string]networking.VPCEndpointServiceInfo)
+ for _, tagFilter := range tagFilters {
+ esInfoByIDForTagFilter, err := m.listVPCEndpointServicesWithTagFilter(ctx, tagFilter)
+ if err != nil {
+ return nil, err
+ }
+ for esID, esInfo := range esInfoByIDForTagFilter {
+ esInfoByID[esID] = esInfo
+ }
+ }
+
+ esInfos := make([]networking.VPCEndpointServiceInfo, 0, len(esInfoByID))
+ for _, esInfo := range esInfoByID {
+ esInfos = append(esInfos, esInfo)
+ }
+ return esInfos, nil
+}
+
+func (m *defaultTaggingManager) listVPCEndpointServicesWithTagFilter(ctx context.Context, tagFilter tracking.TagFilter) (map[string]networking.VPCEndpointServiceInfo, error) {
+ req := &ec2sdk.DescribeVpcEndpointServiceConfigurationsInput{
+ Filters: []*ec2sdk.Filter{},
+ }
+
+ for _, tagKey := range sets.StringKeySet(tagFilter).List() {
+ tagValues := tagFilter[tagKey]
+ var filter ec2sdk.Filter
+ if len(tagValues) == 0 {
+ tagFilterName := "tag-key"
+ filter.Name = awssdk.String(tagFilterName)
+ filter.Values = awssdk.StringSlice([]string{tagKey})
+ } else {
+ tagFilterName := fmt.Sprintf("tag:%v", tagKey)
+ filter.Name = awssdk.String(tagFilterName)
+ filter.Values = awssdk.StringSlice(tagValues)
+ }
+ req.Filters = append(req.Filters, &filter)
+ }
+
+ return m.vpcEndpointServiceManager.FetchVPCESInfosByRequest(ctx, req)
+}
+
// convert tags into AWS SDK tag presentation.
func convertTagsToSDKTags(tags map[string]string) []*ec2sdk.Tag {
if len(tags) == 0 {
diff --git a/pkg/deploy/ec2/tagging_manager__mocks.go b/pkg/deploy/ec2/tagging_manager__mocks.go
new file mode 100644
index 0000000000..dc560b6dfb
--- /dev/null
+++ b/pkg/deploy/ec2/tagging_manager__mocks.go
@@ -0,0 +1,96 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/ec2 (interfaces: TaggingManager)
+
+// Package ec2 is a generated GoMock package.
+package ec2
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ tracking "sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/tracking"
+ networking "sigs.k8s.io/aws-load-balancer-controller/pkg/networking"
+)
+
+// MockTaggingManager is a mock of TaggingManager interface.
+type MockTaggingManager struct {
+ ctrl *gomock.Controller
+ recorder *MockTaggingManagerMockRecorder
+}
+
+// MockTaggingManagerMockRecorder is the mock recorder for MockTaggingManager.
+type MockTaggingManagerMockRecorder struct {
+ mock *MockTaggingManager
+}
+
+// NewMockTaggingManager creates a new mock instance.
+func NewMockTaggingManager(ctrl *gomock.Controller) *MockTaggingManager {
+ mock := &MockTaggingManager{ctrl: ctrl}
+ mock.recorder = &MockTaggingManagerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockTaggingManager) EXPECT() *MockTaggingManagerMockRecorder {
+ return m.recorder
+}
+
+// ListSecurityGroups mocks base method.
+func (m *MockTaggingManager) ListSecurityGroups(arg0 context.Context, arg1 ...tracking.TagFilter) ([]networking.SecurityGroupInfo, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ListSecurityGroups", varargs...)
+ ret0, _ := ret[0].([]networking.SecurityGroupInfo)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListSecurityGroups indicates an expected call of ListSecurityGroups.
+func (mr *MockTaggingManagerMockRecorder) ListSecurityGroups(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecurityGroups", reflect.TypeOf((*MockTaggingManager)(nil).ListSecurityGroups), varargs...)
+}
+
+// ListVPCEndpointServices mocks base method.
+func (m *MockTaggingManager) ListVPCEndpointServices(arg0 context.Context, arg1 ...tracking.TagFilter) ([]networking.VPCEndpointServiceInfo, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ListVPCEndpointServices", varargs...)
+ ret0, _ := ret[0].([]networking.VPCEndpointServiceInfo)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListVPCEndpointServices indicates an expected call of ListVPCEndpointServices.
+func (mr *MockTaggingManagerMockRecorder) ListVPCEndpointServices(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListVPCEndpointServices", reflect.TypeOf((*MockTaggingManager)(nil).ListVPCEndpointServices), varargs...)
+}
+
+// ReconcileTags mocks base method.
+func (m *MockTaggingManager) ReconcileTags(arg0 context.Context, arg1 string, arg2 map[string]string, arg3 ...ReconcileTagsOption) error {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ReconcileTags", varargs...)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ReconcileTags indicates an expected call of ReconcileTags.
+func (mr *MockTaggingManagerMockRecorder) ReconcileTags(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReconcileTags", reflect.TypeOf((*MockTaggingManager)(nil).ReconcileTags), varargs...)
+}
diff --git a/pkg/deploy/ec2/tagging_manager_test.go b/pkg/deploy/ec2/tagging_manager_test.go
index d346d7b0b6..b284a6b172 100644
--- a/pkg/deploy/ec2/tagging_manager_test.go
+++ b/pkg/deploy/ec2/tagging_manager_test.go
@@ -434,6 +434,265 @@ func Test_defaultTaggingManager_ListSecurityGroups(t *testing.T) {
}
}
+func Test_defaultTaggingManager_ListVPCEndpointServices(t *testing.T) {
+ type fetchVPCESInfosByRequestCall struct {
+ req *ec2sdk.DescribeVpcEndpointServiceConfigurationsInput
+ resp map[string]networking.VPCEndpointServiceInfo
+ err error
+ }
+ type fields struct {
+ fetchVPCESInfosByRequestCalls []fetchVPCESInfosByRequestCall
+ }
+ type args struct {
+ tagFilters []tracking.TagFilter
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want []networking.VPCEndpointServiceInfo
+ wantErr error
+ }{
+ {
+ name: "with a single tagFilter",
+ fields: fields{
+ fetchVPCESInfosByRequestCalls: []fetchVPCESInfosByRequestCall{
+ {
+ req: &ec2sdk.DescribeVpcEndpointServiceConfigurationsInput{
+ Filters: []*ec2sdk.Filter{
+ // {
+ // Name: awssdk.String("vpc-id"),
+ // Values: awssdk.StringSlice([]string{"vpc-xxxxxxx"}),
+ // },
+ {
+ Name: awssdk.String("tag:keyA"),
+ Values: awssdk.StringSlice([]string{"valueA"}),
+ },
+ {
+ Name: awssdk.String("tag:keyB"),
+ Values: awssdk.StringSlice([]string{"valueB1", "valueB2"}),
+ },
+ {
+ Name: awssdk.String("tag-key"),
+ Values: awssdk.StringSlice([]string{"keyC"}),
+ },
+ },
+ },
+ resp: map[string]networking.VPCEndpointServiceInfo{
+ "vpces-a": {
+ ServiceID: "vpces-a",
+ Tags: map[string]string{
+ "keyA": "valueA",
+ "keyB": "valueB1",
+ "keyC": "valueC",
+ "keyD": "valueD",
+ },
+ },
+ "vpces-b": {
+ ServiceID: "vpces-b",
+ Tags: map[string]string{
+ "keyA": "valueA",
+ "keyB": "valueB2",
+ "keyC": "valueC",
+ "keyD": "valueD",
+ },
+ },
+ },
+ },
+ },
+ },
+ args: args{
+ tagFilters: []tracking.TagFilter{
+ {
+ "keyA": []string{"valueA"},
+ "keyB": []string{"valueB1", "valueB2"},
+ "keyC": nil,
+ },
+ },
+ },
+ want: []networking.VPCEndpointServiceInfo{
+ {
+ ServiceID: "vpces-a",
+ Tags: map[string]string{
+ "keyA": "valueA",
+ "keyB": "valueB1",
+ "keyC": "valueC",
+ "keyD": "valueD",
+ },
+ },
+ {
+ ServiceID: "vpces-b",
+ Tags: map[string]string{
+ "keyA": "valueA",
+ "keyB": "valueB2",
+ "keyC": "valueC",
+ "keyD": "valueD",
+ },
+ },
+ },
+ },
+ {
+ name: "with two tagFilter",
+ fields: fields{
+ fetchVPCESInfosByRequestCalls: []fetchVPCESInfosByRequestCall{
+ {
+ req: &ec2sdk.DescribeVpcEndpointServiceConfigurationsInput{
+ Filters: []*ec2sdk.Filter{
+ // {
+ // Name: awssdk.String("vpc-id"),
+ // Values: awssdk.StringSlice([]string{"vpc-xxxxxxx"}),
+ // },
+ {
+ Name: awssdk.String("tag:keyA"),
+ Values: awssdk.StringSlice([]string{"valueA"}),
+ },
+ {
+ Name: awssdk.String("tag:keyB"),
+ Values: awssdk.StringSlice([]string{"valueB1", "valueB2"}),
+ },
+ {
+ Name: awssdk.String("tag-key"),
+ Values: awssdk.StringSlice([]string{"keyC"}),
+ },
+ },
+ },
+ resp: map[string]networking.VPCEndpointServiceInfo{
+ "vpces-a": {
+ ServiceID: "vpces-a",
+ Tags: map[string]string{
+ "keyA": "valueA",
+ "keyB": "valueB1",
+ "keyC": "valueC",
+ "keyD": "valueD",
+ },
+ },
+ "vpces-b": {
+ ServiceID: "vpces-b",
+ Tags: map[string]string{
+ "keyA": "valueA",
+ "keyB": "valueB2",
+ "keyC": "valueC",
+ "keyD": "valueD",
+ },
+ },
+ },
+ },
+ {
+ req: &ec2sdk.DescribeVpcEndpointServiceConfigurationsInput{
+ Filters: []*ec2sdk.Filter{
+ // {
+ // Name: awssdk.String("vpc-id"),
+ // Values: awssdk.StringSlice([]string{"vpc-xxxxxxx"}),
+ // },
+ {
+ Name: awssdk.String("tag:keyA"),
+ Values: awssdk.StringSlice([]string{"valueA"}),
+ },
+ {
+ Name: awssdk.String("tag:keyB"),
+ Values: awssdk.StringSlice([]string{"valueB2", "valueB3"}),
+ },
+ {
+ Name: awssdk.String("tag-key"),
+ Values: awssdk.StringSlice([]string{"keyC"}),
+ },
+ },
+ },
+ resp: map[string]networking.VPCEndpointServiceInfo{
+ "vpces-b": {
+ ServiceID: "vpces-b",
+ Tags: map[string]string{
+ "keyA": "valueA",
+ "keyB": "valueB2",
+ "keyC": "valueC",
+ "keyD": "valueD",
+ },
+ },
+ "vpces-c": {
+ ServiceID: "vpces-c",
+ Tags: map[string]string{
+ "keyA": "valueA",
+ "keyB": "valueB3",
+ "keyC": "valueC",
+ "keyD": "valueD",
+ },
+ },
+ },
+ },
+ },
+ },
+ args: args{
+ tagFilters: []tracking.TagFilter{
+ {
+ "keyA": []string{"valueA"},
+ "keyB": []string{"valueB1", "valueB2"},
+ "keyC": nil,
+ },
+ {
+ "keyA": []string{"valueA"},
+ "keyB": []string{"valueB2", "valueB3"},
+ "keyC": nil,
+ },
+ },
+ },
+ want: []networking.VPCEndpointServiceInfo{
+ {
+ ServiceID: "vpces-a",
+ Tags: map[string]string{
+ "keyA": "valueA",
+ "keyB": "valueB1",
+ "keyC": "valueC",
+ "keyD": "valueD",
+ },
+ },
+ {
+ ServiceID: "vpces-b",
+ Tags: map[string]string{
+ "keyA": "valueA",
+ "keyB": "valueB2",
+ "keyC": "valueC",
+ "keyD": "valueD",
+ },
+ },
+ {
+ ServiceID: "vpces-c",
+ Tags: map[string]string{
+ "keyA": "valueA",
+ "keyB": "valueB3",
+ "keyC": "valueC",
+ "keyD": "valueD",
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ vpcEndpointServiceManager := networking.NewMockVPCEndpointServiceManager(ctrl)
+ for _, call := range tt.fields.fetchVPCESInfosByRequestCalls {
+ vpcEndpointServiceManager.EXPECT().FetchVPCESInfosByRequest(gomock.Any(), call.req).Return(call.resp, call.err)
+ }
+ m := &defaultTaggingManager{
+ vpcEndpointServiceManager: vpcEndpointServiceManager,
+ // vpcID: "vpc-xxxxxxx",
+ }
+ got, err := m.ListVPCEndpointServices(context.Background(), tt.args.tagFilters...)
+ if tt.wantErr != nil {
+ assert.EqualError(t, err, tt.wantErr.Error())
+ } else {
+ assert.NoError(t, err)
+ opts := cmpopts.SortSlices(func(lhs networking.VPCEndpointServiceInfo, rhs networking.VPCEndpointServiceInfo) bool {
+ return lhs.ServiceID < rhs.ServiceID
+ })
+ assert.True(t, cmp.Equal(tt.want, got, opts), "diff", cmp.Diff(tt.want, got, opts))
+ }
+ })
+ }
+}
+
func Test_convertTagsToSDKTags(t *testing.T) {
type args struct {
tags map[string]string
diff --git a/pkg/deploy/elbv2/listener_manager.go b/pkg/deploy/elbv2/listener_manager.go
index 44b2308d51..927f460078 100644
--- a/pkg/deploy/elbv2/listener_manager.go
+++ b/pkg/deploy/elbv2/listener_manager.go
@@ -14,7 +14,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/aws-load-balancer-controller/pkg/aws/services"
"sigs.k8s.io/aws-load-balancer-controller/pkg/config"
- "sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/tracking"
+ tracking "sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/tracking"
elbv2equality "sigs.k8s.io/aws-load-balancer-controller/pkg/equality/elbv2"
elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
"sigs.k8s.io/aws-load-balancer-controller/pkg/runtime"
diff --git a/pkg/deploy/stack_deployer.go b/pkg/deploy/stack_deployer.go
index dda035adc3..8545755741 100644
--- a/pkg/deploy/stack_deployer.go
+++ b/pkg/deploy/stack_deployer.go
@@ -2,6 +2,7 @@ package deploy
import (
"context"
+
"github.com/go-logr/logr"
"sigs.k8s.io/aws-load-balancer-controller/pkg/aws"
"sigs.k8s.io/aws-load-balancer-controller/pkg/config"
@@ -24,18 +25,21 @@ type StackDeployer interface {
// NewDefaultStackDeployer constructs new defaultStackDeployer.
func NewDefaultStackDeployer(cloud aws.Cloud, k8sClient client.Client,
- networkingSGManager networking.SecurityGroupManager, networkingSGReconciler networking.SecurityGroupReconciler,
+ networkingSGManager networking.SecurityGroupManager,
+ networkingSGReconciler networking.SecurityGroupReconciler,
+ vpcEndpointServiceManager networking.VPCEndpointServiceManager,
elbv2TaggingManager elbv2.TaggingManager,
config config.ControllerConfig, tagPrefix string, logger logr.Logger) *defaultStackDeployer {
trackingProvider := tracking.NewDefaultProvider(tagPrefix, config.ClusterName)
- ec2TaggingManager := ec2.NewDefaultTaggingManager(cloud.EC2(), networkingSGManager, cloud.VpcID(), logger)
+ ec2TaggingManager := ec2.NewDefaultTaggingManager(cloud.EC2(), networkingSGManager, vpcEndpointServiceManager, cloud.VpcID(), logger)
return &defaultStackDeployer{
cloud: cloud,
k8sClient: k8sClient,
addonsConfig: config.AddonsConfig,
trackingProvider: trackingProvider,
+ ec2ESManager: ec2.NewDefaultEndpointServiceManager(cloud.EC2(), cloud.VpcID(), logger, trackingProvider, ec2TaggingManager, config.ExternalManagedTags),
ec2TaggingManager: ec2TaggingManager,
ec2SGManager: ec2.NewDefaultSecurityGroupManager(cloud.EC2(), trackingProvider, ec2TaggingManager, networkingSGReconciler, cloud.VpcID(), config.ExternalManagedTags, logger),
elbv2TaggingManager: elbv2TaggingManager,
@@ -61,6 +65,7 @@ type defaultStackDeployer struct {
k8sClient client.Client
addonsConfig config.AddonsConfig
trackingProvider tracking.Provider
+ ec2ESManager ec2.EndpointServiceManager
ec2TaggingManager ec2.TaggingManager
ec2SGManager ec2.SecurityGroupManager
elbv2TaggingManager elbv2.TaggingManager
@@ -108,6 +113,11 @@ func (d *defaultStackDeployer) Deploy(ctx context.Context, stack core.Stack) err
synthesizers = append(synthesizers, shield.NewProtectionSynthesizer(d.shieldProtectionManager, d.logger, stack))
}
}
+ if d.addonsConfig.EndpointServiceEnabled {
+ // We need to synthesize endpoints before load balancers so we insert
+ // the endpoint synthesizer to the start of the list.
+ synthesizers = append([]ResourceSynthesizer{ec2.NewEndpointServiceSynthesizer(d.cloud.EC2(), d.trackingProvider, d.ec2TaggingManager, d.ec2ESManager, d.vpcID, d.logger, stack)}, synthesizers...)
+ }
for _, synthesizer := range synthesizers {
if err := synthesizer.Synthesize(ctx); err != nil {
diff --git a/pkg/deploy/tracking/provider_mocks.go b/pkg/deploy/tracking/provider_mocks.go
new file mode 100644
index 0000000000..ab35882acb
--- /dev/null
+++ b/pkg/deploy/tracking/provider_mocks.go
@@ -0,0 +1,119 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/tracking (interfaces: Provider)
+
+// Package tracking is a generated GoMock package.
+package tracking
+
+import (
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ core "sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"
+)
+
+// MockProvider is a mock of Provider interface.
+type MockProvider struct {
+ ctrl *gomock.Controller
+ recorder *MockProviderMockRecorder
+}
+
+// MockProviderMockRecorder is the mock recorder for MockProvider.
+type MockProviderMockRecorder struct {
+ mock *MockProvider
+}
+
+// NewMockProvider creates a new mock instance.
+func NewMockProvider(ctrl *gomock.Controller) *MockProvider {
+ mock := &MockProvider{ctrl: ctrl}
+ mock.recorder = &MockProviderMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockProvider) EXPECT() *MockProviderMockRecorder {
+ return m.recorder
+}
+
+// LegacyTagKeys mocks base method.
+func (m *MockProvider) LegacyTagKeys() []string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LegacyTagKeys")
+ ret0, _ := ret[0].([]string)
+ return ret0
+}
+
+// LegacyTagKeys indicates an expected call of LegacyTagKeys.
+func (mr *MockProviderMockRecorder) LegacyTagKeys() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LegacyTagKeys", reflect.TypeOf((*MockProvider)(nil).LegacyTagKeys))
+}
+
+// ResourceIDTagKey mocks base method.
+func (m *MockProvider) ResourceIDTagKey() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ResourceIDTagKey")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// ResourceIDTagKey indicates an expected call of ResourceIDTagKey.
+func (mr *MockProviderMockRecorder) ResourceIDTagKey() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResourceIDTagKey", reflect.TypeOf((*MockProvider)(nil).ResourceIDTagKey))
+}
+
+// ResourceTags mocks base method.
+func (m *MockProvider) ResourceTags(arg0 core.Stack, arg1 core.Resource, arg2 map[string]string) map[string]string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ResourceTags", arg0, arg1, arg2)
+ ret0, _ := ret[0].(map[string]string)
+ return ret0
+}
+
+// ResourceTags indicates an expected call of ResourceTags.
+func (mr *MockProviderMockRecorder) ResourceTags(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResourceTags", reflect.TypeOf((*MockProvider)(nil).ResourceTags), arg0, arg1, arg2)
+}
+
+// StackLabels mocks base method.
+func (m *MockProvider) StackLabels(arg0 core.Stack) map[string]string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StackLabels", arg0)
+ ret0, _ := ret[0].(map[string]string)
+ return ret0
+}
+
+// StackLabels indicates an expected call of StackLabels.
+func (mr *MockProviderMockRecorder) StackLabels(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackLabels", reflect.TypeOf((*MockProvider)(nil).StackLabels), arg0)
+}
+
+// StackTags mocks base method.
+func (m *MockProvider) StackTags(arg0 core.Stack) map[string]string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StackTags", arg0)
+ ret0, _ := ret[0].(map[string]string)
+ return ret0
+}
+
+// StackTags indicates an expected call of StackTags.
+func (mr *MockProviderMockRecorder) StackTags(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackTags", reflect.TypeOf((*MockProvider)(nil).StackTags), arg0)
+}
+
+// StackTagsLegacy mocks base method.
+func (m *MockProvider) StackTagsLegacy(arg0 core.Stack) map[string]string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StackTagsLegacy", arg0)
+ ret0, _ := ret[0].(map[string]string)
+ return ret0
+}
+
+// StackTagsLegacy indicates an expected call of StackTagsLegacy.
+func (mr *MockProviderMockRecorder) StackTagsLegacy(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackTagsLegacy", reflect.TypeOf((*MockProvider)(nil).StackTagsLegacy), arg0)
+}
diff --git a/pkg/model/ec2/vpc_endpoint_service.go b/pkg/model/ec2/vpc_endpoint_service.go
new file mode 100644
index 0000000000..74793fde4b
--- /dev/null
+++ b/pkg/model/ec2/vpc_endpoint_service.go
@@ -0,0 +1,91 @@
+package ec2
+
+import (
+ "context"
+
+ "github.com/pkg/errors"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"
+)
+
+var _ core.Resource = &VPCEndpointService{}
+
+// VPCEndpointService represents a VPC Endpoint Service.
+type VPCEndpointService struct {
+ core.ResourceMeta `json:"-"`
+
+ // desired state of VPCEndpointService
+ Spec VPCEndpointServiceSpec `json:"spec"`
+
+ // observed state of VPCEndpointService
+ Status *VPCEndpointServiceStatus `json:"status,omitempty"`
+}
+
+// NewVPCEndpointService constructs new VPCEndpointService resource.
+func NewVPCEndpointService(stack core.Stack, id string, spec VPCEndpointServiceSpec) *VPCEndpointService {
+ es := &VPCEndpointService{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointService", id),
+ Spec: spec,
+ Status: nil,
+ }
+ stack.AddResource(es)
+ return es
+}
+
+// SetStatus sets the VPCEndpointService's status
+func (es *VPCEndpointService) SetStatus(status VPCEndpointServiceStatus) {
+ es.Status = &status
+}
+
+// ServiceID returns a token for this VPCEndpointService's serviceID.
+func (es *VPCEndpointService) ServiceID() core.StringToken {
+ return core.NewResourceFieldStringToken(es, "status/serviceID",
+ func(ctx context.Context, res core.Resource, fieldPath string) (s string, err error) {
+ es := res.(*VPCEndpointService)
+ if es.Status == nil {
+ return "", errors.Errorf("VPCEndpointService is not fulfilled yet: %v", es.ID())
+ }
+ return es.Status.ServiceID, nil
+ },
+ )
+}
+
+// VPCEndpointServiceSpec defines the desired state of VPCEndpointService
+type VPCEndpointServiceSpec struct {
+ // whether requests from service consumers to create an endpoint to the service must be accepted
+ AcceptanceRequired *bool `json:"acceptanceRequired"`
+
+ NetworkLoadBalancerArns []core.StringToken `json:"networkLoadBalancerArns"`
+
+ PrivateDNSName *string `json:"privateDnsName"`
+
+ // +optional
+ Tags map[string]string `json:"tags,omitempty"`
+}
+
+// VPCEndpointServiceStatus defines the observed state of VPCEndpointService
+type VPCEndpointServiceStatus struct {
+ // The ID of the endpoint service.
+ ServiceID string `json:"serviceID"`
+
+ BaseEndpointDnsNames []string `json:"baseEndpointDnsNames"`
+}
+
+type VPCEndpointServicePermissions struct {
+ core.ResourceMeta `json:"-"`
+ Spec VPCEndpointServicePermissionsSpec `json:"spec"`
+}
+
+// NewVPCEndpointService constructs new VPCEndpointServicePermissions resource.
+func NewVPCEndpointServicePermissions(stack core.Stack, id string, spec VPCEndpointServicePermissionsSpec) *VPCEndpointServicePermissions {
+ esPermissions := &VPCEndpointServicePermissions{
+ ResourceMeta: core.NewResourceMeta(stack, "AWS::EC2::VPCEndpointServicePermissions", id),
+ Spec: spec,
+ }
+ stack.AddResource(esPermissions)
+ return esPermissions
+}
+
+type VPCEndpointServicePermissionsSpec struct {
+ AllowedPrincipals []string `json:"allowedPrincipals"`
+ ServiceId core.StringToken `json:"serviceID"`
+}
diff --git a/pkg/networking/vpc_endpoint_service_info.go b/pkg/networking/vpc_endpoint_service_info.go
new file mode 100644
index 0000000000..453fb194d5
--- /dev/null
+++ b/pkg/networking/vpc_endpoint_service_info.go
@@ -0,0 +1,61 @@
+package networking
+
+import (
+ awssdk "github.com/aws/aws-sdk-go/aws"
+ ec2sdk "github.com/aws/aws-sdk-go/service/ec2"
+)
+
+// VPCEndpointServiceInfo wraps necessary information about an Endpoint Service.
+type VPCEndpointServiceInfo struct {
+ // The ID of the endpoint service.
+ ServiceID string
+
+ // whether requests from service consumers to create an endpoint to the service must be accepted
+ AcceptanceRequired bool
+
+ NetworkLoadBalancerArns []string
+
+ PrivateDNSName *string
+
+ BaseEndpointDnsNames []string
+ // +optional
+ Tags map[string]string
+}
+
+// NewRawVPCEndpointServiceInfo constructs new VPCEndpointServiceInfo with raw ec2SDK's ServiceConfiguration object.
+func NewRawVPCEndpointServiceInfo(sdkES *ec2sdk.ServiceConfiguration) VPCEndpointServiceInfo {
+ esID := awssdk.StringValue(sdkES.ServiceId)
+
+ tags := make(map[string]string, len(sdkES.Tags))
+ for _, tag := range sdkES.Tags {
+ tags[awssdk.StringValue(tag.Key)] = awssdk.StringValue(tag.Value)
+ }
+ return VPCEndpointServiceInfo{
+ ServiceID: esID,
+ AcceptanceRequired: awssdk.BoolValue(sdkES.AcceptanceRequired),
+ NetworkLoadBalancerArns: awssdk.StringValueSlice(sdkES.NetworkLoadBalancerArns),
+ PrivateDNSName: sdkES.PrivateDnsName,
+ BaseEndpointDnsNames: awssdk.StringValueSlice(sdkES.BaseEndpointDnsNames),
+ Tags: tags,
+ }
+}
+
+// VPCEndpointServiceInfo wraps necessary information about Endpoint Service Permissions.
+type VPCEndpointServicePermissionsInfo struct {
+ // The allowed principals for the endpoint service
+ AllowedPrincipals []string
+
+ // The service these principals apply to
+ ServiceId string
+}
+
+func NewRawVPCEndpointServicePermissionsInfo(sdkPermissions *ec2sdk.DescribeVpcEndpointServicePermissionsOutput) VPCEndpointServicePermissionsInfo {
+ var principals []string
+ for _, p := range sdkPermissions.AllowedPrincipals {
+ principals = append(principals, *p.Principal)
+ }
+
+ return VPCEndpointServicePermissionsInfo{
+ AllowedPrincipals: principals,
+ }
+}
diff --git a/pkg/networking/vpc_endpoint_service_manager.go b/pkg/networking/vpc_endpoint_service_manager.go
new file mode 100644
index 0000000000..a36b997f37
--- /dev/null
+++ b/pkg/networking/vpc_endpoint_service_manager.go
@@ -0,0 +1,83 @@
+package networking
+
+import (
+ "context"
+
+ awssdk "github.com/aws/aws-sdk-go/aws"
+ ec2sdk "github.com/aws/aws-sdk-go/service/ec2"
+ "github.com/go-logr/logr"
+ "github.com/pkg/errors"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/aws/services"
+)
+
+type FetchVPCESInfoOptions struct {
+ // whether to ignore cache and reload Endpoint Service Info from AWS directly.
+ ReloadIgnoringCache bool
+}
+
+// Apply FetchVPCESInfoOption options
+func (opts *FetchVPCESInfoOptions) ApplyOptions(options ...FetchVPCESInfoOption) {
+ for _, option := range options {
+ option(opts)
+ }
+}
+
+type FetchVPCESInfoOption func(opts *FetchVPCESInfoOptions)
+
+// WithReloadIgnoringCache is a option that sets the ReloadIgnoringCache to true.
+func WithVPCESReloadIgnoringCache() FetchVPCESInfoOption {
+ return func(opts *FetchVPCESInfoOptions) {
+ opts.ReloadIgnoringCache = true
+ }
+}
+
+// VPCEndpointServiceManager is an abstraction around EC2's VPC Endpoint Service API.
+type VPCEndpointServiceManager interface {
+ // FetchVPCESInfosByID will fetch VPCEndpointServiceInfo with EndpointService IDs.
+ FetchVPCESInfosByID(ctx context.Context, esIDs []string, opts ...FetchVPCESInfoOption) (map[string]VPCEndpointServiceInfo, error)
+
+ // FetchVPCESInfosByRequest will fetch VPCEndpointServiceInfo with raw DescribeVpcEndpointServiceConfigurationsInput request.
+ FetchVPCESInfosByRequest(ctx context.Context, req *ec2sdk.DescribeVpcEndpointServiceConfigurationsInput) (map[string]VPCEndpointServiceInfo, error)
+}
+
+// NewDefaultVPCEndpointServiceManager constructs new defaultVPCEndpointServiceManager.
+func NewDefaultVPCEndpointServiceManager(ec2Client services.EC2, logger logr.Logger) *defaultVPCEndpointServiceManager {
+ return &defaultVPCEndpointServiceManager{
+ ec2Client: ec2Client,
+ logger: logger,
+ }
+}
+
+var _ VPCEndpointServiceManager = &defaultVPCEndpointServiceManager{}
+
+// default implementation for VPCEndpointServiceManager
+type defaultVPCEndpointServiceManager struct {
+ ec2Client services.EC2
+ logger logr.Logger
+}
+
+func (m *defaultVPCEndpointServiceManager) FetchVPCESInfosByID(ctx context.Context, esIDs []string, opts ...FetchVPCESInfoOption) (map[string]VPCEndpointServiceInfo, error) {
+ return nil, nil
+}
+
+func (m *defaultVPCEndpointServiceManager) FetchVPCESInfosByRequest(ctx context.Context, req *ec2sdk.DescribeVpcEndpointServiceConfigurationsInput) (map[string]VPCEndpointServiceInfo, error) {
+ esInfosByID, err := m.fetchESInfosFromAWS(ctx, req)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to fetch VPCEndpointService information from AWS")
+ }
+ return esInfosByID, nil
+}
+
+func (m *defaultVPCEndpointServiceManager) fetchESInfosFromAWS(ctx context.Context, req *ec2sdk.DescribeVpcEndpointServiceConfigurationsInput) (map[string]VPCEndpointServiceInfo, error) {
+ endpointServices, err := m.ec2Client.DescribeVpcEndpointServicesAsList(ctx, req)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to describe VPCEndpointServices")
+ }
+ esInfoByID := make(map[string]VPCEndpointServiceInfo, len(endpointServices))
+ for _, es := range endpointServices {
+ esID := awssdk.StringValue(es.ServiceId)
+ esInfo := NewRawVPCEndpointServiceInfo(es)
+ esInfoByID[esID] = esInfo
+ }
+ return esInfoByID, nil
+}
diff --git a/pkg/networking/vpc_endpoint_service_manager_mocks.go b/pkg/networking/vpc_endpoint_service_manager_mocks.go
new file mode 100644
index 0000000000..7349ef4851
--- /dev/null
+++ b/pkg/networking/vpc_endpoint_service_manager_mocks.go
@@ -0,0 +1,71 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: sigs.k8s.io/aws-load-balancer-controller/pkg/networking (interfaces: VPCEndpointServiceManager)
+
+// Package networking is a generated GoMock package.
+package networking
+
+import (
+ context "context"
+ reflect "reflect"
+
+ ec2 "github.com/aws/aws-sdk-go/service/ec2"
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockVPCEndpointServiceManager is a mock of VPCEndpointServiceManager interface.
+type MockVPCEndpointServiceManager struct {
+ ctrl *gomock.Controller
+ recorder *MockVPCEndpointServiceManagerMockRecorder
+}
+
+// MockVPCEndpointServiceManagerMockRecorder is the mock recorder for MockVPCEndpointServiceManager.
+type MockVPCEndpointServiceManagerMockRecorder struct {
+ mock *MockVPCEndpointServiceManager
+}
+
+// NewMockVPCEndpointServiceManager creates a new mock instance.
+func NewMockVPCEndpointServiceManager(ctrl *gomock.Controller) *MockVPCEndpointServiceManager {
+ mock := &MockVPCEndpointServiceManager{ctrl: ctrl}
+ mock.recorder = &MockVPCEndpointServiceManagerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockVPCEndpointServiceManager) EXPECT() *MockVPCEndpointServiceManagerMockRecorder {
+ return m.recorder
+}
+
+// FetchVPCESInfosByID mocks base method.
+func (m *MockVPCEndpointServiceManager) FetchVPCESInfosByID(arg0 context.Context, arg1 []string, arg2 ...FetchVPCESInfoOption) (map[string]VPCEndpointServiceInfo, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "FetchVPCESInfosByID", varargs...)
+ ret0, _ := ret[0].(map[string]VPCEndpointServiceInfo)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FetchVPCESInfosByID indicates an expected call of FetchVPCESInfosByID.
+func (mr *MockVPCEndpointServiceManagerMockRecorder) FetchVPCESInfosByID(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchVPCESInfosByID", reflect.TypeOf((*MockVPCEndpointServiceManager)(nil).FetchVPCESInfosByID), varargs...)
+}
+
+// FetchVPCESInfosByRequest mocks base method.
+func (m *MockVPCEndpointServiceManager) FetchVPCESInfosByRequest(arg0 context.Context, arg1 *ec2.DescribeVpcEndpointServiceConfigurationsInput) (map[string]VPCEndpointServiceInfo, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FetchVPCESInfosByRequest", arg0, arg1)
+ ret0, _ := ret[0].(map[string]VPCEndpointServiceInfo)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FetchVPCESInfosByRequest indicates an expected call of FetchVPCESInfosByRequest.
+func (mr *MockVPCEndpointServiceManagerMockRecorder) FetchVPCESInfosByRequest(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchVPCESInfosByRequest", reflect.TypeOf((*MockVPCEndpointServiceManager)(nil).FetchVPCESInfosByRequest), arg0, arg1)
+}
diff --git a/pkg/service/model_build_endpoint_service.go b/pkg/service/model_build_endpoint_service.go
new file mode 100644
index 0000000000..f2913ac37c
--- /dev/null
+++ b/pkg/service/model_build_endpoint_service.go
@@ -0,0 +1,100 @@
+package service
+
+import (
+ "context"
+
+ "github.com/pkg/errors"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
+
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"
+ ec2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/ec2"
+)
+
+const (
+ defaultRawEnabled string = "false"
+)
+
+func (t *defaultModelBuildTask) buildEndpointService(ctx context.Context) error {
+ enabled, err := t.buildEnabled(ctx)
+ if err != nil {
+ return err
+ }
+ if !enabled {
+ return nil
+ }
+
+ acceptanceRequired, err := t.buildAcceptanceRequired(ctx)
+ if err != nil {
+ return err
+ }
+
+ allowedPrincipals := t.buildAllowedPrincipals(ctx)
+ privateDNSName := t.buildPrivateDNSName(ctx)
+
+ tags, err := t.buildListenerTags(ctx)
+ if err != nil {
+ return err
+ }
+
+ esSpec := ec2model.VPCEndpointServiceSpec{
+ AcceptanceRequired: &acceptanceRequired,
+ NetworkLoadBalancerArns: []core.StringToken{t.loadBalancer.LoadBalancerARN()},
+ PrivateDNSName: privateDNSName,
+ Tags: tags,
+ }
+
+ es := ec2model.NewVPCEndpointService(t.stack, "VPCEndpointService", esSpec)
+
+ espSpec := ec2model.VPCEndpointServicePermissionsSpec{
+ AllowedPrincipals: allowedPrincipals,
+ ServiceId: es.ServiceID(),
+ }
+
+ _ = ec2model.NewVPCEndpointServicePermissions(t.stack, "VPCEndpointServicePermissions", espSpec)
+
+ return nil
+}
+
+func (t *defaultModelBuildTask) buildEnabled(_ context.Context) (bool, error) {
+ rawEnabled := defaultRawEnabled
+ _ = t.annotationParser.ParseStringAnnotation(annotations.SvcLBSuffixEndpointServiceEnabled, &rawEnabled, t.service.Annotations, annotations.WithAlternativePrefixes("service.alpha.kubernetes.io"))
+ // We could use strconv here but we want to be explicit
+ switch rawEnabled {
+ case "true":
+ return true, nil
+ case "false":
+ return false, nil
+ default:
+ return false, errors.Errorf("invalid service annotation %v, value must be one of [%v, %v]", annotations.SvcLBSuffixEndpointServiceEnabled, true, false)
+ }
+}
+
+func (t *defaultModelBuildTask) buildAcceptanceRequired(_ context.Context) (bool, error) {
+ rawAcceptanceRequired := ""
+ _ = t.annotationParser.ParseStringAnnotation(annotations.SvcLBSuffixEndpointServiceAcceptanceRequired, &rawAcceptanceRequired, t.service.Annotations, annotations.WithAlternativePrefixes("service.alpha.kubernetes.io"))
+ // We could use strconv here but we want to be explicit
+ switch rawAcceptanceRequired {
+ case "true":
+ return true, nil
+ case "false":
+ return false, nil
+ default:
+ return false, errors.Errorf("invalid service annotation %v, value must be one of [%v, %v]", annotations.SvcLBSuffixEndpointServiceAcceptanceRequired, true, false)
+ }
+}
+
+func (t *defaultModelBuildTask) buildAllowedPrincipals(_ context.Context) []string {
+ var rawAllowedPrincipals []string
+ if exists := t.annotationParser.ParseStringSliceAnnotation(annotations.SvcLBSuffixEndpointServiceAllowedPrincipals, &rawAllowedPrincipals, t.service.Annotations, annotations.WithAlternativePrefixes("service.alpha.kubernetes.io")); !exists {
+ return []string{}
+ }
+ return rawAllowedPrincipals
+}
+
+func (t *defaultModelBuildTask) buildPrivateDNSName(_ context.Context) *string {
+ rawPrivateDNSName := ""
+ if exists := t.annotationParser.ParseStringAnnotation(annotations.SvcLBSuffixEndpointServicePrivateDNSName, &rawPrivateDNSName, t.service.Annotations, annotations.WithAlternativePrefixes("service.alpha.kubernetes.io")); !exists {
+ return nil
+ }
+ return &rawPrivateDNSName
+}
diff --git a/pkg/service/model_build_endpoint_service_test.go b/pkg/service/model_build_endpoint_service_test.go
new file mode 100644
index 0000000000..605a2dfa52
--- /dev/null
+++ b/pkg/service/model_build_endpoint_service_test.go
@@ -0,0 +1,203 @@
+package service
+
+import (
+ "context"
+ "testing"
+
+ awssdk "github.com/aws/aws-sdk-go/aws"
+ "github.com/stretchr/testify/assert"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
+)
+
+func Test_defaultModelBuildTask_buildEnabled(t *testing.T) {
+ tests := []struct {
+ name string
+ svc *corev1.Service
+ wantErr bool
+ want bool
+ }{
+ {
+ name: "Service without annotation",
+ svc: &corev1.Service{},
+ want: false,
+ },
+ {
+ name: "Service with valid annotation",
+ svc: &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ "service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-enabled": "true",
+ },
+ },
+ },
+ want: true,
+ },
+ {
+ name: "Service with invalid annotation",
+ svc: &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ "service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-enabled": "True",
+ },
+ },
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ parser := annotations.NewSuffixAnnotationParser("service.beta.kubernetes.io")
+ builder := &defaultModelBuildTask{
+ annotationParser: parser,
+ service: tt.svc,
+ }
+ got, err := builder.buildEnabled(context.Background())
+ if tt.wantErr {
+ assert.Error(t, err)
+ } else {
+ assert.Equal(t, tt.want, got)
+ }
+ })
+ }
+}
+
+func Test_defaultModelBuildTask_buildAcceptanceRequired(t *testing.T) {
+ tests := []struct {
+ name string
+ svc *corev1.Service
+ wantErr bool
+ want bool
+ }{
+ {
+ name: "Service without annotation",
+ svc: &corev1.Service{},
+ want: false,
+ },
+ {
+ name: "Service with valid annotation",
+ svc: &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ "service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-acceptance-required": "true",
+ },
+ },
+ },
+ want: true,
+ },
+ {
+ name: "Service with invalid annotation",
+ svc: &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ "service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-acceptance-required": "True",
+ },
+ },
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ parser := annotations.NewSuffixAnnotationParser("service.beta.kubernetes.io")
+ builder := &defaultModelBuildTask{
+ annotationParser: parser,
+ service: tt.svc,
+ }
+ got, err := builder.buildAcceptanceRequired(context.Background())
+ if tt.wantErr {
+ assert.Error(t, err)
+ } else {
+ assert.Equal(t, tt.want, got)
+ }
+ })
+ }
+}
+
+func Test_defaultModelBuildTask_buildAllowedPrincipals(t *testing.T) {
+ tests := []struct {
+ name string
+ svc *corev1.Service
+ want []string
+ }{
+ {
+ name: "Service without annotation",
+ svc: &corev1.Service{},
+ want: []string{},
+ },
+ {
+ name: "Service with single arn",
+ svc: &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ "service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-allowed-principals": "arn1",
+ },
+ },
+ },
+ want: []string{"arn1"},
+ },
+ {
+ name: "Service with multiple arns",
+ svc: &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ "service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-allowed-principals": "arn1,arn2",
+ },
+ },
+ },
+ want: []string{"arn1", "arn2"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ parser := annotations.NewSuffixAnnotationParser("service.beta.kubernetes.io")
+ builder := &defaultModelBuildTask{
+ annotationParser: parser,
+ service: tt.svc,
+ }
+ got := builder.buildAllowedPrincipals(context.Background())
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func Test_defaultModelBuildTask_buildPrivateDNSName(t *testing.T) {
+ tests := []struct {
+ name string
+ svc *corev1.Service
+ want *string
+ }{
+ {
+ name: "Service without annotation",
+ svc: &corev1.Service{},
+ want: nil,
+ },
+ {
+ name: "Service with valid annotation",
+ svc: &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ "service.alpha.kubernetes.io/aws-load-balancer-endpoint-service-private-dns-name": "privateDnsName",
+ },
+ },
+ },
+ want: awssdk.String("privateDnsName"),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ parser := annotations.NewSuffixAnnotationParser("service.beta.kubernetes.io")
+ builder := &defaultModelBuildTask{
+ annotationParser: parser,
+ service: tt.svc,
+ }
+ got := builder.buildPrivateDNSName(context.Background())
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/pkg/service/model_builder.go b/pkg/service/model_builder.go
index 961254209b..8ea2236917 100644
--- a/pkg/service/model_builder.go
+++ b/pkg/service/model_builder.go
@@ -2,9 +2,6 @@ package service
import (
"context"
- "strconv"
- "sync"
-
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/go-logr/logr"
"github.com/pkg/errors"
@@ -19,6 +16,8 @@ import (
"sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"
elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
"sigs.k8s.io/aws-load-balancer-controller/pkg/networking"
+ "strconv"
+ "sync"
)
const (
@@ -144,6 +143,9 @@ func (b *defaultModelBuilder) Build(ctx context.Context, service *corev1.Service
defaultHealthCheckTimeoutForInstanceModeLocal: 6,
defaultHealthCheckHealthyThresholdForInstanceModeLocal: 2,
defaultHealthCheckUnhealthyThresholdForInstanceModeLocal: 2,
+
+ defaultEndpointServiceAcceptanceRequired: true,
+ defaultEndpointServicePrivateDnsName: "",
}
if err := task.run(ctx); err != nil {
@@ -213,6 +215,10 @@ type defaultModelBuildTask struct {
defaultHealthCheckTimeoutForInstanceModeLocal int64
defaultHealthCheckHealthyThresholdForInstanceModeLocal int64
defaultHealthCheckUnhealthyThresholdForInstanceModeLocal int64
+
+ // Default VPC Endpoint Service settings
+ defaultEndpointServiceAcceptanceRequired bool
+ defaultEndpointServicePrivateDnsName string
}
func (t *defaultModelBuildTask) run(ctx context.Context) error {
@@ -249,6 +255,10 @@ func (t *defaultModelBuildTask) buildModel(ctx context.Context) error {
if err != nil {
return err
}
+ err = t.buildEndpointService(ctx)
+ if err != nil {
+ return err
+ }
return nil
}
diff --git a/scripts/gen_mocks.sh b/scripts/gen_mocks.sh
index 00d24d39f7..3279966fd3 100755
--- a/scripts/gen_mocks.sh
+++ b/scripts/gen_mocks.sh
@@ -19,4 +19,7 @@ $MOCKGEN -package=networking -destination=./pkg/networking/vpc_info_provider_moc
$MOCKGEN -package=networking -destination=./pkg/networking/backend_sg_provider_mocks.go sigs.k8s.io/aws-load-balancer-controller/pkg/networking BackendSGProvider
$MOCKGEN -package=networking -destination=./pkg/networking/security_group_resolver_mocks.go sigs.k8s.io/aws-load-balancer-controller/pkg/networking SecurityGroupResolver
$MOCKGEN -package=ingress -destination=./pkg/ingress/cert_discovery_mocks.go sigs.k8s.io/aws-load-balancer-controller/pkg/ingress CertDiscovery
-$MOCKGEN -package=elbv2 -destination=./pkg/deploy/elbv2/tagging_manager_mocks.go sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/elbv2 TaggingManager
\ No newline at end of file
+$MOCKGEN -package=elbv2 -destination=./pkg/deploy/elbv2/tagging_manager_mocks.go sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/elbv2 TaggingManager
+$MOCKGEN -package=networking -destination=./pkg/networking/vpc_endpoint_service_manager_mocks.go sigs.k8s.io/aws-load-balancer-controller/pkg/networking VPCEndpointServiceManager
+$MOCKGEN -package=ec2 -destination=./pkg/deploy/ec2/endpoint_service_manager_mocks.go sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/ec2 EndpointServiceManager
+$MOCKGEN -package=tracking -destination=./pkg/deploy/tracking/provider_mocks.go sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/tracking Provider