Skip to content

fix(k8s): apply daemon registry mappings - #1046

Merged
oleksandr-nc merged 10 commits into
mainfrom
fix/k8s-registry-mapping
Sep 25, 2026
Merged

oleksandr-nc merged 10 commits into
mainfrom
fix/k8s-registry-mapping

Conversation

@oleksandr-nc

@oleksandr-nc oleksandr-nc commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1039

Registry mappings of a deploy daemon (occ app_api:daemon:registry:add, or the "Docker registries" dialog) were only applied by the Docker backend. On a Kubernetes daemon the mapping was stored and listed, but KubernetesActions built the image name straight from info.xml, so the cluster kept pulling from the upstream registry. That breaks air-gapped and mirror-only setups.

Changes

  • DaemonConfigService::resolveRegistryTarget() resolves the effective target of the daemon's mappings for the registry an ExApp image comes from: a mirror registry, local, or none. The first usable mapping of a registry wins. The image name (resolveImageRegistry(), used by both Docker image name builders and KubernetesActions::buildImageName()) and the Docker pull decision (shouldPullImage()) both derive from it, so the two backends and the two decisions cannot disagree. On Kubernetes the daemon config is passed down to where the HaRP /exapp/create payload is built, for single and multi-role deployments.
  • Unusable stored entries are ignored when resolving. An entry is unusable when it is not an array, when from or to is missing or not a string, or when the target is empty once surrounding whitespace and trailing slashes are dropped. Before, such an entry raised a TypeError during a Docker deploy or produced an image name with an empty registry. Tolerating them matters more now that Kubernetes resolves mappings too: it does so after the previous Deployment has been removed, so a throwing resolver would leave the ExApp undeployed.
  • addDockerRegistry() and removeDockerRegistry() keep registries a list. Removing an entry that was not the last one left a JSON object with gaps ({"1": {...}}), which breaks the add form of the registries dialog. Data that already has that shape is repaired on the next add or remove.
  • addDockerRegistry() rejects unusable mappings (same cases as above), trims both values, drops trailing slashes and stores only from and to. A stored unusable entry no longer blocks adding a usable mapping for the same source.
  • removeDockerRegistry() no longer fails with a TypeError when an unusable entry is stored next to the mapping that is removed, or when the daemon has no mappings at all.
  • For a local mapping the Kubernetes create payload carries image_pull_policy: Never, so the node uses only an image it already has, the same as Docker skipping the pull. This needs a HaRP that supports the field (feat(k8s): accept image_pull_policy in the create payload HaRP#122, on main, not in a release yet); older HaRP versions ignore the field and keep IfNotPresent.
  • local/ (with a trailing slash) counts as local; before it was treated as a registry named local.

Behaviour

  • A local mapping keeps the image name on both backends. Docker skips the pull. Kubernetes gets imagePullPolicy: Never with a HaRP that supports image_pull_policy, and fails within seconds when the image is not on the node; with an older HaRP the policy stays IfNotPresent and the kubelet pulls the image if the node does not have it. A Kubernetes daemon that already had a local mapping stored therefore needs the image on every node that can schedule the pod once HaRP supports the field.
  • When a source has more than one stored mapping (only possible through the daemon config API, addDockerRegistry() rejects a duplicate source), the first usable entry wins on both backends. Before, the image name took the first non-local entry while the Docker pull decision looked at any local entry.
  • Mappings that are already stored on a Kubernetes daemon were ignored until now. They take effect on the next ExApp install or update. The mirror has to be reachable from the cluster nodes.
  • HaRP does not set imagePullSecrets, so a mirror that needs authentication needs pull credentials configured in the cluster.
  • The GPU image variants (<tag>-cuda, <tag>-rocm) are still not used on Kubernetes.

Signed-off-by: Oleksandr Piskun <oleksandr2088@icloud.com>
Signed-off-by: Oleksandr Piskun <oleksandr2088@icloud.com>
Signed-off-by: Oleksandr Piskun <oleksandr2088@icloud.com>
@oleksandr-nc

Copy link
Copy Markdown
Contributor Author

/backport to stable35

@oleksandr-nc

Copy link
Copy Markdown
Contributor Author

/backport to stable34

@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The change adds shared registry target resolution and validation to DaemonConfigService. Docker image naming and pull decisions use the shared resolver. Kubernetes deployment paths apply registry mappings to images in single-role and multi-role deployments. For local targets, the Kubernetes create payload sets image_pull_policy to Never. Tests cover registry management, resolution, and both deployment backends.

Fixed issue severity: Medium. <fixed_issue_severity>Medium</fixed_issue_severity>

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 1b669

A daemon with a whitespace-only stored registry target can reject a valid replacement and fail to deploy images. Normalize stored targets before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 1b669

Registry mappings now affect images deployed to the cluster. The shared resolution logic reduces disagreement between deployment backends, but a local-image mapping can leave an app undeployed if the image is not available after its existing deployment is removed.

Retained concerns

  • Medium · reliability · inferred: A local mapping now requests image_pull_policy Never on Kubernetes. If the image is absent from an eligible node, redeployment can fail after the previous deployment has been removed; the examined single-role path does not restore it, and multi-role rollback removes newly created roles rather than restoring old ones.
Security review details

Security Blast Radius

  • inferred — A daemon mapping can now redirect Kubernetes image selection for deployments using that daemon, including each multi-role create request. The evidence does not establish the number of affected apps, tenants, or clusters.

Security Findings and Attack Paths

  • inferred — No verified attacker-controlled path to changing a mapping was established. A changed mapping can affect the registry from which Kubernetes deploys an image, but the observed HTTP mutation path retains password confirmation; effective framework authorization was not independently verified.

Trust Boundaries and Controls

  • observed — The Kubernetes action sends the resolved image and conditional pull policy to HaRP. Its source comment states that HaRP without support for the policy field retains IfNotPresent, so a local mapping alone does not establish that every deployed agent will prohibit upstream pulls.

Resilience and Maintainability Implications

  • inferred — Shared target resolution reduces image-name and pull-decision drift between backends, while replacement-after-removal makes image availability and HaRP policy support material to deployment recovery.

Hardening Proposals

  • proposed — Before relying on local mappings for Kubernetes, verify the required image is available on eligible nodes and that the deployed HaRP version enforces Never; retain or restore the previous deployment when replacement fails.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 50 functions across 6 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies the coding requirements in [#1039]. Kubernetes resolves registry mappings through shared DaemonConfigService logic for single-role and multi-role /exapp/create payloads. A local…
Out of Scope Changes check ✅ Passed The changes stay within [#1039]. Shared resolution, mapping validation, malformed-entry handling, Docker pull decisions, and related tests support consistent registry mapping behavior. No unrelated ch…
Title check ✅ Passed The title clearly and concisely identifies the main change: applying daemon registry mappings to Kubernetes deployments.
Description check ✅ Passed The description directly explains the Kubernetes registry-mapping fix, shared resolution logic, local-image behavior, and related implementation details.
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: b53d5b1f-5d41-49fb-bda0-ff700cb724e3

📥 Commits

Reviewing files that changed from the base of the PR and between f699bdb and af4a36b.

📒 Files selected for processing (6)
  • lib/DeployActions/DockerActions.php
  • lib/DeployActions/KubernetesActions.php
  • lib/Service/DaemonConfigService.php
  • tests/php/DeployActions/DockerActionsTest.php
  • tests/php/DeployActions/KubernetesActionsTest.php
  • tests/php/Service/DaemonConfigServiceTest.php

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.

Comment thread lib/DeployActions/DockerActions.php Outdated
return ['error' => 'This Docker registry map does not exist'];
}
$deployConfig['registries'] = array_filter($deployConfig['registries'], function ($registry) use ($registryMap) {
$deployConfig['registries'] = array_values(array_filter($deployConfig['registries'], function ($registry) use ($registryMap) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '150,270p' lib/Service/DaemonConfigService.php
rg -n 'removeDockerRegistry|addDockerRegistry|registries' lib tests/php

Repository: nextcloud/app_api

Length of output: 11740


🏁 Script executed:

sed -n '1,155p' tests/php/Service/DaemonConfigServiceTest.php
printf '\n--- DaemonConfig references and storage/config handling ---\n'
rg -n -C 3 'class DaemonConfig|function getDeployConfig|function setDeployConfig|deploy_config|json_decode|json_encode|registr' lib tests/php migrations appinfo README.md 2>/dev/null | head -n 260

Repository: nextcloud/app_api

Length of output: 28299


🏁 Script executed:

fd -i 'DaemonConfig*' .
printf '\n--- model and mapper candidates ---\n'
for f in $(fd -i 'DaemonConfig*' . | head -n 12); do
  printf '\n### %s\n' "$f"
  rg -n -C 3 'class DaemonConfig|deploy_config|getDeployConfig|setDeployConfig|json|serialize|column' "$f" 2>/dev/null | head -n 100
done

Repository: nextcloud/app_api

Length of output: 31750


Handle malformed stored entries during removal.

If registries contains a string and the requested valid mapping, the callback accesses $registry['from'] on the string. PHP raises a TypeError, which catch (Exception) does not catch. Registry removal then fails with an uncaught error.

Guard non-array entries and use null-coalescing for missing fields.

Proposed fix
 			$deployConfig['registries'] = array_values(array_filter($deployConfig['registries'], function ($registry) use ($registryMap) {
-				return !($registry['from'] === $registryMap['from'] && $registry['to'] === $registryMap['to']);
+				if (!is_array($registry)) {
+					return true;
+				}
+				return !(($registry['from'] ?? null) === $registryMap['from']
+					&& ($registry['to'] ?? null) === $registryMap['to']);
 			}));

Signed-off-by: Oleksandr Piskun <oleksandr2088@icloud.com>
Signed-off-by: Oleksandr Piskun <oleksandr2088@icloud.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Normalize local before checking the special target. · DaemonConfigService.php:241

lib/Service/DaemonConfigService.php:241
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize local before checking the special target.

addDockerRegistry() accepts to: "local/". This condition does not recognize that value as local, then rtrim() returns local and the resolver returns it as a mirror registry. Docker then pulls local/..., and Kubernetes does not preserve the required unchanged-image behavior.

Trim the target before the local comparison. Add a resolver test for to: "local/".

Proposed fix
-			if (($registry['from'] ?? null) !== $imageRegistry || !is_string($target) || $target === 'local') {
+			if (($registry['from'] ?? null) !== $imageRegistry || !is_string($target)) {
 				continue;
 			}
 			$target = rtrim($target, '/');
-			if ($target !== '') {
+			if ($target !== '' && $target !== 'local') {
 				return $target;
 			}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 519481f6-4d41-426c-8b71-be779d00baa4

📥 Commits

Reviewing files that changed from the base of the PR and between af4a36b and cb6817d.

📒 Files selected for processing (4)
  • lib/DeployActions/DockerActions.php
  • lib/Service/DaemonConfigService.php
  • tests/php/DeployActions/DockerActionsTest.php
  • tests/php/Service/DaemonConfigServiceTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/php/DeployActions/DockerActionsTest.php
  • lib/DeployActions/DockerActions.php

Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review.

@kyteinsky kyteinsky left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The local target has no effect on Kubernetes. AppAPI never pulls there. HaRP creates the pods with imagePullPolicy: IfNotPresent, so the image name is kept and the kubelet pulls it if the node does not have it. Docker is unchanged: the name is kept and the pull is skipped.

maybe we can keep the same behaviour as docker here, set to no-pull when this registry mapping is present.

Comment thread lib/DeployActions/DockerActions.php Outdated
Comment on lines 449 to 460

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure but seems like the added lines 450-452 do the same thing which the rest of the function is doing

Signed-off-by: Oleksandr Piskun <oleksandr2088@icloud.com>
Signed-off-by: Oleksandr Piskun <oleksandr2088@icloud.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Ignore unusable stored targets during the duplicate-source check. · DaemonConfigService.php:193

lib/Service/DaemonConfigService.php:193
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Ignore unusable stored targets during the duplicate-source check.

If a stored mapping has from: ghcr.io and to: /, resolveRegistryTarget ignores it. This check still rejects a new, usable ghcr.io mapping. The administrator cannot apply a mirror until the unusable entry is removed separately. Apply the resolver’s target-usability check before treating a source as occupied.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 7758e80f-47eb-485c-b238-27791ef5a2f1

📥 Commits

Reviewing files that changed from the base of the PR and between cb6817d and 80688ef.

📒 Files selected for processing (6)
  • lib/DeployActions/DockerActions.php
  • lib/DeployActions/KubernetesActions.php
  • lib/Service/DaemonConfigService.php
  • tests/php/DeployActions/DockerActionsTest.php
  • tests/php/DeployActions/KubernetesActionsTest.php
  • tests/php/Service/DaemonConfigServiceTest.php

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.

@oleksandr-nc

Copy link
Copy Markdown
Contributor Author

maybe we can keep the same behaviour as docker here, set to no-pull when this registry mapping is present.

good idea - done! 👍

Signed-off-by: Oleksandr Piskun <oleksandr2088@icloud.com>
Signed-off-by: Oleksandr Piskun <oleksandr2088@icloud.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 9a754fcf-76b3-4863-8413-2dcd4b782dd2

📥 Commits

Reviewing files that changed from the base of the PR and between 80688ef and 1b66957.

📒 Files selected for processing (4)
  • lib/Service/DaemonConfigService.php
  • tests/php/DeployActions/DockerActionsTest.php
  • tests/php/DeployActions/KubernetesActionsTest.php
  • tests/php/Service/DaemonConfigServiceTest.php

Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review.

Comment thread lib/Service/DaemonConfigService.php
Signed-off-by: Oleksandr Piskun <oleksandr2088@icloud.com>
@oleksandr-nc
oleksandr-nc merged commit 3b08344 into main Sep 25, 2026
54 checks passed
@oleksandr-nc
oleksandr-nc deleted the fix/k8s-registry-mapping branch September 25, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kubernetes deploy daemon ignores registry mappings (app_api:daemon:registry:add)

2 participants