Skip to content

Commit ff005ea

Browse files
committed
feat(core): add provider groups and batch operations for multi-cluster management
Implements cross-cluster batch operations as specified in the multi-cluster design. Teams can now apply resources to multiple providers in a single invocation using --provider-all or --provider-group flags. Changes: - ReconciliationContext: add providerNames() list and continueOnError() flag - ProviderOptionMixin: add --provider-all, --provider-group, --continue-on-error CLI flags - ProviderResolver: resolves provider names from CLI flags and configuration - ProviderGroupRegistry: manages named groups of providers - JikkouConfigProperties: add provider-groups config section - DefaultApi: multi-provider iteration for reconcile, diff, patch, replace - ResourceReconcileRequest: add providers list and continueOnError for REST API - ReconciliationContextAdapter: wire through multi-provider fields - ResourceReconcileRequestFactory: pass through new context fields Fail-fast by default; --continue-on-error enables fail-open behavior. Results aggregated across providers in response.
1 parent 3037905 commit ff005ea

17 files changed

Lines changed: 1017 additions & 65 deletions

File tree

cli/src/main/java/io/streamthoughts/jikkou/client/command/DiffCommand.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.streamthoughts.jikkou.client.command.validate.ValidationErrorsWriter;
1010
import io.streamthoughts.jikkou.core.JikkouApi;
1111
import io.streamthoughts.jikkou.core.ReconciliationContext;
12+
import io.streamthoughts.jikkou.core.config.Configuration;
1213
import io.streamthoughts.jikkou.core.exceptions.ValidationException;
1314
import io.streamthoughts.jikkou.core.io.writer.ResourceWriter;
1415
import io.streamthoughts.jikkou.core.models.ApiResourceChangeList;
@@ -75,6 +76,8 @@ public class DiffCommand extends CLIBaseCommand implements Callable<Integer> {
7576
LocalResourceRepository localResourceRepository;
7677
@Inject
7778
ResourceWriter writer;
79+
@Inject
80+
Configuration configuration;
7881

7982
/**
8083
* {@inheritDoc}
@@ -112,14 +115,7 @@ private HasItems getResources() {
112115

113116
@NotNull
114117
private ReconciliationContext getReconciliationContext() {
115-
return ReconciliationContext
116-
.builder()
117-
.dryRun(true)
118-
.configuration(configOptionsMixin.getConfiguration())
119-
.selector(selectorOptions.getResourceSelector())
120-
.labels(fileOptions.getLabels())
121-
.annotations(fileOptions.getAnnotations())
122-
.providerName(providerOptionMixin.getProvider())
123-
.build();
118+
return new ProviderResolver(configuration).buildReconciliationContext(
119+
providerOptionMixin, configOptionsMixin, selectorOptions, fileOptions, true);
124120
}
125121
}

cli/src/main/java/io/streamthoughts/jikkou/client/command/ProviderOptionMixin.java

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
import picocli.CommandLine.Option;
1010

1111
/**
12-
* Mixin class for the --provider CLI option.
12+
* Mixin class for the --provider, --provider-all, --provider-group, and --continue-on-error CLI options.
1313
* Use this mixin to add provider selection support to commands that need
14-
* to target a specific provider instance when multiple providers of the
15-
* same type are configured.
14+
* to target a specific provider instance or multiple providers when multiple
15+
* providers of the same type are configured.
1616
*/
1717
public final class ProviderOptionMixin {
1818

@@ -22,6 +22,26 @@ public final class ProviderOptionMixin {
2222
)
2323
public String provider;
2424

25+
@Option(
26+
names = {"--provider-all"},
27+
description = "Apply to all registered providers of matching type",
28+
defaultValue = "false"
29+
)
30+
public boolean providerAll;
31+
32+
@Option(
33+
names = {"--provider-group"},
34+
description = "Apply to a named group of providers (configured in provider-groups)"
35+
)
36+
public String providerGroup;
37+
38+
@Option(
39+
names = {"--continue-on-error"},
40+
description = "Continue reconciliation when a provider fails during batch operations (default: fail-fast)",
41+
defaultValue = "false"
42+
)
43+
public boolean continueOnError;
44+
2545
/**
2646
* Gets the provider name.
2747
*
@@ -30,4 +50,31 @@ public final class ProviderOptionMixin {
3050
public String getProvider() {
3151
return provider;
3252
}
33-
}
53+
54+
/**
55+
* Gets whether --provider-all flag is set.
56+
*
57+
* @return true if batch apply to all providers is requested.
58+
*/
59+
public boolean isProviderAll() {
60+
return providerAll;
61+
}
62+
63+
/**
64+
* Gets the provider group name.
65+
*
66+
* @return the provider group name, or null if not specified.
67+
*/
68+
public String getProviderGroup() {
69+
return providerGroup;
70+
}
71+
72+
/**
73+
* Gets whether --continue-on-error flag is set.
74+
*
75+
* @return true if reconciliation should continue on error.
76+
*/
77+
public boolean isContinueOnError() {
78+
return continueOnError;
79+
}
80+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright (c) The original authors
4+
*
5+
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
6+
*/
7+
package io.streamthoughts.jikkou.client.command;
8+
9+
import io.streamthoughts.jikkou.core.ReconciliationContext;
10+
import io.streamthoughts.jikkou.core.config.Configuration;
11+
import io.streamthoughts.jikkou.runtime.JikkouConfigProperties;
12+
import io.streamthoughts.jikkou.runtime.configurator.ExtensionConfigEntry;
13+
import java.util.List;
14+
import java.util.Map;
15+
import org.jetbrains.annotations.NotNull;
16+
17+
/**
18+
* Resolves provider names from CLI flags (--provider-all, --provider-group)
19+
* using the application configuration, and builds reconciliation contexts
20+
* for multi-provider operations.
21+
*
22+
* @since 0.38.0
23+
*/
24+
public final class ProviderResolver {
25+
26+
private final Configuration configuration;
27+
28+
public ProviderResolver(@NotNull Configuration configuration) {
29+
this.configuration = configuration;
30+
}
31+
32+
/**
33+
* Resolves all enabled provider names from the configuration.
34+
*
35+
* @return list of all enabled provider names
36+
*/
37+
public @NotNull List<String> resolveAllProviders() {
38+
List<ExtensionConfigEntry> providers = JikkouConfigProperties.PROVIDER_CONFIG.get(configuration);
39+
return providers.stream()
40+
.filter(ExtensionConfigEntry::enabled)
41+
.map(ExtensionConfigEntry::name)
42+
.toList();
43+
}
44+
45+
/**
46+
* Resolves provider names for a named group.
47+
*
48+
* @param groupName the group name
49+
* @return list of provider names in the group
50+
* @throws IllegalArgumentException if the group is not found
51+
*/
52+
public @NotNull List<String> resolveProviderGroup(@NotNull String groupName) {
53+
Map<String, List<String>> groups = JikkouConfigProperties.PROVIDER_GROUPS_CONFIG.get(configuration);
54+
List<String> providers = groups.get(groupName);
55+
if (providers == null) {
56+
throw new IllegalArgumentException(String.format(
57+
"Provider group '%s' not found. Available groups: %s. " +
58+
"Configure groups in: jikkou { provider-groups { %s = [\"provider1\", \"provider2\"] } }",
59+
groupName, groups.keySet(), groupName
60+
));
61+
}
62+
return providers;
63+
}
64+
65+
/**
66+
* Resolves provider names from the ProviderOptionMixin flags.
67+
* Returns an empty list if no batch flags are set (single-provider mode).
68+
*
69+
* @param mixin the provider option mixin
70+
* @return list of provider names, empty for single-provider mode
71+
* @throws IllegalArgumentException if mutually exclusive flags are set
72+
*/
73+
public @NotNull List<String> resolveProviderNames(@NotNull ProviderOptionMixin mixin) {
74+
int flagCount = (mixin.isProviderAll() ? 1 : 0)
75+
+ (mixin.getProviderGroup() != null ? 1 : 0)
76+
+ (mixin.getProvider() != null ? 1 : 0);
77+
78+
if (flagCount > 1) {
79+
throw new IllegalArgumentException(
80+
"Options --provider, --provider-all, and --provider-group are mutually exclusive.");
81+
}
82+
83+
if (mixin.isProviderAll()) {
84+
return resolveAllProviders();
85+
}
86+
if (mixin.getProviderGroup() != null) {
87+
return resolveProviderGroup(mixin.getProviderGroup());
88+
}
89+
return List.of();
90+
}
91+
92+
/**
93+
* Builds a {@link ReconciliationContext} from CLI option mixins, resolving
94+
* provider names from batch flags.
95+
*
96+
* @param providerOptions the provider CLI options
97+
* @param configOptions the config CLI options
98+
* @param selectorOptions the selector CLI options
99+
* @param fileOptions the file CLI options
100+
* @param dryRun whether this is a dry-run
101+
* @return a fully configured reconciliation context
102+
* @since 0.38.0
103+
*/
104+
public @NotNull ReconciliationContext buildReconciliationContext(
105+
@NotNull ProviderOptionMixin providerOptions,
106+
@NotNull ConfigOptionsMixin configOptions,
107+
@NotNull SelectorOptionsMixin selectorOptions,
108+
@NotNull FileOptionsMixin fileOptions,
109+
boolean dryRun) {
110+
List<String> providerNames = resolveProviderNames(providerOptions);
111+
return ReconciliationContext.builder()
112+
.dryRun(dryRun)
113+
.configuration(configOptions.getConfiguration())
114+
.selector(selectorOptions.getResourceSelector())
115+
.labels(fileOptions.getLabels())
116+
.annotations(fileOptions.getAnnotations())
117+
.providerName(providerOptions.getProvider())
118+
.providerNames(providerNames)
119+
.continueOnError(providerOptions.isContinueOnError())
120+
.build();
121+
}
122+
}

cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/BaseResourceCommand.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
import io.streamthoughts.jikkou.client.command.ExecOptionsMixin;
1313
import io.streamthoughts.jikkou.client.command.FileOptionsMixin;
1414
import io.streamthoughts.jikkou.client.command.ProviderOptionMixin;
15+
import io.streamthoughts.jikkou.client.command.ProviderResolver;
1516
import io.streamthoughts.jikkou.client.command.SelectorOptionsMixin;
1617
import io.streamthoughts.jikkou.client.command.validate.ValidationErrorsWriter;
1718
import io.streamthoughts.jikkou.client.printer.Printers;
1819
import io.streamthoughts.jikkou.core.JikkouApi;
1920
import io.streamthoughts.jikkou.core.ReconciliationContext;
2021
import io.streamthoughts.jikkou.core.ReconciliationMode;
22+
import io.streamthoughts.jikkou.core.config.Configuration;
2123
import io.streamthoughts.jikkou.core.exceptions.ValidationException;
2224
import io.streamthoughts.jikkou.core.models.ApiChangeResultList;
2325
import io.streamthoughts.jikkou.core.models.HasItems;
@@ -48,7 +50,10 @@ public abstract class BaseResourceCommand extends CLIBaseCommand implements Call
4850

4951
@Inject
5052
LocalResourceRepository localResourceRepository;
51-
53+
54+
@Inject
55+
Configuration configuration;
56+
5257
/**
5358
* {@inheritDoc}
5459
*/
@@ -69,14 +74,8 @@ public Integer call() throws IOException {
6974
}
7075

7176
private @NotNull ReconciliationContext getReconciliationContext() {
72-
return ReconciliationContext.builder()
73-
.dryRun(isDryRun())
74-
.configuration(configOptionsMixin.getConfiguration())
75-
.selector(selectorOptions.getResourceSelector())
76-
.labels(fileOptions.getLabels())
77-
.annotations(fileOptions.getAnnotations())
78-
.providerName(providerOptionMixin.getProvider())
79-
.build();
77+
return new ProviderResolver(configuration).buildReconciliationContext(
78+
providerOptionMixin, configOptionsMixin, selectorOptions, fileOptions, isDryRun());
8079
}
8180

8281
protected @NotNull HasItems getResources() {

cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/PatchResourceCommand.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
import io.streamthoughts.jikkou.client.command.ExecOptionsMixin;
1313
import io.streamthoughts.jikkou.client.command.FileOptionsMixin;
1414
import io.streamthoughts.jikkou.client.command.ProviderOptionMixin;
15+
import io.streamthoughts.jikkou.client.command.ProviderResolver;
1516
import io.streamthoughts.jikkou.client.command.SelectorOptionsMixin;
1617
import io.streamthoughts.jikkou.client.command.validate.ValidationErrorsWriter;
1718
import io.streamthoughts.jikkou.core.JikkouApi;
1819
import io.streamthoughts.jikkou.core.ReconciliationContext;
1920
import io.streamthoughts.jikkou.core.ReconciliationMode;
21+
import io.streamthoughts.jikkou.core.config.Configuration;
2022
import io.streamthoughts.jikkou.core.exceptions.ValidationException;
2123
import io.streamthoughts.jikkou.core.models.ApiChangeResultList;
2224
import io.streamthoughts.jikkou.core.models.HasItems;
@@ -61,6 +63,8 @@ public final class PatchResourceCommand extends CLIBaseCommand implements Callab
6163
JikkouApi api;
6264
@Inject
6365
LocalResourceRepository localResourceRepository;
66+
@Inject
67+
Configuration configuration;
6468

6569
/**
6670
* {@inheritDoc}
@@ -83,14 +87,8 @@ public Integer call() throws IOException {
8387
}
8488

8589
private @NotNull ReconciliationContext getReconciliationContext() {
86-
return ReconciliationContext.builder()
87-
.dryRun(isDryRun())
88-
.configuration(configOptionsMixin.getConfiguration())
89-
.selector(selectorOptions.getResourceSelector())
90-
.labels(fileOptions.getLabels())
91-
.annotations(fileOptions.getAnnotations())
92-
.providerName(providerOptionMixin.getProvider())
93-
.build();
90+
return new ProviderResolver(configuration).buildReconciliationContext(
91+
providerOptionMixin, configOptionsMixin, selectorOptions, fileOptions, isDryRun());
9492
}
9593

9694
public boolean isDryRun() {

cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/ReplaceResourceCommand.java

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
import io.streamthoughts.jikkou.client.command.ExecOptionsMixin;
1313
import io.streamthoughts.jikkou.client.command.FileOptionsMixin;
1414
import io.streamthoughts.jikkou.client.command.ProviderOptionMixin;
15+
import io.streamthoughts.jikkou.client.command.ProviderResolver;
1516
import io.streamthoughts.jikkou.client.command.SelectorOptionsMixin;
1617
import io.streamthoughts.jikkou.client.command.validate.ValidationErrorsWriter;
1718
import io.streamthoughts.jikkou.core.JikkouApi;
1819
import io.streamthoughts.jikkou.core.ReconciliationContext;
20+
import io.streamthoughts.jikkou.core.config.Configuration;
1921
import io.streamthoughts.jikkou.core.exceptions.ValidationException;
2022
import io.streamthoughts.jikkou.core.models.ApiChangeResultList;
2123
import io.streamthoughts.jikkou.core.models.HasItems;
@@ -55,6 +57,9 @@ public final class ReplaceResourceCommand extends CLIBaseCommand implements Call
5557
@Inject
5658
LocalResourceRepository localResourceRepository;
5759

60+
@Inject
61+
Configuration configuration;
62+
5863
/**
5964
* {@inheritDoc}
6065
*/
@@ -72,14 +77,8 @@ public Integer call() throws IOException {
7277
}
7378

7479
private @NotNull ReconciliationContext getReconciliationContext() {
75-
return ReconciliationContext.builder()
76-
.dryRun(isDryRun())
77-
.configuration(configOptionsMixin.getConfiguration())
78-
.selector(selectorOptions.getResourceSelector())
79-
.labels(fileOptions.getLabels())
80-
.annotations(fileOptions.getAnnotations())
81-
.providerName(providerOptionMixin.getProvider())
82-
.build();
80+
return new ProviderResolver(configuration).buildReconciliationContext(
81+
providerOptionMixin, configOptionsMixin, selectorOptions, fileOptions, isDryRun());
8382
}
8483

8584
public boolean isDryRun() {

0 commit comments

Comments
 (0)