11package org.ods.component
22
3- import com.cloudbees.groovy.cps.NonCPS
43import groovy.transform.TypeChecked
54import groovy.transform.TypeCheckingMode
65import org.ods.services.JenkinsService
@@ -58,9 +57,8 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy {
5857 if (! config. containsKey(' helmEnvBasedValuesFiles' )) {
5958 config. helmEnvBasedValuesFiles = []
6059 }
61- if (! config. containsKey(' helmDefaultFlags' )) {
62- config. helmDefaultFlags = [' --install' , ' --atomic' ]
63- }
60+ // helmDefaultFlags are always set and cannot be overridden
61+ config. helmDefaultFlags = [' --install' , ' --atomic' ]
6462 if (! config. containsKey(' helmAdditionalFlags' )) {
6563 config. helmAdditionalFlags = []
6664 }
@@ -70,6 +68,9 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy {
7068 if (! config. helmPrivateKeyCredentialsId) {
7169 config. helmPrivateKeyCredentialsId = " ${ context.cdProject} -helm-private-key"
7270 }
71+ if (! config. containsKey(' helmReleasesHistoryLimit' )) {
72+ config. helmReleasesHistoryLimit = 5
73+ }
7374 this . context = context
7475 this . logger = logger
7576 this . steps = steps
@@ -142,13 +143,17 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy {
142143 mergedHelmValuesFiles. addAll(options. helmValuesFiles)
143144 mergedHelmValuesFiles. addAll(envConfigFiles)
144145
146+ // Add history-max flag to limit stored release revisions
147+ def mergedAdditionalFlags = options. helmAdditionalFlags. collect { it }
148+ mergedAdditionalFlags << " --history-max ${ options.helmReleasesHistoryLimit} " . toString()
149+
145150 openShift. helmUpgrade(
146151 targetProject,
147152 options. helmReleaseName,
148153 mergedHelmValuesFiles,
149154 mergedHelmValues,
150155 options. helmDefaultFlags,
151- options . helmAdditionalFlags ,
156+ mergedAdditionalFlags ,
152157 options. helmDiff
153158 )
154159 }
@@ -157,8 +162,8 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy {
157162
158163 // rollout returns a map like this:
159164 // [
160- // 'DeploymentConfig/foo': [[podName : 'foo-a ', ...], [podName: 'foo-b', ...] ],
161- // 'Deployment/bar': [[podName : 'bar-a ', ...]]
165+ // 'DeploymentConfig/foo': [deploymentId : 'foo', containers: [ ...], ...],
166+ // 'Deployment/bar': [deploymentId : 'bar', containers: [ ...], ... ]
162167 // ]
163168 @TypeChecked (TypeCheckingMode .SKIP )
164169 private Map<String , List<PodData > > getRolloutData (
@@ -169,143 +174,82 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy {
169174 Map<String , List<String > > deploymentKinds = helmStatus. resourcesByKind
170175 .findAll { kind , res -> kind in DEPLOYMENT_KINDS }
171176
177+ // Collect containers organized by resource (kind/name) to preserve all images
178+ Map<String , Map<String , String > > containersByResource = [:]
179+ Map<String , List<String > > resourcesByKind = [:]
180+
172181 deploymentKinds. each { kind , names ->
182+ resourcesByKind[kind] = names
173183 names. each { name ->
174- context. addDeploymentToArtifactURIs(" ${ name} -deploymentMean" ,
175- [
176- type : ' helm' ,
177- selector : options. selector,
178- namespace : context. targetProject,
179- chartDir : options. chartDir,
180- helmReleaseName : options. helmReleaseName,
181- helmEnvBasedValuesFiles : options. helmEnvBasedValuesFiles,
182- helmValuesFiles : options. helmValuesFiles,
183- helmValues : options. helmValues,
184- helmDefaultFlags : options. helmDefaultFlags,
185- helmAdditionalFlags : options. helmAdditionalFlags,
186- helmStatus : helmStatus. toMap(),
187- ]
184+ // Get container images directly from the resource spec (deployment/statefulset/cronjob)
185+ def containers = openShift. getContainerImagesWithNameFromPodSpec(
186+ context. targetProject, kind, name
188187 )
189- def podDataContext = [
190- " targetProject=${ context.targetProject} " ,
191- " selector=${ options.selector} " ,
192- " name=${ name} " ,
193- ]
194- def msgPodsNotFound = " Could not find 'running' pod(s) for '${ podDataContext.join(', ')} '"
195- List<PodData > podData = null
196- for (def i = 0 ; i < options. deployTimeoutRetries; i++ ) {
197- podData = openShift. checkForPodData(context. targetProject, options. selector, name)
198- if (podData) {
199- break
200- }
201- steps. echo(" ${ msgPodsNotFound} - waiting" )
202- steps. sleep(12 )
203- }
204- if (! podData) {
205- throw new RuntimeException (msgPodsNotFound)
206- }
207- logger. debug(" Helm podData for ${ podDataContext.join(', ')} : ${ podData} " )
188+ logger. debug(" Helm container images for ${ kind} /${ name} : ${ containers} " )
208189
209- rolloutData[ " ${ kind } / ${ name} " ] = podData
190+ containersByResource[ name] = containers
210191
211- // We need to find the pod that was created as a result of the deployment.
212- // The previous pod may still be alive when we use a rollout strategy.
213- // We can tell one from the other using their creation timestamp,
214- // being the most recent the one we are interested in.
215- def latestPods = getLatestPods(podData)
216- // While very unlikely, it may happen that there is more than one pod with the same timestamp.
217- // Note that timestamp resolution is seconds.
218- // If that happens, we are unable to know which is the correct pod.
219- // However, it doesn't matter which pod is the right one, if they all have the same images.
220- def sameImages = haveSameImages(latestPods)
221- if (! sameImages) {
222- throw new RuntimeException (
223- " Unable to determine the most recent Pod. " +
224- " Multiple pods running with the same latest creation timestamp " +
225- " and different images found for ${ name} "
226- )
227- }
228- // Deployment and DeploymentConfig resource.
229- context. addDeploymentToArtifactURIs(name, latestPods[0 ]?. toMap())
192+ def podData = new PodData (
193+ deploymentId : name,
194+ containers : containers,
195+ )
196+ rolloutData[name] = [podData]
230197 }
231198 }
232- return rolloutData
233- }
234-
235- /**
236- * Returns the pods with the latest creation timestamp.
237- * Note that the resolution of this timestamp is seconds and there may be more than one pod with the same
238- * latest timestamp.
239- *
240- * @param pods the pods over which to find the latest ones.
241- * @return a list with all the pods sharing the same, latest timestamp.
242- */
243- @NonCPS
244- private static List getLatestPods (Iterable pods ) {
245- return maxElements(pods) { it. podMetaDataCreationTimestamp }
246- }
247-
248- /**
249- * Checks whether all the given pods contain the same images, ignoring order and multiplicity.
250- *
251- * @param pods the pods to check for image equality.
252- * @return true if all the pods have the same images or false otherwise.
253- */
254- @NonCPS
255- private static boolean haveSameImages (Iterable pods ) {
256- return areEqual(pods) { a , b ->
257- def imagesA = a. containers. values() as Set
258- def imagesB = b. containers. values() as Set
259- return imagesA == imagesB
260- }
261- }
262-
263- /**
264- * Selects the items in the iterable which when passed as a parameter to the supplied closure
265- * return the maximum value. A null return value represents the least possible return value,
266- * so any item for which the supplied closure returns null, won't be selected (unless all items return null).
267- * The return list contains all the elements that returned the maximum value.
268- *
269- * @param iterable the iterable over which to search for maximum values.
270- * @param getValue a closure returning the value that corresponds to each element.
271- * @return the list of all the elements for which the closure returns the maximum value.
272- */
273- @NonCPS
274- private static List maxElements (Iterable iterable , Closure getValue ) {
275- if (! iterable) {
276- return [] // Return an empty list if the iterable is null or empty
277- }
278-
279- // Find the maximum value using the closure
280- def maxValue = iterable. collect(getValue). max()
281199
282- // Find all elements with the maximum value
283- return iterable. findAll { getValue(it) == maxValue }
284- }
285-
286- /**
287- * Checks whether all the elements in the given iterable are deemed as equal by the given closure.
288- *
289- * @param iterable the iterable over which to check for element equality.
290- * @param equals a closure that checks two elements for equality.
291- * @return true if all the elements are equal or false otherwise.
292- */
293- @NonCPS
294- private static boolean areEqual (Iterable iterable , Closure equals ) {
295- def equal = true
296- if (iterable) {
297- def first = true
298- def base = null
299- iterable. each {
300- if (first) {
301- base = it
302- first = false
303- } else if (! equals(base, it)) {
304- equal = false
200+ // Create a single deploymentMean entry for the entire helm release
201+ def deploymentMean = [
202+ type : ' helm' ,
203+ selector : options. selector,
204+ namespace : context. targetProject,
205+ chartDir : options. chartDir,
206+ helmReleaseName : options. helmReleaseName,
207+ helmEnvBasedValuesFiles : options. helmEnvBasedValuesFiles,
208+ helmValuesFiles : options. helmValuesFiles,
209+ helmValues : options. helmValues,
210+ helmDefaultFlags : options. helmDefaultFlags,
211+ helmAdditionalFlags : options. helmAdditionalFlags,
212+ helmStatus : helmStatus. toMap(),
213+ resources : resourcesByKind,
214+ ]
215+
216+ // Store deployment mean once per helm release (not per resource)
217+ context. addDeploymentToArtifactURIs(" ${ options.helmReleaseName} -deploymentMean" , deploymentMean)
218+
219+ // Store consolidated deployment data with all unique containers flattened
220+ // Ensure all container images are plain strings (handle deserialization artifacts)
221+ // Flatten containers from all resources into a single map to support multiple containers per resource
222+ def flattenedContainers = [:]
223+ containersByResource. each { resourceName , containerMap ->
224+ containerMap. each { containerName , image ->
225+ // Extract string value - image might be String, or wrapped in Map/List after deserialization
226+ def imageStr = image
227+ while (Map . isInstance(imageStr) && imageStr. size() > 0 ) {
228+ imageStr = imageStr. values()[0 ]
229+ }
230+ while (List . isInstance(imageStr) && imageStr. size() > 0 ) {
231+ imageStr = imageStr[0 ]
232+ }
233+ def stringValue = imageStr. toString()
234+ // Ensure no trailing bracket or other artifacts remain from serialization
235+ // Use double-escaped bracket to ensure proper regex matching in Groovy
236+ stringValue = stringValue. replaceAll(' \\ ]\\ s*$' , ' ' )
237+ // Create a unique key for each container
238+ // If resource has only one container, use resource name; otherwise use resource::container format
239+ def containerKey = containerMap. size() == 1 ? resourceName : " ${ resourceName} ::${ containerName} "
240+ // Only add if this image isn't already present (avoid duplicates of same image)
241+ if (! flattenedContainers. values(). contains(stringValue)) {
242+ flattenedContainers[containerKey] = stringValue
305243 }
306244 }
307245 }
308- return equal
246+ def consolidatedPodData = new PodData (
247+ deploymentId : options. helmReleaseName,
248+ containers : flattenedContainers as Map<String , String > ,
249+ )
250+ context. addDeploymentToArtifactURIs(options. helmReleaseName, consolidatedPodData. toMap())
251+
252+ return rolloutData
309253 }
310254
311- }
255+ }
0 commit comments