From b11373af7b017f83c3b579c6aefc7cb7a1816bfd Mon Sep 17 00:00:00 2001 From: Emilio Setiadarma Date: Tue, 16 May 2023 16:50:02 -0700 Subject: [PATCH] NIFI-11549 Added AzureQueueStorage_v12 Processors - Deprecated GetAzureQueueStorage and PutAzureQueueStorage This closes #7269 Signed-off-by: David Handermann --- .../nifi-azure-processors/pom.xml | 4 + .../queue/AbstractAzureQueueStorage_v12.java | 190 ++++++++++++++++ .../storage/queue/GetAzureQueueStorage.java | 2 + .../queue/GetAzureQueueStorage_v12.java | 210 ++++++++++++++++++ .../storage/queue/PutAzureQueueStorage.java | 2 + .../queue/PutAzureQueueStorage_v12.java | 162 ++++++++++++++ ...orageCredentialsControllerService_v12.java | 6 +- .../org.apache.nifi.processor.Processor | 2 + .../AbstractTestAzureQueueStorage_v12.java | 36 +++ .../queue/TestGetAzureQueueStorage_v12.java | 79 +++++++ .../queue/TestPutAzureQueueStorage_v12.java | 100 +++++++++ 11 files changed, 790 insertions(+), 3 deletions(-) create mode 100644 nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/AbstractAzureQueueStorage_v12.java create mode 100644 nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/GetAzureQueueStorage_v12.java create mode 100644 nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/PutAzureQueueStorage_v12.java create mode 100644 nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/AbstractTestAzureQueueStorage_v12.java create mode 100644 nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/TestGetAzureQueueStorage_v12.java create mode 100644 nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/TestPutAzureQueueStorage_v12.java diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml index 87888cac8c..c42c0c0e23 100644 --- a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml @@ -92,6 +92,10 @@ com.azure azure-security-keyvault-keys + + com.azure + azure-storage-queue + com.microsoft.azure diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/AbstractAzureQueueStorage_v12.java b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/AbstractAzureQueueStorage_v12.java new file mode 100644 index 0000000000..ce1ad8f537 --- /dev/null +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/AbstractAzureQueueStorage_v12.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.processors.azure.storage.queue; + +import com.azure.core.credential.AzureSasCredential; +import com.azure.core.credential.TokenCredential; +import com.azure.core.http.HttpClient; +import com.azure.core.http.ProxyOptions; +import com.azure.core.http.netty.NettyAsyncHttpClientBuilder; +import com.azure.identity.ClientSecretCredentialBuilder; +import com.azure.identity.ManagedIdentityCredentialBuilder; +import com.azure.storage.common.StorageSharedKeyCredential; +import com.azure.storage.queue.QueueClient; +import com.azure.storage.queue.QueueClientBuilder; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.context.PropertyContext; +import org.apache.nifi.expression.ExpressionLanguageScope; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.processor.AbstractProcessor; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.processors.azure.AzureServiceEndpoints; +import org.apache.nifi.processors.azure.storage.utils.AzureStorageUtils; +import org.apache.nifi.services.azure.storage.AzureStorageCredentialsDetails_v12; +import org.apache.nifi.services.azure.storage.AzureStorageCredentialsService_v12; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public abstract class AbstractAzureQueueStorage_v12 extends AbstractProcessor { + public static final PropertyDescriptor QUEUE_NAME = new PropertyDescriptor.Builder() + .name("Queue Name") + .displayName("Queue Name") + .description("Name of the Azure Storage Queue") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_EL_VALIDATOR) + .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES) + .build(); + + public static final PropertyDescriptor ENDPOINT_SUFFIX = new PropertyDescriptor.Builder() + .fromPropertyDescriptor(AzureStorageUtils.ENDPOINT_SUFFIX) + .displayName("Endpoint Suffix") + .description("Storage accounts in public Azure always use a common FQDN suffix. " + + "Override this endpoint suffix with a different suffix in certain circumstances (like Azure Stack or non-public Azure regions).") + .required(true) + .defaultValue(AzureServiceEndpoints.DEFAULT_QUEUE_ENDPOINT_SUFFIX) + .expressionLanguageSupported(ExpressionLanguageScope.NONE) + .build(); + + public static final PropertyDescriptor STORAGE_CREDENTIALS_SERVICE = new PropertyDescriptor.Builder() + .name("Credentials Service") + .displayName("Credentials Service") + .description("Controller Service used to obtain Azure Storage Credentials.") + .identifiesControllerService(AzureStorageCredentialsService_v12.class) + .required(true) + .build(); + + public static final PropertyDescriptor REQUEST_TIMEOUT = new PropertyDescriptor.Builder() + .name("Request Timeout") + .displayName("Request Timeout") + .description("The timeout for read or write requests to Azure Queue Storage. " + + "Defaults to 1 second.") + .required(true) + .defaultValue("10 secs") + .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) + .build(); + + public static final Relationship REL_SUCCESS = new Relationship.Builder() + .name("success") + .description("All successfully processed FlowFiles are routed to this relationship") + .build(); + + public static final Relationship REL_FAILURE = new Relationship.Builder() + .name("failure") + .description("Unsuccessful operations will be transferred to the failure relationship.") + .build(); + + private static final Set RELATIONSHIPS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(REL_SUCCESS, REL_FAILURE))); + + static final String URI_ATTRIBUTE = "azure.queue.uri"; + static final String INSERTION_TIME_ATTRIBUTE = "azure.queue.insertionTime"; + static final String EXPIRATION_TIME_ATTRIBUTE = "azure.queue.expirationTime"; + static final String MESSAGE_ID_ATTRIBUTE = "azure.queue.messageId"; + static final String POP_RECEIPT_ATTRIBUTE = "azure.queue.popReceipt"; + + @Override + public Set getRelationships() { + return RELATIONSHIPS; + } + + @Override + protected Collection customValidate(ValidationContext validationContext) { + final List results = new ArrayList<>(); + final int requestTimeout = validationContext.getProperty(REQUEST_TIMEOUT).asTimePeriod(TimeUnit.SECONDS).intValue(); + + if (requestTimeout <= 0 || requestTimeout > 30) { + results.add(new ValidationResult.Builder() + .valid(false) + .subject(REQUEST_TIMEOUT.getDisplayName()) + .explanation(REQUEST_TIMEOUT.getDisplayName() + " should be greater than 0 secs " + + "and less than or equal to 30 secs") + .build()); + } + + AzureStorageUtils.validateProxySpec(validationContext, results); + + return results; + } + + protected final QueueClient createQueueClient(final ProcessContext context, final FlowFile flowFile) { + final QueueClientBuilder clientBuilder = new QueueClientBuilder(); + + final AzureStorageCredentialsService_v12 storageCredentialsService = context.getProperty(STORAGE_CREDENTIALS_SERVICE).asControllerService(AzureStorageCredentialsService_v12.class); + final Map attributes = flowFile == null ? Collections.emptyMap() : flowFile.getAttributes(); + final AzureStorageCredentialsDetails_v12 storageCredentialsDetails = storageCredentialsService.getCredentialsDetails(attributes); + processCredentials(clientBuilder, storageCredentialsDetails); + processProxyOptions(clientBuilder, context); + + final String endpointSuffix = context.getProperty(ENDPOINT_SUFFIX).getValue(); + clientBuilder.endpoint(String.format("https://%s.%s", storageCredentialsDetails.getAccountName(), endpointSuffix)); + + final String queueName = context.getProperty(QUEUE_NAME).evaluateAttributeExpressions(flowFile).getValue(); + clientBuilder.queueName(queueName); + return clientBuilder.buildClient(); + } + + private void processCredentials(final QueueClientBuilder clientBuilder, final AzureStorageCredentialsDetails_v12 storageCredentialsDetails) { + switch (storageCredentialsDetails.getCredentialsType()) { + case ACCOUNT_KEY: + clientBuilder.credential(new StorageSharedKeyCredential(storageCredentialsDetails.getAccountName(), storageCredentialsDetails.getAccountKey())); + break; + case SAS_TOKEN: + clientBuilder.credential(new AzureSasCredential(storageCredentialsDetails.getSasToken())); + break; + case MANAGED_IDENTITY: + clientBuilder.credential(new ManagedIdentityCredentialBuilder() + .clientId(storageCredentialsDetails.getManagedIdentityClientId()) + .build()); + break; + case SERVICE_PRINCIPAL: + clientBuilder.credential(new ClientSecretCredentialBuilder() + .tenantId(storageCredentialsDetails.getServicePrincipalTenantId()) + .clientId(storageCredentialsDetails.getServicePrincipalClientId()) + .clientSecret(storageCredentialsDetails.getServicePrincipalClientSecret()) + .build()); + break; + case ACCESS_TOKEN: + TokenCredential credential = tokenRequestContext -> Mono.just(storageCredentialsDetails.getAccessToken()); + clientBuilder.credential(credential); + break; + default: + throw new IllegalArgumentException("Unhandled credentials type: " + storageCredentialsDetails.getCredentialsType()); + } + } + + private void processProxyOptions(final QueueClientBuilder clientBuilder, + final PropertyContext propertyContext) { + final ProxyOptions proxyOptions = AzureStorageUtils.getProxyOptions(propertyContext); + final NettyAsyncHttpClientBuilder nettyClientBuilder = new NettyAsyncHttpClientBuilder(); + nettyClientBuilder.proxy(proxyOptions); + + final HttpClient nettyClient = nettyClientBuilder.build(); + clientBuilder.httpClient(nettyClient); + } +} diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/GetAzureQueueStorage.java b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/GetAzureQueueStorage.java index dc5a138574..6e29905b5d 100644 --- a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/GetAzureQueueStorage.java +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/GetAzureQueueStorage.java @@ -26,6 +26,7 @@ import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; import org.apache.nifi.annotation.behavior.WritesAttribute; import org.apache.nifi.annotation.behavior.WritesAttributes; import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.DeprecationNotice; import org.apache.nifi.annotation.documentation.SeeAlso; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.components.PropertyDescriptor; @@ -63,6 +64,7 @@ import java.util.concurrent.TimeUnit; @WritesAttribute(attribute = "azure.queue.messageId", description = "The ID of the retrieved message"), @WritesAttribute(attribute = "azure.queue.popReceipt", description = "The pop receipt of the retrieved message"), }) +@DeprecationNotice(alternatives = GetAzureQueueStorage_v12.class) public class GetAzureQueueStorage extends AbstractAzureQueueStorage { public static final PropertyDescriptor AUTO_DELETE = new PropertyDescriptor.Builder() diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/GetAzureQueueStorage_v12.java b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/GetAzureQueueStorage_v12.java new file mode 100644 index 0000000000..28ba1e1b4e --- /dev/null +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/GetAzureQueueStorage_v12.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.processors.azure.storage.queue; + +import com.azure.core.util.Context; +import com.azure.storage.queue.QueueClient; +import com.azure.storage.queue.models.QueueMessageItem; +import com.azure.storage.queue.models.QueueStorageException; +import org.apache.nifi.annotation.behavior.InputRequirement; +import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; +import org.apache.nifi.annotation.behavior.WritesAttribute; +import org.apache.nifi.annotation.behavior.WritesAttributes; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.SeeAlso; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.proxy.ProxyConfiguration; +import org.apache.nifi.proxy.ProxySpec; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static org.apache.nifi.processors.azure.storage.queue.AbstractAzureQueueStorage_v12.EXPIRATION_TIME_ATTRIBUTE; +import static org.apache.nifi.processors.azure.storage.queue.AbstractAzureQueueStorage_v12.INSERTION_TIME_ATTRIBUTE; +import static org.apache.nifi.processors.azure.storage.queue.AbstractAzureQueueStorage_v12.MESSAGE_ID_ATTRIBUTE; +import static org.apache.nifi.processors.azure.storage.queue.AbstractAzureQueueStorage_v12.POP_RECEIPT_ATTRIBUTE; +import static org.apache.nifi.processors.azure.storage.queue.AbstractAzureQueueStorage_v12.URI_ATTRIBUTE; + +@SeeAlso({PutAzureQueueStorage.class}) +@InputRequirement(Requirement.INPUT_FORBIDDEN) +@Tags({"azure", "queue", "microsoft", "storage", "dequeue", "cloud"}) +@CapabilityDescription("Retrieves the messages from an Azure Queue Storage. The retrieved messages will be deleted from the queue by default. If the requirement is " + + "to consume messages without deleting them, set 'Auto Delete Messages' to 'false'. Note: There might be chances of receiving duplicates in situations like " + + "when a message is received but was unable to be deleted from the queue due to some unexpected situations.") +@WritesAttributes({ + @WritesAttribute(attribute = URI_ATTRIBUTE, description = "The absolute URI of the configured Azure Queue Storage"), + @WritesAttribute(attribute = INSERTION_TIME_ATTRIBUTE, description = "The time when the message was inserted into the queue storage"), + @WritesAttribute(attribute = EXPIRATION_TIME_ATTRIBUTE, description = "The time when the message will expire from the queue storage"), + @WritesAttribute(attribute = MESSAGE_ID_ATTRIBUTE, description = "The ID of the retrieved message"), + @WritesAttribute(attribute = POP_RECEIPT_ATTRIBUTE, description = "The pop receipt of the retrieved message"), +}) +public class GetAzureQueueStorage_v12 extends AbstractAzureQueueStorage_v12 { + public static final PropertyDescriptor AUTO_DELETE = new PropertyDescriptor.Builder() + .name("Auto Delete Messages") + .displayName("Auto Delete Messages") + .description("Specifies whether the received message is to be automatically deleted from the queue.") + .required(true) + .allowableValues("true", "false") + .defaultValue("true") + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .build(); + + public static final PropertyDescriptor MESSAGE_BATCH_SIZE = new PropertyDescriptor.Builder() + .name("Message Batch Size") + .displayName("Message Batch Size") + .description("The number of messages to be retrieved from the queue.") + .required(true) + .addValidator(StandardValidators.createLongValidator(1, 32, true)) + .defaultValue("32") + .build(); + + public static final PropertyDescriptor VISIBILITY_TIMEOUT = new PropertyDescriptor.Builder() + .name("Visibility Timeout") + .displayName("Visibility Timeout") + .description("The duration during which the retrieved message should be invisible to other consumers.") + .required(true) + .defaultValue("30 secs") + .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) + .build(); + + private static final ProxySpec[] PROXY_SPECS = {ProxySpec.HTTP, ProxySpec.SOCKS}; + private static final List PROPERTIES = Collections.unmodifiableList( + Arrays.asList( + QUEUE_NAME, + ENDPOINT_SUFFIX, + STORAGE_CREDENTIALS_SERVICE, + AUTO_DELETE, + MESSAGE_BATCH_SIZE, + VISIBILITY_TIMEOUT, + REQUEST_TIMEOUT, + ProxyConfiguration.createProxyConfigPropertyDescriptor(false, PROXY_SPECS) + ) + ); + + private static final Set RELATIONSHIPS = Collections.singleton(REL_SUCCESS); + + // 7 days is the maximum timeout as per https://learn.microsoft.com/en-us/rest/api/storageservices/get-messages + private static final Duration MAX_VISIBILITY_TIMEOUT = Duration.ofDays(7); + + @Override + public List getSupportedPropertyDescriptors() { + return PROPERTIES; + } + + @Override + protected Collection customValidate(final ValidationContext validationContext) { + final List results = (List) super.customValidate(validationContext); + + final Duration visibilityTimeout = Duration.ofSeconds( + validationContext.getProperty(VISIBILITY_TIMEOUT).asTimePeriod(TimeUnit.SECONDS) + ); + + if (visibilityTimeout.getSeconds() <= 0) { + results.add(new ValidationResult.Builder() + .valid(false) + .subject(VISIBILITY_TIMEOUT.getDisplayName()) + .explanation(VISIBILITY_TIMEOUT.getDisplayName() + " should be greater than 0 secs") + .build()); + } + + if (MAX_VISIBILITY_TIMEOUT.compareTo(visibilityTimeout) < 0) { + results.add(new ValidationResult.Builder() + .valid(false) + .subject(VISIBILITY_TIMEOUT.getDisplayName()) + .explanation(VISIBILITY_TIMEOUT.getDisplayName() + " should not be greater than 7 days") + .build()); + } + + return results; + } + + @Override + public Set getRelationships() { + return RELATIONSHIPS; + } + + @Override + public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { + final int batchSize = context.getProperty(MESSAGE_BATCH_SIZE).asInteger(); + final int visibilityTimeoutInSecs = context.getProperty(VISIBILITY_TIMEOUT).asTimePeriod(TimeUnit.SECONDS).intValue(); + final int requestTimeoutInSecs = context.getProperty(REQUEST_TIMEOUT).asTimePeriod(TimeUnit.SECONDS).intValue(); + final boolean autoDelete = context.getProperty(AUTO_DELETE).asBoolean(); + + final QueueClient queueClient = createQueueClient(context, null); + final Iterable retrievedMessagesIterable; + try { + retrievedMessagesIterable = queueClient.receiveMessages( + batchSize, + Duration.ofSeconds(visibilityTimeoutInSecs), + Duration.ofSeconds(requestTimeoutInSecs), + Context.NONE); + } catch (final QueueStorageException e) { + getLogger().error("Failed to retrieve messages from Azure Storage Queue", e); + context.yield(); + return; + } + + final Map messagesToDelete = new LinkedHashMap<>(); + + for (final QueueMessageItem message : retrievedMessagesIterable) { + FlowFile flowFile = session.create(); + + final Map attributes = new LinkedHashMap<>(); + attributes.put("azure.queue.uri", queueClient.getQueueUrl()); + attributes.put("azure.queue.insertionTime", message.getInsertionTime().toString()); + attributes.put("azure.queue.expirationTime", message.getExpirationTime().toString()); + attributes.put("azure.queue.messageId", message.getMessageId()); + attributes.put("azure.queue.popReceipt", message.getPopReceipt()); + + if (autoDelete) { + messagesToDelete.put(message.getMessageId(), message.getPopReceipt()); + } + + flowFile = session.putAllAttributes(flowFile, attributes); + flowFile = session.write(flowFile, out -> out.write(message.getBody().toString().getBytes())); + + session.transfer(flowFile, REL_SUCCESS); + session.getProvenanceReporter().receive(flowFile, queueClient.getQueueUrl().toString()); + } + + if (autoDelete) { + session.commitAsync(() -> { + for (final Map.Entry entry : messagesToDelete.entrySet()) { + final String messageId = entry.getKey(); + final String popReceipt = entry.getValue(); + queueClient.deleteMessage(messageId, popReceipt); + } + }); + } + } +} diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/PutAzureQueueStorage.java b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/PutAzureQueueStorage.java index d1e0c35f8c..10da98016f 100644 --- a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/PutAzureQueueStorage.java +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/PutAzureQueueStorage.java @@ -33,6 +33,7 @@ import com.microsoft.azure.storage.queue.CloudQueueMessage; import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.DeprecationNotice; import org.apache.nifi.annotation.documentation.SeeAlso; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.components.PropertyDescriptor; @@ -49,6 +50,7 @@ import org.apache.nifi.processors.azure.storage.utils.AzureStorageUtils; @InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED) @Tags({ "azure", "microsoft", "cloud", "storage", "queue", "enqueue" }) @CapabilityDescription("Writes the content of the incoming FlowFiles to the configured Azure Queue Storage.") +@DeprecationNotice(alternatives = PutAzureQueueStorage_v12.class) public class PutAzureQueueStorage extends AbstractAzureQueueStorage { public static final PropertyDescriptor TTL = new PropertyDescriptor.Builder() diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/PutAzureQueueStorage_v12.java b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/PutAzureQueueStorage_v12.java new file mode 100644 index 0000000000..e6389504c5 --- /dev/null +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/storage/queue/PutAzureQueueStorage_v12.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.processors.azure.storage.queue; + +import com.azure.core.util.Context; +import com.azure.storage.queue.QueueClient; +import com.azure.storage.queue.models.QueueStorageException; +import org.apache.nifi.annotation.behavior.InputRequirement; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.SeeAlso; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.proxy.ProxyConfiguration; +import org.apache.nifi.proxy.ProxySpec; + +import java.io.ByteArrayOutputStream; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@SeeAlso({GetAzureQueueStorage_v12.class}) +@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED) +@Tags({ "azure", "microsoft", "cloud", "storage", "queue", "enqueue" }) +@CapabilityDescription("Writes the content of the incoming FlowFiles to the configured Azure Queue Storage.") +public class PutAzureQueueStorage_v12 extends AbstractAzureQueueStorage_v12 { + public static final PropertyDescriptor MESSAGE_TIME_TO_LIVE = new PropertyDescriptor.Builder() + .name("Message Time To Live") + .displayName("Message Time To Live") + .description("Maximum time to allow the message to be in the queue") + .required(true) + .defaultValue("7 days") + .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) + .build(); + + public static final PropertyDescriptor VISIBILITY_TIMEOUT = new PropertyDescriptor.Builder() + .name("Visibility Timeout") + .displayName("Visibility Timeout") + .description("The length of time during which the message will be invisible after it is read. " + + "If the processing unit fails to delete the message after it is read, then the message will reappear in the queue.") + .required(true) + .defaultValue("30 secs") + .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) + .build(); + + private static final ProxySpec[] PROXY_SPECS = {ProxySpec.HTTP, ProxySpec.SOCKS}; + private static final List PROPERTIES = Collections.unmodifiableList( + Arrays.asList( + QUEUE_NAME, + ENDPOINT_SUFFIX, + STORAGE_CREDENTIALS_SERVICE, + MESSAGE_TIME_TO_LIVE, + VISIBILITY_TIMEOUT, + REQUEST_TIMEOUT, + ProxyConfiguration.createProxyConfigPropertyDescriptor(false, PROXY_SPECS) + ) + ); + + // 7 days is the maximum timeout as per https://learn.microsoft.com/en-us/rest/api/storageservices/get-messages + private static final Duration MAX_VISIBILITY_TIMEOUT = Duration.ofDays(7); + + @Override + public List getSupportedPropertyDescriptors() { + return PROPERTIES; + } + + @Override + protected Collection customValidate(final ValidationContext validationContext) { + final List results = (List) super.customValidate(validationContext); + final Duration visibilityTimeout = Duration.ofSeconds( + validationContext.getProperty(VISIBILITY_TIMEOUT).asTimePeriod(TimeUnit.SECONDS) + ); + + if (visibilityTimeout.getSeconds() <= 0) { + results.add(new ValidationResult.Builder() + .valid(false) + .subject(VISIBILITY_TIMEOUT.getDisplayName()) + .explanation(VISIBILITY_TIMEOUT.getDisplayName() + " should be greater than 0 secs") + .build()); + } + + if (MAX_VISIBILITY_TIMEOUT.compareTo(visibilityTimeout) < 0) { + results.add(new ValidationResult.Builder() + .valid(false) + .subject(VISIBILITY_TIMEOUT.getDisplayName()) + .explanation(VISIBILITY_TIMEOUT.getDisplayName() + " should not be greater than 7 days") + .build()); + } + + final int ttl = validationContext.getProperty(MESSAGE_TIME_TO_LIVE).asTimePeriod(TimeUnit.SECONDS).intValue(); + if (ttl <= 0) { + results.add(new ValidationResult.Builder() + .subject(MESSAGE_TIME_TO_LIVE.getDisplayName()) + .valid(false) + .explanation(MESSAGE_TIME_TO_LIVE.getDisplayName() + " should be any positive number") + .build()); + } + + return results; + } + + @Override + public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { + FlowFile flowFile = session.get(); + if (flowFile == null) { + return; + } + + final long startNanos = System.nanoTime(); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + session.exportTo(flowFile, baos); + final String flowFileContent = baos.toString(); + + final int visibilityTimeoutInSecs = context.getProperty(VISIBILITY_TIMEOUT).asTimePeriod(TimeUnit.SECONDS).intValue(); + final int ttl = context.getProperty(MESSAGE_TIME_TO_LIVE).asTimePeriod(TimeUnit.SECONDS).intValue(); + final int requestTimeoutInSecs = context.getProperty(REQUEST_TIMEOUT).asTimePeriod(TimeUnit.SECONDS).intValue(); + + final QueueClient queueClient = createQueueClient(context, flowFile); + try { + queueClient.sendMessageWithResponse( + flowFileContent, + Duration.ofSeconds(visibilityTimeoutInSecs), + Duration.ofSeconds(ttl), + Duration.ofSeconds(requestTimeoutInSecs), + Context.NONE + ); + } catch (final QueueStorageException e) { + getLogger().error("Failed to write message to Azure Queue Storage", e); + flowFile = session.penalize(flowFile); + session.transfer(flowFile, REL_FAILURE); + return; + } + + session.transfer(flowFile, REL_SUCCESS); + final long transmissionMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); + session.getProvenanceReporter().send(flowFile, queueClient.getQueueUrl().toString(), transmissionMillis); + } +} diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/services/azure/storage/AzureStorageCredentialsControllerService_v12.java b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/services/azure/storage/AzureStorageCredentialsControllerService_v12.java index 696f86d27f..ff63b1538f 100644 --- a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/services/azure/storage/AzureStorageCredentialsControllerService_v12.java +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/services/azure/storage/AzureStorageCredentialsControllerService_v12.java @@ -32,12 +32,12 @@ import java.util.List; import java.util.Map; /** - * Provides credentials details for Azure Blob processors + * Provides credentials details for Azure Storage processors * * @see AbstractControllerService */ -@Tags({"azure", "microsoft", "cloud", "storage", "blob", "credentials"}) -@CapabilityDescription("Provides credentials for Azure Blob processors using Azure Blob Storage client library v12.") +@Tags({"azure", "microsoft", "cloud", "storage", "blob", "credentials", "queue"}) +@CapabilityDescription("Provides credentials for Azure Storage processors using Azure Storage client library v12.") public class AzureStorageCredentialsControllerService_v12 extends AbstractControllerService implements AzureStorageCredentialsService_v12 { public static final PropertyDescriptor ACCOUNT_NAME = new PropertyDescriptor.Builder() diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor index f9e86a4add..62c618c7c0 100644 --- a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor @@ -27,3 +27,5 @@ org.apache.nifi.processors.azure.storage.FetchAzureBlobStorage_v12 org.apache.nifi.processors.azure.storage.PutAzureBlobStorage_v12 org.apache.nifi.processors.azure.storage.DeleteAzureBlobStorage_v12 org.apache.nifi.processors.azure.storage.MoveAzureDataLakeStorage +org.apache.nifi.processors.azure.storage.queue.GetAzureQueueStorage_v12 +org.apache.nifi.processors.azure.storage.queue.PutAzureQueueStorage_v12 diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/AbstractTestAzureQueueStorage_v12.java b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/AbstractTestAzureQueueStorage_v12.java new file mode 100644 index 0000000000..6d5d663d9c --- /dev/null +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/AbstractTestAzureQueueStorage_v12.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.processors.azure.storage.queue; + +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.services.azure.storage.AzureStorageCredentialsControllerService_v12; +import org.apache.nifi.services.azure.storage.AzureStorageCredentialsService_v12; +import org.apache.nifi.services.azure.storage.AzureStorageCredentialsType; +import org.apache.nifi.util.TestRunner; + +public abstract class AbstractTestAzureQueueStorage_v12 { + public static final String CREDENTIALS_SERVICE_IDENTIFIER = "credentials-service"; + protected TestRunner runner; + protected AzureStorageCredentialsService_v12 credentialsService = new AzureStorageCredentialsControllerService_v12(); + + protected void setupStorageCredentialsService() throws InitializationException { + runner.addControllerService(CREDENTIALS_SERVICE_IDENTIFIER, credentialsService); + runner.setProperty(credentialsService, AzureStorageCredentialsControllerService_v12.ACCOUNT_NAME, "account-name"); + runner.setProperty(credentialsService, AzureStorageCredentialsControllerService_v12.CREDENTIALS_TYPE, AzureStorageCredentialsType.ACCOUNT_KEY.getAllowableValue()); + runner.setProperty(credentialsService, AzureStorageCredentialsControllerService_v12.ACCOUNT_KEY, "account-key"); + } +} diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/TestGetAzureQueueStorage_v12.java b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/TestGetAzureQueueStorage_v12.java new file mode 100644 index 0000000000..3f91929426 --- /dev/null +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/TestGetAzureQueueStorage_v12.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.processors.azure.storage.queue; + +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.util.TestRunners; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class TestGetAzureQueueStorage_v12 extends AbstractTestAzureQueueStorage_v12 { + @BeforeEach + public void setup() throws InitializationException { + runner = TestRunners.newTestRunner(GetAzureQueueStorage_v12.class); + setupStorageCredentialsService(); + runner.enableControllerService(credentialsService); + runner.setProperty(GetAzureQueueStorage_v12.STORAGE_CREDENTIALS_SERVICE, CREDENTIALS_SERVICE_IDENTIFIER); + runner.setProperty(GetAzureQueueStorage_v12.QUEUE_NAME, "queue"); + runner.setProperty(GetAzureQueueStorage_v12.MESSAGE_BATCH_SIZE, "10"); + } + + @Test + public void testValidVisibilityTimeout() { + runner.setProperty(GetAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "10 secs"); + + runner.assertValid(); + } + + @Test + public void testInvalidVisibilityTimeoutZeroSecs() { + runner.setProperty(GetAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "0 secs"); + + runner.assertNotValid(); + } + + @Test + public void testInvalidVisibilityTimeoutMoreThanSevenDays() { + runner.setProperty(GetAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "8 days"); + + runner.assertNotValid(); + } + + @Test + public void testValidRequestTimeout() { + runner.setProperty(GetAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "10 secs"); + runner.setProperty(GetAzureQueueStorage_v12.REQUEST_TIMEOUT, "15 secs"); + + runner.assertValid(); + } + + @Test + public void testInvalidRequestTimeoutZeroSecs() { + runner.setProperty(GetAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "10 secs"); + runner.setProperty(GetAzureQueueStorage_v12.REQUEST_TIMEOUT, "0 secs"); + + runner.assertNotValid(); + } + + @Test + public void testInvalidRequestTimeoutMoreThanThirtySecs() { + runner.setProperty(GetAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "10 secs"); + runner.setProperty(GetAzureQueueStorage_v12.REQUEST_TIMEOUT, "31 secs"); + + runner.assertNotValid(); + } +} diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/TestPutAzureQueueStorage_v12.java b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/TestPutAzureQueueStorage_v12.java new file mode 100644 index 0000000000..ce0afbeddf --- /dev/null +++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/storage/queue/TestPutAzureQueueStorage_v12.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.processors.azure.storage.queue; + +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.util.TestRunners; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class TestPutAzureQueueStorage_v12 extends AbstractTestAzureQueueStorage_v12 { + @BeforeEach + public void setup() throws InitializationException { + runner = TestRunners.newTestRunner(PutAzureQueueStorage_v12.class); + setupStorageCredentialsService(); + runner.enableControllerService(credentialsService); + runner.setProperty(PutAzureQueueStorage_v12.STORAGE_CREDENTIALS_SERVICE, CREDENTIALS_SERVICE_IDENTIFIER); + runner.setProperty(PutAzureQueueStorage_v12.QUEUE_NAME, "queue"); + } + + @Test + public void testValidVisibilityTimeout() { + runner.setProperty(PutAzureQueueStorage_v12.MESSAGE_TIME_TO_LIVE, "7 days"); + runner.setProperty(PutAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "10 secs"); + + runner.assertValid(); + } + + @Test + public void testInvalidVisibilityTimeoutZeroSecs() { + runner.setProperty(PutAzureQueueStorage_v12.MESSAGE_TIME_TO_LIVE, "7 days"); + runner.setProperty(PutAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "0 secs"); + + runner.assertNotValid(); + } + + @Test + public void testInvalidVisibilityTimeoutMoreThanSevenDays() { + runner.setProperty(PutAzureQueueStorage_v12.MESSAGE_TIME_TO_LIVE, "7 days"); + runner.setProperty(PutAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "8 days"); + + runner.assertNotValid(); + } + + @Test + public void testValidTTL() { + runner.setProperty(PutAzureQueueStorage_v12.MESSAGE_TIME_TO_LIVE, "7 days"); + runner.setProperty(PutAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "30 secs"); + + runner.assertValid(); + } + + @Test + public void testInvalidTTLZeroSecs() { + runner.setProperty(PutAzureQueueStorage_v12.MESSAGE_TIME_TO_LIVE, "0 secs"); + runner.setProperty(PutAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "30 secs"); + + runner.assertNotValid(); + } + + @Test + public void testValidRequestTimeout() { + runner.setProperty(PutAzureQueueStorage_v12.MESSAGE_TIME_TO_LIVE, "7 days"); + runner.setProperty(PutAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "30 secs"); + runner.setProperty(PutAzureQueueStorage_v12.REQUEST_TIMEOUT, "15 secs"); + + runner.assertValid(); + } + + @Test + public void testInvalidRequestTimeoutZeroSecs() { + runner.setProperty(PutAzureQueueStorage_v12.MESSAGE_TIME_TO_LIVE, "7 days"); + runner.setProperty(PutAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "30 secs"); + runner.setProperty(PutAzureQueueStorage_v12.REQUEST_TIMEOUT, "0 secs"); + + runner.assertNotValid(); + } + + @Test + public void testInvalidRequestTimeoutMoreThanThirtySecs() { + runner.setProperty(PutAzureQueueStorage_v12.MESSAGE_TIME_TO_LIVE, "7 days"); + runner.setProperty(PutAzureQueueStorage_v12.VISIBILITY_TIMEOUT, "30 secs"); + runner.setProperty(PutAzureQueueStorage_v12.REQUEST_TIMEOUT, "31 secs"); + + runner.assertNotValid(); + } +}