Skip to content

Commit 49d56fd

Browse files
author
Ryan Lymburner
committed
Initial changes and unit tests
1 parent 84fcfae commit 49d56fd

File tree

11 files changed

+500
-10
lines changed

11 files changed

+500
-10
lines changed

config/crds/bases/application-networking.k8s.aws_serviceexports.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,37 @@ spec:
3838
type: string
3939
metadata:
4040
type: object
41+
spec:
42+
description: spec defines the desired state of ServiceExport
43+
properties:
44+
exportedPorts:
45+
description: |-
46+
exportedPorts defines which ports of the service should be exported and what route types they should be used with.
47+
If not specified, the controller will use the port from the annotation "application-networking.k8s.aws/port"
48+
and create HTTP target groups for backward compatibility.
49+
items:
50+
description: ExportedPort defines a port to be exported and the
51+
route type it should be used with
52+
properties:
53+
port:
54+
description: port is the port number to export
55+
format: int32
56+
type: integer
57+
routeType:
58+
description: |-
59+
routeType is the type of route this port should be used with
60+
Valid values are "HTTP", "GRPC", "TLS"
61+
enum:
62+
- HTTP
63+
- GRPC
64+
- TLS
65+
type: string
66+
required:
67+
- port
68+
- routeType
69+
type: object
70+
type: array
71+
type: object
4172
status:
4273
description: |-
4374
status describes the current state of an exported service.

docs/api-types/service-export.md

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,34 @@ for example, using target groups in the VPC Lattice setup outside Kubernetes.
1212
Note that ServiceExport is not the implementation of Kubernetes [Multicluster Service APIs](https://multicluster.sigs.k8s.io/concepts/multicluster-services-api/);
1313
instead AWS Gateway API Controller uses its own version of the resource for the purpose of Gateway API integration.
1414

15-
16-
### Limitations
17-
* The exported Service can only be used in HTTPRoutes. GRPCRoute is currently not supported.
18-
* Limited to one ServiceExport per Service. If you need multiple exports representing each port,
19-
you should create multiple Service-ServiceExport pairs.
20-
21-
### Annotations
15+
### Annotations (Legacy Method)
2216

2317
* `application-networking.k8s.aws/port`
2418
Represents which port of the exported Service will be used.
2519
When a comma-separated list of ports is provided, the traffic will be distributed to all ports in the list.
20+
21+
**Note:** This annotation is supported for backward compatibility. For new deployments, it's recommended to use the `spec.exportedPorts` field instead.
22+
23+
## Spec Fields
24+
25+
### exportedPorts
26+
27+
The `exportedPorts` field allows you to explicitly define which ports of the service should be exported and what route types they should be used with. This is useful when you have a service with multiple ports serving different protocols.
2628

27-
## Example Configuration
29+
Each exported port has the following fields:
30+
* `port`: The port number to export
31+
* `routeType`: The type of route this port should be used with. Valid values are:
32+
* `HTTP`: For HTTP traffic
33+
* `GRPC`: For gRPC traffic
34+
* `TLS`: For TLS traffic
2835

29-
The following yaml will create a ServiceExport for a Service named `service-1`:
36+
If `exportedPorts` is not specified, the controller will use the port from the annotation "application-networking.k8s.aws/port" and create HTTP target groups for backward compatibility.
37+
38+
## Example Configurations
39+
40+
### Legacy Configuration (Using Annotations)
41+
42+
The following yaml will create a ServiceExport for a Service named `service-1` using the legacy annotation method:
3043
```yaml
3144
apiVersion: application-networking.k8s.aws/v1alpha1
3245
kind: ServiceExport
@@ -36,3 +49,23 @@ metadata:
3649
application-networking.k8s.aws/port: "9200"
3750
spec: {}
3851
```
52+
53+
### Using exportedPorts
54+
55+
The following yaml will create a ServiceExport for a Service named `service-1` with multiple ports for different route types:
56+
```yaml
57+
apiVersion: application-networking.k8s.aws/v1alpha1
58+
kind: ServiceExport
59+
metadata:
60+
name: service-1
61+
spec:
62+
exportedPorts:
63+
- port: 80
64+
routeType: HTTP
65+
- port: 8081
66+
routeType: GRPC
67+
```
68+
69+
This configuration will:
70+
1. Export port 80 to be used with HTTP routes
71+
2. Export port 8081 to be used with gRPC routes

files/examples/inventory-ver2-export.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ metadata:
44
name: inventory-ver2
55
annotations:
66
application-networking.k8s.aws/federation: "amazon-vpc-lattice"
7+
spec:
8+
exportedPorts:
9+
- port: 80
10+
routeType: HTTP
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: application-networking.k8s.aws/v1alpha1
2+
kind: ServiceExport
3+
metadata:
4+
name: multi-protocol-service
5+
annotations:
6+
application-networking.k8s.aws/federation: "amazon-vpc-lattice"
7+
spec:
8+
exportedPorts:
9+
- port: 80
10+
routeType: HTTP
11+
- port: 8081
12+
routeType: GRPC
13+
- port: 443
14+
routeType: TLS

files/examples/service-1-export.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ metadata:
44
name: service-1
55
annotations:
66
application-networking.k8s.aws/federation: "amazon-vpc-lattice"
7+
spec:
8+
exportedPorts:
9+
- port: 80
10+
routeType: HTTP

files/examples/service-2-export.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ metadata:
44
name: service-2
55
annotations:
66
application-networking.k8s.aws/federation: "amazon-vpc-lattice"
7+
spec:
8+
exportedPorts:
9+
- port: 80
10+
routeType: HTTP

files/examples/tls-rate2-export.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@ kind: ServiceExport
33
metadata:
44
name: tls-rate2
55
annotations:
6-
application-networking.k8s.aws/federation: "amazon-vpc-lattice"
6+
application-networking.k8s.aws/federation: "amazon-vpc-lattice"
7+
spec:
8+
exportedPorts:
9+
- port: 443
10+
routeType: TLS

helm/crds/application-networking.k8s.aws_serviceexports.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,37 @@ spec:
3838
type: string
3939
metadata:
4040
type: object
41+
spec:
42+
description: spec defines the desired state of ServiceExport
43+
properties:
44+
exportedPorts:
45+
description: |-
46+
exportedPorts defines which ports of the service should be exported and what route types they should be used with.
47+
If not specified, the controller will use the port from the annotation "application-networking.k8s.aws/port"
48+
and create HTTP target groups for backward compatibility.
49+
items:
50+
description: ExportedPort defines a port to be exported and the
51+
route type it should be used with
52+
properties:
53+
port:
54+
description: port is the port number to export
55+
format: int32
56+
type: integer
57+
routeType:
58+
description: |-
59+
routeType is the type of route this port should be used with
60+
Valid values are "HTTP", "GRPC", "TLS"
61+
enum:
62+
- HTTP
63+
- GRPC
64+
- TLS
65+
type: string
66+
required:
67+
- port
68+
- routeType
69+
type: object
70+
type: array
71+
type: object
4172
status:
4273
description: |-
4374
status describes the current state of an exported service.

pkg/apis/applicationnetworking/v1alpha1/serviceexport_types.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type ServiceExport struct {
3030
apimachineryv1.TypeMeta `json:",inline"`
3131
// +optional
3232
apimachineryv1.ObjectMeta `json:"metadata,omitempty"`
33+
// spec defines the desired state of ServiceExport
34+
// +optional
35+
Spec ServiceExportSpec `json:"spec,omitempty"`
3336
// status describes the current state of an exported service.
3437
// Service configuration comes from the Service that had the same
3538
// name and namespace as this ServiceExport.
@@ -38,6 +41,25 @@ type ServiceExport struct {
3841
Status ServiceExportStatus `json:"status,omitempty"`
3942
}
4043

44+
// ServiceExportSpec defines the desired state of ServiceExport
45+
type ServiceExportSpec struct {
46+
// exportedPorts defines which ports of the service should be exported and what route types they should be used with.
47+
// If not specified, the controller will use the port from the annotation "application-networking.k8s.aws/port"
48+
// and create HTTP target groups for backward compatibility.
49+
// +optional
50+
ExportedPorts []ExportedPort `json:"exportedPorts,omitempty"`
51+
}
52+
53+
// ExportedPort defines a port to be exported and the route type it should be used with
54+
type ExportedPort struct {
55+
// port is the port number to export
56+
Port int32 `json:"port"`
57+
// routeType is the type of route this port should be used with
58+
// Valid values are "HTTP", "GRPC", "TLS"
59+
// +kubebuilder:validation:Enum=HTTP;GRPC;TLS
60+
RouteType string `json:"routeType"`
61+
}
62+
4163
// ServiceExportStatus contains the current status of an export.
4264
type ServiceExportStatus struct {
4365
// +optional

pkg/gateway/model_build_targetgroup.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,40 @@ func (b *SvcExportTargetGroupBuilder) BuildTargetGroup(ctx context.Context, svcE
9898
tgp: policy.NewTargetGroupPolicyHandler(b.log, b.client),
9999
}
100100

101+
// If exportedPorts is defined, we need to handle it differently
102+
// For now, we'll just return the first target group for backward compatibility
103+
// This is used for reconciliation of existing target groups
104+
if len(svcExport.Spec.ExportedPorts) > 0 {
105+
return task.buildTargetGroupForExportedPort(ctx, svcExport.Spec.ExportedPorts[0])
106+
}
107+
101108
return task.buildTargetGroup(ctx)
102109
}
103110

104111
func (t *svcExportTargetGroupModelBuildTask) run(ctx context.Context) error {
112+
// Check if we have exportedPorts defined in the spec
113+
if len(t.serviceExport.Spec.ExportedPorts) > 0 {
114+
// Create target groups for each exported port
115+
for _, exportedPort := range t.serviceExport.Spec.ExportedPorts {
116+
tg, err := t.buildTargetGroupForExportedPort(ctx, exportedPort)
117+
if err != nil {
118+
return fmt.Errorf("failed to build target group for service export %s-%s port %d due to %w",
119+
t.serviceExport.Name, t.serviceExport.Namespace, exportedPort.Port, err)
120+
}
121+
122+
if !tg.IsDeleted {
123+
err = t.buildTargetsForPort(ctx, tg.ID(), exportedPort.Port)
124+
if err != nil {
125+
t.log.Debugf(ctx, "Failed to build targets for service export %s-%s port %d due to %s",
126+
t.serviceExport.Name, t.serviceExport.Namespace, exportedPort.Port, err)
127+
return err
128+
}
129+
}
130+
}
131+
return nil
132+
}
133+
134+
// Fall back to legacy behavior if no exportedPorts are defined
105135
tg, err := t.buildTargetGroup(ctx)
106136
if err != nil {
107137
return fmt.Errorf("failed to build target group for service export %s-%s due to %w",
@@ -129,6 +159,106 @@ func (t *svcExportTargetGroupModelBuildTask) buildTargets(ctx context.Context, s
129159
return nil
130160
}
131161

162+
func (t *svcExportTargetGroupModelBuildTask) buildTargetGroupForExportedPort(ctx context.Context, exportedPort anv1alpha1.ExportedPort) (*model.TargetGroup, error) {
163+
svc := &corev1.Service{}
164+
noSvcFoundAndDeleting := false
165+
if err := t.client.Get(ctx, k8s.NamespacedName(t.serviceExport), svc); err != nil {
166+
if apierrors.IsNotFound(err) && !t.serviceExport.DeletionTimestamp.IsZero() {
167+
// if we're deleting, it's OK if the service isn't there
168+
noSvcFoundAndDeleting = true
169+
} else { // either it's some other error or we aren't deleting
170+
return nil, fmt.Errorf("failed to find corresponding k8sService %s, error :%w ",
171+
k8s.NamespacedName(t.serviceExport), err)
172+
}
173+
}
174+
175+
var ipAddressType string
176+
var err error
177+
if noSvcFoundAndDeleting {
178+
ipAddressType = "IPV4" // just pick a default
179+
} else {
180+
ipAddressType, err = buildTargetGroupIpAddressType(svc)
181+
if err != nil {
182+
return nil, err
183+
}
184+
}
185+
186+
tgp, err := t.tgp.ObjResolvedPolicy(ctx, t.serviceExport)
187+
if err != nil {
188+
return nil, err
189+
}
190+
191+
// Get health check config from policy
192+
_, _, healthCheckConfig, err := parseTargetGroupConfig(tgp)
193+
if err != nil {
194+
return nil, err
195+
}
196+
197+
// Set protocol and protocolVersion based on routeType
198+
var protocol, protocolVersion string
199+
switch exportedPort.RouteType {
200+
case "HTTP":
201+
protocol = vpclattice.TargetGroupProtocolHttp
202+
protocolVersion = vpclattice.TargetGroupProtocolVersionHttp1
203+
case "GRPC":
204+
protocol = vpclattice.TargetGroupProtocolHttp
205+
protocolVersion = vpclattice.TargetGroupProtocolVersionGrpc
206+
case "TLS":
207+
protocol = vpclattice.TargetGroupProtocolTcp
208+
protocolVersion = ""
209+
default:
210+
return nil, fmt.Errorf("unsupported route type: %s", exportedPort.RouteType)
211+
}
212+
213+
spec := model.TargetGroupSpec{
214+
Type: model.TargetGroupTypeIP,
215+
Port: exportedPort.Port,
216+
Protocol: protocol,
217+
ProtocolVersion: protocolVersion,
218+
IpAddressType: ipAddressType,
219+
HealthCheckConfig: healthCheckConfig,
220+
}
221+
spec.VpcId = config.VpcID
222+
spec.K8SSourceType = model.SourceTypeSvcExport
223+
spec.K8SClusterName = config.ClusterName
224+
spec.K8SServiceName = t.serviceExport.Name
225+
spec.K8SServiceNamespace = t.serviceExport.Namespace
226+
spec.K8SProtocolVersion = protocolVersion
227+
228+
// Add a tag for the route type to help with identification
229+
// This is not used by the controller but can be helpful for debugging
230+
if exportedPort.RouteType != "" {
231+
spec.K8SProtocolVersion = exportedPort.RouteType
232+
}
233+
234+
stackTG, err := model.NewTargetGroup(t.stack, spec)
235+
if err != nil {
236+
return nil, err
237+
}
238+
239+
stackTG.IsDeleted = !t.serviceExport.DeletionTimestamp.IsZero()
240+
return stackTG, nil
241+
}
242+
243+
func (t *svcExportTargetGroupModelBuildTask) buildTargetsForPort(ctx context.Context, stackTgId string, port int32) error {
244+
// This is similar to buildTargets but filters endpoints by the specified port
245+
targetsBuilder := NewTargetsBuilder(t.log, t.client, t.stack)
246+
247+
// We need to create a modified ServiceExport with the port annotation set to the specific port
248+
// This is a bit of a hack, but it allows us to reuse the existing BuildForServiceExport method
249+
modifiedServiceExport := t.serviceExport.DeepCopy()
250+
if modifiedServiceExport.Annotations == nil {
251+
modifiedServiceExport.Annotations = make(map[string]string)
252+
}
253+
modifiedServiceExport.Annotations[portAnnotationsKey] = fmt.Sprintf("%d", port)
254+
255+
_, err := targetsBuilder.BuildForServiceExport(ctx, modifiedServiceExport, stackTgId)
256+
if err != nil {
257+
return err
258+
}
259+
return nil
260+
}
261+
132262
func (t *svcExportTargetGroupModelBuildTask) buildTargetGroup(ctx context.Context) (*model.TargetGroup, error) {
133263
svc := &corev1.Service{}
134264
noSvcFoundAndDeleting := false

0 commit comments

Comments
 (0)