@controlplane/cli
Version:
Control Plane Corporation CLI
203 lines • 10.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.K8sWorkloadHandler = void 0;
const pod_template_1 = require("../processors/pod-template");
const helper_1 = require("../../util/helper");
const constants_1 = require("../../config/constants");
class K8sWorkloadHandler {
constructor(filePath, k8sWorkload, mapper) {
this.filePath = filePath;
this.k8sWorkload = k8sWorkload;
this.mapper = mapper;
this.workload = {
kind: 'workload',
name: k8sWorkload.metadata.name,
spec: {
type: 'standard',
containers: [],
defaultOptions: {
autoscaling: {
minScale: 1,
maxScale: 1,
},
capacityAI: false,
},
},
};
}
/*** Public Methods ***/
handle() {
var _a;
// Validate K8s workload
this.validate();
// Process labels as tags
if ((_a = this.k8sWorkload.spec.template.metadata) === null || _a === void 0 ? void 0 : _a.labels) {
this.workload.tags = this.k8sWorkload.spec.template.metadata.labels;
}
if (!this.workload.tags || Object.keys(this.workload.tags).length == 0) {
this.workload.tags = this.k8sWorkload.metadata.labels;
}
// Handle suspend
if (this.k8sWorkload.spec.paused) {
this.workload.spec.defaultOptions.suspend = true;
}
// Process template property
const templateProcessor = new pod_template_1.K8sPodTemplateProcessor(this.filePath, 'spec.template', this.k8sWorkload, this.workload, this.k8sWorkload.spec.template, this.mapper);
templateProcessor.process();
// Process autoscaling
this.processAutoscaling();
// Process rollout options
this.processRolloutOptions();
// Determine Workload Type
this.determineWorkloadType();
return this.workload;
}
/*** Private Methods ***/
processAutoscaling() {
// Process replicas first
if (this.k8sWorkload.spec.replicas) {
this.workload.spec.defaultOptions.autoscaling.minScale = this.k8sWorkload.spec.replicas;
this.workload.spec.defaultOptions.autoscaling.maxScale = this.k8sWorkload.spec.replicas;
}
// Override the value set previously with HPA if it is specified
this.processHpa();
}
processHpa() {
if (!this.mapper.k8sWorkloadNameToHpa[this.k8sWorkload.metadata.name]) {
return;
}
const hpa = this.mapper.k8sWorkloadNameToHpa[this.workload.name];
// Handle min and max scales
if (hpa.spec.minReplicas) {
this.workload.spec.defaultOptions.autoscaling.minScale = hpa.spec.minReplicas;
}
if (hpa.spec.maxReplicas) {
this.workload.spec.defaultOptions.autoscaling.maxScale = hpa.spec.maxReplicas;
}
// Handle scale to zero delay
if (hpa.spec.behavior && hpa.spec.behavior.scaleDown && hpa.spec.behavior.scaleDown.stabilizationWindowSeconds) {
this.workload.spec.defaultOptions.autoscaling.scaleToZeroDelay = hpa.spec.behavior.scaleDown.stabilizationWindowSeconds;
}
// Handle CPU Utilization target value
if (hpa.spec.metrics) {
for (const metric of hpa.spec.metrics) {
if (metric.containerResource &&
metric.containerResource.name.toLowerCase() === 'cpu' &&
metric.containerResource.target.type.toLowerCase() === 'utilization') {
// Take the first target value found
this.workload.spec.defaultOptions.autoscaling.target = metric.containerResource.target.averageUtilization;
break;
}
}
}
}
processRolloutOptions() {
if (!this.k8sWorkload.spec.minReadySeconds &&
!this.k8sWorkload.spec.podManagementPolicy &&
!this.k8sWorkload.spec.strategy &&
!this.k8sWorkload.spec.updateStrategy) {
return;
}
const rolloutOptions = {
minReadySeconds: this.k8sWorkload.spec.minReadySeconds || 0,
maxUnavailableReplicas: '1',
maxSurgeReplicas: '25%',
scalingPolicy: 'OrderedReady',
};
// Update strategy
if (this.k8sWorkload.spec.strategy && this.k8sWorkload.spec.strategy.rollingUpdate) {
// Set max surge replicas
if (this.k8sWorkload.spec.strategy.rollingUpdate.maxSurge) {
rolloutOptions.maxSurgeReplicas = this.k8sWorkload.spec.strategy.rollingUpdate.maxSurge.toString();
}
// Set max unavailable replicas
if (this.k8sWorkload.spec.strategy.rollingUpdate.maxUnavailable) {
rolloutOptions.maxUnavailableReplicas = this.k8sWorkload.spec.strategy.rollingUpdate.maxUnavailable.toString();
}
}
if (this.k8sWorkload.spec.updateStrategy && this.k8sWorkload.spec.updateStrategy.rollingUpdate) {
// Set max unavailable replicas
if (this.k8sWorkload.spec.updateStrategy.rollingUpdate.maxUnavailable) {
rolloutOptions.maxUnavailableReplicas = this.k8sWorkload.spec.updateStrategy.rollingUpdate.maxUnavailable.toString();
}
}
// Update scaling policy
if (this.k8sWorkload.spec.podManagementPolicy) {
rolloutOptions.scalingPolicy = this.k8sWorkload.spec.podManagementPolicy;
}
this.workload.spec.rolloutOptions = rolloutOptions;
}
determineWorkloadType() {
// A dedicated (read-write-once) volume set can only be mounted by a stateful workload; shared volume sets mount anywhere
const isStateful = this.mapper.workloadsWithDedicatedVolumeSets.has(this.workload);
// Collect the signals that make this workload incompatible with the 'serverless' type
const standardReasons = [];
// Iterate over each container
for (const container of this.workload.spec.containers) {
// Check if the container has many or no ports exposed
if (!container.ports || container.ports.length == 0 || container.ports.length > 1) {
standardReasons.push(`container '${container.name}' does not expose exactly one port`);
break;
}
// Check if liveness or readiness probes has grpc used
if (container.livenessProbe && container.livenessProbe.grpc) {
standardReasons.push(`container '${container.name}' uses a gRPC liveness probe`);
break;
}
if (container.readinessProbe && container.readinessProbe.grpc) {
standardReasons.push(`container '${container.name}' uses a gRPC readiness probe`);
break;
}
}
// Check if workload has rollout options
if (this.workload.spec.rolloutOptions) {
standardReasons.push('the workload converts rollout options from the K8s spec');
}
if (standardReasons.length > 0) {
this.workload.spec.type = 'standard';
}
// Type 'stateful' is stronger than type 'standard'
if (isStateful) {
this.workload.spec.type = 'stateful';
}
// Honor the workload type annotation as an explicit override
this.applyWorkloadTypeAnnotation(isStateful, standardReasons);
}
/**
* Pins the workload type from the workload type annotation when present.
*
* @param {boolean} isStateful - Whether the workload mounts a dedicated volume set and therefore requires the 'stateful' type.
* @param {string[]} standardReasons - The signals that make the workload incompatible with the 'serverless' type.
*/
applyWorkloadTypeAnnotation(isStateful, standardReasons) {
var _a;
const pinnedType = (0, helper_1.getCplnAnnotation)(constants_1.K8S_CPLN_ANNOTATION_WORKLOAD_TYPE, this.k8sWorkload, (_a = this.k8sWorkload.spec.template) === null || _a === void 0 ? void 0 : _a.metadata);
if (pinnedType === undefined) {
return;
}
// Only a fixed set of workload types can be pinned through the annotation
if (!constants_1.K8S_CPLN_ANNOTATION_WORKLOAD_TYPES.includes(pinnedType)) {
(0, helper_1.raiseCustomK8sError)(`Annotation '${constants_1.K8S_CPLN_ANNOTATION_WORKLOAD_TYPE}' must be one of [${constants_1.K8S_CPLN_ANNOTATION_WORKLOAD_TYPES.join(', ')}], found '${pinnedType}'`, this.filePath, this.k8sWorkload);
}
// A workload that mounts a dedicated (read-write-once) volume set can only be 'stateful'
if (isStateful && pinnedType != 'stateful') {
(0, helper_1.raiseCustomK8sError)(`Annotation '${constants_1.K8S_CPLN_ANNOTATION_WORKLOAD_TYPE}' cannot be set to '${pinnedType}' because the workload mounts a dedicated (read-write-once) volume set, which requires type 'stateful'`, this.filePath, this.k8sWorkload);
}
// Warn when the pinned type conflicts with the serverless requirements, the API validates authoritatively
if (pinnedType == 'serverless' && standardReasons.length > 0) {
this.mapper.warnings.push(`Workload '${this.workload.name}' is pinned to type 'serverless' but may be rejected by the API because: ${standardReasons.join('; ')}.`);
}
this.workload.spec.type = pinnedType;
}
// Validators //
validate() {
if (!this.k8sWorkload.spec) {
(0, helper_1.ensurePropertyPresence)('spec', this.filePath, this.k8sWorkload);
}
if (!this.k8sWorkload.spec.template) {
(0, helper_1.ensurePropertyPresence)('spec.template', this.filePath, this.k8sWorkload);
}
}
}
exports.K8sWorkloadHandler = K8sWorkloadHandler;
//# sourceMappingURL=workload.js.map