diff --git a/astra-db-java/pom.xml b/astra-db-java/pom.xml
index a69c2a4..c903a4a 100644
--- a/astra-db-java/pom.xml
+++ b/astra-db-java/pom.xml
@@ -71,6 +71,7 @@
org.junit.platform
junit-platform-launcher
+ ${junit.platform.version}
test
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/AstraOpsClient.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/AstraOpsClient.java
index 8779c63..ec7202a 100644
--- a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/AstraOpsClient.java
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/AstraOpsClient.java
@@ -6,6 +6,7 @@
import com.dtsx.astra.sdk.org.TokensClient;
import com.dtsx.astra.sdk.org.UsersClient;
import com.dtsx.astra.sdk.org.domain.*;
+import com.dtsx.astra.sdk.pcu.PcuGroupsClient;
import com.dtsx.astra.sdk.streaming.AstraStreamingClient;
import com.dtsx.astra.sdk.utils.ApiLocator;
import com.dtsx.astra.sdk.utils.ApiResponseHttp;
@@ -166,4 +167,17 @@ public TokensClient tokens() {
return new TokensClient(token, environment);
}
+ // ------------------------------------------------------
+ // WORKING WITH PCU GROUPS
+ // ------------------------------------------------------
+
+ /**
+ * Work with PCU groups.
+ *
+ * @return
+ * pcu groups client
+ */
+ public PcuGroupsClient pcuGroups() { // TODO `pcu()` or `pcuGroups()`?
+ return new PcuGroupsClient(token, environment);
+ }
}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/db/domain/AccessListAddress.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/db/domain/AccessListAddress.java
index 9b35820..6ca4895 100644
--- a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/db/domain/AccessListAddress.java
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/db/domain/AccessListAddress.java
@@ -7,7 +7,7 @@
/**
* Nested Address
*/
-public class AccessListAddress {
+public class AccessListAddress {
/** Address. */
private String address;
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDatacenterAssociationsClient.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDatacenterAssociationsClient.java
new file mode 100644
index 0000000..6bc2a41
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDatacenterAssociationsClient.java
@@ -0,0 +1,118 @@
+package com.dtsx.astra.sdk.pcu;
+
+import com.dtsx.astra.sdk.pcu.domain.PcuGroupDatacenterAssociation;
+import com.dtsx.astra.sdk.pcu.exception.PcuGroupDbAssociationNotFound;
+import com.dtsx.astra.sdk.pcu.exception.PcuGroupNotFoundException;
+import com.dtsx.astra.sdk.AbstractApiClient;
+import com.dtsx.astra.sdk.utils.*;
+import com.fasterxml.jackson.core.type.TypeReference;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+@Slf4j
+public class PcuGroupDatacenterAssociationsClient extends AbstractApiClient {
+ private static final TypeReference> PCU_GROUP_DB_ASSOCIATIONS =
+ new TypeReference<>() {};
+
+ @Getter
+ private final String pcuGroupId;
+
+ public PcuGroupDatacenterAssociationsClient(String token, String pcuGroupId) {
+ this(token, AstraEnvironment.PROD, pcuGroupId);
+ }
+
+ public PcuGroupDatacenterAssociationsClient(String token, AstraEnvironment env, String pcuGroupId) {
+ super(token, env);
+ this.pcuGroupId = pcuGroupId;
+ }
+
+ @Override
+ public String getServiceName() {
+ return "pcu.group.associations.datacenter";
+ }
+
+ // ---------------------------------
+ // ---- CRUD ----
+ // ---------------------------------
+
+ public boolean exist(@NonNull String datacenterId) {
+ Assert.isDatacenterID(datacenterId, "datacenter id");
+
+ return findAll()
+ .anyMatch((assoc) -> assoc.getDatacenterUUID().equals(datacenterId));
+ }
+
+ public PcuGroupDatacenterAssociation findByDatacenterId(@NonNull String datacenterId) {
+ Assert.isDatacenterID(datacenterId, "datacenter id");
+
+ return findAll()
+ .filter((assoc) -> assoc.getDatacenterUUID().equals(datacenterId))
+ .findFirst()
+ .orElseThrow(() -> new PcuGroupDbAssociationNotFound(pcuGroupId, datacenterId));
+ }
+
+ public Stream findAll() {
+ val res = GET(getEndpointPcuAssociations() + "/" + pcuGroupId, getOperationName("findAll"));
+
+ return unmarshallOrThrow(res, PCU_GROUP_DB_ASSOCIATIONS, "get pcu group db associations").stream();
+ }
+
+ public PcuGroupDatacenterAssociation associate(@NonNull String datacenterId) {
+ Assert.isDatacenterID(datacenterId, "datacenter id");
+
+ val res = POST(getEndpointPcuAssociations() + "/" + pcuGroupId + "/" + datacenterId, getOperationName("associate"));
+
+ return unmarshallOrThrow(res, new TypeReference>() {}, "associate db to pcu group").get(0);
+ }
+
+ private record TransferReqBody(String fromPCUGroupUUID, String toPCUGroupUUID, String datacenterUUID) {}
+
+ public PcuGroupDatacenterAssociation transfer(@NonNull String toPcuGroup, @NonNull String datacenterId) {
+ Assert.isUUID(toPcuGroup, "target pcu group id");
+ Assert.isDatacenterID(datacenterId, "datacenter id");
+
+ val reqBody = JsonUtils.marshall(new TransferReqBody(this.pcuGroupId, toPcuGroup, datacenterId));
+ val res = POST(getEndpointPcuAssociations() + "/transfer/" + pcuGroupId, reqBody, getOperationName("transfer"));
+
+ return unmarshallOrThrow(res, new TypeReference>() {}, "transfer db to pcu group").get(0);
+ }
+
+ public void dissociate(@NonNull String datacenterId) {
+ Assert.isDatacenterID(datacenterId, "datacenter id");
+ DELETE(getEndpointPcuAssociations() + "/" + pcuGroupId + "/" + datacenterId, getOperationName("dissociate"));
+ }
+
+ // ---------------------------------
+ // ---- Utilities ----
+ // ---------------------------------
+
+ public String getEndpointPcuAssociations() {
+ return ApiLocator.getApiDevopsEndpoint(environment) + "/pcus/association";
+ }
+
+ private T unmarshallOrThrow(ApiResponseHttp res, TypeReference clazz, String operation) {
+ try {
+ System.out.println(res.getBody());
+ return JsonUtils.unmarshallType(res.getBody(), clazz);
+ } catch (Exception e) {
+ ApiResponseError responseError = null;
+
+ try {
+ responseError = JsonUtils.unmarshallBean(res.getBody(), ApiResponseError.class);
+ } catch (Exception ignored) {}
+
+ if (responseError != null && responseError.getErrors() != null && !responseError.getErrors().isEmpty()) {
+ if (responseError.getErrors().get(0).getId() == 2000367) {
+ throw PcuGroupNotFoundException.forId(pcuGroupId);
+ }
+ }
+
+ throw new IllegalStateException("Expected code 2xx to " + operation + " but got " + res.getCode() + "body=" + res.getBody());
+ }
+ }
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupOpsClient.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupOpsClient.java
new file mode 100644
index 0000000..4b262e4
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupOpsClient.java
@@ -0,0 +1,112 @@
+package com.dtsx.astra.sdk.pcu;
+
+import com.dtsx.astra.sdk.pcu.domain.PcuGroup;
+import com.dtsx.astra.sdk.pcu.domain.PcuGroupStatusType;
+import com.dtsx.astra.sdk.pcu.domain.PcuGroupUpdateRequest;
+import com.dtsx.astra.sdk.pcu.exception.PcuGroupNotFoundException;
+import com.dtsx.astra.sdk.AbstractApiClient;
+import com.dtsx.astra.sdk.utils.ApiLocator;
+import com.dtsx.astra.sdk.utils.AstraEnvironment;
+import com.dtsx.astra.sdk.utils.JsonUtils;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+import java.util.List;
+import java.util.Optional;
+
+@Slf4j
+public class PcuGroupOpsClient extends AbstractApiClient {
+ @Getter
+ private final String pcuGroupId;
+
+ public PcuGroupOpsClient(String token, String pcuGroupId) {
+ this(token, AstraEnvironment.PROD, pcuGroupId);
+ }
+
+ public PcuGroupOpsClient(String token, AstraEnvironment env, String pcuGroupId) {
+ super(token, env);
+ this.pcuGroupId = pcuGroupId;
+ }
+
+ @Override
+ public String getServiceName() {
+ return "pcu.group";
+ }
+
+ // ---------------------------------
+ // ---- READ ----
+ // ---------------------------------
+
+ public Optional find() {
+ try {
+ return Optional.of(get());
+ } catch (PcuGroupNotFoundException e) {
+ return Optional.empty();
+ }
+ }
+
+ public PcuGroup get() {
+ return new PcuGroupsClient(token, environment).findById(pcuGroupId).orElseThrow(() -> PcuGroupNotFoundException.forId(pcuGroupId));
+ }
+
+ public boolean exist() {
+ return find().isPresent();
+ }
+
+ public boolean isActive() {
+ return PcuGroupStatusType.ACTIVE == get().getStatus();
+ }
+
+ public boolean isCreatedOrActive() {
+ return PcuGroupStatusType.CREATED == get().getStatus() || isActive();
+ }
+
+ // ---------------------------------
+ // ---- UPDATE ----
+ // ---------------------------------
+
+ public void update(PcuGroupUpdateRequest req) {
+ val base = get();
+ PUT(getEndpointPcus(), JsonUtils.marshall(List.of(req.withDefaultsAndValidations(base))), getOperationName("update"));
+ }
+
+ // ---------------------------------
+ // ---- MAINTENANCE ----
+ // ---------------------------------
+
+ public void park() {
+ val res = POST(getEndpointPcus() + "/park/" + pcuGroupId, getOperationName("park"));
+
+ if (res.getCode() >= 300) {
+ throw new IllegalStateException("Expected code 200 to park pcu group but got " + res.getCode() + "body=" + res.getBody());
+ }
+ }
+
+ public void unpark() {
+ val res = POST(getEndpointPcus() + "/unpark/" + pcuGroupId, getOperationName("unpark"));
+
+ if (res.getCode() >= 300) {
+ throw new IllegalStateException("Expected code 200 to unpark pcu group but got " + res.getCode() + "body=" + res.getBody());
+ }
+ }
+
+ public void delete() {
+ if (!exist()) {
+ throw PcuGroupNotFoundException.forId(pcuGroupId);
+ }
+ DELETE(getEndpointPcus() + "/" + pcuGroupId, getOperationName("delete"));
+ }
+
+ // ---------------------------------
+ // ---- Utilities ----
+ // ---------------------------------
+
+ public PcuGroupDatacenterAssociationsClient datacenterAssociations() {
+ return new PcuGroupDatacenterAssociationsClient(token, environment, pcuGroupId);
+ }
+
+ public String getEndpointPcus() {
+ return ApiLocator.getApiDevopsEndpoint(environment) + "/pcus";
+ }
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupsClient.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupsClient.java
new file mode 100644
index 0000000..f332380
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupsClient.java
@@ -0,0 +1,129 @@
+package com.dtsx.astra.sdk.pcu;
+
+import com.dtsx.astra.sdk.pcu.domain.PcuGroup;
+import com.dtsx.astra.sdk.pcu.domain.PcuGroupCreationRequest;
+import com.dtsx.astra.sdk.pcu.exception.PcuGroupNotFoundException;
+import com.dtsx.astra.sdk.pcu.exception.PcuGroupsNotFoundException;
+import com.dtsx.astra.sdk.AbstractApiClient;
+import com.dtsx.astra.sdk.utils.*;
+import com.fasterxml.jackson.core.type.TypeReference;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+import java.net.HttpURLConnection;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+@Slf4j
+public class PcuGroupsClient extends AbstractApiClient {
+ private static final TypeReference> RESPONSE_PCU_GROUPS =
+ new TypeReference<>(){};
+
+ public PcuGroupsClient(String token) {
+ super(token, AstraEnvironment.PROD);
+ }
+
+ public PcuGroupsClient(String token, AstraEnvironment env) {
+ super(token, env);
+ }
+
+ @Override
+ public String getServiceName() {
+ return "pcu.groups";
+ }
+
+ // ---------------------------------
+ // ---- CRUD ----
+ // ---------------------------------
+
+ public PcuGroup create(PcuGroupCreationRequest req) {
+ val res = POST(getEndpointPcus(), JsonUtils.marshall(List.of(req.withDefaultsAndValidations())), getOperationName("create"));
+
+ if (HttpURLConnection.HTTP_CREATED != res.getCode()) {
+ throw new IllegalStateException("Expected code 201 to create pcu group but got " + res.getCode() + "body=" + res.getBody());
+ }
+
+ return JsonUtils.unmarshallType(res.getBody(), RESPONSE_PCU_GROUPS).get(0);
+ }
+
+ public Optional findById(String id) {
+ try {
+ return findAllImpl(List.of(id), "id", (_e) -> PcuGroupNotFoundException.forId(id)).findFirst();
+ } catch (PcuGroupNotFoundException e) {
+ return Optional.empty();
+ }
+ }
+
+ public Stream findByTitle(String title) {
+ return findAll().filter(pg -> title.equals(pg.getTitle())); // order is important here since pg.title is nullable
+ }
+
+ public Optional findFirstByTitle(String title) {
+ return findByTitle(title).findFirst();
+ }
+
+ public Stream findAll() {
+ return findAll(null);
+ }
+
+ public Stream findAll(List ids) {
+ return findAllImpl(ids, "ids[%d]", (e) -> new PcuGroupsNotFoundException(e.getErrors().get(0).getMessage()));
+ }
+
+ protected interface FindAll404Handler {
+ RuntimeException getError(ApiResponseError res);
+ }
+
+ private record FindAllReqBody(List pcuGroupUUIDs) {}
+
+ protected Stream findAllImpl(List ids, String validationErrorFmtStr, FindAll404Handler on404) {
+ if (ids != null) {
+ if (ids.isEmpty()) {
+ return Stream.of(); // TODO throw error or just return empty list or return all pcu groups? (devops api does the third)
+ }
+
+ for (var i = 0; i < ids.size(); i++) {
+ Assert.isUUID(ids.get(i), validationErrorFmtStr.formatted(i));
+ }
+ }
+
+ val reqBody = JsonUtils.marshall(new FindAllReqBody(ids));
+ val res = POST(getEndpointPcus() + "/actions/get", reqBody, getOperationName("find"));
+
+ try {
+ return JsonUtils.unmarshallType(res.getBody(), RESPONSE_PCU_GROUPS).stream();
+ } catch(Exception e) {
+ ApiResponseError responseError = null;
+
+ try {
+ responseError = JsonUtils.unmarshallBean(res.getBody(), ApiResponseError.class);
+ } catch (Exception ignored) {}
+
+
+ if (responseError != null && res.getCode() == HttpURLConnection.HTTP_NOT_FOUND) {
+ throw on404.getError(responseError);
+ }
+
+ if (responseError != null && responseError.getErrors() != null && !responseError.getErrors().isEmpty()) {
+ if (responseError.getErrors().get(0).getId() == 340018) { // TODO is this the right error code? also why does find all get special treatment for auth errors?
+ throw new IllegalArgumentException("You have provided an invalid token, please check", e);
+ }
+ }
+
+ throw e;
+ }
+ }
+
+ // ---------------------------------
+ // ---- Utilities ----
+ // ---------------------------------
+
+ public PcuGroupOpsClient group(String pcuGroupId) {
+ return new PcuGroupOpsClient(getToken(), getEnvironment(), pcuGroupId);
+ }
+
+ public String getEndpointPcus() {
+ return ApiLocator.getApiDevopsEndpoint(environment) + "/pcus";
+ }
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroup.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroup.java
new file mode 100644
index 0000000..66c848d
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroup.java
@@ -0,0 +1,36 @@
+package com.dtsx.astra.sdk.pcu.domain;
+
+import com.dtsx.astra.sdk.db.domain.CloudProviderType;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class PcuGroup {
+ @JsonProperty("uuid")
+ private String id;
+ private String orgId;
+
+ private String title;
+ private String description;
+
+ private CloudProviderType cloudProvider;
+ private String region;
+
+ private String instanceType;
+ private PcuProvisionType provisionType;
+
+ private int min;
+ private int max;
+ private int reserved;
+
+ private String createdAt;
+ private String updatedAt;
+ private String createdBy;
+ private String updatedBy;
+
+ private PcuGroupStatusType status;
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreateUpdateRequest.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreateUpdateRequest.java
new file mode 100644
index 0000000..c5d742a
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreateUpdateRequest.java
@@ -0,0 +1,47 @@
+package com.dtsx.astra.sdk.pcu.domain;
+
+import com.dtsx.astra.sdk.db.domain.CloudProviderType;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.SuperBuilder;
+
+@Getter
+@Setter
+@SuperBuilder
+public sealed abstract class PcuGroupCreateUpdateRequest permits PcuGroupCreationRequest, PcuGroupUpdateRequest {
+ protected String title;
+ protected String description;
+
+ protected CloudProviderType cloudProvider;
+ protected String region;
+
+ protected Integer min; // Integers so they're nullable
+ protected Integer max;
+ protected Integer reserved;
+
+ protected void validate() {
+ if (title == null || title.isBlank()) {
+ throw new IllegalArgumentException("PCU group title is required");
+ }
+
+ if (cloudProvider == null) {
+ throw new IllegalArgumentException("PCU group cloud provider is required");
+ }
+
+ if (region == null || region.isBlank()) {
+ throw new IllegalArgumentException("PCU group region is required");
+ }
+
+ if (min == null || min < 1) {
+ throw new IllegalArgumentException("PCU group min must be >= 1");
+ }
+
+ if (max == null || max < min) {
+ throw new IllegalArgumentException("PCU group max must be >= min");
+ }
+
+ if (reserved != null && (reserved < 0 || reserved > min)) {
+ throw new IllegalArgumentException("PCU group reserved must be non-negative and <= min");
+ }
+ }
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreationRequest.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreationRequest.java
new file mode 100644
index 0000000..5a50bf6
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreationRequest.java
@@ -0,0 +1,30 @@
+package com.dtsx.astra.sdk.pcu.domain;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.SuperBuilder;
+
+@Getter
+@Setter
+@SuperBuilder
+public final class PcuGroupCreationRequest extends PcuGroupCreateUpdateRequest {
+ private String instanceType;
+ private PcuProvisionType provisionType;
+
+ public PcuGroupCreationRequest withDefaultsAndValidations() {
+ if (this.provisionType == null) {
+ this.provisionType = PcuProvisionType.SHARED;
+ }
+
+ // TODO do we really want a default for this? (since pcu instance types are changing)
+ if (this.instanceType == null || this.instanceType.isBlank()) {
+ this.instanceType = "standard";
+ }
+
+ if (this.reserved == null) {
+ this.reserved = 0;
+ }
+
+ return this;
+ }
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDatacenterAssociation.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDatacenterAssociation.java
new file mode 100644
index 0000000..48574f5
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDatacenterAssociation.java
@@ -0,0 +1,12 @@
+package com.dtsx.astra.sdk.pcu.domain;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+// TODO add the rest of the fields once the PCU team is clear about what is going on
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class PcuGroupDatacenterAssociation {
+ private String pcuGroupUUID;
+ private String datacenterUUID;
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupStatusType.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupStatusType.java
new file mode 100644
index 0000000..45b4d64
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupStatusType.java
@@ -0,0 +1,12 @@
+package com.dtsx.astra.sdk.pcu.domain;
+
+public enum PcuGroupStatusType {
+ CREATED,
+ PLACING,
+ INITIALIZING,
+ ACTIVE,
+ PARKED,
+ PARKING,
+ UNPARKING,
+ OTHER // TODO make this work with Jackson
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupUpdateRequest.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupUpdateRequest.java
new file mode 100644
index 0000000..02b6d42
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupUpdateRequest.java
@@ -0,0 +1,44 @@
+package com.dtsx.astra.sdk.pcu.domain;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+import lombok.val;
+
+@SuperBuilder
+public non-sealed class PcuGroupUpdateRequest extends PcuGroupCreateUpdateRequest {
+ // TODO once the bug that causes fields to potentially be lost during partial updates is fixed, we can remove the base parameter here
+ public PcuGroupCreateUpdateRequest withDefaultsAndValidations(PcuGroup base) {
+ val internalRep = new InternalRep(
+ builder()
+ .title(this.title == null ? base.getTitle() : this.title)
+ .description(this.description == null ? base.getDescription() : this.description)
+ .cloudProvider(this.cloudProvider == null ? base.getCloudProvider() : this.cloudProvider)
+ .region(this.region == null ? base.getRegion() : this.region)
+ .min(this.min == null ? base.getMin() : this.min)
+ .max(this.max == null ? base.getMax() : this.max)
+ .reserved(this.reserved == null ? base.getReserved() : this.reserved)
+ );
+
+ internalRep.validate();
+
+ return internalRep
+ .setPcuGroupUUID(base.getId())
+ .setInstanceType(base.getInstanceType())
+ .setProvisionType(base.getProvisionType());
+ }
+
+ @Setter
+ @Getter
+ @Accessors(chain = true)
+ public static class InternalRep extends PcuGroupUpdateRequest {
+ private String pcuGroupUUID;
+ private String instanceType;
+ private PcuProvisionType provisionType;
+
+ protected InternalRep(PcuGroupUpdateRequestBuilder, ?> b) {
+ super(b);
+ }
+ }
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuProvisionType.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuProvisionType.java
new file mode 100644
index 0000000..ee4fe1f
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuProvisionType.java
@@ -0,0 +1,17 @@
+package com.dtsx.astra.sdk.pcu.domain;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.Accessors;
+
+@Getter
+@Accessors(fluent = true)
+@RequiredArgsConstructor
+public enum PcuProvisionType {
+ SHARED("shared"),
+ DEDICATED("dedicated");
+
+ @JsonValue
+ private final String fieldValue;
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupDbAssociationNotFound.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupDbAssociationNotFound.java
new file mode 100644
index 0000000..a830e90
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupDbAssociationNotFound.java
@@ -0,0 +1,17 @@
+package com.dtsx.astra.sdk.pcu.exception;
+
+import lombok.Getter;
+
+public class PcuGroupDbAssociationNotFound extends RuntimeException {
+ @Getter
+ private final String pcuGroupId;
+
+ @Getter
+ private final String datacenterId;
+
+ public PcuGroupDbAssociationNotFound(String pcuGroupId, String datacenterId) {
+ super("Association not found for pcu group '" + pcuGroupId + "' and datacenter '" + datacenterId + "'");
+ this.pcuGroupId = pcuGroupId;
+ this.datacenterId = datacenterId;
+ }
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupNotFoundException.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupNotFoundException.java
new file mode 100644
index 0000000..4df7f4e
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupNotFoundException.java
@@ -0,0 +1,28 @@
+package com.dtsx.astra.sdk.pcu.exception;
+
+import lombok.Getter;
+
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType") // what a stupid rule ~ sincerely, a haskeller
+public class PcuGroupNotFoundException extends RuntimeException {
+ @Getter
+ private final Optional title;
+
+ @Getter
+ private final Optional id;
+
+ private PcuGroupNotFoundException(Optional title, Optional id) {
+ super("PCU group " + title.or(() -> id).map(s -> "'" + s + "' ").orElse("") + "has not been found.");
+ this.title = title;
+ this.id = id;
+ }
+
+ public static PcuGroupNotFoundException forTitle(String title) {
+ return new PcuGroupNotFoundException(Optional.of(title), Optional.empty());
+ }
+
+ public static PcuGroupNotFoundException forId(String id) {
+ return new PcuGroupNotFoundException(Optional.empty(), Optional.of(id));
+ }
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupsNotFoundException.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupsNotFoundException.java
new file mode 100644
index 0000000..c079399
--- /dev/null
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupsNotFoundException.java
@@ -0,0 +1,7 @@
+package com.dtsx.astra.sdk.pcu.exception;
+
+public class PcuGroupsNotFoundException extends RuntimeException {
+ public PcuGroupsNotFoundException(String message) {
+ super(message); // unfortunately the devops api doesn't return a very workable error so will just use the message directly
+ }
+}
diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/Assert.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/Assert.java
index 7e2158a..02db862 100644
--- a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/Assert.java
+++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/utils/Assert.java
@@ -16,6 +16,8 @@
package com.dtsx.astra.sdk.utils;
+import java.util.regex.Pattern;
+
/**
* Syntaxic sugar for common validations.
*
@@ -69,4 +71,30 @@ public static void isTrue(Boolean b, String msg) {
}
}
-}
\ No newline at end of file
+ private static final String UUID_PATTERN_STR = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
+
+ private static final Pattern UUID_PATTERN = Pattern.compile("^" + UUID_PATTERN_STR + "$");
+ private static final Pattern DATACENTER_ID_PATTERN = Pattern.compile("^" + UUID_PATTERN_STR + "-\\d+$");
+
+ public static void isUUID(String id, String name) {
+ hasLength(id, name);
+
+ if (!UUID_PATTERN.matcher(id).matches()) {
+ throw new IllegalArgumentException("Parameter '" + name + "' should be a valid UUID");
+ }
+ }
+
+ public static void isDatacenterID(String id, String name) {
+ hasLength(id, name);
+
+ if (!DATACENTER_ID_PATTERN.matcher(id).matches()) {
+ var addendum = "";
+
+ if (UUID_PATTERN.matcher(id).matches()) {
+ addendum = " (missing '-' suffix; did you accidentally pass a database id instead?)";
+ }
+
+ throw new IllegalArgumentException("Parameter '" + name + "' should be a valid datacenter id of format -" + addendum);
+ }
+ }
+}
diff --git a/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/pcu/ParkingTest.java b/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/pcu/ParkingTest.java
new file mode 100644
index 0000000..0d1b53e
--- /dev/null
+++ b/astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/pcu/ParkingTest.java
@@ -0,0 +1,10 @@
+package com.dtsx.astra.sdk.pcu;
+
+import com.dtsx.astra.sdk.AstraOpsClient;
+import com.dtsx.astra.sdk.utils.AstraEnvironment;
+import lombok.val;
+import org.junit.jupiter.api.Test;
+
+public class ParkingTest {
+
+}