From c2a0e20300c82ab3b3858281fc53820980bb3eb2 Mon Sep 17 00:00:00 2001 From: Yuto Nishida Date: Sat, 20 Jun 2026 17:33:52 -0700 Subject: [PATCH] [milky-way] Use associateBy --- .../stage00/orion-system/main.jsonnet | 24 ++++++------- milky-way/lib/calibre-web-automated.libsonnet | 21 +++++------ milky-way/lib/utils.libsonnet | 36 +++++++++++++++++++ 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/milky-way/environments/stage00/orion-system/main.jsonnet b/milky-way/environments/stage00/orion-system/main.jsonnet index 3959ff4..da5107b 100644 --- a/milky-way/environments/stage00/orion-system/main.jsonnet +++ b/milky-way/environments/stage00/orion-system/main.jsonnet @@ -214,7 +214,7 @@ local secrets = import 'milky-way/secrets/k8s-secret-values.jsonnet'; instances: { [sonarrOrionSystemInstanceName]: { hostname: utils.domainOfService(this.sonarr.service), - port: utils.assertAndReturn(this.sonarr.service.spec.ports[0], function(p) p.name == 'webui').port, + port: utils.associateObjectsByKey(this.sonarr.service.spec.ports, 'name')['webui'].port, protocol: 'http', api_key: secrets.sonarr.apiKey, settings: { @@ -224,7 +224,7 @@ local secrets = import 'milky-way/secrets/k8s-secret-values.jsonnet'; qBittorrent: { type: 'qbittorrent', host: utils.domainOfService(this.qbittorrent.service), - port: utils.assertAndReturn(this.qbittorrent.service.spec.ports[0], function(p) p.name == 'webui').port, + port: utils.associateObjectsByKey(this.qbittorrent.service.spec.ports, 'name')['webui'].port, // No username/password: qBittorrent's AuthSubnetWhitelist bypasses auth for // in-cluster callers (Sonarr is in the pod CIDR). See lib/qbittorrent.libsonnet. category: 'tv-sonarr', // qBittorrent category Sonarr tags its grabs with @@ -234,17 +234,15 @@ local secrets = import 'milky-way/secrets/k8s-secret-values.jsonnet'; media_management: { delete_unmanaged_root_folders: false, // also explicit per-instance (belt & suspenders) // These root folders are paths INSIDE the Sonarr container -- the `mdata` PVC, which - // Sonarr mounts at /data (matching qbittorrent so hardlinks stay on one fs). Pin - // volumeMounts[1] as the media mount, then assert that mount is /data, so a future - // mediaMountPath change (or a reordered/renamed mount) fails at evaluation instead of + // Sonarr mounts at /data (matching qbittorrent so hardlinks stay on one fs). Look up + // the media mount by name, then assert that mount is /data, so a future + // mediaMountPath change (or a renamed mount) fails at evaluation instead of // silently leaving these root folders pointing where Sonarr no longer mounts (its API // rejects a non-existent path). The paths below stay LITERAL on purpose -- they must // not auto-follow an accidental mountPath change. - local mediaMount = utils.assertAndReturn( - this.sonarr.deployment.spec.template.spec.containers[0].volumeMounts[1], - function(m) m.name == 'media', - 'sonarr volumeMounts[1] must be the media mount for these buildarr root_folders', - ), + local mediaMount = utils.associateObjectsByKey( + this.sonarr.deployment.spec.template.spec.containers[0].volumeMounts, 'name' + )['media'], assert mediaMount.mountPath == '/data' : 'sonarr media mount must be at /data for these buildarr root_folders to resolve', root_folders: [ @@ -263,7 +261,7 @@ local secrets = import 'milky-way/secrets/k8s-secret-values.jsonnet'; instances: { [prowlarrOrionSystemInstanceName]: { hostname: utils.domainOfService(this.prowlarr.service), - port: utils.assertAndReturn(this.prowlarr.service.spec.ports[0], function(p) p.name == 'webui').port, + port: utils.associateObjectsByKey(this.prowlarr.service.spec.ports, 'name')['webui'].port, protocol: 'http', api_key: secrets.prowlarr.apiKey, settings: { @@ -280,11 +278,11 @@ local secrets = import 'milky-way/secrets/k8s-secret-values.jsonnet'; instance_name: sonarrOrionSystemInstanceName, prowlarr_url: httpUrl( utils.domainOfService(this.prowlarr.service), - utils.assertAndReturn(this.prowlarr.service.spec.ports[0], function(p) p.name == 'webui').port, + utils.associateObjectsByKey(this.prowlarr.service.spec.ports, 'name')['webui'].port, ), base_url: httpUrl( utils.domainOfService(this.sonarr.service), - utils.assertAndReturn(this.sonarr.service.spec.ports[0], function(p) p.name == 'webui').port, + utils.associateObjectsByKey(this.sonarr.service.spec.ports, 'name')['webui'].port, ), sync_level: 'full_sync', }, diff --git a/milky-way/lib/calibre-web-automated.libsonnet b/milky-way/lib/calibre-web-automated.libsonnet index 9a9a7dd..7636508 100644 --- a/milky-way/lib/calibre-web-automated.libsonnet +++ b/milky-way/lib/calibre-web-automated.libsonnet @@ -43,14 +43,12 @@ local defaultAppName = "calibre-web-auto"; ports: [ { containerPort: // the port that the application listens on - local calibreWebAutoContainer = utils.assertAndReturn( - this.statefulset.spec.template.spec.containers[0], - function(container) container.name == name, # Ensure this is the main calibre-web-auto container - ); - local portEnvVarStr = utils.assertAndReturn( - calibreWebAutoContainer.env[4], - function(var) var.name == "CWA_PORT_OVERRIDE", # Ensure this is the CWA_PORT_OVERRIDE env var - ).value; + local calibreWebAutoContainer = utils.associateObjectsByKey( + this.statefulset.spec.template.spec.containers, 'name' + )[name]; # the main calibre-web-auto container, looked up by name + local portEnvVarStr = utils.associateObjectsByKey( + calibreWebAutoContainer.env, 'name' + )["CWA_PORT_OVERRIDE"].value; # the CWA_PORT_OVERRIDE env var, looked up by name std.parseInt(portEnvVarStr), # Parse the port number from the env var name: "webui", }, @@ -141,10 +139,9 @@ local defaultAppName = "calibre-web-auto"; { port: 80, targetPort: - local calibreWebAutoContainer = utils.assertAndReturn( - this.statefulset.spec.template.spec.containers[0], - function(container) container.name == name, - ); + local calibreWebAutoContainer = utils.associateObjectsByKey( + this.statefulset.spec.template.spec.containers, 'name' + )[name]; utils.assertEqualAndReturn(calibreWebAutoContainer.ports[0].name, "webui"), name: "http", }, diff --git a/milky-way/lib/utils.libsonnet b/milky-way/lib/utils.libsonnet index 88d528d..d4ff57a 100644 --- a/milky-way/lib/utils.libsonnet +++ b/milky-way/lib/utils.libsonnet @@ -1,7 +1,32 @@ // The cluster's DNS domain (k3s/Kubernetes default). Used to build in-cluster Service FQDNs. local clusterDomain = 'cluster.local'; +// Turn an array into an object keyed by an injective mapping. `injectiveMap` maps each value to +// its uniquely identifying string; the result maps that string back to the value. Assert +// injectivity FIRST (distinct-key count == element count), so a non-injective map fails with the +// descriptive message below instead of the object comprehension's own opaque "Duplicate field +// name" error -- the assert is evaluated before the comprehension is forced. +local convertArrayWithInjectiveMapping(array, injectiveMap) = ( + local keys = std.map(injectiveMap, array); + local distinctKeys = std.set(keys); + assert std.length(distinctKeys) == std.length(array) : ( + // Surface a concrete collision: the first key shared by >1 element, and two such elements. + local collidingKey = + std.filter(function(k) std.length(std.find(k, keys)) > 1, distinctKeys)[0]; + local collidingIndices = std.find(collidingKey, keys); + 'convertArrayWithInjectiveMapping: injectiveMap is not injective over the array -- ' + + std.toString(std.length(array)) + ' elements map to only ' + + std.toString(std.length(distinctKeys)) + ' distinct keys. Key ' + + std.toString(collidingKey) + ' is shared by ' + + std.toString(array[collidingIndices[0]]) + ' and ' + + std.toString(array[collidingIndices[1]]) + ); + { [injectiveMap(value)]: value for value in array } +); + { + local this = self, + assertEqualAndReturn(got, expected):: ( assert got == expected : 'Expected ' + std.toString(expected) + ', got ' + std.toString(got); got @@ -11,6 +36,17 @@ local clusterDomain = 'cluster.local'; value ), + // Public alias for convertArrayWithInjectiveMapping: key an array by an injective mapping, + // asserting the mapping really is injective over the array. + associateBy(array, injectiveMap):: convertArrayWithInjectiveMapping(array, injectiveMap), + + // Convenience wrapper for an array of objects: key each object by one of its own properties. + // Lets callers look up "the element whose is X" by name instead of by a hardcoded + // array index -- a missing X then fails loudly (missing-key access errors), and a duplicate X + // trips the injectivity assert in convertArrayWithInjectiveMapping. + associateObjectsByKey(arrayOfObjects, injectiveProperty):: + this.associateBy(arrayOfObjects, function(value) value[injectiveProperty]), + // In-cluster DNS FQDN of a Kubernetes Service object: ..svc.. // Validates that the argument really is a core/v1 Service with a name and namespace, throwing a // descriptive error otherwise, so a wrong object passed at a wiring point fails at evaluation -- 2.54.0