feat: reconcile registry image updates
This commit is contained in:
+2
-2
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+11
-11
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user