From 0aeef765bb332b718d9f1eda75cfcb5e2018eea0 Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Fri, 10 Jan 2025 16:41:36 +0100 Subject: [PATCH] feat: implement secret ref finder for native API https://gravitee.atlassian.net/browse/APIM-8058 (cherry picked from commit 6bab4fbe5d3cc68ae19334519d0842b7403800d7) --- .../secrets/AbstractV4APISecretRefFinder.java | 117 +++++++ .../ApiV4DefinitionSecretRefsFinder.java | 99 +----- ...NativeApiV4DefinitionSecretRefsFinder.java | 89 ++++++ .../reactor/spring/ReactorConfiguration.java | 6 + ...veApiV4DefinitionSecretRefsFinderTest.java | 299 ++++++++++++++++++ 5 files changed, 522 insertions(+), 88 deletions(-) create mode 100644 gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/AbstractV4APISecretRefFinder.java create mode 100644 gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/NativeApiV4DefinitionSecretRefsFinder.java create mode 100644 gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/test/java/io/gravitee/gateway/reactive/reactor/v4/secrets/NativeApiV4DefinitionSecretRefsFinderTest.java diff --git a/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/AbstractV4APISecretRefFinder.java b/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/AbstractV4APISecretRefFinder.java new file mode 100644 index 00000000000..ac3408121f3 --- /dev/null +++ b/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/AbstractV4APISecretRefFinder.java @@ -0,0 +1,117 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.gateway.reactive.reactor.v4.secrets; + +import static io.gravitee.secrets.api.discovery.SecretRefsLocation.PLUGIN_KIND; + +import io.gravitee.definition.model.v4.endpointgroup.AbstractEndpoint; +import io.gravitee.definition.model.v4.endpointgroup.AbstractEndpointGroup; +import io.gravitee.definition.model.v4.flow.step.Step; +import io.gravitee.definition.model.v4.listener.entrypoint.AbstractEntrypoint; +import io.gravitee.definition.model.v4.plan.AbstractPlan; +import io.gravitee.definition.model.v4.resource.Resource; +import io.gravitee.definition.model.v4.service.Service; +import io.gravitee.secrets.api.discovery.DefinitionSecretRefsFinder; +import io.gravitee.secrets.api.discovery.DefinitionSecretRefsListener; +import io.gravitee.secrets.api.discovery.SecretRefsLocation; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public abstract class AbstractV4APISecretRefFinder implements DefinitionSecretRefsFinder { + + protected List safeList(List col) { + return col == null ? List.of() : col; + } + + protected Stream safeStream(Collection col) { + return col == null ? Stream.empty() : col.stream(); + } + + protected boolean canHandle(Object definition, Class clazz) { + return definition != null && clazz.isAssignableFrom(definition.getClass()); + } + + protected void processEntrypoint(DefinitionSecretRefsListener listener, AbstractEntrypoint entrypoint) { + listener.onCandidate( + entrypoint.getConfiguration(), + new SecretRefsLocation(PLUGIN_KIND, entrypoint.getType()), + entrypoint::setConfiguration + ); + } + + protected void processPlanConfiguration(DefinitionSecretRefsListener listener, AbstractPlan plan) { + Optional + .ofNullable(plan.getSecurity()) + .ifPresent(security -> + listener.onCandidate( + security.getConfiguration(), + new SecretRefsLocation(PLUGIN_KIND, security.getType()), + security::setConfiguration + ) + ); + } + + protected void processStep(DefinitionSecretRefsListener listener, Step step) { + listener.onCandidate(step.getConfiguration(), new SecretRefsLocation(PLUGIN_KIND, step.getPolicy()), step::setConfiguration); + } + + protected Stream processEndpointGroup( + DefinitionSecretRefsListener listener, + AbstractEndpointGroup endpointGroup + ) { + Optional + .ofNullable(endpointGroup.getSharedConfiguration()) + .ifPresent(payload -> + listener.onCandidate( + payload, + new SecretRefsLocation(PLUGIN_KIND, endpointGroup.getType()), + endpointGroup::setSharedConfiguration + ) + ); + return safeStream(endpointGroup.getEndpoints()); + } + + protected static void processResource(DefinitionSecretRefsListener listener, Resource resource) { + listener.onCandidate( + resource.getConfiguration(), + new SecretRefsLocation(PLUGIN_KIND, resource.getType()), + resource::setConfiguration + ); + } + + protected void processEndpoint(DefinitionSecretRefsListener listener, AbstractEndpoint endpoint) { + listener.onCandidate( + endpoint.getConfiguration(), + new SecretRefsLocation(PLUGIN_KIND, endpoint.getType()), + endpoint::setConfiguration + ); + listener.onCandidate( + endpoint.getSharedConfigurationOverride(), + new SecretRefsLocation(PLUGIN_KIND, endpoint.getType()), + endpoint::setSharedConfigurationOverride + ); + } + + protected void processService(DefinitionSecretRefsListener listener, Service service) { + listener.onCandidate(service.getConfiguration(), new SecretRefsLocation(PLUGIN_KIND, service.getType()), service::setConfiguration); + } +} diff --git a/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/ApiV4DefinitionSecretRefsFinder.java b/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/ApiV4DefinitionSecretRefsFinder.java index 677c46d5ebd..b21dd633883 100644 --- a/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/ApiV4DefinitionSecretRefsFinder.java +++ b/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/ApiV4DefinitionSecretRefsFinder.java @@ -15,8 +15,6 @@ */ package io.gravitee.gateway.reactive.reactor.v4.secrets; -import static io.gravitee.secrets.api.discovery.SecretRefsLocation.PLUGIN_KIND; - import io.gravitee.definition.model.ResponseTemplate; import io.gravitee.definition.model.v4.Api; import io.gravitee.definition.model.v4.endpointgroup.service.EndpointGroupServices; @@ -27,11 +25,9 @@ import io.gravitee.secrets.api.discovery.Definition; import io.gravitee.secrets.api.discovery.DefinitionDescriptor; import io.gravitee.secrets.api.discovery.DefinitionMetadata; -import io.gravitee.secrets.api.discovery.DefinitionSecretRefsFinder; import io.gravitee.secrets.api.discovery.DefinitionSecretRefsListener; import io.gravitee.secrets.api.discovery.SecretRefsLocation; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; @@ -42,13 +38,13 @@ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) * @author GraviteeSource Team */ -public class ApiV4DefinitionSecretRefsFinder implements DefinitionSecretRefsFinder { +public class ApiV4DefinitionSecretRefsFinder extends AbstractV4APISecretRefFinder { public static final String RESPONSE_TEMPLATES_KIND = "response-templates"; @Override public boolean canHandle(Object definition) { - return definition != null && Api.class.isAssignableFrom(definition.getClass()); + return canHandle(definition, Api.class); } @Override @@ -61,23 +57,10 @@ public void findSecretRefs(Api definition, DefinitionSecretRefsListener listener // listeners safeStream(definition.getListeners()) .flatMap(l -> safeStream(l.getEntrypoints())) - .forEach(entrypoint -> - listener.onCandidate( - entrypoint.getConfiguration(), - new SecretRefsLocation(PLUGIN_KIND, entrypoint.getType()), - entrypoint::setConfiguration - ) - ); + .forEach(entrypoint -> processEntrypoint(listener, entrypoint)); // resources - safeStream(definition.getResources()) - .forEach(resource -> - listener.onCandidate( - resource.getConfiguration(), - new SecretRefsLocation(PLUGIN_KIND, resource.getType()), - resource::setConfiguration - ) - ); + safeStream(definition.getResources()).forEach(resource -> processResource(listener, resource)); // flows api and plan List flows = safeList(definition.getPlans()) @@ -96,22 +79,9 @@ public void findSecretRefs(Api definition, DefinitionSecretRefsListener listener flows.stream().flatMap(flow -> safeStream(flow.getSubscribe())) ) ) - .forEach(step -> - listener.onCandidate(step.getConfiguration(), new SecretRefsLocation(PLUGIN_KIND, step.getPolicy()), step::setConfiguration) - ); + .forEach(step -> processStep(listener, step)); - safeStream(definition.getPlans()) - .forEach(plan -> - Optional - .ofNullable(plan.getSecurity()) - .ifPresent(security -> - listener.onCandidate( - security.getConfiguration(), - new SecretRefsLocation(PLUGIN_KIND, security.getType()), - security::setConfiguration - ) - ) - ); + safeStream(definition.getPlans()).forEach(plan -> processPlanConfiguration(listener, plan)); // endpoint groups safeStream(definition.getEndpointGroups()) @@ -124,62 +94,23 @@ public void findSecretRefs(Api definition, DefinitionSecretRefsListener listener if (services.getHealthCheck() != null) { list.add(services.getHealthCheck()); } - list - .stream() - .filter(Service::isEnabled) - .forEach(service -> - listener.onCandidate( - service.getConfiguration(), - new SecretRefsLocation(PLUGIN_KIND, service.getType()), - service::setConfiguration - ) - ); - Optional - .ofNullable(endpointGroup.getSharedConfiguration()) - .ifPresent(payload -> - listener.onCandidate( - payload, - new SecretRefsLocation(PLUGIN_KIND, endpointGroup.getType()), - endpointGroup::setSharedConfiguration - ) - ); - return safeStream(endpointGroup.getEndpoints()); + list.stream().filter(Service::isEnabled).forEach(service -> processService(listener, service)); + return processEndpointGroup(listener, endpointGroup); }) .forEach(endpoint -> { Optional .ofNullable(endpoint.getServices()) .map(EndpointServices::getHealthCheck) .filter(Service::isEnabled) - .ifPresent(service -> - listener.onCandidate( - service.getConfiguration(), - new SecretRefsLocation(PLUGIN_KIND, service.getType()), - service::setConfiguration - ) - ); - listener.onCandidate( - endpoint.getConfiguration(), - new SecretRefsLocation(PLUGIN_KIND, endpoint.getType()), - endpoint::setConfiguration - ); - listener.onCandidate( - endpoint.getSharedConfigurationOverride(), - new SecretRefsLocation(PLUGIN_KIND, endpoint.getType()), - endpoint::setSharedConfigurationOverride - ); + .ifPresent(service -> processService(listener, service)); + processEndpoint(listener, endpoint); }); // services Optional .ofNullable(definition.getServices()) .map(ApiServices::getDynamicProperty) - .ifPresent(dynamicProperty -> - listener.onCandidate( - dynamicProperty.getConfiguration(), - new SecretRefsLocation(PLUGIN_KIND, dynamicProperty.getType()), - dynamicProperty::setConfiguration - ) - ); + .ifPresent(dynamicProperty -> processService(listener, dynamicProperty)); // response templates Map> responseTemplates = definition.getResponseTemplates(); @@ -205,14 +136,6 @@ public void findSecretRefs(Api definition, DefinitionSecretRefsListener listener ); } - private List safeList(List col) { - return col == null ? List.of() : col; - } - - public Stream safeStream(Collection col) { - return col == null ? Stream.empty() : col.stream(); - } - public Stream safeKeySetStream(Map map) { return map == null ? Stream.empty() : map.keySet().stream(); } diff --git a/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/NativeApiV4DefinitionSecretRefsFinder.java b/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/NativeApiV4DefinitionSecretRefsFinder.java new file mode 100644 index 00000000000..8fb81459449 --- /dev/null +++ b/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactive/reactor/v4/secrets/NativeApiV4DefinitionSecretRefsFinder.java @@ -0,0 +1,89 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.gateway.reactive.reactor.v4.secrets; + +import io.gravitee.definition.model.v4.nativeapi.NativeApi; +import io.gravitee.definition.model.v4.nativeapi.NativeApiServices; +import io.gravitee.definition.model.v4.nativeapi.NativeFlow; +import io.gravitee.secrets.api.discovery.Definition; +import io.gravitee.secrets.api.discovery.DefinitionDescriptor; +import io.gravitee.secrets.api.discovery.DefinitionMetadata; +import io.gravitee.secrets.api.discovery.DefinitionSecretRefsListener; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public class NativeApiV4DefinitionSecretRefsFinder extends AbstractV4APISecretRefFinder { + + @Override + public boolean canHandle(Object definition) { + return canHandle(definition, NativeApi.class); + } + + @Override + public DefinitionDescriptor toDefinitionDescriptor(NativeApi definition, DefinitionMetadata metadata) { + return new DefinitionDescriptor(new Definition("native-api-v4", definition.getId()), Optional.ofNullable(metadata.revision())); + } + + @Override + public void findSecretRefs(NativeApi definition, DefinitionSecretRefsListener listener) { + // listeners + safeStream(definition.getListeners()) + .flatMap(l -> safeStream(l.getEntrypoints())) + .forEach(entrypoint -> processEntrypoint(listener, entrypoint)); + + // resources + safeStream(definition.getResources()).forEach(resource -> processResource(listener, resource)); + + // flows api and plan + List flows = safeList(definition.getPlans()) + .stream() + .flatMap(p -> safeStream(p.getFlows())) + .collect(Collectors.toCollection(ArrayList::new)); + flows.addAll(safeList(definition.getFlows())); + Stream + .concat( + Stream.concat( + flows.stream().flatMap(flow -> safeStream(flow.getPublish())), + flows.stream().flatMap(flow -> safeStream(flow.getSubscribe())) + ), + Stream.concat( + flows.stream().flatMap(flow -> safeStream(flow.getConnect())), + flows.stream().flatMap(flow -> safeStream(flow.getInteract())) + ) + ) + .forEach(step -> processStep(listener, step)); + + safeStream(definition.getPlans()).forEach(plan -> processPlanConfiguration(listener, plan)); + + // endpoint groups + safeStream(definition.getEndpointGroups()) + .flatMap(endpointGroup -> processEndpointGroup(listener, endpointGroup)) + .forEach(endpoint -> processEndpoint(listener, endpoint)); + + // services + Optional + .ofNullable(definition.getServices()) + .map(NativeApiServices::getDynamicProperty) + .ifPresent(dynamicProperty -> processService(listener, dynamicProperty)); + } +} diff --git a/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactor/spring/ReactorConfiguration.java b/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactor/spring/ReactorConfiguration.java index 411292a336f..febca8dddcc 100644 --- a/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactor/spring/ReactorConfiguration.java +++ b/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/main/java/io/gravitee/gateway/reactor/spring/ReactorConfiguration.java @@ -41,6 +41,7 @@ import io.gravitee.gateway.reactive.reactor.v4.reactor.ReactorFactory; import io.gravitee.gateway.reactive.reactor.v4.reactor.ReactorFactoryManager; import io.gravitee.gateway.reactive.reactor.v4.secrets.ApiV4DefinitionSecretRefsFinder; +import io.gravitee.gateway.reactive.reactor.v4.secrets.NativeApiV4DefinitionSecretRefsFinder; import io.gravitee.gateway.reactor.Reactor; import io.gravitee.gateway.reactor.handler.AcceptorResolver; import io.gravitee.gateway.reactor.handler.ReactorEventListener; @@ -266,4 +267,9 @@ public NodeTemplateVariableProvider nodeTemplateVariableProvider(Node node, Gate public DefinitionSecretRefsFinder v4ApiDefinitionSecretRefsFinder() { return new ApiV4DefinitionSecretRefsFinder(); } + + @Bean + public DefinitionSecretRefsFinder nativeV4ApiDefinitionSecretRefsFinder() { + return new NativeApiV4DefinitionSecretRefsFinder(); + } } diff --git a/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/test/java/io/gravitee/gateway/reactive/reactor/v4/secrets/NativeApiV4DefinitionSecretRefsFinderTest.java b/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/test/java/io/gravitee/gateway/reactive/reactor/v4/secrets/NativeApiV4DefinitionSecretRefsFinderTest.java new file mode 100644 index 00000000000..2bd100cc5a5 --- /dev/null +++ b/gravitee-apim-gateway/gravitee-apim-gateway-reactor/src/test/java/io/gravitee/gateway/reactive/reactor/v4/secrets/NativeApiV4DefinitionSecretRefsFinderTest.java @@ -0,0 +1,299 @@ +/* + * Copyright © 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.gravitee.gateway.reactive.reactor.v4.secrets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import io.gravitee.definition.model.v4.flow.step.Step; +import io.gravitee.definition.model.v4.nativeapi.*; +import io.gravitee.definition.model.v4.nativeapi.kafka.KafkaListener; +import io.gravitee.definition.model.v4.plan.PlanSecurity; +import io.gravitee.definition.model.v4.resource.Resource; +import io.gravitee.definition.model.v4.service.Service; +import io.gravitee.secrets.api.discovery.Definition; +import io.gravitee.secrets.api.discovery.DefinitionDescriptor; +import io.gravitee.secrets.api.discovery.DefinitionMetadata; +import io.gravitee.secrets.api.discovery.DefinitionSecretRefsFinder; +import java.util.*; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class NativeApiV4DefinitionSecretRefsFinderTest { + + DefinitionSecretRefsFinder underTest = new NativeApiV4DefinitionSecretRefsFinder(); + + @Test + void should_can_handle() { + assertThat(underTest.canHandle(null)).isFalse(); + assertThat(underTest.canHandle(new io.gravitee.definition.model.v4.Api())).isFalse(); + assertThat(underTest.canHandle(new NativeApi())).isTrue(); + } + + @Test + void should_get_definition() { + NativeApi api = new NativeApi(); + api.setId("foo"); + assertThat(underTest.toDefinitionDescriptor(api, new DefinitionMetadata(null))) + .isEqualTo(new DefinitionDescriptor(new Definition("native-api-v4", "foo"), Optional.empty())); + assertThat(underTest.toDefinitionDescriptor(api, new DefinitionMetadata("42"))) + .isEqualTo(new DefinitionDescriptor(new Definition("native-api-v4", "foo"), Optional.of("42"))); + } + + public static Stream apis() { + NativeApi empty = new NativeApi(); + + NativeApi withResource = new NativeApi(); + withResource.setResources(List.of()); + + NativeApi withPlans = new NativeApi(); + withPlans.setPlans(List.of()); + + NativeApi withEmptyPlans = new NativeApi(); + withEmptyPlans.setPlans(List.of(new NativePlan())); + + NativeApi withPlanAndEmptyFlow = new NativeApi(); + NativePlan planEmptyFlow = new NativePlan(); + planEmptyFlow.setFlows(List.of()); + withPlanAndEmptyFlow.setPlans(List.of(planEmptyFlow)); + + NativeApi withPlanAndFlowNoStep = new NativeApi(); + NativePlan planFlowNoStep = new NativePlan(); + planFlowNoStep.setFlows(List.of(new NativeFlow())); + withPlanAndFlowNoStep.setPlans(List.of(planFlowNoStep)); + + NativeApi withEmptyFlow = new NativeApi(); + withEmptyFlow.setFlows(List.of()); + + NativeApi withFlowNoStep = new NativeApi(); + withFlowNoStep.setFlows(List.of(new NativeFlow())); + + NativeApi withEmptyListener = new NativeApi(); + withEmptyListener.setListeners(List.of()); + + NativeApi withNoEndpointGroup = new NativeApi(); + withNoEndpointGroup.setEndpointGroups(List.of()); + + NativeApi withEmptyEndpointGroup = new NativeApi(); + withEmptyEndpointGroup.setEndpointGroups(List.of(new NativeEndpointGroup())); + + return Stream.of( + arguments("empty", empty), + arguments("with resource", withResource), + arguments("with plans", withPlans), + arguments("with empty plans", withEmptyPlans), + arguments("with plan and empty flow", withPlanAndEmptyFlow), + arguments("with plan and flow no step", withPlanAndFlowNoStep), + arguments("with empty flow", withEmptyFlow), + arguments("with flow no step", withFlowNoStep), + arguments("with empty listener", withEmptyListener), + arguments("with empty endpoint group", withEmptyEndpointGroup), + arguments("with no endpoint group", withNoEndpointGroup) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("apis") + void should_no_fail_when_parts_of_the_api_is_null(String name, NativeApi api) { + assertThatCode(() -> underTest.findSecretRefs(api, (config, location, setter) -> setter.accept(processed(config)))) + .doesNotThrowAnyException(); + } + + @Test + void should_find_secrets() { + Set expectedConfigs = new FailOnDuplicateSet<>(); + Set expectedLocations = new FailOnDuplicateSet<>(); + + String entrypointConfig = "entrypoint config"; + String resourceConfig = "resource config"; + String planPublishFlowConfig = "plan flow publish config"; + String planSubscribeFlowConfig = "plan flow subscribe config"; + String planConnectFlowConfig = "plan flow connect config"; + String planInteractFlowConfig = "plan flow interact config"; + String definitionPublishFlowConfig = "definition flow publish config"; + String definitionSubscribeFlowConfig = "definition flow subscribe config"; + String definitionConnectFlowConfig = "definition flow connect config"; + String definitionInteractFlowConfig = "definition flow interact config"; + String planSecurityConfig = "plan security config"; + String endpointGroupSharedConfig = "endpoint group shared config"; + String endpointConfig = "endpoint config"; + String endpointSharedOverrideConfig = "endpoint shared override config"; + String dynamicPropertyServiceConfig = "dyn property config"; + expectedConfigs.add(entrypointConfig); + expectedConfigs.add(resourceConfig); + expectedConfigs.add(planPublishFlowConfig); + expectedConfigs.add(planSubscribeFlowConfig); + expectedConfigs.add(planConnectFlowConfig); + expectedConfigs.add(planInteractFlowConfig); + expectedConfigs.add(definitionPublishFlowConfig); + expectedConfigs.add(definitionSubscribeFlowConfig); + expectedConfigs.add(definitionConnectFlowConfig); + expectedConfigs.add(definitionInteractFlowConfig); + expectedConfigs.add(planSecurityConfig); + expectedConfigs.add(endpointGroupSharedConfig); + expectedConfigs.add(endpointConfig); + expectedConfigs.add(endpointSharedOverrideConfig); + expectedConfigs.add(dynamicPropertyServiceConfig); + + String entrypointType = "test endpoint type"; + String resourceType = "test resource type"; + String planPublishFlowPolicy = "plan flow publish policy"; + String planSubscribeFlowPolicy = "plan flow subscribe policy"; + String planConnectFlowPolicy = "plan flow connect policy"; + String planInteractFlowPolicy = "plan flow interact policy"; + String definitionPublishFlowPolicy = "definition flow publish policy"; + String definitionSubscribeFlowPolicy = "definition flow subscribe policy"; + String definitionConnectFlowPolicy = "definition flow connect policy"; + String definitionInteractFlowPolicy = "definition flow interact policy"; + String planSecurityType = "plan security type"; + String endpointGroupType = "endpoint group type"; + String endpointType = "endpoint type"; + String dynamicPropertyServiceType = "dyn property type"; + expectedLocations.add(entrypointType); + expectedLocations.add(resourceType); + expectedLocations.add(planPublishFlowPolicy); + expectedLocations.add(planSubscribeFlowPolicy); + expectedLocations.add(planConnectFlowPolicy); + expectedLocations.add(planInteractFlowPolicy); + expectedLocations.add(definitionPublishFlowPolicy); + expectedLocations.add(definitionSubscribeFlowPolicy); + expectedLocations.add(definitionConnectFlowPolicy); + expectedLocations.add(definitionInteractFlowPolicy); + expectedLocations.add(planSecurityType); + expectedLocations.add(endpointGroupType); + expectedLocations.add(endpointType); + expectedLocations.add(dynamicPropertyServiceType); + + // pre-test check (+1 because endpoint type is used twice) + assertThat(expectedConfigs).hasSize(expectedLocations.size() + 1); + + NativeApi api = new NativeApi(); + NativeListener kafkaListener = new KafkaListener(); + NativeEntrypoint entryPoint = new NativeEntrypoint(); + entryPoint.setType(entrypointType); + entryPoint.setConfiguration(entrypointConfig); + kafkaListener.setEntrypoints(List.of(entryPoint)); + api.setListeners(List.of(kafkaListener)); + Resource resource = new Resource(); + resource.setType(resourceType); + resource.setConfiguration(resourceConfig); + api.setResources(List.of(resource)); + NativePlan plan = new NativePlan(); + PlanSecurity security = new PlanSecurity(); + security.setConfiguration(planSecurityConfig); + security.setType(planSecurityType); + plan.setSecurity(security); + NativeFlow planFlow = new NativeFlow(); + planFlow.setPublish(List.of(newStep(planPublishFlowConfig, planPublishFlowPolicy))); + planFlow.setSubscribe(List.of(newStep(planSubscribeFlowConfig, planSubscribeFlowPolicy))); + planFlow.setConnect(List.of(newStep(planConnectFlowConfig, planConnectFlowPolicy))); + planFlow.setInteract(List.of(newStep(planInteractFlowConfig, planInteractFlowPolicy))); + plan.setFlows(List.of(planFlow)); + api.setPlans(List.of(plan)); + NativeFlow flow = new NativeFlow(); + flow.setConnect(List.of(newStep(definitionConnectFlowConfig, definitionConnectFlowPolicy))); + flow.setInteract(List.of(newStep(definitionInteractFlowConfig, definitionInteractFlowPolicy))); + flow.setPublish(List.of(newStep(definitionPublishFlowConfig, definitionPublishFlowPolicy))); + flow.setSubscribe(List.of(newStep(definitionSubscribeFlowConfig, definitionSubscribeFlowPolicy))); + api.setFlows(List.of(flow)); + NativeEndpointGroup endpointGroup = new NativeEndpointGroup(); + endpointGroup.setSharedConfiguration(endpointGroupSharedConfig); + endpointGroup.setType(endpointGroupType); + NativeEndpoint endpoint = new NativeEndpoint(); + endpoint.setType(endpointType); + endpoint.setConfiguration(endpointConfig); + endpoint.setSharedConfigurationOverride(endpointSharedOverrideConfig); + endpointGroup.setEndpoints(List.of(endpoint)); + api.setEndpointGroups(List.of(endpointGroup)); + NativeApiServices apiServices = new NativeApiServices(); + Service dynamicPropertyService = new Service(); + dynamicPropertyService.setType(dynamicPropertyServiceType); + dynamicPropertyService.setConfiguration(dynamicPropertyServiceConfig); + apiServices.setDynamicProperty(dynamicPropertyService); + api.setServices(apiServices); + + //data gathering + Set actualConfigs = new HashSet<>(); + Set actualLocations = new HashSet<>(); + + // execute test + underTest.findSecretRefs( + api, + (config, location, setter) -> { + actualConfigs.add(config); + actualLocations.add(location.id()); + // simulate plugin has processed the config + String processed = processed(config); + setter.accept(processed); + } + ); + + // assertions + assertThat(actualConfigs).hasSameElementsAs(expectedConfigs); + assertThat(actualLocations).hasSameElementsAs(expectedLocations); + + assertThat(api.getListeners().get(0).getEntrypoints().get(0).getConfiguration()).isEqualTo(processed(entrypointConfig)); + assertThat(api.getResources().get(0).getConfiguration()).isEqualTo(processed(resourceConfig)); + assertThat(api.getPlans().get(0).getSecurity().getConfiguration()).isEqualTo(processed(planSecurityConfig)); + assertThat(api.getPlans().get(0).getFlows().get(0).getPublish().get(0).getConfiguration()) + .isEqualTo(processed(planPublishFlowConfig)); + assertThat(api.getPlans().get(0).getFlows().get(0).getSubscribe().get(0).getConfiguration()) + .isEqualTo(processed(planSubscribeFlowConfig)); + assertThat(api.getPlans().get(0).getFlows().get(0).getConnect().get(0).getConfiguration()) + .isEqualTo(processed(planConnectFlowConfig)); + assertThat(api.getPlans().get(0).getFlows().get(0).getInteract().get(0).getConfiguration()) + .isEqualTo(processed(planInteractFlowConfig)); + assertThat(api.getFlows().get(0).getPublish().get(0).getConfiguration()).isEqualTo(processed(definitionPublishFlowConfig)); + assertThat(api.getFlows().get(0).getSubscribe().get(0).getConfiguration()).isEqualTo(processed(definitionSubscribeFlowConfig)); + assertThat(api.getFlows().get(0).getConnect().get(0).getConfiguration()).isEqualTo(processed(definitionConnectFlowConfig)); + assertThat(api.getFlows().get(0).getInteract().get(0).getConfiguration()).isEqualTo(processed(definitionInteractFlowConfig)); + assertThat(api.getEndpointGroups().get(0).getSharedConfiguration()).isEqualTo(processed(endpointGroupSharedConfig)); + assertThat(api.getEndpointGroups().get(0).getEndpoints().get(0).getConfiguration()).isEqualTo(processed(endpointConfig)); + } + + private static Step newStep(String configuration, String policy) { + Step requestStep = new Step(); + requestStep.setConfiguration(configuration); + requestStep.setPolicy(policy); + return requestStep; + } + + String processed(String original) { + return original.concat(" - updated!"); + } + + private static class FailOnDuplicateSet extends LinkedHashSet { + + @Override + public boolean add(T o) { + if (!super.add(o)) { + throw new IllegalArgumentException("[" + o + "] exists in set, test cannot work"); + } + return false; + } + } +}