diff --git a/.gitignore b/.gitignore index ef0e747..4fd7653 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ target .jqwik-database dependency-reduced-pom.xml pom.xml.versionsBackup +.run diff --git a/README-CHANGES.xml b/README-CHANGES.xml index 5333e59..a068f97 100644 --- a/README-CHANGES.xml +++ b/README-CHANGES.xml @@ -67,7 +67,7 @@ - + @@ -101,11 +101,12 @@ - + + diff --git a/com.io7m.certusine.tests/src/main/java/com/io7m/certusine/tests/CSVultrDNSTests.java b/com.io7m.certusine.tests/src/main/java/com/io7m/certusine/tests/CSVultrDNSTests.java index 288a0d5..f52ed94 100644 --- a/com.io7m.certusine.tests/src/main/java/com/io7m/certusine/tests/CSVultrDNSTests.java +++ b/com.io7m.certusine.tests/src/main/java/com/io7m/certusine/tests/CSVultrDNSTests.java @@ -21,16 +21,20 @@ import com.io7m.certusine.api.CSDNSRecordNameType.CSDNSRecordNameRelative; import com.io7m.certusine.vultr.CSVultrDNSConfigurators; import com.io7m.jlexing.core.LexicalPositions; +import com.io7m.quixote.core.QWebServerType; +import com.io7m.quixote.core.QWebServers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Map; import static com.io7m.certusine.api.CSTelemetryNoOp.noop; import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public final class CSVultrDNSTests @@ -38,6 +42,8 @@ public final class CSVultrDNSTests private CSFakeVultrServer fakeServer; private CSVultrDNSConfigurators provider; private Path directory; + private QWebServers servers; + private QWebServerType server; @BeforeEach public void setup() @@ -49,6 +55,10 @@ public void setup() CSFakeVultrServer.create(20000); this.provider = new CSVultrDNSConfigurators(); + this.servers = + new QWebServers(); + this.server = + this.servers.create(20001); } @AfterEach @@ -58,6 +68,7 @@ public void tearDown() CSTestDirectories.deleteDirectory(this.directory); this.fakeServer.close(); + this.server.close(); } /** @@ -169,4 +180,207 @@ public void testVultrMissingRequired1() ); }); } + + /** + * If the server returns all the right responses, the execution succeeds. + * + * @throws Exception On errors + */ + + @Test + public void testVultrCreateDelete() + throws Exception + { + final var v = + this.provider.create( + new CSConfigurationParameters( + this.directory, + LexicalPositions.zero(), + Map.ofEntries( + entry("api-key", "abcd"), + entry("api-base", "http://localhost:20001/"), + entry("domain", "example.com") + ) + ) + ); + + this.server.addResponse() + .forPath("/domains/.*") + .withFixedText(text("vultr-dns-records-0.json")); + + this.server.addResponse() + .forPath("/domains/.*") + .withFixedText(text("vultr-dns-records-1.json")); + + this.server.addResponse() + .forPath("/domains/.*") + .forMethod("DELETE") + .withStatus(204) + .withFixedText(""); + + this.server.addResponse() + .forPath("/domains/.*") + .forMethod("DELETE") + .withStatus(204) + .withFixedText(""); + + v.deleteTXTRecord( + noop(), + new CSDNSRecordNameRelative("z"), "200.200.200.201" + ); + + { + final var r = + this.server.requestsReceived().get(0); + assertEquals("/domains/example.com/records", r.path()); + } + + { + final var r = + this.server.requestsReceived().get(1); + assertEquals("/domains/example.com/records", r.path()); + } + + { + final var r = + this.server.requestsReceived().get(2); + assertEquals("/domains/example.com/records/ff690447-7b0d-408b-a408-fd2410281a60", r.path()); + } + + { + final var r = + this.server.requestsReceived().get(3); + assertEquals("/domains/example.com/records/68440bec-b17f-4532-a58b-d2b6a7e3b69c", r.path()); + } + } + + /** + * If the server returns errors, the execution fails. + * + * @throws Exception On errors + */ + + @Test + public void testVultrCreateDeleteFails0() + throws Exception + { + final var v = + this.provider.create( + new CSConfigurationParameters( + this.directory, + LexicalPositions.zero(), + Map.ofEntries( + entry("api-key", "abcd"), + entry("api-base", "http://localhost:20001/"), + entry("domain", "example.com") + ) + ) + ); + + this.server.addResponse() + .forPath("/domains/.*") + .withFixedText(text("vultr-dns-records-0.json")); + + this.server.addResponse() + .forPath("/domains/.*") + .withFixedText(text("vultr-dns-records-1.json")); + + this.server.addResponse() + .forPath("/domains/.*") + .forMethod("DELETE") + .withStatus(500) + .withFixedText(""); + + assertThrows(IOException.class, () -> { + v.deleteTXTRecord( + noop(), + new CSDNSRecordNameRelative("z"), "200.200.200.201" + ); + }); + + { + final var r = + this.server.requestsReceived().get(0); + assertEquals("/domains/example.com/records", r.path()); + } + + { + final var r = + this.server.requestsReceived().get(1); + assertEquals("/domains/example.com/records", r.path()); + } + + { + final var r = + this.server.requestsReceived().get(2); + assertEquals("/domains/example.com/records/ff690447-7b0d-408b-a408-fd2410281a60", r.path()); + } + } + + /** + * If the server returns errors, the execution fails. + * + * @throws Exception On errors + */ + + @Test + public void testVultrCreateDeleteFails1() + throws Exception + { + final var v = + this.provider.create( + new CSConfigurationParameters( + this.directory, + LexicalPositions.zero(), + Map.ofEntries( + entry("api-key", "abcd"), + entry("api-base", "http://localhost:20001/"), + entry("domain", "example.com") + ) + ) + ); + + this.server.addResponse() + .forPath("/domains/.*") + .withFixedText(text("vultr-dns-records-0.json")); + + this.server.addResponse() + .forPath("/domains/.*") + .withStatus(500) + .withFixedText(text("vultr-dns-records-1.json")); + + assertThrows(IOException.class, () -> { + v.deleteTXTRecord( + noop(), + new CSDNSRecordNameRelative("z"), "200.200.200.201" + ); + }); + + { + final var r = + this.server.requestsReceived().get(0); + assertEquals("/domains/example.com/records", r.path()); + } + + { + final var r = + this.server.requestsReceived().get(1); + assertEquals("/domains/example.com/records", r.path()); + } + } + + private static String text( + final String name) + throws IOException + { + try (var stream = + CSVultrDNSTests.class.getResourceAsStream( + "/com/io7m/certusine/tests/%s".formatted(name) + )) { + return new String( + stream.readAllBytes(), + StandardCharsets.UTF_8 + ); + } + } } diff --git a/com.io7m.certusine.tests/src/main/java/module-info.java b/com.io7m.certusine.tests/src/main/java/module-info.java index 2bc4481..dbac427 100644 --- a/com.io7m.certusine.tests/src/main/java/module-info.java +++ b/com.io7m.certusine.tests/src/main/java/module-info.java @@ -46,6 +46,7 @@ requires org.mockito; requires org.shredzone.acme4j; requires org.slf4j; + requires org.dnsjava; exports com.io7m.certusine.tests; } diff --git a/com.io7m.certusine.tests/src/main/resources/com/io7m/certusine/tests/vultr-dns-records-0.json b/com.io7m.certusine.tests/src/main/resources/com/io7m/certusine/tests/vultr-dns-records-0.json new file mode 100644 index 0000000..5ea3a9f --- /dev/null +++ b/com.io7m.certusine.tests/src/main/resources/com/io7m/certusine/tests/vultr-dns-records-0.json @@ -0,0 +1,51 @@ +{ + "records": [ + { + "id": "e9358824-b050-4514-a1ec-39afcff829de", + "type": "A", + "name": "x", + "data": "200.200.200.200", + "priority": 0, + "ttl": 300 + }, + { + "id": "b542b76c-b3c3-483b-8f34-842b854b55d7", + "type": "TXT", + "name": "y", + "data": "\"200.200.200.200\"", + "priority": 0, + "ttl": 300 + }, + { + "id": "c473afb8-6d68-4205-a8ca-2ca81a525c04", + "type": "TXT", + "name": "y", + "data": "\"200.200.200.201\"", + "priority": 0, + "ttl": 300 + }, + { + "id": "0a818032-c985-482c-b62b-ece6f622b956", + "type": "TXT", + "name": "z", + "data": "\"200.200.200.200\"", + "priority": 0, + "ttl": 300 + }, + { + "id": "ff690447-7b0d-408b-a408-fd2410281a60", + "type": "TXT", + "name": "z", + "data": "\"200.200.200.201\"", + "priority": 0, + "ttl": 300 + } + ], + "meta": { + "total": 1000, + "links": { + "next": "Token", + "prev": "" + } + } +} diff --git a/com.io7m.certusine.tests/src/main/resources/com/io7m/certusine/tests/vultr-dns-records-1.json b/com.io7m.certusine.tests/src/main/resources/com/io7m/certusine/tests/vultr-dns-records-1.json new file mode 100644 index 0000000..65a41d1 --- /dev/null +++ b/com.io7m.certusine.tests/src/main/resources/com/io7m/certusine/tests/vultr-dns-records-1.json @@ -0,0 +1,27 @@ +{ + "records": [ + { + "id": "34389b43-b8b9-4d9d-a380-449f05c501d9", + "type": "A", + "name": "x", + "data": "200.200.200.200", + "priority": 0, + "ttl": 300 + }, + { + "id": "68440bec-b17f-4532-a58b-d2b6a7e3b69c", + "type": "TXT", + "name": "z", + "data": "\"200.200.200.201\"", + "priority": 0, + "ttl": 300 + } + ], + "meta": { + "total": 1000, + "links": { + "next": "", + "prev": "" + } + } +} diff --git a/com.io7m.certusine.vultr/pom.xml b/com.io7m.certusine.vultr/pom.xml index 66fae2f..1080f9a 100644 --- a/com.io7m.certusine.vultr/pom.xml +++ b/com.io7m.certusine.vultr/pom.xml @@ -33,6 +33,19 @@ org.slf4j slf4j-api + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + com.io7m.dixmont + com.io7m.dixmont.core + + org.osgi org.osgi.annotation.bundle diff --git a/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrDNSConfigurator.java b/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrDNSConfigurator.java index 983f13e..22fe833 100644 --- a/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrDNSConfigurator.java +++ b/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrDNSConfigurator.java @@ -17,9 +17,13 @@ package com.io7m.certusine.vultr.internal; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleDeserializers; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.io7m.certusine.api.CSDNSConfiguratorType; import com.io7m.certusine.api.CSDNSRecordNameType; import com.io7m.certusine.api.CSTelemetryServiceType; +import com.io7m.dixmont.core.DmJsonRestrictedDeserializers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,7 +32,10 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import java.util.Optional; import static java.nio.charset.StandardCharsets.UTF_8; @@ -46,6 +53,8 @@ public final class CSVultrDNSConfigurator implements CSDNSConfiguratorType private final String apiBase; private final String apiKey; private final String domain; + private final SimpleDeserializers serializers; + private final JsonMapper mapper; /** * A Vultr DNS configurator. @@ -74,6 +83,28 @@ public CSVultrDNSConfigurator( this.client = HttpClient.newHttpClient(); + + this.serializers = + DmJsonRestrictedDeserializers.builder() + .allowClassName( + "java.util.List") + .allowClass(CSVultrDNSRecord.class) + .allowClass(CSVultrDNSResponse.class) + .allowClass(CSVultrPageLinks.class) + .allowClass(CSVultrPageMetadata.class) + .allowClass(List.class) + .allowClass(Optional.class) + .allowClass(String.class) + .allowClass(int.class) + .build(); + + this.mapper = + JsonMapper.builder() + .build(); + + final var simpleModule = new SimpleModule(); + simpleModule.setDeserializers(this.serializers); + this.mapper.registerModule(simpleModule); } @Override @@ -88,10 +119,12 @@ public void createTXTRecord( try { final var targetURI = - URI.create("%s/domains/%s/records".formatted(this.apiBase, this.domain)); + URI.create("%s/domains/%s/records".formatted( + this.apiBase, + this.domain)); LOG.debug( - "creating a TXT record {} = {} for domain {}", + "Creating a TXT record {} = {} for domain {}", recordName, recordValue, this.domain @@ -118,11 +151,11 @@ public void createTXTRecord( final var r = this.client.send(request, HttpResponse.BodyHandlers.ofString()); - LOG.debug("response: {}", r.body()); + LOG.debug("Response: {}", r.body()); if (r.statusCode() != 201) { throw new IOException( - this.strings.format("errorServer", r.statusCode()) + this.strings.format("errorDNSCreate", r.statusCode()) ); } } catch (final Exception e) { @@ -131,14 +164,130 @@ public void createTXTRecord( } } + private CSVultrDNSResponse listTXTRecordsPage( + final Optional key) + throws IOException, InterruptedException + { + final var targetURI = + key.map(cursor -> { + return URI.create( + "%s/domains/%s/records?per_page=500&cursor=%s" + .formatted(this.apiBase, this.domain, cursor) + ); + }).orElseGet(() -> { + return URI.create( + "%s/domains/%s/records?per_page=500" + .formatted(this.apiBase, this.domain) + ); + }); + + final var request = + HttpRequest.newBuilder() + .uri(targetURI) + .GET() + .header("Authorization", "Bearer " + this.apiKey) + .build(); + + final var r = + this.client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (r.statusCode() != 200) { + throw new IOException( + this.strings.format("errorDNSDelete", r.statusCode()) + ); + } + + return this.mapper.readValue(r.body(), CSVultrDNSResponse.class); + } + + private List listTXTRecords() + throws IOException, InterruptedException + { + var response = + this.listTXTRecordsPage(Optional.empty()); + + final var records = + new ArrayList<>(response.records()); + + while (true) { + final var next = response.meta().links().next(); + if (Objects.equals(next, "")) { + break; + } + response = this.listTXTRecordsPage(Optional.of(next)); + records.addAll(response.records()); + } + + return List.copyOf(records); + } + @Override public void deleteTXTRecord( final CSTelemetryServiceType telemetry, final CSDNSRecordNameType recordName, final String recordValue) + throws IOException, InterruptedException { Objects.requireNonNull(recordName, "name"); Objects.requireNonNull(recordValue, "text"); + try { + final var records = this.listTXTRecords(); + LOG.debug("Found {} records", records.size()); + + final var matchingRecords = + records.stream() + .filter(r -> isMatchingTXTRecord(r, recordName, recordValue)) + .toList(); + + LOG.debug("Found {} matching TXT records", matchingRecords.size()); + + for (final var record : matchingRecords) { + final var targetURI = + URI.create( + "%s/domains/%s/records/%s" + .formatted(this.apiBase, this.domain, record.id()) + ); + + LOG.debug("DELETE {}", targetURI); + + final var request = + HttpRequest.newBuilder() + .uri(targetURI) + .DELETE() + .header("Authorization", "Bearer " + this.apiKey) + .build(); + + final var r = + this.client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (r.statusCode() != 204) { + throw new IOException( + this.strings.format("errorDNSDelete", r.statusCode()) + ); + } + } + } catch (final Exception e) { + CSTelemetryServiceType.recordExceptionAndSetError(e); + throw e; + } + } + + private static boolean isMatchingTXTRecord( + final CSVultrDNSRecord r, + final CSDNSRecordNameType recordName, + final String recordValue) + { + if (!Objects.equals(r.type(), "TXT")) { + return false; + } + + if (!Objects.equals(r.name(), recordName.name())) { + return false; + } + + final var valRecord = r.data(); + final var valQuoted = "\"%s\"".formatted(recordValue); + return Objects.equals(valRecord, valQuoted); } } diff --git a/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrDNSRecord.java b/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrDNSRecord.java new file mode 100644 index 0000000..cb69dd6 --- /dev/null +++ b/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrDNSRecord.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.certusine.vultr.internal; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.Objects; + +/** + * The Vultr DNS record. + * + * @param id The ID + * @param type The type + * @param name The name + * @param data The data + * @param priority The priority + * @param ttl The time-to-live + * + * @see "https://www.vultr.com/api/#tag/dns/operation/list-dns-domain-records" + */ + +@JsonDeserialize +public record CSVultrDNSRecord( + @JsonProperty(value = "id") + String id, + @JsonProperty(value = "type") + String type, + @JsonProperty(value = "name") + String name, + @JsonProperty(value = "data") + String data, + @JsonProperty(value = "priority") + int priority, + @JsonProperty(value = "ttl") + int ttl) +{ + /** + * The Vultr DNS record. + * + * @param id The ID + * @param type The type + * @param name The name + * @param data The data + * @param priority The priority + * @param ttl The time-to-live + * + * @see "https://www.vultr.com/api/#tag/dns/operation/list-dns-domain-records" + */ + + public CSVultrDNSRecord + { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(type, "type"); + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(data, "data"); + } +} diff --git a/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrDNSResponse.java b/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrDNSResponse.java new file mode 100644 index 0000000..0926bad --- /dev/null +++ b/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrDNSResponse.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.certusine.vultr.internal; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.List; +import java.util.Objects; + +/** + * The Vultr DNS response. + * + * @param records The DNS records + * @param meta Paging metadata + * + * @see "https://www.vultr.com/api/#tag/dns/operation/list-dns-domain-records" + */ + +@JsonDeserialize +public record CSVultrDNSResponse( + @JsonProperty(value = "records") + List records, + @JsonProperty(value = "meta") + CSVultrPageMetadata meta) +{ + /** + * The Vultr DNS response. + * + * @param records The DNS records + * @param meta Paging metadata + * + * @see "https://www.vultr.com/api/#tag/dns/operation/list-dns-domain-records" + */ + + public CSVultrDNSResponse + { + Objects.requireNonNull(records, "records"); + Objects.requireNonNull(meta, "meta"); + } +} diff --git a/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrPageLinks.java b/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrPageLinks.java new file mode 100644 index 0000000..e728a05 --- /dev/null +++ b/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrPageLinks.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.certusine.vultr.internal; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.Objects; + +/** + * The Vultr page links. + * + * @param next The next token + * @param prev The previous token + * + * @see "https://www.vultr.com/api/#section/Introduction/Meta-and-Pagination" + */ + +@JsonDeserialize +public record CSVultrPageLinks( + @JsonProperty(value = "next") + String next, + @JsonProperty(value = "prev") + String prev) +{ + /** + * The Vultr page links. + * + * @param next The next token + * @param prev The previous token + * + * @see "https://www.vultr.com/api/#section/Introduction/Meta-and-Pagination" + */ + + public CSVultrPageLinks + { + Objects.requireNonNull(next, "next"); + Objects.requireNonNull(prev, "prev"); + } +} diff --git a/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrPageMetadata.java b/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrPageMetadata.java new file mode 100644 index 0000000..828a520 --- /dev/null +++ b/com.io7m.certusine.vultr/src/main/java/com/io7m/certusine/vultr/internal/CSVultrPageMetadata.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2024 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.certusine.vultr.internal; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.Objects; + +/** + * The Vultr page metadata. + * + * @param total The total number of items + * @param links The page links + * + * @see "https://www.vultr.com/api/#section/Introduction/Meta-and-Pagination" + */ + +@JsonDeserialize +public record CSVultrPageMetadata( + @JsonProperty(value = "total") + int total, + @JsonProperty(value = "links") + CSVultrPageLinks links) +{ + /** + * The Vultr page metadata. + * + * @param total The total number of items + * @param links The page links + * + * @see "https://www.vultr.com/api/#section/Introduction/Meta-and-Pagination" + */ + + public CSVultrPageMetadata + { + Objects.requireNonNull(links, "links"); + } +} diff --git a/com.io7m.certusine.vultr/src/main/java/module-info.java b/com.io7m.certusine.vultr/src/main/java/module-info.java index 157d536..0a68087 100644 --- a/com.io7m.certusine.vultr/src/main/java/module-info.java +++ b/com.io7m.certusine.vultr/src/main/java/module-info.java @@ -28,6 +28,8 @@ requires transitive com.io7m.certusine.api; + requires com.fasterxml.jackson.databind; + requires com.io7m.dixmont.core; requires com.io7m.jxtrand.vanilla; requires java.net.http; requires org.slf4j; @@ -36,7 +38,7 @@ with CSVultrDNSConfigurators; opens com.io7m.certusine.vultr.internal - to com.io7m.jxtrand.vanilla; + to com.io7m.jxtrand.vanilla, com.fasterxml.jackson.databind; exports com.io7m.certusine.vultr.internal to com.io7m.certusine.tests; diff --git a/com.io7m.certusine.vultr/src/main/resources/com/io7m/certusine/vultr/internal/Messages.xml b/com.io7m.certusine.vultr/src/main/resources/com/io7m/certusine/vultr/internal/Messages.xml index c75b8f4..0893976 100644 --- a/com.io7m.certusine.vultr/src/main/resources/com/io7m/certusine/vultr/internal/Messages.xml +++ b/com.io7m.certusine.vultr/src/main/resources/com/io7m/certusine/vultr/internal/Messages.xml @@ -4,7 +4,8 @@ DNS configurator is misconfigured. - Could not create a DNS record: HTTP status: {0} + Could not create a DNS record: HTTP status: {0} + Could not delete a DNS record: HTTP status: {0} The Vultr API key. The domain name. The Vultr API base address.