@@ -6,13 +6,28 @@ import (
6
6
"maps"
7
7
"strings"
8
8
"time"
9
+ "unicode"
9
10
10
11
"github.com/prometheus/client_golang/prometheus"
11
12
log "github.com/sirupsen/logrus"
12
13
"google.golang.org/api/compute/v1"
13
14
"k8s.io/apimachinery/pkg/util/wait"
14
15
)
15
16
17
+ var gcpLabelCharReplacer = strings .NewReplacer (
18
+ // slash and dot are common, use different replacement chars:
19
+ "/" , "_" , // replace slashes with underscores
20
+ "." , "-" , // replace dots with dashes
21
+
22
+ // less common characters, replace with dashes:
23
+ " " , "-" , // replace spaces with dashes
24
+ ":" , "-" , // replace colons with dashes
25
+ "," , "-" , // replace commas with dashes
26
+ ";" , "-" , // replace semi-colons with dashes
27
+ "=" , "-" , // replace equals with dashes
28
+ "+" , "-" , // replace plus with dashes
29
+ )
30
+
16
31
type GCPClient interface {
17
32
GetDisk (project , zone , name string ) (* compute.Disk , error )
18
33
SetDiskLabels (project , zone , name string , labelReq * compute.ZoneSetLabelsRequest ) (* compute.Operation , error )
@@ -93,7 +108,6 @@ func addPDVolumeLabels(c GCPClient, volumeID string, labels map[string]string, s
93
108
false ,
94
109
waitForCompletion ); err != nil {
95
110
log .Errorf ("set label operation failed: %s" , err )
96
- promActionsTotal .With (prometheus.Labels {"status" : "error" , "storageclass" : storageclass }).Inc ()
97
111
return
98
112
}
99
113
@@ -145,7 +159,7 @@ func deletePDVolumeLabels(c GCPClient, volumeID string, keys []string, storagecl
145
159
waitForCompletion := func (_ context.Context ) (bool , error ) {
146
160
resp , err := c .GetGCEOp (project , location , op .Name )
147
161
if err != nil {
148
- return false , fmt .Errorf ("failed retrieve status of label update operation : %s" , err )
162
+ return false , fmt .Errorf ("failed to delete labels from PD %s : %s" , disk . Name , err )
149
163
}
150
164
return resp .Status == "DONE" , nil
151
165
}
@@ -154,7 +168,6 @@ func deletePDVolumeLabels(c GCPClient, volumeID string, keys []string, storagecl
154
168
time .Minute ,
155
169
false ,
156
170
waitForCompletion ); err != nil {
157
- promActionsTotal .With (prometheus.Labels {"status" : "error" , "storageclass" : storageclass }).Inc ()
158
171
log .Errorf ("delete label operation failed: %s" , err )
159
172
return
160
173
}
@@ -174,38 +187,106 @@ func parseVolumeID(id string) (string, string, string, error) {
174
187
return project , location , name , nil
175
188
}
176
189
177
- func sanitizeLabelsForGCP (labels map [string ]string ) map [string ]string {
178
- newLabels := make (map [string ]string , len (labels ))
179
- for k , v := range labels {
180
- newLabels [sanitizeKeyForGCP (k )] = sanitizeValueForGCP (v )
181
- }
182
- return newLabels
190
+ // isValidGCPChar returns true if the rune is valid for GCP labels:
191
+ // lowercase letters, numbers, dash, or underscore. International characters are
192
+ // allowed.
193
+ func isValidGCPChar (r rune ) bool {
194
+ return unicode .IsLetter (r ) || unicode .IsDigit (r ) || r == '-' || r == '_'
183
195
}
184
196
185
- func sanitizeKeysForGCP (keys []string ) []string {
186
- newKeys := make ([]string , len (keys ))
187
- for i , k := range keys {
188
- newKeys [i ] = sanitizeKeyForGCP (k )
197
+ // sanitizeGCPLabelComponent handles the common sanitization logic for both keys
198
+ // and values
199
+ func sanitizeGCPLabelComponent (s string , isKey bool ) string {
200
+ // Convert to lowercase
201
+ s = strings .ToLower (s )
202
+
203
+ // Replace invalid characters with dashes
204
+ s = gcpLabelCharReplacer .Replace (s )
205
+
206
+ // Filter to only valid characters
207
+ var b strings.Builder
208
+ for _ , r := range s {
209
+ if isValidGCPChar (r ) {
210
+ b .WriteRune (r )
211
+ }
212
+ }
213
+ s = b .String ()
214
+
215
+ // For keys, ensure they start with a letter
216
+ if isKey && len (s ) > 0 && ! unicode .IsLetter (rune (s [0 ])) {
217
+ s = "k" + s
218
+ }
219
+
220
+ // Remove consecutive dashes/underscores
221
+ for strings .Contains (s , "--" ) || strings .Contains (s , "__" ) {
222
+ s = strings .ReplaceAll (s , "--" , "-" )
223
+ s = strings .ReplaceAll (s , "__" , "_" )
189
224
}
190
- return newKeys
225
+
226
+ // Remove any trailing dashes or underscores
227
+ s = strings .TrimRight (s , "-_" )
228
+
229
+ // Truncate to maximum length
230
+ if len (s ) > 63 {
231
+ s = s [:63 ]
232
+ s = strings .TrimRight (s , "-_" )
233
+ }
234
+
235
+ return s
191
236
}
192
237
193
- // sanitizeKeyForGCP sanitizes a Kubernetes label key to fit GCP's label key constraints
238
+ // sanitizeKeyForGCP sanitizes a Kubernetes label key to fit GCP's label key constraints:
239
+ // - Must start with a lowercase letter or international character
240
+ // - Can only contain lowercase letters, numbers, dashes and underscores
241
+ // - Must be between 1 and 63 characters long
242
+ // - Must use UTF-8 encoding
194
243
func sanitizeKeyForGCP (key string ) string {
195
- key = strings .ToLower (key )
196
- key = strings .NewReplacer ("/" , "_" , "." , "-" ).Replace (key ) // Replace disallowed characters
197
- key = strings .TrimRight (key , "-_" ) // Ensure it does not end with '-' or '_'
244
+ return sanitizeGCPLabelComponent (key , true )
245
+ }
246
+
247
+ // sanitizeValueForGCP sanitizes a Kubernetes label value to fit GCP's label value constraints:
248
+ // - Can be empty
249
+ // - Maximum length of 63 characters
250
+ // - Can only contain lowercase letters, numbers, dashes and underscores
251
+ // - Must use UTF-8 encoding
252
+ func sanitizeValueForGCP (value string ) string {
253
+ return sanitizeGCPLabelComponent (value , false )
254
+ }
255
+
256
+ // sanitizeLabelsForGCP sanitizes a map of Kubernetes labels to fit GCP's constraints.
257
+ // Empty keys after sanitization are dropped from the result.
258
+ func sanitizeLabelsForGCP (labels map [string ]string ) map [string ]string {
259
+ if len (labels ) > 64 {
260
+ // If we have more than 64 labels, only take the first 64
261
+ truncatedLabels := make (map [string ]string , 64 )
262
+ i := 0
263
+ for k , v := range labels {
264
+ if i >= 64 {
265
+ break
266
+ }
267
+ truncatedLabels [k ] = v
268
+ i ++
269
+ }
270
+ labels = truncatedLabels
271
+ }
198
272
199
- if len (key ) > 63 {
200
- key = key [:63 ]
273
+ result := make (map [string ]string , len (labels ))
274
+ for k , v := range labels {
275
+ if sanitizedKey := sanitizeKeyForGCP (k ); sanitizedKey != "" {
276
+ result [sanitizedKey ] = sanitizeValueForGCP (v )
277
+ }
201
278
}
202
- return key
279
+ return result
203
280
}
204
281
205
- // sanitizeKeyForGCP sanitizes a Kubernetes label value to fit GCP's label value constraints
206
- func sanitizeValueForGCP (value string ) string {
207
- if len (value ) > 63 {
208
- value = value [:63 ]
282
+ // sanitizeKeysForGCP sanitizes a slice of label keys to fit GCP's constraints.
283
+ // Empty keys after sanitization are dropped from the result.
284
+ func sanitizeKeysForGCP (keys []string ) []string {
285
+ result := make ([]string , 0 , len (keys ))
286
+ for _ , k := range keys {
287
+ if sanitized := sanitizeKeyForGCP (k ); sanitized != "" {
288
+ result = append (result , sanitized )
289
+ }
209
290
}
210
- return value
291
+ return result
211
292
}
0 commit comments