feat: Lightsail + svg animation + ec2 bug fixes
This commit is contained in:
@@ -34,6 +34,20 @@ class PortAllocatorTest {
|
||||
allocator.release(second);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsOccupiedPortsSkippedDuringAllocation() {
|
||||
PortAllocator allocator = new PortAllocator();
|
||||
int first = allocator.allocate(19900, 19900);
|
||||
List<Integer> occupied = new ArrayList<>();
|
||||
|
||||
int second = allocator.allocate(19900, 19901, occupied::add);
|
||||
|
||||
assertEquals(19901, second);
|
||||
assertEquals(List.of(19900), occupied);
|
||||
allocator.release(first);
|
||||
allocator.release(second);
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasedPortBecomesAvailableAgain() {
|
||||
PortAllocator allocator = new PortAllocator();
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.github.hectorvent.floci.lifecycle;
|
||||
|
||||
import io.github.hectorvent.floci.config.EmulatorConfig;
|
||||
import io.github.hectorvent.floci.services.ec2.Ec2Service;
|
||||
import io.github.hectorvent.floci.services.lightsail.LightsailService;
|
||||
import io.github.hectorvent.floci.config.FlociCertificateAuthority;
|
||||
import io.github.hectorvent.floci.core.common.ContainerTeardown;
|
||||
import io.github.hectorvent.floci.core.common.Resettable;
|
||||
@@ -38,6 +39,7 @@ class EmulatorInfoControllerTest {
|
||||
@Mock private FlociCertificateAuthority certificateAuthority;
|
||||
@Mock private EmulatorConfig config;
|
||||
@Mock private Ec2Service ec2Service;
|
||||
@Mock private LightsailService lightsailService;
|
||||
@Mock private ContainerTeardown sageMakerTeardown;
|
||||
@Mock private ContainerTeardown batchTeardown;
|
||||
@Mock private Resettable resettable;
|
||||
@@ -47,7 +49,7 @@ class EmulatorInfoControllerTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new EmulatorInfoController(serviceRegistry, initLifecycleState, storageFactory,
|
||||
resettables, containerTeardowns, certificateAuthority, config, ec2Service);
|
||||
resettables, containerTeardowns, certificateAuthority, config, ec2Service, lightsailService);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -115,6 +115,6 @@ class CloudWatchLogsMetricFilterResetTest {
|
||||
when(resettables.iterator()).thenAnswer(call -> List.of(resettable).iterator());
|
||||
Instance<ContainerTeardown> teardowns = mock(Instance.class);
|
||||
when(teardowns.iterator()).thenAnswer(call -> List.<ContainerTeardown>of().iterator());
|
||||
return new EmulatorInfoController(null, null, f.factory, resettables, teardowns, null, null, null);
|
||||
return new EmulatorInfoController(null, null, f.factory, resettables, teardowns, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
+120
-25
@@ -10,9 +10,12 @@ import com.github.dockerjava.api.command.InspectContainerCmd;
|
||||
import com.github.dockerjava.api.command.InspectContainerResponse;
|
||||
import com.github.dockerjava.api.command.InspectExecCmd;
|
||||
import com.github.dockerjava.api.command.InspectExecResponse;
|
||||
import com.github.dockerjava.api.command.InspectImageCmd;
|
||||
import com.github.dockerjava.api.command.InspectImageResponse;
|
||||
import com.github.dockerjava.api.command.ListContainersCmd;
|
||||
import com.github.dockerjava.api.command.StartContainerCmd;
|
||||
import com.github.dockerjava.api.exception.DockerException;
|
||||
import com.github.dockerjava.api.exception.NotFoundException;
|
||||
import com.github.dockerjava.api.model.Container;
|
||||
import com.github.dockerjava.api.model.ContainerConfig;
|
||||
import com.github.dockerjava.api.model.ContainerNetwork;
|
||||
@@ -56,8 +59,10 @@ import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -69,6 +74,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.anySet;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.nullable;
|
||||
import static org.mockito.Answers.RETURNS_SELF;
|
||||
import static org.mockito.Answers.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.after;
|
||||
@@ -97,6 +103,8 @@ class Ec2ContainerManagerTest {
|
||||
void resetBridgeIpPolling() {
|
||||
Ec2ContainerManager.containerBridgeIpAttempts = 30;
|
||||
Ec2ContainerManager.containerBridgeIpPollMillis = 500;
|
||||
Ec2ContainerManager.sshBannerAttempts = 30;
|
||||
Ec2ContainerManager.sshBannerPollMillis = 1_000;
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -731,6 +739,8 @@ class Ec2ContainerManagerTest {
|
||||
void launchReturnsTheLeaseWhenTheLaunchThrows() throws Exception {
|
||||
LaunchHarness harness = launchHarness();
|
||||
when(harness.builder.build()).thenThrow(new IllegalStateException("daemon went away"));
|
||||
AtomicInteger persistenceCalls = new AtomicInteger();
|
||||
harness.manager.setInstancePersister(ignored -> persistenceCalls.incrementAndGet());
|
||||
|
||||
Instance instance = leasedInstance("i-throws");
|
||||
|
||||
@@ -742,6 +752,13 @@ class Ec2ContainerManagerTest {
|
||||
verify(harness.vpcNetworkManager, timeout(2000)).detach("us-west-2", "vpc-lease", null);
|
||||
verify(harness.vpcNetworkManager, timeout(2000))
|
||||
.releasePrivateIp("us-west-2", "subnet-lease", "10.0.1.10");
|
||||
assertEquals("failed", instance.getLaunchStatus());
|
||||
assertEquals("impaired", instance.getSshStatus());
|
||||
assertEquals("daemon went away", instance.getStateReasonMessage());
|
||||
assertTrue(instance.getLaunchEvents().stream()
|
||||
.anyMatch(event -> "failed".equals(event.getStatus())
|
||||
&& "daemon went away".equals(event.getMessage())));
|
||||
assertTrue(persistenceCalls.get() >= instance.getLaunchEvents().size());
|
||||
}
|
||||
|
||||
private static Instance leasedInstance(String instanceId) {
|
||||
@@ -789,13 +806,14 @@ class Ec2ContainerManagerTest {
|
||||
when(inspect.exec()).thenReturn(response);
|
||||
|
||||
Instance instance = instance("i-labeltest");
|
||||
instance.setAccountId("123456789012");
|
||||
harness.manager.launch(instance, "ubuntu:24.04", null, "us-west-2");
|
||||
|
||||
verify(harness.builder, timeout(2000)).withLabels(Map.of(
|
||||
"io.floci", "aws",
|
||||
"io.floci.service", "ec2",
|
||||
"io.floci.resource-id", "i-labeltest",
|
||||
"io.floci.account", "000000000000",
|
||||
"io.floci.account", "123456789012",
|
||||
"io.floci.region", "us-west-2"));
|
||||
}
|
||||
|
||||
@@ -968,7 +986,8 @@ class Ec2ContainerManagerTest {
|
||||
Ec2ContainerManager.containerBridgeIpAttempts = 1;
|
||||
Ec2ContainerManager.containerBridgeIpPollMillis = 1;
|
||||
LaunchHarness harness = launchHarness();
|
||||
when(harness.lifecycleManager.create(any(ContainerSpec.class), eq("linux/arm64")))
|
||||
when(harness.lifecycleManager.create(any(ContainerSpec.class), eq("linux/arm64"),
|
||||
any(ContainerLifecycleManager.ContainerCreationObserver.class)))
|
||||
.thenReturn(TEST_CONTAINER_ID);
|
||||
InspectContainerCmd inspect = mock(InspectContainerCmd.class);
|
||||
InspectContainerResponse withIp = inspectResponse("172.18.0.11");
|
||||
@@ -979,7 +998,7 @@ class Ec2ContainerManagerTest {
|
||||
|
||||
harness.manager.launch(instance,
|
||||
new ResolvedAmiImage("floci/ami-ubuntu:24.04-arm64", ResolvedAmiImage.SYSTEMD_RUNTIME, true,
|
||||
"linux/arm64"),
|
||||
"linux/arm64", null, "enhanced", "root", true, "ssh"),
|
||||
null,
|
||||
"us-west-2");
|
||||
|
||||
@@ -987,7 +1006,49 @@ class Ec2ContainerManagerTest {
|
||||
verify(harness.builder).withCmd(List.of("/sbin/init"));
|
||||
verify(harness.builder).withCgroupnsMode("host");
|
||||
verify(harness.builder).withBind("/sys/fs/cgroup", "/sys/fs/cgroup");
|
||||
verify(harness.lifecycleManager).create(any(ContainerSpec.class), eq("linux/arm64"));
|
||||
verify(harness.lifecycleManager).create(any(ContainerSpec.class), eq("linux/arm64"),
|
||||
any(ContainerLifecycleManager.ContainerCreationObserver.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyTailGuestIsRecognizedForSshBootstrapAfterReboot() {
|
||||
assertTrue(Ec2ContainerManager.isLegacyTailGuest(new String[]{"tail", "-f", "/dev/null"}));
|
||||
assertFalse(Ec2ContainerManager.isLegacyTailGuest(new String[]{"/sbin/init"}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingLocalAmiUsesPullableMinimalFallback() {
|
||||
LaunchHarness harness = launchHarness();
|
||||
InspectImageCmd inspect = mock(InspectImageCmd.class);
|
||||
when(harness.containerBuilder.resolveImage("floci/ami-ubuntu:20.04"))
|
||||
.thenReturn("floci/ami-ubuntu:20.04");
|
||||
when(harness.dockerClient.inspectImageCmd("floci/ami-ubuntu:20.04")).thenReturn(inspect);
|
||||
when(inspect.exec()).thenThrow(new NotFoundException("missing"));
|
||||
|
||||
ResolvedAmiImage selected = harness.manager.selectLaunchImage(new ResolvedAmiImage(
|
||||
"floci/ami-ubuntu:20.04", ResolvedAmiImage.SYSTEMD_RUNTIME, false,
|
||||
"linux/amd64", "public.ecr.aws/docker/library/ubuntu:20.04",
|
||||
"enhanced", "root", true, "ssh"));
|
||||
|
||||
assertEquals("public.ecr.aws/docker/library/ubuntu:20.04", selected.dockerImage());
|
||||
assertFalse(selected.systemd());
|
||||
assertEquals("linux/amd64", selected.dockerPlatform());
|
||||
}
|
||||
|
||||
@Test
|
||||
void availableLocalAmiKeepsItsSystemdRuntime() {
|
||||
LaunchHarness harness = launchHarness();
|
||||
InspectImageCmd inspect = mock(InspectImageCmd.class);
|
||||
when(harness.containerBuilder.resolveImage("floci/ami-ubuntu:20.04"))
|
||||
.thenReturn("floci/ami-ubuntu:20.04");
|
||||
when(harness.dockerClient.inspectImageCmd("floci/ami-ubuntu:20.04")).thenReturn(inspect);
|
||||
when(inspect.exec()).thenReturn(mock(InspectImageResponse.class));
|
||||
ResolvedAmiImage image = new ResolvedAmiImage(
|
||||
"floci/ami-ubuntu:20.04", ResolvedAmiImage.SYSTEMD_RUNTIME, false,
|
||||
"linux/amd64", "public.ecr.aws/docker/library/ubuntu:20.04",
|
||||
"enhanced", "root", true, "ssh");
|
||||
|
||||
assertEquals(image, harness.manager.selectLaunchImage(image));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1091,8 +1152,9 @@ class Ec2ContainerManagerTest {
|
||||
Ec2ContainerManager.containerBridgeIpAttempts = 1;
|
||||
Ec2ContainerManager.containerBridgeIpPollMillis = 1;
|
||||
LaunchHarness harness = launchHarness();
|
||||
when(harness.portAllocator.allocate(anyInt(), anyInt())).thenReturn(2201, 2202);
|
||||
when(harness.lifecycleManager.create(any(ContainerSpec.class)))
|
||||
when(harness.portAllocator.allocate(anyInt(), anyInt(), any(IntConsumer.class))).thenReturn(2201, 2202);
|
||||
when(harness.lifecycleManager.create(any(ContainerSpec.class), nullable(String.class),
|
||||
any(ContainerLifecycleManager.ContainerCreationObserver.class)))
|
||||
.thenReturn("container-conflict", TEST_CONTAINER_ID);
|
||||
when(harness.lifecycleManager.startCreated(anyString(), any(ContainerSpec.class)))
|
||||
.thenThrow(new RuntimeException("Bind for 0.0.0.0:2201 failed: port is already allocated"))
|
||||
@@ -1115,6 +1177,28 @@ class Ec2ContainerManagerTest {
|
||||
verify(harness.builder).withPortBinding(22, 2202);
|
||||
}
|
||||
|
||||
@Test
|
||||
void launchRecordsOccupiedCandidatesWhenTheSshPortRangeIsExhausted() throws Exception {
|
||||
LaunchHarness harness = launchHarness();
|
||||
when(harness.portAllocator.allocate(anyInt(), anyInt(), any(IntConsumer.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
IntConsumer observer = invocation.getArgument(2);
|
||||
observer.accept(2200);
|
||||
observer.accept(2201);
|
||||
throw new RuntimeException("No free port available in range 2200-2201");
|
||||
});
|
||||
Instance instance = instance("i-ports-exhausted");
|
||||
|
||||
harness.manager.launch(instance, "ubuntu:24.04", null, "us-west-2");
|
||||
|
||||
awaitUntil(() -> "terminated".equals(instance.getState().getName()), Duration.ofSeconds(2));
|
||||
assertTrue(instance.getLaunchEvents().stream().anyMatch(event -> "retrying".equals(event.getStatus())
|
||||
&& event.getMessage().contains("2200")));
|
||||
assertTrue(instance.getLaunchEvents().stream().anyMatch(event -> "retrying".equals(event.getStatus())
|
||||
&& event.getMessage().contains("2201")));
|
||||
assertEquals("No free port available in range 2200-2201", instance.getStateReasonMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void launchReleasesPortAndCleansUpContainerAfterNonRetryableStartFailure() throws Exception {
|
||||
LaunchHarness harness = launchHarness();
|
||||
@@ -1172,7 +1256,8 @@ class Ec2ContainerManagerTest {
|
||||
LaunchHarness harness = launchHarness();
|
||||
CountDownLatch createEntered = new CountDownLatch(1);
|
||||
CountDownLatch allowCreate = new CountDownLatch(1);
|
||||
when(harness.lifecycleManager.create(any(ContainerSpec.class))).thenAnswer(invocation -> {
|
||||
when(harness.lifecycleManager.create(any(ContainerSpec.class), nullable(String.class),
|
||||
any(ContainerLifecycleManager.ContainerCreationObserver.class))).thenAnswer(invocation -> {
|
||||
createEntered.countDown();
|
||||
assertTrue(allowCreate.await(2, TimeUnit.SECONDS));
|
||||
return TEST_CONTAINER_ID;
|
||||
@@ -1247,7 +1332,8 @@ class Ec2ContainerManagerTest {
|
||||
ExecutorService callerExecutor = Executors.newSingleThreadExecutor();
|
||||
CountDownLatch createEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseCreate = new CountDownLatch(1);
|
||||
when(harness.lifecycleManager().create(any(ContainerSpec.class))).thenAnswer(invocation -> {
|
||||
when(harness.lifecycleManager().create(any(ContainerSpec.class), nullable(String.class),
|
||||
any(ContainerLifecycleManager.ContainerCreationObserver.class))).thenAnswer(invocation -> {
|
||||
createEntered.countDown();
|
||||
releaseCreate.await(2, TimeUnit.SECONDS);
|
||||
throw new RuntimeException("test launch failure");
|
||||
@@ -1368,17 +1454,7 @@ class Ec2ContainerManagerTest {
|
||||
verify(harness.dockerClient, never()).copyArchiveToContainerCmd(TEST_CONTAINER_ID);
|
||||
}
|
||||
|
||||
private static final String[] SSHD_INSTALL_PROBE_CMD = {"sh", "-c",
|
||||
"if ! command -v sshd >/dev/null 2>&1 || ! command -v scp >/dev/null 2>&1; then"
|
||||
+ " if command -v dnf >/dev/null 2>&1; then dnf install -y openssh-server openssh-clients >/dev/null 2>&1;"
|
||||
+ " elif command -v yum >/dev/null 2>&1; then yum install -y openssh-server openssh-clients >/dev/null 2>&1;"
|
||||
+ " elif command -v apt-get >/dev/null 2>&1; then DEBIAN_FRONTEND=noninteractive apt-get install -y openssh-server openssh-client >/dev/null 2>&1;"
|
||||
+ " elif command -v zypper >/dev/null 2>&1; then zypper --non-interactive install -y openssh >/dev/null 2>&1;"
|
||||
+ " elif command -v apk >/dev/null 2>&1; then apk add --no-cache openssh >/dev/null 2>&1;"
|
||||
+ " fi;"
|
||||
+ "fi;"
|
||||
+ "command -v sshd >/dev/null 2>&1 || exit 1;"
|
||||
+ "command -v scp >/dev/null 2>&1 || exit 2"};
|
||||
private static final String[] SSHD_INSTALL_PROBE_CMD = Ec2ContainerManager.sshdInstallProbeCommand();
|
||||
|
||||
@Test
|
||||
void launchWhenSshdInstallFails_doesNotAttemptKeygenOrStart() throws Exception {
|
||||
@@ -1477,6 +1553,8 @@ class Ec2ContainerManagerTest {
|
||||
}
|
||||
|
||||
private static LaunchHarness launchHarness(ExecutorService executor, Duration userDataTimeout) {
|
||||
Ec2ContainerManager.sshBannerAttempts = 1;
|
||||
Ec2ContainerManager.sshBannerPollMillis = 1;
|
||||
ContainerBuilder containerBuilder = mock(ContainerBuilder.class);
|
||||
ContainerBuilder.Builder builder = mock(ContainerBuilder.Builder.class, withSettings().defaultAnswer(RETURNS_SELF));
|
||||
when(containerBuilder.newContainer(anyString())).thenReturn(builder);
|
||||
@@ -1484,6 +1562,13 @@ class Ec2ContainerManagerTest {
|
||||
|
||||
ContainerLifecycleManager lifecycleManager = mock(ContainerLifecycleManager.class);
|
||||
when(lifecycleManager.create(any(ContainerSpec.class))).thenReturn(TEST_CONTAINER_ID);
|
||||
when(lifecycleManager.create(any(ContainerSpec.class), nullable(String.class),
|
||||
any(ContainerLifecycleManager.ContainerCreationObserver.class))).thenAnswer(invocation -> {
|
||||
ContainerLifecycleManager.ContainerCreationObserver observer = invocation.getArgument(2);
|
||||
observer.containerCreationStarted();
|
||||
observer.containerCreated(TEST_CONTAINER_ID);
|
||||
return TEST_CONTAINER_ID;
|
||||
});
|
||||
when(lifecycleManager.isContainerRunning(TEST_CONTAINER_ID)).thenReturn(true);
|
||||
|
||||
DockerHostResolver dockerHostResolver = mock(DockerHostResolver.class);
|
||||
@@ -1491,7 +1576,7 @@ class Ec2ContainerManagerTest {
|
||||
ContainerReachableEndpoint reachableEndpoint = mock(ContainerReachableEndpoint.class);
|
||||
when(reachableEndpoint.baseUrl()).thenReturn("http://localhost.floci.io:4680");
|
||||
PortAllocator portAllocator = mock(PortAllocator.class);
|
||||
when(portAllocator.allocate(anyInt(), anyInt())).thenReturn(2201);
|
||||
when(portAllocator.allocate(anyInt(), anyInt(), any(IntConsumer.class))).thenReturn(2201);
|
||||
|
||||
EmulatorConfig config = mock(EmulatorConfig.class);
|
||||
EmulatorConfig.ServicesConfig services = mock(EmulatorConfig.ServicesConfig.class);
|
||||
@@ -1543,7 +1628,8 @@ class Ec2ContainerManagerTest {
|
||||
reachableEndpoint,
|
||||
executor,
|
||||
userDataTimeout);
|
||||
return new LaunchHarness(manager, lifecycleManager, dockerClient, metadataServer, logStreamer, builder,
|
||||
return new LaunchHarness(manager, lifecycleManager, dockerClient, metadataServer, logStreamer,
|
||||
containerBuilder, builder,
|
||||
portAllocator, portForwardManager, config, vpcNetworkManager, new CopyOnWriteArrayList<>());
|
||||
}
|
||||
|
||||
@@ -1771,12 +1857,19 @@ class Ec2ContainerManagerTest {
|
||||
void launchRunsInstanceAsMetadataOnlyWhenNoDockerDaemonIsReachable() throws Exception {
|
||||
LaunchHarness harness = launchHarness();
|
||||
stubDockerPing(harness.dockerClient(), false);
|
||||
AtomicInteger persistenceCalls = new AtomicInteger();
|
||||
harness.manager.setInstancePersister(ignored -> persistenceCalls.incrementAndGet());
|
||||
|
||||
Instance instance = instance("i-nodocker");
|
||||
|
||||
harness.manager().launch(instance, "ubuntu:24.04", null, "us-west-2");
|
||||
|
||||
assertEquals("running", instance.getState().getName());
|
||||
assertEquals("failed", instance.getLaunchStatus());
|
||||
assertEquals("impaired", instance.getSshStatus());
|
||||
assertTrue(instance.getSshStatusMessage().contains("Docker daemon is unavailable"));
|
||||
assertEquals("creating-container", instance.getLaunchEvents().getLast().getStage());
|
||||
assertTrue(persistenceCalls.get() >= 2);
|
||||
assertNull(instance.getDockerContainerId());
|
||||
verify(harness.lifecycleManager(), never()).create(any(ContainerSpec.class));
|
||||
verify(harness.metadataServer(), never()).registerContainer(anyString(), anyString(), any());
|
||||
@@ -1788,7 +1881,8 @@ class Ec2ContainerManagerTest {
|
||||
PingCmd ping = mock(PingCmd.class);
|
||||
when(harness.dockerClient().pingCmd()).thenReturn(ping);
|
||||
doNothing().doThrow(new RuntimeException("No such file or directory")).when(ping).exec();
|
||||
when(harness.lifecycleManager().create(any(ContainerSpec.class)))
|
||||
when(harness.lifecycleManager().create(any(ContainerSpec.class), nullable(String.class),
|
||||
any(ContainerLifecycleManager.ContainerCreationObserver.class)))
|
||||
.thenThrow(new RuntimeException("No such file or directory"));
|
||||
|
||||
Instance instance = instance("i-dockergone");
|
||||
@@ -1846,9 +1940,10 @@ class Ec2ContainerManagerTest {
|
||||
private record LaunchHarness(Ec2ContainerManager manager,
|
||||
ContainerLifecycleManager lifecycleManager,
|
||||
DockerClient dockerClient,
|
||||
Ec2MetadataServer metadataServer,
|
||||
ContainerLogStreamer logStreamer,
|
||||
ContainerBuilder.Builder builder,
|
||||
Ec2MetadataServer metadataServer,
|
||||
ContainerLogStreamer logStreamer,
|
||||
ContainerBuilder containerBuilder,
|
||||
ContainerBuilder.Builder builder,
|
||||
PortAllocator portAllocator,
|
||||
Ec2PortForwardManager portForwardManager,
|
||||
EmulatorConfig config,
|
||||
|
||||
@@ -37,7 +37,6 @@ class Ec2ImageCatalogTest {
|
||||
"ami-ubuntu2404-amd64",
|
||||
"ami-ubuntu2404-cloud-arm64",
|
||||
"ami-debian12",
|
||||
"ami-debian13",
|
||||
"ami-alpine",
|
||||
"ami-fedora42",
|
||||
"ami-opensuse-leap156"), imageIds);
|
||||
@@ -67,9 +66,21 @@ class Ec2ImageCatalogTest {
|
||||
ResolvedAmiImage image = amiImageResolver.resolveImage("ami-amazonlinux2023");
|
||||
|
||||
assertEquals("floci/ami-amazonlinux:2023", image.dockerImage());
|
||||
assertEquals(ResolvedAmiImage.SYSTEMD_RUNTIME, image.guestRuntime());
|
||||
assertEquals(ResolvedAmiImage.SYSTEMD_RUNTIME, image.initType());
|
||||
assertEquals("root", image.sshUser());
|
||||
assertTrue(image.sshPreinstalled());
|
||||
assertEquals("sshd", image.sshServiceName());
|
||||
assertFalse(image.cloudInit());
|
||||
assertTrue(image.systemd());
|
||||
assertEquals("public.ecr.aws/amazonlinux/amazonlinux:2023", image.fallbackDockerImage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyLocalCatalogImageHasAPullableFallback() {
|
||||
imageCatalog.images().stream()
|
||||
.filter(image -> image.dockerImage.startsWith("floci/"))
|
||||
.forEach(image -> assertTrue(image.fallbackDockerImage != null
|
||||
&& !image.fallbackDockerImage.isBlank(), image.imageId));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -81,11 +92,9 @@ class Ec2ImageCatalogTest {
|
||||
@Test
|
||||
void resolverUsesFedoraAndLocalSshReadyImages() {
|
||||
assertEquals("floci/ami-ubuntu:20.04", amiImageResolver.resolve("ami-0abcdef1234567892"));
|
||||
assertEquals("floci/ami-debian:13", amiImageResolver.resolve("ami-debian13"));
|
||||
assertEquals("floci/ami-fedora:42", amiImageResolver.resolve("ami-fedora42"));
|
||||
assertEquals("floci/ami-opensuse:15.6", amiImageResolver.resolve("ami-opensuse-leap156"));
|
||||
assertTrue(amiImageResolver.resolveImage("ami-0abcdef1234567892").systemd());
|
||||
assertTrue(amiImageResolver.resolveImage("ami-debian13").systemd());
|
||||
assertTrue(amiImageResolver.resolveImage("ami-fedora42").systemd());
|
||||
}
|
||||
|
||||
|
||||
@@ -2215,7 +2215,7 @@ class Ec2IntegrationTest {
|
||||
.formParam("LaunchTemplateName", "sample-template")
|
||||
.formParam("LaunchTemplateData.ImageId", "ami-0abcdef1234567890")
|
||||
.formParam("LaunchTemplateData.InstanceType", "t3.micro")
|
||||
.formParam("LaunchTemplateData.KeyName", "test-key")
|
||||
.formParam("LaunchTemplateData.KeyName", keyPairName)
|
||||
.formParam("LaunchTemplateData.IamInstanceProfile.Name", "sample-profile")
|
||||
.formParam("LaunchTemplateData.SecurityGroupId.1", securityGroupId)
|
||||
.formParam("LaunchTemplateData.UserData", launchTemplateUserData)
|
||||
@@ -2328,7 +2328,7 @@ class Ec2IntegrationTest {
|
||||
.formParam("VersionDescription", "updated by test")
|
||||
.formParam("LaunchTemplateData.ImageId", "ami-0abcdef1234567890")
|
||||
.formParam("LaunchTemplateData.InstanceType", "t3.small")
|
||||
.formParam("LaunchTemplateData.KeyName", "test-key")
|
||||
.formParam("LaunchTemplateData.KeyName", keyPairName)
|
||||
.formParam("LaunchTemplateData.IamInstanceProfile.Name", "sample-profile-v2")
|
||||
.formParam("LaunchTemplateData.SecurityGroupId.1", securityGroupId)
|
||||
.formParam("LaunchTemplateData.UserData", launchTemplateVersionUserData)
|
||||
@@ -2468,7 +2468,7 @@ class Ec2IntegrationTest {
|
||||
.statusCode(200)
|
||||
.body("RunInstancesResponse.instancesSet.item.imageId", equalTo("ami-sample-override"))
|
||||
.body("RunInstancesResponse.instancesSet.item.instanceType", equalTo("c7g.large"))
|
||||
.body("RunInstancesResponse.instancesSet.item.keyName", equalTo("test-key"))
|
||||
.body("RunInstancesResponse.instancesSet.item.keyName", equalTo(keyPairName))
|
||||
.body("RunInstancesResponse.instancesSet.item.iamInstanceProfile.arn",
|
||||
equalTo("arn:aws:iam::000000000000:instance-profile/sample-profile-v2"))
|
||||
.body("RunInstancesResponse.instancesSet.item.groupSet.item.groupId", equalTo(securityGroupId))
|
||||
@@ -2861,7 +2861,7 @@ class Ec2IntegrationTest {
|
||||
.formParam("InstanceType", "t2.micro")
|
||||
.formParam("MinCount", "1")
|
||||
.formParam("MaxCount", "1")
|
||||
.formParam("KeyName", "test-key")
|
||||
.formParam("KeyName", keyPairName)
|
||||
.formParam("SubnetId", subnetId)
|
||||
.formParam("SecurityGroupId.1", securityGroupId)
|
||||
.header("Authorization", AUTH_HEADER)
|
||||
@@ -2872,7 +2872,7 @@ class Ec2IntegrationTest {
|
||||
.body("RunInstancesResponse.instancesSet.item.instanceId", startsWith("i-"))
|
||||
.body("RunInstancesResponse.instancesSet.item.instanceState.name", equalTo("pending"))
|
||||
.body("RunInstancesResponse.instancesSet.item.instanceType", equalTo("t2.micro"))
|
||||
.body("RunInstancesResponse.instancesSet.item.keyName", equalTo("test-key"))
|
||||
.body("RunInstancesResponse.instancesSet.item.keyName", equalTo(keyPairName))
|
||||
.extract().path("RunInstancesResponse.instancesSet.item.instanceId");
|
||||
}
|
||||
|
||||
@@ -3712,7 +3712,7 @@ class Ec2IntegrationTest {
|
||||
.formParam("InstanceType", "t2.micro")
|
||||
.formParam("MinCount", "5")
|
||||
.formParam("MaxCount", "5")
|
||||
.formParam("KeyName", "test-key")
|
||||
.formParam("KeyName", keyPairName)
|
||||
.formParam("SubnetId", subnetId)
|
||||
.formParam("SecurityGroupId.1", securityGroupId)
|
||||
.header("Authorization", AUTH_HEADER)
|
||||
|
||||
@@ -1510,6 +1510,31 @@ class Ec2ServiceTest {
|
||||
assertNotEquals(first.getKeyMaterial(), second.getKeyMaterial());
|
||||
assertNotEquals(first.getPublicKey(), second.getPublicKey());
|
||||
assertNotEquals(first.getKeyFingerprint(), second.getKeyFingerprint());
|
||||
KeyPair persisted = service.describeKeyPairs("us-east-1", List.of("usable-key"), List.of()).getFirst();
|
||||
assertNull(persisted.getKeyMaterial());
|
||||
assertEquals(first.getPublicKey(), persisted.getPublicKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void startupClearsLegacyPersistedPrivateKeyMaterialAcrossAccounts() {
|
||||
AccountAwareStorageBackend<KeyPair> keyPairStore = AccountAwareStorageBackend.inMemory("000000000000");
|
||||
KeyPair legacy = new KeyPair();
|
||||
legacy.setKeyPairId("key-legacy");
|
||||
legacy.setKeyName("legacy-key");
|
||||
legacy.setKeyMaterial("-----BEGIN RSA PRIVATE KEY-----\nsecret\n-----END RSA PRIVATE KEY-----");
|
||||
legacy.setPublicKey("ssh-rsa public");
|
||||
legacy.setRegion("us-east-1");
|
||||
keyPairStore.putForAccount("111111111111", "us-east-1::key-legacy", legacy);
|
||||
Ec2Service service = new Ec2Service(mockConfig(true), mock(Ec2ContainerManager.class),
|
||||
mock(Ec2PortForwardManager.class),
|
||||
mock(AmiImageResolver.class), mock(Ec2ImageCatalog.class), new Ec2InstanceTypeCatalog(),
|
||||
new InMemoryStorageFactory(Map.of("ec2-key-pairs.json", keyPairStore)));
|
||||
|
||||
service.restoreMetadataRegistrations();
|
||||
|
||||
KeyPair restored = keyPairStore.getForAccount("111111111111", "us-east-1::key-legacy").orElseThrow();
|
||||
assertNull(restored.getKeyMaterial());
|
||||
assertEquals("ssh-rsa public", restored.getPublicKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1856,7 +1881,7 @@ class Ec2ServiceTest {
|
||||
AmiImageResolver resolver = mock(AmiImageResolver.class);
|
||||
when(resolver.resolveImage("ami-arm-source"))
|
||||
.thenReturn(new ResolvedAmiImage("arm-image", ResolvedAmiImage.DEFAULT_RUNTIME, false,
|
||||
"linux/arm64"));
|
||||
"linux/arm64", null, "public", "root", false, "sshd"));
|
||||
Ec2Service service = liveService(capturingContainerManager(), resolver, catalog);
|
||||
Reservation sourceReservation = service.runInstances("us-east-1", "ami-arm-source", "t4g.medium",
|
||||
1, 1, null, List.of(), null, null, List.of(), null, null);
|
||||
@@ -2065,6 +2090,135 @@ class Ec2ServiceTest {
|
||||
verify(vpcNetworks, times(2)).reservePrivateIp("us-east-1", "subnet-a", "10.0.1.10");
|
||||
}
|
||||
|
||||
@Test
|
||||
void restorePromotesAPendingInstanceWhoseContainerIsAlreadyRunning() {
|
||||
AccountAwareStorageBackend<Instance> instanceStore = AccountAwareStorageBackend.inMemory("000000000000");
|
||||
Instance instance = persistedInstance("i-pending", "10.0.1.10", InstanceState.pending());
|
||||
instance.setDockerContainerId("container-id");
|
||||
instanceStore.put("us-east-1::i-pending", instance);
|
||||
|
||||
Ec2ContainerManager containerManager = mock(Ec2ContainerManager.class);
|
||||
when(containerManager.restoreMetadataRegistration(instance)).thenReturn(true);
|
||||
VpcNetworkManager vpcNetworks = mock(VpcNetworkManager.class);
|
||||
when(vpcNetworks.enabled()).thenReturn(true);
|
||||
Ec2Service service = new Ec2Service(mockConfig(false), containerManager,
|
||||
mock(Ec2PortForwardManager.class), mock(AmiImageResolver.class), mock(Ec2ImageCatalog.class),
|
||||
new Ec2InstanceTypeCatalog(),
|
||||
new InMemoryStorageFactory(Map.of("ec2-instances.json", instanceStore)), vpcNetworks);
|
||||
|
||||
service.restoreMetadataRegistrations();
|
||||
|
||||
assertEquals("running", instanceStore.get("us-east-1::i-pending").orElseThrow().getState().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoreFailsAPendingInstanceWithoutARecordedContainer() {
|
||||
AccountAwareStorageBackend<Instance> instanceStore = AccountAwareStorageBackend.inMemory("000000000000");
|
||||
Instance instance = persistedInstance("i-pending", "10.0.1.10", InstanceState.pending());
|
||||
instanceStore.put("us-east-1::i-pending", instance);
|
||||
Ec2Service service = new Ec2Service(mockConfig(false), mock(Ec2ContainerManager.class),
|
||||
mock(Ec2PortForwardManager.class), mock(AmiImageResolver.class), mock(Ec2ImageCatalog.class),
|
||||
new Ec2InstanceTypeCatalog(),
|
||||
new InMemoryStorageFactory(Map.of("ec2-instances.json", instanceStore)),
|
||||
mock(VpcNetworkManager.class));
|
||||
|
||||
service.restoreMetadataRegistrations();
|
||||
|
||||
Instance restored = instanceStore.get("us-east-1::i-pending").orElseThrow();
|
||||
assertEquals("terminated", restored.getState().getName());
|
||||
assertEquals("failed", restored.getLaunchStatus());
|
||||
assertEquals("impaired", restored.getSshStatus());
|
||||
assertEquals("startup-reconciliation", restored.getLaunchEvents().getLast().getStage());
|
||||
assertEquals("failed", restored.getLaunchEvents().getLast().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoreFailsPendingInstancesAcrossAccountPartitions() {
|
||||
AccountAwareStorageBackend<Instance> instanceStore = AccountAwareStorageBackend.inMemory("000000000000");
|
||||
Instance instance = persistedInstance("i-other-account", "10.0.1.10", InstanceState.pending());
|
||||
instance.setAccountId("111111111111");
|
||||
instanceStore.putForAccount("111111111111", "us-east-1::i-other-account", instance);
|
||||
Ec2Service service = new Ec2Service(mockConfig(false), mock(Ec2ContainerManager.class),
|
||||
mock(Ec2PortForwardManager.class), mock(AmiImageResolver.class), mock(Ec2ImageCatalog.class),
|
||||
new Ec2InstanceTypeCatalog(),
|
||||
new InMemoryStorageFactory(Map.of("ec2-instances.json", instanceStore)),
|
||||
mock(VpcNetworkManager.class));
|
||||
|
||||
service.restoreMetadataRegistrations();
|
||||
|
||||
Instance restored = instanceStore.getForAccount(
|
||||
"111111111111", "us-east-1::i-other-account").orElseThrow();
|
||||
assertEquals("terminated", restored.getState().getName());
|
||||
assertEquals("failed", restored.getLaunchStatus());
|
||||
assertTrue(instanceStore.getForAccount(
|
||||
"000000000000", "us-east-1::i-other-account").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoreFailsARunningInstanceWhoseLaunchWasInterrupted() {
|
||||
AccountAwareStorageBackend<Instance> instanceStore = AccountAwareStorageBackend.inMemory("000000000000");
|
||||
Instance instance = persistedInstance("i-interrupted", "10.0.1.10", InstanceState.running());
|
||||
instance.setLaunchStatus("in-progress");
|
||||
instanceStore.put("us-east-1::i-interrupted", instance);
|
||||
Ec2Service service = new Ec2Service(mockConfig(false), mock(Ec2ContainerManager.class),
|
||||
mock(Ec2PortForwardManager.class), mock(AmiImageResolver.class), mock(Ec2ImageCatalog.class),
|
||||
new Ec2InstanceTypeCatalog(),
|
||||
new InMemoryStorageFactory(Map.of("ec2-instances.json", instanceStore)),
|
||||
mock(VpcNetworkManager.class));
|
||||
|
||||
service.restoreMetadataRegistrations();
|
||||
|
||||
Instance restored = instanceStore.get("us-east-1::i-interrupted").orElseThrow();
|
||||
assertEquals("terminated", restored.getState().getName());
|
||||
assertEquals("failed", restored.getLaunchStatus());
|
||||
assertEquals("startup-reconciliation", restored.getLaunchEvents().getLast().getStage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void describeInstanceStatusIncludesFailedLaunchOnlyWhenRequested() {
|
||||
AccountAwareStorageBackend<Instance> instanceStore = AccountAwareStorageBackend.inMemory("000000000000");
|
||||
Instance instance = persistedInstance("i-failed", "10.0.1.10", InstanceState.terminated());
|
||||
instance.setLaunchStatus("failed");
|
||||
instance.setSshStatus("impaired");
|
||||
instance.setSshStatusMessage("No free port available in range 2200-2209");
|
||||
instanceStore.put("us-east-1::i-failed", instance);
|
||||
Ec2Service service = new Ec2Service(mockConfig(false), mock(Ec2ContainerManager.class),
|
||||
mock(Ec2PortForwardManager.class), mock(AmiImageResolver.class), mock(Ec2ImageCatalog.class),
|
||||
new Ec2InstanceTypeCatalog(),
|
||||
new InMemoryStorageFactory(Map.of("ec2-instances.json", instanceStore)));
|
||||
|
||||
assertTrue(service.describeInstanceStatus("us-east-1", List.of("i-failed"), false).isEmpty());
|
||||
List<Instance> statuses = service.describeInstanceStatus("us-east-1", List.of("i-failed"), true);
|
||||
assertEquals(1, statuses.size());
|
||||
assertEquals("impaired", statuses.getFirst().getSshStatus());
|
||||
assertEquals("No free port available in range 2200-2209", statuses.getFirst().getSshStatusMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void describeInstanceStatusClearsRecoveredSshLaunchFailure() {
|
||||
AccountAwareStorageBackend<Instance> instanceStore = AccountAwareStorageBackend.inMemory("000000000000");
|
||||
Instance instance = persistedInstance("i-recovered", "10.0.1.10", InstanceState.running());
|
||||
instance.setLaunchStatus("failed");
|
||||
instance.setSshStatus("impaired");
|
||||
instance.setStateReasonCode("Client.SSHUnavailable");
|
||||
instance.setStateReasonMessage("connection refused");
|
||||
instanceStore.put("us-east-1::i-recovered", instance);
|
||||
Ec2ContainerManager containerManager = mock(Ec2ContainerManager.class);
|
||||
when(containerManager.checkSshReadiness(instance))
|
||||
.thenReturn(new Ec2ContainerManager.OperationResult(true, "SSH banner received: SSH-2.0-test"));
|
||||
Ec2Service service = new Ec2Service(mockConfig(false), containerManager,
|
||||
mock(Ec2PortForwardManager.class), mock(AmiImageResolver.class), mock(Ec2ImageCatalog.class),
|
||||
new Ec2InstanceTypeCatalog(),
|
||||
new InMemoryStorageFactory(Map.of("ec2-instances.json", instanceStore)));
|
||||
|
||||
Instance status = service.describeInstanceStatus("us-east-1", List.of("i-recovered"), false).getFirst();
|
||||
|
||||
assertEquals("ready", status.getLaunchStatus());
|
||||
assertEquals("ok", status.getSshStatus());
|
||||
assertNull(status.getStateReasonCode());
|
||||
assertNull(status.getStateReasonMessage());
|
||||
}
|
||||
|
||||
private static Instance persistedInstance(String instanceId, String privateIp, InstanceState state) {
|
||||
Instance instance = new Instance();
|
||||
instance.setInstanceId(instanceId);
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package io.github.hectorvent.floci.services.lightsail;
|
||||
|
||||
import io.github.hectorvent.floci.core.common.docker.ContainerLifecycleManager;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class LightsailContainerManagerTest {
|
||||
|
||||
@Test
|
||||
void deploymentPathsAreRelativeAndTraversalFree() {
|
||||
assertDoesNotThrow(() -> LightsailContainerManager.validateDeploymentPath("assets/site.css"));
|
||||
assertThrows(IllegalArgumentException.class, () -> LightsailContainerManager.validateDeploymentPath("../index.html"));
|
||||
assertThrows(IllegalArgumentException.class, () -> LightsailContainerManager.validateDeploymentPath("assets/../../index.html"));
|
||||
assertThrows(IllegalArgumentException.class, () -> LightsailContainerManager.validateDeploymentPath("/etc/passwd"));
|
||||
assertThrows(IllegalArgumentException.class, () -> LightsailContainerManager.validateDeploymentPath("assets\\site.css"));
|
||||
assertThrows(IllegalArgumentException.class, () -> LightsailContainerManager.validateDeploymentPath("assets//site.css"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nodeDeploymentPathsRejectReservedFilesAndSupervisorUsesReleaseCommand() {
|
||||
assertDoesNotThrow(() -> LightsailContainerManager.validateNodePath("src/server.js"));
|
||||
assertThrows(IllegalArgumentException.class, () -> LightsailContainerManager.validateNodePath(".floci-start"));
|
||||
assertThrows(IllegalArgumentException.class, () -> LightsailContainerManager.validateNodePath("../package.json"));
|
||||
String command = String.join(" ", LightsailContainerManager.nodeSupervisorCommand());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(command.contains(".floci-start"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(command.contains("HOST=0.0.0.0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wordpressTerminationStrictlyRemovesContainersAndBothVolumes() {
|
||||
ContainerLifecycleManager lifecycleManager = mock(ContainerLifecycleManager.class);
|
||||
LightsailContainerManager manager = new LightsailContainerManager(null, lifecycleManager, null, null, null);
|
||||
|
||||
manager.terminate("wordpress", "mariadb", "mariadb-data", "wordpress-content");
|
||||
|
||||
InOrder order = inOrder(lifecycleManager);
|
||||
order.verify(lifecycleManager).stopAndRemoveStrict("wordpress", null);
|
||||
order.verify(lifecycleManager).stopAndRemoveStrict("mariadb", null);
|
||||
order.verify(lifecycleManager).removeVolumeStrict("mariadb-data");
|
||||
order.verify(lifecycleManager).removeVolumeStrict("wordpress-content");
|
||||
}
|
||||
}
|
||||
+129
-1
@@ -2,9 +2,13 @@ package io.github.hectorvent.floci.services.lightsail;
|
||||
|
||||
import io.github.hectorvent.floci.testing.RestAssuredJsonUtils;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.restassured.response.Response;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
@@ -24,6 +28,29 @@ class LightsailIntegrationTest {
|
||||
RestAssuredJsonUtils.configureAwsContentTypes();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void deleteTestInstanceContainers() {
|
||||
Response instances = given()
|
||||
.header("X-Amz-Target", TARGET_PREFIX + "GetInstances")
|
||||
.contentType(CONTENT_TYPE)
|
||||
.body("{}")
|
||||
.when()
|
||||
.post("/");
|
||||
List<String> instanceNames = instances.jsonPath().getList("instances.name", String.class);
|
||||
for (String instanceName : instanceNames) {
|
||||
given()
|
||||
.header("X-Amz-Target", TARGET_PREFIX + "DeleteInstance")
|
||||
.contentType(CONTENT_TYPE)
|
||||
.body("""
|
||||
{"instanceName":"%s"}
|
||||
""".formatted(instanceName))
|
||||
.when()
|
||||
.post("/")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void instanceDiskAndStaticIpLifecycleRoundTripsThroughJsonHandler() {
|
||||
given()
|
||||
@@ -34,7 +61,7 @@ class LightsailIntegrationTest {
|
||||
.post("/")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("blueprints", hasSize(4))
|
||||
.body("blueprints", hasSize(6))
|
||||
.body("blueprints[0].blueprintId", equalTo("ubuntu_22_04"));
|
||||
|
||||
given()
|
||||
@@ -198,6 +225,44 @@ class LightsailIntegrationTest {
|
||||
.body("operations[0].id", notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicPortRulesRemainAwsVisibleWithoutInternalForwardMappings() {
|
||||
given()
|
||||
.header("X-Amz-Target", TARGET_PREFIX + "CreateInstances")
|
||||
.contentType(CONTENT_TYPE)
|
||||
.body("""
|
||||
{"instanceNames":["port-web"],"availabilityZone":"us-east-1a","blueprintId":"ubuntu_22_04","bundleId":"nano_3_0"}
|
||||
""")
|
||||
.when()
|
||||
.post("/")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
|
||||
given()
|
||||
.header("X-Amz-Target", TARGET_PREFIX + "OpenInstancePublicPorts")
|
||||
.contentType(CONTENT_TYPE)
|
||||
.body("""
|
||||
{"instanceName":"port-web","portInfo":{"fromPort":8080,"toPort":8080,"protocol":"tcp","cidrs":["0.0.0.0/0"]}}
|
||||
""")
|
||||
.when()
|
||||
.post("/")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
|
||||
given()
|
||||
.header("X-Amz-Target", TARGET_PREFIX + "GetInstance")
|
||||
.contentType(CONTENT_TYPE)
|
||||
.body("""
|
||||
{"instanceName":"port-web"}
|
||||
""")
|
||||
.when()
|
||||
.post("/")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("instance.networking.ports.fromPort", hasItem(8080))
|
||||
.body("instance._portForwards", equalTo(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyPairsTagsRegionsAndUnsupportedOperationsUseAwsJsonShape() {
|
||||
given()
|
||||
@@ -303,6 +368,69 @@ class LightsailIntegrationTest {
|
||||
.body("message", equalTo("keyPairName is required"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void instanceSnapshotsCanBeCreatedRestoredAndDeleted() {
|
||||
given()
|
||||
.header("X-Amz-Target", TARGET_PREFIX + "CreateInstances")
|
||||
.contentType(CONTENT_TYPE)
|
||||
.body("""
|
||||
{"instanceNames":["snapshot-source"],"availabilityZone":"us-east-1a","blueprintId":"ubuntu_22_04","bundleId":"nano_3_0"}
|
||||
""")
|
||||
.when()
|
||||
.post("/")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
|
||||
given()
|
||||
.header("X-Amz-Target", TARGET_PREFIX + "CreateInstanceSnapshot")
|
||||
.contentType(CONTENT_TYPE)
|
||||
.body("""
|
||||
{"instanceName":"snapshot-source","instanceSnapshotName":"snapshot-one"}
|
||||
""")
|
||||
.when()
|
||||
.post("/")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("operations[0].operationType", equalTo("CreateInstanceSnapshot"));
|
||||
|
||||
given()
|
||||
.header("X-Amz-Target", TARGET_PREFIX + "GetInstanceSnapshot")
|
||||
.contentType(CONTENT_TYPE)
|
||||
.body("""
|
||||
{"instanceSnapshotName":"snapshot-one"}
|
||||
""")
|
||||
.when()
|
||||
.post("/")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("instanceSnapshot.fromInstanceName", equalTo("snapshot-source"))
|
||||
.body("instanceSnapshot.state", equalTo("available"));
|
||||
|
||||
given()
|
||||
.header("X-Amz-Target", TARGET_PREFIX + "CreateInstancesFromSnapshot")
|
||||
.contentType(CONTENT_TYPE)
|
||||
.body("""
|
||||
{"instanceSnapshotName":"snapshot-one","instanceNames":["snapshot-restored"]}
|
||||
""")
|
||||
.when()
|
||||
.post("/")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("operations[0].operationType", equalTo("CreateInstance"));
|
||||
|
||||
given()
|
||||
.header("X-Amz-Target", TARGET_PREFIX + "DeleteInstanceSnapshot")
|
||||
.contentType(CONTENT_TYPE)
|
||||
.body("""
|
||||
{"instanceSnapshotName":"snapshot-one"}
|
||||
""")
|
||||
.when()
|
||||
.post("/")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("operations[0].operationType", equalTo("DeleteInstanceSnapshot"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void diskStaticIpAndHardwareEdgeCasesMatchLightsailBehavior() {
|
||||
given()
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package io.github.hectorvent.floci.services.lightsail;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.github.hectorvent.floci.core.common.RegionResolver;
|
||||
import io.github.hectorvent.floci.core.storage.AccountAwareStorageBackend;
|
||||
import io.github.hectorvent.floci.core.storage.StorageFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class LightsailNodeJsTest {
|
||||
|
||||
private static final String REGION = "us-east-1";
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private LightsailService service;
|
||||
private LightsailContainerManager containerManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
StorageFactory storageFactory = new StorageFactory(null, null) {
|
||||
@Override
|
||||
public synchronized <V> AccountAwareStorageBackend<V> create(String serviceName, String fileName,
|
||||
TypeReference<Map<String, V>> typeReference) {
|
||||
return AccountAwareStorageBackend.inMemory("000000000000");
|
||||
}
|
||||
};
|
||||
containerManager = mock(LightsailContainerManager.class);
|
||||
service = new LightsailService(storageFactory, mapper, new RegionResolver(REGION, "000000000000"),
|
||||
containerManager, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nodeBlueprintUsesCurrentLtsRuntimeAndDoesNotExposeRuntimeMetadata() {
|
||||
LightsailContainerManager.Runtime runtime = LightsailContainerManager.runtimeFor("nodejs");
|
||||
assertEquals("node:24-bookworm", runtime.image());
|
||||
assertEquals("node", runtime.sshUser());
|
||||
assertEquals(3000, runtime.applicationPort());
|
||||
assertEquals("nodejs", service.getBlueprints().path("blueprints").get(5).path("blueprintId").asText());
|
||||
|
||||
when(containerManager.launch(eq("node-app"), eq(REGION), eq("nodejs"), eq("nano_3_0"), isNull(), anyString()))
|
||||
.thenReturn(new LightsailContainerManager.Launch("node-container", null, null, null, 22025,
|
||||
"node-app-volume", "1760000000000"));
|
||||
ObjectNode request = mapper.createObjectNode()
|
||||
.put("availabilityZone", REGION + "a")
|
||||
.put("blueprintId", "nodejs")
|
||||
.put("bundleId", "nano_3_0");
|
||||
request.set("instanceNames", mapper.createArrayNode().add("node-app"));
|
||||
|
||||
service.createInstances(REGION, request);
|
||||
|
||||
ObjectNode instance = (ObjectNode) service.getInstance(REGION, "node-app").path("instance");
|
||||
assertEquals("nodejs", instance.path("blueprintId").asText());
|
||||
assertEquals("node", instance.path("username").asText());
|
||||
assertEquals(3000, instance.path("networking").path("ports").get(1).path("fromPort").asInt());
|
||||
assertFalse(instance.has("applicationVolume"));
|
||||
assertFalse(instance.has("applicationRelease"));
|
||||
assertFalse(instance.has("runtimeMetadata"));
|
||||
|
||||
service.deleteInstance(REGION, "node-app");
|
||||
verify(containerManager).terminate("node-container", null, null, null, "node-app-volume");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nodeSupervisorInitializesPersistentReleaseLayoutAndRestartsApplications() {
|
||||
List<String> command = LightsailContainerManager.nodeSupervisorCommand();
|
||||
|
||||
assertEquals(List.of("sh", "-c"), command.subList(0, 2));
|
||||
String script = command.get(2);
|
||||
assertTrue(script.contains("$app/releases/$FLOCI_LIGHTSAIL_RELEASE_ID"));
|
||||
assertTrue(script.contains("ln -s \"releases/$FLOCI_LIGHTSAIL_RELEASE_ID\" \"$app/current\""));
|
||||
assertTrue(script.contains("tail -n +4"));
|
||||
assertTrue(script.contains("npm start"));
|
||||
assertTrue(script.contains("app.stdout.log"));
|
||||
assertTrue(script.contains("app.stderr.log"));
|
||||
assertTrue(script.contains("app/logs/supervisor.log"));
|
||||
assertTrue(script.contains("delay=$((delay * 2))"));
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package io.github.hectorvent.floci.services.lightsail;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class LightsailPortForwardManagerTest {
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void extractsOnlyTcpRulesWithCidrSources() {
|
||||
ArrayNode rules = mapper.createArrayNode()
|
||||
.add(rule("tcp", 8080, 8081, true))
|
||||
.add(rule("udp", 53, 53, true))
|
||||
.add(rule("tcp", 9090, 9090, false));
|
||||
|
||||
assertEquals(Set.of(8080, 8081), LightsailPortForwardManager.extractPublishablePorts(rules, 20));
|
||||
}
|
||||
|
||||
@Test
|
||||
void includesSshForOsContainerDirectBinding() {
|
||||
ArrayNode rules = mapper.createArrayNode().add(rule("tcp", 22, 22, true));
|
||||
|
||||
assertEquals(Set.of(22), LightsailPortForwardManager.extractPublishablePorts(rules, 20));
|
||||
}
|
||||
|
||||
@Test
|
||||
void skipsRulesWiderThanThePerInstanceLimit() {
|
||||
ArrayNode rules = mapper.createArrayNode().add(rule("tcp", 1, 100, true));
|
||||
|
||||
assertTrue(LightsailPortForwardManager.extractPublishablePorts(rules, 20).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardContainerNameIsDeterministic() {
|
||||
assertEquals("floci-lightsail-fwd-web-8080", LightsailPortForwardManager.forwardContainerName("web", 8080));
|
||||
}
|
||||
|
||||
private ObjectNode rule(String protocol, int fromPort, int toPort, boolean withCidr) {
|
||||
ObjectNode rule = mapper.createObjectNode()
|
||||
.put("protocol", protocol)
|
||||
.put("fromPort", fromPort)
|
||||
.put("toPort", toPort);
|
||||
if (withCidr) {
|
||||
rule.putArray("cidrs").add("0.0.0.0/0");
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -100,10 +100,10 @@ class LightsailResourceProviderTest {
|
||||
class GetSupportedResourceTypes {
|
||||
|
||||
@Test
|
||||
void advertisesFourLightsailTypesAllUnderLightsailService() {
|
||||
void advertisesLightsailResourceTypesAllUnderLightsailService() {
|
||||
Set<SupportedResourceType> types = service.getSupportedResourceTypes();
|
||||
assertEquals(Set.of("lightsail:Instance", "lightsail:Disk",
|
||||
"lightsail:StaticIp", "lightsail:KeyPair"),
|
||||
"lightsail:StaticIp", "lightsail:KeyPair", "lightsail:InstanceSnapshot"),
|
||||
types.stream().map(SupportedResourceType::resourceType).collect(Collectors.toSet()));
|
||||
assertTrue(types.stream().allMatch(t -> t.service().equals("lightsail")));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package io.github.hectorvent.floci.services.lightsail;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.github.hectorvent.floci.core.common.RegionResolver;
|
||||
import io.github.hectorvent.floci.core.storage.AccountAwareStorageBackend;
|
||||
import io.github.hectorvent.floci.core.storage.StorageFactory;
|
||||
import io.github.hectorvent.floci.services.ec2.Ec2KeyMaterial;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class LightsailSshKeyTest {
|
||||
|
||||
private static final String REGION = "us-east-1";
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private LightsailService service;
|
||||
private LightsailContainerManager containerManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
StorageFactory storageFactory = new StorageFactory(null, null) {
|
||||
@Override
|
||||
public synchronized <V> AccountAwareStorageBackend<V> create(String serviceName, String fileName,
|
||||
TypeReference<Map<String, V>> typeReference) {
|
||||
return AccountAwareStorageBackend.inMemory("000000000000");
|
||||
}
|
||||
};
|
||||
containerManager = mock(LightsailContainerManager.class);
|
||||
service = new LightsailService(storageFactory, mapper, new RegionResolver(REGION, "000000000000"),
|
||||
containerManager, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatedKeyPairReturnsUsablePrivatePemAndLaunchesWithItsPublicKey() {
|
||||
ObjectNode createKey = mapper.createObjectNode().put("keyPairName", "deployer");
|
||||
ObjectNode keyResponse = service.createKeyPair(REGION, createKey);
|
||||
String privateKey = new String(Base64.getDecoder().decode(keyResponse.path("privateKeyBase64").asText()),
|
||||
StandardCharsets.UTF_8);
|
||||
String publicKey = new String(Base64.getDecoder().decode(keyResponse.path("publicKeyBase64").asText()),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(privateKey.startsWith("-----BEGIN RSA PRIVATE KEY-----"));
|
||||
assertTrue(Ec2KeyMaterial.fingerprintOf(publicKey) != null);
|
||||
|
||||
when(containerManager.launch(anyString(), anyString(), anyString(), anyString(), isNull(), anyString()))
|
||||
.thenReturn(new LightsailContainerManager.Launch("container", null, null, 22022));
|
||||
ObjectNode createInstance = mapper.createObjectNode()
|
||||
.put("availabilityZone", REGION + "a")
|
||||
.put("blueprintId", "ubuntu_22_04")
|
||||
.put("bundleId", "nano_3_0")
|
||||
.put("keyPairName", "deployer");
|
||||
createInstance.set("instanceNames", mapper.createArrayNode().add("ssh-instance"));
|
||||
|
||||
service.createInstances(REGION, createInstance);
|
||||
|
||||
ArgumentCaptor<String> keyCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(containerManager).launch(eq("ssh-instance"), eq(REGION), eq("ubuntu_22_04"),
|
||||
eq("nano_3_0"), isNull(), keyCaptor.capture());
|
||||
assertEquals(publicKey, keyCaptor.getValue());
|
||||
assertEquals(22022, service.hostPortMappings(REGION, "ssh-instance").getFirst().get("hostPort"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultKeyPairIsGeneratedAndSelectedWhenNoKeyNameIsProvided() {
|
||||
when(containerManager.launch(anyString(), anyString(), anyString(), anyString(), isNull(), anyString()))
|
||||
.thenReturn(new LightsailContainerManager.Launch("container", null, null, 22023));
|
||||
ObjectNode request = mapper.createObjectNode()
|
||||
.put("availabilityZone", REGION + "a")
|
||||
.put("blueprintId", "amazon_linux_2023")
|
||||
.put("bundleId", "nano_3_0");
|
||||
request.set("instanceNames", mapper.createArrayNode().add("default-ssh"));
|
||||
|
||||
service.createInstances(REGION, request);
|
||||
|
||||
ObjectNode key = service.downloadDefaultKeyPair(REGION);
|
||||
String privateKey = new String(Base64.getDecoder().decode(key.path("privateKeyBase64").asText()), StandardCharsets.UTF_8);
|
||||
assertTrue(privateKey.startsWith("-----BEGIN RSA PRIVATE KEY-----"));
|
||||
assertEquals("LightsailDefaultKeyPair", service.getInstance(REGION, "default-ssh")
|
||||
.path("instance").path("sshKeyName").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importedKeyPairIsDecodedAndSelectedForTheInstance() {
|
||||
Ec2KeyMaterial.Generated generated = Ec2KeyMaterial.generateRsa();
|
||||
ObjectNode imported = mapper.createObjectNode().put("keyPairName", "imported").put("publicKeyBase64",
|
||||
Base64.getEncoder().encodeToString(generated.openSshPublicKey().getBytes(StandardCharsets.UTF_8)));
|
||||
service.importKeyPair(REGION, imported);
|
||||
when(containerManager.launch(anyString(), anyString(), anyString(), anyString(), isNull(), anyString()))
|
||||
.thenReturn(new LightsailContainerManager.Launch("container", null, null, 22024));
|
||||
ObjectNode request = mapper.createObjectNode()
|
||||
.put("availabilityZone", REGION + "a")
|
||||
.put("blueprintId", "ubuntu_24_04")
|
||||
.put("bundleId", "nano_3_0")
|
||||
.put("keyPairName", "imported");
|
||||
request.set("instanceNames", mapper.createArrayNode().add("imported-ssh"));
|
||||
|
||||
service.createInstances(REGION, request);
|
||||
|
||||
ArgumentCaptor<String> keyCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(containerManager).launch(eq("imported-ssh"), eq(REGION), eq("ubuntu_24_04"),
|
||||
eq("nano_3_0"), isNull(), keyCaptor.capture());
|
||||
assertEquals(generated.openSshPublicKey(), keyCaptor.getValue());
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package io.github.hectorvent.floci.services.lightsail;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.github.hectorvent.floci.core.common.RegionResolver;
|
||||
import io.github.hectorvent.floci.core.storage.AccountAwareStorageBackend;
|
||||
import io.github.hectorvent.floci.core.storage.StorageFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class LightsailWordPressTest {
|
||||
|
||||
private static final String REGION = "us-east-1";
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private LightsailService service;
|
||||
private LightsailContainerManager containerManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
StorageFactory storageFactory = new StorageFactory(null, null) {
|
||||
@Override
|
||||
public synchronized <V> AccountAwareStorageBackend<V> create(String serviceName, String fileName,
|
||||
TypeReference<Map<String, V>> typeReference) {
|
||||
return AccountAwareStorageBackend.inMemory("000000000000");
|
||||
}
|
||||
};
|
||||
containerManager = mock(LightsailContainerManager.class);
|
||||
service = new LightsailService(storageFactory, mapper, new RegionResolver(REGION, "000000000000"),
|
||||
containerManager, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void wordpressLaunchesAndManagesMariaDbWithoutLeakingRuntimeFields() {
|
||||
when(containerManager.launch(eq("blog"), eq(REGION), eq("wordpress"), eq("nano_3_0"), isNull(), anyString()))
|
||||
.thenReturn(new LightsailContainerManager.Launch("wordpress-id", "mariadb-id", "mariadb-data",
|
||||
"wordpress-content", null));
|
||||
|
||||
ObjectNode request = mapper.createObjectNode()
|
||||
.put("availabilityZone", REGION + "a")
|
||||
.put("blueprintId", "wordpress")
|
||||
.put("bundleId", "nano_3_0");
|
||||
request.set("instanceNames", mapper.createArrayNode().add("blog"));
|
||||
service.createInstances(REGION, request);
|
||||
|
||||
ObjectNode instance = (ObjectNode) service.getInstance(REGION, "blog").path("instance");
|
||||
assertEquals("wordpress", instance.path("blueprintId").asText());
|
||||
assertEquals(80, instance.path("networking").path("ports").get(1).path("fromPort").asInt());
|
||||
assertFalse(instance.has("containerId"));
|
||||
assertFalse(instance.has("databaseContainerId"));
|
||||
assertFalse(instance.has("databaseVolume"));
|
||||
assertFalse(instance.has("wordpressContentVolume"));
|
||||
|
||||
service.stopInstance(REGION, "blog");
|
||||
service.startInstance(REGION, "blog");
|
||||
service.rebootInstance(REGION, "blog");
|
||||
service.deleteInstance(REGION, "blog");
|
||||
|
||||
verify(containerManager).stop("wordpress-id", "mariadb-id");
|
||||
verify(containerManager).start("wordpress-id", "mariadb-id", false);
|
||||
verify(containerManager).reboot("wordpress-id", "mariadb-id", false);
|
||||
verify(containerManager).terminate("wordpress-id", "mariadb-id", "mariadb-data", "wordpress-content", null);
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ class AmiImageToolTest {
|
||||
String catalogText = Files.readString(catalog);
|
||||
assertTrue(catalogText.contains("ami-existing"));
|
||||
assertTrue(catalogText.contains("ami-ubuntu2404-cloud-arm64"));
|
||||
assertTrue(catalogText.contains("guestRuntime: \"systemd\""));
|
||||
assertTrue(catalogText.contains("initType: \"systemd\""));
|
||||
assertTrue(catalogText.contains("cloudInit: true"));
|
||||
assertTrue(catalogText.contains("custom-output/ubuntu-24.04-arm64/provenance.yaml"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user