Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58ab81469b | |||
| fec9ec386f | |||
| ec72ee4fca |
@@ -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
|
||||||
@@ -102,7 +102,10 @@ jobs:
|
|||||||
apk add --no-cache git &&
|
apk add --no-cache git &&
|
||||||
git clone --depth=1 --branch main http://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH} /workspace &&
|
git clone --depth=1 --branch main http://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH} /workspace &&
|
||||||
cd /workspace/backend &&
|
cd /workspace/backend &&
|
||||||
npm ci --legacy-peer-deps &&
|
npm config set fetch-retries 5 &&
|
||||||
|
npm config set fetch-retry-mintimeout 20000 &&
|
||||||
|
npm config set fetch-retry-maxtimeout 120000 &&
|
||||||
|
(npm ci --legacy-peer-deps || (echo 'npm ci failed, retrying...' && sleep 5 && npm ci --legacy-peer-deps) || (echo 'npm ci failed again, retrying...' && sleep 10 && npm ci --legacy-peer-deps)) &&
|
||||||
npm run test -- --ci --runInBand
|
npm run test -- --ci --runInBand
|
||||||
resources:
|
resources:
|
||||||
requests: { cpu: 500m, memory: 1Gi }
|
requests: { cpu: 500m, memory: 1Gi }
|
||||||
@@ -159,7 +162,7 @@ jobs:
|
|||||||
mountPath: /workspace
|
mountPath: /workspace
|
||||||
containers:
|
containers:
|
||||||
- name: kaniko
|
- name: kaniko
|
||||||
image: ${PULL_REGISTRY}/${PROJECT}/kaniko-executor:v1.23.2
|
image: ${PULL_REGISTRY}/${PROJECT}/kaniko-executor:v1.27.6-debug
|
||||||
# Base image (node:24-alpine) is seeded in Harbor abrban/ — avoids
|
# Base image (node:24-alpine) is seeded in Harbor abrban/ — avoids
|
||||||
# flaky direct pulls from docker.io through the egress proxy.
|
# flaky direct pulls from docker.io through the egress proxy.
|
||||||
envFrom:
|
envFrom:
|
||||||
@@ -224,7 +227,7 @@ jobs:
|
|||||||
mountPath: /workspace
|
mountPath: /workspace
|
||||||
containers:
|
containers:
|
||||||
- name: kaniko
|
- name: kaniko
|
||||||
image: ${PULL_REGISTRY}/${PROJECT}/kaniko-executor:v1.23.2
|
image: ${PULL_REGISTRY}/${PROJECT}/kaniko-executor:v1.27.6-debug
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: registry-egress-proxy
|
name: registry-egress-proxy
|
||||||
|
|||||||
@@ -22,16 +22,17 @@ echo "==> [2/4] Scaling old registry deployment down (rollback-friendly)"
|
|||||||
kubectl -n cloudhost scale deploy/registry --replicas=0 || true
|
kubectl -n cloudhost scale deploy/registry --replicas=0 || true
|
||||||
|
|
||||||
echo "==> [3/4] Installing Harbor"
|
echo "==> [3/4] Installing Harbor"
|
||||||
HTTP_PROXY="$(kubectl -n cloudhost get secret registry-egress-proxy -o jsonpath='{.data.HTTP_PROXY}' | base64 -d)"
|
# IMPORTANT: do NOT set HTTP(S)_PROXY on harbor-core for in-cluster registry
|
||||||
HTTPS_PROXY="$(kubectl -n cloudhost get secret registry-egress-proxy -o jsonpath='{.data.HTTPS_PROXY}' | base64 -d)"
|
# traffic. Egress proxy on core causes 502 on blob uploads via harbor-core
|
||||||
NO_PROXY="$(kubectl -n cloudhost get secret registry-egress-proxy -o jsonpath='{.data.NO_PROXY}' | base64 -d)"
|
# (Kaniko/skopeo push fails; Harbor UI metadata never appears).
|
||||||
|
# Keep proxy empty here; Harbor proxy-cache projects can still use project-level
|
||||||
|
# proxy settings when needed. Expand noProxy for safety if proxy is re-enabled.
|
||||||
TMP_PROXY_VALUES="$(mktemp)"
|
TMP_PROXY_VALUES="$(mktemp)"
|
||||||
cat > "${TMP_PROXY_VALUES}" <<EOF
|
cat > "${TMP_PROXY_VALUES}" <<EOF
|
||||||
proxy:
|
proxy:
|
||||||
httpProxy: "${HTTP_PROXY}"
|
httpProxy: ""
|
||||||
httpsProxy: "${HTTPS_PROXY}"
|
httpsProxy: ""
|
||||||
noProxy: "${NO_PROXY}"
|
noProxy: "harbor-core,harbor-jobservice,harbor-database,harbor-registry,harbor-portal,.svc,.cluster.local,10.43.0.0/16,127.0.0.1,localhost,registry.abrban.com"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
helm upgrade --install harbor harbor/harbor \
|
helm upgrade --install harbor harbor/harbor \
|
||||||
@@ -43,6 +44,9 @@ helm upgrade --install harbor harbor/harbor \
|
|||||||
|
|
||||||
rm -f "${TMP_PROXY_VALUES}" || true
|
rm -f "${TMP_PROXY_VALUES}" || true
|
||||||
|
|
||||||
echo "==> [4/4] Done"
|
echo "==> [4/4] Apply registry ingress path split (proxy-* → harbor-core)"
|
||||||
|
kubectl apply -f "$(dirname "$0")/../../../gitops/harbor/registry-ingress.yaml"
|
||||||
|
|
||||||
|
echo "==> Done"
|
||||||
kubectl -n cloudhost get ingress | grep -n registry || true
|
kubectl -n cloudhost get ingress | grep -n registry || true
|
||||||
|
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 . .
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ spec:
|
|||||||
mountPath: /workspace
|
mountPath: /workspace
|
||||||
containers:
|
containers:
|
||||||
- name: kaniko
|
- name: kaniko
|
||||||
image: registry.abrban.com/abrban/kaniko-executor:v1.23.2
|
image: registry.abrban.com/abrban/kaniko-executor:v1.27.6-debug
|
||||||
env:
|
env:
|
||||||
- name: IMAGE_TAG
|
- name: IMAGE_TAG
|
||||||
value: bootstrap
|
value: bootstrap
|
||||||
@@ -40,7 +40,10 @@ spec:
|
|||||||
- -ec
|
- -ec
|
||||||
- |
|
- |
|
||||||
set -eux
|
set -eux
|
||||||
REG="harbor-registry.cloudhost.svc.cluster.local:5000/abrban"
|
REG="harbor-core.cloudhost.svc.cluster.local/abrban"
|
||||||
|
unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy || true
|
||||||
|
export NO_PROXY="harbor-core.cloudhost.svc.cluster.local,harbor-registry.cloudhost.svc.cluster.local,registry.abrban.com,10.43.0.0/16,.svc,.cluster.local"
|
||||||
|
export no_proxy="$NO_PROXY"
|
||||||
/kaniko/executor \
|
/kaniko/executor \
|
||||||
--dockerfile=/workspace/backend/Dockerfile \
|
--dockerfile=/workspace/backend/Dockerfile \
|
||||||
--context=dir:///workspace/backend \
|
--context=dir:///workspace/backend \
|
||||||
@@ -55,6 +58,14 @@ spec:
|
|||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: workspace
|
- name: workspace
|
||||||
mountPath: /workspace
|
mountPath: /workspace
|
||||||
|
- name: docker-config
|
||||||
|
mountPath: /kaniko/.docker
|
||||||
volumes:
|
volumes:
|
||||||
- name: workspace
|
- name: workspace
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
|
- name: docker-config
|
||||||
|
secret:
|
||||||
|
secretName: kaniko-harbor-auth
|
||||||
|
items:
|
||||||
|
- key: .dockerconfigjson
|
||||||
|
path: config.json
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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": "/"}}]
|
||||||
|
|||||||
@@ -40,13 +40,17 @@ spec:
|
|||||||
mountPath: /workspace
|
mountPath: /workspace
|
||||||
containers:
|
containers:
|
||||||
- name: kaniko
|
- name: kaniko
|
||||||
image: registry.abrban.com/abrban/kaniko-executor:v1.23.2
|
image: registry.abrban.com/abrban/kaniko-executor:v1.27.6-debug
|
||||||
command:
|
command:
|
||||||
- sh
|
- sh
|
||||||
- -ec
|
- -ec
|
||||||
- |
|
- |
|
||||||
set -eux
|
set -eux
|
||||||
REG="harbor-registry.cloudhost.svc.cluster.local:5000/abrban"
|
# Push via harbor-core so artifacts appear in Harbor UI
|
||||||
|
REG="harbor-core.cloudhost.svc.cluster.local/abrban"
|
||||||
|
unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy || true
|
||||||
|
export NO_PROXY="harbor-core.cloudhost.svc.cluster.local,harbor-registry.cloudhost.svc.cluster.local,registry.abrban.com,10.43.0.0/16,.svc,.cluster.local"
|
||||||
|
export no_proxy="$NO_PROXY"
|
||||||
/kaniko/executor \
|
/kaniko/executor \
|
||||||
--dockerfile=/workspace/backend/Dockerfile \
|
--dockerfile=/workspace/backend/Dockerfile \
|
||||||
--context=dir:///workspace/backend \
|
--context=dir:///workspace/backend \
|
||||||
@@ -62,9 +66,17 @@ spec:
|
|||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: workspace
|
- name: workspace
|
||||||
mountPath: /workspace
|
mountPath: /workspace
|
||||||
|
- name: docker-config
|
||||||
|
mountPath: /kaniko/.docker
|
||||||
volumes:
|
volumes:
|
||||||
- name: workspace
|
- name: workspace
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
|
- name: docker-config
|
||||||
|
secret:
|
||||||
|
secretName: kaniko-harbor-auth
|
||||||
|
items:
|
||||||
|
- key: .dockerconfigjson
|
||||||
|
path: config.json
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "==> Waiting for build job..."
|
echo "==> Waiting for build job..."
|
||||||
|
|||||||
Reference in New Issue
Block a user