Skip to content

Commit dce8ef2

Browse files
committed
NIFI-15148 - Registry Clients - Support for branch creation
Signed-off-by: Pierre Villard <[email protected]>
1 parent 1c840e4 commit dce8ef2

File tree

28 files changed

+1667
-16
lines changed

28 files changed

+1667
-16
lines changed

nifi-extension-bundles/nifi-extension-utils/nifi-git-flow-registry/src/main/java/org/apache/nifi/registry/flow/git/AbstractGitFlowRegistryClient.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,42 @@ public FlowRegistryBranch getDefaultBranch(final FlowRegistryClientConfiguration
199199
return defaultBranch;
200200
}
201201

202+
@Override
203+
public void createBranch(final FlowRegistryClientConfigurationContext context, final FlowVersionLocation sourceLocation, final String newBranchName)
204+
throws FlowRegistryException, IOException {
205+
if (StringUtils.isBlank(newBranchName)) {
206+
throw new IllegalArgumentException("Branch name must be specified when creating a new branch");
207+
}
208+
209+
final GitRepositoryClient repositoryClient = getRepositoryClient(context);
210+
verifyWritePermissions(repositoryClient);
211+
212+
final String sourceBranch = resolveSourceBranch(context, sourceLocation);
213+
if (StringUtils.isBlank(sourceBranch)) {
214+
throw new FlowRegistryException("Unable to determine source branch for new branch creation");
215+
}
216+
217+
final Optional<String> sourceCommitSha = sourceLocation == null ? Optional.empty() : Optional.ofNullable(sourceLocation.getVersion());
218+
final String trimmedBranchName = newBranchName.trim();
219+
final String trimmedSourceBranch = sourceBranch.trim();
220+
221+
getLogger().info("Creating branch [{}] from branch [{}]", trimmedBranchName, trimmedSourceBranch);
222+
223+
try {
224+
repositoryClient.createBranch(trimmedBranchName, trimmedSourceBranch, sourceCommitSha);
225+
} catch (final UnsupportedOperationException e) {
226+
throw new FlowRegistryException("Configured repository client does not support branch creation", e);
227+
}
228+
}
229+
230+
private String resolveSourceBranch(final FlowRegistryClientConfigurationContext context, final FlowVersionLocation sourceLocation) {
231+
if (sourceLocation != null && StringUtils.isNotBlank(sourceLocation.getBranch())) {
232+
return sourceLocation.getBranch();
233+
}
234+
final String defaultBranch = context.getProperty(REPOSITORY_BRANCH).getValue();
235+
return StringUtils.isBlank(defaultBranch) ? null : defaultBranch;
236+
}
237+
202238
@Override
203239
public Set<FlowRegistryBucket> getBuckets(final FlowRegistryClientConfigurationContext context, final String branch) throws IOException, FlowRegistryException {
204240
final GitRepositoryClient repositoryClient = getRepositoryClient(context);

nifi-extension-bundles/nifi-extension-utils/nifi-git-flow-registry/src/main/java/org/apache/nifi/registry/flow/git/client/GitRepositoryClient.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,21 @@ public interface GitRepositoryClient {
138138
*/
139139
InputStream deleteContent(String filePath, String commitMessage, String branch) throws FlowRegistryException, IOException;
140140

141+
/**
142+
* Creates a new branch in the repository.
143+
*
144+
* @param newBranchName the name of the branch to create
145+
* @param sourceBranch the name of the source branch
146+
* @param sourceCommitSha optional commit SHA to use as the starting point for the new branch. If empty, the head commit of the source branch should be used.
147+
* @throws IOException if an I/O error occurs
148+
* @throws FlowRegistryException if a non-I/O error occurs
149+
* @throws UnsupportedOperationException if the repository implementation does not support branch creation
150+
*/
151+
default void createBranch(final String newBranchName, final String sourceBranch, final Optional<String> sourceCommitSha)
152+
throws IOException, FlowRegistryException {
153+
throw new UnsupportedOperationException("Branch creation is not supported");
154+
}
155+
141156
/**
142157
* Closes any resources held by the client.
143158
*

nifi-extension-bundles/nifi-extension-utils/nifi-git-flow-registry/src/test/java/org/apache/nifi/registry/flow/git/AbstractGitFlowRegistryClientTest.java

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@
2222
import org.apache.nifi.components.PropertyValue;
2323
import org.apache.nifi.logging.ComponentLog;
2424
import org.apache.nifi.registry.flow.FlowRegistryClientConfigurationContext;
25+
import org.apache.nifi.registry.flow.FlowRegistryClientInitializationContext;
2526
import org.apache.nifi.registry.flow.FlowRegistryException;
27+
import org.apache.nifi.registry.flow.FlowVersionLocation;
2628
import org.apache.nifi.registry.flow.git.client.GitCommit;
2729
import org.apache.nifi.registry.flow.git.client.GitCreateContentRequest;
2830
import org.apache.nifi.registry.flow.git.client.GitRepositoryClient;
2931
import org.apache.nifi.util.MockComponentLog;
3032
import org.apache.nifi.util.MockPropertyValue;
3133
import org.junit.jupiter.api.Test;
3234

35+
import javax.net.ssl.SSLContext;
36+
3337
import java.io.IOException;
3438
import java.io.InputStream;
3539
import java.util.List;
@@ -40,6 +44,7 @@
4044
import java.util.concurrent.atomic.AtomicReference;
4145

4246
import static org.junit.jupiter.api.Assertions.assertEquals;
47+
import static org.junit.jupiter.api.Assertions.assertThrows;
4348
import static org.junit.jupiter.api.Assertions.assertTrue;
4449

4550
class AbstractGitFlowRegistryClientTest {
@@ -49,6 +54,7 @@ void verifySuccessful() throws Exception {
4954
final TestGitRepositoryClient repositoryClient = new TestGitRepositoryClient(true, true, Set.of("bucket-a", ".git"));
5055
final AtomicReference<TestGitRepositoryClient> suppliedClient = new AtomicReference<>(repositoryClient);
5156
final TestGitFlowRegistryClient flowRegistryClient = new TestGitFlowRegistryClient(() -> suppliedClient.getAndSet(null), "[email protected]");
57+
flowRegistryClient.initialize(createInitializationContext());
5258
final FlowRegistryClientConfigurationContext context = createContext("main", "[.].*");
5359
final ComponentLog verificationLogger = new MockComponentLog("test-component", this);
5460

@@ -71,6 +77,7 @@ void verifyAuthenticationFailure() {
7177
final TestGitFlowRegistryClient flowRegistryClient = new TestGitFlowRegistryClient(() -> {
7278
throw new FlowRegistryException("Authentication failed");
7379
80+
flowRegistryClient.initialize(createInitializationContext());
7481
final FlowRegistryClientConfigurationContext context = createContext("main", "[.].*");
7582
final ComponentLog verificationLogger = new MockComponentLog("test-component", this);
7683

@@ -85,6 +92,7 @@ void verifyAuthenticationFailure() {
8592
void verifyReadFailureSkipsBucketListing() throws Exception {
8693
final TestGitRepositoryClient repositoryClient = new TestGitRepositoryClient(false, false, Set.of());
8794
final TestGitFlowRegistryClient flowRegistryClient = new TestGitFlowRegistryClient(() -> repositoryClient, "[email protected]");
95+
flowRegistryClient.initialize(createInitializationContext());
8896
final FlowRegistryClientConfigurationContext context = createContext("main", "[.].*");
8997
final ComponentLog verificationLogger = new MockComponentLog("test-component", this);
9098

@@ -100,10 +108,11 @@ void verifyReadFailureSkipsBucketListing() throws Exception {
100108

101109
@Test
102110
void verifyBucketListingFailureReported() throws Exception {
103-
final TestGitRepositoryClient repositoryClient = new TestGitRepositoryClient(true, true, Set.of());
111+
final TestGitRepositoryClient repositoryClient = new TestGitRepositoryClient(true, true, Set.of("bucket-a"));
104112
repositoryClient.setGetTopLevelDirectoryNamesException(new FlowRegistryException("listing error"));
105113

106114
final TestGitFlowRegistryClient flowRegistryClient = new TestGitFlowRegistryClient(() -> repositoryClient, "[email protected]");
115+
flowRegistryClient.initialize(createInitializationContext());
107116
final FlowRegistryClientConfigurationContext context = createContext("main", "[.].*");
108117
final ComponentLog verificationLogger = new MockComponentLog("test-component", this);
109118

@@ -118,6 +127,41 @@ void verifyBucketListingFailureReported() throws Exception {
118127
assertTrue(repositoryClient.isClosed());
119128
}
120129

130+
@Test
131+
void createBranchDelegatesToRepositoryClient() throws Exception {
132+
final TestGitRepositoryClient repositoryClient = new TestGitRepositoryClient(true, true, Set.of("bucket-a"));
133+
final TestGitFlowRegistryClient flowRegistryClient = new TestGitFlowRegistryClient(() -> repositoryClient, "[email protected]");
134+
flowRegistryClient.initialize(createInitializationContext());
135+
final FlowRegistryClientConfigurationContext context = createContext("main", "[.].*");
136+
137+
final FlowVersionLocation sourceLocation = new FlowVersionLocation("source-branch", "bucket-a", "flow-x", "commit-1");
138+
139+
flowRegistryClient.createBranch(context, sourceLocation, " new-branch ");
140+
141+
assertEquals("new-branch", repositoryClient.getCreatedBranch());
142+
assertEquals("source-branch", repositoryClient.getCreatedBranchSource());
143+
assertEquals(Optional.of("commit-1"), repositoryClient.getCreatedBranchCommit());
144+
}
145+
146+
@Test
147+
void createBranchUnsupportedThrowsFlowRegistryException() throws Exception {
148+
final TestGitRepositoryClient repositoryClient = new TestGitRepositoryClient(true, true, Set.of("bucket-a"));
149+
repositoryClient.setBranchCreationUnsupported(true);
150+
151+
final TestGitFlowRegistryClient flowRegistryClient = new TestGitFlowRegistryClient(() -> repositoryClient, "[email protected]");
152+
flowRegistryClient.initialize(createInitializationContext());
153+
final FlowRegistryClientConfigurationContext context = createContext("main", "[.].*");
154+
155+
final FlowVersionLocation sourceLocation = new FlowVersionLocation();
156+
sourceLocation.setBranch("source");
157+
158+
final FlowRegistryException exception = assertThrows(FlowRegistryException.class,
159+
() -> flowRegistryClient.createBranch(context, sourceLocation, "new-branch"));
160+
161+
assertEquals("Configured repository client does not support branch creation", exception.getMessage());
162+
assertTrue(repositoryClient.getCreatedBranchCommit().isEmpty());
163+
}
164+
121165
private FlowRegistryClientConfigurationContext createContext(final String branch, final String exclusionPattern) {
122166
final Map<PropertyDescriptor, PropertyValue> properties = Map.of(
123167
AbstractGitFlowRegistryClient.REPOSITORY_BRANCH, new MockPropertyValue(branch),
@@ -145,6 +189,25 @@ public Optional<String> getNiFiUserIdentity() {
145189
};
146190
}
147191

192+
private FlowRegistryClientInitializationContext createInitializationContext() {
193+
return new FlowRegistryClientInitializationContext() {
194+
@Override
195+
public String getIdentifier() {
196+
return "test-git-client";
197+
}
198+
199+
@Override
200+
public ComponentLog getLogger() {
201+
return new MockComponentLog("test-git-client", AbstractGitFlowRegistryClientTest.this);
202+
}
203+
204+
@Override
205+
public Optional<SSLContext> getSystemSslContext() {
206+
return Optional.empty();
207+
}
208+
};
209+
}
210+
148211
private static class TestGitFlowRegistryClient extends AbstractGitFlowRegistryClient {
149212
private final RepositoryClientSupplier repositoryClientSupplier;
150213
private final String storageLocation;
@@ -186,6 +249,11 @@ private static class TestGitRepositoryClient implements GitRepositoryClient {
186249
private FlowRegistryException topLevelDirectoryNamesException;
187250
private IOException topLevelDirectoryNamesIOException;
188251
private boolean closed;
252+
private boolean branchCreationUnsupported;
253+
private FlowRegistryException createBranchException;
254+
private String createdBranch;
255+
private String createdBranchSource;
256+
private Optional<String> createdBranchCommit = Optional.empty();
189257

190258
TestGitRepositoryClient(final boolean canRead, final boolean canWrite, final Set<String> bucketNames) {
191259
this.canRead = canRead;
@@ -198,11 +266,31 @@ void setGetTopLevelDirectoryNamesException(final FlowRegistryException exception
198266
this.topLevelDirectoryNamesIOException = null;
199267
}
200268

201-
void setGetTopLevelDirectoryNamesException(final IOException exception) {
269+
void setGetTopLevelDirectoryNamesIOException(final IOException exception) {
202270
this.topLevelDirectoryNamesIOException = exception;
203271
this.topLevelDirectoryNamesException = null;
204272
}
205273

274+
void setBranchCreationUnsupported(final boolean unsupported) {
275+
this.branchCreationUnsupported = unsupported;
276+
}
277+
278+
void setCreateBranchException(final FlowRegistryException exception) {
279+
this.createBranchException = exception;
280+
}
281+
282+
String getCreatedBranch() {
283+
return createdBranch;
284+
}
285+
286+
String getCreatedBranchSource() {
287+
return createdBranchSource;
288+
}
289+
290+
Optional<String> getCreatedBranchCommit() {
291+
return createdBranchCommit;
292+
}
293+
206294
boolean isClosed() {
207295
return closed;
208296
}
@@ -228,6 +316,21 @@ public Set<String> getTopLevelDirectoryNames(final String branch) throws IOExcep
228316
return bucketNames;
229317
}
230318

319+
@Override
320+
public void createBranch(final String newBranchName, final String sourceBranch, final Optional<String> sourceCommitSha)
321+
throws IOException, FlowRegistryException {
322+
if (branchCreationUnsupported) {
323+
throw new UnsupportedOperationException("Branch creation not supported");
324+
}
325+
if (createBranchException != null) {
326+
throw createBranchException;
327+
}
328+
329+
createdBranch = newBranchName;
330+
createdBranchSource = sourceBranch;
331+
createdBranchCommit = sourceCommitSha;
332+
}
333+
231334
@Override
232335
public void close() {
233336
closed = true;
@@ -265,7 +368,7 @@ public Optional<String> getContentSha(final String path, final String branch) {
265368

266369
@Override
267370
public String createContent(final GitCreateContentRequest request) {
268-
throw new UnsupportedOperationException("Not required for test");
371+
return "test-commit";
269372
}
270373

271374
@Override

nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubRepositoryClient.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import com.github.benmanes.caffeine.cache.Cache;
2323
import com.github.benmanes.caffeine.cache.Caffeine;
24+
import org.apache.commons.lang3.StringUtils;
2425
import org.apache.nifi.logging.ComponentLog;
2526
import org.apache.nifi.registry.flow.FlowRegistryException;
2627
import org.apache.nifi.registry.flow.git.client.GitCommit;
@@ -425,6 +426,56 @@ public InputStream deleteContent(final String filePath, final String commitMessa
425426
});
426427
}
427428

429+
@Override
430+
public void createBranch(final String newBranchName, final String sourceBranch, final Optional<String> sourceCommitSha)
431+
throws IOException, FlowRegistryException {
432+
if (StringUtils.isBlank(newBranchName)) {
433+
throw new IllegalArgumentException("Branch name must be specified");
434+
}
435+
if (StringUtils.isBlank(sourceBranch)) {
436+
throw new IllegalArgumentException("Source branch must be specified");
437+
}
438+
439+
final String trimmedNewBranch = newBranchName.trim();
440+
final String trimmedSourceBranch = sourceBranch.trim();
441+
final String newBranchRefPath = "heads/" + trimmedNewBranch;
442+
final String sourceBranchRefPath = "heads/" + trimmedSourceBranch;
443+
444+
try {
445+
execute(() -> repository.getRef(newBranchRefPath));
446+
throw new FlowRegistryException("Branch [" + trimmedNewBranch + "] already exists");
447+
} catch (final FileNotFoundException notFound) {
448+
logger.debug("Branch [{}] does not exist and will be created", trimmedNewBranch, notFound);
449+
}
450+
451+
final GHRef sourceBranchRef;
452+
try {
453+
sourceBranchRef = execute(() -> repository.getRef(sourceBranchRefPath));
454+
} catch (final FileNotFoundException notFound) {
455+
throw new FlowRegistryException("Source branch [" + trimmedSourceBranch + "] does not exist", notFound);
456+
}
457+
458+
final String baseCommitSha;
459+
if (sourceCommitSha.isPresent()) {
460+
final String requestedCommitSha = sourceCommitSha.get();
461+
try {
462+
baseCommitSha = execute(() -> repository.getCommit(requestedCommitSha).getSHA1());
463+
} catch (final FileNotFoundException notFound) {
464+
throw new FlowRegistryException("Commit [" + requestedCommitSha + "] was not found in the repository", notFound);
465+
}
466+
} else {
467+
baseCommitSha = sourceBranchRef.getObject().getSha();
468+
}
469+
470+
logger.info("Creating branch [{}] from [{}] at commit [{}] for repository [{}]",
471+
trimmedNewBranch, trimmedSourceBranch, baseCommitSha, repository.getFullName());
472+
473+
execute(() -> {
474+
repository.createRef(BRANCH_REF_PATTERN.formatted(trimmedNewBranch), baseCommitSha);
475+
return null;
476+
});
477+
}
478+
428479
private String getResolvedPath(final String path) {
429480
return repoPath == null ? path : repoPath + "/" + path;
430481
}

0 commit comments

Comments
 (0)