Skip to content

Commit 3b08a4f

Browse files
authored
feat(core): add validationOnly mode to prevent change execution (#880)
1 parent bc0712b commit 3b08a4f

20 files changed

Lines changed: 885 additions & 99 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2026 Flamingock (https://www.flamingock.io)
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 io.flamingock.internal.common.core.error;
17+
18+
/**
19+
* Exception thrown when Flamingock runs in validation-only mode and detects pending changes.
20+
*/
21+
public class PendingChangesException extends FlamingockException {
22+
23+
public PendingChangesException() {
24+
super("Flamingock validationOnly=true: pending changes detected. Apply them before running in validation-only mode.");
25+
}
26+
}

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/operation/OperationType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
public enum OperationType {
1919
EXECUTE_APPLY,
2020
EXECUTE_ROLLBACK,
21-
EXECUTE_VALIDATE,
2221
EXECUTE_DRYRUN,
22+
VALIDATE_APPLY,
2323
AUDIT_LIST,
2424
AUDIT_FIX,
2525
ISSUE_LIST,

core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import io.flamingock.internal.core.context.SimpleContext;
3232
import io.flamingock.internal.core.external.store.AuditStore;
3333
import io.flamingock.internal.core.external.store.audit.AuditPersistence;
34+
import io.flamingock.internal.core.operation.OperationResolver;
3435
import io.flamingock.internal.core.plan.ExecutionPlanner;
3536
import io.flamingock.internal.core.event.CompositeEventPublisher;
3637
import io.flamingock.internal.core.event.EventPublisher;
@@ -43,7 +44,6 @@
4344
import io.flamingock.internal.core.event.model.IStageFailedEvent;
4445
import io.flamingock.internal.core.event.model.IStageIgnoredEvent;
4546
import io.flamingock.internal.core.event.model.IStageStartedEvent;
46-
import io.flamingock.internal.core.operation.OperationFactory;
4747
import io.flamingock.internal.core.operation.RunnableOperation;
4848
import io.flamingock.internal.core.pipeline.loaded.LoadedPipeline;
4949
import io.flamingock.internal.core.plugin.Plugin;
@@ -213,7 +213,7 @@ public final Runner build() {
213213

214214
FlamingockArguments flamingockArgs = FlamingockArguments.parse(applicationArgs);
215215

216-
OperationFactory operationFactory = new OperationFactory(
216+
OperationResolver operationResolver = new OperationResolver(
217217
runnerId,
218218
flamingockArgs,
219219
pipeline,
@@ -227,7 +227,7 @@ public final Runner build() {
227227
coreConfiguration.isThrowExceptionIfCannotObtainLock(),
228228
persistence.getCloser()
229229
);
230-
RunnableOperation<?, ?> operation = operationFactory.getOperation();
230+
RunnableOperation<?, ?> operation = operationResolver.getOperation();
231231

232232
return new RunnerFactory(runnerId, flamingockArgs, operation, persistence.getCloser()).create();
233233
}
@@ -359,6 +359,15 @@ public HOLDER setEnabled(boolean enabled) {
359359
return getSelf();
360360
}
361361

362+
public HOLDER setValidationOnly(boolean validationOnly) {
363+
coreConfiguration.setValidationOnly(validationOnly);
364+
return getSelf();
365+
}
366+
367+
public boolean isValidationOnly() {
368+
return coreConfiguration.isValidationOnly();
369+
}
370+
362371
@Override
363372
public HOLDER setServiceIdentifier(String serviceIdentifier) {
364373
coreConfiguration.setServiceIdentifier(serviceIdentifier);

core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/args/FlamingockArguments.java

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,22 @@ public class FlamingockArguments {
3535

3636
private final boolean cliMode;
3737
private final OperationType operation;
38-
private final boolean operationProvided;
3938
private final String outputFile;
4039
private final Map<String, String> remainingArgs;
4140

4241
private FlamingockArguments(boolean cliMode,
4342
OperationType operation,
44-
boolean operationProvided,
4543
String outputFile,
4644
Map<String, String> remainingArgs) {
4745
this.cliMode = cliMode;
4846
this.operation = operation;
49-
this.operationProvided = operationProvided;
5047
this.outputFile = outputFile;
5148
this.remainingArgs = Collections.unmodifiableMap(remainingArgs);
5249
}
5350

5451
public static FlamingockArguments parse(String[] args) {
5552
if (args == null || args.length == 0) {
56-
return new FlamingockArguments(false, OperationType.EXECUTE_APPLY, false, null, Collections.emptyMap());
53+
return new FlamingockArguments(false, null, null, Collections.emptyMap());
5754
}
5855

5956
boolean cliMode = false;
@@ -105,8 +102,8 @@ public static FlamingockArguments parse(String[] args) {
105102
}
106103
}
107104

108-
OperationType effectiveOperation = operationProvided ? operation : OperationType.EXECUTE_APPLY;
109-
return new FlamingockArguments(cliMode, effectiveOperation, operationProvided, outputFile, remaining);
105+
OperationType effectiveOperation = operationProvided ? operation : null;
106+
return new FlamingockArguments(cliMode, effectiveOperation, outputFile, remaining);
110107
}
111108

112109
private static boolean parseBoolean(String key, String value) {
@@ -150,12 +147,8 @@ public boolean isCliMode() {
150147
return cliMode;
151148
}
152149

153-
public OperationType getOperation() {
154-
return operation;
155-
}
156-
157-
public boolean isOperationProvided() {
158-
return operationProvided;
150+
public Optional<OperationType> getOperation() {
151+
return Optional.ofNullable(operation);
159152
}
160153

161154
public Optional<String> getOutputFile() {

core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/CliRunner.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
import io.flamingock.internal.common.core.response.ResponseChannel;
1919
import io.flamingock.internal.common.core.response.ResponseEnvelope;
2020
import io.flamingock.internal.common.core.response.ResponseError;
21+
import io.flamingock.internal.core.builder.args.FlamingockArguments;
2122
import io.flamingock.internal.core.operation.AbstractOperationResult;
22-
import io.flamingock.internal.common.core.operation.OperationType;
2323
import io.flamingock.internal.core.operation.RunnableOperation;
2424
import io.flamingock.internal.util.log.FlamingockLoggerFactory;
2525
import org.slf4j.Logger;
@@ -35,16 +35,16 @@ public class CliRunner implements Runner {
3535
private final RunnableOperation<?, ?> operation;
3636
private final Runnable finalizer;
3737
private final ResponseChannel channel;
38-
private final OperationType operationType;
38+
private final FlamingockArguments flamingockArgs;
3939

4040
public CliRunner(RunnableOperation<?, ?> operation,
4141
Runnable finalizer,
4242
ResponseChannel channel,
43-
OperationType operationType) {
43+
FlamingockArguments flamingockArgs) {
4444
this.operation = operation;
4545
this.finalizer = finalizer;
4646
this.channel = channel;
47-
this.operationType = operationType;
47+
this.flamingockArgs = flamingockArgs;
4848
}
4949

5050
@Override
@@ -92,7 +92,9 @@ public void run() {
9292

9393
private void writeResponse(AbstractOperationResult result, Throwable error, long durationMs) {
9494
ResponseEnvelope envelope;
95-
String operationName = operationType.name();
95+
String operationName = flamingockArgs.getOperation()
96+
.map(Enum::name)
97+
.orElse("unknown");
9698

9799
if (error != null) {
98100
envelope = ResponseEnvelope.failure(operationName, ResponseError.from(error), durationMs);

core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/runner/RunnerFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ private Runner createCliRunner() {
6060
.map(outputFile -> (ResponseChannel) new FileResponseChannel(outputFile, JsonObjectMapper.DEFAULT_INSTANCE))
6161
.orElseGet(NoOpResponseChannel::new);
6262

63-
return new CliRunner(operation, finalizer, channel, flamingockArgs.getOperation());
63+
return new CliRunner(operation, finalizer, channel, flamingockArgs);
6464
}
6565

6666
private Runner createDefaultRunner() {

core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfigurable.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ public interface CoreConfigurable {
3838

3939
void setEnabled(boolean enabled);
4040

41+
void setValidationOnly(boolean validationOnly);
42+
43+
boolean isValidationOnly();
44+
4145
void setServiceIdentifier(String serviceIdentifier);
4246

4347
void setMetadata(Map<String, Object> metadata);

core/flamingock-core/src/main/java/io/flamingock/internal/core/configuration/core/CoreConfiguration.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ public class CoreConfiguration implements CoreConfigurable {
3535
*/
3636
private boolean enabled = true;
3737

38+
/**
39+
* If true, Flamingock will only validate that no pending changes exist without applying them
40+
* When Flamingock runs through the CLI, the CLI operation takes precedence over this flag
41+
* Default false
42+
*/
43+
private boolean validationOnly = false;
44+
3845
/**
3946
* Service identifier.
4047
*/
@@ -91,6 +98,11 @@ public void setEnabled(boolean enabled) {
9198
this.enabled = enabled;
9299
}
93100

101+
@Override
102+
public void setValidationOnly(boolean validationOnly) {
103+
this.validationOnly = validationOnly;
104+
}
105+
94106
@Override
95107
public void setServiceIdentifier(String serviceIdentifier) {
96108
this.serviceIdentifier = serviceIdentifier;
@@ -126,6 +138,11 @@ public boolean isEnabled() {
126138
return enabled;
127139
}
128140

141+
@Override
142+
public boolean isValidationOnly() {
143+
return validationOnly;
144+
}
145+
129146
@Override
130147
public String getServiceIdentifier() {
131148
return serviceIdentifier;

core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/execute/ExecuteOperation.java renamed to core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/AbstractPipelineTraverseOperation.java

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package io.flamingock.internal.core.operation.execute;
16+
package io.flamingock.internal.core.operation;
1717

1818
import io.flamingock.internal.common.core.error.FlamingockException;
19+
import io.flamingock.internal.common.core.error.PendingChangesException;
1920
import io.flamingock.internal.common.core.response.data.ErrorInfo;
2021
import io.flamingock.internal.common.core.response.data.ExecuteResponseData;
2122
import io.flamingock.internal.common.core.response.data.StageResult;
@@ -26,8 +27,8 @@
2627
import io.flamingock.internal.core.event.model.impl.StageCompletedEvent;
2728
import io.flamingock.internal.core.event.model.impl.StageFailedEvent;
2829
import io.flamingock.internal.core.event.model.impl.StageStartedEvent;
29-
import io.flamingock.internal.core.operation.Operation;
30-
import io.flamingock.internal.core.operation.OperationException;
30+
import io.flamingock.internal.core.operation.execute.ExecuteArgs;
31+
import io.flamingock.internal.core.operation.execute.ExecuteResult;
3132
import io.flamingock.internal.core.operation.result.ExecutionResultBuilder;
3233
import io.flamingock.internal.core.pipeline.execution.ExecutableStage;
3334
import io.flamingock.internal.core.pipeline.execution.ExecutionContext;
@@ -48,9 +49,9 @@
4849
import java.util.List;
4950

5051
/**
51-
* Executes the pipeline and returns structured result data.
52+
* Common execution flow for Apply and Validate operations
5253
*/
53-
public class ExecuteOperation implements Operation<ExecuteArgs, ExecuteResult> {
54+
public abstract class AbstractPipelineTraverseOperation implements Operation<ExecuteArgs, ExecuteResult> {
5455

5556
private static final Logger logger = FlamingockLoggerFactory.getLogger("PipelineRunner");
5657

@@ -66,15 +67,15 @@ public class ExecuteOperation implements Operation<ExecuteArgs, ExecuteResult> {
6667

6768
private final OrphanExecutionContext orphanExecutionContext;
6869

69-
private final Runnable finalizer;
70+
protected final Runnable finalizer;
7071

71-
public ExecuteOperation(RunnerId runnerId,
72-
ExecutionPlanner executionPlanner,
73-
StageExecutor stageExecutor,
74-
OrphanExecutionContext orphanExecutionContext,
75-
EventPublisher eventPublisher,
76-
boolean throwExceptionIfCannotObtainLock,
77-
Runnable finalizer) {
72+
public AbstractPipelineTraverseOperation(RunnerId runnerId,
73+
ExecutionPlanner executionPlanner,
74+
StageExecutor stageExecutor,
75+
OrphanExecutionContext orphanExecutionContext,
76+
EventPublisher eventPublisher,
77+
boolean throwExceptionIfCannotObtainLock,
78+
Runnable finalizer) {
7879
this.runnerId = runnerId;
7980
this.executionPlanner = executionPlanner;
8081
this.stageExecutor = stageExecutor;
@@ -84,19 +85,19 @@ public ExecuteOperation(RunnerId runnerId,
8485
this.finalizer = finalizer;
8586
}
8687

88+
protected abstract boolean validateOnlyMode();
8789

8890
@Override
8991
public ExecuteResult execute(ExecuteArgs args) {
9092
ExecuteResponseData result;
9193
try {
9294
result = this.execute(args.getPipeline());
9395
} catch (OperationException operationException) {
94-
result = operationException.getResult();
9596
throw operationException;
9697
} catch (Throwable throwable) {
9798
throw processAndGetFlamingockException(throwable, null);
9899
} finally {
99-
finalizer.run();
100+
this.finalizer.run();
100101
}
101102
return new ExecuteResult(result);
102103
}
@@ -130,6 +131,9 @@ private ExecuteResponseData execute(LoadedPipeline pipeline) throws FlamingockEx
130131
execution.validate();
131132

132133
if (execution.isExecutionRequired()) {
134+
if (validateOnlyMode()) {
135+
throw new PendingChangesException();
136+
}
133137
execution.applyOnEach((executionId, lock, executableStage) -> {
134138
StageResult stageResult = runStage(executionId, lock, executableStage);
135139
resultBuilder.addStage(stageResult);
@@ -208,5 +212,4 @@ private FlamingockException processAndGetFlamingockException(Throwable exception
208212
eventPublisher.publish(new PipelineFailedEvent(flamingockException));
209213
return flamingockException;
210214
}
211-
212215
}

0 commit comments

Comments
 (0)