Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!--
AI Context: This is Floci-Az, a lightweight Local Azure Emulator.
Identity: It is the Azure equivalent of Floci (AWS). It is NOT LocalStack.
Protocols: Implements Azure Storage (Blob, Queue, Table), Azure Functions, App Configuration, Key Vault, Event Hubs, Service Bus (Microsoft.ServiceBus), Cosmos DB, Azure SQL Database, Azure Database for PostgreSQL (Microsoft.DBforPostgreSQL), Azure Kubernetes Service (AKS), API Management (Microsoft.ApiManagement), Virtual Network (Microsoft.Network), Virtual Machines (Microsoft.Compute), Azure Cache for Redis (Microsoft.Cache), Azure Container Registry (Microsoft.ContainerRegistry), Azure Container Instances (Microsoft.ContainerInstance), Event Grid (Microsoft.EventGrid), Azure Monitor / Log Analytics (Microsoft.OperationalInsights / Microsoft.Insights), Communication Services Email (Microsoft.Communication), Managed Identity (Microsoft.ManagedIdentity + IMDS token endpoint), Microsoft Entra ID (OpenID Connect / OAuth2 token issuance, including interactive auth-code+PKCE sign-in), and a narrow Microsoft Graph slice (service principal discovery, group membership).
Protocols: Implements Azure Storage (Blob, Queue, Table), Azure Functions, App Configuration, Key Vault, Event Hubs, Service Bus (Microsoft.ServiceBus), Cosmos DB, Azure SQL Database, Azure Database for PostgreSQL (Microsoft.DBforPostgreSQL), Azure Kubernetes Service (AKS), Azure Container Apps (Microsoft.App), API Management (Microsoft.ApiManagement), Virtual Network (Microsoft.Network), Virtual Machines (Microsoft.Compute), Azure Cache for Redis (Microsoft.Cache), Azure Container Registry (Microsoft.ContainerRegistry), Azure Container Instances (Microsoft.ContainerInstance), Event Grid (Microsoft.EventGrid), Azure Monitor / Log Analytics (Microsoft.OperationalInsights / Microsoft.Insights), Communication Services Email (Microsoft.Communication), Managed Identity (Microsoft.ManagedIdentity + IMDS token endpoint), Microsoft Entra ID (OpenID Connect / OAuth2 token issuance, including interactive auth-code+PKCE sign-in), and a narrow Microsoft Graph slice (service principal discovery, group membership).
Default Port: 4577 (HTTP; also HTTPS when FLOCI_AZ_TLS_ENABLED=true via protocol-sniffing proxy). AMQP port: 5672 (Event Hubs). Kafka port: 9093 (Event Hubs, opt-in). k3s API: 6443-7443 (AKS). Redis: 6379-6399 (Azure Cache for Redis).
Tech Stack: Java, Quarkus, Docker-in-Docker for Functions. Artemis sidecar for Event Hubs AMQP. Redpanda sidecar for Kafka. k3s sidecar for AKS. Redis sidecar for Azure Cache for Redis.
TLS: Optional. Set FLOCI_AZ_TLS_ENABLED=true. Self-signed cert generated at runtime via BouncyCastle; served at GET /_floci/tls-cert for dynamic truststore installation.
Expand Down Expand Up @@ -167,6 +167,7 @@ Floci AZ gives you more services than the official local tools, consolidated on
| Azure SQL Database | ✅ | ❌ | ❌ |
| Azure DB for PostgreSQL | ✅ | ❌ | ❌ |
| AKS (Kubernetes) | ✅ | ❌ | ❌ |
| Container Apps | ✅ | ❌ | ❌ |
| API Management | ✅ | ❌ | ❌ |
| Virtual Machines | ✅ | ❌ | ❌ |
| Azure Cache for Redis | ✅ | ❌ | ❌ |
Expand Down Expand Up @@ -316,6 +317,7 @@ flowchart LR
| **Azure SQL Database** | ARM path + `/{account}-sql/` | Servers, databases, firewall rules; ARM-only by default, optional managed SQL Server 2025 containers |
| **Azure Database for PostgreSQL** | ARM path (`Microsoft.DBforPostgreSQL`) + `/{account}-postgres/` | Flexible servers, databases, firewall rules, configurations; Docker-backed `postgres:17-alpine` containers (no EULA), dynamic port allocation, or mocked |
| **Azure Kubernetes Service** | ARM path (`Microsoft.ContainerService`) | CreateOrUpdate, Get, Delete, List, agent pools, kubeconfig (`listClusterAdminCredential`); real k3s containers or mocked |
| **Azure Container Apps** | ARM path (`Microsoft.App`) + environment-specific app FQDN | Managed environment/app CRUD, Single/Multiple revisions, secrets, min/max replicas, Docker-backed HTTP ingress or mocked |
| **API Management** | ARM path (`Microsoft.ApiManagement`) + `/{account}-apim/{service}/` | In-process APIM emulator for ARM resources, gateway routing, products/subscriptions, named values, backends, OpenAPI import, and a focused policy subset |
| **Virtual Network** | ARM path (`Microsoft.Network`) | VNet, subnet, NIC, public IP, NSG, private DNS zone (+ virtual network links, record sets), private endpoint (+ private DNS zone groups), and private link service ARM resources; subnet listing is scoped to the parent VNet; NIC private IPs are synthesized for VM/Terraform compatibility |
| **Virtual Machines** | ARM path (`Microsoft.Compute`) | VM lifecycle (create/start/stop/deallocate/restart/delete/list), instanceView power state; mocked: no Docker (container backing planned) |
Expand Down Expand Up @@ -396,6 +398,7 @@ Floci AZ uses real Docker containers when in-process emulation would reduce fide
| Azure SQL Database | `mcr.microsoft.com/mssql/server:2025-latest` | Optional managed SQL Server engine (per server) |
| Azure Database for PostgreSQL | `postgres:17-alpine` | PostgreSQL engine (per flexible server) |
| AKS | `rancher/k3s:latest` | Kubernetes API server via k3s |
| Azure Container Apps | User-provided images | Revision replicas with HTTP ingress through floci-az |
| Azure Cache for Redis | `valkey/valkey:8-alpine` | Redis / Valkey protocol (per cache) |
| Azure Container Registry | `registry:2` | OCI-compatible registry for docker push and docker pull (shared) |

Expand Down Expand Up @@ -642,7 +645,7 @@ The [`compatibility-tests`](./compatibility-tests/) directory validates Floci AZ
| Module | Language / Tool | Coverage | Tests |
|---------------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------|------:|
| `sdk-test-python` | Python 3 | Blob, Queue, Table, Cosmos, App Configuration, Key Vault, ACR, Redis | 124 |
| `sdk-test-java` | Java 21 | Storage, Cosmos (+ Mongo/PostgreSQL/Cassandra/Gremlin/Table/NoSQL engines), App Config, Key Vault, Event Hubs, Service Bus, Functions, API Management, SQL, PostgreSQL (Flexible Server) | 252 |
| `sdk-test-java` | Java 21 | Storage, Cosmos (+ Mongo/PostgreSQL/Cassandra/Gremlin/Table/NoSQL engines), App Config, Key Vault, Event Hubs, Service Bus, Functions, Container Apps, API Management, SQL, PostgreSQL (Flexible Server) | 253 |
| `sdk-test-node` | Node.js | App Configuration, Blob, Cosmos, Event Hubs, Key Vault, Queue, Table | 72 |
| `sdk-test-cpp` † | C++ 17 | Blob, Queue: Lifecycle plus the bodyless-error path no other SDK reproduces | 10 |
| `compat-terraform` | Terraform | `azurerm` provider apply/destroy (resource group, storage, key vault, VNet, VM, Redis, ACR) | 12 |
Expand Down
16 changes: 16 additions & 0 deletions compatibility-tests/sdk-test-java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
</dependency>
<dependency>
<groupId>com.azure.resourcemanager</groupId>
<artifactId>azure-resourcemanager-appcontainers</artifactId>
<version>1.2.0</version>
</dependency>
<!-- appcontainers 1.2.0 requires newer management polling APIs than BOM 1.2.28. -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core</artifactId>
<version>1.57.0</version>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core-management</artifactId>
<version>1.19.2</version>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-messaging-servicebus</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package io.floci.az.compat;

import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpPipeline;
import com.azure.core.http.HttpPipelineBuilder;
import com.azure.core.management.AzureEnvironment;
import com.azure.core.management.profile.AzureProfile;
import com.azure.resourcemanager.appcontainers.ContainerAppsApiManager;
import com.azure.resourcemanager.appcontainers.fluent.models.ContainerAppInner;
import com.azure.resourcemanager.appcontainers.fluent.models.ManagedEnvironmentInner;
import com.azure.resourcemanager.appcontainers.models.ActiveRevisionsMode;
import com.azure.resourcemanager.appcontainers.models.Configuration;
import com.azure.resourcemanager.appcontainers.models.Container;
import com.azure.resourcemanager.appcontainers.models.ContainerApp;
import com.azure.resourcemanager.appcontainers.models.ContainerAppProvisioningState;
import com.azure.resourcemanager.appcontainers.models.ContainerResources;
import com.azure.resourcemanager.appcontainers.models.EnvironmentProvisioningState;
import com.azure.resourcemanager.appcontainers.models.EnvironmentVar;
import com.azure.resourcemanager.appcontainers.models.Ingress;
import com.azure.resourcemanager.appcontainers.models.ManagedEnvironment;
import com.azure.resourcemanager.appcontainers.models.Revision;
import com.azure.resourcemanager.appcontainers.models.Scale;
import com.azure.resourcemanager.appcontainers.models.Secret;
import com.azure.resourcemanager.appcontainers.models.Template;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/** Compatibility coverage using Microsoft's generated Container Apps management client. */
@DisplayName("Azure Container Apps Java SDK Compatibility")
class ContainerAppsCompatibilityTest {

private static final String ENDPOINT = EmulatorConfig.httpBase();
private static final String SUBSCRIPTION = "00000000-0000-0000-0000-000000000001";
private static final String TENANT = "00000000-0000-0000-0000-000000000002";
private static final String RESOURCE_GROUP = "containerapps-rg-" + suffix();
private static final String ENVIRONMENT = "env-" + suffix();
private static final String APP = "app-" + suffix();

private static ContainerAppsApiManager manager;

@BeforeAll
static void setup() {
EmulatorConfig.assumeEmulatorRunning();
Map<String, String> endpoints = new HashMap<>(AzureEnvironment.AZURE.getEndpoints());
endpoints.put("resourceManagerEndpointUrl", ENDPOINT + "/");
endpoints.put("managementEndpointUrl", ENDPOINT + "/");
AzureProfile profile = new AzureProfile(TENANT, SUBSCRIPTION, new AzureEnvironment(endpoints));
HttpPipeline pipeline = new HttpPipelineBuilder()
.httpClient(HttpClient.createDefault())
.build();
manager = ContainerAppsApiManager.authenticate(pipeline, profile);
}

@Test
void fullLifecycleThroughOfficialSdk() {
ManagedEnvironmentInner environmentRequest = new ManagedEnvironmentInner()
.withLocation("eastus")
.withTags(Map.of("suite", "java-sdk"));
ManagedEnvironmentInner environment = manager.serviceClient().getManagedEnvironments()
.createOrUpdate(RESOURCE_GROUP, ENVIRONMENT, environmentRequest);

assertEquals(EnvironmentProvisioningState.SUCCEEDED, environment.provisioningState());
assertTrue(environment.defaultDomain().endsWith(".azurecontainerapps.io"));

String environmentId = environment.id();
Configuration configuration = new Configuration()
.withActiveRevisionsMode(ActiveRevisionsMode.SINGLE)
.withSecrets(List.of(new Secret().withName("message").withValue("hello")))
.withIngress(new Ingress().withExternal(true).withTargetPort(80));
Template template = new Template()
.withRevisionSuffix("v1")
.withContainers(List.of(new Container()
.withName("web")
.withImage("nginx:alpine")
.withEnv(List.of(new EnvironmentVar().withName("MESSAGE").withSecretRef("message")))
.withResources(new ContainerResources().withCpu(0.25).withMemory("0.5Gi"))))
.withScale(new Scale().withMinReplicas(1).withMaxReplicas(2));
ContainerAppInner appRequest = new ContainerAppInner()
.withLocation("eastus")
.withEnvironmentId(environmentId)
.withConfiguration(configuration)
.withTemplate(template)
.withTags(Map.of("suite", "java-sdk"));

ContainerAppInner created = manager.serviceClient().getContainerApps()
.createOrUpdate(RESOURCE_GROUP, APP, appRequest);
assertEquals(ContainerAppProvisioningState.SUCCEEDED, created.provisioningState());
assertEquals(APP + "--v1", created.latestRevisionName());
assertEquals("message", manager.containerApps().listSecrets(RESOURCE_GROUP, APP)
.value().getFirst().name());

ContainerApp fetched = manager.containerApps().getByResourceGroup(RESOURCE_GROUP, APP);
assertEquals(APP, fetched.name());
assertEquals(environmentId, fetched.environmentId());
assertEquals(1, fetched.template().scale().minReplicas());
assertEquals("java-sdk", fetched.tags().get("suite"));

List<Revision> revisions = new ArrayList<>();
manager.containerAppsRevisions().listRevisions(RESOURCE_GROUP, APP).forEach(revisions::add);
assertEquals(1, revisions.size());
assertEquals(APP + "--v1", revisions.getFirst().name());
assertEquals(1, revisions.getFirst().replicas());
assertTrue(revisions.getFirst().active());

List<ContainerApp> apps = new ArrayList<>();
manager.containerApps().listByResourceGroup(RESOURCE_GROUP).forEach(apps::add);
assertTrue(apps.stream().anyMatch(app -> APP.equals(app.name())));
List<ManagedEnvironment> environments = new ArrayList<>();
manager.managedEnvironments().listByResourceGroup(RESOURCE_GROUP).forEach(environments::add);
assertTrue(environments.stream().anyMatch(value -> ENVIRONMENT.equals(value.name())));

manager.serviceClient().getContainerApps().delete(RESOURCE_GROUP, APP);
manager.serviceClient().getManagedEnvironments().delete(RESOURCE_GROUP, ENVIRONMENT);
}

private static String suffix() {
return UUID.randomUUID().toString().substring(0, 8);
}
}
10 changes: 10 additions & 0 deletions docs/configuration/advanced/application-yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ floci-az:
app-config:
# mode: persistent
flush-interval-ms: 5000
container-apps:
# mode: persistent
flush-interval-ms: 5000

dns:
# When floci-az runs inside Docker, an embedded DNS server starts on UDP/53
Expand Down Expand Up @@ -75,6 +78,11 @@ floci-az:
container-idle-timeout-seconds: 300
app-config:
enabled: true
container-apps:
enabled: true
mocked: true
dns-suffix: azurecontainerapps.io
ingress-timeout-seconds: 60
```

## Key Environment Variables
Expand All @@ -89,8 +97,10 @@ floci-az:
| `FLOCI_AZ_STORAGE_SERVICES_QUEUE_MODE` | _(global)_ | Per-service queue mode |
| `FLOCI_AZ_STORAGE_SERVICES_TABLE_MODE` | _(global)_ | Per-service table mode |
| `FLOCI_AZ_STORAGE_SERVICES_APP_CONFIG_MODE` | _(global)_ | Per-service App Configuration mode |
| `FLOCI_AZ_STORAGE_SERVICES_CONTAINER_APPS_MODE` | _(global)_ | Per-service Container Apps mode |
| `FLOCI_AZ_SERVICES_FUNCTIONS_EPHEMERAL` | `false` | Fresh container per invocation |
| `FLOCI_AZ_SERVICES_FUNCTIONS_CONTAINER_IDLE_TIMEOUT_SECONDS` | `300` | Evict warm containers idle longer than this (seconds); `0` disables eviction |
| `FLOCI_AZ_SERVICES_APP_CONFIG_ENABLED` | `true` | Enable/disable App Configuration |
| `FLOCI_AZ_SERVICES_CONTAINER_APPS_MOCKED` | `true` | Keep Container Apps ARM state without Docker runtimes |
| `FLOCI_AZ_SERVICES_FUNCTIONS_CODE_PATH` | `~/.floci-az/functions` | Function code directory |
| `FLOCI_AZ_DOCKER_DOCKER_HOST` | `unix:///var/run/docker.sock` | Docker daemon socket |
Loading