2 Commits

Author SHA1 Message Date
keyhan fec9ec386f Push Kaniko artifacts via harbor-core for Harbor UI visibility.
Build and Deploy Platform / build-and-deploy (push) Failing after 12m36s
Add REGISTRY_PUSH_URL config, route CI Kaniko to harbor-core, and document dual-host kaniko auth for core push plus registry base-image pull.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 11:43:05 +03:30
keyhan ec72ee4fca Fix app image pulls and Harbor kubelet auth for user workloads.
Route k3s registry mirrors through harbor-core ClusterIP with hostname-only auth keys, use HTTP EXT_ENDPOINT so OAuth tokens work on port 80, extend deploy readiness timeout, and harden Kaniko build/dockerfile fallbacks.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 18:38:05 +03:30
12 changed files with 197 additions and 42 deletions
+2 -2
View File
@@ -14,8 +14,8 @@ concurrency:
env: env:
# PULL_REGISTRY: kubelet pulls via k3s mirror → harbor-core (matches registry-pull-secret) # PULL_REGISTRY: kubelet pulls via k3s mirror → harbor-core (matches registry-pull-secret)
PULL_REGISTRY: registry.abrban.com PULL_REGISTRY: registry.abrban.com
# PUSH_REGISTRY: kaniko pushes directly to harbor-registry (internal, no TLS) # PUSH_REGISTRY: kaniko pushes via harbor-core (Harbor UI metadata + blob storage)
PUSH_REGISTRY: harbor-registry.cloudhost.svc.cluster.local:5000 PUSH_REGISTRY: harbor-core.cloudhost.svc.cluster.local
PROJECT: abrban PROJECT: abrban
BUILD_NS: cloudhost-builds BUILD_NS: cloudhost-builds
GITEA_HOST: gitea-http.gitea.svc.cluster.local:3000 GITEA_HOST: gitea-http.gitea.svc.cluster.local:3000
@@ -4,7 +4,7 @@
## Install: ## Install:
## helm upgrade --install harbor harbor/harbor -n cloudhost -f backend/helm/cloudhost-harbor/values-registry.abrban.com.yaml ## helm upgrade --install harbor harbor/harbor -n cloudhost -f backend/helm/cloudhost-harbor/values-registry.abrban.com.yaml
## ##
externalURL: https://registry.abrban.com externalURL: http://registry.abrban.com
proxy: proxy:
# Values are injected by install script from `cloudhost/registry-egress-proxy`. # Values are injected by install script from `cloudhost/registry-egress-proxy`.
+82
View File
@@ -49,6 +49,41 @@ describe('BuildService', () => {
service = module.get(BuildService); service = module.get(BuildService);
}); });
describe('baseImage', () => {
it('prefixes Docker Hub library images (tag colon must not block mirroring)', () => {
const config = (service as any).configService as { get: jest.Mock };
config.get.mockImplementation((key: string) => {
if (key === 'build.baseImageRegistry') return 'registry.abrban.com/abrban';
return undefined;
});
expect((service as any).baseImage('node:20-alpine')).toBe(
'registry.abrban.com/abrban/node:20-alpine',
);
expect((service as any).baseImage('alpine:3.19')).toBe(
'registry.abrban.com/abrban/alpine:3.19',
);
});
it('leaves images that already reference an external registry unchanged', () => {
const config = (service as any).configService as { get: jest.Mock };
config.get.mockImplementation((key: string) => {
if (key === 'build.baseImageRegistry') return 'registry.abrban.com/abrban';
return undefined;
});
expect((service as any).baseImage('mcr.microsoft.com/dotnet/sdk:8.0')).toBe(
'mcr.microsoft.com/dotnet/sdk:8.0',
);
expect((service as any).baseImage('registry.abrban.com/abrban/node:20-alpine')).toBe(
'registry.abrban.com/abrban/node:20-alpine',
);
expect((service as any).baseImage('localhost:5000/myapp:latest')).toBe(
'localhost:5000/myapp:latest',
);
});
});
describe('generateDockerfile', () => { describe('generateDockerfile', () => {
it('generates Go Dockerfile with requested runtime version', () => { it('generates Go Dockerfile with requested runtime version', () => {
const app = { const app = {
@@ -78,6 +113,53 @@ describe('BuildService', () => {
expect(dockerfile).toContain('go build -a -installsuffix cgo -ldflags="-w -s" -o main ./cmd/server'); expect(dockerfile).toContain('go build -a -installsuffix cgo -ldflags="-w -s" -o main ./cmd/server');
}); });
it('generates Node.js Dockerfile with mirrored base images when registry prefix is set', async () => {
const module = await Test.createTestingModule({
providers: [
BuildService,
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string) => {
const map: Record<string, string> = {
'build.namespace': 'cloudhost-builds',
'build.serviceAccount': 'kaniko-builder',
'build.baseImageRegistry': 'registry.abrban.com/abrban',
'registry.url': 'registry.local:5000',
};
return map[key];
}),
},
},
{ provide: ClustersService, useValue: {} },
{
provide: BuildProgressStore,
useValue: { get: jest.fn(), set: jest.fn(), clear: jest.fn() },
},
{ provide: RegistryService, useValue: {} },
{
provide: SourceStorageService,
useValue: {
isObjectStorage: () => false,
materializeToTempFile: jest.fn(),
getSize: jest.fn(),
},
},
],
}).compile();
const mirrored = module.get(BuildService);
const app = {
runtime: AppRuntime.NODEJS,
runtimeVersion: '20',
} as Application;
const dockerfile = (mirrored as any).generateDockerfile(app) as string;
expect(dockerfile).toContain('FROM registry.abrban.com/abrban/node:20-alpine');
expect(dockerfile).toContain('EXPOSE 3000');
});
it('generates Node.js Dockerfile with default port', () => { it('generates Node.js Dockerfile with default port', () => {
const app = { const app = {
runtime: AppRuntime.NODEJS, runtime: AppRuntime.NODEJS,
+15 -5
View File
@@ -73,9 +73,19 @@ export class BuildService {
private baseImage(image: string): string { private baseImage(image: string): string {
const prefix = this.configService.get<string>('build.baseImageRegistry'); const prefix = this.configService.get<string>('build.baseImageRegistry');
if (!prefix) return image; if (!prefix) return image;
const firstSegment = image.split('/')[0];
const hasRegistry = firstSegment.includes('.') || firstSegment.includes(':'); // Official Docker Hub library images have no slash (node:20-alpine, alpine:3.19).
if (hasRegistry) return image; // Do not treat the tag colon as a registry port — that was skipping the mirror.
if (!image.includes('/')) {
return `${prefix}/${image}`;
}
const registryHost = image.split('/')[0];
if (registryHost.includes('.') || registryHost.includes(':') || registryHost === 'localhost') {
return image;
}
// Docker Hub org/user image (e.g. bitnami/redis:7) — mirror through the prefix.
return `${prefix}/${image}`; return `${prefix}/${image}`;
} }
@@ -1195,8 +1205,8 @@ export class BuildService {
FROM ${this.baseImage(`node:${nodeVersion}-alpine`)} AS builder FROM ${this.baseImage(`node:${nodeVersion}-alpine`)} AS builder
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
# Reproducible install from the lockfile when present # Prefer lockfile; fall back when package.json and lockfile are out of sync
RUN if [ -f package-lock.json ]; then npm ci --legacy-peer-deps; else npm install --legacy-peer-deps; fi \\ RUN if [ -f package-lock.json ]; then npm ci --legacy-peer-deps || npm install --legacy-peer-deps; else npm install --legacy-peer-deps; fi \\
&& npm cache clean --force && npm cache clean --force
COPY . . COPY . .
+46 -15
View File
@@ -870,7 +870,7 @@ export class ClustersService implements OnModuleInit, OnModuleDestroy {
const buildNs = this.registryService.getBuildNamespace(); const buildNs = this.registryService.getBuildNamespace();
const saName = this.configService.get<string>('build.serviceAccount') || 'kaniko-builder'; const saName = this.configService.get<string>('build.serviceAccount') || 'kaniko-builder';
const registryUrl = this.registryService.getRegistryUrl(); const registryHost = this.registryService.getRegistryHost();
this.logger.log(`Bootstrapping cluster — namespace: ${buildNs}`); this.logger.log(`Bootstrapping cluster — namespace: ${buildNs}`);
@@ -1100,13 +1100,17 @@ export class ClustersService implements OnModuleInit, OnModuleDestroy {
} }
} }
await this.ensureK3sRegistryMirrors(appsApi, registryUrl); await this.ensureK3sRegistryMirrors(coreApi, appsApi, registryHost);
this.logger.log(`✅ Cluster bootstrap complete — registry: ${registryUrl}`); this.logger.log(`✅ Cluster bootstrap complete — registry: ${registryHost}`);
} }
/** In-cluster registry mirror for k3s/containerd (HTTP). Removes legacy external-registry DaemonSet if present. */ /** In-cluster registry mirror for k3s/containerd (HTTP). Removes legacy external-registry DaemonSet if present. */
private async ensureK3sRegistryMirrors(appsApi: k8s.AppsV1Api, registryUrl: string): Promise<void> { private async ensureK3sRegistryMirrors(
coreApi: k8s.CoreV1Api,
appsApi: k8s.AppsV1Api,
registryHost: string,
): Promise<void> {
const namespace = 'kube-system'; const namespace = 'kube-system';
const legacyDs = 'cloudhost-k3s-registry-config'; const legacyDs = 'cloudhost-k3s-registry-config';
try { try {
@@ -1120,28 +1124,25 @@ export class ClustersService implements OnModuleInit, OnModuleDestroy {
const { username, password } = this.registryService.getRegistryCredentials(); const { username, password } = this.registryService.getRegistryCredentials();
const dsName = 'cloudhost-k3s-registry-mirrors'; const dsName = 'cloudhost-k3s-registry-mirrors';
// The mirror endpoint must be reachable by the node's containerd, which does // containerd on the node does not use cluster DNS — mirror via ClusterIP (Harbor)
// NOT use cluster DNS — so we point it at the registry NodePort on loopback // or loopback NodePort (legacy in-cluster registry).
// (http://127.0.0.1:<nodePort>) instead of the in-cluster service DNS name. const mirrorEndpoint = await this.resolveK3sRegistryMirrorEndpoint(coreApi);
// Otherwise image pulls break whenever node-level resolution of const mirrorHost = mirrorEndpoint.replace(/^https?:\/\//, '');
// *.svc.cluster.local is unavailable (e.g. right after a node restart).
const registryNodePort = 30500;
const nodePortHost = `127.0.0.1:${registryNodePort}`;
const configureScript = [ const configureScript = [
'set -e', 'set -e',
'REG=/host/etc/rancher/k3s/registries.yaml', 'REG=/host/etc/rancher/k3s/registries.yaml',
'mkdir -p /host/etc/rancher/k3s', 'mkdir -p /host/etc/rancher/k3s',
'cat > /tmp/cloudhost-registries.yaml <<EOFREG', 'cat > /tmp/cloudhost-registries.yaml <<EOFREG',
'mirrors:', 'mirrors:',
` "${registryUrl}":`, ` "${registryHost}":`,
' endpoint:', ' endpoint:',
` - "http://${nodePortHost}"`, ` - "${mirrorEndpoint}"`,
'configs:', 'configs:',
` "${registryUrl}":`, ` "${registryHost}":`,
' auth:', ' auth:',
` username: ${JSON.stringify(username)}`, ` username: ${JSON.stringify(username)}`,
` password: ${JSON.stringify(password)}`, ` password: ${JSON.stringify(password)}`,
` "${nodePortHost}":`, ` "${mirrorHost}":`,
' auth:', ' auth:',
` username: ${JSON.stringify(username)}`, ` username: ${JSON.stringify(username)}`,
` password: ${JSON.stringify(password)}`, ` password: ${JSON.stringify(password)}`,
@@ -1206,6 +1207,36 @@ export class ClustersService implements OnModuleInit, OnModuleDestroy {
} }
} }
/**
* containerd on nodes cannot resolve *.svc.cluster.local — use ClusterIP for
* Harbor (harbor-core HTTP) or legacy registry NodePort on loopback.
*/
private async resolveK3sRegistryMirrorEndpoint(coreApi: k8s.CoreV1Api): Promise<string> {
const pushUrl = this.registryService.getRegistryPushUrl();
const platformNs = this.configService.get<string>('platform.namespace') || 'cloudhost';
const harborCoreService = this.configService.get<string>('registry.harborCoreService') || 'harbor-core';
if (pushUrl.includes('harbor-registry')) {
try {
const svc = await coreApi.readNamespacedService({
name: harborCoreService,
namespace: platformNs,
});
const clusterIp = svc.spec?.clusterIP;
if (clusterIp) {
return `http://${clusterIp}`;
}
} catch (err: any) {
this.logger.warn(
`Could not resolve ${harborCoreService} ClusterIP for k3s mirror: ${err.message}`,
);
}
}
const registryNodePort = 30500;
return `http://127.0.0.1:${registryNodePort}`;
}
private parseCpuToMillicores(cpu: string): number { private parseCpuToMillicores(cpu: string): number {
if (!cpu || cpu === '0') return 0; if (!cpu || cpu === '0') return 0;
if (cpu.endsWith('n')) return parseFloat(cpu) / 1_000_000; if (cpu.endsWith('n')) return parseFloat(cpu) / 1_000_000;
+7 -1
View File
@@ -124,11 +124,17 @@ export default () => ({
}, },
registry: { registry: {
/** In-cluster registry — Kaniko push and app image pull (same host). */ /** Kaniko push target — harbor-core when set (Harbor UI metadata); else in-cluster registry. */
url: process.env.REGISTRY_URL || 'registry.cloudhost-builds.svc.cluster.local:5000', url: process.env.REGISTRY_URL || 'registry.cloudhost-builds.svc.cluster.local:5000',
pushUrl:
process.env.REGISTRY_PUSH_URL ||
process.env.REGISTRY_URL ||
'registry.cloudhost-builds.svc.cluster.local:5000',
/** Kubelet / workload pull (external hostname on Harbor setups). */
pullUrl: process.env.REGISTRY_PULL_URL || process.env.REGISTRY_URL || 'registry.cloudhost-builds.svc.cluster.local:5000', pullUrl: process.env.REGISTRY_PULL_URL || process.env.REGISTRY_URL || 'registry.cloudhost-builds.svc.cluster.local:5000',
username: process.env.REGISTRY_USERNAME || 'admin', username: process.env.REGISTRY_USERNAME || 'admin',
password: process.env.REGISTRY_PASSWORD || '', password: process.env.REGISTRY_PASSWORD || '',
harborCoreService: process.env.HARBOR_CORE_SERVICE || 'harbor-core',
}, },
build: { build: {
@@ -182,7 +182,7 @@ export class DeploymentsService implements OnModuleInit {
}); });
await this.kubernetesService.waitForApplicationReady( await this.kubernetesService.waitForApplicationReady(
app, app,
600_000, 1_200_000,
() => this.isDeploymentCancelled(deploymentId), () => this.isDeploymentCancelled(deploymentId),
); );
@@ -295,7 +295,7 @@ export class DeploymentsService implements OnModuleInit {
}); });
await this.kubernetesService.waitForApplicationReady( await this.kubernetesService.waitForApplicationReady(
app, app,
600_000, 1_200_000,
() => this.isDeploymentCancelled(deploymentId), () => this.isDeploymentCancelled(deploymentId),
); );
+8 -1
View File
@@ -35,10 +35,11 @@ export class RegistryService {
return slash === -1 ? url : url.slice(0, slash); return slash === -1 ? url : url.slice(0, slash);
} }
/** Push target host[:port][/project] — Kaniko destination (may differ from pull URL on Harbor). */ /** Push target host[:port][/project] — Kaniko via harbor-core when configured (Harbor UI metadata). */
getRegistryPushUrl(): string { getRegistryPushUrl(): string {
const buildNs = this.getBuildNamespace(); const buildNs = this.getBuildNamespace();
const url = const url =
this.configService.get<string>('registry.pushUrl') ||
this.configService.get<string>('registry.url') || this.configService.get<string>('registry.url') ||
`registry.${buildNs}.svc.cluster.local:5000`; `registry.${buildNs}.svc.cluster.local:5000`;
return url.replace(/^https?:\/\//, ''); return url.replace(/^https?:\/\//, '');
@@ -112,6 +113,12 @@ export class RegistryService {
if (pushHost !== pullHost) { if (pushHost !== pullHost) {
auths[pushHost] = { auth }; auths[pushHost] = { auth };
} }
// Legacy direct-registry push host (base image pulls during Kaniko build).
const directPushHost = this.configService.get<string>('registry.url')?.replace(/^https?:\/\//, '');
const directHostOnly = directPushHost?.includes('/') ? directPushHost.slice(0, directPushHost.indexOf('/')) : directPushHost;
if (directHostOnly && directHostOnly !== pushHost && directHostOnly !== pullHost) {
auths[directHostOnly] = { auth };
}
return JSON.stringify({ auths }); return JSON.stringify({ auths });
} }
+18 -14
View File
@@ -2,21 +2,15 @@
# The real secret is managed as a SealedSecret in the cloud-host-gitops repo # The real secret is managed as a SealedSecret in the cloud-host-gitops repo
# (sealed-secrets/kaniko-harbor-auth.yaml). # (sealed-secrets/kaniko-harbor-auth.yaml).
# #
# Kaniko pushes directly to the internal registry endpoint # Kaniko pushes via harbor-core (Harbor UI metadata). Pull base images may still
# (harbor-registry.cloudhost.svc.cluster.local:5000), which bypasses harbor-core. # use harbor-registry:5000 — include auth for both hosts in one dockerconfigjson.
# That endpoint only accepts the internal registry credential — Harbor robot
# accounts do NOT work there (their tokens are issued by harbor-core's token
# service). Use the harbor_registry_user credential from the harbor-core secret:
# #
# REG_PASS="$(kubectl -n cloudhost get secret harbor-core \ # ADMIN="$(kubectl -n cloudhost get secret harbor-core -o jsonpath='{.data.HARBOR_ADMIN_PASSWORD}' | base64 -d)"
# -o jsonpath='{.data.REGISTRY_CREDENTIAL_PASSWORD}' | base64 -d)" # REG_PASS="$(kubectl -n cloudhost get secret harbor-core -o jsonpath='{.data.REGISTRY_CREDENTIAL_PASSWORD}' | base64 -d)"
# kubectl -n cloudhost-builds create secret docker-registry kaniko-harbor-auth \ # kubectl -n cloudhost-builds create secret generic kaniko-harbor-auth \
# --docker-server=harbor-registry.cloudhost.svc.cluster.local:5000 \ # --from-literal=admin="${ADMIN}" --from-literal=reg_pass="${REG_PASS}" --dry-run=client -o yaml | ...
# --docker-username=harbor_registry_user \
# --docker-password="${REG_PASS}"
# #
# The build-deploy workflow mounts this secret at /kaniko/.docker/config.json # See RUNBOOK-CICD.fa.md for the full procedure.
# inside every Kaniko Job. See RUNBOOK-CICD.fa.md for the full procedure.
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
@@ -27,10 +21,20 @@ stringData:
.dockerconfigjson: | .dockerconfigjson: |
{ {
"auths": { "auths": {
"harbor-core.cloudhost.svc.cluster.local": {
"username": "admin",
"password": "<HARBOR_ADMIN_PASSWORD>",
"auth": "<base64 of admin:password>"
},
"harbor-registry.cloudhost.svc.cluster.local:5000": { "harbor-registry.cloudhost.svc.cluster.local:5000": {
"username": "harbor_registry_user", "username": "harbor_registry_user",
"password": "<REGISTRY_CREDENTIAL_PASSWORD>", "password": "<REGISTRY_CREDENTIAL_PASSWORD>",
"auth": "<base64 of username:password>" "auth": "<base64 of harbor_registry_user:password>"
},
"registry.abrban.com": {
"username": "harbor_registry_user",
"password": "<REGISTRY_CREDENTIAL_PASSWORD>",
"auth": "<base64>"
} }
} }
} }
+2
View File
@@ -2,6 +2,8 @@
# Proxy-cache only works through harbor-core (not harbor-registry or Traefik /v2/ alone). # Proxy-cache only works through harbor-core (not harbor-registry or Traefik /v2/ alone).
# #
# Apply: ./scripts/apply-k3s-registries.sh # Apply: ./scripts/apply-k3s-registries.sh
# Harbor EXT_ENDPOINT should be http://registry.abrban.com so OAuth realm uses HTTP
# (kubelet mirror hits harbor-core on :80; https://<clusterIP>:443 times out).
mirrors: mirrors:
registry.abrban.com: registry.abrban.com:
@@ -103,6 +103,8 @@ backend:
PLATFORM_DOMAIN: apps.abrban.com PLATFORM_DOMAIN: apps.abrban.com
PREVIEW_BASE_DOMAIN: apps.abrban.com PREVIEW_BASE_DOMAIN: apps.abrban.com
FRONTEND_URL: https://panel.abrban.com,https://abrban.com FRONTEND_URL: https://panel.abrban.com,https://abrban.com
# Push via harbor-core so artifacts appear in Harbor UI; pull stays on registry.abrban.com.
REGISTRY_PUSH_URL: harbor-core.cloudhost.svc.cluster.local/abrban
REGISTRY_URL: harbor-registry.cloudhost.svc.cluster.local:5000/abrban REGISTRY_URL: harbor-registry.cloudhost.svc.cluster.local:5000/abrban
REGISTRY_PULL_URL: registry.abrban.com/abrban REGISTRY_PULL_URL: registry.abrban.com/abrban
BUILD_NAMESPACE: cloudhost-builds BUILD_NAMESPACE: cloudhost-builds
+12 -1
View File
@@ -8,6 +8,16 @@ HARBOR_CORE_IP="${HARBOR_CORE_IP:-$(kubectl -n cloudhost get svc harbor-core -o
REG_USER="${REG_USER:-harbor_registry_user}" REG_USER="${REG_USER:-harbor_registry_user}"
REG_PASS="${REG_PASS:-$(kubectl -n cloudhost get secret harbor-core -o jsonpath='{.data.REGISTRY_CREDENTIAL_PASSWORD}' | base64 -d)}" REG_PASS="${REG_PASS:-$(kubectl -n cloudhost get secret harbor-core -o jsonpath='{.data.REGISTRY_CREDENTIAL_PASSWORD}' | base64 -d)}"
REG_PASS_B64="$(printf '%s' "$REG_PASS" | base64 | tr -d '\n')"
kubectl -n "${NS}" delete pod k3s-registries-setup --ignore-not-found
kubectl -n "${NS}" create secret generic k3s-registries-setup-env \
--from-literal=HARBOR_CORE_IP="${HARBOR_CORE_IP}" \
--from-literal=REG_USER="${REG_USER}" \
--from-literal=REG_PASS_B64="${REG_PASS_B64}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n "${NS}" delete pod k3s-registries-setup --ignore-not-found kubectl -n "${NS}" delete pod k3s-registries-setup --ignore-not-found
kubectl -n "${NS}" run k3s-registries-setup \ kubectl -n "${NS}" run k3s-registries-setup \
@@ -23,7 +33,8 @@ kubectl -n "${NS}" run k3s-registries-setup \
"name": "setup", "name": "setup",
"image": "rancher/mirrored-library-busybox:1.36.1", "image": "rancher/mirrored-library-busybox:1.36.1",
"securityContext": {"privileged": true}, "securityContext": {"privileged": true},
"command": ["sh", "-ec", "mkdir -p /host/etc/rancher/k3s && cat > /host/etc/rancher/k3s/registries.yaml <<'REGEOF'\nmirrors:\n registry.abrban.com:\n endpoint:\n - http://${HARBOR_CORE_IP}\n \\\"registry.cloudhost-builds.svc.cluster.local:5000\\\":\n endpoint:\n - \\\"http://127.0.0.1:30500\\\"\nconfigs:\n registry.abrban.com:\n auth:\n username: ${REG_USER}\n password: ${REG_PASS}\n \\\"${HARBOR_CORE_IP}\\\":\n auth:\n username: ${REG_USER}\n password: ${REG_PASS}\n \\\"registry.cloudhost-builds.svc.cluster.local:5000\\\":\n auth:\n username: admin\n password: \\\"\\\"\n \\\"127.0.0.1:30500\\\":\n auth:\n username: admin\n password: \\\"\\\"\nREGEOF\nnsenter -t 1 -m -u -n -i -- systemctl restart k3s 2>/dev/null || true\necho k3s-restarted\nsleep 30"], "envFrom": [{"secretRef": {"name": "k3s-registries-setup-env"}}],
"command": ["sh", "-ec", "REG_PASS=\$(echo \"\$REG_PASS_B64\" | base64 -d); mkdir -p /host/etc/rancher/k3s && cat > /host/etc/rancher/k3s/registries.yaml <<REGEOF\nmirrors:\n registry.abrban.com:\n endpoint:\n - http://\${HARBOR_CORE_IP}\n \\\"registry.cloudhost-builds.svc.cluster.local:5000\\\":\n endpoint:\n - \\\"http://127.0.0.1:30500\\\"\nconfigs:\n registry.abrban.com:\n auth:\n username: \${REG_USER}\n password: \${REG_PASS}\n \\\"\${HARBOR_CORE_IP}\\\":\n auth:\n username: \${REG_USER}\n password: \${REG_PASS}\n \\\"registry.cloudhost-builds.svc.cluster.local:5000\\\":\n auth:\n username: admin\n password: \\\"\\\"\n \\\"127.0.0.1:30500\\\":\n auth:\n username: admin\n password: \\\"\\\"\nREGEOF\nnsenter -t 1 -m -u -n -i -- systemctl restart k3s 2>/dev/null || true\necho k3s-restarted\nsleep 30"],
"volumeMounts": [{"name": "host", "mountPath": "/host"}] "volumeMounts": [{"name": "host", "mountPath": "/host"}]
}], }],
"volumes": [{"name": "host", "hostPath": {"path": "/"}}] "volumes": [{"name": "host", "hostPath": {"path": "/"}}]