Skip to content

Commit c59d7c0

Browse files
authored
Merge pull request #936 from 08volt/annotations
Add Resource Annotations to L4 LB Service
2 parents 7d7d65e + f910ad0 commit c59d7c0

File tree

10 files changed

+340
-84
lines changed

10 files changed

+340
-84
lines changed

cmd/cloud-controller-manager/main.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ var enableDiscretePortForwarding bool
6767
// LoadBalancerClass
6868
var enableRBSDefaultForL4NetLB bool
6969

70+
// enableL4LBAnnotations is bound to a command-line flag. It enables
71+
// the controller to write annotations related to the provisioned resources
72+
// for L4 Load Balancers services
73+
var enableL4LBAnnotations bool
74+
7075
func main() {
7176
rand.Seed(time.Now().UnixNano())
7277

@@ -85,6 +90,7 @@ func main() {
8590
cloudProviderFS.BoolVar(&enableMultiProject, "enable-multi-project", false, "Enables project selection from Node providerID for GCE API calls. CAUTION: Only enable if Node providerID is configured by a trusted source.")
8691
cloudProviderFS.BoolVar(&enableDiscretePortForwarding, "enable-discrete-port-forwarding", false, "Enables forwarding of individual ports instead of port ranges for GCE external load balancers.")
8792
cloudProviderFS.BoolVar(&enableRBSDefaultForL4NetLB, "enable-rbs-default-l4-netlb", false, "Enables RBS defaulting for GCE L4 NetLB")
93+
cloudProviderFS.BoolVar(&enableL4LBAnnotations, "enable-l4-lb-annotations", false, "Enables Annotations for GCE L4 LB Services")
8894

8995
// add new controllers and initializers
9096
nodeIpamController := nodeIPAMController{}
@@ -165,12 +171,22 @@ func cloudInitializer(config *config.CompletedConfig) cloudprovider.Interface {
165171
if enableRBSDefaultForL4NetLB {
166172
gceCloud, ok := (cloud).(*gce.Cloud)
167173
if !ok {
168-
// Fail-fast: If enableRBSDefaultForGCEL4NetLB is set, the cloud
174+
// Fail-fast: If enableRBSDefaultForL4NetLB is set, the cloud
169175
// provider MUST be GCE.
170176
klog.Fatalf("enable-rbs-default-l4-netlb requires GCE cloud provider, but got %T", cloud)
171177
}
172178
gceCloud.SetEnableRBSDefaultForL4NetLB(true)
173179
}
174180

181+
if enableL4LBAnnotations {
182+
gceCloud, ok := (cloud).(*gce.Cloud)
183+
if !ok {
184+
// Fail-fast: If enableL4LBAnnotations is set, the cloud
185+
// provider MUST be GCE.
186+
klog.Fatalf("enable-l4-lb-annotations requires GCE cloud provider, but got %T", cloud)
187+
}
188+
gceCloud.SetEnableL4LBAnnotations(true)
189+
}
190+
175191
return cloud
176192
}

providers/gce/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ go_library(
6868
"//vendor/k8s.io/apimachinery/pkg/types",
6969
"//vendor/k8s.io/apimachinery/pkg/util/errors",
7070
"//vendor/k8s.io/apimachinery/pkg/util/sets",
71+
"//vendor/k8s.io/apimachinery/pkg/util/strategicpatch",
7172
"//vendor/k8s.io/apimachinery/pkg/util/wait",
7273
"//vendor/k8s.io/apimachinery/pkg/watch",
7374
"//vendor/k8s.io/client-go/applyconfigurations/core/v1:core",

providers/gce/gce.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ type Cloud struct {
209209

210210
// enableRBSDefaultForL4NetLB disable Service controller from picking up services by default
211211
enableRBSDefaultForL4NetLB bool
212+
213+
// enableL4LBAnnotations enable annotations related to provisioned resources in GCE
214+
enableL4LBAnnotations bool
212215
}
213216

214217
// ConfigGlobal is the in memory representation of the gce.conf config data
@@ -870,6 +873,10 @@ func (g *Cloud) SetEnableRBSDefaultForL4NetLB(enabled bool) {
870873
g.enableRBSDefaultForL4NetLB = enabled
871874
}
872875

876+
func (g *Cloud) SetEnableL4LBAnnotations(enabled bool) {
877+
g.enableL4LBAnnotations = enabled
878+
}
879+
873880
// getProjectsBasePath returns the compute API endpoint with the `projects/` element.
874881
// The suffix must be added when generating compute resource urls.
875882
func getProjectsBasePath(basePath string) string {

providers/gce/gce_annotations.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,28 @@ const (
8989

9090
// RBSEnabled is an annotation to indicate the Service is opt-in for RBS
9191
RBSEnabled = "enabled"
92+
93+
// serviceStatusPrefix is the prefix used in annotations used to record
94+
// debug information in the Service annotations. This is applicable to L4 LB services.
95+
serviceStatusPrefix = "networking.gke.io"
96+
97+
backendServiceResource = "backend-service"
98+
targetPoolResource = "target-pool"
99+
100+
// backendServiceKey is the annotation key used by l4 controller to record
101+
// GCP Backend service name.
102+
backendServiceKey = serviceStatusPrefix + "/" + backendServiceResource
103+
104+
// targetPoolKey is the annotation key used by l4 controller to record
105+
// GCP Target pool name.
106+
targetPoolKey = serviceStatusPrefix + "/" + targetPoolResource
92107
)
93108

109+
var l4ResourceAnnotationKeys = []string{
110+
backendServiceKey,
111+
targetPoolKey,
112+
}
113+
94114
// GetLoadBalancerAnnotationType returns the type of GCP load balancer which should be assembled.
95115
func GetLoadBalancerAnnotationType(service *v1.Service) LoadBalancerType {
96116
var lbType LoadBalancerType
@@ -176,3 +196,16 @@ func GetLoadBalancerAnnotationSubnet(service *v1.Service) string {
176196
}
177197
return ""
178198
}
199+
200+
// mergeMap merges the update map into the dst map.
201+
// Keys in dst are overwritten by values from update.
202+
// If a value in the update map is an empty string, the key is removed from dst.
203+
func mergeMap(dst, update map[string]string) {
204+
for k, v := range update {
205+
if v == "" {
206+
delete(dst, k)
207+
} else {
208+
dst[k] = v
209+
}
210+
}
211+
}

providers/gce/gce_annotations_test.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"testing"
2424

2525
"github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud"
26-
"k8s.io/api/core/v1"
26+
v1 "k8s.io/api/core/v1"
2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2828

2929
"github.com/stretchr/testify/assert"
@@ -72,3 +72,57 @@ func TestServiceNetworkTierAnnotationKey(t *testing.T) {
7272
})
7373
}
7474
}
75+
76+
func TestMergeMap(t *testing.T) {
77+
for _, tc := range []struct {
78+
desc string
79+
existing map[string]string
80+
updates map[string]string
81+
expectedResult map[string]string
82+
}{
83+
{
84+
desc: "new annotations should be added",
85+
existing: map[string]string{"key1": "val1"},
86+
updates: map[string]string{"key2": "val2"},
87+
expectedResult: map[string]string{
88+
"key1": "val1",
89+
"key2": "val2",
90+
},
91+
},
92+
{
93+
desc: "existing annotations should be overwritten",
94+
existing: map[string]string{"key1": "val1"},
95+
updates: map[string]string{"key1": "val2"},
96+
expectedResult: map[string]string{
97+
"key1": "val2",
98+
},
99+
},
100+
{
101+
desc: "empty value in updates should delete the key",
102+
existing: map[string]string{"key1": "val1", "key2": "val2"},
103+
updates: map[string]string{"key1": ""},
104+
expectedResult: map[string]string{
105+
"key2": "val2",
106+
},
107+
},
108+
{
109+
desc: "mixed updates and deletions",
110+
existing: map[string]string{"key1": "val1", "key2": "val2"},
111+
updates: map[string]string{"key1": "new-val", "key2": ""},
112+
expectedResult: map[string]string{
113+
"key1": "new-val",
114+
},
115+
},
116+
{
117+
desc: "empty updates should result in no change",
118+
existing: map[string]string{"key1": "val1"},
119+
updates: map[string]string{},
120+
expectedResult: map[string]string{"key1": "val1"},
121+
},
122+
} {
123+
t.Run(tc.desc, func(t *testing.T) {
124+
mergeMap(tc.existing, tc.updates)
125+
assert.Equal(t, tc.expectedResult, tc.existing)
126+
})
127+
}
128+
}

providers/gce/gce_loadbalancer.go

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ package gce
2121

2222
import (
2323
"context"
24+
"encoding/json"
2425
"flag"
2526
"fmt"
27+
"reflect"
2628
"sort"
2729
"strings"
2830

2931
v1 "k8s.io/api/core/v1"
3032
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
"k8s.io/apimachinery/pkg/types"
34+
"k8s.io/apimachinery/pkg/util/strategicpatch"
3135
corev1apply "k8s.io/client-go/applyconfigurations/core/v1"
3236
metav1apply "k8s.io/client-go/applyconfigurations/meta/v1"
3337
"k8s.io/klog/v2"
@@ -42,6 +46,23 @@ type cidrs struct {
4246
isSet bool
4347
}
4448

49+
type lbSyncResult struct {
50+
status *v1.LoadBalancerStatus
51+
annotations map[string]string
52+
}
53+
54+
func newLBSyncResult() *lbSyncResult {
55+
annotations := make(map[string]string, len(l4ResourceAnnotationKeys))
56+
57+
for _, key := range l4ResourceAnnotationKeys {
58+
annotations[key] = "" // Initialize to empty string to indicate deletion by default if not set later
59+
}
60+
61+
return &lbSyncResult{
62+
annotations: annotations,
63+
}
64+
}
65+
4566
var (
4667
l4LbSrcRngsFlag cidrs
4768
l7lbSrcRngsFlag cidrs
@@ -197,19 +218,63 @@ func (g *Cloud) EnsureLoadBalancer(ctx context.Context, clusterName string, svc
197218
}
198219
}
199220

200-
var status *v1.LoadBalancerStatus
221+
var syncResult *lbSyncResult
201222
switch desiredScheme {
202223
case cloud.SchemeInternal:
203-
status, err = g.ensureInternalLoadBalancer(clusterName, clusterID, svc, existingFwdRule, nodes)
224+
syncResult, err = g.ensureInternalLoadBalancer(clusterName, clusterID, svc, existingFwdRule, nodes)
204225
default:
205-
status, err = g.ensureExternalLoadBalancer(clusterName, clusterID, svc, existingFwdRule, nodes)
226+
syncResult, err = g.ensureExternalLoadBalancer(clusterName, clusterID, svc, existingFwdRule, nodes)
206227
}
207228
if err != nil {
208229
klog.Errorf("Failed to EnsureLoadBalancer(%s, %s, %s, %s, %s), err: %v", clusterName, svc.Namespace, svc.Name, loadBalancerName, g.region, err)
209-
return status, err
230+
return syncResult.status, err
210231
}
232+
233+
if g.enableL4LBAnnotations {
234+
if err = g.updateL4ResourcesAnnotations(ctx, svc, syncResult.annotations); err != nil {
235+
return syncResult.status, fmt.Errorf("failed to set resource annotations, err: %w", err)
236+
}
237+
}
238+
211239
klog.V(4).Infof("EnsureLoadBalancer(%s, %s, %s, %s, %s): done ensuring loadbalancer.", clusterName, svc.Namespace, svc.Name, loadBalancerName, g.region)
212-
return status, err
240+
return syncResult.status, err
241+
}
242+
243+
func (g *Cloud) updateL4ResourcesAnnotations(ctx context.Context, svc *v1.Service, newL4LBAnnotations map[string]string) error {
244+
newObjectMetadata, shouldUpdate := computeNewAnnotationsIfNeeded(svc, newL4LBAnnotations)
245+
if !shouldUpdate {
246+
return nil
247+
}
248+
newSvc := svc.DeepCopy()
249+
newSvc.ObjectMeta = *newObjectMetadata
250+
251+
patchBytes, err := servicePatchBytes(svc, newSvc)
252+
if err != nil {
253+
return err
254+
}
255+
256+
_, err = g.client.CoreV1().Services(svc.Namespace).Patch(ctx, svc.Name, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}, "status")
257+
258+
return err
259+
}
260+
261+
func servicePatchBytes(oldSvc, newSvc *v1.Service) ([]byte, error) {
262+
oldData, err := json.Marshal(oldSvc)
263+
if err != nil {
264+
return nil, fmt.Errorf("failed to Marshal oldData for svc %s/%s: %v", oldSvc.Namespace, oldSvc.Name, err)
265+
}
266+
267+
newData, err := json.Marshal(newSvc)
268+
if err != nil {
269+
return nil, fmt.Errorf("failed to Marshal newData for svc %s/%s: %v", newSvc.Namespace, newSvc.Name, err)
270+
}
271+
272+
patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, v1.Service{})
273+
if err != nil {
274+
return nil, fmt.Errorf("failed to CreateTwoWayMergePatch for svc %s/%s: %v", oldSvc.Namespace, oldSvc.Name, err)
275+
}
276+
return patchBytes, nil
277+
213278
}
214279

215280
// UpdateLoadBalancer is an implementation of LoadBalancer.UpdateLoadBalancer.
@@ -326,3 +391,15 @@ func hasLoadBalancerPortsError(service *v1.Service) bool {
326391
}
327392
return false
328393
}
394+
395+
// computeNewAnnotationsIfNeeded checks if new annotations should be added to service.
396+
// If needed creates new service meta object.
397+
// This function is used by L4 LB controllers.
398+
func computeNewAnnotationsIfNeeded(svc *v1.Service, newAnnotations map[string]string) (*metav1.ObjectMeta, bool) {
399+
newObjectMeta := svc.ObjectMeta.DeepCopy()
400+
mergeMap(newObjectMeta.Annotations, newAnnotations)
401+
if reflect.DeepEqual(svc.Annotations, newObjectMeta.Annotations) {
402+
return nil, false
403+
}
404+
return newObjectMeta, true
405+
}

providers/gce/gce_loadbalancer_external.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ const (
5555
// Due to an interesting series of design decisions, this handles both creating
5656
// new load balancers and updating existing load balancers, recognizing when
5757
// each is needed.
58-
func (g *Cloud) ensureExternalLoadBalancer(clusterName string, clusterID string, apiService *v1.Service, existingFwdRule *compute.ForwardingRule, nodes []*v1.Node) (*v1.LoadBalancerStatus, error) {
58+
func (g *Cloud) ensureExternalLoadBalancer(clusterName string, clusterID string, apiService *v1.Service, existingFwdRule *compute.ForwardingRule, nodes []*v1.Node) (*lbSyncResult, error) {
59+
syncResult := newLBSyncResult()
60+
5961
// Process services with LoadBalancerClass "networking.gke.io/l4-regional-external-legacy" used for this controller.
6062
// LoadBalancerClass can't be updated so we know this controller should process the NetLB.
6163
// Skip service handling if it uses Regional Backend Services and handled by other controllers
@@ -282,6 +284,7 @@ func (g *Cloud) ensureExternalLoadBalancer(clusterName string, clusterID string,
282284
if err := g.ensureTargetPoolAndHealthCheck(tpExists, tpNeedsRecreation, apiService, loadBalancerName, clusterID, ipAddressToUse, hosts, hcToCreate, hcToDelete); err != nil {
283285
return nil, err
284286
}
287+
syncResult.annotations[targetPoolKey] = loadBalancerName
285288

286289
if tpNeedsRecreation || fwdRuleNeedsUpdate {
287290
klog.Infof("ensureExternalLoadBalancer(%s): Creating forwarding rule, IP %s (tier: %s).", lbRefStr, ipAddressToUse, netTier)
@@ -299,7 +302,8 @@ func (g *Cloud) ensureExternalLoadBalancer(clusterName string, clusterID string,
299302
status := &v1.LoadBalancerStatus{}
300303
status.Ingress = []v1.LoadBalancerIngress{{IP: ipAddressToUse}}
301304

302-
return status, nil
305+
syncResult.status = status
306+
return syncResult, nil
303307
}
304308

305309
// updateExternalLoadBalancer is the external implementation of LoadBalancer.UpdateLoadBalancer.

0 commit comments

Comments
 (0)