Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7617e1f87c | |||
| 299a7704c2 | |||
| a52602186d | |||
| 1b00488b7c | |||
| dfbfa4d2a7 | |||
| 5dd9a1e346 |
@@ -29,6 +29,10 @@ public class EfsException extends AwsException {
|
|||||||
return new EfsException(Status.NOT_FOUND, "MountTargetNotFound", "Mount target " + mountTargetId + " does not exist.");
|
return new EfsException(Status.NOT_FOUND, "MountTargetNotFound", "Mount target " + mountTargetId + " does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static EfsException subnetNotFound(String subnetId) {
|
||||||
|
return new EfsException(Status.BAD_REQUEST, "SubnetNotFound", "The subnet ID '" + subnetId + "' does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
public static EfsException mountTargetConflict(String message) {
|
public static EfsException mountTargetConflict(String message) {
|
||||||
return new EfsException(Status.CONFLICT, "MountTargetConflict", message);
|
return new EfsException(Status.CONFLICT, "MountTargetConflict", message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package io.github.hectorvent.floci.services.efs;
|
|||||||
|
|
||||||
import io.github.hectorvent.floci.core.storage.StorageBackend;
|
import io.github.hectorvent.floci.core.storage.StorageBackend;
|
||||||
import io.github.hectorvent.floci.core.storage.StorageFactory;
|
import io.github.hectorvent.floci.core.storage.StorageFactory;
|
||||||
|
import io.github.hectorvent.floci.core.common.AwsException;
|
||||||
import io.github.hectorvent.floci.core.common.Resettable;
|
import io.github.hectorvent.floci.core.common.Resettable;
|
||||||
|
import io.github.hectorvent.floci.services.ec2.Ec2Service;
|
||||||
|
import io.github.hectorvent.floci.services.ec2.model.Subnet;
|
||||||
import io.github.hectorvent.floci.services.efs.model.*;
|
import io.github.hectorvent.floci.services.efs.model.*;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
@@ -33,13 +36,16 @@ public class EfsService implements Resettable {
|
|||||||
private final ConcurrentHashMap<String, Object> syncLocks = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, Object> syncLocks = new ConcurrentHashMap<>();
|
||||||
private final RegionResolver regionResolver;
|
private final RegionResolver regionResolver;
|
||||||
|
|
||||||
|
private final Ec2Service ec2Service;
|
||||||
|
|
||||||
private Object lockFor(String key) {
|
private Object lockFor(String key) {
|
||||||
return syncLocks.computeIfAbsent(key, k -> new Object());
|
return syncLocks.computeIfAbsent(key, k -> new Object());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public EfsService(StorageFactory storageFactory, RegionResolver regionResolver) {
|
public EfsService(StorageFactory storageFactory, RegionResolver regionResolver, Ec2Service ec2Service) {
|
||||||
this.regionResolver = regionResolver;
|
this.regionResolver = regionResolver;
|
||||||
|
this.ec2Service = ec2Service;
|
||||||
this.fileSystemStore = storageFactory.create("efs", "efs-filesystems.json",
|
this.fileSystemStore = storageFactory.create("efs", "efs-filesystems.json",
|
||||||
new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String, FileSystem>>() {});
|
new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String, FileSystem>>() {});
|
||||||
this.mountTargetStore = storageFactory.create("efs", "efs-mounttargets.json",
|
this.mountTargetStore = storageFactory.create("efs", "efs-mounttargets.json",
|
||||||
@@ -361,14 +367,24 @@ public class EfsService implements Resettable {
|
|||||||
public MountTarget createMountTarget(CreateMountTargetRequest request, String region) {
|
public MountTarget createMountTarget(CreateMountTargetRequest request, String region) {
|
||||||
synchronized (lockFor(regionKey(region, request.getFileSystemId()))) {
|
synchronized (lockFor(regionKey(region, request.getFileSystemId()))) {
|
||||||
FileSystem fs = getFileSystem(region, request.getFileSystemId());
|
FileSystem fs = getFileSystem(region, request.getFileSystemId());
|
||||||
|
Subnet subnet = requireSubnet(region, request.getSubnetId());
|
||||||
|
|
||||||
for (MountTarget existing : mountTargetStore.scan(k -> k.startsWith(region + "::"))) {
|
List<MountTarget> regionMountTargets = mountTargetStore.scan(k -> k.startsWith(region + "::"));
|
||||||
|
for (MountTarget existing : regionMountTargets) {
|
||||||
if (existing.getFileSystemId().equals(request.getFileSystemId())) {
|
if (existing.getFileSystemId().equals(request.getFileSystemId())) {
|
||||||
if (existing.getSubnetId().equals(request.getSubnetId())) {
|
if (existing.getSubnetId().equals(request.getSubnetId())) {
|
||||||
throw EfsException.mountTargetConflict("Mount target already exists in this subnet");
|
throw EfsException.mountTargetConflict("Mount target already exists in this subnet");
|
||||||
}
|
}
|
||||||
|
// AWS allows one mount target per Availability Zone, all in one VPC.
|
||||||
|
if (subnet.getAvailabilityZone().equals(existing.getAvailabilityZoneName())) {
|
||||||
|
throw EfsException.mountTargetConflict("Mount target already exists in this AZ");
|
||||||
|
}
|
||||||
|
if (!subnet.getVpcId().equals(existing.getVpcId())) {
|
||||||
|
throw EfsException.mountTargetConflict("Mount targets of a file system must be in the same VPC");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
long inSubnet = regionMountTargets.stream().filter(existing -> request.getSubnetId().equals(existing.getSubnetId())).count();
|
||||||
|
|
||||||
String mtId = "fsmt-" + UUID.randomUUID().toString().replace("-", "").substring(0, 17);
|
String mtId = "fsmt-" + UUID.randomUUID().toString().replace("-", "").substring(0, 17);
|
||||||
|
|
||||||
@@ -376,11 +392,11 @@ public class EfsService implements Resettable {
|
|||||||
mt.setMountTargetId(mtId);
|
mt.setMountTargetId(mtId);
|
||||||
mt.setFileSystemId(request.getFileSystemId());
|
mt.setFileSystemId(request.getFileSystemId());
|
||||||
mt.setSubnetId(request.getSubnetId());
|
mt.setSubnetId(request.getSubnetId());
|
||||||
mt.setIpAddress(request.getIpAddress() != null ? request.getIpAddress() : "10.0.0.10");
|
mt.setIpAddress(request.getIpAddress() != null ? request.getIpAddress() : hostAddress(subnet.getCidrBlock(), inSubnet));
|
||||||
mt.setLifeCycleState(LifeCycleState.available);
|
mt.setLifeCycleState(LifeCycleState.available);
|
||||||
mt.setVpcId("vpc-12345678");
|
mt.setVpcId(subnet.getVpcId());
|
||||||
mt.setAvailabilityZoneName(region + "a");
|
mt.setAvailabilityZoneName(subnet.getAvailabilityZone());
|
||||||
mt.setAvailabilityZoneId(generateAzId(region + "a"));
|
mt.setAvailabilityZoneId(generateAzId(subnet.getAvailabilityZone()));
|
||||||
mt.setNetworkInterfaceId("eni-" + UUID.randomUUID().toString().replace("-", "").substring(0, 17));
|
mt.setNetworkInterfaceId("eni-" + UUID.randomUUID().toString().replace("-", "").substring(0, 17));
|
||||||
if (request.getSecurityGroups() != null) {
|
if (request.getSecurityGroups() != null) {
|
||||||
mt.setSecurityGroups(new ArrayList<>(request.getSecurityGroups()));
|
mt.setSecurityGroups(new ArrayList<>(request.getSecurityGroups()));
|
||||||
@@ -613,6 +629,32 @@ public class EfsService implements Resettable {
|
|||||||
return region + "::" + id;
|
return region + "::" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Subnet requireSubnet(String region, String subnetId) {
|
||||||
|
if (subnetId == null || subnetId.isBlank()) {
|
||||||
|
throw EfsException.badRequest("SubnetId is required.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ec2Service.requireSubnet(region, subnetId);
|
||||||
|
} catch (AwsException e) {
|
||||||
|
throw EfsException.subnetNotFound(subnetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An address inside the subnet for its n-th mount target, skipping the first four
|
||||||
|
* addresses AWS reserves in every subnet.
|
||||||
|
*/
|
||||||
|
static String hostAddress(String cidrBlock, long index) {
|
||||||
|
String[] parts = cidrBlock.split("/");
|
||||||
|
String[] octets = parts[0].split("\\.");
|
||||||
|
long base = 0;
|
||||||
|
for (String octet : octets) {
|
||||||
|
base = (base << 8) | Long.parseLong(octet);
|
||||||
|
}
|
||||||
|
long address = base + 4 + index;
|
||||||
|
return ((address >> 24) & 0xff) + "." + ((address >> 16) & 0xff) + "." + ((address >> 8) & 0xff) + "." + (address & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
private String generateAzId(String azName) {
|
private String generateAzId(String azName) {
|
||||||
if (azName == null || azName.length() < 2) return azName + "-id";
|
if (azName == null || azName.length() < 2) return azName + "-id";
|
||||||
String region = azName.substring(0, azName.length() - 1);
|
String region = azName.substring(0, azName.length() - 1);
|
||||||
|
|||||||
@@ -243,6 +243,9 @@ public class GlueService {
|
|||||||
}
|
}
|
||||||
database.setName(databaseName);
|
database.setName(databaseName);
|
||||||
database.setCatalogId(regionResolver.getAccountId());
|
database.setCatalogId(regionResolver.getAccountId());
|
||||||
|
if (database.getCreateTime() == null) {
|
||||||
|
database.setCreateTime(Instant.now());
|
||||||
|
}
|
||||||
databaseStore.put(databaseName, database);
|
databaseStore.put(databaseName, database);
|
||||||
if (tags != null && !tags.isEmpty()) {
|
if (tags != null && !tags.isEmpty()) {
|
||||||
resourceGroupsTaggingService.tagResources(List.of(databaseArn(region, databaseName)), tags, region);
|
resourceGroupsTaggingService.tagResources(List.of(databaseArn(region, databaseName)), tags, region);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package io.github.hectorvent.floci.services.glue.model;
|
|||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -47,10 +48,12 @@ public class Connection {
|
|||||||
|
|
||||||
@JsonProperty("CreationTime")
|
@JsonProperty("CreationTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant creationTime;
|
private Instant creationTime;
|
||||||
|
|
||||||
@JsonProperty("LastUpdatedTime")
|
@JsonProperty("LastUpdatedTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant lastUpdatedTime;
|
private Instant lastUpdatedTime;
|
||||||
|
|
||||||
@JsonProperty("LastUpdatedBy")
|
@JsonProperty("LastUpdatedBy")
|
||||||
@@ -64,6 +67,7 @@ public class Connection {
|
|||||||
|
|
||||||
@JsonProperty("LastConnectionValidationTime")
|
@JsonProperty("LastConnectionValidationTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant lastConnectionValidationTime;
|
private Instant lastConnectionValidationTime;
|
||||||
|
|
||||||
@JsonProperty("AuthenticationConfiguration")
|
@JsonProperty("AuthenticationConfiguration")
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package io.github.hectorvent.floci.services.glue.model;
|
package io.github.hectorvent.floci.services.glue.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class Crawler {
|
public class Crawler {
|
||||||
@JsonProperty("Classifiers")
|
@JsonProperty("Classifiers")
|
||||||
private List<String> classifiers;
|
private List<String> classifiers;
|
||||||
@@ -22,6 +25,7 @@ public class Crawler {
|
|||||||
|
|
||||||
@JsonProperty("CreationTime")
|
@JsonProperty("CreationTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant creationTime;
|
private Instant creationTime;
|
||||||
|
|
||||||
@JsonProperty("DatabaseName")
|
@JsonProperty("DatabaseName")
|
||||||
@@ -38,6 +42,7 @@ public class Crawler {
|
|||||||
|
|
||||||
@JsonProperty("LastUpdated")
|
@JsonProperty("LastUpdated")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant lastUpdated;
|
private Instant lastUpdated;
|
||||||
|
|
||||||
@JsonProperty("LineageConfiguration")
|
@JsonProperty("LineageConfiguration")
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package io.github.hectorvent.floci.services.glue.model;
|
package io.github.hectorvent.floci.services.glue.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class Database {
|
public class Database {
|
||||||
@JsonProperty("Name")
|
@JsonProperty("Name")
|
||||||
private String name;
|
private String name;
|
||||||
@@ -18,6 +21,7 @@ public class Database {
|
|||||||
private Map<String, String> parameters;
|
private Map<String, String> parameters;
|
||||||
@JsonProperty("CreateTime")
|
@JsonProperty("CreateTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant createTime;
|
private Instant createTime;
|
||||||
@JsonProperty("CatalogId")
|
@JsonProperty("CatalogId")
|
||||||
private String catalogId;
|
private String catalogId;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package io.github.hectorvent.floci.services.glue.model;
|
|||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -22,10 +23,12 @@ public class GluePolicy {
|
|||||||
|
|
||||||
@JsonProperty("CreateTime")
|
@JsonProperty("CreateTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant createTime;
|
private Instant createTime;
|
||||||
|
|
||||||
@JsonProperty("UpdateTime")
|
@JsonProperty("UpdateTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant updateTime;
|
private Instant updateTime;
|
||||||
|
|
||||||
public String getPolicyInJson() { return policyInJson; }
|
public String getPolicyInJson() { return policyInJson; }
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package io.github.hectorvent.floci.services.glue.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a Glue timestamp as AWS does: epoch seconds to the millisecond, in the shortest
|
||||||
|
* form ({@code 1792964720.27}, not {@code 1792964720.270000000}). The AWS SDK for
|
||||||
|
* JavaScript v3 keeps any number whose text differs from its shortest form as a
|
||||||
|
* BigDecimal, and then refuses it as a timestamp.
|
||||||
|
*/
|
||||||
|
@RegisterForReflection
|
||||||
|
public class GlueTimestampSerializer extends JsonSerializer<Instant> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(Instant value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||||
|
gen.writeNumber(epochSeconds(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String epochSeconds(Instant value) {
|
||||||
|
return BigDecimal.valueOf(value.toEpochMilli(), 3).stripTrailingZeros().toPlainString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package io.github.hectorvent.floci.services.glue.model;
|
package io.github.hectorvent.floci.services.glue.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
|
|
||||||
@@ -9,6 +11,7 @@ import java.time.Instant;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class Job {
|
public class Job {
|
||||||
@JsonProperty("AllocatedCapacity")
|
@JsonProperty("AllocatedCapacity")
|
||||||
private Integer allocatedCapacity;
|
private Integer allocatedCapacity;
|
||||||
@@ -24,6 +27,7 @@ public class Job {
|
|||||||
|
|
||||||
@JsonProperty("CreatedOn")
|
@JsonProperty("CreatedOn")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant createdOn;
|
private Instant createdOn;
|
||||||
|
|
||||||
@JsonProperty("DefaultArguments")
|
@JsonProperty("DefaultArguments")
|
||||||
@@ -49,6 +53,7 @@ public class Job {
|
|||||||
|
|
||||||
@JsonProperty("LastModifiedOn")
|
@JsonProperty("LastModifiedOn")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant lastModifiedOn;
|
private Instant lastModifiedOn;
|
||||||
|
|
||||||
@JsonProperty("LogUri")
|
@JsonProperty("LogUri")
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package io.github.hectorvent.floci.services.glue.model;
|
package io.github.hectorvent.floci.services.glue.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class LastCrawlInfo {
|
public class LastCrawlInfo {
|
||||||
@JsonProperty("ErrorMessage")
|
@JsonProperty("ErrorMessage")
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
@@ -21,6 +24,7 @@ public class LastCrawlInfo {
|
|||||||
|
|
||||||
@JsonProperty("StartTime")
|
@JsonProperty("StartTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant startTime;
|
private Instant startTime;
|
||||||
|
|
||||||
@JsonProperty("Status")
|
@JsonProperty("Status")
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package io.github.hectorvent.floci.services.glue.model;
|
package io.github.hectorvent.floci.services.glue.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class Partition {
|
public class Partition {
|
||||||
@JsonProperty("Values")
|
@JsonProperty("Values")
|
||||||
private List<String> values;
|
private List<String> values;
|
||||||
@@ -17,9 +20,11 @@ public class Partition {
|
|||||||
private String tableName;
|
private String tableName;
|
||||||
@JsonProperty("CreationTime")
|
@JsonProperty("CreationTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant creationTime;
|
private Instant creationTime;
|
||||||
@JsonProperty("LastAccessTime")
|
@JsonProperty("LastAccessTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant lastAccessTime;
|
private Instant lastAccessTime;
|
||||||
@JsonProperty("StorageDescriptor")
|
@JsonProperty("StorageDescriptor")
|
||||||
private StorageDescriptor storageDescriptor;
|
private StorageDescriptor storageDescriptor;
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package io.github.hectorvent.floci.services.glue.model;
|
package io.github.hectorvent.floci.services.glue.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class Table {
|
public class Table {
|
||||||
@JsonProperty("Name")
|
@JsonProperty("Name")
|
||||||
private String name;
|
private String name;
|
||||||
@@ -19,12 +22,15 @@ public class Table {
|
|||||||
private String owner;
|
private String owner;
|
||||||
@JsonProperty("CreateTime")
|
@JsonProperty("CreateTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant createTime;
|
private Instant createTime;
|
||||||
@JsonProperty("UpdateTime")
|
@JsonProperty("UpdateTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant updateTime;
|
private Instant updateTime;
|
||||||
@JsonProperty("LastAccessTime")
|
@JsonProperty("LastAccessTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant lastAccessTime;
|
private Instant lastAccessTime;
|
||||||
@JsonProperty("PartitionKeys")
|
@JsonProperty("PartitionKeys")
|
||||||
private List<Column> partitionKeys;
|
private List<Column> partitionKeys;
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package io.github.hectorvent.floci.services.glue.model;
|
package io.github.hectorvent.floci.services.glue.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RegisterForReflection
|
@RegisterForReflection
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class UserDefinedFunction {
|
public class UserDefinedFunction {
|
||||||
@JsonProperty("FunctionName")
|
@JsonProperty("FunctionName")
|
||||||
private String functionName;
|
private String functionName;
|
||||||
@@ -23,6 +26,7 @@ public class UserDefinedFunction {
|
|||||||
private String ownerType;
|
private String ownerType;
|
||||||
@JsonProperty("CreateTime")
|
@JsonProperty("CreateTime")
|
||||||
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
|
||||||
|
@JsonSerialize(using = GlueTimestampSerializer.class)
|
||||||
private Instant createTime;
|
private Instant createTime;
|
||||||
@JsonProperty("ResourceUris")
|
@JsonProperty("ResourceUris")
|
||||||
private List<ResourceUri> resourceUris;
|
private List<ResourceUri> resourceUris;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class EfsIntegrationTest {
|
|||||||
.body("""
|
.body("""
|
||||||
{
|
{
|
||||||
"FileSystemId": "%s",
|
"FileSystemId": "%s",
|
||||||
"SubnetId": "subnet-12345",
|
"SubnetId": "subnet-default-us-east-1-a",
|
||||||
"SecurityGroups": ["sg-11111"]
|
"SecurityGroups": ["sg-11111"]
|
||||||
}
|
}
|
||||||
""".formatted(fileSystemId))
|
""".formatted(fileSystemId))
|
||||||
@@ -100,7 +100,10 @@ class EfsIntegrationTest {
|
|||||||
.statusCode(200)
|
.statusCode(200)
|
||||||
.body("MountTargetId", startsWith("fsmt-"))
|
.body("MountTargetId", startsWith("fsmt-"))
|
||||||
.body("FileSystemId", equalTo(fileSystemId))
|
.body("FileSystemId", equalTo(fileSystemId))
|
||||||
.body("SubnetId", equalTo("subnet-12345"))
|
.body("SubnetId", equalTo("subnet-default-us-east-1-a"))
|
||||||
|
.body("VpcId", equalTo("vpc-default-us-east-1"))
|
||||||
|
.body("AvailabilityZoneName", equalTo("us-east-1a"))
|
||||||
|
.body("IpAddress", equalTo("172.31.0.4"))
|
||||||
.extract().jsonPath().getString("MountTargetId");
|
.extract().jsonPath().getString("MountTargetId");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package io.github.hectorvent.floci.services.efs;
|
||||||
|
|
||||||
|
import io.github.hectorvent.floci.testing.RestAssuredJsonUtils;
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static io.restassured.RestAssured.given;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/** Mount targets take their VPC, zone and address from the EC2 subnet they are created in. */
|
||||||
|
@QuarkusTest
|
||||||
|
class EfsMountTargetSubnetTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void configureRestAssured() {
|
||||||
|
RestAssuredJsonUtils.configureAwsContentTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createFileSystem(String token) {
|
||||||
|
return given()
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("{\"CreationToken\": \"" + token + "\"}")
|
||||||
|
.when()
|
||||||
|
.post("/2015-02-01/file-systems")
|
||||||
|
.then()
|
||||||
|
.statusCode(201)
|
||||||
|
.extract().jsonPath().getString("FileSystemId");
|
||||||
|
}
|
||||||
|
|
||||||
|
private io.restassured.response.ValidatableResponse createMountTarget(String fileSystemId, String subnetId) {
|
||||||
|
return given()
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("{\"FileSystemId\": \"" + fileSystemId + "\", \"SubnetId\": \"" + subnetId + "\"}")
|
||||||
|
.when()
|
||||||
|
.post("/2015-02-01/mount-targets")
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void usesTheZoneAndVpcOfEachSubnet() {
|
||||||
|
String fileSystemId = createFileSystem("subnet-zones");
|
||||||
|
|
||||||
|
createMountTarget(fileSystemId, "subnet-default-us-east-1-b")
|
||||||
|
.statusCode(200)
|
||||||
|
.body("VpcId", equalTo("vpc-default-us-east-1"))
|
||||||
|
.body("AvailabilityZoneName", equalTo("us-east-1b"))
|
||||||
|
.body("IpAddress", equalTo("172.31.16.4"));
|
||||||
|
createMountTarget(fileSystemId, "subnet-default-us-east-1-c")
|
||||||
|
.statusCode(200)
|
||||||
|
.body("AvailabilityZoneName", equalTo("us-east-1c"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void refusesASubnetThatDoesNotExist() {
|
||||||
|
String fileSystemId = createFileSystem("subnet-missing");
|
||||||
|
|
||||||
|
createMountTarget(fileSystemId, "subnet-nope")
|
||||||
|
.statusCode(400)
|
||||||
|
.body("__type", equalTo("SubnetNotFound"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void allowsOneMountTargetPerZone() {
|
||||||
|
String fileSystemId = createFileSystem("subnet-one-per-zone");
|
||||||
|
String vpcId = "vpc-default-us-east-1";
|
||||||
|
String secondSubnet = given()
|
||||||
|
.formParam("Action", "CreateSubnet")
|
||||||
|
.formParam("VpcId", vpcId)
|
||||||
|
.formParam("CidrBlock", "172.31.200.0/24")
|
||||||
|
.formParam("AvailabilityZone", "us-east-1a")
|
||||||
|
.header("Authorization", "AWS4-HMAC-SHA256 Credential=test/20260101/us-east-1/ec2/aws4_request")
|
||||||
|
.when()
|
||||||
|
.post("/")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.extract().path("CreateSubnetResponse.subnet.subnetId");
|
||||||
|
|
||||||
|
createMountTarget(fileSystemId, "subnet-default-us-east-1-a").statusCode(200);
|
||||||
|
createMountTarget(fileSystemId, secondSubnet)
|
||||||
|
.statusCode(409)
|
||||||
|
.body("__type", equalTo("MountTargetConflict"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void picksAddressesAfterTheFourAwsReserves() {
|
||||||
|
assertEquals("10.0.1.4", EfsService.hostAddress("10.0.1.0/24", 0));
|
||||||
|
assertEquals("10.0.2.1", EfsService.hostAddress("10.0.1.0/24", 253));
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
package io.github.hectorvent.floci.services.glue;
|
||||||
|
|
||||||
|
import io.github.hectorvent.floci.testing.RestAssuredJsonUtils;
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static io.restassured.RestAssured.given;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.hasKey;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AWS leaves unset fields out of Glue responses and reports times in milliseconds.
|
||||||
|
* A null or nanosecond timestamp instead breaks the AWS SDK for JavaScript v3,
|
||||||
|
* which refuses to read either.
|
||||||
|
*/
|
||||||
|
@QuarkusTest
|
||||||
|
class GlueOmitsNullFieldsIntegrationTest {
|
||||||
|
|
||||||
|
private static final String CONTENT_TYPE = "application/x-amz-json-1.1";
|
||||||
|
private static final String DATABASE = "nulls_" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void configureRestAssured() {
|
||||||
|
RestAssuredJsonUtils.configureAwsContentTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static io.restassured.response.ValidatableResponse glue(String action, String body) {
|
||||||
|
return given().contentType(CONTENT_TYPE)
|
||||||
|
.header("X-Amz-Target", "AWSGlue." + action)
|
||||||
|
.body(body)
|
||||||
|
.when().post("/")
|
||||||
|
.then().statusCode(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aNewTableHasOnlyAMillisecondCreateTime() {
|
||||||
|
glue("CreateDatabase", "{\"DatabaseInput\": {\"Name\": \"" + DATABASE + "\"}}");
|
||||||
|
glue("CreateTable", """
|
||||||
|
{
|
||||||
|
"DatabaseName": "%s",
|
||||||
|
"TableInput": {
|
||||||
|
"Name": "orders",
|
||||||
|
"StorageDescriptor": {"Columns": [{"Name": "id", "Type": "int"}], "Location": "s3://data/orders/"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(DATABASE));
|
||||||
|
|
||||||
|
String tables = glue("GetTables", "{\"DatabaseName\": \"" + DATABASE + "\"}")
|
||||||
|
.body("TableList[0].Name", equalTo("orders"))
|
||||||
|
.body("TableList[0].CreateTime", notNullValue())
|
||||||
|
.body("TableList[0]", not(hasKey("UpdateTime")))
|
||||||
|
.body("TableList[0]", not(hasKey("LastAccessTime")))
|
||||||
|
.extract().asString();
|
||||||
|
// Milliseconds in their shortest form, as AWS sends them: no trailing zeros.
|
||||||
|
assertTrue(tables.matches("(?s).*\"CreateTime\":\\d+(\\.\\d{0,2}[1-9])?[,}].*"), tables);
|
||||||
|
|
||||||
|
glue("GetDatabase", "{\"Name\": \"" + DATABASE + "\"}")
|
||||||
|
.body("Database", not(hasKey("Description")))
|
||||||
|
.body("Database.CreateTime", notNullValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package io.github.hectorvent.floci.services.glue.model;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class GlueTimestampSerializerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void writesMillisecondsInTheirShortestForm() {
|
||||||
|
assertEquals("1792964720.273", GlueTimestampSerializer.epochSeconds(Instant.ofEpochSecond(1792964720, 273_456_789)));
|
||||||
|
assertEquals("1792964720.27", GlueTimestampSerializer.epochSeconds(Instant.ofEpochSecond(1792964720, 270_000_000)));
|
||||||
|
assertEquals("1792964720", GlueTimestampSerializer.epochSeconds(Instant.ofEpochSecond(1792964720)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user