mirror of https://github.com/dapr/java-sdk.git
[1.15] support for api token (#1453)
* supporting spring boot property for API TOKEN on workflow interceptor (#1452) Signed-off-by: salaboy <Salaboy@gmail.com> * Supporting placement and scheduler custom images (#1450) * supporting placement and scheduler custom images Signed-off-by: salaboy <Salaboy@gmail.com> * Bump org.apache.commons:commons-lang3 from 3.9 to 3.18.0 (#1446) Bumps org.apache.commons:commons-lang3 from 3.9 to 3.18.0. --- updated-dependencies: - dependency-name: org.apache.commons:commons-lang3 dependency-version: 3.18.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: artur-ciocanu <artur.ciocanu@gmail.com> Signed-off-by: salaboy <Salaboy@gmail.com> * Update dapr docs for Hugo upgrade (#1443) Signed-off-by: Marc Duiker <marcduiker@users.noreply.github.com> Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com> Signed-off-by: salaboy <Salaboy@gmail.com> * Adds note about workflow start time (#1444) Signed-off-by: joshvanl <me@joshvanl.dev> Co-authored-by: Dapr Bot <56698301+dapr-bot@users.noreply.github.com> Co-authored-by: artur-ciocanu <artur.ciocanu@gmail.com> Signed-off-by: salaboy <Salaboy@gmail.com> * adding test to validate canonical names with substitutes Signed-off-by: salaboy <Salaboy@gmail.com> * Migrate PubSub removing flaky test (#1407) * Migrate PubSub removing flaky test Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com> * Adjust assertion Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com> * Change assert Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com> * Apply pull request suggestions Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com> * Use custom ObjectSerializer Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com> --------- Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com> Co-authored-by: artur-ciocanu <artur.ciocanu@gmail.com> Signed-off-by: salaboy <Salaboy@gmail.com> * adding tests for coverage Signed-off-by: salaboy <Salaboy@gmail.com> --------- Signed-off-by: salaboy <Salaboy@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Marc Duiker <marcduiker@users.noreply.github.com> Signed-off-by: joshvanl <me@joshvanl.dev> Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: artur-ciocanu <artur.ciocanu@gmail.com> Co-authored-by: Marc Duiker <marcduiker@users.noreply.github.com> Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com> Co-authored-by: Josh van Leeuwen <me@joshvanl.dev> Co-authored-by: Dapr Bot <56698301+dapr-bot@users.noreply.github.com> Co-authored-by: Matheus Cruz <56329339+mcruzdev@users.noreply.github.com> --------- Signed-off-by: salaboy <Salaboy@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Marc Duiker <marcduiker@users.noreply.github.com> Signed-off-by: joshvanl <me@joshvanl.dev> Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com> Co-authored-by: salaboy <Salaboy@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: artur-ciocanu <artur.ciocanu@gmail.com> Co-authored-by: Marc Duiker <marcduiker@users.noreply.github.com> Co-authored-by: Josh van Leeuwen <me@joshvanl.dev> Co-authored-by: Dapr Bot <56698301+dapr-bot@users.noreply.github.com> Co-authored-by: Matheus Cruz <56329339+mcruzdev@users.noreply.github.com> Signed-off-by: siri-varma <siri.varma@outlook.com>
This commit is contained in:
parent
fe63a9330c
commit
15da2eaf36
|
|
@ -68,6 +68,11 @@ public class DaprClientAutoConfiguration {
|
|||
builder.withPropertyOverride(Properties.GRPC_PORT, String.valueOf(grpcPort));
|
||||
}
|
||||
|
||||
String apiToken = daprConnectionDetails.getApiToken();
|
||||
if (apiToken != null) {
|
||||
builder.withPropertyOverride(Properties.API_TOKEN, apiToken);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
|
@ -145,6 +150,11 @@ public class DaprClientAutoConfiguration {
|
|||
propertyOverrides.put(Properties.GRPC_PORT.getName(), String.valueOf(grpcPort));
|
||||
}
|
||||
|
||||
String apiToken = daprConnectionDetails.getApiToken();
|
||||
if (apiToken != null) {
|
||||
propertyOverrides.put(Properties.API_TOKEN.getName(), apiToken);
|
||||
}
|
||||
|
||||
return new Properties(propertyOverrides);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ limitations under the License.
|
|||
|
||||
package io.dapr.spring.boot.autoconfigure.client;
|
||||
|
||||
import io.dapr.spring.data.DaprKeyValueAdapterResolver;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "dapr.client")
|
||||
|
|
@ -22,6 +21,7 @@ public class DaprClientProperties {
|
|||
private String grpcEndpoint;
|
||||
private Integer httpPort;
|
||||
private Integer grpcPort;
|
||||
private String apiToken;
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -36,12 +36,15 @@ public class DaprClientProperties {
|
|||
* @param grpcEndpoint grpc endpoint to interact with the Dapr Sidecar
|
||||
* @param httpPort http port to interact with the Dapr Sidecar
|
||||
* @param grpcPort grpc port to interact with the Dapr Sidecar
|
||||
* @param apiToken dapr API token to interact with the Dapr Sidecar
|
||||
*/
|
||||
public DaprClientProperties(String httpEndpoint, String grpcEndpoint, Integer httpPort, Integer grpcPort) {
|
||||
public DaprClientProperties(String httpEndpoint, String grpcEndpoint, Integer httpPort, Integer grpcPort,
|
||||
String apiToken) {
|
||||
this.httpEndpoint = httpEndpoint;
|
||||
this.grpcEndpoint = grpcEndpoint;
|
||||
this.httpPort = httpPort;
|
||||
this.grpcPort = grpcPort;
|
||||
this.apiToken = apiToken;
|
||||
}
|
||||
|
||||
public String getHttpEndpoint() {
|
||||
|
|
@ -75,4 +78,12 @@ public class DaprClientProperties {
|
|||
public void setGrpcPort(Integer grpcPort) {
|
||||
this.grpcPort = grpcPort;
|
||||
}
|
||||
|
||||
public String getApiToken() {
|
||||
return apiToken;
|
||||
}
|
||||
|
||||
public void setApiToken(String apiToken) {
|
||||
this.apiToken = apiToken;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ package io.dapr.spring.boot.autoconfigure.client;
|
|||
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
|
||||
|
||||
public interface DaprConnectionDetails extends ConnectionDetails {
|
||||
|
||||
String getHttpEndpoint();
|
||||
|
||||
String getGrpcEndpoint();
|
||||
|
|
@ -23,4 +24,7 @@ public interface DaprConnectionDetails extends ConnectionDetails {
|
|||
Integer getHttpPort();
|
||||
|
||||
Integer getGrpcPort();
|
||||
|
||||
String getApiToken();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,4 +40,10 @@ class PropertiesDaprConnectionDetails implements DaprConnectionDetails {
|
|||
public Integer getGrpcPort() {
|
||||
return this.daprClientProperties.getGrpcPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApiToken() {
|
||||
return this.daprClientProperties.getApiToken();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public class DaprClientPropertiesTest {
|
|||
public void shouldCreateDaprClientPropertiesCorrectly() {
|
||||
|
||||
DaprClientProperties properties = new DaprClientProperties(
|
||||
"http://localhost", "localhost", 3500, 50001
|
||||
"http://localhost", "localhost", 3500, 50001, "ABC"
|
||||
);
|
||||
|
||||
SoftAssertions.assertSoftly(softly -> {
|
||||
|
|
@ -39,6 +39,7 @@ public class DaprClientPropertiesTest {
|
|||
softly.assertThat(properties.getHttpEndpoint()).isEqualTo("http://localhost");
|
||||
softly.assertThat(properties.getHttpPort()).isEqualTo(3500);
|
||||
softly.assertThat(properties.getGrpcPort()).isEqualTo(50001);
|
||||
softly.assertThat(properties.getApiToken()).isEqualTo("ABC");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -52,12 +53,14 @@ public class DaprClientPropertiesTest {
|
|||
properties.setGrpcPort(50001);
|
||||
properties.setHttpEndpoint("http://localhost");
|
||||
properties.setHttpPort(3500);
|
||||
properties.setApiToken("ABC");
|
||||
|
||||
SoftAssertions.assertSoftly(softAssertions -> {
|
||||
softAssertions.assertThat(properties.getGrpcEndpoint()).isEqualTo("localhost");
|
||||
softAssertions.assertThat(properties.getHttpEndpoint()).isEqualTo("http://localhost");
|
||||
softAssertions.assertThat(properties.getHttpPort()).isEqualTo(3500);
|
||||
softAssertions.assertThat(properties.getGrpcPort()).isEqualTo(50001);
|
||||
softAssertions.assertThat(properties.getApiToken()).isEqualTo("ABC");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,5 +41,13 @@ public class DaprContainerConnectionDetailsFactory
|
|||
public Integer getGrpcPort() {
|
||||
return getContainer().getGrpcPort();
|
||||
}
|
||||
|
||||
/*
|
||||
* No API Token for local container
|
||||
*/
|
||||
@Override
|
||||
public String getApiToken() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,8 +37,7 @@ import java.util.concurrent.TimeoutException;
|
|||
*/
|
||||
public class DaprWorkflowClient implements AutoCloseable {
|
||||
|
||||
private static final ClientInterceptor WORKFLOW_INTERCEPTOR = new ApiTokenClientInterceptor();
|
||||
|
||||
private ClientInterceptor workflowApiTokenInterceptor;
|
||||
private DurableTaskClient innerClient;
|
||||
private ManagedChannel grpcChannel;
|
||||
|
||||
|
|
@ -55,7 +54,7 @@ public class DaprWorkflowClient implements AutoCloseable {
|
|||
* @param properties Properties for the GRPC Channel.
|
||||
*/
|
||||
public DaprWorkflowClient(Properties properties) {
|
||||
this(NetworkUtils.buildGrpcManagedChannel(properties, WORKFLOW_INTERCEPTOR));
|
||||
this(NetworkUtils.buildGrpcManagedChannel(properties, new ApiTokenClientInterceptor(properties)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ import io.grpc.Metadata;
|
|||
import io.grpc.MethodDescriptor;
|
||||
|
||||
public class ApiTokenClientInterceptor implements ClientInterceptor {
|
||||
|
||||
private Properties properties;
|
||||
|
||||
public ApiTokenClientInterceptor(Properties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
|
||||
MethodDescriptor<ReqT, RespT> methodDescriptor,
|
||||
|
|
@ -34,7 +41,7 @@ public class ApiTokenClientInterceptor implements ClientInterceptor {
|
|||
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(clientCall) {
|
||||
@Override
|
||||
public void start(final Listener<RespT> responseListener, final Metadata metadata) {
|
||||
String daprApiToken = Properties.API_TOKEN.get();
|
||||
String daprApiToken = properties.getValue(Properties.API_TOKEN);
|
||||
if (daprApiToken != null) {
|
||||
metadata.put(Metadata.Key.of(Headers.DAPR_API_TOKEN, Metadata.ASCII_STRING_MARSHALLER), daprApiToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.grpc.ClientInterceptor;
|
|||
import io.grpc.ManagedChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
|
@ -30,7 +31,7 @@ import java.util.concurrent.ExecutorService;
|
|||
import java.util.concurrent.Executors;
|
||||
|
||||
public class WorkflowRuntimeBuilder {
|
||||
private static final ClientInterceptor WORKFLOW_INTERCEPTOR = new ApiTokenClientInterceptor();
|
||||
private ClientInterceptor workflowApiTokenInterceptor;
|
||||
private static volatile WorkflowRuntime instance;
|
||||
private final Logger logger;
|
||||
private final Set<String> workflows = new HashSet<>();
|
||||
|
|
@ -62,7 +63,8 @@ public class WorkflowRuntimeBuilder {
|
|||
}
|
||||
|
||||
private WorkflowRuntimeBuilder(Properties properties, Logger logger) {
|
||||
this.managedChannel = NetworkUtils.buildGrpcManagedChannel(properties, WORKFLOW_INTERCEPTOR);
|
||||
this.workflowApiTokenInterceptor = new ApiTokenClientInterceptor(properties);
|
||||
this.managedChannel = NetworkUtils.buildGrpcManagedChannel(properties, workflowApiTokenInterceptor);
|
||||
this.builder = new DurableTaskGrpcWorkerBuilder().grpcChannel(this.managedChannel);
|
||||
this.logger = logger;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,10 +56,10 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
|
|||
private static final YamlConverter<Subscription> SUBSCRIPTION_CONVERTER = new SubscriptionYamlConverter(YAML_MAPPER);
|
||||
private static final YamlConverter<HttpEndpoint> HTTPENDPOINT_CONVERTER = new HttpEndpointYamlConverter(YAML_MAPPER);
|
||||
private static final YamlConverter<Configuration> CONFIGURATION_CONVERTER = new ConfigurationYamlConverter(
|
||||
YAML_MAPPER);
|
||||
YAML_MAPPER);
|
||||
private static final WaitStrategy WAIT_STRATEGY = Wait.forHttp("/v1.0/healthz/outbound")
|
||||
.forPort(DAPRD_DEFAULT_HTTP_PORT)
|
||||
.forStatusCodeMatching(statusCode -> statusCode >= 200 && statusCode <= 399);
|
||||
.forPort(DAPRD_DEFAULT_HTTP_PORT)
|
||||
.forStatusCodeMatching(statusCode -> statusCode >= 200 && statusCode <= 399);
|
||||
|
||||
private final Set<Component> components = new HashSet<>();
|
||||
private final Set<Subscription> subscriptions = new HashSet<>();
|
||||
|
|
@ -68,8 +68,8 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
|
|||
private String appChannelAddress = "localhost";
|
||||
private String placementService = "placement";
|
||||
private String schedulerService = "scheduler";
|
||||
private String placementDockerImageName = DAPR_PLACEMENT_IMAGE_TAG;
|
||||
private String schedulerDockerImageName = DAPR_SCHEDULER_IMAGE_TAG;
|
||||
private DockerImageName placementDockerImageName = DockerImageName.parse(DAPR_PLACEMENT_IMAGE_TAG);
|
||||
private DockerImageName schedulerDockerImageName = DockerImageName.parse(DAPR_SCHEDULER_IMAGE_TAG);
|
||||
|
||||
private Configuration configuration;
|
||||
private DaprPlacementContainer placementContainer;
|
||||
|
|
@ -82,6 +82,7 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
|
|||
|
||||
/**
|
||||
* Creates a new Dapr container.
|
||||
*
|
||||
* @param dockerImageName Docker image name.
|
||||
*/
|
||||
public DaprContainer(DockerImageName dockerImageName) {
|
||||
|
|
@ -94,6 +95,7 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
|
|||
|
||||
/**
|
||||
* Creates a new Dapr container.
|
||||
*
|
||||
* @param image Docker image name.
|
||||
*/
|
||||
public DaprContainer(String image) {
|
||||
|
|
@ -166,16 +168,26 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
|
|||
return this;
|
||||
}
|
||||
|
||||
public DaprContainer withPlacementImage(String placementDockerImageName) {
|
||||
public DaprContainer withPlacementImage(DockerImageName placementDockerImageName) {
|
||||
this.placementDockerImageName = placementDockerImageName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DaprContainer withSchedulerImage(String schedulerDockerImageName) {
|
||||
public DaprContainer withPlacementImage(String placementDockerImageName) {
|
||||
this.placementDockerImageName = DockerImageName.parse(placementDockerImageName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DaprContainer withSchedulerImage(DockerImageName schedulerDockerImageName) {
|
||||
this.schedulerDockerImageName = schedulerDockerImageName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DaprContainer withSchedulerImage(String schedulerDockerImageName) {
|
||||
this.schedulerDockerImageName = DockerImageName.parse(schedulerDockerImageName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DaprContainer withReusablePlacement(boolean shouldReusePlacement) {
|
||||
this.shouldReusePlacement = shouldReusePlacement;
|
||||
return this;
|
||||
|
|
@ -203,6 +215,7 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
|
|||
|
||||
/**
|
||||
* Adds a Dapr component from a YAML file.
|
||||
*
|
||||
* @param path Path to the YAML file.
|
||||
* @return This container.
|
||||
*/
|
||||
|
|
@ -217,7 +230,7 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
|
|||
Map<String, Object> spec = (Map<String, Object>) component.get("spec");
|
||||
String version = (String) spec.get("version");
|
||||
List<Map<String, String>> specMetadata =
|
||||
(List<Map<String, String>>) spec.getOrDefault("metadata", Collections.emptyMap());
|
||||
(List<Map<String, String>>) spec.getOrDefault("metadata", Collections.emptyList());
|
||||
|
||||
ArrayList<MetadataEntry> metadataEntries = new ArrayList<>();
|
||||
|
||||
|
|
@ -260,17 +273,17 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
|
|||
|
||||
if (this.placementContainer == null) {
|
||||
this.placementContainer = new DaprPlacementContainer(this.placementDockerImageName)
|
||||
.withNetwork(getNetwork())
|
||||
.withNetworkAliases(placementService)
|
||||
.withReuse(this.shouldReusePlacement);
|
||||
.withNetwork(getNetwork())
|
||||
.withNetworkAliases(placementService)
|
||||
.withReuse(this.shouldReusePlacement);
|
||||
this.placementContainer.start();
|
||||
}
|
||||
|
||||
if (this.schedulerContainer == null) {
|
||||
this.schedulerContainer = new DaprSchedulerContainer(this.schedulerDockerImageName)
|
||||
.withNetwork(getNetwork())
|
||||
.withNetworkAliases(schedulerService)
|
||||
.withReuse(this.shouldReuseScheduler);
|
||||
.withNetwork(getNetwork())
|
||||
.withNetworkAliases(schedulerService)
|
||||
.withReuse(this.shouldReuseScheduler);
|
||||
this.schedulerContainer.start();
|
||||
}
|
||||
|
||||
|
|
@ -386,6 +399,14 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
|
|||
return DEFAULT_IMAGE_NAME;
|
||||
}
|
||||
|
||||
public DockerImageName getPlacementDockerImageName() {
|
||||
return placementDockerImageName;
|
||||
}
|
||||
|
||||
public DockerImageName getSchedulerDockerImageName() {
|
||||
return schedulerDockerImageName;
|
||||
}
|
||||
|
||||
// Required by spotbugs plugin
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package io.dapr.testcontainers;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class DaprContainerTest {
|
||||
|
||||
@Test
|
||||
public void schedulerAndPlacementCustomImagesTest() {
|
||||
|
||||
DaprContainer dapr = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
|
||||
.withAppName("dapr-app")
|
||||
.withSchedulerImage(DockerImageName.parse("custom/scheduler:1.15.4")
|
||||
.asCompatibleSubstituteFor("daprio/scheduler:1.15.4"))
|
||||
.withPlacementImage(DockerImageName.parse("custom/placement:1.15.4")
|
||||
.asCompatibleSubstituteFor("daprio/placement:1.15.4"))
|
||||
.withAppPort(8081)
|
||||
.withDaprLogLevel(DaprLogLevel.DEBUG)
|
||||
.withAppChannelAddress("host.testcontainers.internal");
|
||||
|
||||
|
||||
assertEquals("custom/placement:1.15.4", dapr.getPlacementDockerImageName().asCanonicalNameString());
|
||||
assertEquals("custom/scheduler:1.15.4", dapr.getSchedulerDockerImageName().asCanonicalNameString());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void schedulerAndPlacementCustomImagesStringTest() {
|
||||
|
||||
DaprContainer dapr = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
|
||||
.withAppName("dapr-app")
|
||||
.withSchedulerImage("daprio/scheduler:1.15.4")
|
||||
.withPlacementImage("daprio/placement:1.15.4")
|
||||
.withAppPort(8081)
|
||||
.withDaprLogLevel(DaprLogLevel.DEBUG)
|
||||
.withAppChannelAddress("host.testcontainers.internal");
|
||||
|
||||
|
||||
assertEquals("daprio/placement:1.15.4", dapr.getPlacementDockerImageName().asCanonicalNameString());
|
||||
assertEquals("daprio/scheduler:1.15.4", dapr.getSchedulerDockerImageName().asCanonicalNameString());
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue