Skip to content

Commit 57ed9d7

Browse files
committed
feat(kubernetes): Allow adding custom labels to worker pods
Add a config property for the `KubernetesSenderConfig` to allow adding custom labels for the created jobs. Signed-off-by: Martin Nonnenmacher <martin.nonnenmacher@doubleopen.org>
1 parent 05fdd5b commit 57ed9d7

File tree

5 files changed

+115
-4
lines changed

5 files changed

+115
-4
lines changed

transport/kubernetes/src/main/kotlin/KubernetesMessageSender.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ internal class KubernetesMessageSender<T : Any>(
9999

100100
val msgConfig = config.forMessage(message)
101101
val envVars = createEnvironment()
102-
val labels = createTraceIdLabels(traceId) + mapOf(
102+
val labels = config.labels + createTraceIdLabels(traceId) + mapOf(
103103
RUN_ID_LABEL to message.header.ortRunId.toString(),
104104
WORKER_LABEL to endpoint.configPrefix
105105
)

transport/kubernetes/src/main/kotlin/KubernetesSenderConfig.kt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ data class KubernetesSenderConfig(
100100
/** A list with [PvcVolumeMount]s to be added to the new pod. */
101101
val pvcVolumes: List<PvcVolumeMount> = emptyList(),
102102

103+
/**
104+
* A list of labels to add to the new pod. The labels `ort-worker`, `run-id` and any `trace-id-*` are inserted
105+
* automatically by the Kubernetes sender and cannot be overridden; matching entries in this map are ignored.
106+
*/
107+
val labels: Map<String, String> = emptyMap(),
108+
103109
/**
104110
* A map with annotations to be added to the new pod. The map defines the keys and values of annotation.
105111
* Corresponding annotations are added to the _template_ section of newly created jobs, so that they are present
@@ -182,6 +188,14 @@ data class KubernetesSenderConfig(
182188
*/
183189
private const val MOUNT_PVCS_PROPERTY = "mountPvcs"
184190

191+
/**
192+
* The name of the configuration property defining the labels to add to new pods. The value of this property is
193+
* interpreted as a comma-separated list of labels, ignoring whitespace around the labels. Each label must have
194+
* the format _key=value_. The labels `ort-worker`, `run-id` and any `trace-id-*` are inserted automatically by
195+
* the Kubernetes sender and cannot be overridden; matching entries in this list are ignored.
196+
*/
197+
private const val LABELS_PROPERTY = "labels"
198+
185199
/**
186200
* The name of the configuration property that allows defining annotations based on environment variables.
187201
* If available, the value of this property is interpreted as a comma-separated list of environment variable
@@ -249,6 +263,9 @@ data class KubernetesSenderConfig(
249263
/** A regular expression to parse a PVC-based volume mount declaration. */
250264
private val mountPvcDeclarationRegex = Regex("""(\S+)\s*->\s*([^,]+),([RrWw])""")
251265

266+
/** A regular expression to split the list of labels. */
267+
private val splitLabelsRegex = splitRegex(LIST_SEPARATOR)
268+
252269
/** A regular expression to split the list with environment variables defining annotations. */
253270
private val splitAnnotationVariablesRegex = splitRegex(LIST_SEPARATOR)
254271

@@ -270,6 +287,7 @@ data class KubernetesSenderConfig(
270287
args = config.getStringOrDefault(ARGS_PROPERTY, "").splitAtWhitespace(),
271288
secretVolumes = config.parseSecretVolumeMounts(),
272289
pvcVolumes = config.parsePvcVolumeMounts(),
290+
labels = createLabels(config.getStringOrDefault(LABELS_PROPERTY, "")),
273291
annotations = createAnnotations(config.getStringOrDefault(ANNOTATIONS_VARIABLES_PROPERTY, "")),
274292
serviceAccountName = config.getStringOrNull(SERVICE_ACCOUNT_PROPERTY),
275293
enableDebugLogging = config.getBooleanOrDefault(ENABLE_DEBUG_LOGGING_PROPERTY, false),
@@ -329,6 +347,36 @@ data class KubernetesSenderConfig(
329347
PvcVolumeMount(claimName, mountPath, readOnly.lowercase() == "r")
330348
}
331349

350+
/**
351+
* Create the map with labels based on the given [labels]. Extract the labels from the comma-delimited string,
352+
* ignoring whitespace around the key value pairs. Each label must have the format _key=value_. Ignore invalid
353+
* labels, but log a warning for them. Also ignore labels with reserved keys, but log a warning for them as
354+
* well.
355+
*/
356+
private fun createLabels(labels: String): Map<String, String> {
357+
if (labels.isEmpty()) return emptyMap()
358+
359+
val reservedLabels = listOf("ort-worker", "run-id")
360+
361+
return labels.split(splitLabelsRegex).mapNotNull { label ->
362+
val keyValue = label.split(splitKeyValueRegex, limit = 2)
363+
if (keyValue.size != 2 || keyValue[0].isEmpty() || keyValue[1].isEmpty()) {
364+
logger.warn("Ignore invalid label declaration: '$label'. Labels must have the format 'key=value'.")
365+
return@mapNotNull null
366+
}
367+
368+
if (keyValue[0] in reservedLabels || keyValue[0].startsWith("trace-id-")) {
369+
logger.warn(
370+
"Ignore label with reserved key '${keyValue[0]}'. The keys 'ort-worker', 'run-id' and " +
371+
"'trace-id-*' are reserved and cannot be used in custom labels."
372+
)
373+
return@mapNotNull null
374+
}
375+
376+
keyValue[0] to keyValue[1]
377+
}.toMap()
378+
}
379+
332380
/**
333381
* Create the map with annotations based on the given string with [variableNames]. Extract the names from the
334382
* comma-delimited string. Look up the values of the referenced variables and extract the keys and values of

transport/kubernetes/src/test/kotlin/KubernetesMessageSenderFactoryTest.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ private const val ARGS = "run \"all tests\" fast"
4747
private const val SECRET_MOUNTS =
4848
"secret1->/mnt/sec1|sub1 \"secret2->/path/with/white space\" \"secret3 -> /mnt/other | sub2\""
4949
private const val PVC_MOUNTS = "pvc1->/mnt/pvc1,R \"pvc2->/path/with/white space,W\" \"pvc3 -> /mnt/other,r\""
50+
private const val LABELS = "label1=value1 , label2 = value2"
5051
private const val SERVICE_ACCOUNT = "test_service_account"
5152

5253
private val annotationVariables = mapOf(
@@ -74,6 +75,7 @@ class KubernetesMessageSenderFactoryTest : StringSpec({
7475
"$keyPrefix.enableDebugLogging" to "true",
7576
"$keyPrefix.mountSecrets" to SECRET_MOUNTS,
7677
"$keyPrefix.mountPvcs" to PVC_MOUNTS,
78+
"$keyPrefix.labels" to LABELS,
7779
"$keyPrefix.annotationVariables" to annotationVariables.keys.joinToString(),
7880
"$keyPrefix.serviceAccount" to SERVICE_ACCOUNT
7981
)
@@ -104,6 +106,10 @@ class KubernetesMessageSenderFactoryTest : StringSpec({
104106
PvcVolumeMount("pvc2", "/path/with/white space", readOnly = false),
105107
PvcVolumeMount("pvc3", "/mnt/other", readOnly = true)
106108
)
109+
labels shouldContainExactly mapOf(
110+
"label1" to "value1",
111+
"label2" to "value2"
112+
)
107113
annotations shouldContainExactly mapOf(
108114
"ort-server.org/test" to "true",
109115
"ort-server.org/performance" to "fast"
@@ -137,6 +143,7 @@ class KubernetesMessageSenderFactoryTest : StringSpec({
137143
restartPolicy shouldBe "OnFailure"
138144
imagePullSecret should beNull()
139145
secretVolumes should beEmpty()
146+
labels.keys should beEmpty()
140147
annotations.keys should beEmpty()
141148
serviceAccountName should beNull()
142149
enableDebugLogging shouldBe false
@@ -186,6 +193,46 @@ class KubernetesMessageSenderFactoryTest : StringSpec({
186193
)
187194
}
188195

196+
"Invalid labels are ignored" {
197+
val keyPrefix = "analyzer.sender"
198+
val labels = "label1=value1 , invalid label, label2 = value2, =invalid, invalid="
199+
val configMap = mapOf(
200+
"$keyPrefix.type" to KubernetesSenderConfig.TRANSPORT_NAME,
201+
"$keyPrefix.namespace" to NAMESPACE,
202+
"$keyPrefix.imageName" to IMAGE_NAME,
203+
"$keyPrefix.labels" to labels
204+
)
205+
val configManager = ConfigManager.create(ConfigFactory.parseMap(configMap))
206+
207+
val sender = MessageSenderFactory.createSender(AnalyzerEndpoint, configManager)
208+
209+
sender.shouldBeTypeOf<KubernetesMessageSender<AnalyzerEndpoint>>()
210+
sender.config.labels shouldContainExactly mapOf(
211+
"label1" to "value1",
212+
"label2" to "value2"
213+
)
214+
}
215+
216+
"Reserved labels are ignored" {
217+
val keyPrefix = "analyzer.sender"
218+
val labels = "label1=value1,label2=value2,ort-worker=invalid,run-id=invalid,trace-id-0=invalid"
219+
val configMap = mapOf(
220+
"$keyPrefix.type" to KubernetesSenderConfig.TRANSPORT_NAME,
221+
"$keyPrefix.namespace" to NAMESPACE,
222+
"$keyPrefix.imageName" to IMAGE_NAME,
223+
"$keyPrefix.labels" to labels
224+
)
225+
val configManager = ConfigManager.create(ConfigFactory.parseMap(configMap))
226+
227+
val sender = MessageSenderFactory.createSender(AnalyzerEndpoint, configManager)
228+
229+
sender.shouldBeTypeOf<KubernetesMessageSender<AnalyzerEndpoint>>()
230+
sender.config.labels shouldContainExactly mapOf(
231+
"label1" to "value1",
232+
"label2" to "value2"
233+
)
234+
}
235+
189236
"Invalid variables defining annotations are ignored" {
190237
val keyPrefix = "analyzer.sender"
191238
val validVariable = "validVariable"

transport/kubernetes/src/test/kotlin/KubernetesMessageSenderTest.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,17 @@ class KubernetesMessageSenderTest : StringSpec({
119119

120120
verifyLabels(
121121
actualLabels = job.metadata?.labels.orEmpty(),
122-
expectedRunId = message.header.ortRunId
122+
expectedRunId = message.header.ortRunId,
123+
customLabels = senderConfig.labels
123124
)
124125

125126
val jobAnnotations = job.spec?.template?.metadata?.annotations.orEmpty()
126127
jobAnnotations shouldBe annotations
127128

128129
verifyLabels(
129130
actualLabels = job.spec?.template?.metadata?.labels.orEmpty(),
130-
expectedRunId = message.header.ortRunId
131+
expectedRunId = message.header.ortRunId,
132+
customLabels = senderConfig.labels
131133
)
132134
}
133135

@@ -278,6 +280,7 @@ private fun createConfig(vararg overrides: Pair<String, String>): KubernetesSend
278280
"imagePullSecret" to "image_pull_secret",
279281
"mountSecrets" to "secretService->/mnt/secret topSecret->/mnt/top/secret|sub-secret",
280282
"mountPvcs" to "pvc1->/mnt/readOnly,R pvc2->/mnt/data,W",
283+
"labels" to "label1=value1,label2=value2,ort-worker=invalid,run-id=invalid,trace-id-0=invalid",
281284
"annotationVariables" to "v1,v2",
282285
"serviceAccountName" to "test_service_account"
283286
)
@@ -296,12 +299,16 @@ private fun createConfig(vararg overrides: Pair<String, String>): KubernetesSend
296299
private fun messageWithProperties(vararg properties: Pair<String, String>): Message<AnalyzerRequest> =
297300
message.copy(header = header.copy(transportProperties = mapOf(*properties)))
298301

299-
private fun verifyLabels(actualLabels: Map<String, String>, expectedRunId: Long) {
302+
private fun verifyLabels(actualLabels: Map<String, String>, expectedRunId: Long, customLabels: Map<String, String>) {
300303
val traceIdFromLabels = (0..3).fold("") { id, idx ->
301304
id + actualLabels.getValue("trace-id-$idx")
302305
}
303306
traceIdFromLabels shouldBe traceId
304307

305308
actualLabels["run-id"] shouldBe expectedRunId.toString()
306309
actualLabels["ort-worker"] shouldBe "analyzer"
310+
311+
customLabels.forEach { (key, value) ->
312+
actualLabels[key] shouldBe value
313+
}
307314
}

website/docs/admin-guide/infrastructure/transport/kubernetes.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ endpoint {
2626
commands = "/bin/sh"
2727
args = "-c java my.pkg.MyClass"
2828
mountSecrets = "server-secrets->/mnt/secrets server-certificates->/mnt/certificates"
29+
labels = "label1=value1,label2=value2"
2930
annotationVariables = "ANNOTATION_VAR1, ANNOTATION_VAR2"
3031
serviceAccount = "my_service_account"
3132
cpuRequest = 250m
@@ -108,6 +109,14 @@ On this path, for each key of the secret a file is created whose content is the
108109
To achieve this, the Kubernetes Transport implementation generates corresponding `volume` and `volumeMount` declarations in the pod configuration.
109110
This mechanism is useful not only for secrets but also for other kinds of external data that should be accessible from a pod, for instance, custom certificates.
110111

112+
### `labels`
113+
114+
**Default: `empty`**
115+
116+
A comma-separated list of labels to be added to the job.
117+
Labels are key-value pairs and must have the form `key=value`.
118+
The labels `ort-worker`, `run-id` and any `trace-id-*` are inserted automatically by the Kubernetes sender and cannot be overridden; matching entries in this list are ignored.
119+
111120
### `annotationVariables`
112121

113122
**Default: `empty`**

0 commit comments

Comments
 (0)