Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
376cd4d
test: add locked Ruby SDK compatibility harness
vedit Sep 9, 2026
776212f
feat: add scoped Compute operations and network resources
vedit Sep 9, 2026
ca75cca
feat: emulate Compute instances and zonal disk lifecycle
vedit Sep 9, 2026
7c21f14
feat: emulate global Compute images and snapshots
vedit Sep 9, 2026
980dfb1
feat: emulate global application load balancer resources
vedit Sep 9, 2026
ffbd91b
feat: add persistent GCS XML multipart upload lifecycle
vedit Sep 9, 2026
b04d284
fix: scope Monitoring storage to the requested project
vedit Sep 9, 2026
32ab475
fix: align Compute mutations with official SDK wire contracts
vedit Sep 9, 2026
bf34544
fix: default blank object content types to binary media
vedit Sep 9, 2026
6021b67
test: cover cloud SDK lifecycles transfers and restart recovery
vedit Sep 9, 2026
bc33baa
ci: pin and record the native runtime image
vedit Sep 9, 2026
52de2df
ci: retain Java SDK reports from every restart phase
vedit Sep 9, 2026
f325236
test: verify multipart pagination and optimistic concurrency
vedit Sep 9, 2026
7e5252c
ci: probe native health within the isolated network
vedit Sep 9, 2026
4cbc40a
ci: allow isolated SDK containers to write test evidence
vedit Sep 9, 2026
8c8fcb0
fix(compute): preserve exact address ownership and native release sem…
vedit Sep 9, 2026
bb53340
fix(gcs): finalize multipart metadata before publishing events
vedit Sep 9, 2026
9ad82ec
ci: warm Ruby SDK dependency caches
vedit Sep 9, 2026
9b4d6c8
fix(gcs): restore compatibility with the updated Java Storage SDK
vedit Sep 11, 2026
060bb64
chore: merge upstream GCS deletion and GKE updates
vedit Sep 11, 2026
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
108 changes: 108 additions & 0 deletions .github/workflows/cloud-contracts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Cloud SDK contracts

on:
push:
branches: ['codex/vagon-integration']
workflow_dispatch:

permissions:
contents: read

concurrency:
group: cloud-contracts-${{ github.ref }}
cancel-in-progress: true

jobs:
native-contracts:
runs-on: ubuntu-24.04-arm
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- uses: graalvm/setup-graalvm@5298d94fb55a4f185c602eeac5de1b553882abe2
with:
java-version: '25'
distribution: mandrel
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: JVM regression suite
run: mvn -B -ntp test
- name: Preserve JVM results
if: always()
uses: actions/upload-artifact@v7
with:
name: jvm-regression
path: target/surefire-reports/*.xml
- name: Native image check
run: |
mvn -B -ntp package -Dnative -DskipTests -Dquarkus.native.additional-build-args-append=-Ob
mkdir -p native/arm64 evidence
cp target/*-runner native/arm64/application
cp target/*.so native/arm64/ 2>/dev/null || true
docker pull registry.access.redhat.com/ubi9-micro:9.7
docker image inspect registry.access.redhat.com/ubi9-micro:9.7 --format '{{index .RepoDigests 0}}' > evidence/native-runtime.txt
docker build -f docker/Dockerfile.native-package --build-arg TARGETARCH=arm64 --build-arg NATIVE_RUNTIME_IMAGE="$(cat evidence/native-runtime.txt)" --label org.opencontainers.image.revision="$GITHUB_SHA" -t floci-contract-native .
docker image inspect floci-contract-native --format '{{.Id}}' > evidence/native-image.txt
git rev-parse HEAD > evidence/source-sha.txt
sha256sum native/arm64/application > evidence/native-binary.txt
- name: Prepare isolated SDK clients
run: |
docker build --build-arg RUBY_BUILD_IMAGE=ruby:3.4.5@sha256:73e085e58f9496502b0463a787b497b961d946bc1db43c6782c0c3c95281e432 -t floci-contract-ruby compatibility-tests/sdk-test-ruby
docker pull maven:3.9-eclipse-temurin-25@sha256:d67198007bb4441b07d45587320f83154de80ece3608f80408ef14c6ea847753
docker run --rm -v "$PWD/compatibility-tests/sdk-test-java:/workspace" -v "$HOME/.m2:/root/.m2" -w /workspace \
maven:3.9-eclipse-temurin-25@sha256:d67198007bb4441b07d45587320f83154de80ece3608f80408ef14c6ea847753 \
mvn -B -ntp -DskipTests test dependency:go-offline dependency:get -Dartifact=org.apache.maven.surefire:surefire-junit-platform:3.5.5
docker run --rm -v "$HOME/.m2:/root/.m2" \
maven:3.9-eclipse-temurin-25@sha256:d67198007bb4441b07d45587320f83154de80ece3608f80408ef14c6ea847753 \
mvn -B -ntp dependency:get -Dartifact=org.junit.platform:junit-platform-launcher:1.12.2
docker network create --internal contracts
docker run -d --name floci-gcp --network contracts \
-e FLOCI_GCP_BASE_URL=http://floci-gcp:4588 -e FLOCI_GCP_STORAGE_MODE=persistent \
-e FLOCI_GCP_SERVICES_KAFKA_MOCK=true -e FLOCI_GCP_SERVICES_CLOUDRUN_MOCK=true \
-e FLOCI_GCP_SERVICES_CLOUDSQL_MOCK=true -e FLOCI_GCP_SERVICES_GKE_MOCK=true \
"$(cat evidence/native-image.txt)"
timeout 60 bash -c 'until docker exec floci-gcp /usr/local/bin/healthcheck.sh; do sleep 1; done'
- name: Java and Ruby native SDK contracts with restart
run: |
ruby_contract() {
mkdir -p "evidence/ruby-$1"
# Capability-free containers still need write access to synthetic results/state.
chmod 777 evidence "evidence/ruby-$1"
docker run --rm --network contracts --cap-drop=ALL --security-opt=no-new-privileges:true \
-e FLOCI_GCP_ENDPOINT=http://floci-gcp:4588 -e FLOCI_GCP_GRPC_ENDPOINT=floci-gcp:4588 \
-e FLOCI_GCP_RESTART_PHASE="$1" -e FLOCI_GCP_RESTART_STATE=/state/ruby-restart.json \
-v "$PWD/evidence:/state" -v "$PWD/evidence/ruby-$1:/results" --entrypoint bundle \
floci-contract-ruby exec ruby "test/$2.rb"
}
java_contract() {
mkdir -p "evidence/java-$1"
chmod 777 evidence "evidence/java-$1"
local status=0
docker run --rm --network contracts --cap-drop=ALL --security-opt=no-new-privileges:true \
-e FLOCI_GCP_ENDPOINT=http://floci-gcp:4588 -e FLOCI_GCP_GRPC_ENDPOINT=floci-gcp:4588 \
-e FLOCI_GCP_RESTART_PHASE="$1" -e FLOCI_GCP_RESTART_STATE=/state/java-restart.properties \
-v "$PWD/evidence:/state" -v "$PWD/compatibility-tests/sdk-test-java:/workspace" -v "$HOME/.m2:/root/.m2:ro" -w /workspace --entrypoint /bin/bash \
maven:3.9-eclipse-temurin-25@sha256:d67198007bb4441b07d45587320f83154de80ece3608f80408ef14c6ea847753 \
-c 'rm -f target/surefire-reports/*.xml; exec mvn -o -B -ntp -Dtest="$1" test' _ "$2" || status=$?
cp compatibility-tests/sdk-test-java/target/surefire-reports/*.xml "evidence/java-$1/"
return "$status"
}
ruby_contract all all
java_contract roundtrip 'ComputeContractTest,ComputeRoutingContractTest,GcsXmlMultipartContractTest,NetworkUsageContractTest,StorageContractTest,GcsDownscopedTokenTest,GcsResumableUploadTest,GcsSignedUrlTest,GcsGrpcTest,GcsServiceAccountAuthenticationTest'
ruby_contract seed restart_probe
java_contract seed RestartContractTest
docker restart floci-gcp
timeout 60 bash -c 'until docker exec floci-gcp /usr/local/bin/healthcheck.sh; do sleep 1; done'
ruby_contract verify restart_probe
java_contract verify RestartContractTest
- name: Teardown and logs
if: always()
run: |
mkdir -p evidence
docker logs floci-gcp > evidence/emulator.log 2>&1 || true
docker rm -fv floci-gcp || true
docker network rm contracts || true
- name: Contract evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: native-sdk-contracts
path: evidence/
1 change: 1 addition & 0 deletions .github/workflows/compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ jobs:
matrix:
test:
- sdk-test-java
- sdk-test-ruby

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Ruby Cache Warming Is Missing

Adding sdk-test-ruby only to the compatibility matrix breaks the documented requirement that the cache-warming matrix mirror it exactly. Pull-request jobs use a read-only cache, so the Ruby image will always build cold instead of receiving a cache populated from main. Add the Ruby target to warm-compat-caches.yml.

Knowledge Base Used:

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9ad82ec. sdk-test-ruby is now in warm-compat-caches.yml, and changes to its Gemfile or Gemfile.lock trigger warming. Runner architecture and cache scope still match compatibility.yml; PR jobs remain cache readers.

Validated exact matrix equality and both dependency triggers. The pinned Ruby image and SDK/restart suites are also exercised in the linked validation run. Base-branch cache publication occurs through the existing warming workflow after merge; this does not claim the upstream main cache is already warm.

Validation at 9ad82ec: all 986 hosted JVM tests and 30 native Java/Ruby SDK and restart checks passed. Run and JUnit artifacts.

- sdk-test-node
- sdk-test-python
- sdk-test-go
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/warm-compat-caches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ on:
- 'compatibility-tests/*/Dockerfile'
- 'compatibility-tests/*/pom.xml' # sdk-test-java
- 'compatibility-tests/*/requirements.txt' # sdk-test-python
- 'compatibility-tests/sdk-test-ruby/Gemfile'
- 'compatibility-tests/sdk-test-ruby/Gemfile.lock'
- 'compatibility-tests/*/package*.json' # sdk-test-node
# sdk-test-go and sdk-test-rust are the exceptions: both Dockerfiles do
# `COPY . .` *before* `go mod tidy` / `cargo build --locked`, so their
Expand Down Expand Up @@ -63,6 +65,7 @@ jobs:
# Mirrors compatibility.yml's native-compat-test legs exactly.
test:
- sdk-test-java
- sdk-test-ruby
- sdk-test-node
- sdk-test-python
- sdk-test-go
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ floci-gcp emulates GCP services across storage, messaging, identity, and managed
| Messaging and events | Pub/Sub, Managed Kafka, Eventarc |
| Security and identity | Secret Manager, Cloud KMS, IAM, IAM Service Account Credentials, Firebase Auth (Identity Platform) |
| Container orchestration | GKE (Kubernetes Engine) |
| Virtual machine control plane | Compute Engine, VPC networking, global external application load-balancer configuration |
| Serverless control planes | Cloud Run, Cloud Functions |
| Task scheduling | Cloud Tasks, Cloud Scheduler |
| Databases | Cloud SQL for PostgreSQL |
Expand All @@ -217,6 +218,7 @@ floci-gcp emulates GCP services across storage, messaging, identity, and managed

| Service | Protocol | Notable features |
|---|---|---|
| **[Compute Engine](docs/services/compute.md)** | REST JSON | Scoped operations, synthetic catalogs, VPC/subnets/firewalls/addresses, VM and disk lifecycle, images/snapshots, NEGs and global HTTP load-balancer configuration; no guest execution or traffic forwarding |
| **Cloud Storage (GCS)** | gRPC v2 + REST XML + REST JSON | Buckets, objects, streaming and resumable upload, ranged download, object compose, ACLs, bucket IAM, conditional requests (preconditions), versioning, lifecycle, CORS, pre-signed URLs (V4), batch API, Pub/Sub object notifications, customer-supplied encryption keys (CSEK) |
| **Pub/Sub** | gRPC + REST JSON | Topics, subscriptions, publish, pull, streaming pull, push delivery, snapshots, seek, field masks on update, subscription filters (attribute filter language) |
| **Firestore** | gRPC | Documents, collections, queries (all operators), field transforms, aggregation (COUNT), transactions, batch writes, real-time listeners (`listen` stream) |
Expand Down
9 changes: 9 additions & 0 deletions compatibility-tests/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ default:

test-all:
just test-java
just test-ruby
just test-python
just test-node
just test-go
Expand Down Expand Up @@ -129,3 +130,11 @@ setup: setup-java setup-python setup-node setup-go setup-rust
[working-directory: 'sdk-test-java']
clean:
mvn clean -q

[working-directory: 'sdk-test-ruby']
test-ruby:
RESULTS_DIR=results bundle exec ruby -Itest test/all.rb

[working-directory: 'sdk-test-ruby']
setup-ruby:
bundle install
3 changes: 2 additions & 1 deletion compatibility-tests/sdk-test-java/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM maven:3.9-eclipse-temurin-21
ARG JAVA_BUILD_IMAGE=maven:3.9-eclipse-temurin-21
FROM ${JAVA_BUILD_IMAGE}

WORKDIR /app

Expand Down
9 changes: 6 additions & 3 deletions compatibility-tests/sdk-test-java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@
<description>GCP SDK Java compatibility tests for Floci GCP emulator</description>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<gcp.bom.version>26.53.0</gcp.bom.version>
<gcp.bom.version>26.87.0</gcp.bom.version>
<junit.version>5.12.2</junit.version>
<assertj.version>3.27.3</assertj.version>
<surefire.version>3.5.5</surefire.version>
Expand All @@ -36,6 +35,10 @@

<dependencies>
<!-- GCP Client Libraries -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-compute</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-pubsub</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.floci.gcp.test;

import com.google.api.gax.core.NoCredentialsProvider;
import com.google.cloud.compute.v1.*;
import org.junit.jupiter.api.Test;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.*;

class ComputeContractTest {
private static String endpoint() { return TestFixtures.endpoint() + "/"; }
@Test void lifecycleImagesSnapshotsAndRetainedDisks() throws Exception {
String project = TestFixtures.uniqueName("java-compute"), zone = "us-central1-a";
var auth = NoCredentialsProvider.create();
try (var networks = NetworksClient.create(NetworksSettings.newBuilder().setEndpoint(endpoint()).setCredentialsProvider(auth).build());
var subnets = SubnetworksClient.create(SubnetworksSettings.newBuilder().setEndpoint(endpoint()).setCredentialsProvider(auth).build());
var instances = InstancesClient.create(InstancesSettings.newBuilder().setEndpoint(endpoint()).setCredentialsProvider(auth).build());
var disks = DisksClient.create(DisksSettings.newBuilder().setEndpoint(endpoint()).setCredentialsProvider(auth).build());
var images = ImagesClient.create(ImagesSettings.newBuilder().setEndpoint(endpoint()).setCredentialsProvider(auth).build());
var snapshots = SnapshotsClient.create(SnapshotsSettings.newBuilder().setEndpoint(endpoint()).setCredentialsProvider(auth).build())) {
try {
networks.insertAsync(project, Network.newBuilder().setName("net").setAutoCreateSubnetworks(false).build()).get(20, TimeUnit.SECONDS);
subnets.insertAsync(project, "us-central1", Subnetwork.newBuilder().setName("subnet").setNetwork("global/networks/net").setIpCidrRange("10.40.0.0/24").build()).get(20, TimeUnit.SECONDS);
var vm = Instance.newBuilder().setName("vm").setMachineType("zones/" + zone + "/machineTypes/n2-standard-4")
.addNetworkInterfaces(NetworkInterface.newBuilder().setSubnetwork("regions/us-central1/subnetworks/subnet"))
.addDisks(AttachedDisk.newBuilder().setBoot(true).setAutoDelete(false).setInitializeParams(AttachedDiskInitializeParams.newBuilder().setDiskSizeGb(20))).build();
instances.insertAsync(project, zone, vm).get(20, TimeUnit.SECONDS);
assertThat(instances.get(project, zone, "vm").getStatus()).isEqualTo("RUNNING");
var current = instances.get(project, zone, "vm");
var labels = InstancesSetLabelsRequest.newBuilder().setLabelFingerprint(current.getLabelFingerprint()).putLabels("purpose", "contract").build();
instances.setLabelsAsync(project, zone, "vm", labels).get(20, TimeUnit.SECONDS);
assertThatThrownBy(() -> instances.setLabelsAsync(project, zone, "vm", labels).get(20, TimeUnit.SECONDS)).isInstanceOf(java.util.concurrent.ExecutionException.class);
instances.setMetadataAsync(project, zone, "vm", Metadata.newBuilder().setFingerprint(current.getMetadata().getFingerprint())
.addItems(Items.newBuilder().setKey("startup-script").setValue("echo synthetic")).build()).get(20, TimeUnit.SECONDS);
instances.setTagsAsync(project, zone, "vm", Tags.newBuilder().setFingerprint(current.getTags().getFingerprint()).addItems("desktop").build()).get(20, TimeUnit.SECONDS);
assertThat(instances.get(project, zone, "vm").getTags().getItemsList()).containsExactly("desktop");
instances.stopAsync(project, zone, "vm").get(20, TimeUnit.SECONDS);
instances.setMachineTypeAsync(project, zone, "vm", InstancesSetMachineTypeRequest.newBuilder().setMachineType("zones/" + zone + "/machineTypes/n2-standard-8").build()).get(20, TimeUnit.SECONDS);
instances.startAsync(project, zone, "vm").get(20, TimeUnit.SECONDS);
instances.resetAsync(project, zone, "vm").get(20, TimeUnit.SECONDS);
disks.insertAsync(project, zone, Disk.newBuilder().setName("data").setSizeGb(100).setType("zones/" + zone + "/diskTypes/hyperdisk-balanced").setProvisionedIops(4000).setProvisionedThroughput(200).build()).get(20, TimeUnit.SECONDS);
disks.updateAsync(project, zone, "data", Disk.newBuilder().setProvisionedIops(6000).setProvisionedThroughput(300).build()).get(20, TimeUnit.SECONDS);
assertThat(disks.get(project, zone, "data").getProvisionedIops()).isEqualTo(6000);
instances.attachDiskAsync(project, zone, "vm", AttachedDisk.newBuilder().setSource("zones/" + zone + "/disks/data").setDeviceName("data").build()).get(20, TimeUnit.SECONDS);
assertThatThrownBy(() -> disks.deleteAsync(project, zone, "data").get(20, TimeUnit.SECONDS)).hasCauseInstanceOf(com.google.api.gax.rpc.InvalidArgumentException.class);
instances.detachDiskAsync(project, zone, "vm", "data").get(20, TimeUnit.SECONDS);
snapshots.insertAsync(project, Snapshot.newBuilder().setName("snapshot").setSourceDisk("zones/" + zone + "/disks/data").build()).get(20, TimeUnit.SECONDS);
snapshots.setLabelsAsync(project, "snapshot", GlobalSetLabelsRequest.newBuilder()
.setLabelFingerprint(snapshots.get(project, "snapshot").getLabelFingerprint()).putLabels("purpose", "restore").build()).get(20, TimeUnit.SECONDS);
assertThat(snapshots.get(project, "snapshot").getLabelsMap()).containsEntry("purpose", "restore");
assertThatThrownBy(() -> disks.insertAsync(project, zone, Disk.newBuilder().setName("missing-source").setSourceSnapshot("global/snapshots/missing").build()).get(20, TimeUnit.SECONDS))
.hasCauseInstanceOf(com.google.api.gax.rpc.NotFoundException.class);
disks.insertAsync(project, "us-central1-b", Disk.newBuilder().setName("restored").setSourceSnapshot("global/snapshots/snapshot").build()).get(20, TimeUnit.SECONDS);
assertThat(disks.get(project, "us-central1-b", "restored").getSizeGb()).isEqualTo(100);
images.insertAsync(project, Image.newBuilder().setName("image").setFamily("desktop").setSourceDisk("zones/" + zone + "/disks/data").build()).get(20, TimeUnit.SECONDS);
images.insertAsync(project, Image.newBuilder().setName("copy").setFamily("desktop").setSourceImage("global/images/image").build()).get(20, TimeUnit.SECONDS);
assertThat(images.getFromFamily(project, "desktop").getName()).isEqualTo("copy");
assertThat(images.list(ListImagesRequest.newBuilder().setProject(project).setMaxResults(1).build()).iterateAll()).hasSize(2);
instances.deleteAsync(project, zone, "vm").get(20, TimeUnit.SECONDS);
assertThat(disks.get(project, zone, "vm").getSizeGb()).isEqualTo(20);
} finally {
for (var vm : instances.list(project, zone).iterateAll()) { instances.deleteAsync(project, zone, vm.getName()).get(20, TimeUnit.SECONDS); }
for (var image : images.list(project).iterateAll()) { images.deleteAsync(project, image.getName()).get(20, TimeUnit.SECONDS); }
for (var snapshot : snapshots.list(project).iterateAll()) { snapshots.deleteAsync(project, snapshot.getName()).get(20, TimeUnit.SECONDS); }
for (String z : new String[]{zone, "us-central1-b"}) {
for (var disk : disks.list(project, z).iterateAll()) { disks.deleteAsync(project, z, disk.getName()).get(20, TimeUnit.SECONDS); }
}
for (var subnet : subnets.list(project, "us-central1").iterateAll()) { subnets.deleteAsync(project, "us-central1", subnet.getName()).get(20, TimeUnit.SECONDS); }
for (var network : networks.list(project).iterateAll()) { networks.deleteAsync(project, network.getName()).get(20, TimeUnit.SECONDS); }
}
}
}
}
Loading