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/smartstream": "^3.4.0",
"@push.rocks/smartstring": "^4.1.0", "@push.rocks/smartstring": "^4.1.0",
"@push.rocks/taskbuffer": "^8.0.2", "@push.rocks/taskbuffer": "^8.0.2",
"@serve.zone/api": "^5.3.2", "@serve.zone/api": "^5.3.4",
"@serve.zone/interfaces": "^5.4.5", "@serve.zone/interfaces": "^5.4.6",
"@tsclass/tsclass": "^9.5.0", "@tsclass/tsclass": "^9.5.0",
"@types/node": "25.6.0" "@types/node": "25.6.0"
}, },
+11 -11
View File
@@ -69,11 +69,11 @@ importers:
specifier: ^8.0.2 specifier: ^8.0.2
version: 8.0.2 version: 8.0.2
'@serve.zone/api': '@serve.zone/api':
specifier: ^5.3.2 specifier: ^5.3.4
version: 5.3.2(@push.rocks/smartserve@2.0.3) version: 5.3.4(@push.rocks/smartserve@2.0.3)
'@serve.zone/interfaces': '@serve.zone/interfaces':
specifier: ^5.4.5 specifier: ^5.4.6
version: 5.4.5 version: 5.4.6
'@tsclass/tsclass': '@tsclass/tsclass':
specifier: ^9.5.0 specifier: ^9.5.0
version: 9.5.0 version: 9.5.0
@@ -1516,11 +1516,11 @@ packages:
'@sec-ant/readable-stream@0.4.1': '@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@serve.zone/api@5.3.2': '@serve.zone/api@5.3.4':
resolution: {integrity: sha512-ETQ4KSNfhDP7O1WxXXLcMn/A+jZtDfd7FjuQ0k3n8tnXG9hExh8ZmqvMwVj8eT2CnXO+xQVlbAgT0HLMLnxCfA==} resolution: {integrity: sha512-3CqyeZkZPCJ4775UoNPKfknhTlAk6zmU/MVVSu6DoIAWgUaOuAlLUHlV45xIGtHmKAppsiYUoyoEhBLTZf9iMw==}
'@serve.zone/interfaces@5.4.5': '@serve.zone/interfaces@5.4.6':
resolution: {integrity: sha512-asqUUjem3MGfIbseovHR8SxE+6FvjeQEYtV+PxcyY8YRXJ/vE3hNCDs7ePXgBbh4JXa+vNMaXHsFfz5Vrk6Ggg==} resolution: {integrity: sha512-o4k7Wr6t3NLiP6gfAZZz8Jx8RlQ4sZYHTbhr4WkXzGf78vczFRIuFLyY1Y+TTNzDLEIzLVIyMsuECMV1KTwB2Q==}
'@sindresorhus/is@5.6.0': '@sindresorhus/is@5.6.0':
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
@@ -6929,7 +6929,7 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {} '@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: dependencies:
'@api.global/typedrequest': 3.1.10 '@api.global/typedrequest': 3.1.10
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
@@ -6938,7 +6938,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstream': 3.4.0 '@push.rocks/smartstream': 3.4.0
'@serve.zone/interfaces': 5.4.5 '@serve.zone/interfaces': 5.4.6
'@tsclass/tsclass': 9.5.0 '@tsclass/tsclass': 9.5.0
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
@@ -6949,7 +6949,7 @@ snapshots:
- utf-8-validate - utf-8-validate
- vue - vue
'@serve.zone/interfaces@5.4.5': '@serve.zone/interfaces@5.4.6':
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartlog-interfaces': 3.0.2
+89 -10
View File
@@ -19,6 +19,63 @@ export class ClusterManager {
this.coreflowRef = coreflowRefArg; 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 * starts the cluster manager
*/ */
@@ -184,10 +241,21 @@ export class ClusterManager {
await this.coreflowRef.cloudlyConnector.cloudlyApiClient.image.getImageById( await this.coreflowRef.cloudlyConnector.cloudlyApiClient.image.getImageById(
serviceArgFromCloudly.data.imageId, serviceArgFromCloudly.data.imageId,
); );
const deploymentLabels = this.getWorkloadServiceDeploymentLabels(
serviceArgFromCloudly,
containerImageFromCloudly,
);
let localDockerImage: plugins.docker.DockerImage; let localDockerImage: plugins.docker.DockerImage;
// lets get the docker image for the service // 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( const imageStream = await containerImageFromCloudly.pullImageVersion(
serviceArgFromCloudly.data.imageVersion, serviceArgFromCloudly.data.imageVersion,
); );
@@ -199,8 +267,8 @@ export class ClusterManager {
}, },
); );
} else if ( } else if (
containerImageFromCloudly.data.location.externalRegistryId && containerImageFromCloudly.data.location?.externalRegistryId &&
containerImageFromCloudly.data.location.externalImageTag containerImageFromCloudly.data.location?.externalImageTag
) { ) {
const externalRegistry = const externalRegistry =
await this.coreflowRef.cloudlyConnector.cloudlyApiClient.externalRegistry.getRegistryById( 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}`); throw new Error(`Missing required Docker network ${this.commonDockerData.networkNames.sznWebgateway}`);
} }
if (containerService && (await containerService.needsUpdate())) { if (containerService) {
await containerService.remove(); const existingLabels = containerService.Spec.Labels || {};
if (containerSecret) { const cloudlyDeploymentLabelsChanged = Object.entries(deploymentLabels).some(([key, value]) => {
await containerSecret.remove(); 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) { if (!containerService) {
@@ -278,7 +357,7 @@ export class ClusterManager {
networks: [webGatewayNetwork], networks: [webGatewayNetwork],
secrets: [containerSecret], secrets: [containerSecret],
ports: [], ports: [],
labels: {}, labels: deploymentLabels,
resources: serviceArgFromCloudly.data.resources, resources: serviceArgFromCloudly.data.resources,
// TODO: introduce a clean name here, that is guaranteed to work with APIs. // TODO: introduce a clean name here, that is guaranteed to work with APIs.
networkAlias: serviceArgFromCloudly.data.name, networkAlias: serviceArgFromCloudly.data.name,