From 307214b9a8e139c8a52e00f41998ff4a85e68d73 Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 22 Oct 2025 20:04:25 -0500 Subject: [PATCH 1/4] initial pcu support implementation --- .../sdk/db/domain/AccessListAddress.java | 2 +- .../sdk/pcu/PcuGroupDbAssociationsClient.java | 139 ++++++++++++++++++ .../dtsx/astra/sdk/pcu/PcuGroupOpsClient.java | 90 ++++++++++++ .../dtsx/astra/sdk/pcu/PcuGroupsClient.java | 123 ++++++++++++++++ .../dtsx/astra/sdk/pcu/domain/PcuGroup.java | 34 +++++ .../pcu/domain/PcuGroupCreationBuilder.java | 32 ++++ .../pcu/domain/PcuGroupCreationRequest.java | 36 +++++ .../sdk/pcu/domain/PcuGroupDbAssociation.java | 12 ++ .../sdk/pcu/domain/PcuGroupStatusType.java | 12 ++ .../sdk/pcu/domain/PcuProvisionType.java | 17 +++ .../PcuGroupDbAssociationNotFound.java | 17 +++ .../exception/PcuGroupNotFoundException.java | 28 ++++ .../exception/PcuGroupsNotFoundException.java | 7 + .../java/com/dtsx/astra/sdk/utils/Assert.java | 30 +++- 14 files changed, 577 insertions(+), 2 deletions(-) create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDbAssociationsClient.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupOpsClient.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupsClient.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroup.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreationBuilder.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreationRequest.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDbAssociation.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupStatusType.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuProvisionType.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupDbAssociationNotFound.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupNotFoundException.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/exception/PcuGroupsNotFoundException.java 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 9b358208..6ca48953 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/PcuGroupDbAssociationsClient.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDbAssociationsClient.java new file mode 100644 index 00000000..0f5d66ba --- /dev/null +++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDbAssociationsClient.java @@ -0,0 +1,139 @@ +package com.dtsx.astra.sdk.pcu; + +import com.dtsx.astra.sdk.AbstractApiClient; +import com.dtsx.astra.sdk.db.domain.Datacenter; +import com.dtsx.astra.sdk.pcu.domain.PcuGroup; +import com.dtsx.astra.sdk.pcu.domain.PcuGroupDbAssociation; +import com.dtsx.astra.sdk.pcu.exception.PcuGroupDbAssociationNotFound; +import com.dtsx.astra.sdk.pcu.exception.PcuGroupNotFoundException; +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 PcuGroupDbAssociationsClient extends AbstractApiClient { + private static final TypeReference> PCU_GROUP_DB_ASSOCIATIONS = + new TypeReference<>() {}; + + @Getter + private final String pcuGroupId; + + public PcuGroupDbAssociationsClient(String token, String pcuGroupId) { + this(token, AstraEnvironment.PROD, pcuGroupId); + } + + public PcuGroupDbAssociationsClient(String token, AstraEnvironment env, String pcuGroupId) { + super(token, env); + this.pcuGroupId = pcuGroupId; + } + + @Override + public String getServiceName() { + return "pcu.group.associations.db"; + } + + // --------------------------------- + // ---- CRUD ---- + // --------------------------------- + + public boolean exist(@NonNull String datacenterId) { + Assert.isDatacenterID(datacenterId, "datacenter id"); + + return findAll() + .anyMatch((assoc) -> assoc.getDatacenterUUID().equals(datacenterId)); + } + + public boolean exist(@NonNull Datacenter datacenter) { + return exist(datacenter.getId()); + } + + public PcuGroupDbAssociation findByDatacenterId(@NonNull String datacenterId) { + Assert.isDatacenterID(datacenterId, "datacenter id"); + + return findAll() + .filter((assoc) -> assoc.getDatacenterUUID().equals(datacenterId)) + .findFirst() + .orElseThrow(() -> new PcuGroupDbAssociationNotFound(pcuGroupId, datacenterId)); + } + + public PcuGroupDbAssociation findByDatacenter(@NonNull Datacenter datacenter) { + return findByDatacenterId(datacenter.getId()); + } + + public Stream findAll() { + val res = GET(getEndpointPcuAssociations() + "/" + pcuGroupId, getOperationName("findAll")); + + return unmarshallOrThrow(res, PCU_GROUP_DB_ASSOCIATIONS, 200, "get pcu group db associations").stream(); + } + + public PcuGroupDbAssociation associate(@NonNull String datacenterId) { + Assert.isDatacenterID(datacenterId, "datacenter id"); + + val res = POST(getEndpointPcuAssociations() + "/" + pcuGroupId + "/" + datacenterId, getOperationName("associate")); + + return unmarshallOrThrow(res, new TypeReference<>() {}, 201, "associate db to pcu group"); + } + + public PcuGroupDbAssociation associate(@NonNull Datacenter datacenter) { + return associate(datacenter.getId()); + } + + private record TransferReqBody(String fromPCUGroupUUID, String toPCUGroupUUID, String datacenterUUID) {} + + public PcuGroupDbAssociation 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<>() {}, 200, "transfer db to pcu group"); + } + + public PcuGroupDbAssociation transfer(@NonNull PcuGroup toPcuGroup, @NonNull Datacenter datacenter) { + return transfer(toPcuGroup.getUuid(), datacenter.getId()); + } + + public void dissociate(@NonNull String datacenterId) { + Assert.isDatacenterID(datacenterId, "datacenter id"); + DELETE(getEndpointPcuAssociations() + "/" + pcuGroupId + "/" + datacenterId, getOperationName("dissociate")); + } + + public void dissociate(@NonNull Datacenter datacenter) { + dissociate(datacenter.getId()); + } + + // --------------------------------- + // ---- Utilities ---- + // --------------------------------- + + public String getEndpointPcuAssociations() { + return ApiLocator.getApiDevopsEndpoint(environment) + "/pcus/association"; + } + + private T unmarshallOrThrow(ApiResponseHttp res, TypeReference clazz, int expectedCode, String operation) { + try { + 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().getFirst().getId() == 2000367) { + throw PcuGroupNotFoundException.forId(pcuGroupId); + } + } + + throw new IllegalStateException("Expected code " + expectedCode + " 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 00000000..73939a28 --- /dev/null +++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupOpsClient.java @@ -0,0 +1,90 @@ +package com.dtsx.astra.sdk.pcu; + +import com.dtsx.astra.sdk.AbstractApiClient; +import com.dtsx.astra.sdk.pcu.domain.PcuGroup; +import com.dtsx.astra.sdk.pcu.domain.PcuGroupStatusType; +import com.dtsx.astra.sdk.pcu.exception.PcuGroupNotFoundException; +import com.dtsx.astra.sdk.utils.ApiLocator; +import com.dtsx.astra.sdk.utils.AstraEnvironment; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import lombok.val; + +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"; + } + + public Optional find() { + try { + return Optional.of(get()); + } catch (PcuGroupNotFoundException e) { + return Optional.empty(); + } + } + + public PcuGroup get() { + return new PcuGroupsClient(token, environment).findById(pcuGroupId); + } + + public boolean exist() { + return find().isPresent(); + } + + public boolean isActive() { + return PcuGroupStatusType.ACTIVE == get().getStatus(); + } + + public boolean isCreatedOrActive() { + return PcuGroupStatusType.CREATED == get().getStatus() || isActive(); + } + + // --------------------------------- + // ---- MAINTENANCE ---- + // --------------------------------- + + public void park() { + val res = POST(getEndpointPcus() + "/park/" + pcuGroupId, getOperationName("park")); + assertHttpCodeAccepted(res, "park", pcuGroupId); + } + + public void unpark() { + val res = POST(getEndpointPcus() + "/unpark/" + pcuGroupId, getOperationName("unpark")); + assertHttpCodeAccepted(res, "unpark", pcuGroupId); + } + + public void delete() { + if (!exist()) { + throw PcuGroupNotFoundException.forId(pcuGroupId); + } + DELETE(getEndpointPcus() + "/" + pcuGroupId, getOperationName("delete")); + } + + // --------------------------------- + // ---- Utilities ---- + // --------------------------------- + + public PcuGroupDbAssociationsClient dbAssociations() { + return new PcuGroupDbAssociationsClient(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 00000000..931bc0c6 --- /dev/null +++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupsClient.java @@ -0,0 +1,123 @@ +package com.dtsx.astra.sdk.pcu; + +import com.dtsx.astra.sdk.AbstractApiClient; +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.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.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(req), getOperationName("create")); + + if (HttpURLConnection.HTTP_CREATED != res.getCode()) { + throw new IllegalStateException("Expected code 201 to create db but got " + res.getCode() + "body=" + res.getBody()); + } + + return JsonUtils.unmarshallType(res.getBody(), RESPONSE_PCU_GROUPS).getFirst(); + } + + public PcuGroup findById(String id) { + return findAllImpl(List.of(id), "id", (_e) -> PcuGroupNotFoundException.forId(id)).findFirst().orElseThrow(); // it can never throw unless the API itself is broken + } + + public PcuGroup findByTitle(String title) { + return findAll() + .filter(pg -> pg.getTitle().equals(title)) + .findFirst() + .orElseThrow(() -> PcuGroupNotFoundException.forTitle(title)); + } + + public Stream findAll() { + return findAll(null); + } + + public Stream findAll(List ids) { + return findAllImpl(ids, "ids[%d]", (e) -> new PcuGroupsNotFoundException(e.getErrors().getFirst().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().getFirst().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 00000000..e68d9a4a --- /dev/null +++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroup.java @@ -0,0 +1,34 @@ +package com.dtsx.astra.sdk.pcu.domain; + +import com.dtsx.astra.sdk.db.domain.CloudProviderType; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class PcuGroup { + private String uuid; // TODO should we call it 'id' for consistency? w/ @JsonProperty("uuid") + should this be a UUID? + private String orgId; + + private String title; + private String description; // TODO should we use Optional<...>s? + + private CloudProviderType cloudProvider; + private String region; + + private String instanceType; + private PcuProvisionType provisionType; + + private int min; + private int max; + private int reserved; + + private String createdAt; // TODO should these be DateTimes/Instants instead? + 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/PcuGroupCreationBuilder.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreationBuilder.java new file mode 100644 index 00000000..0fe67981 --- /dev/null +++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreationBuilder.java @@ -0,0 +1,32 @@ +package com.dtsx.astra.sdk.pcu.domain; + +import com.dtsx.astra.sdk.db.domain.CloudProviderType; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Setter +@Accessors(fluent = true, chain = true) +public class PcuGroupCreationBuilder { + protected String title; // TODO should we have an alias setter function called 'name' for consistency/ease-of-use? + protected String description; + + protected CloudProviderType cloudProvider; + protected String cloudRegion; + + protected String instanceType; + protected PcuProvisionType provisionType; + + protected int minCapacity; + protected int maxCapacity; + protected int reservedCapacity; + + public PcuGroupCreationBuilder() {} + + public PcuGroupCreationBuilder(String title) { + this.title = title; + } + + public PcuGroupCreationRequest build() { + return new PcuGroupCreationRequest(this); + } +} 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 00000000..ecc9917e --- /dev/null +++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreationRequest.java @@ -0,0 +1,36 @@ +package com.dtsx.astra.sdk.pcu.domain; + +import com.dtsx.astra.sdk.db.domain.CloudProviderType; +import lombok.Value; + +@Value +public class PcuGroupCreationRequest { + String title; + String description; + + CloudProviderType cloudProvider; + String region; + + String instanceType; + PcuProvisionType provisionType; + + int min; + int max; + int reserved; + + public PcuGroupCreationRequest(PcuGroupCreationBuilder builder) { + this.title = builder.title; + this.description = builder.description; + this.cloudProvider = builder.cloudProvider; + this.region = builder.cloudRegion; + this.instanceType = builder.instanceType; + this.provisionType = builder.provisionType; + this.min = builder.minCapacity; + this.max = builder.maxCapacity; + this.reserved = builder.reservedCapacity; + } + + public static PcuGroupCreationBuilder builder() { + return new PcuGroupCreationBuilder(); + } +} diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDbAssociation.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDbAssociation.java new file mode 100644 index 00000000..7697c494 --- /dev/null +++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDbAssociation.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 PcuGroupDbAssociation { + 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 00000000..6ff009dc --- /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; + +// TODO should this be a sealed interface with an 'OTHER()' (or UNKNOWN) implementation to future-proof? +public enum PcuGroupStatusType { + CREATED, + PLACING, + INITIALIZING, + ACTIVE, + PARKED, + PARKING, + UNPARKING +} 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 00000000..ee4fe1f9 --- /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 00000000..a830e90d --- /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 00000000..4df7f4ea --- /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 00000000..c0793997 --- /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 7e2158af..02db8622 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); + } + } +} From 5f77d13d169cee5acc82b8f4020b6155350d4368 Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 22 Oct 2025 20:27:48 -0500 Subject: [PATCH 2/4] a --- .../java/com/dtsx/astra/sdk/AstraOpsClient.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 8779c630..861b7d29 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() { + return new PcuGroupsClient(token, environment); + } } From 512fe85ebb3dd6e7c667c374a203529b30f930db Mon Sep 17 00:00:00 2001 From: toptobes Date: Mon, 27 Oct 2025 10:54:33 -0500 Subject: [PATCH 3/4] work --- ...PcuGroupDatacenterAssociationsClient.java} | 8 +-- .../dtsx/astra/sdk/pcu/PcuGroupOpsClient.java | 19 ++++++- .../dtsx/astra/sdk/pcu/PcuGroupsClient.java | 3 +- .../domain/PcuGroupCreateUpdateRequest.java | 49 +++++++++++++++++++ .../pcu/domain/PcuGroupCreationBuilder.java | 32 ------------ .../pcu/domain/PcuGroupCreationRequest.java | 44 +++++++---------- .../sdk/pcu/domain/PcuGroupUpdateRequest.java | 44 +++++++++++++++++ 7 files changed, 133 insertions(+), 66 deletions(-) rename astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/{PcuGroupDbAssociationsClient.java => PcuGroupDatacenterAssociationsClient.java} (94%) create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreateUpdateRequest.java delete mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreationBuilder.java create mode 100644 astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupUpdateRequest.java diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDbAssociationsClient.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDatacenterAssociationsClient.java similarity index 94% rename from astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDbAssociationsClient.java rename to astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDatacenterAssociationsClient.java index 0f5d66ba..c03dfd8a 100644 --- a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDbAssociationsClient.java +++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDatacenterAssociationsClient.java @@ -17,25 +17,25 @@ import java.util.stream.Stream; @Slf4j -public class PcuGroupDbAssociationsClient extends AbstractApiClient { +public class PcuGroupDatacenterAssociationsClient extends AbstractApiClient { private static final TypeReference> PCU_GROUP_DB_ASSOCIATIONS = new TypeReference<>() {}; @Getter private final String pcuGroupId; - public PcuGroupDbAssociationsClient(String token, String pcuGroupId) { + public PcuGroupDatacenterAssociationsClient(String token, String pcuGroupId) { this(token, AstraEnvironment.PROD, pcuGroupId); } - public PcuGroupDbAssociationsClient(String token, AstraEnvironment env, String pcuGroupId) { + public PcuGroupDatacenterAssociationsClient(String token, AstraEnvironment env, String pcuGroupId) { super(token, env); this.pcuGroupId = pcuGroupId; } @Override public String getServiceName() { - return "pcu.group.associations.db"; + return "pcu.group.associations.datacenter"; } // --------------------------------- 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 index 73939a28..2e9757ba 100644 --- 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 @@ -3,9 +3,11 @@ import com.dtsx.astra.sdk.AbstractApiClient; 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.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; @@ -31,6 +33,10 @@ public String getServiceName() { return "pcu.group"; } + // --------------------------------- + // ---- READ ---- + // --------------------------------- + public Optional find() { try { return Optional.of(get()); @@ -55,6 +61,15 @@ public boolean isCreatedOrActive() { return PcuGroupStatusType.CREATED == get().getStatus() || isActive(); } + // --------------------------------- + // ---- UPDATE ---- + // --------------------------------- + + public void update(PcuGroupUpdateRequest req) { + val base = get(); + PATCH(getEndpointPcus() + "/" + pcuGroupId, JsonUtils.marshall(req.withDefaultsAndValidations(base)), getOperationName("update")); + } + // --------------------------------- // ---- MAINTENANCE ---- // --------------------------------- @@ -80,8 +95,8 @@ public void delete() { // ---- Utilities ---- // --------------------------------- - public PcuGroupDbAssociationsClient dbAssociations() { - return new PcuGroupDbAssociationsClient(token, environment, pcuGroupId); + public PcuGroupDatacenterAssociationsClient datacenterAssociations() { + return new PcuGroupDatacenterAssociationsClient(token, environment, pcuGroupId); } public String getEndpointPcus() { 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 index 931bc0c6..c14c80d1 100644 --- 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 @@ -2,6 +2,7 @@ import com.dtsx.astra.sdk.AbstractApiClient; import com.dtsx.astra.sdk.pcu.domain.PcuGroup; +import com.dtsx.astra.sdk.pcu.domain.PcuGroupCreateUpdateRequest; import com.dtsx.astra.sdk.pcu.domain.PcuGroupCreationRequest; import com.dtsx.astra.sdk.pcu.exception.PcuGroupNotFoundException; import com.dtsx.astra.sdk.pcu.exception.PcuGroupsNotFoundException; @@ -37,7 +38,7 @@ public String getServiceName() { // --------------------------------- public PcuGroup create(PcuGroupCreationRequest req) { - val res = POST(getEndpointPcus(), JsonUtils.marshall(req), getOperationName("create")); + val res = POST(getEndpointPcus(), JsonUtils.marshall(req.withDefaultsAndValidations()), getOperationName("create")); if (HttpURLConnection.HTTP_CREATED != res.getCode()) { throw new IllegalStateException("Expected code 201 to create db but got " + res.getCode() + "body=" + res.getBody()); 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 00000000..5ca244c2 --- /dev/null +++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreateUpdateRequest.java @@ -0,0 +1,49 @@ +package com.dtsx.astra.sdk.pcu.domain; + +import com.dtsx.astra.sdk.db.domain.CloudProviderType; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@ToString +@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/PcuGroupCreationBuilder.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreationBuilder.java deleted file mode 100644 index 0fe67981..00000000 --- a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupCreationBuilder.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.dtsx.astra.sdk.pcu.domain; - -import com.dtsx.astra.sdk.db.domain.CloudProviderType; -import lombok.Setter; -import lombok.experimental.Accessors; - -@Setter -@Accessors(fluent = true, chain = true) -public class PcuGroupCreationBuilder { - protected String title; // TODO should we have an alias setter function called 'name' for consistency/ease-of-use? - protected String description; - - protected CloudProviderType cloudProvider; - protected String cloudRegion; - - protected String instanceType; - protected PcuProvisionType provisionType; - - protected int minCapacity; - protected int maxCapacity; - protected int reservedCapacity; - - public PcuGroupCreationBuilder() {} - - public PcuGroupCreationBuilder(String title) { - this.title = title; - } - - public PcuGroupCreationRequest build() { - return new PcuGroupCreationRequest(this); - } -} 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 index ecc9917e..e8591db8 100644 --- 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 @@ -1,36 +1,26 @@ package com.dtsx.astra.sdk.pcu.domain; -import com.dtsx.astra.sdk.db.domain.CloudProviderType; -import lombok.Value; +import lombok.experimental.SuperBuilder; -@Value -public class PcuGroupCreationRequest { - String title; - String description; +@SuperBuilder +public final class PcuGroupCreationRequest extends PcuGroupCreateUpdateRequest { + private String instanceType; + private PcuProvisionType provisionType; - CloudProviderType cloudProvider; - String region; + public PcuGroupCreationRequest withDefaultsAndValidations() { + if (this.provisionType == null) { + this.provisionType = PcuProvisionType.SHARED; + } - String instanceType; - PcuProvisionType provisionType; + // 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"; + } - int min; - int max; - int reserved; + if (this.reserved == null) { + this.reserved = 0; + } - public PcuGroupCreationRequest(PcuGroupCreationBuilder builder) { - this.title = builder.title; - this.description = builder.description; - this.cloudProvider = builder.cloudProvider; - this.region = builder.cloudRegion; - this.instanceType = builder.instanceType; - this.provisionType = builder.provisionType; - this.min = builder.minCapacity; - this.max = builder.maxCapacity; - this.reserved = builder.reservedCapacity; - } - - public static PcuGroupCreationBuilder builder() { - return new PcuGroupCreationBuilder(); + return this; } } 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 00000000..99045ae8 --- /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.AllArgsConstructor; +import lombok.Setter; +import lombok.With; +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.getUuid()) + .setInstanceType(base.getInstanceType()) + .setProvisionType(base.getProvisionType()); + } + + @Setter + @Accessors(chain = true) + public static class InternalRep extends PcuGroupUpdateRequest { + private String pcuGroupUUID; + private String instanceType; + private PcuProvisionType provisionType; + + protected InternalRep(PcuGroupUpdateRequestBuilder b) { + super(b); + } + } +} From cec64a87aa177e83f3f9612358bcfbc35ea98744 Mon Sep 17 00:00:00 2001 From: toptobes Date: Tue, 2 Dec 2025 00:29:13 -0600 Subject: [PATCH 4/4] work --- astra-db-java/pom.xml | 1 + .../com/dtsx/astra/sdk/AstraOpsClient.java | 2 +- .../PcuGroupDatacenterAssociationsClient.java | 49 ++++++------------- .../dtsx/astra/sdk/pcu/PcuGroupOpsClient.java | 17 +++++-- .../dtsx/astra/sdk/pcu/PcuGroupsClient.java | 33 +++++++------ .../dtsx/astra/sdk/pcu/domain/PcuGroup.java | 8 +-- .../domain/PcuGroupCreateUpdateRequest.java | 2 - .../pcu/domain/PcuGroupCreationRequest.java | 4 ++ ...ava => PcuGroupDatacenterAssociation.java} | 2 +- .../sdk/pcu/domain/PcuGroupStatusType.java | 4 +- .../sdk/pcu/domain/PcuGroupUpdateRequest.java | 6 +-- .../com/dtsx/astra/sdk/pcu/ParkingTest.java | 10 ++++ 12 files changed, 72 insertions(+), 66 deletions(-) rename astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/{PcuGroupDbAssociation.java => PcuGroupDatacenterAssociation.java} (87%) create mode 100644 astra-sdk-devops/src/test/java/com/dtsx/astra/sdk/pcu/ParkingTest.java diff --git a/astra-db-java/pom.xml b/astra-db-java/pom.xml index a69c2a4b..c903a4a7 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 861b7d29..ec7202ab 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 @@ -177,7 +177,7 @@ public TokensClient tokens() { * @return * pcu groups client */ - public PcuGroupsClient pcuGroups() { + 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/pcu/PcuGroupDatacenterAssociationsClient.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/PcuGroupDatacenterAssociationsClient.java index c03dfd8a..6bc2a417 100644 --- 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 @@ -1,11 +1,9 @@ package com.dtsx.astra.sdk.pcu; -import com.dtsx.astra.sdk.AbstractApiClient; -import com.dtsx.astra.sdk.db.domain.Datacenter; -import com.dtsx.astra.sdk.pcu.domain.PcuGroup; -import com.dtsx.astra.sdk.pcu.domain.PcuGroupDbAssociation; +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; @@ -18,7 +16,7 @@ @Slf4j public class PcuGroupDatacenterAssociationsClient extends AbstractApiClient { - private static final TypeReference> PCU_GROUP_DB_ASSOCIATIONS = + private static final TypeReference> PCU_GROUP_DB_ASSOCIATIONS = new TypeReference<>() {}; @Getter @@ -49,11 +47,7 @@ public boolean exist(@NonNull String datacenterId) { .anyMatch((assoc) -> assoc.getDatacenterUUID().equals(datacenterId)); } - public boolean exist(@NonNull Datacenter datacenter) { - return exist(datacenter.getId()); - } - - public PcuGroupDbAssociation findByDatacenterId(@NonNull String datacenterId) { + public PcuGroupDatacenterAssociation findByDatacenterId(@NonNull String datacenterId) { Assert.isDatacenterID(datacenterId, "datacenter id"); return findAll() @@ -62,42 +56,30 @@ public PcuGroupDbAssociation findByDatacenterId(@NonNull String datacenterId) { .orElseThrow(() -> new PcuGroupDbAssociationNotFound(pcuGroupId, datacenterId)); } - public PcuGroupDbAssociation findByDatacenter(@NonNull Datacenter datacenter) { - return findByDatacenterId(datacenter.getId()); - } - - public Stream findAll() { + public Stream findAll() { val res = GET(getEndpointPcuAssociations() + "/" + pcuGroupId, getOperationName("findAll")); - return unmarshallOrThrow(res, PCU_GROUP_DB_ASSOCIATIONS, 200, "get pcu group db associations").stream(); + return unmarshallOrThrow(res, PCU_GROUP_DB_ASSOCIATIONS, "get pcu group db associations").stream(); } - public PcuGroupDbAssociation associate(@NonNull String datacenterId) { + public PcuGroupDatacenterAssociation associate(@NonNull String datacenterId) { Assert.isDatacenterID(datacenterId, "datacenter id"); val res = POST(getEndpointPcuAssociations() + "/" + pcuGroupId + "/" + datacenterId, getOperationName("associate")); - return unmarshallOrThrow(res, new TypeReference<>() {}, 201, "associate db to pcu group"); - } - - public PcuGroupDbAssociation associate(@NonNull Datacenter datacenter) { - return associate(datacenter.getId()); + return unmarshallOrThrow(res, new TypeReference>() {}, "associate db to pcu group").get(0); } private record TransferReqBody(String fromPCUGroupUUID, String toPCUGroupUUID, String datacenterUUID) {} - public PcuGroupDbAssociation transfer(@NonNull String toPcuGroup, @NonNull String datacenterId) { + 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<>() {}, 200, "transfer db to pcu group"); - } - - public PcuGroupDbAssociation transfer(@NonNull PcuGroup toPcuGroup, @NonNull Datacenter datacenter) { - return transfer(toPcuGroup.getUuid(), datacenter.getId()); + return unmarshallOrThrow(res, new TypeReference>() {}, "transfer db to pcu group").get(0); } public void dissociate(@NonNull String datacenterId) { @@ -105,10 +87,6 @@ public void dissociate(@NonNull String datacenterId) { DELETE(getEndpointPcuAssociations() + "/" + pcuGroupId + "/" + datacenterId, getOperationName("dissociate")); } - public void dissociate(@NonNull Datacenter datacenter) { - dissociate(datacenter.getId()); - } - // --------------------------------- // ---- Utilities ---- // --------------------------------- @@ -117,8 +95,9 @@ public String getEndpointPcuAssociations() { return ApiLocator.getApiDevopsEndpoint(environment) + "/pcus/association"; } - private T unmarshallOrThrow(ApiResponseHttp res, TypeReference clazz, int expectedCode, String operation) { + 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; @@ -128,12 +107,12 @@ private T unmarshallOrThrow(ApiResponseHttp res, TypeReference clazz, int } catch (Exception ignored) {} if (responseError != null && responseError.getErrors() != null && !responseError.getErrors().isEmpty()) { - if (responseError.getErrors().getFirst().getId() == 2000367) { + if (responseError.getErrors().get(0).getId() == 2000367) { throw PcuGroupNotFoundException.forId(pcuGroupId); } } - throw new IllegalStateException("Expected code " + expectedCode + " to " + operation + " but got " + res.getCode() + "body=" + res.getBody()); + 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 index 2e9757ba..4b262e49 100644 --- 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 @@ -1,10 +1,10 @@ package com.dtsx.astra.sdk.pcu; -import com.dtsx.astra.sdk.AbstractApiClient; 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; @@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j; import lombok.val; +import java.util.List; import java.util.Optional; @Slf4j @@ -46,7 +47,7 @@ public Optional find() { } public PcuGroup get() { - return new PcuGroupsClient(token, environment).findById(pcuGroupId); + return new PcuGroupsClient(token, environment).findById(pcuGroupId).orElseThrow(() -> PcuGroupNotFoundException.forId(pcuGroupId)); } public boolean exist() { @@ -67,7 +68,7 @@ public boolean isCreatedOrActive() { public void update(PcuGroupUpdateRequest req) { val base = get(); - PATCH(getEndpointPcus() + "/" + pcuGroupId, JsonUtils.marshall(req.withDefaultsAndValidations(base)), getOperationName("update")); + PUT(getEndpointPcus(), JsonUtils.marshall(List.of(req.withDefaultsAndValidations(base))), getOperationName("update")); } // --------------------------------- @@ -76,12 +77,18 @@ public void update(PcuGroupUpdateRequest req) { public void park() { val res = POST(getEndpointPcus() + "/park/" + pcuGroupId, getOperationName("park")); - assertHttpCodeAccepted(res, "park", pcuGroupId); + + 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")); - assertHttpCodeAccepted(res, "unpark", pcuGroupId); + + if (res.getCode() >= 300) { + throw new IllegalStateException("Expected code 200 to unpark pcu group but got " + res.getCode() + "body=" + res.getBody()); + } } public void delete() { 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 index c14c80d1..f3323808 100644 --- 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 @@ -1,11 +1,10 @@ package com.dtsx.astra.sdk.pcu; -import com.dtsx.astra.sdk.AbstractApiClient; import com.dtsx.astra.sdk.pcu.domain.PcuGroup; -import com.dtsx.astra.sdk.pcu.domain.PcuGroupCreateUpdateRequest; 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; @@ -13,6 +12,7 @@ import java.net.HttpURLConnection; import java.util.List; +import java.util.Optional; import java.util.stream.Stream; @Slf4j @@ -38,24 +38,29 @@ public String getServiceName() { // --------------------------------- public PcuGroup create(PcuGroupCreationRequest req) { - val res = POST(getEndpointPcus(), JsonUtils.marshall(req.withDefaultsAndValidations()), getOperationName("create")); + 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 db but got " + res.getCode() + "body=" + res.getBody()); + 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).getFirst(); + 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 PcuGroup findById(String id) { - return findAllImpl(List.of(id), "id", (_e) -> PcuGroupNotFoundException.forId(id)).findFirst().orElseThrow(); // it can never throw unless the API itself is broken + public Stream findByTitle(String title) { + return findAll().filter(pg -> title.equals(pg.getTitle())); // order is important here since pg.title is nullable } - public PcuGroup findByTitle(String title) { - return findAll() - .filter(pg -> pg.getTitle().equals(title)) - .findFirst() - .orElseThrow(() -> PcuGroupNotFoundException.forTitle(title)); + public Optional findFirstByTitle(String title) { + return findByTitle(title).findFirst(); } public Stream findAll() { @@ -63,7 +68,7 @@ public Stream findAll() { } public Stream findAll(List ids) { - return findAllImpl(ids, "ids[%d]", (e) -> new PcuGroupsNotFoundException(e.getErrors().getFirst().getMessage())); + return findAllImpl(ids, "ids[%d]", (e) -> new PcuGroupsNotFoundException(e.getErrors().get(0).getMessage())); } protected interface FindAll404Handler { @@ -101,7 +106,7 @@ protected Stream findAllImpl(List ids, String validationErrorF } if (responseError != null && responseError.getErrors() != null && !responseError.getErrors().isEmpty()) { - if (responseError.getErrors().getFirst().getId() == 340018) { // TODO is this the right error code? also why does find all get special treatment for auth errors? + 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); } } 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 index e68d9a4a..66c848da 100644 --- 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 @@ -2,6 +2,7 @@ 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; @@ -9,11 +10,12 @@ @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class PcuGroup { - private String uuid; // TODO should we call it 'id' for consistency? w/ @JsonProperty("uuid") + should this be a UUID? + @JsonProperty("uuid") + private String id; private String orgId; private String title; - private String description; // TODO should we use Optional<...>s? + private String description; private CloudProviderType cloudProvider; private String region; @@ -25,7 +27,7 @@ public class PcuGroup { private int max; private int reserved; - private String createdAt; // TODO should these be DateTimes/Instants instead? + private String createdAt; private String updatedAt; private String createdBy; private String updatedBy; 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 index 5ca244c2..c5d742ad 100644 --- 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 @@ -3,12 +3,10 @@ import com.dtsx.astra.sdk.db.domain.CloudProviderType; import lombok.Getter; import lombok.Setter; -import lombok.ToString; import lombok.experimental.SuperBuilder; @Getter @Setter -@ToString @SuperBuilder public sealed abstract class PcuGroupCreateUpdateRequest permits PcuGroupCreationRequest, PcuGroupUpdateRequest { protected String title; 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 index e8591db8..5a50bf61 100644 --- 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 @@ -1,7 +1,11 @@ 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; diff --git a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDbAssociation.java b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDatacenterAssociation.java similarity index 87% rename from astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDbAssociation.java rename to astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDatacenterAssociation.java index 7697c494..48574f5d 100644 --- a/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDbAssociation.java +++ b/astra-sdk-devops/src/main/java/com/dtsx/astra/sdk/pcu/domain/PcuGroupDatacenterAssociation.java @@ -6,7 +6,7 @@ // TODO add the rest of the fields once the PCU team is clear about what is going on @Data @JsonIgnoreProperties(ignoreUnknown = true) -public class PcuGroupDbAssociation { +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 index 6ff009dc..45b4d641 100644 --- 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 @@ -1,6 +1,5 @@ package com.dtsx.astra.sdk.pcu.domain; -// TODO should this be a sealed interface with an 'OTHER()' (or UNKNOWN) implementation to future-proof? public enum PcuGroupStatusType { CREATED, PLACING, @@ -8,5 +7,6 @@ public enum PcuGroupStatusType { ACTIVE, PARKED, PARKING, - UNPARKING + 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 index 99045ae8..02b6d424 100644 --- 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 @@ -1,8 +1,7 @@ package com.dtsx.astra.sdk.pcu.domain; -import lombok.AllArgsConstructor; +import lombok.Getter; import lombok.Setter; -import lombok.With; import lombok.experimental.Accessors; import lombok.experimental.SuperBuilder; import lombok.val; @@ -25,12 +24,13 @@ public PcuGroupCreateUpdateRequest withDefaultsAndValidations(PcuGroup base) { internalRep.validate(); return internalRep - .setPcuGroupUUID(base.getUuid()) + .setPcuGroupUUID(base.getId()) .setInstanceType(base.getInstanceType()) .setProvisionType(base.getProvisionType()); } @Setter + @Getter @Accessors(chain = true) public static class InternalRep extends PcuGroupUpdateRequest { private String pcuGroupUUID; 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 00000000..0d1b53e4 --- /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 { + +}