From f8e8cc43c48bb6ff9a338398315e512d75789f02 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 28 Apr 2026 16:07:32 +0000 Subject: [PATCH] feat: reconcile registry image updates --- package.json | 4 +- pnpm-lock.yaml | 22 +++--- ts/coreflow.classes.clustermanager.ts | 99 ++++++++++++++++++++++++--- 3 files changed, 102 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index d816b8f..f43d650 100644 --- a/package.json +++ b/package.json @@ -79,8 +79,8 @@ "@push.rocks/smartstream": "^3.4.0", "@push.rocks/smartstring": "^4.1.0", "@push.rocks/taskbuffer": "^8.0.2", - "@serve.zone/api": "^5.3.2", - "@serve.zone/interfaces": "^5.4.5", + "@serve.zone/api": "^5.3.4", + "@serve.zone/interfaces": "^5.4.6", "@tsclass/tsclass": "^9.5.0", "@types/node": "25.6.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28ed0f0..19fbc6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,11 +69,11 @@ importers: specifier: ^8.0.2 version: 8.0.2 '@serve.zone/api': - specifier: ^5.3.2 - version: 5.3.2(@push.rocks/smartserve@2.0.3) + specifier: ^5.3.4 + version: 5.3.4(@push.rocks/smartserve@2.0.3) '@serve.zone/interfaces': - specifier: ^5.4.5 - version: 5.4.5 + specifier: ^5.4.6 + version: 5.4.6 '@tsclass/tsclass': specifier: ^9.5.0 version: 9.5.0 @@ -1516,11 +1516,11 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@serve.zone/api@5.3.2': - resolution: {integrity: sha512-ETQ4KSNfhDP7O1WxXXLcMn/A+jZtDfd7FjuQ0k3n8tnXG9hExh8ZmqvMwVj8eT2CnXO+xQVlbAgT0HLMLnxCfA==} + '@serve.zone/api@5.3.4': + resolution: {integrity: sha512-3CqyeZkZPCJ4775UoNPKfknhTlAk6zmU/MVVSu6DoIAWgUaOuAlLUHlV45xIGtHmKAppsiYUoyoEhBLTZf9iMw==} - '@serve.zone/interfaces@5.4.5': - resolution: {integrity: sha512-asqUUjem3MGfIbseovHR8SxE+6FvjeQEYtV+PxcyY8YRXJ/vE3hNCDs7ePXgBbh4JXa+vNMaXHsFfz5Vrk6Ggg==} + '@serve.zone/interfaces@5.4.6': + resolution: {integrity: sha512-o4k7Wr6t3NLiP6gfAZZz8Jx8RlQ4sZYHTbhr4WkXzGf78vczFRIuFLyY1Y+TTNzDLEIzLVIyMsuECMV1KTwB2Q==} '@sindresorhus/is@5.6.0': resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} @@ -6929,7 +6929,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@serve.zone/api@5.3.2(@push.rocks/smartserve@2.0.3)': + '@serve.zone/api@5.3.4(@push.rocks/smartserve@2.0.3)': dependencies: '@api.global/typedrequest': 3.1.10 '@api.global/typedrequest-interfaces': 3.0.19 @@ -6938,7 +6938,7 @@ snapshots: '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrx': 3.0.10 '@push.rocks/smartstream': 3.4.0 - '@serve.zone/interfaces': 5.4.5 + '@serve.zone/interfaces': 5.4.6 '@tsclass/tsclass': 9.5.0 transitivePeerDependencies: - '@nuxt/kit' @@ -6949,7 +6949,7 @@ snapshots: - utf-8-validate - vue - '@serve.zone/interfaces@5.4.5': + '@serve.zone/interfaces@5.4.6': dependencies: '@api.global/typedrequest-interfaces': 3.0.19 '@push.rocks/smartlog-interfaces': 3.0.2 diff --git a/ts/coreflow.classes.clustermanager.ts b/ts/coreflow.classes.clustermanager.ts index e29ca1c..ab20b7c 100644 --- a/ts/coreflow.classes.clustermanager.ts +++ b/ts/coreflow.classes.clustermanager.ts @@ -19,6 +19,63 @@ export class ClusterManager { this.coreflowRef = coreflowRefArg; } + private getWorkloadServiceDeploymentLabels( + serviceArgFromCloudly: plugins.servezoneInterfaces.data.IService, + containerImageFromCloudly: plugins.servezoneInterfaces.data.IImage, + ) { + const desiredImageVersion = + serviceArgFromCloudly.data.imageVersion || + serviceArgFromCloudly.data.registryTarget?.tag || + 'latest'; + const desiredImageVersionData = (containerImageFromCloudly.data.versions || []).find((versionArg) => { + return versionArg.versionString === desiredImageVersion; + }); + const desiredRegistryDigest = desiredImageVersionData?.digest || ( + containerImageFromCloudly.data.lastPushEvent?.tag === desiredImageVersion + ? containerImageFromCloudly.data.lastPushEvent.digest + : '' + ); + + return { + 'serve.zone.serviceId': serviceArgFromCloudly.id, + 'serve.zone.imageId': serviceArgFromCloudly.data.imageId || '', + 'serve.zone.imageVersion': desiredImageVersion, + 'serve.zone.registryImageUrl': serviceArgFromCloudly.data.registryTarget?.imageUrl || '', + 'serve.zone.registryDigest': desiredRegistryDigest || '', + }; + } + + private async pullRegistryTargetImage( + registryTargetArg: plugins.servezoneInterfaces.data.IRegistryTarget, + ): Promise { + const registryImageName = `${registryTargetArg.registryHost}/${registryTargetArg.repository}`; + const registryImageTag = registryTargetArg.tag || 'latest'; + const registryImageRef = `${registryImageName}:${registryImageTag}`; + const response = await this.coreflowRef.dockerHost.request( + 'POST', + `/images/create?fromImage=${encodeURIComponent(registryImageName)}&tag=${encodeURIComponent( + registryImageTag, + )}`, + ); + if (response.statusCode >= 300) { + const existingImage = await this.coreflowRef.dockerHost.getImageByName(registryImageRef); + if (existingImage) { + logger.log( + 'warn', + `registry pull failed for ${registryImageRef}, using locally cached image`, + ); + return existingImage; + } + throw new Error(`Failed to pull registry image ${registryImageRef}`); + } + + const localDockerImage = await this.coreflowRef.dockerHost.getImageByName(registryImageRef); + if (!localDockerImage) { + throw new Error(`Registry image ${registryImageRef} not found after pull`); + } + return localDockerImage; + } + /** * starts the cluster manager */ @@ -184,10 +241,21 @@ export class ClusterManager { await this.coreflowRef.cloudlyConnector.cloudlyApiClient.image.getImageById( serviceArgFromCloudly.data.imageId, ); + const deploymentLabels = this.getWorkloadServiceDeploymentLabels( + serviceArgFromCloudly, + containerImageFromCloudly, + ); let localDockerImage: plugins.docker.DockerImage; // lets get the docker image for the service - if (containerImageFromCloudly.data.location.internal) { + if (serviceArgFromCloudly.data.registryTarget) { + await this.coreflowRef.dockerHost.auth({ + username: this.coreflowRef.cloudlyConnector.identity.name, + password: this.coreflowRef.cloudlyConnector.coreflowJumpCode, + serveraddress: serviceArgFromCloudly.data.registryTarget.registryHost, + }); + localDockerImage = await this.pullRegistryTargetImage(serviceArgFromCloudly.data.registryTarget); + } else if (containerImageFromCloudly.data.location?.internal) { const imageStream = await containerImageFromCloudly.pullImageVersion( serviceArgFromCloudly.data.imageVersion, ); @@ -199,8 +267,8 @@ export class ClusterManager { }, ); } else if ( - containerImageFromCloudly.data.location.externalRegistryId && - containerImageFromCloudly.data.location.externalImageTag + containerImageFromCloudly.data.location?.externalRegistryId && + containerImageFromCloudly.data.location?.externalImageTag ) { const externalRegistry = await this.coreflowRef.cloudlyConnector.cloudlyApiClient.externalRegistry.getRegistryById( @@ -243,13 +311,24 @@ export class ClusterManager { throw new Error(`Missing required Docker network ${this.commonDockerData.networkNames.sznWebgateway}`); } - if (containerService && (await containerService.needsUpdate())) { - await containerService.remove(); - if (containerSecret) { - await containerSecret.remove(); + if (containerService) { + const existingLabels = containerService.Spec.Labels || {}; + const cloudlyDeploymentLabelsChanged = Object.entries(deploymentLabels).some(([key, value]) => { + return existingLabels[key] !== value; + }); + const dockerImageNeedsUpdate = serviceArgFromCloudly.data.registryTarget + ? false + : await containerService.needsUpdate(); + + if (cloudlyDeploymentLabelsChanged || dockerImageNeedsUpdate) { + logger.log('info', `service ${serviceArgFromCloudly.data.name} desired state changed, recreating`); + await containerService.remove(); + if (containerSecret) { + await containerSecret.remove(); + } + containerService = null; + containerSecret = null; } - containerService = null; - containerSecret = null; } if (!containerService) { @@ -278,7 +357,7 @@ export class ClusterManager { networks: [webGatewayNetwork], secrets: [containerSecret], ports: [], - labels: {}, + labels: deploymentLabels, resources: serviceArgFromCloudly.data.resources, // TODO: introduce a clean name here, that is guaranteed to work with APIs. networkAlias: serviceArgFromCloudly.data.name,