feat: reconcile registry image updates

This commit is contained in:
2026-04-28 16:07:32 +00:00
parent b221e9fe43
commit f8e8cc43c4
3 changed files with 102 additions and 23 deletions
+2 -2
View File
@@ -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"
},
+11 -11
View File
@@ -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
+84 -5
View File
@@ -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<plugins.docker.DockerImage> {
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,7 +311,17 @@ export class ClusterManager {
throw new Error(`Missing required Docker network ${this.commonDockerData.networkNames.sznWebgateway}`);
}
if (containerService && (await containerService.needsUpdate())) {
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();
@@ -251,6 +329,7 @@ export class ClusterManager {
containerService = null;
containerSecret = null;
}
}
if (!containerService) {
containerSecret = await this.coreflowRef.dockerHost.getSecretByName(
@@ -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,