Skip to content

Commit e5a4857

Browse files
feat(orca-front50): Adding scheduling agent to Disable unused Pipelines on Front50
1 parent 086a55d commit e5a4857

File tree

8 files changed

+452
-0
lines changed

8 files changed

+452
-0
lines changed

orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/persistence/DualExecutionRepository.kt

+10
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,16 @@ class DualExecutionRepository(
230230
).distinctBy { it.id }
231231
}
232232

233+
override fun retrievePipelineConfigIdsForApplicationWithCriteria(
234+
@Nonnull application: String,
235+
@Nonnull criteria: ExecutionCriteria
236+
): List<String> {
237+
return (
238+
primary.retrievePipelineConfigIdsForApplicationWithCriteria(application, criteria) +
239+
previous.retrievePipelineConfigIdsForApplicationWithCriteria(application, criteria)
240+
)
241+
}
242+
233243
override fun retrievePipelinesForPipelineConfigIdsBetweenBuildTimeBoundary(
234244
pipelineConfigIds: MutableList<String>,
235245
buildTimeStartBoundary: Long,

orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/persistence/ExecutionRepository.java

+3
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ Collection<PipelineExecution> retrievePipelineExecutionDetailsForApplication(
107107
@Nonnull List<String> pipelineConfigIds,
108108
int queryTimeoutSeconds);
109109

110+
List<String> retrievePipelineConfigIdsForApplicationWithCriteria(
111+
@Nonnull String application, @Nonnull ExecutionCriteria criteria);
112+
110113
/**
111114
* Returns executions in the time boundary. Redis impl does not respect pageSize or offset params,
112115
* and returns all executions. Sql impl respects these params.

orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/persistence/InMemoryExecutionRepository.kt

+10
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,16 @@ class InMemoryExecutionRepository : ExecutionRepository {
305305
.distinctBy { it.id }
306306
}
307307

308+
override fun retrievePipelineConfigIdsForApplicationWithCriteria(
309+
@Nonnull application: String,
310+
@Nonnull criteria: ExecutionCriteria
311+
): List<String> {
312+
return pipelines.values
313+
.filter { it.application == application }
314+
.applyCriteria(criteria)
315+
.map { it.id }
316+
}
317+
308318
override fun retrieveOrchestrationForCorrelationId(correlationId: String): PipelineExecution {
309319
return retrieveByCorrelationId(ORCHESTRATION, correlationId)
310320
}

orca-front50/orca-front50.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ dependencies {
4040
testImplementation("com.github.ben-manes.caffeine:guava")
4141
testImplementation("org.apache.groovy:groovy-json")
4242
testRuntimeOnly("net.bytebuddy:byte-buddy")
43+
testImplementation("org.junit.jupiter:junit-jupiter-api")
44+
testImplementation("org.assertj:assertj-core")
45+
testImplementation("org.mockito:mockito-junit-jupiter")
4346
}
4447

4548
sourceSets {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2024 Harness, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.netflix.spinnaker.orca.front50.scheduling;
17+
18+
import static com.netflix.spinnaker.orca.api.pipeline.models.ExecutionType.PIPELINE;
19+
20+
import com.netflix.spectator.api.Id;
21+
import com.netflix.spectator.api.LongTaskTimer;
22+
import com.netflix.spectator.api.Registry;
23+
import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerHttpException;
24+
import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus;
25+
import com.netflix.spinnaker.orca.front50.Front50Service;
26+
import com.netflix.spinnaker.orca.notifications.AbstractPollingNotificationAgent;
27+
import com.netflix.spinnaker.orca.notifications.NotificationClusterLock;
28+
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository;
29+
import java.time.Clock;
30+
import java.time.ZoneOffset;
31+
import java.util.Arrays;
32+
import java.util.List;
33+
import java.util.Map;
34+
import java.util.stream.Collectors;
35+
import org.slf4j.Logger;
36+
import org.slf4j.LoggerFactory;
37+
import org.springframework.beans.factory.annotation.Autowired;
38+
import org.springframework.beans.factory.annotation.Value;
39+
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
40+
import org.springframework.stereotype.Component;
41+
42+
@Component
43+
@ConditionalOnExpression(
44+
"${pollers.unused-pipelines-disable.enabled:false} && ${execution-repository.sql.enabled:false}")
45+
public class UnusedPipelineDisablePollingNotificationAgent
46+
extends AbstractPollingNotificationAgent {
47+
48+
Front50Service front50service;
49+
50+
private static final List<String> COMPLETED_STATUSES =
51+
ExecutionStatus.COMPLETED.stream().map(Enum::toString).collect(Collectors.toList());
52+
53+
private final Logger log =
54+
LoggerFactory.getLogger(UnusedPipelineDisablePollingNotificationAgent.class);
55+
56+
private final Clock clock;
57+
private final ExecutionRepository executionRepository;
58+
private final Registry registry;
59+
60+
private final long pollingIntervalSec;
61+
private final int thresholdDays;
62+
private final boolean dryRun;
63+
64+
private final Id timerId;
65+
66+
@Autowired
67+
public UnusedPipelineDisablePollingNotificationAgent(
68+
NotificationClusterLock clusterLock,
69+
ExecutionRepository executionRepository,
70+
Front50Service front50Service,
71+
Clock clock,
72+
Registry registry,
73+
@Value("${pollers.unused-pipelines-disable.interval-sec:3600}") long pollingIntervalSec,
74+
@Value("${pollers.unused-pipelines-disable.threshold-days:365}") int thresholdDays,
75+
@Value("${pollers.unused-pipelines-disable.dry-run:true}") boolean dryRun) {
76+
super(clusterLock);
77+
this.executionRepository = executionRepository;
78+
this.clock = clock;
79+
this.registry = registry;
80+
this.pollingIntervalSec = pollingIntervalSec;
81+
this.thresholdDays = thresholdDays;
82+
this.dryRun = dryRun;
83+
this.front50service = front50Service;
84+
85+
timerId = registry.createId("pollers.unusedPipelineDisable.timing");
86+
}
87+
88+
@Override
89+
protected long getPollingInterval() {
90+
return pollingIntervalSec * 1000;
91+
}
92+
93+
@Override
94+
protected String getNotificationType() {
95+
return UnusedPipelineDisablePollingNotificationAgent.class.getSimpleName();
96+
}
97+
98+
@Override
99+
protected void tick() {
100+
LongTaskTimer timer = registry.longTaskTimer(timerId);
101+
long timerId = timer.start();
102+
try {
103+
executionRepository
104+
.retrieveAllApplicationNames(PIPELINE)
105+
.forEach(
106+
app -> {
107+
log.info("Evaluating " + app + " for unused pipelines");
108+
List<String> pipelineConfigIds =
109+
front50service.getPipelines(app, false, "enabled").stream()
110+
.map(p -> (String) p.get("id"))
111+
.collect(Collectors.toList());
112+
113+
ExecutionRepository.ExecutionCriteria criteria =
114+
new ExecutionRepository.ExecutionCriteria();
115+
criteria.setStatuses(COMPLETED_STATUSES);
116+
criteria.setStartTimeCutoff(
117+
clock.instant().atZone(ZoneOffset.UTC).minusDays(thresholdDays).toInstant());
118+
119+
List<String> orcaExecutionsCount =
120+
executionRepository.retrievePipelineConfigIdsForApplicationWithCriteria(
121+
app, criteria);
122+
123+
disableAppPipelines(app, orcaExecutionsCount, pipelineConfigIds);
124+
});
125+
} catch (Exception e) {
126+
log.error("Disabling pipelines failed", e);
127+
} finally {
128+
timer.stop(timerId);
129+
}
130+
}
131+
132+
public void disableAppPipelines(
133+
String app, List<String> orcaExecutionsCount, List<String> front50PipelineConfigIds) {
134+
135+
List<String> front50PipelineConfigIdsNotExecuted =
136+
front50PipelineConfigIds.stream()
137+
.filter(p -> !orcaExecutionsCount.contains(p))
138+
.collect(Collectors.toList());
139+
140+
log.info(
141+
"Found "
142+
+ front50PipelineConfigIdsNotExecuted.size()
143+
+ " pipelines to disable for Application "
144+
+ app);
145+
front50PipelineConfigIdsNotExecuted.forEach(
146+
p -> {
147+
log.info("Disabling pipeline execution " + p);
148+
if (!dryRun) {
149+
disableFront50PipelineConfigId(p);
150+
}
151+
});
152+
}
153+
154+
public void disableFront50PipelineConfigId(String pipelineConfigId) {
155+
Map<String, Object> pipeline = front50service.getPipeline(pipelineConfigId);
156+
if (pipeline.get("disabled") == null || !(boolean) pipeline.get("disabled")) {
157+
pipeline.put("disabled", true);
158+
try {
159+
front50service.updatePipeline(pipelineConfigId, pipeline);
160+
} catch (SpinnakerHttpException e) {
161+
if (Arrays.asList(404, 403).contains(e.getResponseCode())) {
162+
log.warn("Failed to disable pipeline " + pipelineConfigId + " due to " + e.getMessage());
163+
} else {
164+
throw e;
165+
}
166+
}
167+
}
168+
}
169+
}

0 commit comments

Comments
 (0)