Skip to content

Commit 1cc5360

Browse files
committed
NIFI-3785: Added feature to move controller services between process groups with the new UI
1 parent a8e8b6a commit 1cc5360

File tree

20 files changed

+965
-3
lines changed

20 files changed

+965
-3
lines changed

nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ public class StandardControllerServiceNode extends AbstractComponentNode impleme
115115
private volatile LogLevel bulletinLevel = LogLevel.WARN;
116116

117117
private final AtomicBoolean active;
118+
private final AtomicBoolean moving;
118119

119120
public StandardControllerServiceNode(final LoggableComponent<ControllerService> implementation, final LoggableComponent<ControllerService> proxiedControllerService,
120121
final ControllerServiceInvocationHandler invocationHandler, final String id, final ValidationContextFactory validationContextFactory,
@@ -134,6 +135,7 @@ public StandardControllerServiceNode(final LoggableComponent<ControllerService>
134135
super(id, validationContextFactory, serviceProvider, componentType, componentCanonicalClass, reloadComponent, extensionManager, validationTrigger, isExtensionMissing);
135136
this.serviceProvider = serviceProvider;
136137
this.active = new AtomicBoolean();
138+
this.moving = new AtomicBoolean(false);
137139
setControllerServiceAndProxy(implementation, proxiedControllerService, invocationHandler);
138140
stateTransition = new ServiceStateTransition(this);
139141
this.comment = "";
@@ -445,7 +447,14 @@ public ControllerServiceState getState() {
445447
public boolean isActive() {
446448
return this.active.get();
447449
}
448-
450+
@Override
451+
public boolean isMoving() {
452+
return this.moving.get();
453+
}
454+
@Override
455+
public void setMoving(boolean moving) {
456+
this.moving.set(moving);
457+
}
449458
@Override
450459
public boolean awaitEnabled(final long timePeriod, final TimeUnit timeUnit) throws InterruptedException {
451460
LOG.debug("Waiting up to {} {} for {} to be enabled", timePeriod, timeUnit, this);

nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2592,7 +2592,9 @@ public void removeControllerService(final ControllerServiceNode service) {
25922592
throw new IllegalStateException("ControllerService " + service.getIdentifier() + " is not a member of this Process Group");
25932593
}
25942594

2595-
service.verifyCanDelete();
2595+
if (!service.isMoving()) {
2596+
service.verifyCanDelete();
2597+
}
25962598

25972599
try (final NarCloseable x = NarCloseable.withComponentNarLoader(extensionManager, service.getControllerServiceImplementation().getClass(), service.getIdentifier())) {
25982600
final ConfigurationContext configurationContext = new StandardConfigurationContext(service, controllerServiceProvider, null);
@@ -2639,6 +2641,7 @@ public void removeControllerService(final ControllerServiceNode service) {
26392641
} catch (Throwable t) {
26402642
}
26412643
}
2644+
service.setMoving(false);
26422645
writeLock.unlock();
26432646
}
26442647
}

nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ public interface ControllerServiceNode extends ComponentNode, VersionedComponent
206206
*/
207207
boolean isActive();
208208

209+
boolean isMoving();
210+
211+
void setMoving(boolean moving);
212+
209213
/**
210214
* Waits up to the given amount of time for the Controller Service to transition to an ENABLED state.
211215
* @param timePeriod maximum amount of time to wait

nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,6 +2086,16 @@ ControllerServiceReferencingComponentsEntity updateControllerServiceReferencingC
20862086
*/
20872087
ControllerServiceEntity updateControllerService(Revision revision, ControllerServiceDTO controllerServiceDTO);
20882088

2089+
/**
2090+
* Moves the specified controller service.
2091+
*
2092+
* @param revision Revision to compare with current base revision
2093+
* @param controllerServiceDTO The controller service DTO
2094+
* @param newProcessGroupID The id of the process group the controller service is being moved to
2095+
* @return The controller service DTO
2096+
*/
2097+
ControllerServiceEntity moveControllerService(final Revision revision, final ControllerServiceDTO controllerServiceDTO, final String newProcessGroupID);
2098+
20892099
/**
20902100
* Performs verification of the given Configuration for the Controller Service with the given ID
20912101
* @param controllerServiceId the id of the controller service

nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2948,6 +2948,41 @@ public ControllerServiceEntity updateControllerService(final Revision revision,
29482948
return entityFactory.createControllerServiceEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, bulletinEntities);
29492949
}
29502950

2951+
@Override
2952+
public ControllerServiceEntity moveControllerService(final Revision revision, final ControllerServiceDTO controllerServiceDTO, final String newProcessGroupID) {
2953+
// get the component, ensure we have access to it, and perform the move request
2954+
final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceDTO.getId());
2955+
final RevisionUpdate<ControllerServiceDTO> snapshot = updateComponent(revision,
2956+
controllerService,
2957+
() -> {
2958+
final ProcessGroup oldParentGroup = controllerService.getProcessGroup();
2959+
controllerService.setMoving(true);
2960+
oldParentGroup.removeControllerService(controllerService);
2961+
if (!oldParentGroup.isRootGroup() && oldParentGroup.getParent().getIdentifier().equals(newProcessGroupID)) {
2962+
// move to parent process group
2963+
oldParentGroup.getParent().addControllerService(controllerService);
2964+
} else {
2965+
// move to child process group
2966+
oldParentGroup.getProcessGroup(newProcessGroupID).addControllerService(controllerService);
2967+
}
2968+
return controllerService;
2969+
},
2970+
cs -> {
2971+
final ControllerServiceDTO dto = dtoFactory.createControllerServiceDto(cs);
2972+
final ControllerServiceReference ref = controllerService.getReferences();
2973+
final ControllerServiceReferencingComponentsEntity referencingComponentsEntity = createControllerServiceReferencingComponentsEntity(ref);
2974+
dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents());
2975+
return dto;
2976+
});
2977+
2978+
final PermissionsDTO permissions = dtoFactory.createPermissionsDto(controllerService);
2979+
final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService));
2980+
final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId()));
2981+
final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList());
2982+
controllerService.performValidation();
2983+
return entityFactory.createControllerServiceEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, bulletinEntities);
2984+
}
2985+
29512986
@Override
29522987
public List<ConfigVerificationResultDTO> performControllerServiceConfigVerification(final String controllerServiceId, final Map<String, String> properties, final Map<String, String> variables) {
29532988
return controllerServiceDAO.verifyConfiguration(controllerServiceId, properties, variables);

nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.nifi.web.api;
1818

19+
import java.util.ArrayList;
1920
import java.util.Collections;
2021
import java.util.HashSet;
2122
import java.util.List;
@@ -53,6 +54,7 @@
5354
import org.apache.nifi.authorization.AuthorizeParameterReference;
5455
import org.apache.nifi.authorization.Authorizer;
5556
import org.apache.nifi.authorization.ComponentAuthorizable;
57+
import org.apache.nifi.authorization.ProcessGroupAuthorizable;
5658
import org.apache.nifi.authorization.RequestAction;
5759
import org.apache.nifi.authorization.resource.Authorizable;
5860
import org.apache.nifi.authorization.resource.OperationAuthorizable;
@@ -680,6 +682,117 @@ public Response updateControllerService(
680682
);
681683
}
682684

685+
/**
686+
* Moves the specified Controller Service to parent/child process groups.
687+
*
688+
* @param id The id of the controller service to update.
689+
* @param requestControllerServiceEntity A controllerServiceEntity.
690+
* @return A controllerServiceEntity.
691+
*/
692+
@PUT
693+
@Consumes(MediaType.APPLICATION_JSON)
694+
@Produces(MediaType.APPLICATION_JSON)
695+
@Path("{id}/move")
696+
@Operation(
697+
summary = "Move Controller Service to the specified Process Group.",
698+
responses = @ApiResponse(content = @Content(schema = @Schema(implementation = ControllerServiceEntity.class))),
699+
security = {
700+
@SecurityRequirement(name = "Write - /controller-services/{uuid}"),
701+
@SecurityRequirement(name = "Write - Parent Process Group if scoped by Process Group - /process-groups/{uuid}"),
702+
@SecurityRequirement(name = "Write - Controller if scoped by Controller - /controller"),
703+
@SecurityRequirement(name = "Read - any referenced Controller Services - /controller-services/{uuid}")
704+
})
705+
@ApiResponses(
706+
value = {
707+
@ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
708+
@ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
709+
@ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
710+
@ApiResponse(responseCode = "404", description = "The specified resource could not be found."),
711+
@ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
712+
}
713+
)
714+
public Response moveControllerServices(
715+
@Parameter(
716+
description = "The controller service id.",
717+
required = true
718+
)
719+
@PathParam("id") final String id,
720+
@Parameter(
721+
description = "The controller service entity",
722+
required = true
723+
)
724+
final ControllerServiceEntity requestControllerServiceEntity) {
725+
726+
if (requestControllerServiceEntity == null) {
727+
throw new IllegalArgumentException("Controller service must be specified.");
728+
}
729+
730+
if (requestControllerServiceEntity.getRevision() == null) {
731+
throw new IllegalArgumentException("Revision must be specified.");
732+
}
733+
734+
if (requestControllerServiceEntity.getParentGroupId() == null) {
735+
throw new IllegalArgumentException("ParentGroupId must be specified.");
736+
}
737+
738+
final ControllerServiceDTO requestControllerServiceDTO = serviceFacade.getControllerService(id, true).getComponent();
739+
740+
final Revision requestRevision = getRevision(requestControllerServiceEntity, id);
741+
return withWriteLock(
742+
serviceFacade,
743+
serviceFacade.getControllerService(id, true),
744+
requestRevision,
745+
lookup -> {
746+
// authorize the service
747+
final ComponentAuthorizable authorizable = lookup.getControllerService(requestControllerServiceDTO.getId());
748+
authorizable.getAuthorizable().authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
749+
750+
// authorize the current and new process groups
751+
final ProcessGroupAuthorizable authorizableProcessGroupNew = lookup.getProcessGroup(requestControllerServiceEntity.getParentGroupId());
752+
authorizableProcessGroupNew.getAuthorizable().authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
753+
754+
final ProcessGroupAuthorizable authorizableProcessGroupOld = lookup.getProcessGroup(requestControllerServiceDTO.getParentGroupId());
755+
authorizableProcessGroupOld.getAuthorizable().authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
756+
757+
// Verify all referencing and referenced components are still within scope
758+
List<String> conflictingComponents = new ArrayList<>();
759+
requestControllerServiceDTO.getReferencingComponents().forEach(e -> {
760+
if (authorizableProcessGroupNew.getProcessGroup().findProcessor(e.getId()) == null
761+
&& authorizableProcessGroupNew.getProcessGroup().findControllerService(e.getId(), true, false) == null) {
762+
conflictingComponents.add("[" + e.getComponent().getName() + "]");
763+
}
764+
765+
final Authorizable referencingComponent = lookup.getControllerServiceReferencingComponent(requestControllerServiceDTO.getId(), e.getId());
766+
OperationAuthorizable.authorizeOperation(referencingComponent, authorizer, NiFiUserUtils.getNiFiUser());
767+
});
768+
769+
requestControllerServiceDTO.getProperties().forEach((key, value) -> {
770+
try {
771+
ControllerServiceEntity refControllerService = serviceFacade.getControllerService(value, false);
772+
if (refControllerService != null) {
773+
if (authorizableProcessGroupNew.getProcessGroup().findControllerService(value, false, true) == null) {
774+
conflictingComponents.add("[" + refControllerService.getComponent().getName() + "]");
775+
}
776+
}
777+
} catch (Exception ignored) { }
778+
});
779+
780+
if (!conflictingComponents.isEmpty()) {
781+
String errorMessage = "Could not move controller service because the following components would be out of scope: ";
782+
errorMessage += String.join(" ", conflictingComponents);
783+
throw new IllegalStateException(errorMessage);
784+
}
785+
},
786+
null,
787+
(revision, controllerServiceEntity) -> {
788+
final ControllerServiceDTO controllerService = controllerServiceEntity.getComponent();
789+
final ControllerServiceEntity entity = serviceFacade.moveControllerService(revision, controllerService, requestControllerServiceEntity.getParentGroupId());
790+
populateRemainingControllerServiceEntityContent(entity);
791+
return generateOkResponse(entity).build();
792+
}
793+
);
794+
}
795+
683796
/**
684797
* Removes the specified controller service.
685798
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<!--
2+
~ Licensed to the Apache Software Foundation (ASF) under one or more
3+
~ contributor license agreements. See the NOTICE file distributed with
4+
~ this work for additional information regarding copyright ownership.
5+
~ The ASF licenses this file to You under the Apache License, Version 2.0
6+
~ (the "License"); you may not use this file except in compliance with
7+
~ the License. You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<h2 mat-dialog-title>Move Controller Service</h2>
19+
@if ((controllerService$ | async)!; as controllerService) {
20+
<form class="controller-service-enable-form" [formGroup]="moveControllerServiceForm">
21+
<mat-dialog-content>
22+
<div class="py-4 flex gap-x-4">
23+
<div class="flex basis-2/3 flex-col gap-y-4 pr-4 overflow-hidden">
24+
<div class="flex flex-col">
25+
<div>Service</div>
26+
<div
27+
class="accent-color font-medium overflow-ellipsis overflow-hidden whitespace-nowrap"
28+
[title]="controllerService.component.name">
29+
{{ controllerService.component.name }}
30+
</div>
31+
</div>
32+
<div>
33+
<mat-form-field>
34+
<mat-label>Process Group:</mat-label>
35+
<mat-select formControlName="processGroups" >
36+
@for (option of controllerServiceActionProcessGroups; track option) {
37+
<mat-option
38+
[value]="option.value">
39+
{{ option.text }}
40+
</mat-option>
41+
}
42+
</mat-select>
43+
</mat-form-field>
44+
</div>
45+
</div>
46+
<div class="flex basis-1/3 flex-col">
47+
<div>
48+
Referencing Components
49+
<i
50+
class="fa fa-info-circle"
51+
nifiTooltip
52+
[tooltipComponentType]="TextTip"
53+
tooltipInputData="Other components referencing this controller service."></i>
54+
</div>
55+
<div class="relative h-full border" style="min-height: 320px">
56+
<div class="absolute inset-0 overflow-y-auto p-1">
57+
<controller-service-references
58+
[serviceReferences]="controllerService.component.referencingComponents"
59+
[goToReferencingComponent]="
60+
goToReferencingComponent
61+
"></controller-service-references>
62+
</div>
63+
</div>
64+
</div>
65+
</div>
66+
</mat-dialog-content>
67+
<mat-dialog-actions align="end">
68+
<button mat-button mat-dialog-close>Cancel</button>
69+
<button type="button" color="primary" (click)="submitForm()" mat-button>Move</button>
70+
</mat-dialog-actions>
71+
</form>
72+
}
73+
<ng-template #stepInProgress>
74+
<div class="fa fa-spin fa-circle-o-notch primary-color"></div>
75+
</ng-template>
76+
<ng-template #stepComplete>
77+
<div class="complete fa fa-check success-color"></div>
78+
</ng-template>
79+
<ng-template #stepError>
80+
<div class="fa fa-times warn-color"></div>
81+
</ng-template>
82+
<ng-template #stepNotStarted><div class="w-3.5"></div></ng-template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
@use '@angular/material' as mat;
19+
20+
.controller-service-move-form {
21+
@include mat.button-density(-1);
22+
23+
.mat-mdc-form-field {
24+
width: 100%;
25+
}
26+
}

0 commit comments

Comments
 (0)