diff --git a/build.gradle b/build.gradle index 85151cce..8f28161c 100644 --- a/build.gradle +++ b/build.gradle @@ -62,9 +62,9 @@ configurations { } dependencies { - api 'gyro:gyro-core:0.99.6' + (releaseBuild ? '' : '-SNAPSHOT') + api 'gyro:gyro-core:1.0.0' + (releaseBuild ? '' : '-SNAPSHOT') - implementation enforcedPlatform('com.google.cloud:libraries-bom:8.1.0') + implementation enforcedPlatform('com.google.cloud:libraries-bom:16.2.0') // XXX: com.google.apis:google-api-services-iam:v1-rev316-1.25.0 uses older version of com.google.api-client:google-api-client implementation 'com.google.api-client:google-api-client:1.30.9' implementation 'com.google.apis:google-api-services-iam:v1-rev316-1.25.0' @@ -73,9 +73,10 @@ dependencies { implementation 'com.google.cloud:google-cloud-kms' implementation 'com.google.cloud:google-cloud-resourcemanager' implementation 'com.google.cloud:google-cloud-storage' + implementation 'com.google.cloud:google-cloud-pubsub' implementation 'com.psddev:dari-util:3.3.607-xe0f27a' - gyroDoclet "gyro:gyro-doclet:0.99.1" + gyroDoclet "gyro:gyro-doclet:1.0.0" } checkstyle { diff --git a/codegen/build.gradle b/codegen/build.gradle index 14bf8d74..af9050ea 100644 --- a/codegen/build.gradle +++ b/codegen/build.gradle @@ -50,7 +50,7 @@ repositories { } dependencies { - api 'gyro:gyro-core:0.99.1-SNAPSHOT' + api 'gyro:gyro-core:1.0.0-SNAPSHOT' api 'io.airlift:airline:0.8' implementation 'com.google.guava:guava:28.1-jre' diff --git a/examples/pubsub/snapshot.gyro b/examples/pubsub/snapshot.gyro new file mode 100644 index 00000000..816d27cc --- /dev/null +++ b/examples/pubsub/snapshot.gyro @@ -0,0 +1,63 @@ +google::topic topic-example-for-snapshot + name: "topic-example-for-snapshot" + + labels: { + name: "topic-example-for-snapshot" + } +end + +google::subscription subscription-push-example + name: "subscription-push-example-for-snapshot" + topic: $(google::topic topic-example-for-snapshot) + + ack-deadline-seconds: 10 + enable-message-ordering: false + filter: "" + retain-acked-messages: true + + dead-letter-policy + dead-letter-topic: $(google::topic topic-example-for-snapshot) + max-delivery-attempts: 5 + end + + expiration-policy + ttl + seconds: 604800 + nanos: 0 + end + end + + retry-policy + maximum-backoff + seconds: 600 + nanos: 0 + end + + minimum-backoff + seconds: 600 + nanos: 0 + end + end + + message-retention + seconds: 604800 + nanos: 0 + end + + push-config + push-endpoint: "https://google.com" + end + + labels: { + name: "subscription-push-example-for-snapshot" + } +end + +google::snapshot example-snapshot + name: "example-snapshot" + subscription: $(google::subscription subscription-push-example) + + labels: { + name: "example-snapshot" + } +end diff --git a/examples/pubsub/subscription.gyro b/examples/pubsub/subscription.gyro new file mode 100644 index 00000000..8d71526f --- /dev/null +++ b/examples/pubsub/subscription.gyro @@ -0,0 +1,92 @@ +google::topic topic-example-for-subscription + name: "topic-example-for-subscription" + + labels: { + name: "topic-example-for-subscription" + } +end + +google::subscription subscription-pull-example + name: "subscription-pull-example" + topic: $(google::topic topic-example-for-subscription) + + ack-deadline-seconds: 15 + enable-message-ordering: false + filter: "" + retain-acked-messages: false + + expiration-policy + ttl + seconds: 2678400 + nanos: 0 + end + end + + message-retention + seconds: 525780 + nanos: 0 + end + + retry-policy + maximum-backoff + seconds: 600 + nanos: 0 + end + + minimum-backoff + seconds: 600 + nanos: 0 + end + end + + labels: { + name: "subscription-pull-example" + } +end + +google::subscription subscription-push-example + name: "subscription-push-example" + topic: $(google::topic topic-example-for-subscription) + + ack-deadline-seconds: 10 + enable-message-ordering: false + filter: "" + retain-acked-messages: true + + dead-letter-policy + dead-letter-topic: $(google::topic topic-example-for-subscription) + max-delivery-attempts: 5 + end + + expiration-policy + ttl + seconds: 604800 + nanos: 0 + end + end + + retry-policy + maximum-backoff + seconds: 600 + nanos: 0 + end + + minimum-backoff + seconds: 600 + nanos: 0 + end + end + + message-retention + seconds: 604800 + nanos: 0 + end + + push-config + push-endpoint: "https://google.com" + end + + labels: { + name: "subscription-push-example" + } +end diff --git a/examples/pubsub/topic.gyro b/examples/pubsub/topic.gyro new file mode 100644 index 00000000..8070b642 --- /dev/null +++ b/examples/pubsub/topic.gyro @@ -0,0 +1,7 @@ +google::topic topic-example + name: "topic-example" + + labels: { + name: "topic-example" + } +end diff --git a/src/main/java/gyro/google/GoogleCredentials.java b/src/main/java/gyro/google/GoogleCredentials.java index 1db83f21..66d853fb 100644 --- a/src/main/java/gyro/google/GoogleCredentials.java +++ b/src/main/java/gyro/google/GoogleCredentials.java @@ -35,6 +35,10 @@ import com.google.auth.http.HttpCredentialsAdapter; import com.google.cloud.kms.v1.KeyManagementServiceClient; import com.google.cloud.kms.v1.KeyManagementServiceSettings; +import com.google.cloud.pubsub.v1.SubscriptionAdminClient; +import com.google.cloud.pubsub.v1.SubscriptionAdminSettings; +import com.google.cloud.pubsub.v1.TopicAdminClient; +import com.google.cloud.pubsub.v1.TopicAdminSettings; import gyro.core.GyroException; import gyro.core.GyroInputStream; import gyro.core.auth.Credentials; @@ -127,6 +131,26 @@ private T getNonGeneralizedClient(Class clientClass) { throw new GyroException( String.format("Unable to create %s client", clientClass.getSimpleName())); } + } else if (clientClass.getSimpleName().equals("TopicAdminClient")) { + try { + TopicAdminSettings topicAdminSettings = TopicAdminSettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(getGoogleCredentials())) + .build(); + return (T) TopicAdminClient.create(topicAdminSettings); + } catch (IOException ex) { + throw new GyroException( + String.format("Unable to create %s client", clientClass.getSimpleName())); + } + } else if (clientClass.getSimpleName().equals("SubscriptionAdminClient")) { + try { + SubscriptionAdminSettings subscriptionAdminSettings = SubscriptionAdminSettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(getGoogleCredentials())) + .build(); + return (T) SubscriptionAdminClient.create(subscriptionAdminSettings); + } catch (IOException ex) { + throw new GyroException( + String.format("Unable to create %s client", clientClass.getSimpleName())); + } } else { throw new GyroException( String.format("Unable to create %s client", clientClass.getSimpleName())); diff --git a/src/main/java/gyro/google/pubsub/DeadLetterPolicy.java b/src/main/java/gyro/google/pubsub/DeadLetterPolicy.java new file mode 100644 index 00000000..a7f00a5c --- /dev/null +++ b/src/main/java/gyro/google/pubsub/DeadLetterPolicy.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.Range; +import gyro.core.validation.Required; +import gyro.google.Copyable; + +public class DeadLetterPolicy extends Diffable implements Copyable { + + private TopicResource deadLetterTopic; + private Integer maxDeliveryAttempts; + + /** + * The topic to which dead letter messages should be published. + */ + @Required + @Updatable + public TopicResource getDeadLetterTopic() { + return deadLetterTopic; + } + + public void setDeadLetterTopic(TopicResource deadLetterTopic) { + this.deadLetterTopic = deadLetterTopic; + } + + /** + * The maximum number of delivery attempts for any message. + */ + @Required + @Updatable + @Range(min = 5, max = 100) + public Integer getMaxDeliveryAttempts() { + return maxDeliveryAttempts; + } + + public void setMaxDeliveryAttempts(Integer maxDeliveryAttempts) { + this.maxDeliveryAttempts = maxDeliveryAttempts; + } + + @Override + public String primaryKey() { + return ""; + } + + @Override + public void copyFrom(com.google.pubsub.v1.DeadLetterPolicy model) throws Exception { + setDeadLetterTopic(findById(TopicResource.class, model.getDeadLetterTopic())); + setMaxDeliveryAttempts(model.getMaxDeliveryAttempts()); + } + + com.google.pubsub.v1.DeadLetterPolicy toDeadLetterPolicy() { + return com.google.pubsub.v1.DeadLetterPolicy.newBuilder() + .setDeadLetterTopic(getDeadLetterTopic().getReferenceName()) + .setMaxDeliveryAttempts(getMaxDeliveryAttempts()) + .build(); + } +} diff --git a/src/main/java/gyro/google/pubsub/Duration.java b/src/main/java/gyro/google/pubsub/Duration.java new file mode 100644 index 00000000..26203e08 --- /dev/null +++ b/src/main/java/gyro/google/pubsub/Duration.java @@ -0,0 +1,92 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import gyro.core.resource.Diffable; +import gyro.core.validation.ValidationError; +import gyro.google.Copyable; + +public class Duration extends Diffable implements Copyable { + + private Integer nanos; + private Long seconds; + + /** + * The nanosecond value of the duration. + */ + public Integer getNanos() { + return nanos; + } + + public void setNanos(Integer nanos) { + this.nanos = nanos; + } + + /** + * The second value for of the duration. + */ + public Long getSeconds() { + return seconds; + } + + public void setSeconds(Long seconds) { + this.seconds = seconds; + } + + @Override + public String primaryKey() { + return String.format( + "Duration: %s seconds, %s nano seconds", + getSeconds() != null ? getSeconds().toString() : "0", + getNanos() != null ? getNanos().toString() : "0"); + } + + @Override + public void copyFrom(com.google.protobuf.Duration model) throws Exception { + setNanos(model.getNanos()); + setSeconds(model.getSeconds()); + } + + @Override + public List validate(Set configuredFields) { + List errors = new ArrayList<>(); + + if (getSeconds() == null && getNanos() == null) { + errors.add(new ValidationError(this, null, "At least one of 'seconds' or 'nanos' is required")); + } + + return errors; + } + + com.google.protobuf.Duration toDuration() { + com.google.protobuf.Duration.Builder builder = com.google.protobuf.Duration.newBuilder(); + + if (getSeconds() != null) { + builder.setSeconds(getSeconds()); + } + + if (getNanos() != null) { + builder.setNanos(getNanos()); + } + + return builder.build(); + } +} diff --git a/src/main/java/gyro/google/pubsub/ExpirationPolicy.java b/src/main/java/gyro/google/pubsub/ExpirationPolicy.java new file mode 100644 index 00000000..c829883f --- /dev/null +++ b/src/main/java/gyro/google/pubsub/ExpirationPolicy.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import gyro.core.resource.Diffable; +import gyro.core.validation.Required; +import gyro.google.Copyable; + +public class ExpirationPolicy extends Diffable implements Copyable { + + private Duration ttl; + + /** + * The "time-to-live" duration for the subscription. + * + * @subresource gyro.google.pubsub.Duration + */ + @Required + public Duration getTtl() { + return ttl; + } + + public void setTtl(Duration ttl) { + this.ttl = ttl; + } + + @Override + public String primaryKey() { + return ""; + } + + @Override + public void copyFrom(com.google.pubsub.v1.ExpirationPolicy model) throws Exception { + Duration duration = newSubresource(Duration.class); + duration.copyFrom(model.getTtl()); + setTtl(duration); + } + + com.google.pubsub.v1.ExpirationPolicy toExpirationPolicy() { + return com.google.pubsub.v1.ExpirationPolicy.newBuilder().setTtl(getTtl().toDuration()).build(); + } +} diff --git a/src/main/java/gyro/google/pubsub/MessageStoragePolicy.java b/src/main/java/gyro/google/pubsub/MessageStoragePolicy.java new file mode 100644 index 00000000..7c443e5a --- /dev/null +++ b/src/main/java/gyro/google/pubsub/MessageStoragePolicy.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import java.util.ArrayList; +import java.util.List; + +import gyro.core.resource.Diffable; +import gyro.core.validation.Required; +import gyro.google.Copyable; + +public class MessageStoragePolicy extends Diffable implements Copyable { + + private List allowedPersistenceRegions; + + /** + * The list of IDs of GCP regions where messages that are published to the topic may be persisted in storage. + */ + @Required + public List getAllowedPersistenceRegions() { + if (allowedPersistenceRegions == null) { + allowedPersistenceRegions = new ArrayList<>(); + } + + return allowedPersistenceRegions; + } + + public void setAllowedPersistenceRegions(List allowedPersistenceRegions) { + this.allowedPersistenceRegions = allowedPersistenceRegions; + } + + @Override + public String primaryKey() { + return ""; + } + + @Override + public void copyFrom(com.google.pubsub.v1.MessageStoragePolicy model) throws Exception { + getAllowedPersistenceRegions().clear(); + setAllowedPersistenceRegions(new ArrayList<>(model.getAllowedPersistenceRegionsList())); + } + + protected com.google.pubsub.v1.MessageStoragePolicy toMessageStoragePolicy() { + return com.google.pubsub.v1.MessageStoragePolicy.newBuilder() + .addAllAllowedPersistenceRegions(getAllowedPersistenceRegions()) + .build(); + } +} diff --git a/src/main/java/gyro/google/pubsub/OidcToken.java b/src/main/java/gyro/google/pubsub/OidcToken.java new file mode 100644 index 00000000..4ccc9a77 --- /dev/null +++ b/src/main/java/gyro/google/pubsub/OidcToken.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import com.google.pubsub.v1.PushConfig; +import gyro.core.resource.Diffable; +import gyro.google.Copyable; + +public class OidcToken extends Diffable implements Copyable { + + private String audience; + private String serviceAccountEmail; + + /** + * The audience to be used when generating OIDC token. The audience claim identifies the recipients that the JWT is intended for. + */ + public String getAudience() { + return audience; + } + + public void setAudience(String audience) { + this.audience = audience; + } + + /** + * The email of the service account to be used for generating the OIDC token. + */ + public String getServiceAccountEmail() { + return serviceAccountEmail; + } + + public void setServiceAccountEmail(String serviceAccountEmail) { + this.serviceAccountEmail = serviceAccountEmail; + } + + @Override + public String primaryKey() { + return ""; + } + + @Override + public void copyFrom(PushConfig.OidcToken model) throws Exception { + setAudience(model.getAudience()); + setServiceAccountEmail(model.getServiceAccountEmail()); + } + + PushConfig.OidcToken toOidcToken() { + return PushConfig.OidcToken.newBuilder() + .setAudience(getAudience()) + .setServiceAccountEmail(getServiceAccountEmail()) + .build(); + } +} diff --git a/src/main/java/gyro/google/pubsub/PushConfig.java b/src/main/java/gyro/google/pubsub/PushConfig.java new file mode 100644 index 00000000..e0c7b908 --- /dev/null +++ b/src/main/java/gyro/google/pubsub/PushConfig.java @@ -0,0 +1,104 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import java.util.HashMap; +import java.util.Map; + +import gyro.core.resource.Diffable; +import gyro.core.validation.Required; +import gyro.google.Copyable; + +public class PushConfig extends Diffable implements Copyable { + + private Map attributes; + private OidcToken oidcToken; + private String pushEndpoint; + + /** + * The endpoint configuration attributes that can be used to control different aspects of the message delivery. + */ + public Map getAttributes() { + if (attributes == null) { + attributes = new HashMap<>(); + } + + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + /** + * The OIDC JWT token created by Pub/Sub as an authorization header in the HTTP request for every pushed message. + * + * @subresource gyro.google.pubsub.OidcToken + */ + public OidcToken getOidcToken() { + return oidcToken; + } + + public void setOidcToken(OidcToken oidcToken) { + this.oidcToken = oidcToken; + } + + /** + * The URL locating the endpoint to which messages should be pushed. + */ + @Required + public String getPushEndpoint() { + return pushEndpoint; + } + + public void setPushEndpoint(String pushEndpoint) { + this.pushEndpoint = pushEndpoint; + } + + @Override + public String primaryKey() { + return ""; + } + + @Override + public void copyFrom(com.google.pubsub.v1.PushConfig model) throws Exception { + setAttributes(model.getAttributesMap()); + setPushEndpoint(model.getPushEndpoint()); + + setOidcToken(null); + if (model.hasOidcToken()) { + OidcToken oidcToken = newSubresource(OidcToken.class); + oidcToken.copyFrom(model.getOidcToken()); + setOidcToken(oidcToken); + } + } + + com.google.pubsub.v1.PushConfig toPushConfig() { + com.google.pubsub.v1.PushConfig.Builder builder = com.google.pubsub.v1.PushConfig.newBuilder() + .setPushEndpoint(getPushEndpoint()); + + if (getOidcToken() != null) { + builder.setOidcToken(getOidcToken().toOidcToken()); + } + + if (!getAttributes().isEmpty()) { + builder.putAllAttributes(getAttributes()).build(); + } + + return builder.build(); + } +} diff --git a/src/main/java/gyro/google/pubsub/RetryPolicy.java b/src/main/java/gyro/google/pubsub/RetryPolicy.java new file mode 100644 index 00000000..a404158f --- /dev/null +++ b/src/main/java/gyro/google/pubsub/RetryPolicy.java @@ -0,0 +1,102 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import gyro.core.resource.Diffable; +import gyro.core.validation.ValidationError; +import gyro.google.Copyable; + +public class RetryPolicy extends Diffable implements Copyable { + + private Duration maximumBackoff; + private Duration minimumBackoff; + + /** + * The maximum delay between consecutive deliveries of a given message. + * + * @subresource gyro.google.pubsub.Duration + */ + public Duration getMaximumBackoff() { + return maximumBackoff; + } + + public void setMaximumBackoff(Duration maximumBackoff) { + this.maximumBackoff = maximumBackoff; + } + + /** + * The minimum delay between consecutive deliveries of a given message. + * + * @subresource gyro.google.pubsub.Duration + */ + public Duration getMinimumBackoff() { + return minimumBackoff; + } + + public void setMinimumBackoff(Duration minimumBackoff) { + this.minimumBackoff = minimumBackoff; + } + + @Override + public String primaryKey() { + return ""; + } + + @Override + public void copyFrom(com.google.pubsub.v1.RetryPolicy model) throws Exception { + if (model.hasMaximumBackoff()) { + Duration maxBackOff = newSubresource(Duration.class); + maxBackOff.copyFrom(model.getMaximumBackoff()); + setMaximumBackoff(maxBackOff); + } + + if (model.hasMinimumBackoff()) { + Duration minBackOff = newSubresource(Duration.class); + minBackOff.copyFrom(model.getMaximumBackoff()); + setMinimumBackoff(minBackOff); + } + } + + com.google.pubsub.v1.RetryPolicy toRetryPolicy() { + com.google.pubsub.v1.RetryPolicy.Builder builder = com.google.pubsub.v1.RetryPolicy.newBuilder(); + + if (getMaximumBackoff() != null) { + builder.setMaximumBackoff(getMaximumBackoff().toDuration()); + } + + if (getMinimumBackoff() != null) { + builder.setMinimumBackoff(getMinimumBackoff().toDuration()); + } + + return builder.build(); + } + + @Override + public List validate(Set configuredFields) { + List errors = new ArrayList<>(); + + if (getMinimumBackoff() == null && getMaximumBackoff() == null) { + errors.add(new ValidationError(this, null, "At least one of 'minimum-back-off' or 'maximum-back-off' is required.")); + } + + return errors; + } +} diff --git a/src/main/java/gyro/google/pubsub/SnapshotFinder.java b/src/main/java/gyro/google/pubsub/SnapshotFinder.java new file mode 100644 index 00000000..2b434207 --- /dev/null +++ b/src/main/java/gyro/google/pubsub/SnapshotFinder.java @@ -0,0 +1,96 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; + +import com.google.api.gax.rpc.NotFoundException; +import com.google.cloud.pubsub.v1.SubscriptionAdminClient; +import com.google.pubsub.v1.ProjectName; +import com.google.pubsub.v1.Snapshot; +import gyro.core.Type; +import gyro.google.GoogleFinder; +import gyro.google.util.Utils; + +/** + * Query for snapshot. + * + * Example + * ------- + * + * .. code-block:: gyro + * + * snapshot: $(external-query google::snapshot {name: "example-snapshot"}) + */ +@Type("snapshot") +public class SnapshotFinder extends GoogleFinder { + + private String name; + + /** + * The name of the snapshot. + */ + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + protected List findAllGoogle(SubscriptionAdminClient client) throws Exception { + List snapshots = new ArrayList<>(); + + try { + client.listSnapshots(ProjectName.format(getProjectId())).iterateAll().forEach(snapshots::add); + } catch (NotFoundException ignore) { + // Project not found + } finally { + client.shutdownNow(); + } + + return snapshots; + } + + @Override + protected List findGoogle(SubscriptionAdminClient client, Map filters) throws Exception { + List snapshots = new ArrayList<>(); + + try { + Snapshot snapshot = StreamSupport.stream(client.listSnapshots(ProjectName.newBuilder() + .setProject(getProjectId()) + .build()).iterateAll().spliterator(), false) + .filter(r -> Utils.getSnapshotNameFromId(r.getName()).equals(filters.get("name"))) + .findFirst() + .orElse(null); + + if (snapshot != null) { + snapshots.add(snapshot); + } + } catch (NotFoundException ignore) { + // Subscription not found + } finally { + client.shutdownNow(); + } + + return snapshots; + } +} diff --git a/src/main/java/gyro/google/pubsub/SnapshotResource.java b/src/main/java/gyro/google/pubsub/SnapshotResource.java new file mode 100644 index 00000000..99bae1df --- /dev/null +++ b/src/main/java/gyro/google/pubsub/SnapshotResource.java @@ -0,0 +1,225 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.StreamSupport; + +import com.google.api.gax.rpc.NotFoundException; +import com.google.cloud.pubsub.v1.SubscriptionAdminClient; +import com.google.protobuf.FieldMask; +import com.google.pubsub.v1.CreateSnapshotRequest; +import com.google.pubsub.v1.ProjectName; +import com.google.pubsub.v1.ProjectSnapshotName; +import com.google.pubsub.v1.Snapshot; +import com.google.pubsub.v1.UpdateSnapshotRequest; +import gyro.core.GyroUI; +import gyro.core.Type; +import gyro.core.resource.Id; +import gyro.core.resource.Output; +import gyro.core.resource.Resource; +import gyro.core.resource.Updatable; +import gyro.core.scope.State; +import gyro.core.validation.Required; +import gyro.google.Copyable; +import gyro.google.GoogleResource; +import gyro.google.util.Utils; + +/** + * Add a snapshot. + * + * Example + * ------- + * + * .. code-block:: gyro + * + * google::snapshot example-snapshot + * name: "example-snapshot" + * subscription: $(google::subscription subscription-push-example) + * + * labels: { + * "example-label": "example-value" + * } + * end + */ +@Type("snapshot") +public class SnapshotResource extends GoogleResource implements Copyable { + + private Map labels; + private String name; + private SubscriptionResource subscription; + + // Read-only + private String resourceName; + private String expireTime; + private TopicResource topic; + + /** + * The set of labels for the snapshot. + */ + @Updatable + public Map getLabels() { + if (labels == null) { + labels = new HashMap<>(); + } + + return labels; + } + + public void setLabels(Map labels) { + this.labels = labels; + } + + /** + * The name of the snapshot. + */ + @Required + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The name of the subscription from which this snapshot is retaining messages. + */ + @Required + public SubscriptionResource getSubscription() { + return subscription; + } + + public void setSubscription(SubscriptionResource subscription) { + this.subscription = subscription; + } + + /** + * The full name of the snapshot along with the project path. + */ + @Id + @Output + public String getResourceName() { + return resourceName; + } + + public void setResourceName(String resourceName) { + this.resourceName = resourceName; + } + + /** + * The time until which the snapshot is guaranteed to exist. + */ + @Output + public String getExpireTime() { + return expireTime; + } + + public void setExpireTime(String expireTime) { + this.expireTime = expireTime; + } + + /** + * The name of the topic from which this snapshot is retaining messages. + */ + @Output + public TopicResource getTopic() { + return topic; + } + + public void setTopic(TopicResource topic) { + this.topic = topic; + } + + @Override + public void copyFrom(Snapshot model) throws Exception { + setName(Utils.getSnapshotNameFromId(model.getName())); + setLabels(model.getLabelsMap()); + setResourceName(model.getName()); + setTopic(findById(TopicResource.class, model.getTopic())); + + if (model.hasExpireTime()) { + setExpireTime(model.getExpireTime().toString()); + } + } + + @Override + protected boolean doRefresh() throws Exception { + SubscriptionAdminClient client = createClient(SubscriptionAdminClient.class); + Snapshot snapshot = null; + + try { + snapshot = getSnapshot(client); + } catch (NotFoundException ignore) { + // project not found + } finally { + client.shutdownNow(); + } + + if (snapshot == null) { + return false; + } + + copyFrom(snapshot); + + return true; + } + + @Override + protected void doCreate(GyroUI ui, State state) throws Exception { + SubscriptionAdminClient client = createClient(SubscriptionAdminClient.class); + + copyFrom(client.createSnapshot(CreateSnapshotRequest.newBuilder() + .setName(ProjectSnapshotName.format(getProjectId(), getName())) + .setSubscription(getSubscription().getReferenceName()) + .putAllLabels(getLabels()) + .build())); + } + + @Override + protected void doUpdate( + GyroUI ui, State state, Resource current, Set changedFieldNames) throws Exception { + SubscriptionAdminClient client = createClient(SubscriptionAdminClient.class); + + Snapshot.Builder builder = getSnapshot(client).toBuilder(); + builder.clearLabels(); + builder.putAllLabels(getLabels()); + + client.updateSnapshot(UpdateSnapshotRequest.newBuilder() + .setSnapshot(builder.build()) + .setUpdateMask(FieldMask.newBuilder().addPaths("labels").build()) + .build()); + } + + @Override + protected void doDelete(GyroUI ui, State state) throws Exception { + SubscriptionAdminClient client = createClient(SubscriptionAdminClient.class); + + client.deleteSnapshot(getResourceName()); + } + + private Snapshot getSnapshot(SubscriptionAdminClient client) { + return StreamSupport.stream(client.listSnapshots(ProjectName.newBuilder() + .setProject(getProjectId()) + .build()).iterateAll().spliterator(), false) + .filter(r -> r.getName().equals(getResourceName())) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/gyro/google/pubsub/SubscriptionFinder.java b/src/main/java/gyro/google/pubsub/SubscriptionFinder.java new file mode 100644 index 00000000..9792b577 --- /dev/null +++ b/src/main/java/gyro/google/pubsub/SubscriptionFinder.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.google.api.gax.rpc.NotFoundException; +import com.google.cloud.pubsub.v1.SubscriptionAdminClient; +import com.google.pubsub.v1.ProjectName; +import com.google.pubsub.v1.ProjectSubscriptionName; +import com.google.pubsub.v1.Subscription; +import gyro.core.Type; +import gyro.google.GoogleFinder; + +/** + * Query for subscriptions. + * + * Example + * ------- + * + * .. code-block:: gyro + * + * subscription: $(external-query google::subscription {name: "subscription-example"}) + */ +@Type("subscription") +public class SubscriptionFinder extends GoogleFinder { + + private String name; + + /** + * The name of the subscription. + */ + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + protected List findAllGoogle(SubscriptionAdminClient client) throws Exception { + List subscriptions = new ArrayList<>(); + + try { + client.listSubscriptions(ProjectName.format(getProjectId())).iterateAll().forEach(subscriptions::add); + } catch (NotFoundException ignore) { + // Either topic or subscription not found + } finally { + client.shutdownNow(); + } + + return subscriptions; + } + + @Override + protected List findGoogle( + SubscriptionAdminClient client, Map filters) throws Exception { + List subscriptions = new ArrayList<>(); + + try { + Subscription subscription = client.getSubscription(ProjectSubscriptionName.format( + getProjectId(), + filters.get("name"))); + subscriptions.add(subscription); + } catch (NotFoundException ignore) { + // Subscription not found + } finally { + client.shutdownNow(); + } + + return subscriptions; + } +} diff --git a/src/main/java/gyro/google/pubsub/SubscriptionResource.java b/src/main/java/gyro/google/pubsub/SubscriptionResource.java new file mode 100644 index 00000000..d41e8830 --- /dev/null +++ b/src/main/java/gyro/google/pubsub/SubscriptionResource.java @@ -0,0 +1,535 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import com.google.api.gax.rpc.NotFoundException; +import com.google.cloud.pubsub.v1.SubscriptionAdminClient; +import com.google.cloud.pubsub.v1.TopicAdminClient; +import com.google.protobuf.FieldMask; +import com.google.pubsub.v1.DetachSubscriptionRequest; +import com.google.pubsub.v1.ProjectSubscriptionName; +import com.google.pubsub.v1.Subscription; +import com.google.pubsub.v1.UpdateSubscriptionRequest; +import gyro.core.GyroUI; +import gyro.core.Type; +import gyro.core.resource.Id; +import gyro.core.resource.Output; +import gyro.core.resource.Resource; +import gyro.core.resource.Updatable; +import gyro.core.scope.State; +import gyro.core.validation.Range; +import gyro.core.validation.Required; +import gyro.google.Copyable; +import gyro.google.GoogleResource; +import gyro.google.util.Utils; + +/** + * Add a subscription. + * + * Example + * ------- + * + * .. code-block:: gyro + * + * google::subscription subscription-pull-example + * name: "subscription-pull-example" + * topic: $(google::topic topic-example-for-subscription) + * + * ack-deadline-seconds: 15 + * enable-message-ordering: false + * filter: "" + * retain-acked-messages: false + * + * expiration-policy + * ttl + * seconds: 2678400 + * nanos: 0 + * end + * end + * + * message-retention + * seconds: 525780 + * nanos: 0 + * end + * + * retry-policy + * maximum-backoff + * seconds: 600 + * nanos: 0 + * end + * + * minimum-backoff + * seconds: 600 + * nanos: 0 + * end + * end + * + * labels: { + * name: "subscription-pull-example" + * } + * end + */ +@Type("subscription") +public class SubscriptionResource extends GoogleResource implements Copyable { + + private String name; + private TopicResource topic; + private Integer ackDeadlineSeconds; + private DeadLetterPolicy deadLetterPolicy; + private Boolean detached; + private Boolean enableMessageOrdering; + private ExpirationPolicy expirationPolicy; + private String filter; + private Map labels; + private Duration messageRetention; + private PushConfig pushConfig; + private Boolean retainAckedMessages; + private RetryPolicy retryPolicy; + + // Read-only + private String referenceName; + + /** + * The name of the subscription. + */ + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The topic from which this subscription is receiving messages. + */ + @Required + public TopicResource getTopic() { + return topic; + } + + public void setTopic(TopicResource topic) { + this.topic = topic; + } + + /** + * The approximate amount of time (on a best-effort basis) Pub/Sub waits for the subscriber to acknowledge receipt before resending the message. + */ + @Range(min = 10, max = 600) + @Updatable + public Integer getAckDeadlineSeconds() { + return ackDeadlineSeconds; + } + + public void setAckDeadlineSeconds(Integer ackDeadlineSeconds) { + this.ackDeadlineSeconds = ackDeadlineSeconds; + } + + /** + * The policy that specifies the conditions for dead lettering messages in this subscription. + * + * @subresource gyro.google.pubsub.DeadLetterPolicy + */ + @Updatable + public DeadLetterPolicy getDeadLetterPolicy() { + return deadLetterPolicy; + } + + public void setDeadLetterPolicy(DeadLetterPolicy deadLetterPolicy) { + this.deadLetterPolicy = deadLetterPolicy; + } + + /** + * When set to ``true``, the subscription is detached from its topic. Once detached cannot be re attached. + */ + @Updatable + public Boolean getDetached() { + return detached; + } + + public void setDetached(Boolean detached) { + this.detached = detached; + } + + /** + * When set to ``true``, messages published with the same ordering key in a message will be delivered to the subscribers in the order in which they are received by the Pub/Sub system. + */ + public Boolean getEnableMessageOrdering() { + return enableMessageOrdering; + } + + public void setEnableMessageOrdering(Boolean enableMessageOrdering) { + this.enableMessageOrdering = enableMessageOrdering; + } + + /** + * The policy that specifies the conditions for this subscription's expiration. + * + * @subresource gyro.google.pubsub.ExpirationPolicy + */ + @Updatable + public ExpirationPolicy getExpirationPolicy() { + return expirationPolicy; + } + + public void setExpirationPolicy(ExpirationPolicy expirationPolicy) { + this.expirationPolicy = expirationPolicy; + } + + /** + * The expression written in the Pub/Sub `filter language `_. + */ + @Updatable + public String getFilter() { + return filter; + } + + public void setFilter(String filter) { + this.filter = filter; + } + + /** + * The set of labels for the subscription. + */ + @Updatable + public Map getLabels() { + if (labels == null) { + labels = new HashMap<>(); + } + + return labels; + } + + public void setLabels(Map labels) { + this.labels = labels; + } + + /** + * The configuration for how long to retain unacknowledged messages in the subscription's backlog, from the moment a message is published. + * + * @subresource gyro.google.pubsub.Duration + */ + @Updatable + public Duration getMessageRetention() { + return this.messageRetention; + } + + public void setMessageRetention(Duration messageRetention) { + this.messageRetention = messageRetention; + } + + /** + * The push delivery configuration for the subscription. + * + * @subresource gyro.google.pubsub.PushConfig + */ + @Updatable + public PushConfig getPushConfig() { + return pushConfig; + } + + public void setPushConfig(PushConfig pushConfig) { + this.pushConfig = pushConfig; + } + + /** + * When set to ``true``, the acknowledged messages are retained. + */ + @Updatable + public Boolean getRetainAckedMessages() { + return retainAckedMessages; + } + + public void setRetainAckedMessages(Boolean retainAckedMessages) { + this.retainAckedMessages = retainAckedMessages; + } + + /** + * The policy that specifies how Pub/Sub retries message delivery for this subscription. + * + * @subresource gyro.google.pubsub.RetryPolicy + */ + @Updatable + public RetryPolicy getRetryPolicy() { + return retryPolicy; + } + + public void setRetryPolicy(RetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + } + + /** + * The full name of the subscription path including the project path. + */ + @Id + @Output + public String getReferenceName() { + return referenceName; + } + + public void setReferenceName(String referenceName) { + this.referenceName = referenceName; + } + + @Override + public void copyFrom(Subscription model) throws Exception { + setAckDeadlineSeconds(model.getAckDeadlineSeconds()); + setFilter(model.getFilter()); + setEnableMessageOrdering(model.getEnableMessageOrdering()); + setLabels(model.getLabelsMap()); + setName(Utils.getSubscriptionNameFromId(model.getName())); + setRetainAckedMessages(model.getRetainAckedMessages()); + setTopic(findById(TopicResource.class, model.getTopic())); + setDetached(model.getDetached()); + + setMessageRetention(null); + if (model.hasMessageRetentionDuration()) { + Duration duration = newSubresource(Duration.class); + duration.copyFrom(model.getMessageRetentionDuration()); + setMessageRetention(duration); + } + + setDeadLetterPolicy(null); + if (model.hasDeadLetterPolicy()) { + DeadLetterPolicy deadLetterPolicy = newSubresource(DeadLetterPolicy.class); + deadLetterPolicy.copyFrom(model.getDeadLetterPolicy()); + setDeadLetterPolicy(deadLetterPolicy); + } + + setRetryPolicy(null); + if (model.hasRetryPolicy()) { + RetryPolicy retryPolicy = newSubresource(RetryPolicy.class); + retryPolicy.copyFrom(model.getRetryPolicy()); + setRetryPolicy(retryPolicy); + } + + setPushConfig(null); + if (model.hasPushConfig()) { + PushConfig pushConfig = newSubresource(PushConfig.class); + pushConfig.copyFrom(model.getPushConfig()); + setPushConfig(pushConfig); + } + + setExpirationPolicy(null); + if (model.hasExpirationPolicy()) { + ExpirationPolicy expirationPolicy = newSubresource(ExpirationPolicy.class); + expirationPolicy.copyFrom(model.getExpirationPolicy()); + setExpirationPolicy(expirationPolicy); + } + + setReferenceName(model.getName()); + } + + @Override + protected boolean doRefresh() throws Exception { + SubscriptionAdminClient client = createClient(SubscriptionAdminClient.class); + Subscription subscription = null; + + try { + subscription = client.getSubscription(ProjectSubscriptionName.format( + getProjectId(), + getName())); + } catch (NotFoundException ignore) { + // Subscription not found + } finally { + client.shutdownNow(); + } + + if (subscription == null) { + return false; + } + + copyFrom(subscription); + return true; + } + + @Override + protected void doCreate(GyroUI ui, State state) throws Exception { + SubscriptionAdminClient client = createClient(SubscriptionAdminClient.class); + + Subscription.Builder builder = Subscription.newBuilder() + .setTopic(getTopic().getReferenceName()); + + if (getName() != null) { + builder.setName(ProjectSubscriptionName.format(getProjectId(), getName())); + } + + if (getAckDeadlineSeconds() != null) { + builder.setAckDeadlineSeconds(getAckDeadlineSeconds()); + } + + if (getDeadLetterPolicy() != null) { + builder.setDeadLetterPolicy(getDeadLetterPolicy().toDeadLetterPolicy()); + } + + if (getFilter() != null) { + builder.setFilter(getFilter()); + } + + if (getEnableMessageOrdering() != null) { + builder.setEnableMessageOrdering(getEnableMessageOrdering()); + } + + if (getExpirationPolicy() != null) { + builder.setExpirationPolicy(getExpirationPolicy().toExpirationPolicy()); + } + + if (getMessageRetention() != null) { + builder.setMessageRetentionDuration(getMessageRetention().toDuration()); + } + + if (getPushConfig() != null) { + builder.setPushConfig(getPushConfig().toPushConfig()); + } + + if (getRetainAckedMessages() != null) { + builder.setRetainAckedMessages(getRetainAckedMessages()); + } + + if (getRetryPolicy() != null) { + builder.setRetryPolicy(getRetryPolicy().toRetryPolicy()); + } + + if (!getLabels().isEmpty()) { + builder.putAllLabels(getLabels()); + } + + try { + Subscription subscription = client.createSubscription(builder.build()); + + copyFrom(subscription); + + if (getDetached().equals(Boolean.TRUE)) { + TopicAdminClient topicClient = createClient(TopicAdminClient.class); + + try { + topicClient.detachSubscription(DetachSubscriptionRequest.newBuilder() + .setSubscription(getReferenceName()).build()); + } finally { + topicClient.shutdown(); + } + } + } finally { + client.shutdownNow(); + } + } + + @Override + protected void doUpdate( + GyroUI ui, State state, Resource current, Set changedFieldNames) throws Exception { + SubscriptionAdminClient client = createClient(SubscriptionAdminClient.class); + + try { + if (changedFieldNames.contains("detached")) { + TopicAdminClient topicClient = createClient(TopicAdminClient.class); + + try { + topicClient.detachSubscription(DetachSubscriptionRequest.newBuilder() + .setSubscription(getReferenceName()).build()); + } finally { + topicClient.shutdown(); + } + + changedFieldNames.remove("detached"); + } + + if (!changedFieldNames.isEmpty()) { + Subscription subscription = client.getSubscription(getReferenceName()); + + Subscription.Builder subscriptionBuilder = subscription.toBuilder(); + + FieldMask.Builder fieldMaskBuilder = FieldMask.newBuilder(); + + if (changedFieldNames.contains("ack-deadline-seconds")) { + subscriptionBuilder.clearAckDeadlineSeconds(); + subscriptionBuilder.setAckDeadlineSeconds(getAckDeadlineSeconds()); + fieldMaskBuilder.addPaths("ack_deadline_seconds"); + } + + if (changedFieldNames.contains("dead-letter-policy")) { + subscriptionBuilder.clearDeadLetterPolicy(); + subscriptionBuilder.setDeadLetterPolicy(getDeadLetterPolicy().toDeadLetterPolicy()); + fieldMaskBuilder.addPaths("dead_letter_policy"); + } + + if (changedFieldNames.contains("filter")) { + subscriptionBuilder.clearFilter(); + subscriptionBuilder.setFilter(getFilter()); + fieldMaskBuilder.addPaths("filter"); + } + + if (changedFieldNames.contains("expiration-policy")) { + subscriptionBuilder.clearExpirationPolicy(); + subscriptionBuilder.setExpirationPolicy(getExpirationPolicy().toExpirationPolicy()); + fieldMaskBuilder.addPaths("expiration_policy"); + } + + if (changedFieldNames.contains("message-retention")) { + subscriptionBuilder.clearMessageRetentionDuration(); + subscriptionBuilder.setMessageRetentionDuration(getMessageRetention().toDuration()); + fieldMaskBuilder.addPaths("message_retention_duration"); + } + + if (changedFieldNames.contains("push-config")) { + subscriptionBuilder.clearPushConfig(); + subscriptionBuilder.setPushConfig(getPushConfig().toPushConfig()); + fieldMaskBuilder.addPaths("push_config"); + } + + if (changedFieldNames.contains("retain-acked-messages")) { + subscriptionBuilder.clearRetainAckedMessages(); + subscriptionBuilder.setRetainAckedMessages(getRetainAckedMessages()); + fieldMaskBuilder.addPaths("retain_acked_messages"); + } + + if (changedFieldNames.contains("labels")) { + subscriptionBuilder.clearLabels(); + subscriptionBuilder.putAllLabels(getLabels()); + fieldMaskBuilder.addPaths("labels"); + } + + if (changedFieldNames.contains("retry-policy")) { + subscriptionBuilder.clearRetryPolicy(); + subscriptionBuilder.setRetryPolicy(getRetryPolicy().toRetryPolicy()); + fieldMaskBuilder.addPaths("retry_policy"); + } + + client.updateSubscription(UpdateSubscriptionRequest.newBuilder() + .setSubscription(subscriptionBuilder.build()) + .setUpdateMask(fieldMaskBuilder.build()) + .build()); + } + } finally { + client.shutdownNow(); + } + } + + @Override + protected void doDelete(GyroUI ui, State state) throws Exception { + SubscriptionAdminClient client = createClient(SubscriptionAdminClient.class); + + try { + client.deleteSubscription(getReferenceName()); + } finally { + client.shutdownNow(); + } + } +} diff --git a/src/main/java/gyro/google/pubsub/TopicFinder.java b/src/main/java/gyro/google/pubsub/TopicFinder.java new file mode 100644 index 00000000..2d3eaf21 --- /dev/null +++ b/src/main/java/gyro/google/pubsub/TopicFinder.java @@ -0,0 +1,84 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.google.api.gax.rpc.NotFoundException; +import com.google.cloud.pubsub.v1.TopicAdminClient; +import com.google.pubsub.v1.ProjectName; +import com.google.pubsub.v1.Topic; +import com.google.pubsub.v1.TopicName; +import gyro.core.Type; +import gyro.google.GoogleFinder; + +/** + * Query for topics. + * + * Example + * ------- + * + * .. code-block:: gyro + * + * topic: $(external-query google::topic {name: "topic-example"}) + */ +@Type("topic") +public class TopicFinder extends GoogleFinder { + + private String name; + + /** + * The name of the topic. + */ + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + protected List findAllGoogle(TopicAdminClient client) throws Exception { + List topics = new ArrayList<>(); + try { + client.listTopics(ProjectName.format(getProjectId())).iterateAll().forEach(topics::add); + } finally { + client.shutdownNow(); + } + + return topics; + } + + @Override + protected List findGoogle(TopicAdminClient client, Map filters) throws Exception { + List topics = new ArrayList<>(); + + try { + Topic topic = client.getTopic(TopicName.format(getProjectId(), filters.get("name"))); + topics.add(topic); + } catch (NotFoundException ignore) { + // topic not found + } finally { + client.shutdownNow(); + } + + return topics; + } +} diff --git a/src/main/java/gyro/google/pubsub/TopicResource.java b/src/main/java/gyro/google/pubsub/TopicResource.java new file mode 100644 index 00000000..8e010ca6 --- /dev/null +++ b/src/main/java/gyro/google/pubsub/TopicResource.java @@ -0,0 +1,254 @@ +/* + * Copyright 2021, Brightspot. + * + * 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 gyro.google.pubsub; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import com.google.api.gax.rpc.NotFoundException; +import com.google.cloud.pubsub.v1.TopicAdminClient; +import com.google.protobuf.FieldMask; +import com.google.pubsub.v1.Topic; +import com.google.pubsub.v1.TopicName; +import com.google.pubsub.v1.UpdateTopicRequest; +import com.psddev.dari.util.ObjectUtils; +import gyro.core.GyroUI; +import gyro.core.Type; +import gyro.core.resource.Id; +import gyro.core.resource.Output; +import gyro.core.resource.Resource; +import gyro.core.resource.Updatable; +import gyro.core.scope.State; +import gyro.core.validation.Required; +import gyro.google.Copyable; +import gyro.google.GoogleResource; +import gyro.google.kms.CryptoKeyResource; +import gyro.google.util.Utils; + +/** + * Add a topic. + * + * Example + * ------- + * + * .. code-block:: gyro + * + * google::topic topic-example + * name: "topic-example" + * + * labels: { + * name: "topic-example" + * } + * end + */ +@Type("topic") +public class TopicResource extends GoogleResource implements Copyable { + + private CryptoKeyResource kmsKey; + private Map labels; + private MessageStoragePolicy messageStoragePolicy; + private String name; + + // Read-only + private String referenceName; + + /** + * The Cloud KMS CryptoKey to be used to protect access to messages published on this topic. + */ + @Updatable + public CryptoKeyResource getKmsKey() { + return kmsKey; + } + + public void setKmsKey(CryptoKeyResource kmsKey) { + this.kmsKey = kmsKey; + } + + /** + * The set of labels for the topic. + */ + @Updatable + public Map getLabels() { + if (labels == null) { + labels = new HashMap<>(); + } + + return labels; + } + + public void setLabels(Map labels) { + this.labels = labels; + } + + /** + * The message storage policy configuration. + * + * @subresource gyro.google.pubsub.MessageStoragePolicy + */ + @Updatable + public MessageStoragePolicy getMessageStoragePolicy() { + return messageStoragePolicy; + } + + public void setMessageStoragePolicy(MessageStoragePolicy messageStoragePolicy) { + this.messageStoragePolicy = messageStoragePolicy; + } + + /** + * The name of the topic. + */ + @Required + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The full name of the topic along with project path. + */ + @Id + @Output + public String getReferenceName() { + return referenceName; + } + + public void setReferenceName(String referenceName) { + this.referenceName = referenceName; + } + + @Override + public void copyFrom(Topic model) throws Exception { + if (!ObjectUtils.isBlank(model.getKmsKeyName())) { + setKmsKey(findById(CryptoKeyResource.class, model.getKmsKeyName())); + } + + setLabels(model.getLabelsMap()); + setName(Utils.getTopicNameFromId(model.getName())); + setReferenceName(model.getName()); + + setMessageStoragePolicy(null); + if (model.hasMessageStoragePolicy()) { + MessageStoragePolicy storagePolicy = newSubresource(MessageStoragePolicy.class); + storagePolicy.copyFrom(model.getMessageStoragePolicy()); + setMessageStoragePolicy(storagePolicy); + } + } + + @Override + protected boolean doRefresh() throws Exception { + TopicAdminClient client = createClient(TopicAdminClient.class); + + Topic topic = null; + + try { + topic = client.getTopic(TopicName.format(getProjectId(), getName())); + } catch (NotFoundException ignore) { + // topic not found + } finally { + client.shutdownNow(); + } + + if (topic == null) { + return false; + } + + copyFrom(topic); + + return true; + } + + @Override + protected void doCreate(GyroUI ui, State state) throws Exception { + TopicAdminClient client = createClient(TopicAdminClient.class); + + Topic.Builder builder = Topic.newBuilder().setName(TopicName.format(getProjectId(), getName())); + + if (!getLabels().isEmpty()) { + builder.putAllLabels(getLabels()); + } + + if (getKmsKey() != null) { + builder.setKmsKeyName(getKmsKey().getId()); + } + + if (getMessageStoragePolicy() != null) { + builder.setMessageStoragePolicy(getMessageStoragePolicy().toMessageStoragePolicy()); + } + + try { + client.createTopic(builder.build()); + } finally { + client.shutdownNow(); + } + + refresh(); + } + + @Override + protected void doUpdate( + GyroUI ui, State state, Resource current, Set changedFieldNames) throws Exception { + TopicAdminClient client = createClient(TopicAdminClient.class); + + try { + Topic topic = client.getTopic(getReferenceName()); + + Topic.Builder topicBuilder = topic.toBuilder(); + + FieldMask.Builder fieldMaskBuilder = FieldMask.newBuilder(); + + if (changedFieldNames.contains("labels")) { + topicBuilder.clearLabels(); + topicBuilder.putAllLabels(getLabels()); + fieldMaskBuilder.addPaths("labels"); + } + + if (changedFieldNames.contains("kms-key")) { + topicBuilder.clearKmsKeyName(); + topicBuilder.setKmsKeyName(getKmsKey().getName()); + fieldMaskBuilder.addPaths("kms_key_name"); + } + + if (changedFieldNames.contains("message-storage-policy")) { + topicBuilder.clearMessageStoragePolicy(); + topicBuilder.setMessageStoragePolicy(getMessageStoragePolicy().toMessageStoragePolicy()); + fieldMaskBuilder.addPaths("message_storage_policy"); + } + + client.updateTopic(UpdateTopicRequest.newBuilder() + .setUpdateMask(fieldMaskBuilder.build()) + .setTopic(topicBuilder.build()) + .build()); + } finally { + client.shutdownNow(); + } + } + + @Override + protected void doDelete(GyroUI ui, State state) throws Exception { + TopicAdminClient client = createClient(TopicAdminClient.class); + + try { + client.deleteTopic(getReferenceName()); + } finally { + client.shutdownNow(); + } + } +} diff --git a/src/main/java/gyro/google/pubsub/package-info.java b/src/main/java/gyro/google/pubsub/package-info.java new file mode 100644 index 00000000..9c782edd --- /dev/null +++ b/src/main/java/gyro/google/pubsub/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2021, Brightspot. + * + * 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. + */ + +@DocGroup("PubSub") +package gyro.google.pubsub; + +import gyro.core.resource.DocGroup; diff --git a/src/main/java/gyro/google/util/Utils.java b/src/main/java/gyro/google/util/Utils.java index 47eadd44..31677efb 100644 --- a/src/main/java/gyro/google/util/Utils.java +++ b/src/main/java/gyro/google/util/Utils.java @@ -137,4 +137,22 @@ public static String getKmsPrimaryKeyVersionFromId(String id) { int index = list.indexOf("cryptoKeyVersions"); return list.get(index + 1); } + + public static String getTopicNameFromId(String id) { + List list = Arrays.asList(id.split("/")); + int index = list.indexOf("topics"); + return list.get(index + 1); + } + + public static String getSubscriptionNameFromId(String id) { + List list = Arrays.asList(id.split("/")); + int index = list.indexOf("subscriptions"); + return list.get(index + 1); + } + + public static String getSnapshotNameFromId(String id) { + List list = Arrays.asList(id.split("/")); + int index = list.indexOf("snapshots"); + return list.get(index + 1); + } }