diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c58bf0a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,111 @@
+name: CI
+
+# Public repo → GitHub-hosted Actions are free. Kept fast: Build and Vet share
+# one job so `go vet` reuses the compile cache `go build` just populated
+# (standalone `go vet ./...` recompiles everything and is slow). Test runs in
+# parallel; the quick checks (Tidy/Format) give sub-minute feedback. Every job
+# restores the Go build+module cache. One run per PR (+ push to master);
+# superseded runs cancel.
+on:
+ pull_request:
+ push:
+ branches: [master]
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build-vet:
+ name: Build & Vet
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: "1.25"
+ cache: false
+ # Manual cache with restore-keys so a changed go.sum still restores the
+ # previous build+module cache (only the changed deps recompile) instead
+ # of a full cold compile of the whole cloud-SDK dependency tree.
+ - uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cache/go-build
+ ~/go/pkg/mod
+ key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ go-${{ runner.os }}-
+ - name: go build
+ run: go build ./...
+ - name: go vet (reuses the build cache)
+ run: go vet ./...
+
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: "1.25"
+ cache: false
+ - uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cache/go-build
+ ~/go/pkg/mod
+ key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ go-${{ runner.os }}-
+ - run: go test ./...
+
+ tidy:
+ name: Tidy
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: "1.25"
+ cache: true
+ - name: go mod tidy is clean
+ run: |
+ go mod tidy
+ git diff --exit-code go.mod go.sum
+
+ format:
+ name: Format
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: "1.25"
+ cache: true
+ - name: gofmt (report-only)
+ run: |
+ unformatted="$(gofmt -l .)"
+ if [ -n "$unformatted" ]; then
+ echo "::warning::gofmt needed on the following files (advisory):"
+ echo "$unformatted"
+ else
+ echo "all files gofmt-clean"
+ fi
+
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: "1.25"
+ cache: true
+ - name: install golangci-lint v2
+ run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b "$(go env GOPATH)/bin"
+ - name: golangci-lint run (report-only)
+ run: "$(go env GOPATH)/bin/golangci-lint run --timeout=9m ./... || echo '::warning::golangci-lint reported findings (advisory)'"
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
new file mode 100644
index 0000000..c2932b0
--- /dev/null
+++ b/.github/workflows/security.yml
@@ -0,0 +1,83 @@
+name: Security
+
+# Security scanning, separate from CI so it runs in parallel and is easy to
+# read. Each scanner is its own cached job. Runs on PRs, on push to master, and
+# weekly (so newly-disclosed CVEs are caught even without a push).
+on:
+ pull_request:
+ push:
+ branches: [master]
+ schedule:
+ - cron: "0 6 * * 1" # Mondays 06:00 UTC
+
+permissions:
+ contents: read
+
+concurrency:
+ group: security-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ govulncheck:
+ name: Vulnerabilities (govulncheck)
+ runs-on: ubuntu-latest
+ # Advisory to start: flags known CVEs in dependencies the code reaches.
+ # Flip to blocking once the baseline is confirmed clean.
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: "1.25"
+ cache: true
+ - name: install govulncheck
+ run: go install golang.org/x/vuln/cmd/govulncheck@latest
+ - name: govulncheck (report-only)
+ run: "$(go env GOPATH)/bin/govulncheck ./... || echo '::warning::govulncheck reported findings (advisory)'"
+
+ gosec:
+ name: SAST (gosec)
+ runs-on: ubuntu-latest
+ # Advisory to start: static security analysis (hardcoded creds, weak crypto,
+ # unsafe patterns). Reports without blocking until the baseline is triaged.
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: "1.25"
+ cache: true
+ - name: install gosec
+ run: go install github.com/securego/gosec/v2/cmd/gosec@latest
+ - name: gosec (report-only)
+ run: "$(go env GOPATH)/bin/gosec -exclude-generated ./... || echo '::warning::gosec reported findings (advisory)'"
+
+ codeql:
+ name: CodeQL (SAST)
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ security-events: write
+ actions: read
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version: "1.25"
+ cache: true
+ - uses: github/codeql-action/init@v3
+ with:
+ languages: go
+ - uses: github/codeql-action/autobuild@v3
+ - uses: github/codeql-action/analyze@v3
+
+ dependency-review:
+ name: Dependency Review
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ steps:
+ - uses: actions/checkout@v4
+ - name: Review dependency changes for known vulnerabilities
+ uses: actions/dependency-review-action@v4
+ with:
+ fail-on-severity: high
diff --git a/README.md b/README.md
index 8161668..3117df1 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,8 @@
-
-
+
+
@@ -30,7 +30,7 @@ It ships two surfaces you can mix and match:
## Install
```bash
-go get github.com/stackshy/cloudemu
+go get github.com/stackshy/cloudemu/v2
```
Requires Go 1.25+.
@@ -45,8 +45,8 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
cloud := cloudemu.NewAWS()
diff --git a/cloudemu.go b/cloudemu.go
index 137a09d..e166666 100644
--- a/cloudemu.go
+++ b/cloudemu.go
@@ -1,10 +1,10 @@
package cloudemu
import (
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/providers/aws"
- "github.com/stackshy/cloudemu/providers/azure"
- "github.com/stackshy/cloudemu/providers/gcp"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/aws"
+ "github.com/stackshy/cloudemu/v2/providers/azure"
+ "github.com/stackshy/cloudemu/v2/providers/gcp"
)
// NewAWS creates a new AWS mock provider.
diff --git a/cloudemu_test.go b/cloudemu_test.go
index dae2266..6560e63 100644
--- a/cloudemu_test.go
+++ b/cloudemu_test.go
@@ -7,34 +7,34 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/compute"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/cost"
- "github.com/stackshy/cloudemu/database/driver"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
- "github.com/stackshy/cloudemu/inject"
- mqdriver "github.com/stackshy/cloudemu/messagequeue/driver"
- "github.com/stackshy/cloudemu/metrics"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
- serverlessdriver "github.com/stackshy/cloudemu/serverless/driver"
- "github.com/stackshy/cloudemu/storage"
- storagedriver "github.com/stackshy/cloudemu/storage/driver"
-
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
-
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- loggingdriver "github.com/stackshy/cloudemu/logging/driver"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
- "github.com/stackshy/cloudemu/topology"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/compute"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ "github.com/stackshy/cloudemu/v2/services/cost"
+ "github.com/stackshy/cloudemu/v2/services/database/driver"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
+ mqdriver "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
+ serverlessdriver "github.com/stackshy/cloudemu/v2/services/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/services/storage"
+ storagedriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
+
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
+
+ "github.com/stackshy/cloudemu/v2/features/topology"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
+ loggingdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
func TestStorageLifecycle(t *testing.T) {
@@ -8229,6 +8229,7 @@ func testGSIOperations(t *testing.T, ctx context.Context, d driver.Database) {
t.Errorf("expected NotFound for missing table, got %v", err)
}
}
+
// Event Source Mapping Tests
func testEventSourceMapping(t *testing.T, ctx context.Context, d serverlessdriver.Serverless) {
t.Helper()
@@ -8423,6 +8424,7 @@ func TestEventSourceMappingGCP(t *testing.T) {
p := NewGCP()
testEventSourceMapping(t, ctx, p.CloudFunctions)
}
+
// ─── VPC Endpoints ────────────────────────────────────────────
func TestVPCEndpointAWS(t *testing.T) {
diff --git a/doc.go b/doc.go
index 761112b..b27a7b6 100644
--- a/doc.go
+++ b/doc.go
@@ -1,19 +1,24 @@
// Package cloudemu provides zero-cost, in-memory cloud emulation of
// AWS, Azure, and GCP cloud services for Go.
//
-// cloudemu follows a three-layer architecture:
+// The repository is organized by role so new services and features slot into
+// predictable places:
//
-// - Portable API: High-level types (storage.Bucket, compute.Compute, etc.)
-// that wrap drivers with cross-cutting concerns like recording, metrics,
-// rate limiting, and error injection.
+// - services/: the emulated cloud services. Each holds the Portable API
+// type (e.g. services/storage's storage.Bucket) plus its driver interface
+// under services//driver.
//
-// - Driver Interfaces: Minimal contracts (storage/driver, compute/driver, etc.)
-// that each provider must implement.
+// - providers/{aws,azure,gcp}: in-memory backends implementing the drivers.
//
-// - Provider Implementations: In-memory backends (providers/aws/s3, providers/azure/blobstorage,
-// providers/gcp/gcs, etc.) powered by a generic memstore.
+// - server/{aws,azure,gcp}: SDK-compat HTTP servers that speak each cloud's
+// real wire protocol, so unmodified SDK clients drive the backends.
//
-// 10 cloud services are covered across all three providers: Storage, Compute,
-// Database, Serverless, Networking, Monitoring, IAM, DNS, Load Balancer,
-// and Message Queue.
+// - features/: cross-cutting capabilities you wrap drivers with —
+// chaos, recorder, metrics, inject, ratelimit, and topology.
+//
+// - config, errors: foundational options and the canonical error type.
+//
+// The three surfaces build on the same drivers: the SDK-compat server (the
+// primary entrypoint), the Portable API (services/), and the cross-cutting
+// features, so a behavior implemented in a driver lights up across all of them.
package cloudemu
diff --git a/docs/architecture.md b/docs/architecture.md
index 647e5ec..baa6bf3 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -33,11 +33,11 @@ CloudEmu follows a three-layer architecture that separates portable API concerns
### Layer 1: Portable API
-The top layer lives in service-specific packages (`storage/`, `compute/`, `database/`, etc.). Each portable API type wraps a driver with cross-cutting concerns. For example, `storage.Bucket` wraps `driver.Bucket` and adds call recording, metrics collection, rate limiting, error injection, and simulated latency to every operation. This layer is provider-agnostic -- the same `storage.Bucket` works with S3, Blob Storage, or GCS.
+The top layer lives in service-specific packages (`services/storage/`, `services/compute/`, `services/database/`, etc.). Each portable API type wraps a driver with cross-cutting concerns. For example, `storage.Bucket` wraps `driver.Bucket` and adds call recording, metrics collection, rate limiting, error injection, and simulated latency to every operation. This layer is provider-agnostic -- the same `storage.Bucket` works with S3, Blob Storage, or GCS.
### Layer 2: Driver Interfaces
-Each service defines a minimal Go interface in `/driver/driver.go`. These interfaces specify the operations that every provider must implement. For example, `storage/driver/driver.go` defines the `Bucket` interface with methods like `CreateBucket`, `PutObject`, `GetObject`, etc. Driver interfaces use plain Go types (no cloud SDK dependencies).
+Each service defines a minimal Go interface in `/driver/driver.go`. These interfaces specify the operations that every provider must implement. For example, `services/storage/driver/driver.go` defines the `Bucket` interface with methods like `CreateBucket`, `PutObject`, `GetObject`, etc. Driver interfaces use plain Go types (no cloud SDK dependencies).
### Layer 3: Provider Implementations
@@ -47,7 +47,7 @@ The bottom layer contains the actual mock implementations for each cloud provide
Sitting above Layer 2 (driver interfaces) are **cross-service engines** that consume driver interfaces directly without going through the portable API. They're peers of each other, not layers in the three-layer stack. Two exist today:
-- `topology/` -- reads from compute, networking, and DNS drivers to simulate real network connectivity (`CanConnect`, `TraceRoute`, `Resolve`, security-group and NACL evaluation). See [topology.md](topology.md).
+- `features/topology/` -- reads from compute, networking, and DNS drivers to simulate real network connectivity (`CanConnect`, `TraceRoute`, `Resolve`, security-group and NACL evaluation). See [topology.md](topology.md).
- `server/` -- exposes driver interfaces over HTTP in each cloud's native SDK wire format, so real `aws-sdk-go-v2`, `azure-sdk-for-go`, and `cloud.google.com/go` clients work against CloudEmu by only changing the endpoint. Covers Storage, Compute, Database, Networking, Monitoring, Serverless, and Message Queue across all 3 providers (AWS S3/EC2/DynamoDB/Lambda/SQS/CloudWatch + sibling Azure ARM and GCP REST handlers). Uses a pluggable `Handler` registry so new services drop in as self-contained packages without touching the core. See [sdk-server.md](sdk-server.md).
Both engines depend only on Layer 2 interfaces -- never on concrete provider types -- so they work uniformly across AWS, Azure, and GCP backends.
@@ -117,7 +117,7 @@ aws.DynamoDB.CreateTable(ctx, tableConfig)
cloudemu.go # Entry point: NewAWS(), NewAzure(), NewGCP()
cloudemu_test.go # All tests
doc.go # Package documentation
-go.mod # Module: github.com/stackshy/cloudemu
+go.mod # Module: github.com/stackshy/cloudemu/v2
config/
options.go # Options, WithClock, WithRegion, etc.
clock.go # Clock, RealClock, FakeClock
@@ -126,53 +126,25 @@ errors/
internal/
memstore/ # Generic Store[V]
idgen/ # ID generators (ARNs, Azure IDs, GCP IDs)
-statemachine/
- machine.go # Generic FSM
- transitions.go # Transition map
-pagination/ # Generic Paginate[T]
-recorder/ # Call recording for assertions
-metrics/ # In-memory metrics collection
-ratelimit/ # Token bucket rate limiter
-inject/ # Error injection (policies + injector)
-cost/
- cost.go # Cost tracker with default rates
-storage/
- storage.go # Portable storage API
- driver/driver.go # Bucket interface
-compute/
- driver/driver.go # Compute interface
-database/
- driver/driver.go # Database interface
-serverless/
- driver/driver.go # Serverless interface
-networking/
- driver/driver.go # Networking interface
-monitoring/
- driver/driver.go # Monitoring interface
-iam/
- driver/driver.go # IAM interface
-dns/
- driver/driver.go # DNS interface
-loadbalancer/
- driver/driver.go # LoadBalancer interface
-messagequeue/
- driver/driver.go # MessageQueue interface
-cache/
- driver/driver.go # Cache interface
-secrets/
- driver/driver.go # Secrets interface
-logging/
- driver/driver.go # Logging interface
-notification/
- driver/driver.go # Notification interface
-containerregistry/
- driver/driver.go # ContainerRegistry interface
-eventbus/
- driver/driver.go # EventBus interface
-relationaldb/
- driver/driver.go # Relational DB interface (RDS, Aurora, Neptune,
- # DocumentDB, Redshift, Azure SQL, Postgres/MySQL
- # Flexible Server, Cloud SQL)
+ statemachine/ # Generic FSM
+ pagination/ # Generic Paginate[T]
+services/ # emulated cloud services (portable API + driver interface)
+ storage/
+ storage.go # Portable storage API
+ driver/driver.go # Bucket interface
+ compute/ database/ relationaldb/ serverless/ networking/ monitoring/
+ iam/ dns/ loadbalancer/ messagequeue/ cache/ secrets/ logging/
+ notification/ eventbus/ containerregistry/ kubernetes/ resourcediscovery/
+ bedrock/ sagemaker/ vertexai/ databricks/ azureai/ azuresearch/
+ parameterstore/ tablestorage/ cost/
+ # each: .go (portable API) + driver/ (interface)
+features/ # cross-cutting capabilities you wrap drivers with
+ chaos/ # fault / latency / throttle injection
+ recorder/ # call recording for assertions
+ metrics/ # in-memory metrics collection
+ ratelimit/ # token-bucket rate limiter
+ inject/ # error injection (policies + injector)
+ topology/ # network reachability (CanConnect / TraceRoute / Resolve)
providers/
aws/
aws.go # AWS factory (wires all services)
diff --git a/docs/chaos.md b/docs/chaos.md
index e9c7eea..8070694 100644
--- a/docs/chaos.md
+++ b/docs/chaos.md
@@ -10,10 +10,10 @@ Wrap any driver with the chaos engine before handing it to the portable API or t
```go
import (
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ "github.com/stackshy/cloudemu/v2/config"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
cloud := cloudemu.NewAWS()
diff --git a/docs/features.md b/docs/features.md
index 22a7275..2ef78de 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -289,12 +289,12 @@ import (
"time"
"errors"
- "github.com/stackshy/cloudemu/storage"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/inject"
- cerrors "github.com/stackshy/cloudemu/errors"
+ "github.com/stackshy/cloudemu/v2/services/storage"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
rec := recorder.New()
@@ -374,7 +374,7 @@ clock.Set(time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC))
## 10. Cross-Service Resource Discovery
-CloudEmu ships a cross-service inventory engine (`resourcediscovery/`) that walks every service driver a provider holds and returns a single normalized view of what exists. It sits next to the `topology/` engine as a peer of the portable API — it owns no state, constructs from driver interfaces, and is purely query-driven.
+CloudEmu ships a cross-service inventory engine (`services/resourcediscovery/`) that walks every service driver a provider holds and returns a single normalized view of what exists. It sits next to the `features/topology/` engine as a peer of the portable API — it owns no state, constructs from driver interfaces, and is purely query-driven.
The engine is the foundation for three SDK-compat handlers that speak the real cloud inventory APIs: **AWS Resource Explorer 2 + Resource Groups Tagging API**, **Azure Resource Graph**, and **GCP Cloud Asset Inventory**. A tag set through any one of those paths is immediately visible through the others, and through the engine's own `SearchByTag`.
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 08e2767..7c21c47 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -3,7 +3,7 @@
## Installation
```bash
-go get github.com/stackshy/cloudemu
+go get github.com/stackshy/cloudemu/v2
```
Requires Go 1.25.0 or later.
@@ -20,7 +20,7 @@ package main
import (
"context"
- "github.com/stackshy/cloudemu"
+ "github.com/stackshy/cloudemu/v2"
)
func main() {
@@ -71,7 +71,7 @@ import (
"context"
"fmt"
- "github.com/stackshy/cloudemu"
+ "github.com/stackshy/cloudemu/v2"
)
func main() {
@@ -114,8 +114,8 @@ import (
"context"
"fmt"
- "github.com/stackshy/cloudemu"
- cdriver "github.com/stackshy/cloudemu/compute/driver"
+ "github.com/stackshy/cloudemu/v2"
+ cdriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
func main() {
@@ -162,8 +162,8 @@ import (
"context"
"fmt"
- "github.com/stackshy/cloudemu"
- ddriver "github.com/stackshy/cloudemu/database/driver"
+ "github.com/stackshy/cloudemu/v2"
+ ddriver "github.com/stackshy/cloudemu/v2/services/database/driver"
)
func main() {
@@ -228,8 +228,8 @@ All three factory functions accept `config.Option` values for customization.
import (
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
)
// Custom region
@@ -272,7 +272,7 @@ CloudEmu uses canonical error codes from the `errors` package. Use the helper fu
```go
import (
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
// Try to get a non-existent bucket
@@ -377,9 +377,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/config"
- sdriver "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ sdriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/docs/integration.md b/docs/integration.md
index 42d3e47..817b03c 100644
--- a/docs/integration.md
+++ b/docs/integration.md
@@ -35,7 +35,7 @@ Keep CloudEmu in `_test.go` only — never import it from production code.
## Tell your AI agent (paste into your repo's `AGENTS.md`)
```markdown
-CloudEmu (github.com/stackshy/cloudemu) is an in-memory cloud emulator. To integrate it:
+CloudEmu (github.com/stackshy/cloudemu/v2) is an in-memory cloud emulator. To integrate it:
do NOT create a demo main.go — wire it into the real code so existing tests exercise it.
Make the SDK endpoint injectable however this codebase prefers (an env var, a config field,
or set directly in the test — your choice) and point it at CloudEmu: AWS o.BaseEndpoint,
diff --git a/docs/sdk-server.md b/docs/sdk-server.md
index 63dcd22..a3d83c9 100644
--- a/docs/sdk-server.md
+++ b/docs/sdk-server.md
@@ -16,8 +16,8 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
cloud := cloudemu.NewAWS()
@@ -53,8 +53,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
cp := cloudemu.NewAzure()
@@ -91,8 +91,8 @@ client, _ := armcompute.NewVirtualMachinesClient("sub-1", fakeCred{}, opts)
```go
import (
gcpcompute "cloud.google.com/go/compute/apiv1"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
"google.golang.org/api/option"
)
@@ -129,8 +129,8 @@ import (
databricks "github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/compute"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
cp := cloudemu.NewAzure()
diff --git a/docs/services.md b/docs/services.md
index beb46ed..67fc7d3 100644
--- a/docs/services.md
+++ b/docs/services.md
@@ -23,17 +23,18 @@ This document lists every service and operation available in CloudEmu across all
| 15 | Container Registry | `ecr` | `acr` | `artifactregistry` |
| 16 | Event Bus | `eventbridge` | `eventgrid` | `eventarc` |
| 17 | Relational Database | `rds` (+ Aurora/Neptune/DocumentDB engines), `redshift` | `azuresql`, `postgresflex`, `mysqlflex` | `cloudsql` |
-| 18 | Kubernetes | `eks` + shared `kubernetes/` | `aks` + shared `kubernetes/` | `gke` + shared `kubernetes/` |
+| 18 | Kubernetes | `eks` + shared `services/kubernetes/` | `aks` + shared `services/kubernetes/` | `gke` + shared `services/kubernetes/` |
| 19 | Resource Discovery | `resourceexplorer2` + `resourcegroupstaggingapi` | `resourcegraph` | `cloudasset` |
| 20 | Generative AI | `bedrock` (+ `bedrock-runtime`) | — | — |
| 21 | Databricks | — | `databricks` | — |
-| 22 | Machine Learning | `sagemaker` (+ `sagemaker-runtime`) | _(planned: Azure ML / AI Foundry)_ | `vertexai` |
+| 22 | Machine Learning | `sagemaker` (+ `sagemaker-runtime`) | `azureai` (CognitiveServices + MachineLearningServices) | `vertexai` |
+| 23 | AI Search | — | `azuresearch` (Microsoft.Search) | — |
---
## 1. Storage
-**Driver interface:** `storage/driver/driver.go`
+**Driver interface:** `services/storage/driver/driver.go`
**AWS:** S3 | **Azure:** Blob Storage | **GCP:** GCS
### Bucket Operations
@@ -131,7 +132,7 @@ This document lists every service and operation available in CloudEmu across all
## 2. Compute
-**Driver interface:** `compute/driver/driver.go`
+**Driver interface:** `services/compute/driver/driver.go`
**AWS:** EC2 | **Azure:** Virtual Machines | **GCP:** GCE
### Instance Operations
@@ -222,7 +223,7 @@ This document lists every service and operation available in CloudEmu across all
## 3. Database
-**Driver interface:** `database/driver/driver.go`
+**Driver interface:** `services/database/driver/driver.go`
**AWS:** DynamoDB | **Azure:** Cosmos DB | **GCP:** Firestore
### Table Operations
@@ -287,7 +288,7 @@ This document lists every service and operation available in CloudEmu across all
## 4. Serverless
-**Driver interface:** `serverless/driver/driver.go`
+**Driver interface:** `services/serverless/driver/driver.go`
**AWS:** Lambda | **Azure:** Functions | **GCP:** Cloud Functions
### Function Operations
@@ -353,7 +354,7 @@ This document lists every service and operation available in CloudEmu across all
## 5. Networking
-**Driver interface:** `networking/driver/driver.go`
+**Driver interface:** `services/networking/driver/driver.go`
**AWS:** VPC | **Azure:** VNet | **GCP:** GCP VPC
### VPC Operations
@@ -473,7 +474,7 @@ This document lists every service and operation available in CloudEmu across all
## 6. Monitoring
-**Driver interface:** `monitoring/driver/driver.go`
+**Driver interface:** `services/monitoring/driver/driver.go`
**AWS:** CloudWatch | **Azure:** Azure Monitor | **GCP:** Cloud Monitoring
### Metric Operations
@@ -514,7 +515,7 @@ This document lists every service and operation available in CloudEmu across all
## 7. IAM
-**Driver interface:** `iam/driver/driver.go`
+**Driver interface:** `services/iam/driver/driver.go`
**AWS:** IAM | **Azure:** Azure IAM | **GCP:** GCP IAM
### Users
@@ -598,7 +599,7 @@ This document lists every service and operation available in CloudEmu across all
## 8. DNS
-**Driver interface:** `dns/driver/driver.go`
+**Driver interface:** `services/dns/driver/driver.go`
**AWS:** Route 53 | **Azure:** Azure DNS | **GCP:** Cloud DNS
### Zone Operations
@@ -637,7 +638,7 @@ This document lists every service and operation available in CloudEmu across all
## 9. Load Balancer
-**Driver interface:** `loadbalancer/driver/driver.go`
+**Driver interface:** `services/loadbalancer/driver/driver.go`
**AWS:** ELB | **Azure:** Azure LB | **GCP:** GCP LB
### Load Balancer Operations
@@ -695,7 +696,7 @@ This document lists every service and operation available in CloudEmu across all
## 10. Message Queue
-**Driver interface:** `messagequeue/driver/driver.go`
+**Driver interface:** `services/messagequeue/driver/driver.go`
**AWS:** SQS | **Azure:** Service Bus | **GCP:** Pub/Sub
### Queue Operations
@@ -748,7 +749,7 @@ This document lists every service and operation available in CloudEmu across all
## 11. Cache
-**Driver interface:** `cache/driver/driver.go`
+**Driver interface:** `services/cache/driver/driver.go`
**AWS:** ElastiCache | **Azure:** Azure Cache | **GCP:** Memorystore
### Cache Instance Operations
@@ -793,7 +794,7 @@ This document lists every service and operation available in CloudEmu across all
## 12. Secrets
-**Driver interface:** `secrets/driver/driver.go`
+**Driver interface:** `services/secrets/driver/driver.go`
**AWS:** Secrets Manager | **Azure:** Key Vault | **GCP:** Secret Manager
### Secret Operations
@@ -819,7 +820,7 @@ This document lists every service and operation available in CloudEmu across all
## 13. Logging
-**Driver interface:** `logging/driver/driver.go`
+**Driver interface:** `services/logging/driver/driver.go`
**AWS:** CloudWatch Logs | **Azure:** Log Analytics | **GCP:** Cloud Logging
### Log Group Operations
@@ -861,7 +862,7 @@ This document lists every service and operation available in CloudEmu across all
## 14. Notification
-**Driver interface:** `notification/driver/driver.go`
+**Driver interface:** `services/notification/driver/driver.go`
**AWS:** SNS | **Azure:** Notification Hubs | **GCP:** FCM
### Topic Operations
@@ -893,7 +894,7 @@ This document lists every service and operation available in CloudEmu across all
## 15. Container Registry
-**Driver interface:** `containerregistry/driver/driver.go`
+**Driver interface:** `services/containerregistry/driver/driver.go`
**AWS:** ECR | **Azure:** ACR | **GCP:** Artifact Registry
### Repository Management
@@ -936,7 +937,7 @@ This document lists every service and operation available in CloudEmu across all
## 16. Event Bus
-**Driver interface:** `eventbus/driver/driver.go`
+**Driver interface:** `services/eventbus/driver/driver.go`
**AWS:** EventBridge | **Azure:** Event Grid | **GCP:** Eventarc
### Bus Management
@@ -985,7 +986,7 @@ This document lists every service and operation available in CloudEmu across all
## 17. Relational Database
-**Driver interface:** `relationaldb/driver/driver.go`
+**Driver interface:** `services/relationaldb/driver/driver.go`
**AWS:** `rds` (covers Aurora, Neptune, and DocumentDB engines), `redshift` | **Azure:** `azuresql`, `postgresflex`, `mysqlflex` | **GCP:** `cloudsql`
A single portable interface backs every RDBMS handler. Engine selection (MySQL / PostgreSQL / Aurora / Neptune / DocumentDB / Redshift / Cloud SQL / Azure SQL / …) is a field on the input config, not a separate driver.
@@ -1038,7 +1039,7 @@ A single portable interface backs every RDBMS handler. Engine selection (MySQL /
## 18. Kubernetes
**Control plane:** AWS `eks`, Azure `aks`, GCP `gke` — cluster, node-pool, and addon / Fargate-profile / maintenance-config lifecycle, driven by the real cloud SDKs.
-**Data plane:** shared `kubernetes/` package — an in-memory Kubernetes API server registered by every cluster across all three providers. Kubeconfigs returned by the control plane point at `/k8s/` so `client-go` and `kubectl` operate end-to-end.
+**Data plane:** shared `services/kubernetes/` package — an in-memory Kubernetes API server registered by every cluster across all three providers. Kubeconfigs returned by the control plane point at `/k8s/` so `client-go` and `kubectl` operate end-to-end.
Each provider exposes its native control-plane API. The data plane has no portable driver — clients connect via the kubeconfig the control plane hands out, then talk standard Kubernetes REST.
@@ -1074,7 +1075,7 @@ Operations: **18**
Operations: **26**
-### Data plane (`kubernetes/`)
+### Data plane (`services/kubernetes/`)
Shared in-memory K8s API server registered by every cluster from any provider. URL: `/k8s//...`. Anonymous auth (kubeconfigs use `insecure-skip-tls-verify: true`).
@@ -1099,11 +1100,11 @@ Shared in-memory K8s API server registered by every cluster from any provider. U
## 19. Resource Discovery
-**Engine:** `resourcediscovery/` — a cross-service inventory engine that walks the Compute, Networking, Storage, Database, and Serverless drivers of any provider and returns a normalized `Resource` view (provider, service, type, ID, ARN/URN, region, tags, created-at). Auto-wired by every provider factory and exposed as `Provider.ResourceDiscovery`.
+**Engine:** `services/resourcediscovery/` — a cross-service inventory engine that walks the Compute, Networking, Storage, Database, and Serverless drivers of any provider and returns a normalized `Resource` view (provider, service, type, ID, ARN/URN, region, tags, created-at). Auto-wired by every provider factory and exposed as `Provider.ResourceDiscovery`.
**SDK-compat handlers:** AWS Resource Explorer Two + Resource Groups Tagging API, Azure Resource Graph, and GCP Cloud Asset Inventory. All three sit on top of the same engine, so a tag written through any one path is visible through the others.
-### Engine (`resourcediscovery/`)
+### Engine (`services/resourcediscovery/`)
| Operation | Signature |
|-----------|-----------|
@@ -1154,7 +1155,7 @@ Operations: **Engine 8** + **AWS Resource Explorer 1** + **AWS Resource Groups T
## 20. Generative AI
-**Driver interface:** `bedrock/driver/driver.go`
+**Driver interface:** `services/bedrock/driver/driver.go`
**AWS:** `bedrock` (+ `bedrock-runtime`) | **Azure:** — | **GCP:** —
AWS-only. Backs the real `aws-sdk-go-v2/service/bedrock` and `.../bedrockruntime` clients against the in-memory backend.
@@ -1222,7 +1223,7 @@ AWS-only. Backs the real `aws-sdk-go-v2/service/bedrock` and `.../bedrockruntime
## 21. Databricks
-**Driver interfaces:** `databricks/driver/driver.go` (control plane), `databricks/driver/dataplane.go` (data plane)
+**Driver interfaces:** `services/databricks/driver/driver.go` (control plane), `services/databricks/driver/dataplane.go` (data plane)
**AWS:** — | **Azure:** `databricks` | **GCP:** —
Azure-only. The control plane backs the real `armdatabricks` SDK; the data plane backs the real `databricks-sdk-go` WorkspaceClient. The SDK-compat-only workspace families (secrets, tokens, git credentials, repos, DBFS, workspace files, SQL warehouses, pipelines, serving endpoints, SCIM identity, Unity Catalog) have no portable Go API — see [sdk-server.md](sdk-server.md).
@@ -1327,7 +1328,7 @@ Azure-only. The control plane backs the real `armdatabricks` SDK; the data plane
### AWS — SageMaker AI
-**Driver interface:** `sagemaker/driver/driver.go` (control plane + `Runtime`)
+**Driver interface:** `services/sagemaker/driver/driver.go` (control plane + `Runtime`)
The control plane speaks awsJson1_1 (`X-Amz-Target: SageMaker.*`); the runtime speaks
restJson1 (`POST /endpoints/{name}/invocations`). Asynchronous jobs complete synchronously
@@ -1353,7 +1354,7 @@ clients. **Total: 121 operations.**
### GCP — Vertex AI
-**Driver interface:** `vertexai/driver/` — `aiplatform.googleapis.com`
+**Driver interface:** `services/vertexai/driver/` — `aiplatform.googleapis.com`
REST rooted at `/v1/projects/{p}/locations/{l}/...` with the Model Garden `generateContent`
surface at `/v1/publishers/...`. Control-plane mutations return done
@@ -1382,6 +1383,72 @@ Layer-1 wrapper (`vertexai/vertexai.go`), chaos injection (`chaos.WrapVertexAI`)
rates integrate Vertex with the cross-cutting layers like every other service.
**Total: 128 operations** (Go API/driver).
+### Azure — Azure AI
+
+**Driver interface:** `services/azureai/driver/` — spans both ARM providers plus the data planes.
+**Azure:** Azure AI Foundry / AI Studio / Azure OpenAI (`Microsoft.CognitiveServices`) and
+Azure Machine Learning (`Microsoft.MachineLearningServices`).
+
+ARM control-plane PUT returns the resource inline with a terminal `provisioningState` so the
+SDK LRO poller terminates on the first response. The data plane is host/path-routed
+(`*.openai.azure.com/openai/...`, `*.inference.ml.azure.com/score`). Auto-metrics push to
+Azure Monitor via `SetMonitoring`.
+
+| Family | Resources / Operations |
+|--------|------------------------|
+| AI Services accounts | accounts CRUD, list by RG/sub, listKeys, regenerateKey, listModels, listSkus, listUsages |
+| Model deployments | accounts/deployments CRUD + list (gpt-4o, embeddings, …) |
+| AI Foundry projects | accounts/projects CRUD + list |
+| Responsible AI | accounts/raiPolicies CRUD + list |
+| Commitment plans | accounts/commitmentPlans CRUD + list |
+| Private endpoints | accounts/privateEndpointConnections CRUD + list |
+| Azure OpenAI inference | chat/completions, completions, embeddings |
+| Agents / Assistants | assistants, threads, messages, runs (CRUD/list) |
+| AML workspaces | workspaces (Default/Hub/Project/FeatureStore) CRUD, list by RG/sub |
+| AML compute | computes CRUD + list, start/stop/restart (state machine) |
+| AML endpoints | online/batchEndpoints CRUD + list, deployments CRUD + list |
+| AML jobs | jobs create/get/list/cancel |
+| AML assets | models, data, environments, components, featuresets — versioned CRUD + list (container/versions) |
+| AML datastores / connections / schedules | CRUD + list |
+| AML registries | cross-workspace registries CRUD + list |
+| AML scoring | online-endpoint `/score` data plane |
+
+Full Go API/driver, in-memory provider, SDK-compat ARM + data-plane HTTP server, a portable
+Layer-1 wrapper (`azureai/azureai.go`), chaos injection (`chaos.WrapAzureAI`), and cost rates
+integrate Azure AI with the cross-cutting layers like every other service.
+**Total: 92 operations** (Go API/driver) — 31 CognitiveServices + 46 MachineLearningServices
++ 15 data plane — all exposed over the SDK-compat HTTP server.
+
+---
+
+## 23. AI Search
+
+**Driver interface:** `services/azuresearch/driver/` — `Microsoft.Search/searchServices` (ARM control
+plane) plus the `{service}.search.windows.net` data plane.
+**Azure:** Azure AI Search (the RAG / retrieval backbone). **AWS / GCP:** _not applicable_.
+
+ARM PUT returns the resource inline with a terminal `provisioningState`; the data plane is
+host/path-routed (service name from the `{service}.search.windows.net` subdomain). Auto-metrics
+push to Azure Monitor via `SetMonitoring`.
+
+| Family | Resources / Operations |
+|--------|------------------------|
+| Services (control) | searchServices CRUD, list by RG/sub, update; listAdminKeys, regenerateAdminKey, listQueryKeys, createQueryKey, deleteQueryKey |
+| Private networking | sharedPrivateLinkResources CRUD+list, privateEndpointConnections CRUD+list |
+| Indexes | create-or-update, get, list, delete |
+| Documents | index (upload/merge/mergeOrUpload/delete), search (+count), suggest, autocomplete, count, get-by-key |
+| Indexers | create-or-update, get, list, delete, run, reset, status |
+| Data sources | create-or-update, get, list, delete |
+| Skillsets | create-or-update, get, list, delete |
+| Synonym maps | create-or-update, get, list, delete |
+| Aliases | create-or-update, get, list, delete |
+| Service statistics | counts + storage usage |
+
+Full Go API/driver, in-memory provider, SDK-compat ARM + data-plane HTTP server, a portable
+Layer-1 wrapper (`azuresearch/azuresearch.go`), chaos injection (`chaos.WrapAzureSearch`), and
+cost rates integrate Azure AI Search with the cross-cutting layers like every other service.
+**Total: 53 operations** (Go API/driver) — 19 control plane + 34 data plane.
+
---
## Summary
@@ -1413,5 +1480,7 @@ rates integrate Vertex with the cross-cutting layers like every other service.
| Generative AI — AWS Bedrock | 22 |
| Databricks — Azure (control + data plane) | 52 |
| Machine Learning — AWS SageMaker (control plane + runtime) | 121 |
+| Machine Learning — Azure AI (CognitiveServices + MachineLearningServices + data plane) | 92 |
| Machine Learning — GCP Vertex AI (Go API/driver) | 128 |
-| **Grand Total** | **821** |
+| AI Search — Azure AI Search (control + data plane) | 53 |
+| **Grand Total** | **966** |
diff --git a/examples/main.go b/examples/main.go
index c3e5881..a38c835 100644
--- a/examples/main.go
+++ b/examples/main.go
@@ -5,12 +5,12 @@ import (
"fmt"
"time"
- "github.com/stackshy/cloudemu"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- storagedriver "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
+ storagedriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
func main() {
diff --git a/chaos/chaos.go b/features/chaos/chaos.go
similarity index 99%
rename from chaos/chaos.go
rename to features/chaos/chaos.go
index d6a9c3d..176c208 100644
--- a/chaos/chaos.go
+++ b/features/chaos/chaos.go
@@ -22,7 +22,7 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2/config"
)
// Effect is what a chaos scenario produces for a given call. Either field may
diff --git a/chaos/chaos_test.go b/features/chaos/chaos_test.go
similarity index 98%
rename from chaos/chaos_test.go
rename to features/chaos/chaos_test.go
index 52f53b6..a42c02c 100644
--- a/chaos/chaos_test.go
+++ b/features/chaos/chaos_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
)
func TestNewWithNilClockUsesRealClock(t *testing.T) {
diff --git a/chaos/integration_test.go b/features/chaos/integration_test.go
similarity index 95%
rename from chaos/integration_test.go
rename to features/chaos/integration_test.go
index 51fbeb9..3a9530f 100644
--- a/chaos/integration_test.go
+++ b/features/chaos/integration_test.go
@@ -11,10 +11,10 @@ import (
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- cloudemuConfig "github.com/stackshy/cloudemu/config"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ cloudemuConfig "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
// TestSDKObservesChaos proves the central architectural promise: a real
diff --git a/chaos/random.go b/features/chaos/random.go
similarity index 100%
rename from chaos/random.go
rename to features/chaos/random.go
diff --git a/chaos/random_test.go b/features/chaos/random_test.go
similarity index 95%
rename from chaos/random_test.go
rename to features/chaos/random_test.go
index 2c46de0..172b2ed 100644
--- a/chaos/random_test.go
+++ b/features/chaos/random_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
)
// random.go's randFloat is unexported but is exercised by ProbabilisticFailure.
diff --git a/chaos/scenarios.go b/features/chaos/scenarios.go
similarity index 99%
rename from chaos/scenarios.go
rename to features/chaos/scenarios.go
index 44ce933..1818ebb 100644
--- a/chaos/scenarios.go
+++ b/features/chaos/scenarios.go
@@ -4,7 +4,7 @@ import (
"sync/atomic"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
// window holds the common "active from start to start+duration" timing logic.
diff --git a/chaos/scenarios_test.go b/features/chaos/scenarios_test.go
similarity index 98%
rename from chaos/scenarios_test.go
rename to features/chaos/scenarios_test.go
index cae6818..80606b7 100644
--- a/chaos/scenarios_test.go
+++ b/features/chaos/scenarios_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
)
func TestServiceOutageInsideWindow(t *testing.T) {
diff --git a/chaos/wrappers.go b/features/chaos/wrappers.go
similarity index 97%
rename from chaos/wrappers.go
rename to features/chaos/wrappers.go
index 2358349..a58e3f5 100644
--- a/chaos/wrappers.go
+++ b/features/chaos/wrappers.go
@@ -4,9 +4,9 @@ import (
"context"
"time"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- storagedriver "github.com/stackshy/cloudemu/storage/driver"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
+ storagedriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
// Driver-wrapper middleware. Each Wrap* takes a real driver and returns one
diff --git a/features/chaos/wrappers_azureai.go b/features/chaos/wrappers_azureai.go
new file mode 100644
index 0000000..acd2abe
--- /dev/null
+++ b/features/chaos/wrappers_azureai.go
@@ -0,0 +1,95 @@
+package chaos
+
+import (
+ "context"
+
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+// chaosAzureAI wraps an Azure AI service. It consults the engine on the calls
+// most worth failing in tests — account/workspace and deployment/job creation,
+// the inference and scoring runtimes — and delegates every other operation
+// through the embedded driver.AzureAI unchanged.
+type chaosAzureAI struct {
+ driver.AzureAI
+ engine *Engine
+}
+
+// WrapAzureAI returns an Azure AI service that injects chaos on creation and
+// runtime calls. service+operation pairs use the "azureai" service name.
+func WrapAzureAI(inner driver.AzureAI, engine *Engine) driver.AzureAI {
+ return &chaosAzureAI{AzureAI: inner, engine: engine}
+}
+
+//nolint:gocritic // cfg matches the driver signature; delegated unchanged on success.
+func (c *chaosAzureAI) CreateAccount(ctx context.Context, cfg driver.AccountConfig) (*driver.Account, error) {
+ if err := applyChaos(ctx, c.engine, "azureai", "CreateAccount"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureAI.CreateAccount(ctx, cfg)
+}
+
+//nolint:gocritic // cfg matches the driver signature; delegated unchanged on success.
+func (c *chaosAzureAI) CreateDeployment(ctx context.Context, cfg driver.DeploymentConfig) (*driver.Deployment, error) {
+ if err := applyChaos(ctx, c.engine, "azureai", "CreateDeployment"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureAI.CreateDeployment(ctx, cfg)
+}
+
+//nolint:gocritic // cfg matches the driver signature; delegated unchanged on success.
+func (c *chaosAzureAI) CreateMLWorkspace(ctx context.Context, cfg driver.MLWorkspaceConfig) (*driver.MLWorkspace, error) {
+ if err := applyChaos(ctx, c.engine, "azureai", "CreateMLWorkspace"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureAI.CreateMLWorkspace(ctx, cfg)
+}
+
+//nolint:gocritic // cfg matches the driver signature; delegated unchanged on success.
+func (c *chaosAzureAI) CreateJob(ctx context.Context, cfg driver.JobConfig) (*driver.Job, error) {
+ if err := applyChaos(ctx, c.engine, "azureai", "CreateJob"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureAI.CreateJob(ctx, cfg)
+}
+
+//nolint:gocritic // cfg matches the driver signature; delegated unchanged on success.
+func (c *chaosAzureAI) CreateEndpoint(ctx context.Context, cfg driver.EndpointConfig) (*driver.Endpoint, error) {
+ if err := applyChaos(ctx, c.engine, "azureai", "CreateEndpoint"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureAI.CreateEndpoint(ctx, cfg)
+}
+
+func (c *chaosAzureAI) ChatCompletions(
+ ctx context.Context, account, deployment string, req driver.ChatCompletionRequest,
+) (*driver.ChatCompletionResponse, error) {
+ if err := applyChaos(ctx, c.engine, "azureai", "ChatCompletions"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureAI.ChatCompletions(ctx, account, deployment, req)
+}
+
+func (c *chaosAzureAI) Embeddings(
+ ctx context.Context, account, deployment string, req driver.EmbeddingsRequest,
+) (*driver.EmbeddingsResponse, error) {
+ if err := applyChaos(ctx, c.engine, "azureai", "Embeddings"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureAI.Embeddings(ctx, account, deployment, req)
+}
+
+func (c *chaosAzureAI) ScoreOnlineEndpoint(ctx context.Context, endpoint string, body []byte) ([]byte, error) {
+ if err := applyChaos(ctx, c.engine, "azureai", "ScoreOnlineEndpoint"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureAI.ScoreOnlineEndpoint(ctx, endpoint, body)
+}
diff --git a/features/chaos/wrappers_azureai_test.go b/features/chaos/wrappers_azureai_test.go
new file mode 100644
index 0000000..508f068
--- /dev/null
+++ b/features/chaos/wrappers_azureai_test.go
@@ -0,0 +1,66 @@
+package chaos_test
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+func newChaosAzureAI(t *testing.T) (driver.AzureAI, *chaos.Engine) {
+ t.Helper()
+
+ e := chaos.New(config.RealClock{})
+ t.Cleanup(e.Stop)
+
+ return chaos.WrapAzureAI(cloudemu.NewAzure().AzureAI, e), e
+}
+
+func TestWrapAzureAICreateAccountChaos(t *testing.T) {
+ a, e := newChaosAzureAI(t)
+ ctx := context.Background()
+
+ cfg := driver.AccountConfig{Name: "ai", ResourceGroup: "rg", Location: "eastus", Kind: "AIServices"}
+ if _, err := a.CreateAccount(ctx, cfg); err != nil {
+ t.Fatalf("baseline: %v", err)
+ }
+
+ e.Apply(chaos.ServiceOutage("azureai", time.Hour))
+
+ cfg.Name = "ai2"
+ if _, err := a.CreateAccount(ctx, cfg); err == nil {
+ t.Error("expected chaos error on CreateAccount")
+ }
+}
+
+func TestWrapAzureAIChatCompletionsChaos(t *testing.T) {
+ a, e := newChaosAzureAI(t)
+ ctx := context.Background()
+
+ req := driver.ChatCompletionRequest{Messages: []driver.ChatMessage{{Role: "user", Content: "hi"}}}
+ if _, err := a.ChatCompletions(ctx, "ai", "gpt4o", req); err != nil {
+ t.Fatalf("baseline: %v", err)
+ }
+
+ e.Apply(chaos.ServiceOutage("azureai", time.Hour))
+
+ if _, err := a.ChatCompletions(ctx, "ai", "gpt4o", req); err == nil {
+ t.Error("expected chaos error on ChatCompletions")
+ }
+}
+
+func TestWrapAzureAIUnwrappedOperationPassesThrough(t *testing.T) {
+ a, e := newChaosAzureAI(t)
+ ctx := context.Background()
+
+ e.Apply(chaos.ServiceOutage("azureai", time.Hour))
+
+ // ListAccounts is not a chaos-wrapped operation; it must still succeed.
+ if _, err := a.ListAccounts(ctx); err != nil {
+ t.Errorf("unwrapped ListAccounts should pass through, got: %v", err)
+ }
+}
diff --git a/features/chaos/wrappers_azuresearch.go b/features/chaos/wrappers_azuresearch.go
new file mode 100644
index 0000000..cba198b
--- /dev/null
+++ b/features/chaos/wrappers_azuresearch.go
@@ -0,0 +1,61 @@
+package chaos
+
+import (
+ "context"
+
+ "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+// chaosAzureSearch wraps an Azure AI Search service. It consults the engine on
+// the calls most worth failing in tests — service and index creation, document
+// indexing, and the query runtime — and delegates every other operation through
+// the embedded driver.AzureSearch unchanged.
+type chaosAzureSearch struct {
+ driver.AzureSearch
+ engine *Engine
+}
+
+// WrapAzureSearch returns an Azure AI Search service that injects chaos on
+// creation and runtime calls. service+operation pairs use the "azuresearch"
+// service name.
+func WrapAzureSearch(inner driver.AzureSearch, engine *Engine) driver.AzureSearch {
+ return &chaosAzureSearch{AzureSearch: inner, engine: engine}
+}
+
+//nolint:gocritic // cfg matches the driver signature; delegated unchanged on success.
+func (c *chaosAzureSearch) CreateService(ctx context.Context, cfg driver.ServiceConfig) (*driver.Service, error) {
+ if err := applyChaos(ctx, c.engine, "azuresearch", "CreateService"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureSearch.CreateService(ctx, cfg)
+}
+
+func (c *chaosAzureSearch) CreateOrUpdateIndex(ctx context.Context, service string, idx driver.Index) (*driver.Index, error) {
+ if err := applyChaos(ctx, c.engine, "azuresearch", "CreateOrUpdateIndex"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureSearch.CreateOrUpdateIndex(ctx, service, idx)
+}
+
+func (c *chaosAzureSearch) IndexDocuments(
+ ctx context.Context, service, index string, actions []driver.IndexAction,
+) ([]driver.IndexResult, error) {
+ if err := applyChaos(ctx, c.engine, "azuresearch", "IndexDocuments"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureSearch.IndexDocuments(ctx, service, index, actions)
+}
+
+//nolint:gocritic // req matches the driver signature; delegated unchanged on success.
+func (c *chaosAzureSearch) SearchDocuments(
+ ctx context.Context, service, index string, req driver.SearchRequest,
+) (*driver.SearchResponse, error) {
+ if err := applyChaos(ctx, c.engine, "azuresearch", "SearchDocuments"); err != nil {
+ return nil, err
+ }
+
+ return c.AzureSearch.SearchDocuments(ctx, service, index, req)
+}
diff --git a/features/chaos/wrappers_azuresearch_test.go b/features/chaos/wrappers_azuresearch_test.go
new file mode 100644
index 0000000..2885915
--- /dev/null
+++ b/features/chaos/wrappers_azuresearch_test.go
@@ -0,0 +1,70 @@
+package chaos_test
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ provsearch "github.com/stackshy/cloudemu/v2/providers/azure/azuresearch"
+ "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+func newChaosAzureSearch(t *testing.T) (driver.AzureSearch, *chaos.Engine) {
+ t.Helper()
+
+ e := chaos.New(config.RealClock{})
+ t.Cleanup(e.Stop)
+
+ o := config.NewOptions(config.WithAccountID("sub-1"))
+
+ return chaos.WrapAzureSearch(provsearch.New(o), e), e
+}
+
+func TestWrapAzureSearchCreateServiceChaos(t *testing.T) {
+ a, e := newChaosAzureSearch(t)
+ ctx := context.Background()
+
+ cfg := driver.ServiceConfig{Name: "s", ResourceGroup: "rg", Location: "eastus"}
+ if _, err := a.CreateService(ctx, cfg); err != nil {
+ t.Fatalf("baseline: %v", err)
+ }
+
+ e.Apply(chaos.ServiceOutage("azuresearch", time.Hour))
+
+ cfg.Name = "s2"
+ if _, err := a.CreateService(ctx, cfg); err == nil {
+ t.Error("expected chaos error on CreateService")
+ }
+}
+
+func TestWrapAzureSearchSearchChaos(t *testing.T) {
+ a, e := newChaosAzureSearch(t)
+ ctx := context.Background()
+
+ _, _ = a.CreateService(ctx, driver.ServiceConfig{Name: "s", ResourceGroup: "rg", Location: "eastus"})
+ _, _ = a.CreateOrUpdateIndex(ctx, "s", driver.Index{Name: "i"})
+
+ if _, err := a.SearchDocuments(ctx, "s", "i", driver.SearchRequest{Search: "*"}); err != nil {
+ t.Fatalf("baseline: %v", err)
+ }
+
+ e.Apply(chaos.ServiceOutage("azuresearch", time.Hour))
+
+ if _, err := a.SearchDocuments(ctx, "s", "i", driver.SearchRequest{Search: "*"}); err == nil {
+ t.Error("expected chaos error on SearchDocuments")
+ }
+}
+
+func TestWrapAzureSearchUnwrappedPassesThrough(t *testing.T) {
+ a, e := newChaosAzureSearch(t)
+ ctx := context.Background()
+
+ e.Apply(chaos.ServiceOutage("azuresearch", time.Hour))
+
+ // ListServices is not a chaos-wrapped operation; it must still succeed.
+ if _, err := a.ListServices(ctx); err != nil {
+ t.Errorf("unwrapped ListServices should pass through, got: %v", err)
+ }
+}
diff --git a/chaos/wrappers_baseline_test.go b/features/chaos/wrappers_baseline_test.go
similarity index 90%
rename from chaos/wrappers_baseline_test.go
rename to features/chaos/wrappers_baseline_test.go
index 9b5b6d5..bcfce43 100644
--- a/chaos/wrappers_baseline_test.go
+++ b/features/chaos/wrappers_baseline_test.go
@@ -5,18 +5,18 @@ import (
"testing"
"time"
- regdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
- mqdriver "github.com/stackshy/cloudemu/messagequeue/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
- serverlessdriver "github.com/stackshy/cloudemu/serverless/driver"
+ regdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
+ mqdriver "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
+ serverlessdriver "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// Baseline tests exercise every wrapped op against an engine with no scenarios
@@ -242,4 +242,3 @@ func TestWrapEventBusBaseline(t *testing.T) {
_ = b.DeleteRule(ctx, "b", "rule")
_ = b.DeleteEventBus(ctx, "b")
}
-
diff --git a/chaos/wrappers_cache.go b/features/chaos/wrappers_cache.go
similarity index 97%
rename from chaos/wrappers_cache.go
rename to features/chaos/wrappers_cache.go
index 2f0306e..b1072bf 100644
--- a/chaos/wrappers_cache.go
+++ b/features/chaos/wrappers_cache.go
@@ -4,7 +4,7 @@ import (
"context"
"time"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
// chaosCache wraps a cache driver. Hot-path: K/V ops and atomic counters.
diff --git a/chaos/wrappers_cache_test.go b/features/chaos/wrappers_cache_test.go
similarity index 93%
rename from chaos/wrappers_cache_test.go
rename to features/chaos/wrappers_cache_test.go
index 824d178..c31ac5c 100644
--- a/chaos/wrappers_cache_test.go
+++ b/features/chaos/wrappers_cache_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
const (
diff --git a/chaos/wrappers_containerregistry.go b/features/chaos/wrappers_containerregistry.go
similarity index 97%
rename from chaos/wrappers_containerregistry.go
rename to features/chaos/wrappers_containerregistry.go
index 912ffff..46bbcbc 100644
--- a/chaos/wrappers_containerregistry.go
+++ b/features/chaos/wrappers_containerregistry.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- regdriver "github.com/stackshy/cloudemu/containerregistry/driver"
+ regdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
// chaosContainerRegistry wraps a container registry driver. Hot-path:
diff --git a/chaos/wrappers_containerregistry_test.go b/features/chaos/wrappers_containerregistry_test.go
similarity index 94%
rename from chaos/wrappers_containerregistry_test.go
rename to features/chaos/wrappers_containerregistry_test.go
index 352e6c9..f9cd6d5 100644
--- a/chaos/wrappers_containerregistry_test.go
+++ b/features/chaos/wrappers_containerregistry_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- regdriver "github.com/stackshy/cloudemu/containerregistry/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ regdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
func newChaosContainerRegistry(t *testing.T) (regdriver.ContainerRegistry, *chaos.Engine) {
diff --git a/chaos/wrappers_dns.go b/features/chaos/wrappers_dns.go
similarity index 97%
rename from chaos/wrappers_dns.go
rename to features/chaos/wrappers_dns.go
index 18b797f..1e83176 100644
--- a/chaos/wrappers_dns.go
+++ b/features/chaos/wrappers_dns.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
// chaosDNS wraps a DNS driver. Hot-path: zone and record CRUD.
diff --git a/chaos/wrappers_dns_test.go b/features/chaos/wrappers_dns_test.go
similarity index 95%
rename from chaos/wrappers_dns_test.go
rename to features/chaos/wrappers_dns_test.go
index 80c3489..2946c9b 100644
--- a/chaos/wrappers_dns_test.go
+++ b/features/chaos/wrappers_dns_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
func newChaosDNS(t *testing.T) (dnsdriver.DNS, *chaos.Engine) {
diff --git a/chaos/wrappers_eventbus.go b/features/chaos/wrappers_eventbus.go
similarity index 97%
rename from chaos/wrappers_eventbus.go
rename to features/chaos/wrappers_eventbus.go
index c91e067..90282e6 100644
--- a/chaos/wrappers_eventbus.go
+++ b/features/chaos/wrappers_eventbus.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
// chaosEventBus wraps an event bus driver. Hot-path: bus CRUD, rule CRUD,
diff --git a/chaos/wrappers_eventbus_test.go b/features/chaos/wrappers_eventbus_test.go
similarity index 95%
rename from chaos/wrappers_eventbus_test.go
rename to features/chaos/wrappers_eventbus_test.go
index 5936bd1..52d3599 100644
--- a/chaos/wrappers_eventbus_test.go
+++ b/features/chaos/wrappers_eventbus_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
func newChaosEventBus(t *testing.T) (ebdriver.EventBus, *chaos.Engine) {
diff --git a/chaos/wrappers_iam.go b/features/chaos/wrappers_iam.go
similarity index 98%
rename from chaos/wrappers_iam.go
rename to features/chaos/wrappers_iam.go
index de6fff3..7451c30 100644
--- a/chaos/wrappers_iam.go
+++ b/features/chaos/wrappers_iam.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
// chaosIAM wraps an IAM driver. Hot-path: user/role/policy CRUD plus
diff --git a/chaos/wrappers_iam_test.go b/features/chaos/wrappers_iam_test.go
similarity index 95%
rename from chaos/wrappers_iam_test.go
rename to features/chaos/wrappers_iam_test.go
index 55c4f39..696b224 100644
--- a/chaos/wrappers_iam_test.go
+++ b/features/chaos/wrappers_iam_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
func newChaosIAM(t *testing.T) (iamdriver.IAM, *chaos.Engine) {
diff --git a/chaos/wrappers_loadbalancer.go b/features/chaos/wrappers_loadbalancer.go
similarity index 97%
rename from chaos/wrappers_loadbalancer.go
rename to features/chaos/wrappers_loadbalancer.go
index 5b1bab8..309e600 100644
--- a/chaos/wrappers_loadbalancer.go
+++ b/features/chaos/wrappers_loadbalancer.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
// chaosLoadBalancer wraps a load balancer driver. Hot-path: LB / target group /
diff --git a/chaos/wrappers_loadbalancer_test.go b/features/chaos/wrappers_loadbalancer_test.go
similarity index 95%
rename from chaos/wrappers_loadbalancer_test.go
rename to features/chaos/wrappers_loadbalancer_test.go
index dc5756a..c6c806d 100644
--- a/chaos/wrappers_loadbalancer_test.go
+++ b/features/chaos/wrappers_loadbalancer_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
func newChaosLoadBalancer(t *testing.T) (lbdriver.LoadBalancer, *chaos.Engine) {
diff --git a/chaos/wrappers_logging.go b/features/chaos/wrappers_logging.go
similarity index 97%
rename from chaos/wrappers_logging.go
rename to features/chaos/wrappers_logging.go
index 50b4b89..fe2912d 100644
--- a/chaos/wrappers_logging.go
+++ b/features/chaos/wrappers_logging.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
// chaosLogging wraps a logging driver. Hot-path: log group CRUD plus
diff --git a/chaos/wrappers_logging_test.go b/features/chaos/wrappers_logging_test.go
similarity index 93%
rename from chaos/wrappers_logging_test.go
rename to features/chaos/wrappers_logging_test.go
index 5ed34ea..4d66c35 100644
--- a/chaos/wrappers_logging_test.go
+++ b/features/chaos/wrappers_logging_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
func newChaosLogging(t *testing.T) (logdriver.Logging, *chaos.Engine) {
diff --git a/chaos/wrappers_messagequeue.go b/features/chaos/wrappers_messagequeue.go
similarity index 97%
rename from chaos/wrappers_messagequeue.go
rename to features/chaos/wrappers_messagequeue.go
index 6a39ae8..a654bec 100644
--- a/chaos/wrappers_messagequeue.go
+++ b/features/chaos/wrappers_messagequeue.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- mqdriver "github.com/stackshy/cloudemu/messagequeue/driver"
+ mqdriver "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
)
// chaosMessageQueue wraps a message queue driver. Hot-path: queue CRUD plus
diff --git a/chaos/wrappers_messagequeue_test.go b/features/chaos/wrappers_messagequeue_test.go
similarity index 94%
rename from chaos/wrappers_messagequeue_test.go
rename to features/chaos/wrappers_messagequeue_test.go
index ebf6c25..dd414c5 100644
--- a/chaos/wrappers_messagequeue_test.go
+++ b/features/chaos/wrappers_messagequeue_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- mqdriver "github.com/stackshy/cloudemu/messagequeue/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ mqdriver "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
)
func newChaosMessageQueue(t *testing.T) (mqdriver.MessageQueue, *chaos.Engine) {
diff --git a/chaos/wrappers_monitoring.go b/features/chaos/wrappers_monitoring.go
similarity index 96%
rename from chaos/wrappers_monitoring.go
rename to features/chaos/wrappers_monitoring.go
index ada10ef..8564474 100644
--- a/chaos/wrappers_monitoring.go
+++ b/features/chaos/wrappers_monitoring.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// chaosMonitoring wraps a monitoring driver. Hot-path: metric and alarm CRUD.
diff --git a/chaos/wrappers_monitoring_test.go b/features/chaos/wrappers_monitoring_test.go
similarity index 92%
rename from chaos/wrappers_monitoring_test.go
rename to features/chaos/wrappers_monitoring_test.go
index e12b003..e6b51ac 100644
--- a/chaos/wrappers_monitoring_test.go
+++ b/features/chaos/wrappers_monitoring_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
func newChaosMonitoring(t *testing.T) (mondriver.Monitoring, *chaos.Engine) {
diff --git a/chaos/wrappers_networking.go b/features/chaos/wrappers_networking.go
similarity index 97%
rename from chaos/wrappers_networking.go
rename to features/chaos/wrappers_networking.go
index 361f7ba..ee9cd26 100644
--- a/chaos/wrappers_networking.go
+++ b/features/chaos/wrappers_networking.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// chaosNetworking wraps a networking driver. Hot-path: VPC/Subnet/SG CRUD +
diff --git a/chaos/wrappers_networking_test.go b/features/chaos/wrappers_networking_test.go
similarity index 96%
rename from chaos/wrappers_networking_test.go
rename to features/chaos/wrappers_networking_test.go
index ee1f5bb..e615385 100644
--- a/chaos/wrappers_networking_test.go
+++ b/features/chaos/wrappers_networking_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
func newChaosNetworking(t *testing.T) (netdriver.Networking, *chaos.Engine) {
diff --git a/chaos/wrappers_notification.go b/features/chaos/wrappers_notification.go
similarity index 96%
rename from chaos/wrappers_notification.go
rename to features/chaos/wrappers_notification.go
index 4aa2040..2acd31f 100644
--- a/chaos/wrappers_notification.go
+++ b/features/chaos/wrappers_notification.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
// chaosNotification wraps a notification driver. All ops are wrapped — the
diff --git a/chaos/wrappers_notification_test.go b/features/chaos/wrappers_notification_test.go
similarity index 93%
rename from chaos/wrappers_notification_test.go
rename to features/chaos/wrappers_notification_test.go
index e6eb940..5693b66 100644
--- a/chaos/wrappers_notification_test.go
+++ b/features/chaos/wrappers_notification_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
func newChaosNotification(t *testing.T) (notifdriver.Notification, *chaos.Engine) {
diff --git a/chaos/wrappers_sagemaker.go b/features/chaos/wrappers_sagemaker.go
similarity index 98%
rename from chaos/wrappers_sagemaker.go
rename to features/chaos/wrappers_sagemaker.go
index 9d19f20..3a3397b 100644
--- a/chaos/wrappers_sagemaker.go
+++ b/features/chaos/wrappers_sagemaker.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// chaosSageMaker wraps a SageMaker service. It consults the engine on the
diff --git a/chaos/wrappers_sagemaker_test.go b/features/chaos/wrappers_sagemaker_test.go
similarity index 91%
rename from chaos/wrappers_sagemaker_test.go
rename to features/chaos/wrappers_sagemaker_test.go
index 94b15b7..0ab6803 100644
--- a/chaos/wrappers_sagemaker_test.go
+++ b/features/chaos/wrappers_sagemaker_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
func newChaosSageMaker(t *testing.T) (driver.Service, *chaos.Engine) {
diff --git a/chaos/wrappers_secrets.go b/features/chaos/wrappers_secrets.go
similarity index 96%
rename from chaos/wrappers_secrets.go
rename to features/chaos/wrappers_secrets.go
index 232fcc5..789877f 100644
--- a/chaos/wrappers_secrets.go
+++ b/features/chaos/wrappers_secrets.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
// chaosSecrets wraps a secrets driver. All ops are wrapped — the surface is
diff --git a/chaos/wrappers_secrets_test.go b/features/chaos/wrappers_secrets_test.go
similarity index 93%
rename from chaos/wrappers_secrets_test.go
rename to features/chaos/wrappers_secrets_test.go
index 9425b80..1200b37 100644
--- a/chaos/wrappers_secrets_test.go
+++ b/features/chaos/wrappers_secrets_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
func newChaosSecrets(t *testing.T) (secretsdriver.Secrets, *chaos.Engine) {
diff --git a/chaos/wrappers_serverless.go b/features/chaos/wrappers_serverless.go
similarity index 96%
rename from chaos/wrappers_serverless.go
rename to features/chaos/wrappers_serverless.go
index d8b9047..d8a9534 100644
--- a/chaos/wrappers_serverless.go
+++ b/features/chaos/wrappers_serverless.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- serverlessdriver "github.com/stackshy/cloudemu/serverless/driver"
+ serverlessdriver "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// chaosServerless wraps a serverless driver. Hot-path: function CRUD + Invoke.
diff --git a/chaos/wrappers_serverless_test.go b/features/chaos/wrappers_serverless_test.go
similarity index 93%
rename from chaos/wrappers_serverless_test.go
rename to features/chaos/wrappers_serverless_test.go
index 07988ad..e53c16c 100644
--- a/chaos/wrappers_serverless_test.go
+++ b/features/chaos/wrappers_serverless_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- serverlessdriver "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ serverlessdriver "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
func newChaosServerless(t *testing.T) (serverlessdriver.Serverless, *chaos.Engine) {
diff --git a/chaos/wrappers_test.go b/features/chaos/wrappers_test.go
similarity index 96%
rename from chaos/wrappers_test.go
rename to features/chaos/wrappers_test.go
index 6b31f8b..79801ab 100644
--- a/chaos/wrappers_test.go
+++ b/features/chaos/wrappers_test.go
@@ -5,13 +5,13 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- awsdynamo "github.com/stackshy/cloudemu/providers/aws/dynamodb"
- awsec2 "github.com/stackshy/cloudemu/providers/aws/ec2"
- awss3 "github.com/stackshy/cloudemu/providers/aws/s3"
- storagedriver "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ awsdynamo "github.com/stackshy/cloudemu/v2/providers/aws/dynamodb"
+ awsec2 "github.com/stackshy/cloudemu/v2/providers/aws/ec2"
+ awss3 "github.com/stackshy/cloudemu/v2/providers/aws/s3"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
+ storagedriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
// withChaos wraps a fresh AWS S3 mock with a chaos-applied engine.
diff --git a/chaos/wrappers_vertexai.go b/features/chaos/wrappers_vertexai.go
similarity index 98%
rename from chaos/wrappers_vertexai.go
rename to features/chaos/wrappers_vertexai.go
index 47ef9ce..8110277 100644
--- a/chaos/wrappers_vertexai.go
+++ b/features/chaos/wrappers_vertexai.go
@@ -3,7 +3,7 @@ package chaos
import (
"context"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// chaosVertexAI wraps a Vertex AI service. It consults the engine on the calls
diff --git a/chaos/wrappers_vertexai_test.go b/features/chaos/wrappers_vertexai_test.go
similarity index 90%
rename from chaos/wrappers_vertexai_test.go
rename to features/chaos/wrappers_vertexai_test.go
index e3d534a..38eab52 100644
--- a/chaos/wrappers_vertexai_test.go
+++ b/features/chaos/wrappers_vertexai_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/chaos"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/chaos"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func newChaosVertexAI(t *testing.T) (driver.VertexAI, *chaos.Engine) {
diff --git a/inject/errors.go b/features/inject/errors.go
similarity index 100%
rename from inject/errors.go
rename to features/inject/errors.go
diff --git a/inject/inject_test.go b/features/inject/inject_test.go
similarity index 100%
rename from inject/inject_test.go
rename to features/inject/inject_test.go
diff --git a/inject/policy.go b/features/inject/policy.go
similarity index 100%
rename from inject/policy.go
rename to features/inject/policy.go
diff --git a/metrics/collector.go b/features/metrics/collector.go
similarity index 100%
rename from metrics/collector.go
rename to features/metrics/collector.go
diff --git a/metrics/metric.go b/features/metrics/metric.go
similarity index 100%
rename from metrics/metric.go
rename to features/metrics/metric.go
diff --git a/metrics/metrics_test.go b/features/metrics/metrics_test.go
similarity index 100%
rename from metrics/metrics_test.go
rename to features/metrics/metrics_test.go
diff --git a/metrics/query.go b/features/metrics/query.go
similarity index 100%
rename from metrics/query.go
rename to features/metrics/query.go
diff --git a/ratelimit/limiter.go b/features/ratelimit/limiter.go
similarity index 92%
rename from ratelimit/limiter.go
rename to features/ratelimit/limiter.go
index c1eb6ff..3b3743e 100644
--- a/ratelimit/limiter.go
+++ b/features/ratelimit/limiter.go
@@ -5,8 +5,8 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
// Limiter implements a token bucket rate limiter.
diff --git a/ratelimit/limiter_test.go b/features/ratelimit/limiter_test.go
similarity index 97%
rename from ratelimit/limiter_test.go
rename to features/ratelimit/limiter_test.go
index b509484..d70a2b5 100644
--- a/ratelimit/limiter_test.go
+++ b/features/ratelimit/limiter_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/recorder/call.go b/features/recorder/call.go
similarity index 100%
rename from recorder/call.go
rename to features/recorder/call.go
diff --git a/recorder/matcher.go b/features/recorder/matcher.go
similarity index 100%
rename from recorder/matcher.go
rename to features/recorder/matcher.go
diff --git a/recorder/recorder.go b/features/recorder/recorder.go
similarity index 100%
rename from recorder/recorder.go
rename to features/recorder/recorder.go
diff --git a/recorder/recorder_test.go b/features/recorder/recorder_test.go
similarity index 100%
rename from recorder/recorder_test.go
rename to features/recorder/recorder_test.go
diff --git a/topology/cidr.go b/features/topology/cidr.go
similarity index 96%
rename from topology/cidr.go
rename to features/topology/cidr.go
index 707cd9a..4a490c0 100644
--- a/topology/cidr.go
+++ b/features/topology/cidr.go
@@ -4,7 +4,7 @@ import (
"net"
"sort"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// ipInCIDR checks whether the given IP address is contained in the CIDR block.
diff --git a/topology/connectivity.go b/features/topology/connectivity.go
similarity index 96%
rename from topology/connectivity.go
rename to features/topology/connectivity.go
index 24c83f0..0b009e3 100644
--- a/topology/connectivity.go
+++ b/features/topology/connectivity.go
@@ -4,9 +4,9 @@ import (
"context"
"fmt"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// CanConnect evaluates whether two instances can communicate on the given
diff --git a/topology/resolve.go b/features/topology/resolve.go
similarity index 100%
rename from topology/resolve.go
rename to features/topology/resolve.go
diff --git a/topology/security.go b/features/topology/security.go
similarity index 96%
rename from topology/security.go
rename to features/topology/security.go
index 8091103..d81b143 100644
--- a/topology/security.go
+++ b/features/topology/security.go
@@ -4,8 +4,8 @@ import (
"context"
"fmt"
- cerrors "github.com/stackshy/cloudemu/errors"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// EvaluateSecurityGroups checks whether traffic from srcSG to dstSG is allowed
diff --git a/topology/topology.go b/features/topology/topology.go
similarity index 71%
rename from topology/topology.go
rename to features/topology/topology.go
index d4c3413..55abbc8 100644
--- a/topology/topology.go
+++ b/features/topology/topology.go
@@ -1,9 +1,9 @@
package topology
import (
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Engine evaluates network topology and connectivity between cloud resources.
diff --git a/topology/topology_test.go b/features/topology/topology_test.go
similarity index 97%
rename from topology/topology_test.go
rename to features/topology/topology_test.go
index 03f631c..ba954d5 100644
--- a/topology/topology_test.go
+++ b/features/topology/topology_test.go
@@ -5,13 +5,13 @@ import (
"testing"
"time"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/config"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/providers/aws/ec2"
- "github.com/stackshy/cloudemu/providers/aws/route53"
- "github.com/stackshy/cloudemu/providers/aws/vpc"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/aws/ec2"
+ "github.com/stackshy/cloudemu/v2/providers/aws/route53"
+ "github.com/stackshy/cloudemu/v2/providers/aws/vpc"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/topology/traceroute.go b/features/topology/traceroute.go
similarity index 93%
rename from topology/traceroute.go
rename to features/topology/traceroute.go
index 38e58bd..71446f2 100644
--- a/topology/traceroute.go
+++ b/features/topology/traceroute.go
@@ -4,8 +4,8 @@ import (
"context"
"fmt"
- cerrors "github.com/stackshy/cloudemu/errors"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// TraceRoute returns the network path from a source instance to a destination IP.
diff --git a/topology/types.go b/features/topology/types.go
similarity index 100%
rename from topology/types.go
rename to features/topology/types.go
diff --git a/go.mod b/go.mod
index c7b4013..2938a52 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/stackshy/cloudemu
+module github.com/stackshy/cloudemu/v2
go 1.25.0
@@ -6,16 +6,20 @@ require (
cloud.google.com/go/compute v1.60.0
cloud.google.com/go/firestore v1.22.0
cloud.google.com/go/storage v1.62.1
+ github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.7.2
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1
github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.2
+ github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v3 v3.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.3
+ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.8.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.6.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/databricks/armdatabricks v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventgrid/armeventgrid/v2 v2.3.0
+ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/machinelearning/armmachinelearning/v4 v4.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/notificationhubs/armnotificationhubs v1.2.0
@@ -23,10 +27,12 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v3 v3.3.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0
+ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/search/armsearch v1.4.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicebus/armservicebus/v2 v2.0.0-beta.3
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.5.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4
+ github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.1
github.com/aws/aws-sdk-go-v2 v1.42.1
github.com/aws/aws-sdk-go-v2/config v1.32.14
github.com/aws/aws-sdk-go-v2/credentials v1.19.14
@@ -58,6 +64,8 @@ require (
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.42.5
github.com/aws/aws-sdk-go-v2/service/sns v1.41.0
github.com/aws/aws-sdk-go-v2/service/sqs v1.42.27
+ github.com/aws/aws-sdk-go-v2/service/ssm v1.71.0
+ github.com/aws/aws-sdk-go-v2/service/sts v1.41.10
github.com/aws/smithy-go v1.27.3
github.com/databricks/databricks-sdk-go v0.144.0
github.com/fxamacker/cbor/v2 v2.9.1
@@ -97,7 +105,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
- github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
diff --git a/go.sum b/go.sum
index a09622a..ece8da1 100644
--- a/go.sum
+++ b/go.sum
@@ -26,6 +26,8 @@ cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.7.2 h1:+hDUZnYHHoXu05iXiJcL53MZW7raZZejB8ZtzVW7yyc=
+github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.7.2/go.mod h1:49PyorVrwk6G+e8Vghvn7EkAS6wSPdXEu5a8iW2/vC8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
@@ -34,12 +36,16 @@ github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 h1:l
github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3/go.mod h1:MAm7bk0oDLmD8yIkvfbxPW04fxzphPyL+7GzwHxOp6Y=
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.2 h1:zqxnp53f5Jn5PFU5Av4mvyWEbZ7whg72AoOCEzlXFKc=
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.2/go.mod h1:Krtog/7tz27z75TwM5cIS8bxEH4dcBUezcq+kGVeZEo=
+github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1 h1:j0hhYS006eJ54vusoap0f2NVZ1YY3QnaAEnLM68f0SQ=
+github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1/go.mod h1:AdtInaXmK8eYmbjezRWgLz+Qs46nc9Up9GWGwteWNfw=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v3 v3.0.0 h1:FErSe/vQGefbSuVwBV9JlrRXgG1uOFyW6TCXERX89s4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v3 v3.0.0/go.mod h1:3rnszxfW8gCLRhoFPgvfXhwSzLam4cHQZ50SugID/sg=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.3 h1:puJogXZNILDxFHrXTSgjF9P7lgJkr37hr31h5r7CC7I=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.3/go.mod h1:ZCM0BEa95+Ov7zRyPZS40SY1pFANErtVgERanZHtgcg=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.8.0 h1:ZMGAqCZov8+7iFUPWKVcTaLgNXUeTlz20sIuWkQWNfg=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.8.0/go.mod h1:BElPQ/GZtrdQ2i5uDZw3OKLE1we75W0AEWyeBR1TWQA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0 h1:5n7dPVqsWfVKw+ZiEKSd3Kzu7gwBkbEBkeXb8rgaE9Q=
@@ -58,14 +64,16 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFG
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/machinelearning/armmachinelearning/v4 v4.0.0 h1:H5ykGRBhX8CJKpB2tiRVut1DPbH7BnYKQ+orFTD7+JY=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/machinelearning/armmachinelearning/v4 v4.0.0/go.mod h1:nivJvZio5SHpEASAk8LKIueqs7zEHU+iRLKWbx3Xi10=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0 h1:3jDMffAwnvs6qmOqhjNVHB29AKxs6brnzJeo65E1YwM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers v1.2.0/go.mod h1:0mKVz3WT8oNjBunT1zD/HPwMleQ72QClMa7Gmsm+6Kc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 h1:QM6sE5k2ZT/vI5BEe0r7mqjsUSnhVBFbOsVkEuaEfiA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0/go.mod h1:243D9iHbcQXoFUtgHJwL7gl2zx1aDuDMjvBZVGr2uW0=
-github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights v1.0.0 h1:hdC0QToUqJikvZXE/ZSHafNv10IcYYkCPY7B99QhNSc=
-github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights v1.0.0/go.mod h1:FwSGecUzQMdebvcN8JqqLlsfgfXZn+Gw+vX9xpHhUMc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/notificationhubs/armnotificationhubs v1.2.0 h1:ZzshIzB4SnLLHFFHbBMxG5Sn2QCo9LWl4K5Nqz0Eysk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/notificationhubs/armnotificationhubs v1.2.0/go.mod h1:YuV5NCOq5toIonfwdVfw2j99Uuy25Df2Tb5O83ZIuV4=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights v1.0.0 h1:hdC0QToUqJikvZXE/ZSHafNv10IcYYkCPY7B99QhNSc=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights v1.0.0/go.mod h1:FwSGecUzQMdebvcN8JqqLlsfgfXZn+Gw+vX9xpHhUMc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 h1:HzqcSJWx32XQdr8KtxAu/SZJj0PqDo9tKf2YGPdynV0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0/go.mod h1:nKcJObAisSPDrO9lMuuCBoYY7Ki7ADt8p6XmBhpKNTk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v3 v3.3.0 h1:EkL5dmUoy1OlzVfsbkcHayOvOJgheyRYL3wM/RHizzg=
@@ -74,6 +82,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourceg
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/search/armsearch v1.4.0 h1:zBdabY8pMSMLPb1XJnFSEdJi9Bd0h+VMjh1uU8B6Yp8=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/search/armsearch v1.4.0/go.mod h1:Y2Q3nB3UfSnG9nALOpPAjflXPM3jL/n2ZmYIu2Occ9g=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicebus/armservicebus/v2 v2.0.0-beta.3 h1:JLPf82byRFgfB0f2feJMmRfCCFV0W9/GayiiewTvjlw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicebus/armservicebus/v2 v2.0.0-beta.3/go.mod h1:9sfaaa+UF5VVus+Tr/bd1qm1oRoltnewm3HpiT9l8VU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0 h1:S087deZ0kP1RUg4pU7w9U9xpUedTCbOtz+mnd0+hrkQ=
@@ -86,6 +96,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfg
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.1 h1:qvrrnQ2mIjwY7IVlQuNB0ma43Nr74+9ZTZJ60KlmlV4=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.1/go.mod h1:FkF/Az07vR3S4sBdjCuisznWfFWOD8u6Ibm/g/oyDAk=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 h1:4iB+IesclUXdP0ICgAabvq2FYLXrJWKx1fJQ+GxSo3Y=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
@@ -134,12 +146,12 @@ github.com/aws/aws-sdk-go-v2/service/ecr v1.58.4 h1:fo6cmbxkKq/OtKUG0sK70fDsYjtK
github.com/aws/aws-sdk-go-v2/service/ecr v1.58.4/go.mod h1:7VJFM2lSPHz2I1rRb0a+lbphoOp7hXIgYjGhSTOLY7k=
github.com/aws/aws-sdk-go-v2/service/eks v1.83.0 h1:mS5rkyFt+NYryy0p4n8o80tJjBmXiQrRCQjP8jZcSLY=
github.com/aws/aws-sdk-go-v2/service/eks v1.83.0/go.mod h1:JQcyECIV9iZHm+GMrWn1pTPTJYRavOVsqPvlCbjt+Fg=
+github.com/aws/aws-sdk-go-v2/service/elasticache v1.55.0 h1:aIfwo2WwNv4Ya/wdg6X7oCVkCjVzACErH/BSsy7EQGM=
+github.com/aws/aws-sdk-go-v2/service/elasticache v1.55.0/go.mod h1:roYWQ6ZmGI1VshRoopJCfMYdDgI1z4ArMtTOJJjsHXg=
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.56.0 h1:OJRqQ6G7RjmwJ9fkhFgcJBSinjrLJxfd5AacBUrhKXc=
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.56.0/go.mod h1:qNnJkZTDHDL2sO8hyVH2yILcfSEkjP/pIns2JsF1g1o=
github.com/aws/aws-sdk-go-v2/service/eventbridge v1.47.0 h1:LuPLDBMCmldgg779sq8swfEdRNMeKlaT852uxKYCOkk=
github.com/aws/aws-sdk-go-v2/service/eventbridge v1.47.0/go.mod h1:3g/foYPw/4CT8yV7/A1QsbvnhDZW/2x2uzl4vkqX49o=
-github.com/aws/aws-sdk-go-v2/service/elasticache v1.55.0 h1:aIfwo2WwNv4Ya/wdg6X7oCVkCjVzACErH/BSsy7EQGM=
-github.com/aws/aws-sdk-go-v2/service/elasticache v1.55.0/go.mod h1:roYWQ6ZmGI1VshRoopJCfMYdDgI1z4ArMtTOJJjsHXg=
github.com/aws/aws-sdk-go-v2/service/iam v1.53.10 h1:kcN3I3llO7VwIY5w3Pc5FmEonpsr23Ou7Cwk4qf7dik=
github.com/aws/aws-sdk-go-v2/service/iam v1.53.10/go.mod h1:1vkJzjCYC3byO0kIrBqLPzvZpuvYhPXkuyARs6E7tM4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
@@ -182,6 +194,8 @@ github.com/aws/aws-sdk-go-v2/service/sns v1.41.0 h1:GT6QdvVfByxl1/AJQe7PNbLtQDj0
github.com/aws/aws-sdk-go-v2/service/sns v1.41.0/go.mod h1:5EnTxMpMVeiY0vcjjN/a958FFaHrS6XfXcyRBzDKDCE=
github.com/aws/aws-sdk-go-v2/service/sqs v1.42.27 h1:QgaWXVmNDxv/U/3UIHfGb7ohvtFgerf/bYcYylj4i8E=
github.com/aws/aws-sdk-go-v2/service/sqs v1.42.27/go.mod h1:8S6ExnLprS0oIeA8ZlHkJUJ0BMpKqnRPws/S0jegTqQ=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.71.0 h1:Z5oWeBPXlKfYMctLgbnD0bXbhuqpxsV/KnZpsLLEMLQ=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.71.0/go.mod h1:xabzRvdbMs3FG9kU5M6RUOuCW6wXDkpdIqoXXNzA1nQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6fuOwWlWpD2StNLTceKpys=
diff --git a/internal/memstore/store_test.go b/internal/memstore/store_test.go
index 6f0f153..c369885 100644
--- a/internal/memstore/store_test.go
+++ b/internal/memstore/store_test.go
@@ -62,11 +62,11 @@ func TestStore_Has(t *testing.T) {
func TestStore_Delete(t *testing.T) {
tests := []struct {
- name string
- setup map[string]string
- deleteKey string
- expectDel bool
- expectLen int
+ name string
+ setup map[string]string
+ deleteKey string
+ expectDel bool
+ expectLen int
}{
{name: "delete existing key", setup: map[string]string{"a": "1", "b": "2"}, deleteKey: "a", expectDel: true, expectLen: 1},
{name: "delete missing key", setup: map[string]string{"a": "1"}, deleteKey: "z", expectDel: false, expectLen: 1},
@@ -235,9 +235,9 @@ func TestStore_Filter(t *testing.T) {
expect map[string]int
}{
{
- name: "filter even values",
- setup: map[string]int{"a": 1, "b": 2, "c": 3, "d": 4},
- pred: func(_ string, v int) bool { return v%2 == 0 },
+ name: "filter even values",
+ setup: map[string]int{"a": 1, "b": 2, "c": 3, "d": 4},
+ pred: func(_ string, v int) bool { return v%2 == 0 },
expect: map[string]int{"b": 2, "d": 4},
},
{
diff --git a/pagination/pagination.go b/internal/pagination/pagination.go
similarity index 100%
rename from pagination/pagination.go
rename to internal/pagination/pagination.go
diff --git a/pagination/pagination_test.go b/internal/pagination/pagination_test.go
similarity index 100%
rename from pagination/pagination_test.go
rename to internal/pagination/pagination_test.go
diff --git a/pagination/token.go b/internal/pagination/token.go
similarity index 100%
rename from pagination/token.go
rename to internal/pagination/token.go
diff --git a/statemachine/machine.go b/internal/statemachine/machine.go
similarity index 98%
rename from statemachine/machine.go
rename to internal/statemachine/machine.go
index 5990b79..e14aaa4 100644
--- a/statemachine/machine.go
+++ b/internal/statemachine/machine.go
@@ -4,7 +4,7 @@ import (
"fmt"
"sync"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
// Callback is called when a state transition occurs.
diff --git a/statemachine/statemachine_test.go b/internal/statemachine/statemachine_test.go
similarity index 98%
rename from statemachine/statemachine_test.go
rename to internal/statemachine/statemachine_test.go
index 87e82e4..6cfa778 100644
--- a/statemachine/statemachine_test.go
+++ b/internal/statemachine/statemachine_test.go
@@ -61,10 +61,10 @@ func TestMachine_GetState_NotFound(t *testing.T) {
func TestMachine_Transition_Valid(t *testing.T) {
tests := []struct {
- name string
- initial string
- target string
- expectSt string
+ name string
+ initial string
+ target string
+ expectSt string
}{
{name: "pending to running", initial: "pending", target: "running", expectSt: "running"},
{name: "running to stopped", initial: "running", target: "stopped", expectSt: "stopped"},
diff --git a/statemachine/transitions.go b/internal/statemachine/transitions.go
similarity index 100%
rename from statemachine/transitions.go
rename to internal/statemachine/transitions.go
diff --git a/providers/aws/aws.go b/providers/aws/aws.go
index d4153e7..39ac3ef 100644
--- a/providers/aws/aws.go
+++ b/providers/aws/aws.go
@@ -2,29 +2,30 @@
package aws
import (
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/providers/aws/awsiam"
- "github.com/stackshy/cloudemu/providers/aws/bedrock"
- "github.com/stackshy/cloudemu/providers/aws/cloudwatch"
- "github.com/stackshy/cloudemu/providers/aws/cloudwatchlogs"
- "github.com/stackshy/cloudemu/providers/aws/dynamodb"
- "github.com/stackshy/cloudemu/providers/aws/ec2"
- "github.com/stackshy/cloudemu/providers/aws/ecr"
- "github.com/stackshy/cloudemu/providers/aws/eks"
- "github.com/stackshy/cloudemu/providers/aws/elasticache"
- "github.com/stackshy/cloudemu/providers/aws/elb"
- "github.com/stackshy/cloudemu/providers/aws/eventbridge"
- "github.com/stackshy/cloudemu/providers/aws/lambda"
- "github.com/stackshy/cloudemu/providers/aws/rds"
- "github.com/stackshy/cloudemu/providers/aws/redshift"
- "github.com/stackshy/cloudemu/providers/aws/route53"
- "github.com/stackshy/cloudemu/providers/aws/s3"
- "github.com/stackshy/cloudemu/providers/aws/sagemaker"
- "github.com/stackshy/cloudemu/providers/aws/secretsmanager"
- "github.com/stackshy/cloudemu/providers/aws/sns"
- "github.com/stackshy/cloudemu/providers/aws/sqs"
- "github.com/stackshy/cloudemu/providers/aws/vpc"
- "github.com/stackshy/cloudemu/resourcediscovery"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/aws/awsiam"
+ "github.com/stackshy/cloudemu/v2/providers/aws/bedrock"
+ "github.com/stackshy/cloudemu/v2/providers/aws/cloudwatch"
+ "github.com/stackshy/cloudemu/v2/providers/aws/cloudwatchlogs"
+ "github.com/stackshy/cloudemu/v2/providers/aws/dynamodb"
+ "github.com/stackshy/cloudemu/v2/providers/aws/ec2"
+ "github.com/stackshy/cloudemu/v2/providers/aws/ecr"
+ "github.com/stackshy/cloudemu/v2/providers/aws/eks"
+ "github.com/stackshy/cloudemu/v2/providers/aws/elasticache"
+ "github.com/stackshy/cloudemu/v2/providers/aws/elb"
+ "github.com/stackshy/cloudemu/v2/providers/aws/eventbridge"
+ "github.com/stackshy/cloudemu/v2/providers/aws/lambda"
+ "github.com/stackshy/cloudemu/v2/providers/aws/rds"
+ "github.com/stackshy/cloudemu/v2/providers/aws/redshift"
+ "github.com/stackshy/cloudemu/v2/providers/aws/route53"
+ "github.com/stackshy/cloudemu/v2/providers/aws/s3"
+ "github.com/stackshy/cloudemu/v2/providers/aws/sagemaker"
+ "github.com/stackshy/cloudemu/v2/providers/aws/secretsmanager"
+ "github.com/stackshy/cloudemu/v2/providers/aws/sns"
+ "github.com/stackshy/cloudemu/v2/providers/aws/sqs"
+ "github.com/stackshy/cloudemu/v2/providers/aws/ssm"
+ "github.com/stackshy/cloudemu/v2/providers/aws/vpc"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
)
// Provider holds all AWS mock services.
@@ -50,6 +51,7 @@ type Provider struct {
EKS *eks.Mock
Bedrock *bedrock.Mock
SageMaker *sagemaker.Mock
+ SSM *ssm.Mock
ResourceDiscovery *resourcediscovery.Engine
}
@@ -78,6 +80,7 @@ func New(opts ...config.Option) *Provider {
EKS: eks.New(o),
Bedrock: bedrock.New(o),
SageMaker: sagemaker.New(o),
+ SSM: ssm.New(o),
}
p.EC2.SetMonitoring(p.CloudWatch)
p.S3.SetMonitoring(p.CloudWatch)
diff --git a/providers/aws/awsiam/iam.go b/providers/aws/awsiam/iam.go
index fe04353..dc6200d 100644
--- a/providers/aws/awsiam/iam.go
+++ b/providers/aws/awsiam/iam.go
@@ -8,11 +8,11 @@ import (
"strings"
"sync"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/iam/driver"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
const timeFormat = "2006-01-02T15:04:05Z"
diff --git a/providers/aws/awsiam/iam_test.go b/providers/aws/awsiam/iam_test.go
index 7f85b30..9fad400 100644
--- a/providers/aws/awsiam/iam_test.go
+++ b/providers/aws/awsiam/iam_test.go
@@ -7,9 +7,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/iam/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
func newTestMock() *Mock {
diff --git a/providers/aws/bedrock/bedrock.go b/providers/aws/bedrock/bedrock.go
index 175316c..df8c298 100644
--- a/providers/aws/bedrock/bedrock.go
+++ b/providers/aws/bedrock/bedrock.go
@@ -9,11 +9,11 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/bedrock/driver"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/bedrock/driver"
)
// Compile-time check that Mock implements driver.Bedrock.
diff --git a/providers/aws/bedrock/bedrock_test.go b/providers/aws/bedrock/bedrock_test.go
index 96349f0..500c022 100644
--- a/providers/aws/bedrock/bedrock_test.go
+++ b/providers/aws/bedrock/bedrock_test.go
@@ -6,8 +6,8 @@ import (
"testing"
"time"
- bedrockdriver "github.com/stackshy/cloudemu/bedrock/driver"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2/config"
+ bedrockdriver "github.com/stackshy/cloudemu/v2/services/bedrock/driver"
)
const titanModel = "amazon.titan-text-express-v1"
diff --git a/providers/aws/bedrock/management.go b/providers/aws/bedrock/management.go
index e27d064..4650bc5 100644
--- a/providers/aws/bedrock/management.go
+++ b/providers/aws/bedrock/management.go
@@ -3,9 +3,9 @@ package bedrock
import (
"context"
- "github.com/stackshy/cloudemu/bedrock/driver"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/bedrock/driver"
)
// guardrailDraftVersion is the working version assigned to a freshly created
diff --git a/providers/aws/bedrock/management_test.go b/providers/aws/bedrock/management_test.go
index 6f09cbd..530ef6c 100644
--- a/providers/aws/bedrock/management_test.go
+++ b/providers/aws/bedrock/management_test.go
@@ -4,7 +4,7 @@ import (
"context"
"testing"
- bedrockdriver "github.com/stackshy/cloudemu/bedrock/driver"
+ bedrockdriver "github.com/stackshy/cloudemu/v2/services/bedrock/driver"
)
func newGuardrail(t *testing.T, m *Mock, name string) *bedrockdriver.Guardrail {
diff --git a/providers/aws/bedrock/runtime.go b/providers/aws/bedrock/runtime.go
index f47c5e6..f72898f 100644
--- a/providers/aws/bedrock/runtime.go
+++ b/providers/aws/bedrock/runtime.go
@@ -6,9 +6,9 @@ import (
"fmt"
"strings"
- "github.com/stackshy/cloudemu/bedrock/driver"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/bedrock/driver"
)
const (
diff --git a/providers/aws/cloudwatch/cloudwatch.go b/providers/aws/cloudwatch/cloudwatch.go
index 2770b1b..cb0d118 100644
--- a/providers/aws/cloudwatch/cloudwatch.go
+++ b/providers/aws/cloudwatch/cloudwatch.go
@@ -9,11 +9,11 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Compile-time check that Mock implements driver.Monitoring.
diff --git a/providers/aws/cloudwatch/cloudwatch_test.go b/providers/aws/cloudwatch/cloudwatch_test.go
index 8c7412a..dcecb3b 100644
--- a/providers/aws/cloudwatch/cloudwatch_test.go
+++ b/providers/aws/cloudwatch/cloudwatch_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
func newTestMock() *Mock {
diff --git a/providers/aws/cloudwatchlogs/cloudwatchlogs.go b/providers/aws/cloudwatchlogs/cloudwatchlogs.go
index e03606a..f178cd0 100644
--- a/providers/aws/cloudwatchlogs/cloudwatchlogs.go
+++ b/providers/aws/cloudwatchlogs/cloudwatchlogs.go
@@ -7,12 +7,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/logging/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/logging/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/providers/aws/cloudwatchlogs/cloudwatchlogs_test.go b/providers/aws/cloudwatchlogs/cloudwatchlogs_test.go
index 74706ad..936f6f1 100644
--- a/providers/aws/cloudwatchlogs/cloudwatchlogs_test.go
+++ b/providers/aws/cloudwatchlogs/cloudwatchlogs_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/logging/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/logging/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/aws/dynamodb/dynamodb.go b/providers/aws/dynamodb/dynamodb.go
index a9555c3..8de8a46 100644
--- a/providers/aws/dynamodb/dynamodb.go
+++ b/providers/aws/dynamodb/dynamodb.go
@@ -8,12 +8,12 @@ import (
"sync"
"sync/atomic"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/database/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/pagination"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/internal/pagination"
+ "github.com/stackshy/cloudemu/v2/services/database/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Scan/query filter operator constants.
diff --git a/providers/aws/dynamodb/dynamodb_test.go b/providers/aws/dynamodb/dynamodb_test.go
index e666d76..50dd035 100644
--- a/providers/aws/dynamodb/dynamodb_test.go
+++ b/providers/aws/dynamodb/dynamodb_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/database/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/providers/aws/cloudwatch"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/aws/cloudwatch"
+ "github.com/stackshy/cloudemu/v2/services/database/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
func newTestMock() *Mock {
diff --git a/providers/aws/ec2/asg.go b/providers/aws/ec2/asg.go
index 1fe8bd7..bfdac46 100644
--- a/providers/aws/ec2/asg.go
+++ b/providers/aws/ec2/asg.go
@@ -3,9 +3,9 @@ package ec2
import (
"context"
- "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
const (
diff --git a/providers/aws/ec2/ec2.go b/providers/aws/ec2/ec2.go
index 8824876..c8360b5 100644
--- a/providers/aws/ec2/ec2.go
+++ b/providers/aws/ec2/ec2.go
@@ -6,14 +6,14 @@ import (
"sync/atomic"
"time"
- "github.com/stackshy/cloudemu/compute"
- "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/statemachine"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/internal/statemachine"
+ "github.com/stackshy/cloudemu/v2/services/compute"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
var _ driver.Compute = (*Mock)(nil)
@@ -211,6 +211,13 @@ func (m *Mock) RunInstances(ctx context.Context, cfg driver.InstanceConfig, coun
return nil, cerrors.New(cerrors.InvalidArgument, "count must be greater than 0")
}
+ // Bound the requested count so an oversized MaxCount can't drive an
+ // unbounded slice allocation (real providers cap instances per call).
+ const maxRunInstances = 1000
+ if count > maxRunInstances {
+ return nil, cerrors.Newf(cerrors.InvalidArgument, "count %d exceeds the maximum of %d per call", count, maxRunInstances)
+ }
+
results := make([]driver.Instance, 0, count)
for i := 0; i < count; i++ {
diff --git a/providers/aws/ec2/ec2_test.go b/providers/aws/ec2/ec2_test.go
index 20defa8..9283b4f 100644
--- a/providers/aws/ec2/ec2_test.go
+++ b/providers/aws/ec2/ec2_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/compute"
- "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/compute"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
func newTestMock() *Mock {
diff --git a/providers/aws/ec2/spot.go b/providers/aws/ec2/spot.go
index 7a8276c..181f716 100644
--- a/providers/aws/ec2/spot.go
+++ b/providers/aws/ec2/spot.go
@@ -3,9 +3,9 @@ package ec2
import (
"context"
- "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
const (
diff --git a/providers/aws/ec2/template.go b/providers/aws/ec2/template.go
index a8a38bd..71c7d38 100644
--- a/providers/aws/ec2/template.go
+++ b/providers/aws/ec2/template.go
@@ -4,9 +4,9 @@ import (
"context"
"sync/atomic"
- "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
//nolint:gochecknoglobals // atomic counter for template versioning
diff --git a/providers/aws/ecr/ecr.go b/providers/aws/ecr/ecr.go
index f47a11c..84514e0 100644
--- a/providers/aws/ecr/ecr.go
+++ b/providers/aws/ecr/ecr.go
@@ -10,11 +10,11 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/containerregistry/driver"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/providers/aws/ecr/ecr_test.go b/providers/aws/ecr/ecr_test.go
index 6259a8f..96927d3 100644
--- a/providers/aws/ecr/ecr_test.go
+++ b/providers/aws/ecr/ecr_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/containerregistry/driver"
- "github.com/stackshy/cloudemu/providers/aws/cloudwatch"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/aws/cloudwatch"
+ "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -547,9 +547,9 @@ func TestTagImageEmptyTagRejected(t *testing.T) {
func TestImageTagMutability(t *testing.T) {
tests := []struct {
- name string
+ name string
mutability string
- expectErr bool
+ expectErr bool
}{
{
name: "MUTABLE allows duplicate tags",
diff --git a/providers/aws/eks/eks.go b/providers/aws/eks/eks.go
index 65c67db..68a4aaa 100644
--- a/providers/aws/eks/eks.go
+++ b/providers/aws/eks/eks.go
@@ -18,13 +18,13 @@ import (
"fmt"
"sync"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/kubernetes"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- eksdriver "github.com/stackshy/cloudemu/providers/aws/eks/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ eksdriver "github.com/stackshy/cloudemu/v2/providers/aws/eks/driver"
+ "github.com/stackshy/cloudemu/v2/services/kubernetes"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Wave 1 placeholder for the cluster API server endpoint. Wave 2 will swap
diff --git a/providers/aws/eks/eks_k8s_test.go b/providers/aws/eks/eks_k8s_test.go
index b4d7f1f..93271da 100644
--- a/providers/aws/eks/eks_k8s_test.go
+++ b/providers/aws/eks/eks_k8s_test.go
@@ -9,8 +9,8 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu/kubernetes"
- eksdriver "github.com/stackshy/cloudemu/providers/aws/eks/driver"
+ eksdriver "github.com/stackshy/cloudemu/v2/providers/aws/eks/driver"
+ "github.com/stackshy/cloudemu/v2/services/kubernetes"
)
func TestSetK8sAPI_CreateClusterRegistersWithAPIServer(t *testing.T) {
diff --git a/providers/aws/eks/eks_test.go b/providers/aws/eks/eks_test.go
index 88b646e..e178b2d 100644
--- a/providers/aws/eks/eks_test.go
+++ b/providers/aws/eks/eks_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- eksdriver "github.com/stackshy/cloudemu/providers/aws/eks/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ eksdriver "github.com/stackshy/cloudemu/v2/providers/aws/eks/driver"
)
func newTestMock() *Mock {
diff --git a/providers/aws/elasticache/elasticache.go b/providers/aws/elasticache/elasticache.go
index b868f70..fad75d5 100644
--- a/providers/aws/elasticache/elasticache.go
+++ b/providers/aws/elasticache/elasticache.go
@@ -8,11 +8,11 @@ import (
"strconv"
"time"
- "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/cache/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const defaultRedisPort = 6379
diff --git a/providers/aws/elasticache/elasticache_test.go b/providers/aws/elasticache/elasticache_test.go
index a58ceac..b6521f3 100644
--- a/providers/aws/elasticache/elasticache_test.go
+++ b/providers/aws/elasticache/elasticache_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/cache/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/aws/elb/elb.go b/providers/aws/elb/elb.go
index 9be1315..6d7f3a5 100644
--- a/providers/aws/elb/elb.go
+++ b/providers/aws/elb/elb.go
@@ -6,11 +6,11 @@ import (
"fmt"
"sync"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/loadbalancer/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
// Compile-time check that Mock implements driver.LoadBalancer.
diff --git a/providers/aws/elb/elb_test.go b/providers/aws/elb/elb_test.go
index eb370c3..185ed9c 100644
--- a/providers/aws/elb/elb_test.go
+++ b/providers/aws/elb/elb_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/loadbalancer/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
func newTestMock() *Mock {
diff --git a/providers/aws/eventbridge/eventbridge.go b/providers/aws/eventbridge/eventbridge.go
index 2f876b8..e5ea2c1 100644
--- a/providers/aws/eventbridge/eventbridge.go
+++ b/providers/aws/eventbridge/eventbridge.go
@@ -9,12 +9,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/providers/aws/eventbridge/eventbridge_test.go b/providers/aws/eventbridge/eventbridge_test.go
index aa17896..ece958f 100644
--- a/providers/aws/eventbridge/eventbridge_test.go
+++ b/providers/aws/eventbridge/eventbridge_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/providers/aws/cloudwatch"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/aws/cloudwatch"
+ "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -61,8 +61,8 @@ func TestCreateEventBus(t *testing.T) {
expectErr: true,
},
{
- name: "duplicate default bus",
- cfg: driver.EventBusConfig{Name: "default"},
+ name: "duplicate default bus",
+ cfg: driver.EventBusConfig{Name: "default"},
expectErr: true,
},
{
@@ -562,63 +562,63 @@ func TestPutEvents(t *testing.T) {
func TestEventPatternMatching(t *testing.T) {
tests := []struct {
- name string
- pattern string
- event driver.Event
- shouldMatch bool
+ name string
+ pattern string
+ event driver.Event
+ shouldMatch bool
}{
{
- name: "match by source",
- pattern: `{"source":["aws.ec2"]}`,
- event: driver.Event{Source: "aws.ec2", DetailType: "EC2 Instance State-change"},
+ name: "match by source",
+ pattern: `{"source":["aws.ec2"]}`,
+ event: driver.Event{Source: "aws.ec2", DetailType: "EC2 Instance State-change"},
shouldMatch: true,
},
{
- name: "no match by source",
- pattern: `{"source":["aws.ec2"]}`,
- event: driver.Event{Source: "aws.s3", DetailType: "Object Created"},
+ name: "no match by source",
+ pattern: `{"source":["aws.ec2"]}`,
+ event: driver.Event{Source: "aws.s3", DetailType: "Object Created"},
shouldMatch: false,
},
{
- name: "match by detail-type",
- pattern: `{"detail-type":["OrderCreated"]}`,
- event: driver.Event{Source: "my.app", DetailType: "OrderCreated"},
+ name: "match by detail-type",
+ pattern: `{"detail-type":["OrderCreated"]}`,
+ event: driver.Event{Source: "my.app", DetailType: "OrderCreated"},
shouldMatch: true,
},
{
- name: "no match by detail-type",
- pattern: `{"detail-type":["OrderCreated"]}`,
- event: driver.Event{Source: "my.app", DetailType: "OrderShipped"},
+ name: "no match by detail-type",
+ pattern: `{"detail-type":["OrderCreated"]}`,
+ event: driver.Event{Source: "my.app", DetailType: "OrderShipped"},
shouldMatch: false,
},
{
- name: "match by source and detail-type",
- pattern: `{"source":["aws.ec2"],"detail-type":["EC2 Instance State-change"]}`,
- event: driver.Event{Source: "aws.ec2", DetailType: "EC2 Instance State-change"},
+ name: "match by source and detail-type",
+ pattern: `{"source":["aws.ec2"],"detail-type":["EC2 Instance State-change"]}`,
+ event: driver.Event{Source: "aws.ec2", DetailType: "EC2 Instance State-change"},
shouldMatch: true,
},
{
- name: "source matches but detail-type does not",
- pattern: `{"source":["aws.ec2"],"detail-type":["EC2 Instance State-change"]}`,
- event: driver.Event{Source: "aws.ec2", DetailType: "Other Event"},
+ name: "source matches but detail-type does not",
+ pattern: `{"source":["aws.ec2"],"detail-type":["EC2 Instance State-change"]}`,
+ event: driver.Event{Source: "aws.ec2", DetailType: "Other Event"},
shouldMatch: false,
},
{
- name: "empty pattern matches all",
- pattern: "",
- event: driver.Event{Source: "anything", DetailType: "anything"},
+ name: "empty pattern matches all",
+ pattern: "",
+ event: driver.Event{Source: "anything", DetailType: "anything"},
shouldMatch: true,
},
{
- name: "multiple sources",
- pattern: `{"source":["aws.ec2","aws.s3"]}`,
- event: driver.Event{Source: "aws.s3", DetailType: "Object Created"},
+ name: "multiple sources",
+ pattern: `{"source":["aws.ec2","aws.s3"]}`,
+ event: driver.Event{Source: "aws.s3", DetailType: "Object Created"},
shouldMatch: true,
},
{
- name: "invalid json pattern",
- pattern: "not json",
- event: driver.Event{Source: "anything"},
+ name: "invalid json pattern",
+ pattern: "not json",
+ event: driver.Event{Source: "anything"},
shouldMatch: false,
},
}
diff --git a/providers/aws/lambda/aliases.go b/providers/aws/lambda/aliases.go
index aa91563..f9203ef 100644
--- a/providers/aws/lambda/aliases.go
+++ b/providers/aws/lambda/aliases.go
@@ -4,9 +4,9 @@ import (
"context"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// CreateAlias creates a new alias pointing to a specific function version.
diff --git a/providers/aws/lambda/concurrency.go b/providers/aws/lambda/concurrency.go
index d852638..2e3ba9d 100644
--- a/providers/aws/lambda/concurrency.go
+++ b/providers/aws/lambda/concurrency.go
@@ -3,8 +3,8 @@ package lambda
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// PutFunctionConcurrency sets reserved concurrency for a function.
diff --git a/providers/aws/lambda/esm.go b/providers/aws/lambda/esm.go
index 66b11c5..fbc8db3 100644
--- a/providers/aws/lambda/esm.go
+++ b/providers/aws/lambda/esm.go
@@ -5,8 +5,8 @@ import (
"fmt"
"sync/atomic"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// mappingCounter is used to generate unique UUIDs for event source mappings.
diff --git a/providers/aws/lambda/lambda.go b/providers/aws/lambda/lambda.go
index f24355d..e912a58 100644
--- a/providers/aws/lambda/lambda.go
+++ b/providers/aws/lambda/lambda.go
@@ -7,12 +7,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
var _ driver.Serverless = (*Mock)(nil)
diff --git a/providers/aws/lambda/lambda_test.go b/providers/aws/lambda/lambda_test.go
index 32b87c5..b3308a9 100644
--- a/providers/aws/lambda/lambda_test.go
+++ b/providers/aws/lambda/lambda_test.go
@@ -7,10 +7,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/providers/aws/cloudwatch"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/aws/cloudwatch"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
func newTestMock() *Mock {
diff --git a/providers/aws/lambda/layers.go b/providers/aws/lambda/layers.go
index d9dae95..bb47c57 100644
--- a/providers/aws/lambda/layers.go
+++ b/providers/aws/lambda/layers.go
@@ -7,10 +7,10 @@ import (
"strconv"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// PublishLayerVersion publishes a new version of a layer.
diff --git a/providers/aws/lambda/versions.go b/providers/aws/lambda/versions.go
index 19cf5b5..f5ea0cd 100644
--- a/providers/aws/lambda/versions.go
+++ b/providers/aws/lambda/versions.go
@@ -5,8 +5,8 @@ import (
"strconv"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// latestVersion is the symbolic version for the current function code.
diff --git a/providers/aws/rds/rds.go b/providers/aws/rds/rds.go
index 9e010dd..5fec370 100644
--- a/providers/aws/rds/rds.go
+++ b/providers/aws/rds/rds.go
@@ -12,12 +12,12 @@ import (
"fmt"
"sync"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
const (
diff --git a/providers/aws/rds/rds_test.go b/providers/aws/rds/rds_test.go
index aa786b0..4673d96 100644
--- a/providers/aws/rds/rds_test.go
+++ b/providers/aws/rds/rds_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
func newTestMock() *Mock {
diff --git a/providers/aws/redshift/redshift.go b/providers/aws/redshift/redshift.go
index 60e78bd..fb06422 100644
--- a/providers/aws/redshift/redshift.go
+++ b/providers/aws/redshift/redshift.go
@@ -14,12 +14,12 @@ import (
"fmt"
"sync"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- rdbdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ rdbdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
const (
diff --git a/providers/aws/redshift/redshift_test.go b/providers/aws/redshift/redshift_test.go
index 1ca2e8b..6d8e2cb 100644
--- a/providers/aws/redshift/redshift_test.go
+++ b/providers/aws/redshift/redshift_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- rdbdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ rdbdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
func newTestMock() *Mock {
diff --git a/providers/aws/route53/healthcheck.go b/providers/aws/route53/healthcheck.go
index 58e6a38..7bef2dd 100644
--- a/providers/aws/route53/healthcheck.go
+++ b/providers/aws/route53/healthcheck.go
@@ -3,9 +3,9 @@ package route53
import (
"context"
- "github.com/stackshy/cloudemu/dns/driver"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
const (
diff --git a/providers/aws/route53/route53.go b/providers/aws/route53/route53.go
index 8d26771..a59720a 100644
--- a/providers/aws/route53/route53.go
+++ b/providers/aws/route53/route53.go
@@ -5,11 +5,11 @@ import (
"context"
"strings"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/dns/driver"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
// Compile-time check that Mock implements driver.DNS.
diff --git a/providers/aws/route53/route53_test.go b/providers/aws/route53/route53_test.go
index 58d5688..cc0fb8e 100644
--- a/providers/aws/route53/route53_test.go
+++ b/providers/aws/route53/route53_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/dns/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
func newTestMock() *Mock {
diff --git a/providers/aws/s3/s3.go b/providers/aws/s3/s3.go
index 0d0b398..3fca634 100644
--- a/providers/aws/s3/s3.go
+++ b/providers/aws/s3/s3.go
@@ -9,13 +9,13 @@ import (
"strings"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/pagination"
- "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/internal/pagination"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
const (
@@ -540,8 +540,14 @@ func (m *Mock) CompleteMultipartUpload(_ context.Context, bucket, key, uploadID
}
func assemblePartsInOrder(allParts map[int][]byte, parts []driver.UploadPart) []byte {
+ // S3 assembles parts by ascending PartNumber regardless of the order the
+ // client lists them in CompleteMultipartUpload; sort so an out-of-order
+ // (or unsorted-SDK) Complete doesn't corrupt the object.
+ ordered := append([]driver.UploadPart(nil), parts...)
+ sort.Slice(ordered, func(i, j int) bool { return ordered[i].PartNumber < ordered[j].PartNumber })
+
var data []byte
- for _, p := range parts {
+ for _, p := range ordered {
data = append(data, allParts[p.PartNumber]...)
}
diff --git a/providers/aws/s3/s3_test.go b/providers/aws/s3/s3_test.go
index 9f58dab..2023da5 100644
--- a/providers/aws/s3/s3_test.go
+++ b/providers/aws/s3/s3_test.go
@@ -7,10 +7,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/providers/aws/cloudwatch"
- "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/aws/cloudwatch"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
func newTestMock() *Mock {
@@ -692,7 +692,7 @@ func TestCompleteMultipartUploadPartsValidation(t *testing.T) {
assertEqual(t, "AAAA", string(obj.Data))
})
- t.Run("reversed order yields part2+part1 data", func(t *testing.T) {
+ t.Run("parts listed out of order still assemble by PartNumber", func(t *testing.T) {
mp, err := m.CreateMultipartUpload(ctx, "bkt", "file2.bin", "application/octet-stream")
requireNoError(t, err)
@@ -701,12 +701,14 @@ func TestCompleteMultipartUploadPartsValidation(t *testing.T) {
part2, err := m.UploadPart(ctx, "bkt", "file2.bin", mp.UploadID, 2, []byte("BBBB"))
requireNoError(t, err)
+ // Parts passed to Complete in reverse order; S3 assembles by ascending
+ // PartNumber, so the object is part1||part2 regardless of list order.
err = m.CompleteMultipartUpload(ctx, "bkt", "file2.bin", mp.UploadID, []driver.UploadPart{*part2, *part1})
requireNoError(t, err)
obj, err := m.GetObject(ctx, "bkt", "file2.bin")
requireNoError(t, err)
- assertEqual(t, "BBBBAAAA", string(obj.Data))
+ assertEqual(t, "AAAABBBB", string(obj.Data))
})
t.Run("non-existent part 99 returns error", func(t *testing.T) {
diff --git a/providers/aws/sagemaker/cluster.go b/providers/aws/sagemaker/cluster.go
index a787bb3..b6cd024 100644
--- a/providers/aws/sagemaker/cluster.go
+++ b/providers/aws/sagemaker/cluster.go
@@ -4,8 +4,8 @@ import (
"context"
"strconv"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// maxClusterInstancesPerGroup bounds the per-group instance count so an
diff --git a/providers/aws/sagemaker/featurestore.go b/providers/aws/sagemaker/featurestore.go
index 554e99a..f677600 100644
--- a/providers/aws/sagemaker/featurestore.go
+++ b/providers/aws/sagemaker/featurestore.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// recordKey joins a feature group name and a record identifier.
diff --git a/providers/aws/sagemaker/inference.go b/providers/aws/sagemaker/inference.go
index d2c2497..8039ad8 100644
--- a/providers/aws/sagemaker/inference.go
+++ b/providers/aws/sagemaker/inference.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// --- Model ---
diff --git a/providers/aws/sagemaker/jobs.go b/providers/aws/sagemaker/jobs.go
index 0fd59c2..d5a5b33 100644
--- a/providers/aws/sagemaker/jobs.go
+++ b/providers/aws/sagemaker/jobs.go
@@ -3,9 +3,9 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// --- Training jobs ---
diff --git a/providers/aws/sagemaker/metrics.go b/providers/aws/sagemaker/metrics.go
index d6949b2..a8e9689 100644
--- a/providers/aws/sagemaker/metrics.go
+++ b/providers/aws/sagemaker/metrics.go
@@ -3,7 +3,7 @@ package sagemaker
import (
"context"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// nominalLatencyMs is the synthetic per-invocation ModelLatency the emulator
diff --git a/providers/aws/sagemaker/notebook.go b/providers/aws/sagemaker/notebook.go
index 71b0182..80f57fa 100644
--- a/providers/aws/sagemaker/notebook.go
+++ b/providers/aws/sagemaker/notebook.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// --- Notebook instance ---
diff --git a/providers/aws/sagemaker/pipeline.go b/providers/aws/sagemaker/pipeline.go
index 7d7f424..85017d7 100644
--- a/providers/aws/sagemaker/pipeline.go
+++ b/providers/aws/sagemaker/pipeline.go
@@ -3,9 +3,9 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// --- Pipeline ---
diff --git a/providers/aws/sagemaker/registry.go b/providers/aws/sagemaker/registry.go
index 2289cb8..dfaeb7b 100644
--- a/providers/aws/sagemaker/registry.go
+++ b/providers/aws/sagemaker/registry.go
@@ -4,8 +4,8 @@ import (
"context"
"strconv"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// --- Model package group ---
diff --git a/providers/aws/sagemaker/runtime.go b/providers/aws/sagemaker/runtime.go
index 273270f..609ad29 100644
--- a/providers/aws/sagemaker/runtime.go
+++ b/providers/aws/sagemaker/runtime.go
@@ -3,9 +3,9 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// InvokeEndpoint performs an emulated synchronous inference call. The endpoint
diff --git a/providers/aws/sagemaker/sagemaker.go b/providers/aws/sagemaker/sagemaker.go
index 9deb8da..afffa82 100644
--- a/providers/aws/sagemaker/sagemaker.go
+++ b/providers/aws/sagemaker/sagemaker.go
@@ -14,11 +14,11 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// Compile-time checks that Mock implements the driver interfaces.
diff --git a/providers/aws/sagemaker/sagemaker_test.go b/providers/aws/sagemaker/sagemaker_test.go
index 45d277d..f413bdc 100644
--- a/providers/aws/sagemaker/sagemaker_test.go
+++ b/providers/aws/sagemaker/sagemaker_test.go
@@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu/config"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/providers/aws/cloudwatch"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/aws/cloudwatch"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
func newTestMock() *Mock {
diff --git a/providers/aws/sagemaker/studio.go b/providers/aws/sagemaker/studio.go
index a51cc85..7a46c8c 100644
--- a/providers/aws/sagemaker/studio.go
+++ b/providers/aws/sagemaker/studio.go
@@ -3,9 +3,9 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// scopedKey joins a domain ID and a child name into a single store key.
diff --git a/providers/aws/sagemaker/tags.go b/providers/aws/sagemaker/tags.go
index bd65dfd..1e55f56 100644
--- a/providers/aws/sagemaker/tags.go
+++ b/providers/aws/sagemaker/tags.go
@@ -3,7 +3,7 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// setTags records the initial tags for a resource ARN (no-op for empty input).
diff --git a/providers/aws/secretsmanager/secretsmanager.go b/providers/aws/secretsmanager/secretsmanager.go
index 0b57664..4f3faaf 100644
--- a/providers/aws/secretsmanager/secretsmanager.go
+++ b/providers/aws/secretsmanager/secretsmanager.go
@@ -6,11 +6,11 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/secrets/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
// Compile-time check that Mock implements driver.Secrets.
diff --git a/providers/aws/secretsmanager/secretsmanager_test.go b/providers/aws/secretsmanager/secretsmanager_test.go
index 1f5a780..3ba7f13 100644
--- a/providers/aws/secretsmanager/secretsmanager_test.go
+++ b/providers/aws/secretsmanager/secretsmanager_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/secrets/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/secrets/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/aws/sns/sns.go b/providers/aws/sns/sns.go
index 6096a6a..eaaf9f7 100644
--- a/providers/aws/sns/sns.go
+++ b/providers/aws/sns/sns.go
@@ -5,12 +5,12 @@ import (
"context"
"sync"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/notification/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
// Compile-time check that Mock implements driver.Notification.
diff --git a/providers/aws/sns/sns_test.go b/providers/aws/sns/sns_test.go
index 960163f..4711d0b 100644
--- a/providers/aws/sns/sns_test.go
+++ b/providers/aws/sns/sns_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/notification/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/notification/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/aws/sqs/sqs.go b/providers/aws/sqs/sqs.go
index 6b8357b..4fb7b92 100644
--- a/providers/aws/sqs/sqs.go
+++ b/providers/aws/sqs/sqs.go
@@ -8,12 +8,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/messagequeue/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Compile-time check that Mock implements driver.MessageQueue.
diff --git a/providers/aws/sqs/sqs_test.go b/providers/aws/sqs/sqs_test.go
index bef5975..b526aab 100644
--- a/providers/aws/sqs/sqs_test.go
+++ b/providers/aws/sqs/sqs_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/messagequeue/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/providers/aws/cloudwatch"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/aws/cloudwatch"
+ "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
func newTestMock() (*Mock, *config.FakeClock) {
diff --git a/providers/aws/ssm/ssm.go b/providers/aws/ssm/ssm.go
new file mode 100644
index 0000000..287ba02
--- /dev/null
+++ b/providers/aws/ssm/ssm.go
@@ -0,0 +1,506 @@
+// Package ssm provides an in-memory mock implementation of AWS Systems Manager
+// (SSM) Parameter Store.
+//
+// This is the Layer-3 driver implementation. The portable Layer-1 wrapper that
+// adds recording/metrics/rate-limiting/error-injection/latency lives in the
+// module-root parameterstore package (parameterstore/parameterstore.go).
+//
+// Values are stored verbatim regardless of Type; SecureString parameters are
+// NOT encrypted (there is no real KMS integration), so WithDecryption is a
+// no-op and the raw value is always returned.
+package ssm
+
+import (
+ "context"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/parameterstore/driver"
+)
+
+// Compile-time check that Mock implements driver.ParameterStore.
+var _ driver.ParameterStore = (*Mock)(nil)
+
+// version is a single stored revision of a parameter.
+type version struct {
+ value string
+ typ string
+ dataType string
+ version int64
+ lastModified string
+ labels []string
+}
+
+// paramData holds all versions and current metadata for a parameter name.
+type paramData struct {
+ name string
+ description string
+ tier string
+ versions []*version
+ latest int64
+ mu sync.RWMutex
+}
+
+// Mock is an in-memory mock implementation of SSM Parameter Store.
+type Mock struct {
+ params *memstore.Store[*paramData]
+ opts *config.Options
+}
+
+// New creates a new SSM Parameter Store mock.
+func New(opts *config.Options) *Mock {
+ return &Mock{
+ params: memstore.New[*paramData](),
+ opts: opts,
+ }
+}
+
+func (m *Mock) now() string {
+ return m.opts.Clock.Now().UTC().Format(time.RFC3339)
+}
+
+// arn builds the ARN for a parameter. AWS uses "parameter" + the name (which
+// begins with "/" for hierarchical names), joined without a separating slash.
+func (m *Mock) arn(name string) string {
+ return idgen.AWSARN("ssm", m.opts.Region, m.opts.AccountID, "parameter"+ensureLeadingSlash(name))
+}
+
+// ensureLeadingSlash normalizes a name so hierarchical ARNs render like
+// "parameter/a/b" while flat names render like "parameter/flat".
+func ensureLeadingSlash(name string) string {
+ if strings.HasPrefix(name, "/") {
+ return name
+ }
+
+ return "/" + name
+}
+
+func defaultType(t string) string {
+ switch t {
+ case driver.TypeString, driver.TypeStringList, driver.TypeSecureString:
+ return t
+ default:
+ return driver.TypeString
+ }
+}
+
+// PutParameter creates a new parameter or, when Overwrite is set, appends a new
+// version to an existing one.
+func (m *Mock) PutParameter(ctx context.Context, cfg driver.PutConfig) (int64, string, error) {
+ if cfg.Name == "" {
+ return 0, "", errors.New(errors.InvalidArgument, "parameter name is required")
+ }
+
+ tier := cfg.Tier
+ if tier == "" {
+ tier = "Standard"
+ }
+
+ dataType := cfg.DataType
+ if dataType == "" {
+ dataType = "text"
+ }
+
+ now := m.now()
+
+ if existing, ok := m.params.Get(cfg.Name); ok {
+ existing.mu.Lock()
+ defer existing.mu.Unlock()
+
+ if !cfg.Overwrite {
+ return 0, "", errors.Newf(errors.AlreadyExists,
+ "parameter %q already exists; set Overwrite to update it", cfg.Name)
+ }
+
+ next := existing.latest + 1
+ existing.versions = append(existing.versions, &version{
+ value: cfg.Value,
+ typ: defaultType(cfg.Type),
+ dataType: dataType,
+ version: next,
+ lastModified: now,
+ })
+ existing.latest = next
+ existing.description = cfg.Description
+ existing.tier = tier
+
+ return next, tier, nil
+ }
+
+ pd := ¶mData{
+ name: cfg.Name,
+ description: cfg.Description,
+ tier: tier,
+ latest: 1,
+ versions: []*version{{
+ value: cfg.Value,
+ typ: defaultType(cfg.Type),
+ dataType: dataType,
+ version: 1,
+ lastModified: now,
+ }},
+ }
+
+ // SetIfAbsent guards against a concurrent create racing between Get and Set.
+ if !m.params.SetIfAbsent(cfg.Name, pd) {
+ // Lost the race: retry as an overwrite path only if allowed.
+ if !cfg.Overwrite {
+ return 0, "", errors.Newf(errors.AlreadyExists,
+ "parameter %q already exists; set Overwrite to update it", cfg.Name)
+ }
+
+ cfg.Overwrite = true
+
+ return m.PutParameter(ctx, cfg)
+ }
+
+ return 1, tier, nil
+}
+
+// resolveSelector splits a name of the form "name:selector" into its base name
+// and selector (a version number or a label). An empty selector means latest.
+func resolveSelector(name string) (base, selector string) {
+ // Hierarchical names contain slashes but never a colon in the path itself,
+ // so the last colon (if any) introduces a version/label selector.
+ if i := strings.LastIndex(name, ":"); i >= 0 {
+ return name[:i], name[i+1:]
+ }
+
+ return name, ""
+}
+
+// pick returns the version matching the selector (empty = latest, numeric =
+// version, otherwise a label). Callers must hold pd.mu.
+func (pd *paramData) pick(selector string) (*version, bool) {
+ if selector == "" {
+ return pd.versionByNumber(pd.latest)
+ }
+
+ if n, err := strconv.ParseInt(selector, 10, 64); err == nil {
+ return pd.versionByNumber(n)
+ }
+
+ for _, v := range pd.versions {
+ for _, l := range v.labels {
+ if l == selector {
+ return v, true
+ }
+ }
+ }
+
+ return nil, false
+}
+
+func (pd *paramData) versionByNumber(n int64) (*version, bool) {
+ for _, v := range pd.versions {
+ if v.version == n {
+ return v, true
+ }
+ }
+
+ return nil, false
+}
+
+func (m *Mock) toParameter(pd *paramData, v *version, selector string) driver.Parameter {
+ name := pd.name
+ if selector != "" {
+ name = pd.name + ":" + selector
+ }
+
+ return driver.Parameter{
+ Name: pd.name,
+ Type: v.typ,
+ Value: v.value,
+ Version: v.version,
+ ARN: m.arn(pd.name),
+ DataType: v.dataType,
+ LastModified: v.lastModified,
+ Selector: selectorFor(name, pd.name),
+ }
+}
+
+func selectorFor(requested, base string) string {
+ if requested == base {
+ return ""
+ }
+
+ return strings.TrimPrefix(requested, base+":")
+}
+
+// GetParameter retrieves a single parameter by name, honoring an optional
+// ":version" or ":label" selector suffix. withDecryption is accepted but has no
+// effect: SecureString values are stored and returned in the clear.
+func (m *Mock) GetParameter(_ context.Context, name string, _ bool) (*driver.Parameter, error) {
+ base, selector := resolveSelector(name)
+
+ pd, ok := m.params.Get(base)
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "parameter %q not found", base)
+ }
+
+ pd.mu.RLock()
+ defer pd.mu.RUnlock()
+
+ v, ok := pd.pick(selector)
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "parameter %q version/label %q not found", base, selector)
+ }
+
+ p := m.toParameter(pd, v, selector)
+
+ return &p, nil
+}
+
+// GetParameters retrieves multiple parameters, reporting names that were not
+// found (or whose selector did not resolve) as invalid rather than erroring.
+func (m *Mock) GetParameters(_ context.Context, names []string, _ bool) ([]driver.Parameter, []string, error) {
+ found := make([]driver.Parameter, 0, len(names))
+
+ var invalid []string
+
+ for _, name := range names {
+ base, selector := resolveSelector(name)
+
+ pd, ok := m.params.Get(base)
+ if !ok {
+ invalid = append(invalid, name)
+ continue
+ }
+
+ pd.mu.RLock()
+ v, ok := pd.pick(selector)
+ if !ok {
+ pd.mu.RUnlock()
+
+ invalid = append(invalid, name)
+
+ continue
+ }
+
+ found = append(found, m.toParameter(pd, v, selector))
+ pd.mu.RUnlock()
+ }
+
+ return found, invalid, nil
+}
+
+// GetParametersByPath returns the latest version of every parameter under a
+// hierarchical path. With Recursive false, only direct children are returned;
+// with Recursive true, the whole subtree is returned.
+func (m *Mock) GetParametersByPath(_ context.Context, in driver.GetByPathInput) ([]driver.Parameter, error) {
+ path := in.Path
+ if path == "" {
+ path = "/"
+ }
+
+ if !strings.HasPrefix(path, "/") {
+ return nil, errors.New(errors.InvalidArgument, "path must begin with '/'")
+ }
+
+ prefix := path
+ if !strings.HasSuffix(prefix, "/") {
+ prefix += "/"
+ }
+
+ var out []driver.Parameter
+
+ for _, pd := range m.params.All() {
+ name := pd.name
+ if !strings.HasPrefix(name, prefix) {
+ continue
+ }
+
+ rest := strings.TrimPrefix(name, prefix)
+ // Non-recursive: only direct children (no further "/" in the remainder).
+ if !in.Recursive && strings.Contains(rest, "/") {
+ continue
+ }
+
+ pd.mu.RLock()
+ if v, ok := pd.versionByNumber(pd.latest); ok {
+ out = append(out, m.toParameter(pd, v, ""))
+ }
+ pd.mu.RUnlock()
+ }
+
+ sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
+
+ return out, nil
+}
+
+// DeleteParameter removes a parameter and all its versions.
+func (m *Mock) DeleteParameter(_ context.Context, name string) error {
+ if !m.params.Delete(name) {
+ return errors.Newf(errors.NotFound, "parameter %q not found", name)
+ }
+
+ return nil
+}
+
+// DeleteParameters removes multiple parameters, returning the names deleted and
+// the names that did not exist.
+func (m *Mock) DeleteParameters(_ context.Context, names []string) ([]string, []string, error) {
+ var deleted, invalid []string
+
+ for _, name := range names {
+ if m.params.Delete(name) {
+ deleted = append(deleted, name)
+ } else {
+ invalid = append(invalid, name)
+ }
+ }
+
+ return deleted, invalid, nil
+}
+
+// DescribeParameters lists metadata (no values) for all parameters.
+func (m *Mock) DescribeParameters(_ context.Context) ([]driver.ParameterMetadata, error) {
+ all := m.params.All()
+
+ out := make([]driver.ParameterMetadata, 0, len(all))
+
+ for _, pd := range all {
+ pd.mu.RLock()
+ if v, ok := pd.versionByNumber(pd.latest); ok {
+ out = append(out, driver.ParameterMetadata{
+ Name: pd.name,
+ Type: v.typ,
+ Description: pd.description,
+ Version: pd.latest,
+ ARN: m.arn(pd.name),
+ Tier: pd.tier,
+ DataType: v.dataType,
+ LastModified: v.lastModified,
+ LastModifiedUser: idgen.AWSARN("iam", "", m.opts.AccountID, "user/cloudemu"),
+ })
+ }
+ pd.mu.RUnlock()
+ }
+
+ sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
+
+ return out, nil
+}
+
+// GetParameterHistory returns every version of a parameter, oldest first.
+func (m *Mock) GetParameterHistory(_ context.Context, name string) ([]driver.Parameter, error) {
+ pd, ok := m.params.Get(name)
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "parameter %q not found", name)
+ }
+
+ pd.mu.RLock()
+ defer pd.mu.RUnlock()
+
+ out := make([]driver.Parameter, 0, len(pd.versions))
+ for _, v := range pd.versions {
+ out = append(out, driver.Parameter{
+ Name: pd.name,
+ Type: v.typ,
+ Value: v.value,
+ Version: v.version,
+ ARN: m.arn(pd.name),
+ DataType: v.dataType,
+ LastModified: v.lastModified,
+ })
+ }
+
+ return out, nil
+}
+
+// LabelParameterVersion attaches labels to a specific version (0 = latest),
+// returning the version the labels were applied to and any labels rejected.
+// A label attached to a new version is removed from any older version that held
+// it, matching real SSM semantics.
+func (m *Mock) LabelParameterVersion(_ context.Context, name string, ver int64, labels []string) (int64, []string, error) {
+ pd, ok := m.params.Get(name)
+ if !ok {
+ return 0, nil, errors.Newf(errors.NotFound, "parameter %q not found", name)
+ }
+
+ pd.mu.Lock()
+ defer pd.mu.Unlock()
+
+ if ver == 0 {
+ ver = pd.latest
+ }
+
+ target, ok := pd.versionByNumber(ver)
+ if !ok {
+ return 0, nil, errors.Newf(errors.NotFound, "parameter %q version %d not found", name, ver)
+ }
+
+ var invalid []string
+
+ for _, label := range labels {
+ // Real SSM rejects labels that begin with a digit or with "aws"/"ssm"
+ // (case-insensitive). Rejecting them here also prevents a numeric label
+ // like "5" from being attached and then shadowed by version-number
+ // resolution in pick().
+ if !validLabel(label) {
+ invalid = append(invalid, label)
+ continue
+ }
+
+ // Detach the label from any other version first.
+ for _, v := range pd.versions {
+ if v == target {
+ continue
+ }
+
+ v.labels = removeString(v.labels, label)
+ }
+
+ if !containsString(target.labels, label) {
+ target.labels = append(target.labels, label)
+ }
+ }
+
+ return ver, invalid, nil
+}
+
+// validLabel reports whether a parameter label is acceptable to real SSM: it
+// must be non-empty, must not start with a digit, and must not start with
+// "aws" or "ssm" (case-insensitive).
+func validLabel(label string) bool {
+ if label == "" {
+ return false
+ }
+
+ if label[0] >= '0' && label[0] <= '9' {
+ return false
+ }
+
+ lower := strings.ToLower(label)
+
+ return !strings.HasPrefix(lower, "aws") && !strings.HasPrefix(lower, "ssm")
+}
+
+func containsString(ss []string, s string) bool {
+ for _, x := range ss {
+ if x == s {
+ return true
+ }
+ }
+
+ return false
+}
+
+func removeString(ss []string, s string) []string {
+ out := ss[:0]
+
+ for _, x := range ss {
+ if x != s {
+ out = append(out, x)
+ }
+ }
+
+ return out
+}
diff --git a/providers/aws/ssm/ssm_test.go b/providers/aws/ssm/ssm_test.go
new file mode 100644
index 0000000..bd1c27f
--- /dev/null
+++ b/providers/aws/ssm/ssm_test.go
@@ -0,0 +1,146 @@
+package ssm_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/providers/aws/ssm"
+ "github.com/stackshy/cloudemu/v2/services/parameterstore/driver"
+)
+
+func newMock() *ssm.Mock {
+ return ssm.New(config.NewOptions())
+}
+
+func TestPutOverwriteAndHistory(t *testing.T) {
+ m := newMock()
+ ctx := context.Background()
+
+ v, _, err := m.PutParameter(ctx, driver.PutConfig{Name: "/a", Value: "1", Type: driver.TypeString})
+ if err != nil {
+ t.Fatalf("PutParameter v1: %v", err)
+ }
+
+ if v != 1 {
+ t.Fatalf("first version = %d, want 1", v)
+ }
+
+ if _, _, err := m.PutParameter(ctx, driver.PutConfig{Name: "/a", Value: "2", Type: driver.TypeString}); err == nil {
+ t.Fatal("PutParameter without Overwrite: want AlreadyExists, got nil")
+ } else if !cerrors.IsAlreadyExists(err) {
+ t.Fatalf("want AlreadyExists, got %v", err)
+ }
+
+ v2, _, err := m.PutParameter(ctx, driver.PutConfig{Name: "/a", Value: "2", Type: driver.TypeString, Overwrite: true})
+ if err != nil {
+ t.Fatalf("PutParameter overwrite: %v", err)
+ }
+
+ if v2 != 2 {
+ t.Fatalf("overwrite version = %d, want 2", v2)
+ }
+
+ hist, err := m.GetParameterHistory(ctx, "/a")
+ if err != nil {
+ t.Fatalf("GetParameterHistory: %v", err)
+ }
+
+ if len(hist) != 2 || hist[0].Value != "1" || hist[1].Value != "2" {
+ t.Fatalf("history = %+v, want [1 2]", hist)
+ }
+}
+
+func TestLabelParameterVersionAndSelector(t *testing.T) {
+ m := newMock()
+ ctx := context.Background()
+
+ if _, _, err := m.PutParameter(ctx, driver.PutConfig{Name: "/svc/cfg", Value: "old", Type: driver.TypeString}); err != nil {
+ t.Fatalf("Put v1: %v", err)
+ }
+
+ if _, _, err := m.PutParameter(ctx, driver.PutConfig{Name: "/svc/cfg", Value: "new", Type: driver.TypeString, Overwrite: true}); err != nil {
+ t.Fatalf("Put v2: %v", err)
+ }
+
+ // Label v1 "prod".
+ applied, invalid, err := m.LabelParameterVersion(ctx, "/svc/cfg", 1, []string{"prod"})
+ if err != nil {
+ t.Fatalf("LabelParameterVersion: %v", err)
+ }
+
+ if applied != 1 || len(invalid) != 0 {
+ t.Fatalf("applied=%d invalid=%v, want 1 []", applied, invalid)
+ }
+
+ got, err := m.GetParameter(ctx, "/svc/cfg:prod", false)
+ if err != nil {
+ t.Fatalf("GetParameter(:prod): %v", err)
+ }
+
+ if got.Value != "old" || got.Version != 1 {
+ t.Fatalf("label selector got %q v%d, want old v1", got.Value, got.Version)
+ }
+
+ // Latest (no selector) is still v2.
+ latest, err := m.GetParameter(ctx, "/svc/cfg", false)
+ if err != nil {
+ t.Fatalf("GetParameter(latest): %v", err)
+ }
+
+ if latest.Value != "new" || latest.Version != 2 {
+ t.Fatalf("latest got %q v%d, want new v2", latest.Value, latest.Version)
+ }
+}
+
+func TestGetParametersByPathRecursive(t *testing.T) {
+ m := newMock()
+ ctx := context.Background()
+
+ for _, n := range []string{"/p/a", "/p/b", "/p/sub/c"} {
+ if _, _, err := m.PutParameter(ctx, driver.PutConfig{Name: n, Value: "x", Type: driver.TypeString}); err != nil {
+ t.Fatalf("Put %s: %v", n, err)
+ }
+ }
+
+ shallow, err := m.GetParametersByPath(ctx, driver.GetByPathInput{Path: "/p"})
+ if err != nil {
+ t.Fatalf("GetParametersByPath: %v", err)
+ }
+
+ if len(shallow) != 2 {
+ t.Fatalf("non-recursive returned %d, want 2", len(shallow))
+ }
+
+ deep, err := m.GetParametersByPath(ctx, driver.GetByPathInput{Path: "/p", Recursive: true})
+ if err != nil {
+ t.Fatalf("GetParametersByPath recursive: %v", err)
+ }
+
+ if len(deep) != 3 {
+ t.Fatalf("recursive returned %d, want 3", len(deep))
+ }
+}
+
+func TestDeleteParameters(t *testing.T) {
+ m := newMock()
+ ctx := context.Background()
+
+ if _, _, err := m.PutParameter(ctx, driver.PutConfig{Name: "/d/1", Value: "x", Type: driver.TypeString}); err != nil {
+ t.Fatalf("Put: %v", err)
+ }
+
+ deleted, invalid, err := m.DeleteParameters(ctx, []string{"/d/1", "/d/missing"})
+ if err != nil {
+ t.Fatalf("DeleteParameters: %v", err)
+ }
+
+ if len(deleted) != 1 || deleted[0] != "/d/1" {
+ t.Fatalf("deleted = %v, want [/d/1]", deleted)
+ }
+
+ if len(invalid) != 1 || invalid[0] != "/d/missing" {
+ t.Fatalf("invalid = %v, want [/d/missing]", invalid)
+ }
+}
diff --git a/providers/aws/vpc/acl.go b/providers/aws/vpc/acl.go
index e6e925d..e3438b1 100644
--- a/providers/aws/vpc/acl.go
+++ b/providers/aws/vpc/acl.go
@@ -4,9 +4,9 @@ import (
"context"
"sort"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Default ACL rule constants.
diff --git a/providers/aws/vpc/eip.go b/providers/aws/vpc/eip.go
index 55d2238..bacbfc5 100644
--- a/providers/aws/vpc/eip.go
+++ b/providers/aws/vpc/eip.go
@@ -3,9 +3,9 @@ package vpc
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type eipData struct {
diff --git a/providers/aws/vpc/endpoint.go b/providers/aws/vpc/endpoint.go
index 31c9762..b5137d0 100644
--- a/providers/aws/vpc/endpoint.go
+++ b/providers/aws/vpc/endpoint.go
@@ -3,9 +3,9 @@ package vpc
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// copyStringSlice creates a shallow copy of a string slice.
diff --git a/providers/aws/vpc/flowlog.go b/providers/aws/vpc/flowlog.go
index ddee19b..960fffb 100644
--- a/providers/aws/vpc/flowlog.go
+++ b/providers/aws/vpc/flowlog.go
@@ -4,9 +4,9 @@ import (
"context"
"fmt"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Flow log status constants.
diff --git a/providers/aws/vpc/igw.go b/providers/aws/vpc/igw.go
index 35cc97e..9952272 100644
--- a/providers/aws/vpc/igw.go
+++ b/providers/aws/vpc/igw.go
@@ -3,9 +3,9 @@ package vpc
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Internet gateway state constants.
diff --git a/providers/aws/vpc/natgw.go b/providers/aws/vpc/natgw.go
index b87378b..da2ba80 100644
--- a/providers/aws/vpc/natgw.go
+++ b/providers/aws/vpc/natgw.go
@@ -3,9 +3,9 @@ package vpc
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// NAT gateway state constants.
diff --git a/providers/aws/vpc/peering.go b/providers/aws/vpc/peering.go
index 2eb1900..5409359 100644
--- a/providers/aws/vpc/peering.go
+++ b/providers/aws/vpc/peering.go
@@ -5,9 +5,9 @@ import (
"fmt"
"net"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Peering status constants.
diff --git a/providers/aws/vpc/routetable.go b/providers/aws/vpc/routetable.go
index f03a2e7..012635e 100644
--- a/providers/aws/vpc/routetable.go
+++ b/providers/aws/vpc/routetable.go
@@ -3,9 +3,9 @@ package vpc
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Route target type constants.
diff --git a/providers/aws/vpc/rtassoc.go b/providers/aws/vpc/rtassoc.go
index d225eaa..a716c2c 100644
--- a/providers/aws/vpc/rtassoc.go
+++ b/providers/aws/vpc/rtassoc.go
@@ -3,9 +3,9 @@ package vpc
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type rtAssocData struct {
diff --git a/providers/aws/vpc/vpc.go b/providers/aws/vpc/vpc.go
index 2b49bc3..b6b1fd2 100644
--- a/providers/aws/vpc/vpc.go
+++ b/providers/aws/vpc/vpc.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Time format and mock constants.
diff --git a/providers/aws/vpc/vpc_test.go b/providers/aws/vpc/vpc_test.go
index 65b46a6..9616a35 100644
--- a/providers/aws/vpc/vpc_test.go
+++ b/providers/aws/vpc/vpc_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
func newTestMock() *Mock {
@@ -104,8 +104,8 @@ func TestCreateSubnet(t *testing.T) {
{name: "empty VPC ID", cfg: driver.SubnetConfig{CIDRBlock: "10.0.1.0/24"}, expectErr: true},
{name: "empty CIDR", cfg: driver.SubnetConfig{VPCID: v.ID}, expectErr: true},
{
- name: "VPC not found",
- cfg: driver.SubnetConfig{VPCID: "vpc-nope", CIDRBlock: "10.0.1.0/24"},
+ name: "VPC not found",
+ cfg: driver.SubnetConfig{VPCID: "vpc-nope", CIDRBlock: "10.0.1.0/24"},
expectErr: true,
},
}
diff --git a/providers/azure/acr/acr.go b/providers/azure/acr/acr.go
index 689edc0..6c664a8 100644
--- a/providers/azure/acr/acr.go
+++ b/providers/azure/acr/acr.go
@@ -10,12 +10,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/containerregistry/driver"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/providers/azure/acr/acr_test.go b/providers/azure/acr/acr_test.go
index 9de240b..55c4e4d 100644
--- a/providers/azure/acr/acr_test.go
+++ b/providers/azure/acr/acr_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/containerregistry/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/azure/aks/aks.go b/providers/azure/aks/aks.go
index 4571486..93b7496 100644
--- a/providers/azure/aks/aks.go
+++ b/providers/azure/aks/aks.go
@@ -14,12 +14,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/kubernetes"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/kubernetes"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/providers/azure/aks/aks_k8s_test.go b/providers/azure/aks/aks_k8s_test.go
index ae25c03..f743e3a 100644
--- a/providers/azure/aks/aks_k8s_test.go
+++ b/providers/azure/aks/aks_k8s_test.go
@@ -9,7 +9,7 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu/kubernetes"
+ "github.com/stackshy/cloudemu/v2/services/kubernetes"
)
func TestSetK8sAPI_CreateRegistersWithAPIServer(t *testing.T) {
diff --git a/providers/azure/aks/aks_test.go b/providers/azure/aks/aks_test.go
index 01ad217..a8ea580 100644
--- a/providers/azure/aks/aks_test.go
+++ b/providers/azure/aks/aks_test.go
@@ -6,7 +6,7 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2/config"
)
func newTestMock() *Mock {
diff --git a/providers/azure/azure.go b/providers/azure/azure.go
index d9d43b9..8e2dbad 100644
--- a/providers/azure/azure.go
+++ b/providers/azure/azure.go
@@ -2,43 +2,52 @@
package azure
import (
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/providers/azure/acr"
- "github.com/stackshy/cloudemu/providers/azure/aks"
- "github.com/stackshy/cloudemu/providers/azure/azurecache"
- "github.com/stackshy/cloudemu/providers/azure/azuredns"
- "github.com/stackshy/cloudemu/providers/azure/azureiam"
- "github.com/stackshy/cloudemu/providers/azure/azurelb"
- "github.com/stackshy/cloudemu/providers/azure/azuremonitor"
- "github.com/stackshy/cloudemu/providers/azure/azuresql"
- "github.com/stackshy/cloudemu/providers/azure/blobstorage"
- "github.com/stackshy/cloudemu/providers/azure/cosmosdb"
- "github.com/stackshy/cloudemu/providers/azure/databricks"
- "github.com/stackshy/cloudemu/providers/azure/eventgrid"
- "github.com/stackshy/cloudemu/providers/azure/functions"
- "github.com/stackshy/cloudemu/providers/azure/keyvault"
- "github.com/stackshy/cloudemu/providers/azure/loganalytics"
- "github.com/stackshy/cloudemu/providers/azure/mysqlflex"
- "github.com/stackshy/cloudemu/providers/azure/notificationhubs"
- "github.com/stackshy/cloudemu/providers/azure/postgresflex"
- "github.com/stackshy/cloudemu/providers/azure/servicebus"
- "github.com/stackshy/cloudemu/providers/azure/virtualmachines"
- "github.com/stackshy/cloudemu/providers/azure/vnet"
- "github.com/stackshy/cloudemu/resourcediscovery"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/azure/acr"
+ "github.com/stackshy/cloudemu/v2/providers/azure/aks"
+ "github.com/stackshy/cloudemu/v2/providers/azure/azureai"
+ "github.com/stackshy/cloudemu/v2/providers/azure/azurecache"
+ "github.com/stackshy/cloudemu/v2/providers/azure/azuredns"
+ "github.com/stackshy/cloudemu/v2/providers/azure/azureiam"
+ "github.com/stackshy/cloudemu/v2/providers/azure/azurelb"
+ "github.com/stackshy/cloudemu/v2/providers/azure/azuremonitor"
+ "github.com/stackshy/cloudemu/v2/providers/azure/azuresearch"
+ "github.com/stackshy/cloudemu/v2/providers/azure/azuresql"
+ "github.com/stackshy/cloudemu/v2/providers/azure/blobstorage"
+ "github.com/stackshy/cloudemu/v2/providers/azure/cosmosdb"
+ "github.com/stackshy/cloudemu/v2/providers/azure/databricks"
+ "github.com/stackshy/cloudemu/v2/providers/azure/eventgrid"
+ "github.com/stackshy/cloudemu/v2/providers/azure/functions"
+ "github.com/stackshy/cloudemu/v2/providers/azure/keyvault"
+ "github.com/stackshy/cloudemu/v2/providers/azure/loganalytics"
+ "github.com/stackshy/cloudemu/v2/providers/azure/mysqlflex"
+ "github.com/stackshy/cloudemu/v2/providers/azure/notificationhubs"
+ "github.com/stackshy/cloudemu/v2/providers/azure/postgresflex"
+ "github.com/stackshy/cloudemu/v2/providers/azure/servicebus"
+ "github.com/stackshy/cloudemu/v2/providers/azure/tablestorage"
+ "github.com/stackshy/cloudemu/v2/providers/azure/virtualmachines"
+ "github.com/stackshy/cloudemu/v2/providers/azure/vnet"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
)
// Provider holds all Azure mock services.
type Provider struct {
- BlobStorage *blobstorage.Mock
- VirtualMachines *virtualmachines.Mock
- CosmosDB *cosmosdb.Mock
- Functions *functions.Mock
- VNet *vnet.Mock
- Monitor *azuremonitor.Mock
- IAM *azureiam.Mock
- DNS *azuredns.Mock
- LB *azurelb.Mock
- ServiceBus *servicebus.Mock
+ BlobStorage *blobstorage.Mock
+ VirtualMachines *virtualmachines.Mock
+ CosmosDB *cosmosdb.Mock
+ Functions *functions.Mock
+ VNet *vnet.Mock
+ Monitor *azuremonitor.Mock
+ IAM *azureiam.Mock
+ DNS *azuredns.Mock
+ LB *azurelb.Mock
+ ServiceBus *servicebus.Mock
+ // QueueStorage backs the Azure Queue Storage data-plane handler. It reuses
+ // the messagequeue provider, but is a distinct instance from ServiceBus so
+ // the two services keep separate queue namespaces.
+ QueueStorage *servicebus.Mock
+ // TableStorage backs the Azure Table Storage data-plane handler.
+ TableStorage *tablestorage.Mock
Cache *azurecache.Mock
KeyVault *keyvault.Mock
LogAnalytics *loganalytics.Mock
@@ -50,6 +59,8 @@ type Provider struct {
MySQLFlex *mysqlflex.Mock
AKS *aks.Mock
Databricks *databricks.Mock
+ AzureAI *azureai.Mock
+ AzureSearch *azuresearch.Mock
ResourceDiscovery *resourcediscovery.Engine
}
@@ -68,6 +79,8 @@ func New(opts ...config.Option) *Provider {
DNS: azuredns.New(o),
LB: azurelb.New(o),
ServiceBus: servicebus.New(o),
+ QueueStorage: servicebus.New(o),
+ TableStorage: tablestorage.New(o),
Cache: azurecache.New(o),
KeyVault: keyvault.New(o),
LogAnalytics: loganalytics.New(o),
@@ -79,6 +92,8 @@ func New(opts ...config.Option) *Provider {
MySQLFlex: mysqlflex.New(o),
AKS: aks.New(o),
Databricks: databricks.New(o),
+ AzureAI: azureai.New(o),
+ AzureSearch: azuresearch.New(o),
}
p.VirtualMachines.SetMonitoring(p.Monitor)
p.BlobStorage.SetMonitoring(p.Monitor)
@@ -94,6 +109,8 @@ func New(opts ...config.Option) *Provider {
p.PostgresFlex.SetMonitoring(p.Monitor)
p.MySQLFlex.SetMonitoring(p.Monitor)
p.AKS.SetMonitoring(p.Monitor)
+ p.AzureAI.SetMonitoring(p.Monitor)
+ p.AzureSearch.SetMonitoring(p.Monitor)
p.ResourceDiscovery = resourcediscovery.New(
resourcediscovery.ProviderAzure, o.AccountID, o.Region,
diff --git a/providers/azure/azureai/azureai.go b/providers/azure/azureai/azureai.go
new file mode 100644
index 0000000..7012c3e
--- /dev/null
+++ b/providers/azure/azureai/azureai.go
@@ -0,0 +1,372 @@
+// Package azureai provides an in-memory mock implementation of Azure AI across
+// both ARM providers — Microsoft.CognitiveServices (AI Foundry / AI Studio /
+// the AI Services resource / Azure OpenAI) and Microsoft.MachineLearningServices
+// (Azure Machine Learning) — plus the Azure OpenAI inference, AI Foundry
+// Agents/Assistants, and AML scoring data planes.
+package azureai
+
+import (
+ "context"
+ "fmt"
+ "hash/fnv"
+ "strconv"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+)
+
+// Compile-time check that Mock implements the CognitiveServices surface.
+var _ driver.CognitiveServices = (*Mock)(nil)
+
+const (
+ csProvider = "Microsoft.CognitiveServices"
+ defaultCSSKU = "S0"
+ kindDefault = "Default"
+)
+
+// Mock is the in-memory Azure AI service.
+type Mock struct {
+ // CognitiveServices stores.
+ accounts *memstore.Store[*driver.Account]
+ accountKeys *memstore.Store[*driver.AccountKeys]
+ deployments *memstore.Store[*driver.Deployment]
+ projects *memstore.Store[*driver.Project]
+ raiPolicies *memstore.Store[*driver.RaiPolicy]
+ commitmentPlans *memstore.Store[*driver.CommitmentPlan]
+ privateEndpoints *memstore.Store[*driver.PrivateEndpointConnection]
+
+ // Data-plane stores (assistants API).
+ assistants *memstore.Store[*driver.Assistant]
+ threads *memstore.Store[*driver.Thread]
+ messages *memstore.Store[*driver.ThreadMessage]
+ runs *memstore.Store[*driver.Run]
+
+ // MachineLearningServices stores.
+ mlWorkspaces *memstore.Store[*driver.MLWorkspace]
+ computes *memstore.Store[*driver.Compute]
+ mlEndpoints *memstore.Store[*driver.Endpoint]
+ mlDeploys *memstore.Store[*driver.EndpointDeployment]
+ jobs *memstore.Store[*driver.Job]
+ assets *memstore.Store[*driver.Asset]
+ datastores *memstore.Store[*driver.Datastore]
+ connections *memstore.Store[*driver.Connection]
+ mlSchedules *memstore.Store[*driver.MLSchedule]
+ registries *memstore.Store[*driver.Registry]
+
+ seq atomic.Int64
+
+ opts *config.Options
+ monitoring mondriver.Monitoring
+}
+
+// New creates a new Azure AI mock with the given configuration options.
+func New(opts *config.Options) *Mock {
+ return &Mock{
+ accounts: memstore.New[*driver.Account](),
+ accountKeys: memstore.New[*driver.AccountKeys](),
+ deployments: memstore.New[*driver.Deployment](),
+ projects: memstore.New[*driver.Project](),
+ raiPolicies: memstore.New[*driver.RaiPolicy](),
+ commitmentPlans: memstore.New[*driver.CommitmentPlan](),
+ privateEndpoints: memstore.New[*driver.PrivateEndpointConnection](),
+ assistants: memstore.New[*driver.Assistant](),
+ threads: memstore.New[*driver.Thread](),
+ messages: memstore.New[*driver.ThreadMessage](),
+ runs: memstore.New[*driver.Run](),
+ mlWorkspaces: memstore.New[*driver.MLWorkspace](),
+ computes: memstore.New[*driver.Compute](),
+ mlEndpoints: memstore.New[*driver.Endpoint](),
+ mlDeploys: memstore.New[*driver.EndpointDeployment](),
+ jobs: memstore.New[*driver.Job](),
+ assets: memstore.New[*driver.Asset](),
+ datastores: memstore.New[*driver.Datastore](),
+ connections: memstore.New[*driver.Connection](),
+ mlSchedules: memstore.New[*driver.MLSchedule](),
+ registries: memstore.New[*driver.Registry](),
+ opts: opts,
+ }
+}
+
+// SetMonitoring wires a monitoring backend for auto-metric emission. Optional;
+// when unset, metric emission is a no-op.
+func (m *Mock) SetMonitoring(mon mondriver.Monitoring) {
+ m.monitoring = mon
+}
+
+// --- shared helpers ---
+
+func (m *Mock) now() string {
+ return m.opts.Clock.Now().UTC().Format(time.RFC3339)
+}
+
+// key joins path segments into a unique store key.
+func key(parts ...string) string {
+ return strings.Join(parts, "/")
+}
+
+func copyMap(in map[string]string) map[string]string {
+ if in == nil {
+ return nil
+ }
+
+ out := make(map[string]string, len(in))
+ for k, v := range in {
+ out[k] = v
+ }
+
+ return out
+}
+
+// emitMetric pushes an auto-metric to the wired monitoring backend (no-op when
+// monitoring is unset). Failures never affect the control plane.
+func (m *Mock) emitMetric(metricName string, value float64, dims map[string]string) {
+ if m.monitoring == nil {
+ return
+ }
+
+ _ = m.monitoring.PutMetricData(context.Background(), []mondriver.MetricDatum{{
+ Namespace: csProvider, MetricName: metricName, Value: value,
+ Unit: "Count", Dimensions: dims, Timestamp: m.opts.Clock.Now(),
+ }})
+}
+
+func csOrDefaultSKU(name string) string {
+ if name == "" {
+ return defaultCSSKU
+ }
+
+ return name
+}
+
+// accountEndpoint derives the data-plane endpoint for an account from its kind.
+func accountEndpoint(name, kind string) string {
+ host := "cognitiveservices"
+ if strings.EqualFold(kind, "OpenAI") {
+ host = "openai"
+ } else if strings.EqualFold(kind, "AIServices") {
+ host = "services.ai"
+ }
+
+ return "https://" + name + "." + host + ".azure.com/"
+}
+
+// --- Accounts ---
+
+func cloneAccount(a *driver.Account) *driver.Account {
+ out := *a
+ out.Tags = copyMap(a.Tags)
+
+ return &out
+}
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateAccount(_ context.Context, cfg driver.AccountConfig) (*driver.Account, error) {
+ switch {
+ case cfg.Name == "":
+ return nil, errors.New(errors.InvalidArgument, "account name is required")
+ case cfg.ResourceGroup == "":
+ return nil, errors.New(errors.InvalidArgument, "resource group is required")
+ case cfg.Location == "":
+ return nil, errors.New(errors.InvalidArgument, "location is required")
+ }
+
+ k := key(cfg.ResourceGroup, cfg.Name)
+
+ kind := cfg.Kind
+ if kind == "" {
+ kind = "AIServices"
+ }
+
+ // ARM PUT is create-or-update: apply mutable fields to a copy of an
+ // existing account, preserving identity fields.
+ if existing, ok := m.accounts.Get(k); ok {
+ updated := *existing
+ updated.SKUName = csOrDefaultSKU(cfg.SKUName)
+ updated.Tags = copyMap(cfg.Tags)
+
+ if cfg.CustomDomain != "" {
+ updated.CustomDomain = cfg.CustomDomain
+ }
+
+ m.accounts.Set(k, &updated)
+
+ return cloneAccount(&updated), nil
+ }
+
+ acct := &driver.Account{
+ ID: idgen.AzureID(m.opts.AccountID, cfg.ResourceGroup, csProvider, "accounts", cfg.Name),
+ Name: cfg.Name,
+ ResourceGroup: cfg.ResourceGroup,
+ Location: cfg.Location,
+ Kind: kind,
+ SKUName: csOrDefaultSKU(cfg.SKUName),
+ Endpoint: accountEndpoint(cfg.Name, kind),
+ CustomDomain: cfg.CustomDomain,
+ ProvisioningState: driver.StateSucceeded,
+ Tags: copyMap(cfg.Tags),
+ CreatedAt: m.now(),
+ }
+ m.accounts.Set(k, acct)
+ m.emitMetric("account/count", 1, map[string]string{"kind": kind})
+
+ return cloneAccount(acct), nil
+}
+
+func (m *Mock) GetAccount(_ context.Context, resourceGroup, name string) (*driver.Account, error) {
+ a, ok := m.accounts.Get(key(resourceGroup, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "account %q not found", name)
+ }
+
+ return cloneAccount(a), nil
+}
+
+func (m *Mock) DeleteAccount(_ context.Context, resourceGroup, name string) error {
+ if !m.accounts.Delete(key(resourceGroup, name)) {
+ return errors.Newf(errors.NotFound, "account %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) UpdateAccountTags(
+ _ context.Context, resourceGroup, name string, tags map[string]string,
+) (*driver.Account, error) {
+ k := key(resourceGroup, name)
+
+ a, ok := m.accounts.Get(k)
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "account %q not found", name)
+ }
+
+ updated := *a
+ updated.Tags = copyMap(tags)
+ m.accounts.Set(k, &updated)
+
+ return cloneAccount(&updated), nil
+}
+
+func (m *Mock) ListAccountsByResourceGroup(_ context.Context, resourceGroup string) ([]driver.Account, error) {
+ out := make([]driver.Account, 0)
+
+ for _, a := range m.accounts.All() {
+ if a.ResourceGroup == resourceGroup {
+ out = append(out, *cloneAccount(a))
+ }
+ }
+
+ return out, nil
+}
+
+func (m *Mock) ListAccounts(_ context.Context) ([]driver.Account, error) {
+ all := m.accounts.All()
+ out := make([]driver.Account, 0, len(all))
+
+ for _, a := range all {
+ out = append(out, *cloneAccount(a))
+ }
+
+ return out, nil
+}
+
+// currentAccountKeys returns the persisted keys, or the deterministic defaults
+// when the account has never had a key regenerated.
+func (m *Mock) currentAccountKeys(resourceGroup, name string) *driver.AccountKeys {
+ if k, ok := m.accountKeys.Get(key(resourceGroup, name)); ok {
+ out := *k
+
+ return &out
+ }
+
+ return &driver.AccountKeys{
+ Key1: deterministicKey(resourceGroup, name, "1"),
+ Key2: deterministicKey(resourceGroup, name, "2"),
+ }
+}
+
+func (m *Mock) ListAccountKeys(_ context.Context, resourceGroup, name string) (*driver.AccountKeys, error) {
+ if !m.accounts.Has(key(resourceGroup, name)) {
+ return nil, errors.Newf(errors.NotFound, "account %q not found", name)
+ }
+
+ return m.currentAccountKeys(resourceGroup, name), nil
+}
+
+func (m *Mock) RegenerateAccountKey(_ context.Context, resourceGroup, name, keyName string) (*driver.AccountKeys, error) {
+ if !m.accounts.Has(key(resourceGroup, name)) {
+ return nil, errors.Newf(errors.NotFound, "account %q not found", name)
+ }
+
+ keys := m.currentAccountKeys(resourceGroup, name)
+
+ // Salt the named key with a monotonic counter so the rotation is distinct
+ // and observable via a subsequent ListAccountKeys; the other key is stable.
+ salt := "regen-" + strconv.FormatInt(m.seq.Add(1), 16)
+ if strings.EqualFold(keyName, "Key2") {
+ keys.Key2 = deterministicKey(resourceGroup, name, salt)
+ } else {
+ keys.Key1 = deterministicKey(resourceGroup, name, salt)
+ }
+
+ m.accountKeys.Set(key(resourceGroup, name), keys)
+
+ out := *keys
+
+ return &out, nil
+}
+
+func (m *Mock) ListAccountUsages(_ context.Context, resourceGroup, name string) ([]driver.Usage, error) {
+ if !m.accounts.Has(key(resourceGroup, name)) {
+ return nil, errors.Newf(errors.NotFound, "account %q not found", name)
+ }
+
+ deps, _ := m.ListDeployments(context.Background(), resourceGroup, name)
+
+ return []driver.Usage{
+ {Name: "TokensPerMinute", CurrentValue: float64(len(deps)) * 1000, Limit: 240000, Unit: "Count"},
+ {Name: "DeploymentCount", CurrentValue: float64(len(deps)), Limit: 32, Unit: "Count"},
+ }, nil
+}
+
+func (m *Mock) ListAccountModels(_ context.Context, resourceGroup, name string) ([]driver.AccountModel, error) {
+ a, ok := m.accounts.Get(key(resourceGroup, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "account %q not found", name)
+ }
+
+ // A representative catalog scoped to the account's kind.
+ return []driver.AccountModel{
+ {Name: "gpt-4o", Version: "2024-08-06", Format: "OpenAI", Kind: a.Kind},
+ {Name: "gpt-4o-mini", Version: "2024-07-18", Format: "OpenAI", Kind: a.Kind},
+ {Name: "text-embedding-3-large", Version: "1", Format: "OpenAI", Kind: a.Kind},
+ }, nil
+}
+
+func (m *Mock) ListAccountSkus(_ context.Context, resourceGroup, name string) ([]driver.AccountSKU, error) {
+ if !m.accounts.Has(key(resourceGroup, name)) {
+ return nil, errors.Newf(errors.NotFound, "account %q not found", name)
+ }
+
+ return []driver.AccountSKU{
+ {Name: "F0", Tier: "Free"},
+ {Name: "S0", Tier: "Standard"},
+ }, nil
+}
+
+// deterministicKey derives a stable 32-hex-char access key from its inputs.
+func deterministicKey(parts ...string) string {
+ s := strings.Join(parts, ":")
+ h1 := fnv.New64a()
+ _, _ = h1.Write([]byte(s))
+ h2 := fnv.New64a()
+ _, _ = h2.Write([]byte(s + "#salt"))
+
+ return fmt.Sprintf("%016x%016x", h1.Sum64(), h2.Sum64())
+}
diff --git a/providers/azure/azureai/cognitiveservices.go b/providers/azure/azureai/cognitiveservices.go
new file mode 100644
index 0000000..7d0d5c0
--- /dev/null
+++ b/providers/azure/azureai/cognitiveservices.go
@@ -0,0 +1,352 @@
+package azureai
+
+import (
+ "context"
+ "strings"
+
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+// subResourceID builds the ARM ID for a child resource of an account.
+func (m *Mock) subResourceID(resourceGroup, account, childType, name string) string {
+ return idgen.AzureID(m.opts.AccountID, resourceGroup, csProvider, "accounts", account) +
+ "/" + childType + "/" + name
+}
+
+// requireAccount returns NotFound when the parent account is absent.
+func (m *Mock) requireAccount(resourceGroup, account string) error {
+ if !m.accounts.Has(key(resourceGroup, account)) {
+ return errors.Newf(errors.NotFound, "account %q not found", account)
+ }
+
+ return nil
+}
+
+// childKey builds a store key scoped under an account: rg/account/type/name.
+func childKey(resourceGroup, account, childType, name string) string {
+ return key(resourceGroup, account, childType, name)
+}
+
+// childPrefix builds the store-key prefix for listing children of an account.
+func childPrefix(resourceGroup, account, childType string) string {
+ return key(resourceGroup, account, childType) + "/"
+}
+
+// --- Deployments ---
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateDeployment(_ context.Context, cfg driver.DeploymentConfig) (*driver.Deployment, error) {
+ if err := m.requireAccount(cfg.ResourceGroup, cfg.Account); err != nil {
+ return nil, err
+ }
+
+ if cfg.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "deployment name is required")
+ }
+
+ skuName := cfg.SKUName
+ if skuName == "" {
+ skuName = "Standard"
+ }
+
+ d := &driver.Deployment{
+ ID: m.subResourceID(cfg.ResourceGroup, cfg.Account, "deployments", cfg.Name),
+ Name: cfg.Name,
+ ModelName: cfg.ModelName,
+ ModelVersion: cfg.ModelVersion,
+ ModelFormat: cfg.ModelFormat,
+ SKUName: skuName,
+ SKUCapacity: cfg.SKUCapacity,
+ ProvisioningState: driver.StateSucceeded,
+ CreatedAt: m.now(),
+ }
+ m.deployments.Set(childKey(cfg.ResourceGroup, cfg.Account, "deployments", cfg.Name), d)
+ m.emitMetric("deployment/count", 1, map[string]string{"model": cfg.ModelName})
+
+ out := *d
+
+ return &out, nil
+}
+
+func (m *Mock) GetDeployment(_ context.Context, resourceGroup, account, name string) (*driver.Deployment, error) {
+ d, ok := m.deployments.Get(childKey(resourceGroup, account, "deployments", name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "deployment %q not found", name)
+ }
+
+ out := *d
+
+ return &out, nil
+}
+
+func (m *Mock) DeleteDeployment(_ context.Context, resourceGroup, account, name string) error {
+ if !m.deployments.Delete(childKey(resourceGroup, account, "deployments", name)) {
+ return errors.Newf(errors.NotFound, "deployment %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListDeployments(_ context.Context, resourceGroup, account string) ([]driver.Deployment, error) {
+ prefix := childPrefix(resourceGroup, account, "deployments")
+ out := make([]driver.Deployment, 0)
+
+ for k, d := range m.deployments.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *d)
+ }
+ }
+
+ return out, nil
+}
+
+// --- Projects (AI Foundry) ---
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateProject(_ context.Context, cfg driver.ProjectConfig) (*driver.Project, error) {
+ if err := m.requireAccount(cfg.ResourceGroup, cfg.Account); err != nil {
+ return nil, err
+ }
+
+ if cfg.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "project name is required")
+ }
+
+ p := &driver.Project{
+ ID: m.subResourceID(cfg.ResourceGroup, cfg.Account, "projects", cfg.Name),
+ Name: cfg.Name,
+ Location: cfg.Location,
+ DisplayName: cfg.DisplayName,
+ Description: cfg.Description,
+ ProvisioningState: driver.StateSucceeded,
+ Tags: copyMap(cfg.Tags),
+ CreatedAt: m.now(),
+ }
+ m.projects.Set(childKey(cfg.ResourceGroup, cfg.Account, "projects", cfg.Name), p)
+
+ return cloneProject(p), nil
+}
+
+func cloneProject(p *driver.Project) *driver.Project {
+ out := *p
+ out.Tags = copyMap(p.Tags)
+
+ return &out
+}
+
+func (m *Mock) GetProject(_ context.Context, resourceGroup, account, name string) (*driver.Project, error) {
+ p, ok := m.projects.Get(childKey(resourceGroup, account, "projects", name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "project %q not found", name)
+ }
+
+ return cloneProject(p), nil
+}
+
+func (m *Mock) DeleteProject(_ context.Context, resourceGroup, account, name string) error {
+ if !m.projects.Delete(childKey(resourceGroup, account, "projects", name)) {
+ return errors.Newf(errors.NotFound, "project %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListProjects(_ context.Context, resourceGroup, account string) ([]driver.Project, error) {
+ prefix := childPrefix(resourceGroup, account, "projects")
+ out := make([]driver.Project, 0)
+
+ for k, p := range m.projects.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *cloneProject(p))
+ }
+ }
+
+ return out, nil
+}
+
+// --- RAI policies ---
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateRaiPolicy(_ context.Context, cfg driver.RaiPolicyConfig) (*driver.RaiPolicy, error) {
+ if err := m.requireAccount(cfg.ResourceGroup, cfg.Account); err != nil {
+ return nil, err
+ }
+
+ if cfg.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "RAI policy name is required")
+ }
+
+ mode := cfg.Mode
+ if mode == "" {
+ mode = kindDefault
+ }
+
+ p := &driver.RaiPolicy{
+ ID: m.subResourceID(cfg.ResourceGroup, cfg.Account, "raiPolicies", cfg.Name),
+ Name: cfg.Name,
+ Mode: mode,
+ BasePolicy: cfg.BasePolicy,
+ CreatedAt: m.now(),
+ }
+ m.raiPolicies.Set(childKey(cfg.ResourceGroup, cfg.Account, "raiPolicies", cfg.Name), p)
+
+ out := *p
+
+ return &out, nil
+}
+
+func (m *Mock) GetRaiPolicy(_ context.Context, resourceGroup, account, name string) (*driver.RaiPolicy, error) {
+ p, ok := m.raiPolicies.Get(childKey(resourceGroup, account, "raiPolicies", name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "RAI policy %q not found", name)
+ }
+
+ out := *p
+
+ return &out, nil
+}
+
+func (m *Mock) DeleteRaiPolicy(_ context.Context, resourceGroup, account, name string) error {
+ if !m.raiPolicies.Delete(childKey(resourceGroup, account, "raiPolicies", name)) {
+ return errors.Newf(errors.NotFound, "RAI policy %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListRaiPolicies(_ context.Context, resourceGroup, account string) ([]driver.RaiPolicy, error) {
+ prefix := childPrefix(resourceGroup, account, "raiPolicies")
+ out := make([]driver.RaiPolicy, 0)
+
+ for k, p := range m.raiPolicies.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *p)
+ }
+ }
+
+ return out, nil
+}
+
+// --- Commitment plans ---
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateCommitmentPlan(_ context.Context, cfg driver.CommitmentPlanConfig) (*driver.CommitmentPlan, error) {
+ if err := m.requireAccount(cfg.ResourceGroup, cfg.Account); err != nil {
+ return nil, err
+ }
+
+ if cfg.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "commitment plan name is required")
+ }
+
+ p := &driver.CommitmentPlan{
+ ID: m.subResourceID(cfg.ResourceGroup, cfg.Account, "commitmentPlans", cfg.Name),
+ Name: cfg.Name,
+ PlanType: cfg.PlanType,
+ Tier: cfg.Tier,
+ AutoRenew: cfg.AutoRenew,
+ ProvisioningState: driver.StateSucceeded,
+ CreatedAt: m.now(),
+ }
+ m.commitmentPlans.Set(childKey(cfg.ResourceGroup, cfg.Account, "commitmentPlans", cfg.Name), p)
+
+ out := *p
+
+ return &out, nil
+}
+
+func (m *Mock) GetCommitmentPlan(_ context.Context, resourceGroup, account, name string) (*driver.CommitmentPlan, error) {
+ p, ok := m.commitmentPlans.Get(childKey(resourceGroup, account, "commitmentPlans", name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "commitment plan %q not found", name)
+ }
+
+ out := *p
+
+ return &out, nil
+}
+
+func (m *Mock) DeleteCommitmentPlan(_ context.Context, resourceGroup, account, name string) error {
+ if !m.commitmentPlans.Delete(childKey(resourceGroup, account, "commitmentPlans", name)) {
+ return errors.Newf(errors.NotFound, "commitment plan %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListCommitmentPlans(_ context.Context, resourceGroup, account string) ([]driver.CommitmentPlan, error) {
+ prefix := childPrefix(resourceGroup, account, "commitmentPlans")
+ out := make([]driver.CommitmentPlan, 0)
+
+ for k, p := range m.commitmentPlans.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *p)
+ }
+ }
+
+ return out, nil
+}
+
+// --- Private-endpoint connections ---
+
+func (m *Mock) PutPrivateEndpointConnection(
+ _ context.Context, resourceGroup, account, name, status string,
+) (*driver.PrivateEndpointConnection, error) {
+ if err := m.requireAccount(resourceGroup, account); err != nil {
+ return nil, err
+ }
+
+ if status == "" {
+ status = "Approved"
+ }
+
+ c := &driver.PrivateEndpointConnection{
+ ID: m.subResourceID(resourceGroup, account, "privateEndpointConnections", name),
+ Name: name,
+ Status: status,
+ ProvisioningState: driver.StateSucceeded,
+ }
+ m.privateEndpoints.Set(childKey(resourceGroup, account, "privateEndpointConnections", name), c)
+
+ out := *c
+
+ return &out, nil
+}
+
+func (m *Mock) GetPrivateEndpointConnection(
+ _ context.Context, resourceGroup, account, name string,
+) (*driver.PrivateEndpointConnection, error) {
+ c, ok := m.privateEndpoints.Get(childKey(resourceGroup, account, "privateEndpointConnections", name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "private endpoint connection %q not found", name)
+ }
+
+ out := *c
+
+ return &out, nil
+}
+
+func (m *Mock) DeletePrivateEndpointConnection(_ context.Context, resourceGroup, account, name string) error {
+ if !m.privateEndpoints.Delete(childKey(resourceGroup, account, "privateEndpointConnections", name)) {
+ return errors.Newf(errors.NotFound, "private endpoint connection %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListPrivateEndpointConnections(
+ _ context.Context, resourceGroup, account string,
+) ([]driver.PrivateEndpointConnection, error) {
+ prefix := childPrefix(resourceGroup, account, "privateEndpointConnections")
+ out := make([]driver.PrivateEndpointConnection, 0)
+
+ for k, c := range m.privateEndpoints.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *c)
+ }
+ }
+
+ return out, nil
+}
diff --git a/providers/azure/azureai/dataplane.go b/providers/azure/azureai/dataplane.go
new file mode 100644
index 0000000..be8f819
--- /dev/null
+++ b/providers/azure/azureai/dataplane.go
@@ -0,0 +1,333 @@
+package azureai
+
+import (
+ "context"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+// Compile-time check that Mock implements the data-plane surface.
+var _ driver.DataPlane = (*Mock)(nil)
+
+// embeddingDims is the fixed synthetic embedding width.
+const embeddingDims = 16
+
+// roleUser is the default/user chat role.
+const roleUser = "user"
+
+// FNV-1a parameters and the modulus used to derive deterministic embeddings.
+const (
+ fnvOffset uint32 = 2166136261
+ fnvPrime uint32 = 16777619
+ embedMod uint32 = 1000
+)
+
+func (m *Mock) nextID(prefix string) string {
+ return prefix + "_" + idHex(m.seq.Add(1))
+}
+
+func idHex(n int64) string {
+ const digits = "0123456789abcdef"
+
+ if n == 0 {
+ return "0"
+ }
+
+ var b [16]byte
+
+ i := len(b)
+ for n > 0 {
+ i--
+ b[i] = digits[n&0xf]
+ n >>= 4
+ }
+
+ return string(b[i:])
+}
+
+func (m *Mock) unixNow() int64 {
+ return m.opts.Clock.Now().Unix()
+}
+
+// approxTokens is a deterministic whitespace-based token estimate.
+func approxTokens(text string) int {
+ if strings.TrimSpace(text) == "" {
+ return 0
+ }
+
+ return len(strings.Fields(text))
+}
+
+// --- Azure OpenAI inference ---
+
+func (m *Mock) ChatCompletions(
+ _ context.Context, _, deployment string, req driver.ChatCompletionRequest,
+) (*driver.ChatCompletionResponse, error) {
+ prompt := 0
+ last := ""
+
+ for _, msg := range req.Messages {
+ prompt += approxTokens(msg.Content)
+
+ if msg.Role == roleUser {
+ last = msg.Content
+ }
+ }
+
+ reply := "Echo: " + last
+ completion := approxTokens(reply)
+
+ m.emitMetric("inference/chatCompletions", 1, map[string]string{"deployment": deployment})
+
+ return &driver.ChatCompletionResponse{
+ ID: m.nextID("chatcmpl"),
+ Model: deployment,
+ Created: m.unixNow(),
+ Choices: []driver.ChatChoice{{
+ Index: 0,
+ Message: driver.ChatMessage{Role: "assistant", Content: reply},
+ FinishReason: "stop",
+ }},
+ Usage: driver.TokenUsage{
+ PromptTokens: prompt, CompletionTokens: completion, TotalTokens: prompt + completion,
+ },
+ }, nil
+}
+
+func (m *Mock) Embeddings(
+ _ context.Context, _, deployment string, req driver.EmbeddingsRequest,
+) (*driver.EmbeddingsResponse, error) {
+ data := make([]driver.EmbeddingData, 0, len(req.Input))
+ prompt := 0
+
+ for i, in := range req.Input {
+ prompt += approxTokens(in)
+ data = append(data, driver.EmbeddingData{Index: i, Embedding: embed(in)})
+ }
+
+ m.emitMetric("inference/embeddings", float64(len(req.Input)), map[string]string{"deployment": deployment})
+
+ return &driver.EmbeddingsResponse{
+ Model: deployment,
+ Data: data,
+ Usage: driver.TokenUsage{PromptTokens: prompt, TotalTokens: prompt},
+ }, nil
+}
+
+// embed returns a deterministic unit-ish vector derived from the input.
+func embed(s string) []float64 {
+ out := make([]float64, embeddingDims)
+ h := fnvOffset
+
+ for i := 0; i < len(s); i++ {
+ h ^= uint32(s[i])
+ h *= fnvPrime
+ out[i%embeddingDims] += float64(h%embedMod) / float64(embedMod)
+ }
+
+ return out
+}
+
+func (m *Mock) Completions(
+ _ context.Context, _, deployment string, req driver.CompletionsRequest,
+) (*driver.CompletionsResponse, error) {
+ prompt := approxTokens(req.Prompt)
+ text := "Echo: " + req.Prompt
+ completion := approxTokens(text)
+
+ m.emitMetric("inference/completions", 1, map[string]string{"deployment": deployment})
+
+ return &driver.CompletionsResponse{
+ ID: m.nextID("cmpl"),
+ Model: deployment,
+ Created: m.unixNow(),
+ Choices: []driver.CompletionChoice{{Text: text, Index: 0, FinishReason: "stop"}},
+ Usage: driver.TokenUsage{PromptTokens: prompt, CompletionTokens: completion, TotalTokens: prompt + completion},
+ }, nil
+}
+
+// --- Assistants API ---
+
+func (m *Mock) CreateAssistant(_ context.Context, cfg driver.AssistantConfig) (*driver.Assistant, error) {
+ if cfg.Model == "" {
+ return nil, errors.New(errors.InvalidArgument, "model is required")
+ }
+
+ a := &driver.Assistant{
+ ID: m.nextID("asst"), Model: cfg.Model, Name: cfg.Name,
+ Instructions: cfg.Instructions, CreatedAt: m.unixNow(),
+ }
+ m.assistants.Set(key(cfg.Account, a.ID), a)
+
+ out := *a
+
+ return &out, nil
+}
+
+func (m *Mock) GetAssistant(_ context.Context, account, id string) (*driver.Assistant, error) {
+ a, ok := m.assistants.Get(key(account, id))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "assistant %q not found", id)
+ }
+
+ out := *a
+
+ return &out, nil
+}
+
+func (m *Mock) ListAssistants(_ context.Context, account string) ([]driver.Assistant, error) {
+ prefix := account + "/"
+ out := make([]driver.Assistant, 0)
+
+ for k, a := range m.assistants.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *a)
+ }
+ }
+
+ sort.SliceStable(out, func(i, j int) bool { return seqFromID(out[i].ID) < seqFromID(out[j].ID) })
+
+ return out, nil
+}
+
+func (m *Mock) DeleteAssistant(_ context.Context, account, id string) error {
+ if !m.assistants.Delete(key(account, id)) {
+ return errors.Newf(errors.NotFound, "assistant %q not found", id)
+ }
+
+ return nil
+}
+
+func (m *Mock) CreateThread(_ context.Context, account string) (*driver.Thread, error) {
+ t := &driver.Thread{ID: m.nextID("thread"), CreatedAt: m.unixNow()}
+ m.threads.Set(key(account, t.ID), t)
+
+ out := *t
+
+ return &out, nil
+}
+
+func (m *Mock) GetThread(_ context.Context, account, id string) (*driver.Thread, error) {
+ t, ok := m.threads.Get(key(account, id))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "thread %q not found", id)
+ }
+
+ out := *t
+
+ return &out, nil
+}
+
+func (m *Mock) DeleteThread(_ context.Context, account, id string) error {
+ if !m.threads.Delete(key(account, id)) {
+ return errors.Newf(errors.NotFound, "thread %q not found", id)
+ }
+
+ return nil
+}
+
+func (m *Mock) CreateMessage(_ context.Context, account, thread, role, content string) (*driver.ThreadMessage, error) {
+ if !m.threads.Has(key(account, thread)) {
+ return nil, errors.Newf(errors.NotFound, "thread %q not found", thread)
+ }
+
+ if role == "" {
+ role = roleUser
+ }
+
+ msg := &driver.ThreadMessage{
+ ID: m.nextID("msg"), ThreadID: thread, Role: role, Content: content, CreatedAt: m.unixNow(),
+ }
+ m.messages.Set(key(account, thread, msg.ID), msg)
+
+ out := *msg
+
+ return &out, nil
+}
+
+func (m *Mock) ListMessages(_ context.Context, account, thread string) ([]driver.ThreadMessage, error) {
+ if !m.threads.Has(key(account, thread)) {
+ return nil, errors.Newf(errors.NotFound, "thread %q not found", thread)
+ }
+
+ prefix := key(account, thread) + "/"
+ out := make([]driver.ThreadMessage, 0)
+
+ for k, msg := range m.messages.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *msg)
+ }
+ }
+
+ // Return in creation order; message IDs carry a monotonic sequence.
+ sort.SliceStable(out, func(i, j int) bool { return seqFromID(out[i].ID) < seqFromID(out[j].ID) })
+
+ return out, nil
+}
+
+// seqFromID extracts the monotonic sequence encoded as the hex suffix of an ID
+// minted by nextID ("_"). Unparseable IDs sort first.
+func seqFromID(id string) int64 {
+ _, hexPart, ok := strings.Cut(id, "_")
+ if !ok {
+ return -1
+ }
+
+ n, err := strconv.ParseInt(hexPart, 16, 64)
+ if err != nil {
+ return -1
+ }
+
+ return n
+}
+
+func (m *Mock) CreateRun(_ context.Context, account, thread, assistant string) (*driver.Run, error) {
+ if !m.threads.Has(key(account, thread)) {
+ return nil, errors.Newf(errors.NotFound, "thread %q not found", thread)
+ }
+
+ if !m.assistants.Has(key(account, assistant)) {
+ return nil, errors.Newf(errors.NotFound, "assistant %q not found", assistant)
+ }
+
+ // Runs complete synchronously in the emulator.
+ run := &driver.Run{
+ ID: m.nextID("run"), ThreadID: thread, AssistantID: assistant,
+ Status: "completed", CreatedAt: m.unixNow(),
+ }
+ m.runs.Set(key(account, thread, run.ID), run)
+
+ out := *run
+
+ return &out, nil
+}
+
+func (m *Mock) GetRun(_ context.Context, account, thread, id string) (*driver.Run, error) {
+ run, ok := m.runs.Get(key(account, thread, id))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "run %q not found", id)
+ }
+
+ out := *run
+
+ return &out, nil
+}
+
+// --- AML online-endpoint scoring (filled by the AML phase's data plane) ---
+
+func (m *Mock) ScoreOnlineEndpoint(_ context.Context, endpoint string, body []byte) ([]byte, error) {
+ if endpoint == "" {
+ return nil, errors.New(errors.InvalidArgument, "endpoint is required")
+ }
+
+ m.emitMetric("inference/score", 1, map[string]string{"endpoint": endpoint})
+
+ out := make([]byte, len(body))
+ copy(out, body)
+
+ return out, nil
+}
diff --git a/providers/azure/azureai/machinelearning.go b/providers/azure/azureai/machinelearning.go
new file mode 100644
index 0000000..694686d
--- /dev/null
+++ b/providers/azure/azureai/machinelearning.go
@@ -0,0 +1,265 @@
+package azureai
+
+import (
+ "context"
+ "strings"
+
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+// Compile-time check that Mock implements the MachineLearning surface.
+var _ driver.MachineLearning = (*Mock)(nil)
+
+const mlProvider = "Microsoft.MachineLearningServices"
+
+func (m *Mock) mlID(resourceGroup, resourceType, name string) string {
+ return idgen.AzureID(m.opts.AccountID, resourceGroup, mlProvider, resourceType, name)
+}
+
+// wsChildID builds an ARM ID for a resource nested under a workspace.
+func (m *Mock) wsChildID(resourceGroup, workspace, childPath string) string {
+ return m.mlID(resourceGroup, "workspaces", workspace) + "/" + childPath
+}
+
+// --- Workspaces ---
+
+func cloneMLWorkspace(w *driver.MLWorkspace) *driver.MLWorkspace {
+ out := *w
+ out.Tags = copyMap(w.Tags)
+
+ return &out
+}
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateMLWorkspace(_ context.Context, cfg driver.MLWorkspaceConfig) (*driver.MLWorkspace, error) {
+ switch {
+ case cfg.Name == "":
+ return nil, errors.New(errors.InvalidArgument, "workspace name is required")
+ case cfg.ResourceGroup == "":
+ return nil, errors.New(errors.InvalidArgument, "resource group is required")
+ case cfg.Location == "":
+ return nil, errors.New(errors.InvalidArgument, "location is required")
+ }
+
+ k := key(cfg.ResourceGroup, cfg.Name)
+
+ kind := cfg.Kind
+ if kind == "" {
+ kind = kindDefault
+ }
+
+ if existing, ok := m.mlWorkspaces.Get(k); ok {
+ updated := *existing
+ updated.FriendlyName = cfg.FriendlyName
+ updated.Description = cfg.Description
+ updated.Tags = copyMap(cfg.Tags)
+ m.mlWorkspaces.Set(k, &updated)
+
+ return cloneMLWorkspace(&updated), nil
+ }
+
+ ws := &driver.MLWorkspace{
+ ID: m.mlID(cfg.ResourceGroup, "workspaces", cfg.Name),
+ Name: cfg.Name,
+ ResourceGroup: cfg.ResourceGroup,
+ Location: cfg.Location,
+ Kind: kind,
+ FriendlyName: cfg.FriendlyName,
+ Description: cfg.Description,
+ DiscoveryURL: "https://" + orLower(cfg.Location) + ".api.azureml.ms/discovery",
+ ProvisioningState: driver.StateSucceeded,
+ Tags: copyMap(cfg.Tags),
+ CreatedAt: m.now(),
+ }
+ m.mlWorkspaces.Set(k, ws)
+ m.emitMetric("workspace/count", 1, map[string]string{"kind": kind})
+
+ return cloneMLWorkspace(ws), nil
+}
+
+func orLower(s string) string {
+ if s == "" {
+ return "eastus"
+ }
+
+ return strings.ToLower(s)
+}
+
+func (m *Mock) GetMLWorkspace(_ context.Context, resourceGroup, name string) (*driver.MLWorkspace, error) {
+ w, ok := m.mlWorkspaces.Get(key(resourceGroup, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "workspace %q not found", name)
+ }
+
+ return cloneMLWorkspace(w), nil
+}
+
+func (m *Mock) DeleteMLWorkspace(_ context.Context, resourceGroup, name string) error {
+ if !m.mlWorkspaces.Delete(key(resourceGroup, name)) {
+ return errors.Newf(errors.NotFound, "workspace %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) UpdateMLWorkspaceTags(
+ _ context.Context, resourceGroup, name string, tags map[string]string,
+) (*driver.MLWorkspace, error) {
+ k := key(resourceGroup, name)
+
+ w, ok := m.mlWorkspaces.Get(k)
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "workspace %q not found", name)
+ }
+
+ updated := *w
+ updated.Tags = copyMap(tags)
+ m.mlWorkspaces.Set(k, &updated)
+
+ return cloneMLWorkspace(&updated), nil
+}
+
+func (m *Mock) ListMLWorkspacesByResourceGroup(_ context.Context, resourceGroup string) ([]driver.MLWorkspace, error) {
+ out := make([]driver.MLWorkspace, 0)
+
+ for _, w := range m.mlWorkspaces.All() {
+ if w.ResourceGroup == resourceGroup {
+ out = append(out, *cloneMLWorkspace(w))
+ }
+ }
+
+ return out, nil
+}
+
+func (m *Mock) ListMLWorkspaces(_ context.Context) ([]driver.MLWorkspace, error) {
+ all := m.mlWorkspaces.All()
+ out := make([]driver.MLWorkspace, 0, len(all))
+
+ for _, w := range all {
+ out = append(out, *cloneMLWorkspace(w))
+ }
+
+ return out, nil
+}
+
+// requireWorkspace returns NotFound when the parent workspace is absent.
+func (m *Mock) requireWorkspace(resourceGroup, workspace string) error {
+ if !m.mlWorkspaces.Has(key(resourceGroup, workspace)) {
+ return errors.Newf(errors.NotFound, "workspace %q not found", workspace)
+ }
+
+ return nil
+}
+
+// --- Computes ---
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateCompute(_ context.Context, cfg driver.ComputeConfig) (*driver.Compute, error) {
+ if err := m.requireWorkspace(cfg.ResourceGroup, cfg.Workspace); err != nil {
+ return nil, err
+ }
+
+ if cfg.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "compute name is required")
+ }
+
+ ct := cfg.ComputeType
+ if ct == "" {
+ ct = "AmlCompute"
+ }
+
+ c := &driver.Compute{
+ ID: m.wsChildID(cfg.ResourceGroup, cfg.Workspace, "computes/"+cfg.Name),
+ Name: cfg.Name,
+ ComputeType: ct,
+ VMSize: cfg.VMSize,
+ MinNodes: cfg.MinNodes,
+ MaxNodes: cfg.MaxNodes,
+ State: "Running",
+ ProvisioningState: driver.StateSucceeded,
+ CreatedAt: m.now(),
+ }
+ m.computes.Set(key(cfg.ResourceGroup, cfg.Workspace, "computes", cfg.Name), c)
+
+ out := *c
+
+ return &out, nil
+}
+
+func (m *Mock) GetCompute(_ context.Context, resourceGroup, workspace, name string) (*driver.Compute, error) {
+ c, ok := m.computes.Get(key(resourceGroup, workspace, "computes", name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "compute %q not found", name)
+ }
+
+ out := *c
+
+ return &out, nil
+}
+
+func (m *Mock) DeleteCompute(_ context.Context, resourceGroup, workspace, name string) error {
+ if !m.computes.Delete(key(resourceGroup, workspace, "computes", name)) {
+ return errors.Newf(errors.NotFound, "compute %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListComputes(_ context.Context, resourceGroup, workspace string) ([]driver.Compute, error) {
+ prefix := key(resourceGroup, workspace, "computes") + "/"
+ out := make([]driver.Compute, 0)
+
+ for k, c := range m.computes.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *c)
+ }
+ }
+
+ return out, nil
+}
+
+func (m *Mock) StartCompute(_ context.Context, resourceGroup, workspace, name string) error {
+ return m.setComputeState(resourceGroup, workspace, name, "Stopped", "Running")
+}
+
+func (m *Mock) StopCompute(_ context.Context, resourceGroup, workspace, name string) error {
+ return m.setComputeState(resourceGroup, workspace, name, "Running", "Stopped")
+}
+
+func (m *Mock) RestartCompute(_ context.Context, resourceGroup, workspace, name string) error {
+ k := key(resourceGroup, workspace, "computes", name)
+
+ c, ok := m.computes.Get(k)
+ if !ok {
+ return errors.Newf(errors.NotFound, "compute %q not found", name)
+ }
+
+ updated := *c
+ updated.State = "Running"
+ m.computes.Set(k, &updated)
+
+ return nil
+}
+
+// setComputeState copy-then-Sets a compute to target only from the required
+// source state, rejecting illegal transitions like the real API.
+func (m *Mock) setComputeState(resourceGroup, workspace, name, from, target string) error {
+ k := key(resourceGroup, workspace, "computes", name)
+
+ c, ok := m.computes.Get(k)
+ if !ok {
+ return errors.Newf(errors.NotFound, "compute %q not found", name)
+ }
+
+ if c.State != from {
+ return errors.Newf(errors.FailedPrecondition, "compute %q is %s; cannot transition to %s", name, c.State, target)
+ }
+
+ updated := *c
+ updated.State = target
+ m.computes.Set(k, &updated)
+
+ return nil
+}
diff --git a/providers/azure/azureai/machinelearning_more.go b/providers/azure/azureai/machinelearning_more.go
new file mode 100644
index 0000000..f945aeb
--- /dev/null
+++ b/providers/azure/azureai/machinelearning_more.go
@@ -0,0 +1,547 @@
+package azureai
+
+import (
+ "context"
+ "strconv"
+ "strings"
+
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+// versionNewer reports whether candidate is a newer asset version than current.
+// Integer versions compare numerically ("10" > "9"); otherwise lexically.
+func versionNewer(candidate, current string) bool {
+ cn, cerr := strconv.Atoi(candidate)
+ rn, rerr := strconv.Atoi(current)
+
+ if cerr == nil && rerr == nil {
+ return cn > rn
+ }
+
+ return candidate > current
+}
+
+// --- Endpoints (online + batch) ---
+
+func endpointCollection(kind string) string {
+ if strings.EqualFold(kind, "batch") {
+ return "batchEndpoints"
+ }
+
+ return "onlineEndpoints"
+}
+
+func cloneEndpoint(e *driver.Endpoint) *driver.Endpoint {
+ out := *e
+
+ if e.Traffic != nil {
+ out.Traffic = make(map[string]int, len(e.Traffic))
+ for k, v := range e.Traffic {
+ out.Traffic[k] = v
+ }
+ }
+
+ return &out
+}
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateEndpoint(_ context.Context, cfg driver.EndpointConfig) (*driver.Endpoint, error) {
+ if err := m.requireWorkspace(cfg.ResourceGroup, cfg.Workspace); err != nil {
+ return nil, err
+ }
+
+ if cfg.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "endpoint name is required")
+ }
+
+ coll := endpointCollection(cfg.Kind)
+
+ auth := cfg.AuthMode
+ if auth == "" {
+ auth = "Key"
+ }
+
+ e := &driver.Endpoint{
+ ID: m.wsChildID(cfg.ResourceGroup, cfg.Workspace, coll+"/"+cfg.Name),
+ Name: cfg.Name,
+ Kind: cfg.Kind,
+ AuthMode: auth,
+ Description: cfg.Description,
+ ScoringURI: "https://" + cfg.Name + ".inference.ml.azure.com/score",
+ ProvisioningState: driver.StateSucceeded,
+ Traffic: map[string]int{},
+ CreatedAt: m.now(),
+ }
+ m.mlEndpoints.Set(key(cfg.ResourceGroup, cfg.Workspace, coll, cfg.Name), e)
+
+ return cloneEndpoint(e), nil
+}
+
+func (m *Mock) GetEndpoint(_ context.Context, resourceGroup, workspace, kind, name string) (*driver.Endpoint, error) {
+ e, ok := m.mlEndpoints.Get(key(resourceGroup, workspace, endpointCollection(kind), name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "endpoint %q not found", name)
+ }
+
+ return cloneEndpoint(e), nil
+}
+
+func (m *Mock) DeleteEndpoint(_ context.Context, resourceGroup, workspace, kind, name string) error {
+ if !m.mlEndpoints.Delete(key(resourceGroup, workspace, endpointCollection(kind), name)) {
+ return errors.Newf(errors.NotFound, "endpoint %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListEndpoints(_ context.Context, resourceGroup, workspace, kind string) ([]driver.Endpoint, error) {
+ prefix := key(resourceGroup, workspace, endpointCollection(kind)) + "/"
+ out := make([]driver.Endpoint, 0)
+
+ for k, e := range m.mlEndpoints.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *cloneEndpoint(e))
+ }
+ }
+
+ return out, nil
+}
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateEndpointDeployment(_ context.Context, cfg driver.EndpointDeploymentConfig) (*driver.EndpointDeployment, error) {
+ coll := endpointCollection(cfg.EndpointKind)
+ if !m.mlEndpoints.Has(key(cfg.ResourceGroup, cfg.Workspace, coll, cfg.Endpoint)) {
+ return nil, errors.Newf(errors.NotFound, "endpoint %q not found", cfg.Endpoint)
+ }
+
+ if cfg.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "deployment name is required")
+ }
+
+ d := &driver.EndpointDeployment{
+ ID: m.wsChildID(cfg.ResourceGroup, cfg.Workspace, coll+"/"+cfg.Endpoint+"/deployments/"+cfg.Name),
+ Name: cfg.Name, Model: cfg.Model, InstanceType: cfg.InstanceType, InstanceCount: cfg.InstanceCount,
+ ProvisioningState: driver.StateSucceeded, CreatedAt: m.now(),
+ }
+ m.mlDeploys.Set(key(cfg.ResourceGroup, cfg.Workspace, coll, cfg.Endpoint, "deployments", cfg.Name), d)
+
+ out := *d
+
+ return &out, nil
+}
+
+func (m *Mock) GetEndpointDeployment(
+ _ context.Context, resourceGroup, workspace, kind, endpoint, name string,
+) (*driver.EndpointDeployment, error) {
+ d, ok := m.mlDeploys.Get(key(resourceGroup, workspace, endpointCollection(kind), endpoint, "deployments", name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "deployment %q not found", name)
+ }
+
+ out := *d
+
+ return &out, nil
+}
+
+func (m *Mock) DeleteEndpointDeployment(_ context.Context, resourceGroup, workspace, kind, endpoint, name string) error {
+ if !m.mlDeploys.Delete(key(resourceGroup, workspace, endpointCollection(kind), endpoint, "deployments", name)) {
+ return errors.Newf(errors.NotFound, "deployment %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListEndpointDeployments(
+ _ context.Context, resourceGroup, workspace, kind, endpoint string,
+) ([]driver.EndpointDeployment, error) {
+ prefix := key(resourceGroup, workspace, endpointCollection(kind), endpoint, "deployments") + "/"
+ out := make([]driver.EndpointDeployment, 0)
+
+ for k, d := range m.mlDeploys.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *d)
+ }
+ }
+
+ return out, nil
+}
+
+// --- Jobs ---
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateJob(_ context.Context, cfg driver.JobConfig) (*driver.Job, error) {
+ if err := m.requireWorkspace(cfg.ResourceGroup, cfg.Workspace); err != nil {
+ return nil, err
+ }
+
+ name := cfg.Name
+ if name == "" {
+ name = m.nextID("job")
+ }
+
+ jt := cfg.JobType
+ if jt == "" {
+ jt = "Command"
+ }
+
+ j := &driver.Job{
+ ID: m.wsChildID(cfg.ResourceGroup, cfg.Workspace, "jobs/"+name), Name: name,
+ JobType: jt, DisplayName: cfg.DisplayName, Status: "Completed", CreatedAt: m.now(),
+ }
+ m.jobs.Set(key(cfg.ResourceGroup, cfg.Workspace, "jobs", name), j)
+
+ out := *j
+
+ return &out, nil
+}
+
+func (m *Mock) GetJob(_ context.Context, resourceGroup, workspace, name string) (*driver.Job, error) {
+ j, ok := m.jobs.Get(key(resourceGroup, workspace, "jobs", name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "job %q not found", name)
+ }
+
+ out := *j
+
+ return &out, nil
+}
+
+func (m *Mock) ListJobs(_ context.Context, resourceGroup, workspace string) ([]driver.Job, error) {
+ prefix := key(resourceGroup, workspace, "jobs") + "/"
+ out := make([]driver.Job, 0)
+
+ for k, j := range m.jobs.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *j)
+ }
+ }
+
+ return out, nil
+}
+
+func (m *Mock) CancelJob(_ context.Context, resourceGroup, workspace, name string) error {
+ k := key(resourceGroup, workspace, "jobs", name)
+
+ j, ok := m.jobs.Get(k)
+ if !ok {
+ return errors.Newf(errors.NotFound, "job %q not found", name)
+ }
+
+ updated := *j
+ updated.Status = "Canceled"
+ m.jobs.Set(k, &updated)
+
+ return nil
+}
+
+// --- Versioned assets ---
+
+func cloneAsset(a *driver.Asset) *driver.Asset {
+ out := *a
+ out.Properties = copyMap(a.Properties)
+
+ return &out
+}
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateAsset(_ context.Context, cfg driver.AssetConfig) (*driver.Asset, error) {
+ if err := m.requireWorkspace(cfg.ResourceGroup, cfg.Workspace); err != nil {
+ return nil, err
+ }
+
+ switch {
+ case cfg.AssetType == "":
+ return nil, errors.New(errors.InvalidArgument, "asset type is required")
+ case cfg.Name == "":
+ return nil, errors.New(errors.InvalidArgument, "asset name is required")
+ }
+
+ version := cfg.Version
+ if version == "" {
+ version = "1"
+ }
+
+ a := &driver.Asset{
+ ID: m.wsChildID(cfg.ResourceGroup, cfg.Workspace, cfg.AssetType+"/"+cfg.Name+"/versions/"+version),
+ Name: cfg.Name, Version: version, AssetType: cfg.AssetType, Description: cfg.Description,
+ Path: cfg.Path, Properties: copyMap(cfg.Properties), CreatedAt: m.now(),
+ }
+ m.assets.Set(key(cfg.ResourceGroup, cfg.Workspace, cfg.AssetType, cfg.Name, version), a)
+
+ return cloneAsset(a), nil
+}
+
+func (m *Mock) GetAsset(_ context.Context, resourceGroup, workspace, assetType, name, version string) (*driver.Asset, error) {
+ a, ok := m.assets.Get(key(resourceGroup, workspace, assetType, name, version))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "%s %q version %q not found", assetType, name, version)
+ }
+
+ return cloneAsset(a), nil
+}
+
+func (m *Mock) DeleteAsset(_ context.Context, resourceGroup, workspace, assetType, name, version string) error {
+ if !m.assets.Delete(key(resourceGroup, workspace, assetType, name, version)) {
+ return errors.Newf(errors.NotFound, "%s %q version %q not found", assetType, name, version)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListAssetVersions(_ context.Context, resourceGroup, workspace, assetType, name string) ([]driver.Asset, error) {
+ prefix := key(resourceGroup, workspace, assetType, name) + "/"
+ out := make([]driver.Asset, 0)
+
+ for k, a := range m.assets.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *cloneAsset(a))
+ }
+ }
+
+ return out, nil
+}
+
+// ListAssetContainers returns the latest version of each named asset container.
+func (m *Mock) ListAssetContainers(_ context.Context, resourceGroup, workspace, assetType string) ([]driver.Asset, error) {
+ prefix := key(resourceGroup, workspace, assetType) + "/"
+ latest := make(map[string]*driver.Asset)
+
+ for k, a := range m.assets.All() {
+ if strings.HasPrefix(k, prefix) {
+ if cur, ok := latest[a.Name]; !ok || versionNewer(a.Version, cur.Version) {
+ latest[a.Name] = a
+ }
+ }
+ }
+
+ out := make([]driver.Asset, 0, len(latest))
+ for _, a := range latest {
+ out = append(out, *cloneAsset(a))
+ }
+
+ return out, nil
+}
+
+// --- Datastores ---
+
+//nolint:gocritic,dupl // cfg matches driver sig; uniform child CRUD recurs across collections.
+func (m *Mock) CreateDatastore(_ context.Context, cfg driver.DatastoreConfig) (*driver.Datastore, error) {
+ if err := m.requireWorkspace(cfg.ResourceGroup, cfg.Workspace); err != nil {
+ return nil, err
+ }
+
+ if cfg.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "datastore name is required")
+ }
+
+ d := &driver.Datastore{
+ ID: m.wsChildID(cfg.ResourceGroup, cfg.Workspace, "datastores/"+cfg.Name), Name: cfg.Name,
+ StoreType: cfg.StoreType, AccountName: cfg.AccountName, Container: cfg.Container, CreatedAt: m.now(),
+ }
+ m.datastores.Set(key(cfg.ResourceGroup, cfg.Workspace, "datastores", cfg.Name), d)
+
+ out := *d
+
+ return &out, nil
+}
+
+func (m *Mock) GetDatastore(_ context.Context, resourceGroup, workspace, name string) (*driver.Datastore, error) {
+ d, ok := m.datastores.Get(key(resourceGroup, workspace, "datastores", name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "datastore %q not found", name)
+ }
+
+ out := *d
+
+ return &out, nil
+}
+
+func (m *Mock) DeleteDatastore(_ context.Context, resourceGroup, workspace, name string) error {
+ if !m.datastores.Delete(key(resourceGroup, workspace, "datastores", name)) {
+ return errors.Newf(errors.NotFound, "datastore %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListDatastores(_ context.Context, resourceGroup, workspace string) ([]driver.Datastore, error) {
+ prefix := key(resourceGroup, workspace, "datastores") + "/"
+ out := make([]driver.Datastore, 0)
+
+ for k, d := range m.datastores.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *d)
+ }
+ }
+
+ return out, nil
+}
+
+// --- Connections ---
+
+//nolint:gocritic,dupl // cfg matches driver sig; uniform child CRUD recurs across collections.
+func (m *Mock) CreateConnection(_ context.Context, cfg driver.ConnectionConfig) (*driver.Connection, error) {
+ if err := m.requireWorkspace(cfg.ResourceGroup, cfg.Workspace); err != nil {
+ return nil, err
+ }
+
+ if cfg.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "connection name is required")
+ }
+
+ c := &driver.Connection{
+ ID: m.wsChildID(cfg.ResourceGroup, cfg.Workspace, "connections/"+cfg.Name), Name: cfg.Name,
+ Category: cfg.Category, Target: cfg.Target, AuthType: cfg.AuthType, CreatedAt: m.now(),
+ }
+ m.connections.Set(key(cfg.ResourceGroup, cfg.Workspace, "connections", cfg.Name), c)
+
+ out := *c
+
+ return &out, nil
+}
+
+func (m *Mock) GetConnection(_ context.Context, resourceGroup, workspace, name string) (*driver.Connection, error) {
+ c, ok := m.connections.Get(key(resourceGroup, workspace, "connections", name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "connection %q not found", name)
+ }
+
+ out := *c
+
+ return &out, nil
+}
+
+func (m *Mock) DeleteConnection(_ context.Context, resourceGroup, workspace, name string) error {
+ if !m.connections.Delete(key(resourceGroup, workspace, "connections", name)) {
+ return errors.Newf(errors.NotFound, "connection %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListConnections(_ context.Context, resourceGroup, workspace string) ([]driver.Connection, error) {
+ prefix := key(resourceGroup, workspace, "connections") + "/"
+ out := make([]driver.Connection, 0)
+
+ for k, c := range m.connections.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *c)
+ }
+ }
+
+ return out, nil
+}
+
+// --- Schedules ---
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateMLSchedule(_ context.Context, cfg driver.MLScheduleConfig) (*driver.MLSchedule, error) {
+ if err := m.requireWorkspace(cfg.ResourceGroup, cfg.Workspace); err != nil {
+ return nil, err
+ }
+
+ if cfg.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "schedule name is required")
+ }
+
+ s := &driver.MLSchedule{
+ ID: m.wsChildID(cfg.ResourceGroup, cfg.Workspace, "schedules/"+cfg.Name), Name: cfg.Name,
+ Cron: cfg.Cron, DisplayName: cfg.DisplayName, IsEnabled: true, CreatedAt: m.now(),
+ }
+ m.mlSchedules.Set(key(cfg.ResourceGroup, cfg.Workspace, "schedules", cfg.Name), s)
+
+ out := *s
+
+ return &out, nil
+}
+
+func (m *Mock) GetMLSchedule(_ context.Context, resourceGroup, workspace, name string) (*driver.MLSchedule, error) {
+ s, ok := m.mlSchedules.Get(key(resourceGroup, workspace, "schedules", name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "schedule %q not found", name)
+ }
+
+ out := *s
+
+ return &out, nil
+}
+
+func (m *Mock) DeleteMLSchedule(_ context.Context, resourceGroup, workspace, name string) error {
+ if !m.mlSchedules.Delete(key(resourceGroup, workspace, "schedules", name)) {
+ return errors.Newf(errors.NotFound, "schedule %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListMLSchedules(_ context.Context, resourceGroup, workspace string) ([]driver.MLSchedule, error) {
+ prefix := key(resourceGroup, workspace, "schedules") + "/"
+ out := make([]driver.MLSchedule, 0)
+
+ for k, s := range m.mlSchedules.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *s)
+ }
+ }
+
+ return out, nil
+}
+
+// --- Registries ---
+
+func cloneRegistry(r *driver.Registry) *driver.Registry {
+ out := *r
+ out.Tags = copyMap(r.Tags)
+
+ return &out
+}
+
+func (m *Mock) CreateRegistry(_ context.Context, cfg driver.RegistryConfig) (*driver.Registry, error) {
+ switch {
+ case cfg.Name == "":
+ return nil, errors.New(errors.InvalidArgument, "registry name is required")
+ case cfg.ResourceGroup == "":
+ return nil, errors.New(errors.InvalidArgument, "resource group is required")
+ case cfg.Location == "":
+ return nil, errors.New(errors.InvalidArgument, "location is required")
+ }
+
+ r := &driver.Registry{
+ ID: m.mlID(cfg.ResourceGroup, "registries", cfg.Name), Name: cfg.Name, Location: cfg.Location,
+ Description: cfg.Description, ProvisioningState: driver.StateSucceeded,
+ Tags: copyMap(cfg.Tags), CreatedAt: m.now(),
+ }
+ m.registries.Set(key(cfg.ResourceGroup, cfg.Name), r)
+
+ return cloneRegistry(r), nil
+}
+
+func (m *Mock) GetRegistry(_ context.Context, resourceGroup, name string) (*driver.Registry, error) {
+ r, ok := m.registries.Get(key(resourceGroup, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "registry %q not found", name)
+ }
+
+ return cloneRegistry(r), nil
+}
+
+func (m *Mock) DeleteRegistry(_ context.Context, resourceGroup, name string) error {
+ if !m.registries.Delete(key(resourceGroup, name)) {
+ return errors.Newf(errors.NotFound, "registry %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListRegistries(_ context.Context, resourceGroup string) ([]driver.Registry, error) {
+ out := make([]driver.Registry, 0)
+
+ for _, r := range m.registries.All() {
+ if strings.HasPrefix(r.ID, "/subscriptions/"+m.opts.AccountID+"/resourceGroups/"+resourceGroup+"/") {
+ out = append(out, *cloneRegistry(r))
+ }
+ }
+
+ return out, nil
+}
diff --git a/providers/azure/azurecache/azurecache.go b/providers/azure/azurecache/azurecache.go
index 62d610d..eed6702 100644
--- a/providers/azure/azurecache/azurecache.go
+++ b/providers/azure/azurecache/azurecache.go
@@ -8,11 +8,11 @@ import (
"strconv"
"time"
- "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/cache/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const defaultRedisSSLPort = 6380
diff --git a/providers/azure/azurecache/azurecache_test.go b/providers/azure/azurecache/azurecache_test.go
index 3da43c5..5552bbe 100644
--- a/providers/azure/azurecache/azurecache_test.go
+++ b/providers/azure/azurecache/azurecache_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/cache/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/azure/azuredns/dns.go b/providers/azure/azuredns/dns.go
index ea522c1..3dd36da 100644
--- a/providers/azure/azuredns/dns.go
+++ b/providers/azure/azuredns/dns.go
@@ -5,11 +5,11 @@ import (
"context"
"strings"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/dns/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
// Compile-time check that Mock implements driver.DNS.
diff --git a/providers/azure/azuredns/dns_test.go b/providers/azure/azuredns/dns_test.go
index 106a105..04276b3 100644
--- a/providers/azure/azuredns/dns_test.go
+++ b/providers/azure/azuredns/dns_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/dns/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/dns/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/azure/azuredns/healthcheck.go b/providers/azure/azuredns/healthcheck.go
index 486ed56..c1e40ca 100644
--- a/providers/azure/azuredns/healthcheck.go
+++ b/providers/azure/azuredns/healthcheck.go
@@ -3,9 +3,9 @@ package azuredns
import (
"context"
- "github.com/stackshy/cloudemu/dns/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
const (
diff --git a/providers/azure/azureiam/iam.go b/providers/azure/azureiam/iam.go
index 4a944cd..1dafde8 100644
--- a/providers/azure/azureiam/iam.go
+++ b/providers/azure/azureiam/iam.go
@@ -8,11 +8,11 @@ import (
"strings"
"sync"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/iam/driver"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
const timeFormat = "2006-01-02T15:04:05Z"
diff --git a/providers/azure/azureiam/iam_test.go b/providers/azure/azureiam/iam_test.go
index 82c018e..e0c3a0a 100644
--- a/providers/azure/azureiam/iam_test.go
+++ b/providers/azure/azureiam/iam_test.go
@@ -6,9 +6,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/iam/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/iam/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/azure/azurelb/lb.go b/providers/azure/azurelb/lb.go
index 6542ce9..c257fe4 100644
--- a/providers/azure/azurelb/lb.go
+++ b/providers/azure/azurelb/lb.go
@@ -6,11 +6,11 @@ import (
"fmt"
"sync"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/loadbalancer/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
// Compile-time check that Mock implements driver.LoadBalancer.
diff --git a/providers/azure/azurelb/lb_test.go b/providers/azure/azurelb/lb_test.go
index 79328db..defc2f7 100644
--- a/providers/azure/azurelb/lb_test.go
+++ b/providers/azure/azurelb/lb_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/loadbalancer/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/azure/azuremonitor/monitor.go b/providers/azure/azuremonitor/monitor.go
index 9a1ba17..493c978 100644
--- a/providers/azure/azuremonitor/monitor.go
+++ b/providers/azure/azuremonitor/monitor.go
@@ -9,11 +9,11 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Compile-time check that Mock implements driver.Monitoring.
diff --git a/providers/azure/azuremonitor/monitor_test.go b/providers/azure/azuremonitor/monitor_test.go
index 15a3620..cd05637 100644
--- a/providers/azure/azuremonitor/monitor_test.go
+++ b/providers/azure/azuremonitor/monitor_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/azure/azuresearch/azuresearch.go b/providers/azure/azuresearch/azuresearch.go
new file mode 100644
index 0000000..610ee5a
--- /dev/null
+++ b/providers/azure/azuresearch/azuresearch.go
@@ -0,0 +1,271 @@
+// Package azuresearch provides an in-memory mock of Azure AI Search
+// (Microsoft.Search/searchServices) — the ARM control plane (service lifecycle,
+// admin/query keys, private links) and the search data plane (indexes,
+// documents, indexers, data sources, skillsets, synonym maps, aliases).
+package azuresearch
+
+import (
+ "context"
+ "fmt"
+ "hash/fnv"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+)
+
+// Compile-time check that Mock implements the full surface.
+var _ driver.AzureSearch = (*Mock)(nil)
+
+const (
+ searchProvider = "Microsoft.Search"
+ defaultSKU = "standard"
+)
+
+// Mock is the in-memory Azure AI Search service.
+type Mock struct {
+ services *memstore.Store[*driver.Service]
+ adminKeys *memstore.Store[*driver.AdminKeys]
+ queryKeys *memstore.Store[*driver.QueryKey]
+ sharedLinks *memstore.Store[*driver.SharedPrivateLink]
+ privateConns *memstore.Store[*driver.PrivateEndpointConnection]
+
+ // Data-plane stores (keyed by service[/index]/name).
+ indexes *memstore.Store[*driver.Index]
+ documents *memstore.Store[map[string]any]
+ indexers *memstore.Store[*driver.Indexer]
+ indexerRuns *memstore.Store[*driver.IndexerStatus]
+ dataSources *memstore.Store[*driver.DataSource]
+ skillsets *memstore.Store[*driver.Skillset]
+ synonymMaps *memstore.Store[*driver.SynonymMap]
+ aliases *memstore.Store[*driver.Alias]
+
+ seq atomic.Int64
+ opts *config.Options
+ monitoring mondriver.Monitoring
+}
+
+// New creates a new Azure AI Search mock.
+func New(opts *config.Options) *Mock {
+ return &Mock{
+ services: memstore.New[*driver.Service](),
+ adminKeys: memstore.New[*driver.AdminKeys](),
+ queryKeys: memstore.New[*driver.QueryKey](),
+ sharedLinks: memstore.New[*driver.SharedPrivateLink](),
+ privateConns: memstore.New[*driver.PrivateEndpointConnection](),
+ indexes: memstore.New[*driver.Index](),
+ documents: memstore.New[map[string]any](),
+ indexers: memstore.New[*driver.Indexer](),
+ indexerRuns: memstore.New[*driver.IndexerStatus](),
+ dataSources: memstore.New[*driver.DataSource](),
+ skillsets: memstore.New[*driver.Skillset](),
+ synonymMaps: memstore.New[*driver.SynonymMap](),
+ aliases: memstore.New[*driver.Alias](),
+ opts: opts,
+ }
+}
+
+// SetMonitoring wires a monitoring backend for auto-metric emission.
+func (m *Mock) SetMonitoring(mon mondriver.Monitoring) { m.monitoring = mon }
+
+// --- shared helpers ---
+
+func (m *Mock) now() string { return m.opts.Clock.Now().UTC().Format(time.RFC3339) }
+
+func key(parts ...string) string { return strings.Join(parts, "/") }
+
+func copyMap(in map[string]string) map[string]string {
+ if in == nil {
+ return nil
+ }
+
+ out := make(map[string]string, len(in))
+ for k, v := range in {
+ out[k] = v
+ }
+
+ return out
+}
+
+func (m *Mock) etag() string {
+ return fmt.Sprintf(`"%016x"`, m.seq.Add(1))
+}
+
+func (m *Mock) emitMetric(metricName string, value float64, dims map[string]string) {
+ if m.monitoring == nil {
+ return
+ }
+
+ _ = m.monitoring.PutMetricData(context.Background(), []mondriver.MetricDatum{{
+ Namespace: searchProvider, MetricName: metricName, Value: value,
+ Unit: "Count", Dimensions: dims, Timestamp: m.opts.Clock.Now(),
+ }})
+}
+
+func skuOrDefault(s string) string {
+ if s == "" {
+ return defaultSKU
+ }
+
+ return s
+}
+
+func hashHex(parts ...string) string {
+ h := fnv.New64a()
+ _, _ = h.Write([]byte(strings.Join(parts, ":")))
+ h2 := fnv.New64a()
+ _, _ = h2.Write([]byte(strings.Join(parts, ":") + "#k"))
+
+ return fmt.Sprintf("%016x%016x", h.Sum64(), h2.Sum64())
+}
+
+// --- Services ---
+
+func cloneService(s *driver.Service) *driver.Service {
+ out := *s
+ out.Tags = copyMap(s.Tags)
+
+ return &out
+}
+
+//nolint:gocritic // cfg matches the driver signature; copied once on entry.
+func (m *Mock) CreateService(_ context.Context, cfg driver.ServiceConfig) (*driver.Service, error) {
+ switch {
+ case cfg.Name == "":
+ return nil, errors.New(errors.InvalidArgument, "service name is required")
+ case cfg.ResourceGroup == "":
+ return nil, errors.New(errors.InvalidArgument, "resource group is required")
+ case cfg.Location == "":
+ return nil, errors.New(errors.InvalidArgument, "location is required")
+ }
+
+ k := key(cfg.ResourceGroup, cfg.Name)
+
+ replicas := cfg.ReplicaCount
+ if replicas < 1 {
+ replicas = 1
+ }
+
+ partitions := cfg.PartitionCount
+ if partitions < 1 {
+ partitions = 1
+ }
+
+ if existing, ok := m.services.Get(k); ok {
+ updated := *existing
+ updated.SKUName = skuOrDefault(cfg.SKUName)
+ updated.ReplicaCount = replicas
+ updated.PartitionCount = partitions
+ updated.Tags = copyMap(cfg.Tags)
+ m.services.Set(k, &updated)
+
+ return cloneService(&updated), nil
+ }
+
+ hosting := cfg.HostingMode
+ if hosting == "" {
+ hosting = "default"
+ }
+
+ svc := &driver.Service{
+ ID: idgen.AzureID(m.opts.AccountID, cfg.ResourceGroup, searchProvider, "searchServices", cfg.Name),
+ Name: cfg.Name,
+ ResourceGroup: cfg.ResourceGroup,
+ Location: cfg.Location,
+ SKUName: skuOrDefault(cfg.SKUName),
+ ReplicaCount: replicas,
+ PartitionCount: partitions,
+ HostingMode: hosting,
+ Endpoint: "https://" + cfg.Name + ".search.windows.net",
+ Status: driver.StatusRunning,
+ ProvisioningState: driver.StateSucceeded,
+ Tags: copyMap(cfg.Tags),
+ CreatedAt: m.now(),
+ }
+ m.services.Set(k, svc)
+ m.emitMetric("service/count", 1, map[string]string{"sku": svc.SKUName})
+
+ return cloneService(svc), nil
+}
+
+func (m *Mock) GetService(_ context.Context, resourceGroup, name string) (*driver.Service, error) {
+ s, ok := m.services.Get(key(resourceGroup, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "search service %q not found", name)
+ }
+
+ return cloneService(s), nil
+}
+
+func (m *Mock) DeleteService(_ context.Context, resourceGroup, name string) error {
+ if !m.services.Delete(key(resourceGroup, name)) {
+ return errors.Newf(errors.NotFound, "search service %q not found", name)
+ }
+
+ return nil
+}
+
+func (m *Mock) UpdateService(
+ _ context.Context, resourceGroup, name string, replicas, partitions int, tags map[string]string,
+) (*driver.Service, error) {
+ k := key(resourceGroup, name)
+
+ s, ok := m.services.Get(k)
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "search service %q not found", name)
+ }
+
+ updated := *s
+ if replicas > 0 {
+ updated.ReplicaCount = replicas
+ }
+
+ if partitions > 0 {
+ updated.PartitionCount = partitions
+ }
+
+ if tags != nil {
+ updated.Tags = copyMap(tags)
+ }
+
+ m.services.Set(k, &updated)
+
+ return cloneService(&updated), nil
+}
+
+func (m *Mock) ListServicesByResourceGroup(_ context.Context, resourceGroup string) ([]driver.Service, error) {
+ out := make([]driver.Service, 0)
+
+ for _, s := range m.services.All() {
+ if s.ResourceGroup == resourceGroup {
+ out = append(out, *cloneService(s))
+ }
+ }
+
+ return out, nil
+}
+
+func (m *Mock) ListServices(_ context.Context) ([]driver.Service, error) {
+ all := m.services.All()
+ out := make([]driver.Service, 0, len(all))
+
+ for _, s := range all {
+ out = append(out, *cloneService(s))
+ }
+
+ return out, nil
+}
+
+func (m *Mock) requireService(resourceGroup, name string) error {
+ if !m.services.Has(key(resourceGroup, name)) {
+ return errors.Newf(errors.NotFound, "search service %q not found", name)
+ }
+
+ return nil
+}
diff --git a/providers/azure/azuresearch/control_keys.go b/providers/azure/azuresearch/control_keys.go
new file mode 100644
index 0000000..89686ce
--- /dev/null
+++ b/providers/azure/azuresearch/control_keys.go
@@ -0,0 +1,227 @@
+package azuresearch
+
+import (
+ "context"
+ "strconv"
+ "strings"
+
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+// svcChildID builds the ARM ID for a resource nested under a search service.
+func (m *Mock) svcChildID(resourceGroup, service, childPath string) string {
+ if s, ok := m.services.Get(key(resourceGroup, service)); ok {
+ return s.ID + "/" + childPath
+ }
+
+ return ""
+}
+
+// --- Admin & query keys ---
+
+// currentAdminKeys returns the persisted admin keys, or the deterministic
+// defaults when the service has never had a key regenerated.
+func (m *Mock) currentAdminKeys(resourceGroup, name string) *driver.AdminKeys {
+ if k, ok := m.adminKeys.Get(key(resourceGroup, name)); ok {
+ out := *k
+
+ return &out
+ }
+
+ return &driver.AdminKeys{
+ Primary: hashHex(resourceGroup, name, "admin-primary"),
+ Secondary: hashHex(resourceGroup, name, "admin-secondary"),
+ }
+}
+
+func (m *Mock) ListAdminKeys(_ context.Context, resourceGroup, name string) (*driver.AdminKeys, error) {
+ if err := m.requireService(resourceGroup, name); err != nil {
+ return nil, err
+ }
+
+ return m.currentAdminKeys(resourceGroup, name), nil
+}
+
+func (m *Mock) RegenerateAdminKey(_ context.Context, resourceGroup, name, which string) (*driver.AdminKeys, error) {
+ if err := m.requireService(resourceGroup, name); err != nil {
+ return nil, err
+ }
+
+ keys := m.currentAdminKeys(resourceGroup, name)
+
+ // Salt with a monotonic counter so each regeneration yields a distinct key
+ // and the rotation is observable via a subsequent ListAdminKeys.
+ salt := "regen-" + strconv.FormatInt(m.seq.Add(1), 16)
+ if strings.EqualFold(which, "secondary") {
+ keys.Secondary = hashHex(resourceGroup, name, "admin-secondary", salt)
+ } else {
+ keys.Primary = hashHex(resourceGroup, name, "admin-primary", salt)
+ }
+
+ m.adminKeys.Set(key(resourceGroup, name), keys)
+
+ out := *keys
+
+ return &out, nil
+}
+
+func (m *Mock) ListQueryKeys(_ context.Context, resourceGroup, name string) ([]driver.QueryKey, error) {
+ if err := m.requireService(resourceGroup, name); err != nil {
+ return nil, err
+ }
+
+ prefix := key(resourceGroup, name) + "/"
+ out := []driver.QueryKey{{Name: "", Key: hashHex(resourceGroup, name, "query-default")}}
+
+ for k, qk := range m.queryKeys.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *qk)
+ }
+ }
+
+ return out, nil
+}
+
+func (m *Mock) CreateQueryKey(_ context.Context, resourceGroup, name, keyName string) (*driver.QueryKey, error) {
+ if err := m.requireService(resourceGroup, name); err != nil {
+ return nil, err
+ }
+
+ qk := &driver.QueryKey{Name: keyName, Key: hashHex(resourceGroup, name, "query", keyName)}
+ m.queryKeys.Set(key(resourceGroup, name, qk.Key), qk)
+
+ out := *qk
+
+ return &out, nil
+}
+
+func (m *Mock) DeleteQueryKey(_ context.Context, resourceGroup, name, queryKey string) error {
+ if err := m.requireService(resourceGroup, name); err != nil {
+ return err
+ }
+
+ if !m.queryKeys.Delete(key(resourceGroup, name, queryKey)) {
+ return errors.Newf(errors.NotFound, "query key not found")
+ }
+
+ return nil
+}
+
+// --- Shared private links ---
+
+func (m *Mock) PutSharedPrivateLink(
+ _ context.Context, resourceGroup, name, linkName, groupID, privateLinkID string,
+) (*driver.SharedPrivateLink, error) {
+ if err := m.requireService(resourceGroup, name); err != nil {
+ return nil, err
+ }
+
+ l := &driver.SharedPrivateLink{
+ ID: m.svcChildID(resourceGroup, name, "sharedPrivateLinkResources/"+linkName),
+ Name: linkName,
+ GroupID: groupID,
+ PrivateLinkID: privateLinkID,
+ Status: "Pending",
+ ProvisioningState: driver.StateSucceeded,
+ }
+ m.sharedLinks.Set(key(resourceGroup, name, "spl", linkName), l)
+
+ out := *l
+
+ return &out, nil
+}
+
+func (m *Mock) GetSharedPrivateLink(_ context.Context, resourceGroup, name, linkName string) (*driver.SharedPrivateLink, error) {
+ l, ok := m.sharedLinks.Get(key(resourceGroup, name, "spl", linkName))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "shared private link %q not found", linkName)
+ }
+
+ out := *l
+
+ return &out, nil
+}
+
+func (m *Mock) DeleteSharedPrivateLink(_ context.Context, resourceGroup, name, linkName string) error {
+ if !m.sharedLinks.Delete(key(resourceGroup, name, "spl", linkName)) {
+ return errors.Newf(errors.NotFound, "shared private link %q not found", linkName)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListSharedPrivateLinks(_ context.Context, resourceGroup, name string) ([]driver.SharedPrivateLink, error) {
+ prefix := key(resourceGroup, name, "spl") + "/"
+ out := make([]driver.SharedPrivateLink, 0)
+
+ for k, l := range m.sharedLinks.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *l)
+ }
+ }
+
+ return out, nil
+}
+
+// --- Private endpoint connections ---
+
+func (m *Mock) PutPrivateEndpointConnection(
+ _ context.Context, resourceGroup, name, connName, status string,
+) (*driver.PrivateEndpointConnection, error) {
+ if err := m.requireService(resourceGroup, name); err != nil {
+ return nil, err
+ }
+
+ if status == "" {
+ status = "Approved"
+ }
+
+ c := &driver.PrivateEndpointConnection{
+ ID: m.svcChildID(resourceGroup, name, "privateEndpointConnections/"+connName),
+ Name: connName,
+ Status: status,
+ ProvisioningState: driver.StateSucceeded,
+ }
+ m.privateConns.Set(key(resourceGroup, name, "pec", connName), c)
+
+ out := *c
+
+ return &out, nil
+}
+
+func (m *Mock) GetPrivateEndpointConnection(
+ _ context.Context, resourceGroup, name, connName string,
+) (*driver.PrivateEndpointConnection, error) {
+ c, ok := m.privateConns.Get(key(resourceGroup, name, "pec", connName))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "private endpoint connection %q not found", connName)
+ }
+
+ out := *c
+
+ return &out, nil
+}
+
+func (m *Mock) DeletePrivateEndpointConnection(_ context.Context, resourceGroup, name, connName string) error {
+ if !m.privateConns.Delete(key(resourceGroup, name, "pec", connName)) {
+ return errors.Newf(errors.NotFound, "private endpoint connection %q not found", connName)
+ }
+
+ return nil
+}
+
+func (m *Mock) ListPrivateEndpointConnections(
+ _ context.Context, resourceGroup, name string,
+) ([]driver.PrivateEndpointConnection, error) {
+ prefix := key(resourceGroup, name, "pec") + "/"
+ out := make([]driver.PrivateEndpointConnection, 0)
+
+ for k, c := range m.privateConns.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *c)
+ }
+ }
+
+ return out, nil
+}
diff --git a/providers/azure/azuresearch/dataplane.go b/providers/azure/azuresearch/dataplane.go
new file mode 100644
index 0000000..ead5e81
--- /dev/null
+++ b/providers/azure/azuresearch/dataplane.go
@@ -0,0 +1,508 @@
+package azuresearch
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+// maxSearchTop bounds the number of documents a single search returns so an
+// unvalidated request can't trigger an unbounded response.
+const maxSearchTop = 1000
+
+// --- Indexes ---
+
+func cloneIndex(i *driver.Index) *driver.Index {
+ out := *i
+ out.Fields = append([]driver.Field(nil), i.Fields...)
+
+ return &out
+}
+
+func (m *Mock) CreateOrUpdateIndex(_ context.Context, service string, idx driver.Index) (*driver.Index, error) {
+ if idx.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "index name is required")
+ }
+
+ stored := cloneIndex(&idx)
+ stored.ETag = m.etag()
+ m.indexes.Set(key(service, idx.Name), stored)
+ m.emitMetric("index/count", 1, map[string]string{"service": service})
+
+ return cloneIndex(stored), nil
+}
+
+func (m *Mock) GetIndex(_ context.Context, service, name string) (*driver.Index, error) {
+ i, ok := m.indexes.Get(key(service, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "index %q not found", name)
+ }
+
+ return cloneIndex(i), nil
+}
+
+func (m *Mock) ListIndexes(_ context.Context, service string) ([]driver.Index, error) {
+ prefix := service + "/"
+ out := make([]driver.Index, 0)
+
+ for k, i := range m.indexes.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, *cloneIndex(i))
+ }
+ }
+
+ return out, nil
+}
+
+func (m *Mock) DeleteIndex(_ context.Context, service, name string) error {
+ if !m.indexes.Delete(key(service, name)) {
+ return errors.Newf(errors.NotFound, "index %q not found", name)
+ }
+
+ // Drop the index's documents too.
+ docPrefix := key(service, name) + "/"
+ for k := range m.documents.All() {
+ if strings.HasPrefix(k, docPrefix) {
+ m.documents.Delete(k)
+ }
+ }
+
+ return nil
+}
+
+// keyField returns the name of the index's key field (defaults to "id").
+func (m *Mock) keyField(service, index string) string {
+ if idx, ok := m.indexes.Get(key(service, index)); ok {
+ for _, f := range idx.Fields {
+ if f.Key {
+ return f.Name
+ }
+ }
+ }
+
+ return "id"
+}
+
+// --- Documents ---
+
+func (m *Mock) IndexDocuments(_ context.Context, service, index string, actions []driver.IndexAction) ([]driver.IndexResult, error) {
+ if !m.indexes.Has(key(service, index)) {
+ return nil, errors.Newf(errors.NotFound, "index %q not found", index)
+ }
+
+ kf := m.keyField(service, index)
+ results := make([]driver.IndexResult, 0, len(actions))
+
+ for _, act := range actions {
+ kv, hasKey := act.Document[kf]
+ docKey := fmt.Sprintf("%v", kv)
+
+ if !hasKey || docKey == "" {
+ results = append(results, driver.IndexResult{
+ Key: docKey, Status: false, StatusCode: 400, ErrorMsg: "document key field " + kf + " is missing or empty",
+ })
+
+ continue
+ }
+
+ storeKey := key(service, index, docKey)
+
+ switch act.Action {
+ case "delete":
+ // Delete is idempotent in Azure AI Search: removing an absent key returns 200.
+ m.documents.Delete(storeKey)
+ case "merge":
+ if !m.mergeInto(storeKey, act.Document) {
+ results = append(results, driver.IndexResult{
+ Key: docKey, Status: false, StatusCode: 404, ErrorMsg: "document not found for merge",
+ })
+
+ continue
+ }
+ case "mergeOrUpload":
+ // Merge onto an existing doc, preserving unspecified fields; insert otherwise.
+ if !m.mergeInto(storeKey, act.Document) {
+ m.documents.Set(storeKey, copyDoc(act.Document))
+ }
+ default: // upload — full replace.
+ m.documents.Set(storeKey, copyDoc(act.Document))
+ }
+
+ results = append(results, driver.IndexResult{Key: docKey, Status: true, StatusCode: 200})
+ }
+
+ m.emitMetric("document/indexed", float64(len(actions)), map[string]string{"index": index})
+
+ return results, nil
+}
+
+// mergeInto merges src onto the stored doc at storeKey, preserving fields absent
+// from src. Returns false if no doc exists at storeKey.
+func (m *Mock) mergeInto(storeKey string, src map[string]any) bool {
+ existing, ok := m.documents.Get(storeKey)
+ if !ok {
+ return false
+ }
+
+ merged := copyDoc(existing)
+ for k, v := range src {
+ merged[k] = v
+ }
+
+ m.documents.Set(storeKey, merged)
+
+ return true
+}
+
+func copyDoc(in map[string]any) map[string]any {
+ out := make(map[string]any, len(in))
+ for k, v := range in {
+ out[k] = v
+ }
+
+ return out
+}
+
+//nolint:gocritic // req matches the driver signature; copied on entry.
+func (m *Mock) SearchDocuments(_ context.Context, service, index string, req driver.SearchRequest) (*driver.SearchResponse, error) {
+ if !m.indexes.Has(key(service, index)) {
+ return nil, errors.Newf(errors.NotFound, "index %q not found", index)
+ }
+
+ filter, err := parseFilter(req.Filter)
+ if err != nil {
+ return nil, err
+ }
+
+ docs := m.indexDocs(service, index)
+
+ // Naive full-text match: keep docs whose any string field contains the
+ // (case-insensitive) search term; "" or "*" matches everything. The optional
+ // $filter comparison is applied on top.
+ term := strings.ToLower(strings.TrimSpace(req.Search))
+ matched := make([]driver.SearchResult, 0, len(docs))
+
+ for _, d := range docs {
+ if term != "" && term != "*" && !docContains(d, term) {
+ continue
+ }
+
+ if !filter.match(d) {
+ continue
+ }
+
+ matched = append(matched, driver.SearchResult{Score: 1.0, Document: d})
+ }
+
+ orderResults(matched, req.OrderBy, m.keyField(service, index))
+
+ resp := &driver.SearchResponse{Count: -1}
+ if req.Count {
+ resp.Count = int64(len(matched))
+ }
+
+ // Paginate first, then project $select onto the returned page.
+ page := paginate(matched, req.Skip, req.Top)
+ for i := range page {
+ page[i].Document = applySelect(page[i].Document, req.Select)
+ }
+
+ resp.Results = page
+
+ m.emitMetric("query/count", 1, map[string]string{"index": index})
+
+ return resp, nil
+}
+
+func (m *Mock) indexDocs(service, index string) []map[string]any {
+ prefix := key(service, index) + "/"
+ out := make([]map[string]any, 0)
+
+ for k, d := range m.documents.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, copyDoc(d))
+ }
+ }
+
+ return out
+}
+
+func docContains(d map[string]any, term string) bool {
+ for _, v := range d {
+ if s, ok := v.(string); ok && strings.Contains(strings.ToLower(s), term) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func applySelect(d map[string]any, sel []string) map[string]any {
+ if len(sel) == 0 {
+ return copyDoc(d)
+ }
+
+ out := make(map[string]any, len(sel))
+
+ for _, f := range sel {
+ if v, ok := d[f]; ok {
+ out[f] = v
+ }
+ }
+
+ return out
+}
+
+// orderResults sorts rs by the $orderby clause ("field [asc|desc]"), falling
+// back to the index key field (ascending) when no clause is given. Only the
+// first clause is honored.
+func orderResults(rs []driver.SearchResult, orderBy, keyField string) {
+ field, desc := keyField, false
+
+ if ob := strings.TrimSpace(orderBy); ob != "" {
+ firstClause, _, _ := strings.Cut(ob, ",")
+
+ clause := strings.Fields(firstClause)
+ if len(clause) > 0 {
+ field = clause[0]
+ }
+
+ if len(clause) > 1 && strings.EqualFold(clause[1], "desc") {
+ desc = true
+ }
+ }
+
+ sort.SliceStable(rs, func(i, j int) bool {
+ c := compareAny(rs[i].Document[field], rs[j].Document[field])
+ if desc {
+ return c > 0
+ }
+
+ return c < 0
+ })
+}
+
+// compareAny orders two document values numerically when both are numbers and
+// lexically otherwise. Returns -1, 0, or 1.
+func compareAny(a, b any) int {
+ af, aok := toFloat(a)
+ bf, bok := toFloat(b)
+
+ if aok && bok {
+ switch {
+ case af < bf:
+ return -1
+ case af > bf:
+ return 1
+ default:
+ return 0
+ }
+ }
+
+ return strings.Compare(fmt.Sprintf("%v", a), fmt.Sprintf("%v", b))
+}
+
+func toFloat(v any) (float64, bool) {
+ switch n := v.(type) {
+ case float64:
+ return n, true
+ case float32:
+ return float64(n), true
+ case int:
+ return float64(n), true
+ case int64:
+ return float64(n), true
+ case json.Number:
+ f, err := n.Float64()
+ return f, err == nil
+ default:
+ return 0, false
+ }
+}
+
+// docFilter is a single OData comparison clause: field op value.
+type docFilter struct {
+ field string
+ op string
+ value any // float64 or string
+}
+
+// filterOps maps each supported OData comparison operator to a predicate over
+// the result of compareAny (-1, 0, or 1).
+//
+//nolint:gochecknoglobals // immutable operator table
+var filterOps = map[string]func(int) bool{
+ "eq": func(c int) bool { return c == 0 },
+ "ne": func(c int) bool { return c != 0 },
+ "gt": func(c int) bool { return c > 0 },
+ "ge": func(c int) bool { return c >= 0 },
+ "lt": func(c int) bool { return c < 0 },
+ "le": func(c int) bool { return c <= 0 },
+}
+
+// parseFilter parses a single OData comparison ("price gt 100", "name eq 'blue'").
+// Boolean combinations and functions are unsupported and rejected rather than
+// silently ignored. An empty filter yields a nil (match-everything) filter.
+func parseFilter(expr string) (*docFilter, error) {
+ expr = strings.TrimSpace(expr)
+ if expr == "" {
+ return nil, nil //nolint:nilnil // nil filter means "match everything".
+ }
+
+ toks := strings.Fields(expr)
+
+ const minTokens = 3
+ if len(toks) < minTokens {
+ return nil, errors.Newf(errors.InvalidArgument, "unsupported filter expression %q", expr)
+ }
+
+ field, op := toks[0], strings.ToLower(toks[1])
+ if _, ok := filterOps[op]; !ok {
+ return nil, errors.Newf(errors.InvalidArgument, "unsupported filter operator %q", op)
+ }
+
+ f := &docFilter{field: field, op: op}
+ rawVal := strings.Join(toks[2:], " ")
+
+ switch {
+ case strings.HasPrefix(rawVal, "'"):
+ if !strings.HasSuffix(rawVal, "'") || len(rawVal) < 2 || strings.Contains(rawVal[1:len(rawVal)-1], "'") {
+ return nil, errors.Newf(errors.InvalidArgument, "unsupported filter expression %q", expr)
+ }
+
+ f.value = rawVal[1 : len(rawVal)-1]
+ case len(toks) > minTokens:
+ return nil, errors.Newf(errors.InvalidArgument, "only single comparisons are supported: %q", expr)
+ default:
+ if n, err := strconv.ParseFloat(rawVal, 64); err == nil {
+ f.value = n
+ } else {
+ f.value = rawVal
+ }
+ }
+
+ return f, nil
+}
+
+// match reports whether doc satisfies the filter; a nil filter matches all.
+func (f *docFilter) match(doc map[string]any) bool {
+ if f == nil {
+ return true
+ }
+
+ v, ok := doc[f.field]
+ if !ok {
+ return false
+ }
+
+ pred, ok := filterOps[f.op]
+ if !ok {
+ return false
+ }
+
+ return pred(compareAny(v, f.value))
+}
+
+func paginate(rs []driver.SearchResult, skip, top int) []driver.SearchResult {
+ if skip < 0 {
+ skip = 0
+ }
+
+ if skip >= len(rs) {
+ return []driver.SearchResult{}
+ }
+
+ rs = rs[skip:]
+
+ if top <= 0 || top > maxSearchTop {
+ top = maxSearchTop
+ }
+
+ if top < len(rs) {
+ rs = rs[:top]
+ }
+
+ return rs
+}
+
+func (m *Mock) SuggestDocuments(
+ _ context.Context, service, index, searchText, _ string, top int,
+) ([]driver.SuggestResult, error) {
+ if !m.indexes.Has(key(service, index)) {
+ return nil, errors.Newf(errors.NotFound, "index %q not found", index)
+ }
+
+ term := strings.ToLower(strings.TrimSpace(searchText))
+ out := make([]driver.SuggestResult, 0)
+
+ for _, d := range m.indexDocs(service, index) {
+ if text, ok := firstStringContaining(d, term); ok {
+ out = append(out, driver.SuggestResult{Text: text, Document: d})
+ }
+
+ if top > 0 && len(out) >= top {
+ break
+ }
+ }
+
+ return out, nil
+}
+
+func (m *Mock) AutocompleteDocuments(
+ _ context.Context, service, index, searchText, _ string, top int,
+) ([]string, error) {
+ if !m.indexes.Has(key(service, index)) {
+ return nil, errors.Newf(errors.NotFound, "index %q not found", index)
+ }
+
+ term := strings.ToLower(strings.TrimSpace(searchText))
+ seen := make(map[string]bool)
+ out := make([]string, 0)
+
+ for _, d := range m.indexDocs(service, index) {
+ if text, ok := firstStringContaining(d, term); ok && !seen[text] {
+ seen[text] = true
+
+ out = append(out, text)
+ }
+
+ if top > 0 && len(out) >= top {
+ break
+ }
+ }
+
+ return out, nil
+}
+
+func firstStringContaining(d map[string]any, term string) (string, bool) {
+ for _, v := range d {
+ if s, ok := v.(string); ok && (term == "" || strings.Contains(strings.ToLower(s), term)) {
+ return s, true
+ }
+ }
+
+ return "", false
+}
+
+func (m *Mock) CountDocuments(_ context.Context, service, index string) (int64, error) {
+ if !m.indexes.Has(key(service, index)) {
+ return 0, errors.Newf(errors.NotFound, "index %q not found", index)
+ }
+
+ return int64(len(m.indexDocs(service, index))), nil
+}
+
+func (m *Mock) GetDocument(_ context.Context, service, index, docKey string) (map[string]any, error) {
+ d, ok := m.documents.Get(key(service, index, docKey))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "document %q not found", docKey)
+ }
+
+ return copyDoc(d), nil
+}
diff --git a/providers/azure/azuresearch/dataplane_more.go b/providers/azure/azuresearch/dataplane_more.go
new file mode 100644
index 0000000..9b3cf49
--- /dev/null
+++ b/providers/azure/azuresearch/dataplane_more.go
@@ -0,0 +1,314 @@
+package azuresearch
+
+import (
+ "context"
+ "strings"
+
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+// childList collects values whose store key is under service+"/".
+func childKeysUnder[V any](store interface{ All() map[string]V }, service string) []V {
+ prefix := service + "/"
+ out := make([]V, 0)
+
+ for k, v := range store.All() {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, v)
+ }
+ }
+
+ return out
+}
+
+// --- Indexers ---
+
+//nolint:gocritic // cfg matches the driver signature; copied on entry.
+func (m *Mock) CreateOrUpdateIndexer(_ context.Context, service string, cfg driver.IndexerConfig) (*driver.Indexer, error) {
+ if cfg.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "indexer name is required")
+ }
+
+ ix := &driver.Indexer{
+ Name: cfg.Name, DataSourceName: cfg.DataSourceName, TargetIndex: cfg.TargetIndex,
+ SkillsetName: cfg.SkillsetName, Schedule: cfg.Schedule, ETag: m.etag(),
+ }
+ m.indexers.Set(key(service, cfg.Name), ix)
+ m.indexerRuns.Set(key(service, cfg.Name), &driver.IndexerStatus{Name: cfg.Name, Status: "success", LastResult: "success"})
+
+ out := *ix
+
+ return &out, nil
+}
+
+func (m *Mock) GetIndexer(_ context.Context, service, name string) (*driver.Indexer, error) {
+ ix, ok := m.indexers.Get(key(service, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "indexer %q not found", name)
+ }
+
+ out := *ix
+
+ return &out, nil
+}
+
+func (m *Mock) ListIndexers(_ context.Context, service string) ([]driver.Indexer, error) {
+ out := make([]driver.Indexer, 0)
+ for _, ix := range childKeysUnder[*driver.Indexer](m.indexers, service) {
+ out = append(out, *ix)
+ }
+
+ return out, nil
+}
+
+func (m *Mock) DeleteIndexer(_ context.Context, service, name string) error {
+ if !m.indexers.Delete(key(service, name)) {
+ return errors.Newf(errors.NotFound, "indexer %q not found", name)
+ }
+
+ m.indexerRuns.Delete(key(service, name))
+
+ return nil
+}
+
+func (m *Mock) RunIndexer(_ context.Context, service, name string) error {
+ return m.setIndexerStatus(service, name, "running")
+}
+
+func (m *Mock) ResetIndexer(_ context.Context, service, name string) error {
+ return m.setIndexerStatus(service, name, "reset")
+}
+
+func (m *Mock) setIndexerStatus(service, name, status string) error {
+ if !m.indexers.Has(key(service, name)) {
+ return errors.Newf(errors.NotFound, "indexer %q not found", name)
+ }
+
+ m.indexerRuns.Set(key(service, name), &driver.IndexerStatus{Name: name, Status: status, LastResult: status})
+
+ return nil
+}
+
+func (m *Mock) GetIndexerStatus(_ context.Context, service, name string) (*driver.IndexerStatus, error) {
+ st, ok := m.indexerRuns.Get(key(service, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "indexer %q not found", name)
+ }
+
+ out := *st
+
+ return &out, nil
+}
+
+// --- Data sources ---
+
+//nolint:gocritic // ds matches the driver signature; copied on entry.
+func (m *Mock) CreateOrUpdateDataSource(_ context.Context, service string, ds driver.DataSource) (*driver.DataSource, error) {
+ if ds.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "data source name is required")
+ }
+
+ stored := ds
+ stored.ETag = m.etag()
+ m.dataSources.Set(key(service, ds.Name), &stored)
+
+ out := stored
+
+ return &out, nil
+}
+
+func (m *Mock) GetDataSource(_ context.Context, service, name string) (*driver.DataSource, error) {
+ ds, ok := m.dataSources.Get(key(service, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "data source %q not found", name)
+ }
+
+ out := *ds
+
+ return &out, nil
+}
+
+func (m *Mock) ListDataSources(_ context.Context, service string) ([]driver.DataSource, error) {
+ out := make([]driver.DataSource, 0)
+ for _, ds := range childKeysUnder[*driver.DataSource](m.dataSources, service) {
+ out = append(out, *ds)
+ }
+
+ return out, nil
+}
+
+func (m *Mock) DeleteDataSource(_ context.Context, service, name string) error {
+ if !m.dataSources.Delete(key(service, name)) {
+ return errors.Newf(errors.NotFound, "data source %q not found", name)
+ }
+
+ return nil
+}
+
+// --- Skillsets ---
+
+func (m *Mock) CreateOrUpdateSkillset(_ context.Context, service string, sk driver.Skillset) (*driver.Skillset, error) {
+ if sk.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "skillset name is required")
+ }
+
+ stored := sk
+ stored.ETag = m.etag()
+ m.skillsets.Set(key(service, sk.Name), &stored)
+
+ out := stored
+
+ return &out, nil
+}
+
+func (m *Mock) GetSkillset(_ context.Context, service, name string) (*driver.Skillset, error) {
+ sk, ok := m.skillsets.Get(key(service, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "skillset %q not found", name)
+ }
+
+ out := *sk
+
+ return &out, nil
+}
+
+func (m *Mock) ListSkillsets(_ context.Context, service string) ([]driver.Skillset, error) {
+ out := make([]driver.Skillset, 0)
+ for _, sk := range childKeysUnder[*driver.Skillset](m.skillsets, service) {
+ out = append(out, *sk)
+ }
+
+ return out, nil
+}
+
+func (m *Mock) DeleteSkillset(_ context.Context, service, name string) error {
+ if !m.skillsets.Delete(key(service, name)) {
+ return errors.Newf(errors.NotFound, "skillset %q not found", name)
+ }
+
+ return nil
+}
+
+// --- Synonym maps ---
+
+func (m *Mock) CreateOrUpdateSynonymMap(_ context.Context, service string, sm driver.SynonymMap) (*driver.SynonymMap, error) {
+ if sm.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "synonym map name is required")
+ }
+
+ stored := sm
+ if stored.Format == "" {
+ stored.Format = "solr"
+ }
+
+ stored.ETag = m.etag()
+ m.synonymMaps.Set(key(service, sm.Name), &stored)
+
+ out := stored
+
+ return &out, nil
+}
+
+func (m *Mock) GetSynonymMap(_ context.Context, service, name string) (*driver.SynonymMap, error) {
+ sm, ok := m.synonymMaps.Get(key(service, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "synonym map %q not found", name)
+ }
+
+ out := *sm
+
+ return &out, nil
+}
+
+func (m *Mock) ListSynonymMaps(_ context.Context, service string) ([]driver.SynonymMap, error) {
+ out := make([]driver.SynonymMap, 0)
+ for _, sm := range childKeysUnder[*driver.SynonymMap](m.synonymMaps, service) {
+ out = append(out, *sm)
+ }
+
+ return out, nil
+}
+
+func (m *Mock) DeleteSynonymMap(_ context.Context, service, name string) error {
+ if !m.synonymMaps.Delete(key(service, name)) {
+ return errors.Newf(errors.NotFound, "synonym map %q not found", name)
+ }
+
+ return nil
+}
+
+// --- Aliases ---
+
+func (m *Mock) CreateOrUpdateAlias(_ context.Context, service string, alias driver.Alias) (*driver.Alias, error) {
+ if alias.Name == "" {
+ return nil, errors.New(errors.InvalidArgument, "alias name is required")
+ }
+
+ stored := alias
+ stored.Indexes = append([]string(nil), alias.Indexes...)
+ stored.ETag = m.etag()
+ m.aliases.Set(key(service, alias.Name), &stored)
+
+ out := stored
+ out.Indexes = append([]string(nil), stored.Indexes...)
+
+ return &out, nil
+}
+
+func (m *Mock) GetAlias(_ context.Context, service, name string) (*driver.Alias, error) {
+ a, ok := m.aliases.Get(key(service, name))
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "alias %q not found", name)
+ }
+
+ out := *a
+ out.Indexes = append([]string(nil), a.Indexes...)
+
+ return &out, nil
+}
+
+func (m *Mock) ListAliases(_ context.Context, service string) ([]driver.Alias, error) {
+ out := make([]driver.Alias, 0)
+
+ for _, a := range childKeysUnder[*driver.Alias](m.aliases, service) {
+ c := *a
+ c.Indexes = append([]string(nil), a.Indexes...)
+ out = append(out, c)
+ }
+
+ return out, nil
+}
+
+func (m *Mock) DeleteAlias(_ context.Context, service, name string) error {
+ if !m.aliases.Delete(key(service, name)) {
+ return errors.Newf(errors.NotFound, "alias %q not found", name)
+ }
+
+ return nil
+}
+
+// --- Service statistics ---
+
+func (m *Mock) GetServiceStatistics(_ context.Context, service string) (*driver.ServiceStatistics, error) {
+ stats := &driver.ServiceStatistics{}
+
+ idxPrefix := service + "/"
+ for k := range m.indexes.All() {
+ if strings.HasPrefix(k, idxPrefix) {
+ stats.IndexCount++
+ }
+ }
+
+ for k := range m.documents.All() {
+ if strings.HasPrefix(k, idxPrefix) {
+ stats.DocumentCount++
+ }
+ }
+
+ stats.IndexerCount = len(childKeysUnder[*driver.Indexer](m.indexers, service))
+ stats.DataSourceCount = len(childKeysUnder[*driver.DataSource](m.dataSources, service))
+ stats.StorageBytes = stats.DocumentCount * 1024
+
+ return stats, nil
+}
diff --git a/providers/azure/azuresql/azuresql.go b/providers/azure/azuresql/azuresql.go
index 79c2589..dc77779 100644
--- a/providers/azure/azuresql/azuresql.go
+++ b/providers/azure/azuresql/azuresql.go
@@ -26,12 +26,12 @@ import (
"fmt"
"sync"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
const (
diff --git a/providers/azure/azuresql/azuresql_test.go b/providers/azure/azuresql/azuresql_test.go
index 91b6cde..ec41275 100644
--- a/providers/azure/azuresql/azuresql_test.go
+++ b/providers/azure/azuresql/azuresql_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
func newTestMock() *Mock {
diff --git a/providers/azure/blobstorage/blobstorage.go b/providers/azure/blobstorage/blobstorage.go
index 65f49b7..1d44535 100644
--- a/providers/azure/blobstorage/blobstorage.go
+++ b/providers/azure/blobstorage/blobstorage.go
@@ -10,13 +10,13 @@ import (
"strings"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/pagination"
- "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/internal/pagination"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
const (
diff --git a/providers/azure/blobstorage/blobstorage_test.go b/providers/azure/blobstorage/blobstorage_test.go
index cb46a1d..d6ce9cf 100644
--- a/providers/azure/blobstorage/blobstorage_test.go
+++ b/providers/azure/blobstorage/blobstorage_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/storage/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -221,13 +221,13 @@ func TestListBlobs(t *testing.T) {
require.NoError(t, m.PutObject(ctx, "bucket", "root.txt", []byte("r"), "text/plain", nil))
tests := []struct {
- name string
- bucket string
- opts driver.ListOptions
- wantErr bool
- wantCount int
- wantPrefixes []string
- errMsg string
+ name string
+ bucket string
+ opts driver.ListOptions
+ wantErr bool
+ wantCount int
+ wantPrefixes []string
+ errMsg string
}{
{name: "all objects", bucket: "bucket", opts: driver.ListOptions{}, wantCount: 3},
{name: "prefix filter", bucket: "bucket", opts: driver.ListOptions{Prefix: "dir/"}, wantCount: 2},
diff --git a/providers/azure/cosmosdb/cosmosdb.go b/providers/azure/cosmosdb/cosmosdb.go
index 9ee2fba..317aef1 100644
--- a/providers/azure/cosmosdb/cosmosdb.go
+++ b/providers/azure/cosmosdb/cosmosdb.go
@@ -9,12 +9,12 @@ import (
"sync"
"sync/atomic"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/database/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/pagination"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/internal/pagination"
+ "github.com/stackshy/cloudemu/v2/services/database/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Scan/query filter operator constants.
diff --git a/providers/azure/cosmosdb/cosmosdb_test.go b/providers/azure/cosmosdb/cosmosdb_test.go
index f7569ac..eb073af 100644
--- a/providers/azure/cosmosdb/cosmosdb_test.go
+++ b/providers/azure/cosmosdb/cosmosdb_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/database/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/database/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/azure/databricks/databricks.go b/providers/azure/databricks/databricks.go
index 3293399..51ccea7 100644
--- a/providers/azure/databricks/databricks.go
+++ b/providers/azure/databricks/databricks.go
@@ -9,11 +9,11 @@ import (
"sync/atomic"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/databricks/driver"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
// Compile-time checks that Mock implements both Databricks interfaces.
diff --git a/providers/azure/databricks/databricks_test.go b/providers/azure/databricks/databricks_test.go
index 0293509..5c15fc6 100644
--- a/providers/azure/databricks/databricks_test.go
+++ b/providers/azure/databricks/databricks_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/databricks/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
func newTestMock() *Mock {
diff --git a/providers/azure/databricks/dataplane.go b/providers/azure/databricks/dataplane.go
index 2498909..9a73a54 100644
--- a/providers/azure/databricks/dataplane.go
+++ b/providers/azure/databricks/dataplane.go
@@ -4,9 +4,9 @@ import (
"context"
"strconv"
- "github.com/stackshy/cloudemu/databricks/driver"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
// --- instance pools ---
diff --git a/providers/azure/databricks/dataplane_compute.go b/providers/azure/databricks/dataplane_compute.go
index 5ba9031..58729fd 100644
--- a/providers/azure/databricks/dataplane_compute.go
+++ b/providers/azure/databricks/dataplane_compute.go
@@ -3,8 +3,8 @@ package databricks
import (
"context"
- "github.com/stackshy/cloudemu/databricks/driver"
- "github.com/stackshy/cloudemu/errors"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
// ResizeCluster changes a cluster's worker count or autoscale bounds.
diff --git a/providers/azure/databricks/dataplane_compute_test.go b/providers/azure/databricks/dataplane_compute_test.go
index d158507..42d23e0 100644
--- a/providers/azure/databricks/dataplane_compute_test.go
+++ b/providers/azure/databricks/dataplane_compute_test.go
@@ -4,7 +4,7 @@ import (
"context"
"testing"
- "github.com/stackshy/cloudemu/databricks/driver"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
func TestClusterResizePinMetadata(t *testing.T) {
diff --git a/providers/azure/databricks/dataplane_more.go b/providers/azure/databricks/dataplane_more.go
index ad4692e..75c6bfb 100644
--- a/providers/azure/databricks/dataplane_more.go
+++ b/providers/azure/databricks/dataplane_more.go
@@ -3,9 +3,9 @@ package databricks
import (
"context"
- "github.com/stackshy/cloudemu/databricks/driver"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
// --- job runs ---
diff --git a/providers/azure/databricks/dataplane_more_test.go b/providers/azure/databricks/dataplane_more_test.go
index 9b2f87e..7801410 100644
--- a/providers/azure/databricks/dataplane_more_test.go
+++ b/providers/azure/databricks/dataplane_more_test.go
@@ -4,7 +4,7 @@ import (
"context"
"testing"
- "github.com/stackshy/cloudemu/databricks/driver"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
func TestJobRunLifecycle(t *testing.T) {
diff --git a/providers/azure/databricks/dataplane_test.go b/providers/azure/databricks/dataplane_test.go
index 5914437..07a676e 100644
--- a/providers/azure/databricks/dataplane_test.go
+++ b/providers/azure/databricks/dataplane_test.go
@@ -4,7 +4,7 @@ import (
"context"
"testing"
- "github.com/stackshy/cloudemu/databricks/driver"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
func TestInstancePoolLifecycle(t *testing.T) {
diff --git a/providers/azure/eventgrid/eventgrid.go b/providers/azure/eventgrid/eventgrid.go
index 7207131..8778b42 100644
--- a/providers/azure/eventgrid/eventgrid.go
+++ b/providers/azure/eventgrid/eventgrid.go
@@ -9,12 +9,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/providers/azure/eventgrid/eventgrid_test.go b/providers/azure/eventgrid/eventgrid_test.go
index b349c23..ed48157 100644
--- a/providers/azure/eventgrid/eventgrid_test.go
+++ b/providers/azure/eventgrid/eventgrid_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/eventbus/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -507,10 +507,10 @@ func TestPutEvents(t *testing.T) {
func TestEventPatternMatching(t *testing.T) {
tests := []struct {
- name string
- pattern string
- event driver.Event
- expectMatch bool
+ name string
+ pattern string
+ event driver.Event
+ expectMatch bool
}{
{
name: "empty pattern matches all",
diff --git a/providers/azure/functions/aliases.go b/providers/azure/functions/aliases.go
index 79bfd04..232a724 100644
--- a/providers/azure/functions/aliases.go
+++ b/providers/azure/functions/aliases.go
@@ -4,9 +4,9 @@ import (
"context"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// CreateAlias creates a new deployment slot alias pointing to a specific function version.
diff --git a/providers/azure/functions/concurrency.go b/providers/azure/functions/concurrency.go
index a5ccf78..8cf8fe1 100644
--- a/providers/azure/functions/concurrency.go
+++ b/providers/azure/functions/concurrency.go
@@ -3,8 +3,8 @@ package functions
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// PutFunctionConcurrency sets reserved concurrency for an Azure Function.
diff --git a/providers/azure/functions/esm.go b/providers/azure/functions/esm.go
index 595f629..7329a15 100644
--- a/providers/azure/functions/esm.go
+++ b/providers/azure/functions/esm.go
@@ -5,8 +5,8 @@ import (
"fmt"
"sync/atomic"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// mappingCounter is used to generate unique UUIDs for event source mappings.
diff --git a/providers/azure/functions/functions.go b/providers/azure/functions/functions.go
index d79446c..a7d1b8d 100644
--- a/providers/azure/functions/functions.go
+++ b/providers/azure/functions/functions.go
@@ -8,12 +8,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// Compile-time check that Mock implements driver.Serverless.
diff --git a/providers/azure/functions/functions_test.go b/providers/azure/functions/functions_test.go
index 81ff958..fd1147c 100644
--- a/providers/azure/functions/functions_test.go
+++ b/providers/azure/functions/functions_test.go
@@ -7,9 +7,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -389,11 +389,11 @@ func TestDeleteAlias(t *testing.T) {
require.NoError(t, err)
tests := []struct {
- name string
- fnName string
- alias string
- wantErr bool
- errMsg string
+ name string
+ fnName string
+ alias string
+ wantErr bool
+ errMsg string
}{
{name: "success", fnName: "fn1", alias: "prod"},
{name: "alias not found", fnName: "fn1", alias: "missing", wantErr: true, errMsg: "not found"},
diff --git a/providers/azure/functions/layers.go b/providers/azure/functions/layers.go
index eae0990..696a8f5 100644
--- a/providers/azure/functions/layers.go
+++ b/providers/azure/functions/layers.go
@@ -7,10 +7,10 @@ import (
"strconv"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// PublishLayerVersion publishes a new version of a shared code extension.
diff --git a/providers/azure/functions/versions.go b/providers/azure/functions/versions.go
index 44516f8..cd5c045 100644
--- a/providers/azure/functions/versions.go
+++ b/providers/azure/functions/versions.go
@@ -5,8 +5,8 @@ import (
"strconv"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// latestVersion is the symbolic version for the current function code.
diff --git a/providers/azure/keyvault/keyvault.go b/providers/azure/keyvault/keyvault.go
index 4722350..0ad37f8 100644
--- a/providers/azure/keyvault/keyvault.go
+++ b/providers/azure/keyvault/keyvault.go
@@ -6,11 +6,11 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/secrets/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
// Compile-time check that Mock implements driver.Secrets.
diff --git a/providers/azure/keyvault/keyvault_test.go b/providers/azure/keyvault/keyvault_test.go
index ebbe27d..d53840f 100644
--- a/providers/azure/keyvault/keyvault_test.go
+++ b/providers/azure/keyvault/keyvault_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/secrets/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/secrets/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/azure/loganalytics/loganalytics.go b/providers/azure/loganalytics/loganalytics.go
index 52dab31..7f1e701 100644
--- a/providers/azure/loganalytics/loganalytics.go
+++ b/providers/azure/loganalytics/loganalytics.go
@@ -7,12 +7,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/logging/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/logging/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/providers/azure/loganalytics/loganalytics_test.go b/providers/azure/loganalytics/loganalytics_test.go
index 57af6a1..a2f3ec7 100644
--- a/providers/azure/loganalytics/loganalytics_test.go
+++ b/providers/azure/loganalytics/loganalytics_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/logging/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/logging/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/azure/mysqlflex/mysqlflex.go b/providers/azure/mysqlflex/mysqlflex.go
index bcf1cff..cbc4108 100644
--- a/providers/azure/mysqlflex/mysqlflex.go
+++ b/providers/azure/mysqlflex/mysqlflex.go
@@ -14,12 +14,12 @@ import (
"context"
"sync"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
const (
diff --git a/providers/azure/mysqlflex/mysqlflex_test.go b/providers/azure/mysqlflex/mysqlflex_test.go
index e2d8c40..218cc2f 100644
--- a/providers/azure/mysqlflex/mysqlflex_test.go
+++ b/providers/azure/mysqlflex/mysqlflex_test.go
@@ -6,8 +6,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
func newTestMock() *Mock {
diff --git a/providers/azure/notificationhubs/notificationhubs.go b/providers/azure/notificationhubs/notificationhubs.go
index ee08675..98c40f6 100644
--- a/providers/azure/notificationhubs/notificationhubs.go
+++ b/providers/azure/notificationhubs/notificationhubs.go
@@ -6,12 +6,12 @@ import (
"fmt"
"sync"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/notification/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
// Compile-time check that Mock implements driver.Notification.
diff --git a/providers/azure/notificationhubs/notificationhubs_test.go b/providers/azure/notificationhubs/notificationhubs_test.go
index 9ec0d29..affd872 100644
--- a/providers/azure/notificationhubs/notificationhubs_test.go
+++ b/providers/azure/notificationhubs/notificationhubs_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/notification/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/notification/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/azure/postgresflex/postgresflex.go b/providers/azure/postgresflex/postgresflex.go
index de4808a..18ca63c 100644
--- a/providers/azure/postgresflex/postgresflex.go
+++ b/providers/azure/postgresflex/postgresflex.go
@@ -19,11 +19,11 @@ import (
"fmt"
"sync"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
const (
diff --git a/providers/azure/postgresflex/postgresflex_test.go b/providers/azure/postgresflex/postgresflex_test.go
index d2e6972..f5970bf 100644
--- a/providers/azure/postgresflex/postgresflex_test.go
+++ b/providers/azure/postgresflex/postgresflex_test.go
@@ -6,8 +6,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
func newTestMock() *Mock {
diff --git a/providers/azure/servicebus/servicebus.go b/providers/azure/servicebus/servicebus.go
index bd333d1..41b1d23 100644
--- a/providers/azure/servicebus/servicebus.go
+++ b/providers/azure/servicebus/servicebus.go
@@ -8,12 +8,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/messagequeue/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Compile-time check that Mock implements driver.MessageQueue.
diff --git a/providers/azure/servicebus/servicebus_test.go b/providers/azure/servicebus/servicebus_test.go
index 37a397a..9b9fe0e 100644
--- a/providers/azure/servicebus/servicebus_test.go
+++ b/providers/azure/servicebus/servicebus_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/messagequeue/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -511,8 +511,8 @@ func TestSetQueueAttributes(t *testing.T) {
t.Run("update attributes", func(t *testing.T) {
err := m.SetQueueAttributes(ctx, url, map[string]int{
- "DelaySeconds": 5,
- "VisibilityTimeout": 60,
+ "DelaySeconds": 5,
+ "VisibilityTimeout": 60,
"MaximumMessageSize": 1024,
})
require.NoError(t, err)
diff --git a/providers/azure/tablestorage/tablestorage.go b/providers/azure/tablestorage/tablestorage.go
new file mode 100644
index 0000000..e325292
--- /dev/null
+++ b/providers/azure/tablestorage/tablestorage.go
@@ -0,0 +1,292 @@
+// Package tablestorage provides an in-memory mock implementation of the
+// Azure Table Storage entity store, satisfying tablestorage/driver.TableStorage.
+package tablestorage
+
+import (
+ "context"
+ "strings"
+ "sync"
+
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ driver "github.com/stackshy/cloudemu/v2/services/tablestorage/driver"
+)
+
+// Compile-time check that Mock implements driver.TableStorage.
+var _ driver.TableStorage = (*Mock)(nil)
+
+// tableData holds one table's entities, keyed by "partitionKey\x00rowKey".
+type tableData struct {
+ mu sync.RWMutex
+ entities map[string]driver.Entity
+}
+
+// Mock is an in-memory Table Storage backend.
+type Mock struct {
+ mu sync.RWMutex
+ tables map[string]*tableData
+ opts *config.Options
+}
+
+// New creates a new Table Storage mock.
+func New(opts *config.Options) *Mock {
+ return &Mock{
+ tables: make(map[string]*tableData),
+ opts: opts,
+ }
+}
+
+func entityKey(partitionKey, rowKey string) string {
+ return partitionKey + "\x00" + rowKey
+}
+
+// CreateTable creates a new empty table.
+func (m *Mock) CreateTable(_ context.Context, name string) error {
+ if name == "" {
+ return errors.New(errors.InvalidArgument, "table name is required")
+ }
+
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if _, ok := m.tables[name]; ok {
+ return errors.Newf(errors.AlreadyExists, "table %q already exists", name)
+ }
+
+ m.tables[name] = &tableData{entities: make(map[string]driver.Entity)}
+
+ return nil
+}
+
+// DeleteTable removes a table and all its entities.
+func (m *Mock) DeleteTable(_ context.Context, name string) error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if _, ok := m.tables[name]; !ok {
+ return errors.Newf(errors.NotFound, "table %q not found", name)
+ }
+
+ delete(m.tables, name)
+
+ return nil
+}
+
+// ListTables returns the names of all tables.
+func (m *Mock) ListTables(_ context.Context) ([]string, error) {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
+ names := make([]string, 0, len(m.tables))
+ for name := range m.tables {
+ names = append(names, name)
+ }
+
+ return names, nil
+}
+
+func (m *Mock) table(name string) (*tableData, error) {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
+ td, ok := m.tables[name]
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "table %q not found", name)
+ }
+
+ return td, nil
+}
+
+// InsertEntity adds a new entity. It fails if an entity with the same
+// PartitionKey/RowKey already exists.
+func (m *Mock) InsertEntity(_ context.Context, table, partitionKey, rowKey string, entity driver.Entity) error {
+ if partitionKey == "" || rowKey == "" {
+ return errors.New(errors.InvalidArgument, "PartitionKey and RowKey are required")
+ }
+
+ td, err := m.table(table)
+ if err != nil {
+ return err
+ }
+
+ td.mu.Lock()
+ defer td.mu.Unlock()
+
+ key := entityKey(partitionKey, rowKey)
+ if _, ok := td.entities[key]; ok {
+ return errors.Newf(errors.AlreadyExists, "entity (%q,%q) already exists", partitionKey, rowKey)
+ }
+
+ td.entities[key] = cloneEntity(entity)
+
+ return nil
+}
+
+// GetEntity returns the entity addressed by partitionKey/rowKey.
+func (m *Mock) GetEntity(_ context.Context, table, partitionKey, rowKey string) (driver.Entity, error) {
+ td, err := m.table(table)
+ if err != nil {
+ return nil, err
+ }
+
+ td.mu.RLock()
+ defer td.mu.RUnlock()
+
+ ent, ok := td.entities[entityKey(partitionKey, rowKey)]
+ if !ok {
+ return nil, errors.Newf(errors.NotFound, "entity (%q,%q) not found", partitionKey, rowKey)
+ }
+
+ return cloneEntity(ent), nil
+}
+
+// UpdateEntity merges or replaces an existing entity.
+func (m *Mock) UpdateEntity(
+ _ context.Context, table, partitionKey, rowKey string, entity driver.Entity, mode driver.UpdateMode,
+) error {
+ td, err := m.table(table)
+ if err != nil {
+ return err
+ }
+
+ td.mu.Lock()
+ defer td.mu.Unlock()
+
+ key := entityKey(partitionKey, rowKey)
+
+ existing, ok := td.entities[key]
+ if !ok {
+ return errors.Newf(errors.NotFound, "entity (%q,%q) not found", partitionKey, rowKey)
+ }
+
+ if mode == driver.UpdateModeReplace {
+ td.entities[key] = cloneEntity(entity)
+ return nil
+ }
+
+ merged := cloneEntity(existing)
+ for k, v := range entity {
+ merged[k] = v
+ }
+
+ td.entities[key] = merged
+
+ return nil
+}
+
+// DeleteEntity removes an entity.
+func (m *Mock) DeleteEntity(_ context.Context, table, partitionKey, rowKey string) error {
+ td, err := m.table(table)
+ if err != nil {
+ return err
+ }
+
+ td.mu.Lock()
+ defer td.mu.Unlock()
+
+ key := entityKey(partitionKey, rowKey)
+ if _, ok := td.entities[key]; !ok {
+ return errors.Newf(errors.NotFound, "entity (%q,%q) not found", partitionKey, rowKey)
+ }
+
+ delete(td.entities, key)
+
+ return nil
+}
+
+// QueryEntities returns entities matching the given options. Filtering
+// supports a partition-key restriction plus a best-effort OData $filter parse
+// (see matchesFilter); unrecognized filters match everything.
+func (m *Mock) QueryEntities(_ context.Context, table string, opts driver.QueryOptions) ([]driver.Entity, error) {
+ td, err := m.table(table)
+ if err != nil {
+ return nil, err
+ }
+
+ td.mu.RLock()
+ defer td.mu.RUnlock()
+
+ conds := parseFilter(opts.Filter)
+
+ results := make([]driver.Entity, 0, len(td.entities))
+
+ for _, ent := range td.entities {
+ if opts.PartitionKey != "" && asString(ent["PartitionKey"]) != opts.PartitionKey {
+ continue
+ }
+
+ if !matchesConds(ent, conds) {
+ continue
+ }
+
+ results = append(results, cloneEntity(ent))
+ }
+
+ return results, nil
+}
+
+func cloneEntity(e driver.Entity) driver.Entity {
+ out := make(driver.Entity, len(e))
+ for k, v := range e {
+ out[k] = v
+ }
+
+ return out
+}
+
+// eqCond is a single "property eq value" equality condition.
+type eqCond struct {
+ prop string
+ val string
+}
+
+// parseFilter extracts the equality conditions from a simple OData $filter of
+// the form "Prop eq 'val' and Prop2 eq 'val2'". Anything it can't parse is
+// dropped, so an unsupported filter degrades to "match all" rather than an
+// error — adequate for the common query-by-partition case.
+func parseFilter(filter string) []eqCond {
+ filter = strings.TrimSpace(filter)
+ if filter == "" {
+ return nil
+ }
+
+ var conds []eqCond
+
+ for _, clause := range strings.Split(filter, " and ") {
+ fields := strings.Fields(strings.TrimSpace(clause))
+
+ const eqParts = 3
+ if len(fields) != eqParts || !strings.EqualFold(fields[1], "eq") {
+ continue
+ }
+
+ conds = append(conds, eqCond{prop: fields[0], val: unquote(fields[2])})
+ }
+
+ return conds
+}
+
+func matchesConds(ent driver.Entity, conds []eqCond) bool {
+ for _, c := range conds {
+ if asString(ent[c.prop]) != c.val {
+ return false
+ }
+ }
+
+ return true
+}
+
+// unquote strips surrounding single quotes from an OData string literal.
+func unquote(s string) string {
+ s = strings.TrimSpace(s)
+ if len(s) >= 2 && s[0] == '\'' && s[len(s)-1] == '\'' {
+ return s[1 : len(s)-1]
+ }
+
+ return s
+}
+
+func asString(v any) string {
+ s, _ := v.(string)
+ return s
+}
diff --git a/providers/azure/virtualmachines/asg.go b/providers/azure/virtualmachines/asg.go
index 5685dea..a565cc6 100644
--- a/providers/azure/virtualmachines/asg.go
+++ b/providers/azure/virtualmachines/asg.go
@@ -3,9 +3,9 @@ package virtualmachines
import (
"context"
- "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
const (
diff --git a/providers/azure/virtualmachines/spot.go b/providers/azure/virtualmachines/spot.go
index 5b029bd..00a105e 100644
--- a/providers/azure/virtualmachines/spot.go
+++ b/providers/azure/virtualmachines/spot.go
@@ -3,9 +3,9 @@ package virtualmachines
import (
"context"
- "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
const (
diff --git a/providers/azure/virtualmachines/template.go b/providers/azure/virtualmachines/template.go
index e9cf85c..64f88a6 100644
--- a/providers/azure/virtualmachines/template.go
+++ b/providers/azure/virtualmachines/template.go
@@ -4,9 +4,9 @@ import (
"context"
"sync/atomic"
- "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
//nolint:gochecknoglobals // atomic counter for template versioning
diff --git a/providers/azure/virtualmachines/vm.go b/providers/azure/virtualmachines/vm.go
index 094ea48..acde863 100644
--- a/providers/azure/virtualmachines/vm.go
+++ b/providers/azure/virtualmachines/vm.go
@@ -7,14 +7,14 @@ import (
"sync/atomic"
"time"
- "github.com/stackshy/cloudemu/compute"
- "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/statemachine"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/internal/statemachine"
+ "github.com/stackshy/cloudemu/v2/services/compute"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Compile-time check that Mock implements driver.Compute.
@@ -207,6 +207,13 @@ func (m *Mock) RunInstances(ctx context.Context, cfg driver.InstanceConfig, coun
return nil, cerrors.New(cerrors.InvalidArgument, "count must be greater than 0")
}
+ // Bound the requested count so an oversized MaxCount can't drive an
+ // unbounded slice allocation (real providers cap instances per call).
+ const maxRunInstances = 1000
+ if count > maxRunInstances {
+ return nil, cerrors.Newf(cerrors.InvalidArgument, "count %d exceeds the maximum of %d per call", count, maxRunInstances)
+ }
+
results := make([]driver.Instance, 0, count)
for i := 0; i < count; i++ {
diff --git a/providers/azure/virtualmachines/vm_test.go b/providers/azure/virtualmachines/vm_test.go
index 298e46f..2051ff2 100644
--- a/providers/azure/virtualmachines/vm_test.go
+++ b/providers/azure/virtualmachines/vm_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/compute"
- "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/config"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/compute"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/azure/vnet/acl.go b/providers/azure/vnet/acl.go
index eaca778..8c53a23 100644
--- a/providers/azure/vnet/acl.go
+++ b/providers/azure/vnet/acl.go
@@ -4,9 +4,9 @@ import (
"context"
"sort"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Default ACL rule constants.
diff --git a/providers/azure/vnet/eip.go b/providers/azure/vnet/eip.go
index cddebc2..ff454ce 100644
--- a/providers/azure/vnet/eip.go
+++ b/providers/azure/vnet/eip.go
@@ -3,9 +3,9 @@ package vnet
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type eipData struct {
diff --git a/providers/azure/vnet/endpoint.go b/providers/azure/vnet/endpoint.go
index 070d2de..7ff0cf8 100644
--- a/providers/azure/vnet/endpoint.go
+++ b/providers/azure/vnet/endpoint.go
@@ -3,9 +3,9 @@ package vnet
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// copyStringSlice creates a shallow copy of a string slice.
diff --git a/providers/azure/vnet/flowlog.go b/providers/azure/vnet/flowlog.go
index 583c893..f954932 100644
--- a/providers/azure/vnet/flowlog.go
+++ b/providers/azure/vnet/flowlog.go
@@ -4,9 +4,9 @@ import (
"context"
"fmt"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Flow log status constants.
diff --git a/providers/azure/vnet/igw.go b/providers/azure/vnet/igw.go
index 748f31a..feee738 100644
--- a/providers/azure/vnet/igw.go
+++ b/providers/azure/vnet/igw.go
@@ -3,9 +3,9 @@ package vnet
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Internet gateway state constants.
diff --git a/providers/azure/vnet/natgw.go b/providers/azure/vnet/natgw.go
index d76e1d0..58896ad 100644
--- a/providers/azure/vnet/natgw.go
+++ b/providers/azure/vnet/natgw.go
@@ -3,9 +3,9 @@ package vnet
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// NAT gateway state constants.
diff --git a/providers/azure/vnet/peering.go b/providers/azure/vnet/peering.go
index 33faa5e..951f34f 100644
--- a/providers/azure/vnet/peering.go
+++ b/providers/azure/vnet/peering.go
@@ -5,9 +5,9 @@ import (
"fmt"
"net"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Peering status constants.
diff --git a/providers/azure/vnet/routetable.go b/providers/azure/vnet/routetable.go
index 60832d5..e37c77f 100644
--- a/providers/azure/vnet/routetable.go
+++ b/providers/azure/vnet/routetable.go
@@ -3,9 +3,9 @@ package vnet
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Route target type constants.
diff --git a/providers/azure/vnet/rtassoc.go b/providers/azure/vnet/rtassoc.go
index fa13301..43633f5 100644
--- a/providers/azure/vnet/rtassoc.go
+++ b/providers/azure/vnet/rtassoc.go
@@ -3,9 +3,9 @@ package vnet
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type rtAssocData struct {
diff --git a/providers/azure/vnet/vnet.go b/providers/azure/vnet/vnet.go
index 7caf422..3f187ce 100644
--- a/providers/azure/vnet/vnet.go
+++ b/providers/azure/vnet/vnet.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Time format and mock constants.
diff --git a/providers/azure/vnet/vnet_test.go b/providers/azure/vnet/vnet_test.go
index 70f5578..6e09d49 100644
--- a/providers/azure/vnet/vnet_test.go
+++ b/providers/azure/vnet/vnet_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/artifactregistry/artifactregistry.go b/providers/gcp/artifactregistry/artifactregistry.go
index 8b9dc41..9219e7b 100644
--- a/providers/gcp/artifactregistry/artifactregistry.go
+++ b/providers/gcp/artifactregistry/artifactregistry.go
@@ -10,12 +10,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/containerregistry/driver"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/providers/gcp/artifactregistry/artifactregistry_test.go b/providers/gcp/artifactregistry/artifactregistry_test.go
index 28ead38..9d92359 100644
--- a/providers/gcp/artifactregistry/artifactregistry_test.go
+++ b/providers/gcp/artifactregistry/artifactregistry_test.go
@@ -6,10 +6,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/containerregistry/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/providers/gcp/cloudmonitoring"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/cloudmonitoring"
+ "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -486,11 +486,11 @@ func TestTagImageEmptyTagRejected(t *testing.T) {
func TestImageTagMutability(t *testing.T) {
tests := []struct {
- name string
+ name string
mutSetting string
- pushTag string
- retagTag string
- expectErr bool
+ pushTag string
+ retagTag string
+ expectErr bool
}{
{
name: "mutable allows overwrite",
diff --git a/providers/gcp/clouddns/dns.go b/providers/gcp/clouddns/dns.go
index 06e7be6..8de4c99 100644
--- a/providers/gcp/clouddns/dns.go
+++ b/providers/gcp/clouddns/dns.go
@@ -5,11 +5,11 @@ import (
"context"
"strings"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/dns/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
// Compile-time check that Mock implements driver.DNS.
diff --git a/providers/gcp/clouddns/dns_test.go b/providers/gcp/clouddns/dns_test.go
index ea6cb24..dc1c89c 100644
--- a/providers/gcp/clouddns/dns_test.go
+++ b/providers/gcp/clouddns/dns_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/dns/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/dns/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -172,12 +172,12 @@ func TestDeleteRecord(t *testing.T) {
require.NoError(t, err)
tests := []struct {
- name string
- zoneID string
- recName string
- recType string
- wantErr bool
- errSubstr string
+ name string
+ zoneID string
+ recName string
+ recType string
+ wantErr bool
+ errSubstr string
}{
{name: "success", zoneID: zone.ID, recName: "www.example.com", recType: "A"},
{name: "zone not found", zoneID: "missing", recName: "www.example.com", recType: "A", wantErr: true, errSubstr: "not found"},
diff --git a/providers/gcp/clouddns/healthcheck.go b/providers/gcp/clouddns/healthcheck.go
index 3e82787..ca6fb69 100644
--- a/providers/gcp/clouddns/healthcheck.go
+++ b/providers/gcp/clouddns/healthcheck.go
@@ -3,9 +3,9 @@ package clouddns
import (
"context"
- "github.com/stackshy/cloudemu/dns/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
const (
diff --git a/providers/gcp/cloudfunctions/aliases.go b/providers/gcp/cloudfunctions/aliases.go
index 3caad1e..e378365 100644
--- a/providers/gcp/cloudfunctions/aliases.go
+++ b/providers/gcp/cloudfunctions/aliases.go
@@ -4,9 +4,9 @@ import (
"context"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// CreateAlias creates a new traffic split alias pointing to a specific function version.
diff --git a/providers/gcp/cloudfunctions/concurrency.go b/providers/gcp/cloudfunctions/concurrency.go
index 6ac19af..b2e436f 100644
--- a/providers/gcp/cloudfunctions/concurrency.go
+++ b/providers/gcp/cloudfunctions/concurrency.go
@@ -3,8 +3,8 @@ package cloudfunctions
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// PutFunctionConcurrency sets max instances (concurrency) for a Cloud Function.
diff --git a/providers/gcp/cloudfunctions/esm.go b/providers/gcp/cloudfunctions/esm.go
index ffb4ea5..5648b74 100644
--- a/providers/gcp/cloudfunctions/esm.go
+++ b/providers/gcp/cloudfunctions/esm.go
@@ -5,8 +5,8 @@ import (
"fmt"
"sync/atomic"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// mappingCounter is used to generate unique UUIDs for event source mappings.
diff --git a/providers/gcp/cloudfunctions/functions.go b/providers/gcp/cloudfunctions/functions.go
index 0db5fcc..8fc51b3 100644
--- a/providers/gcp/cloudfunctions/functions.go
+++ b/providers/gcp/cloudfunctions/functions.go
@@ -8,12 +8,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// Compile-time check that Mock implements driver.Serverless.
diff --git a/providers/gcp/cloudfunctions/functions_test.go b/providers/gcp/cloudfunctions/functions_test.go
index 65ab595..7e64289 100644
--- a/providers/gcp/cloudfunctions/functions_test.go
+++ b/providers/gcp/cloudfunctions/functions_test.go
@@ -7,9 +7,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/cloudfunctions/layers.go b/providers/gcp/cloudfunctions/layers.go
index 7b2d8a8..a72fc15 100644
--- a/providers/gcp/cloudfunctions/layers.go
+++ b/providers/gcp/cloudfunctions/layers.go
@@ -7,10 +7,10 @@ import (
"strconv"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// PublishLayerVersion publishes a new version of a shared buildpack layer.
diff --git a/providers/gcp/cloudfunctions/versions.go b/providers/gcp/cloudfunctions/versions.go
index 8aa72d6..6b08665 100644
--- a/providers/gcp/cloudfunctions/versions.go
+++ b/providers/gcp/cloudfunctions/versions.go
@@ -5,8 +5,8 @@ import (
"strconv"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// latestVersion is the symbolic version for the current function code.
diff --git a/providers/gcp/cloudlogging/cloudlogging.go b/providers/gcp/cloudlogging/cloudlogging.go
index a0f73c4..799d994 100644
--- a/providers/gcp/cloudlogging/cloudlogging.go
+++ b/providers/gcp/cloudlogging/cloudlogging.go
@@ -7,12 +7,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/logging/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/logging/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/providers/gcp/cloudlogging/cloudlogging_test.go b/providers/gcp/cloudlogging/cloudlogging_test.go
index 58d064b..8ee9c4d 100644
--- a/providers/gcp/cloudlogging/cloudlogging_test.go
+++ b/providers/gcp/cloudlogging/cloudlogging_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/logging/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/logging/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/cloudmonitoring/monitoring.go b/providers/gcp/cloudmonitoring/monitoring.go
index 26ae155..8d12773 100644
--- a/providers/gcp/cloudmonitoring/monitoring.go
+++ b/providers/gcp/cloudmonitoring/monitoring.go
@@ -9,11 +9,11 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Compile-time check that Mock implements driver.Monitoring.
diff --git a/providers/gcp/cloudmonitoring/monitoring_test.go b/providers/gcp/cloudmonitoring/monitoring_test.go
index 280407f..433d688 100644
--- a/providers/gcp/cloudmonitoring/monitoring_test.go
+++ b/providers/gcp/cloudmonitoring/monitoring_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/cloudsql/cloudsql.go b/providers/gcp/cloudsql/cloudsql.go
index c3fe617..703efbd 100644
--- a/providers/gcp/cloudsql/cloudsql.go
+++ b/providers/gcp/cloudsql/cloudsql.go
@@ -13,12 +13,12 @@ import (
"fmt"
"sync"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
const (
diff --git a/providers/gcp/cloudsql/cloudsql_test.go b/providers/gcp/cloudsql/cloudsql_test.go
index a7cd4a0..f7554b4 100644
--- a/providers/gcp/cloudsql/cloudsql_test.go
+++ b/providers/gcp/cloudsql/cloudsql_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
func newTestMock() *Mock {
diff --git a/providers/gcp/eventarc/eventarc.go b/providers/gcp/eventarc/eventarc.go
index 629fc24..0da08ec 100644
--- a/providers/gcp/eventarc/eventarc.go
+++ b/providers/gcp/eventarc/eventarc.go
@@ -9,11 +9,11 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/providers/gcp/eventarc/eventarc_test.go b/providers/gcp/eventarc/eventarc_test.go
index 1efe1c2..4ab4359 100644
--- a/providers/gcp/eventarc/eventarc_test.go
+++ b/providers/gcp/eventarc/eventarc_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/eventbus/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/providers/gcp/cloudmonitoring"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/cloudmonitoring"
+ "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/fcm/fcm.go b/providers/gcp/fcm/fcm.go
index 988bab2..a3cda9b 100644
--- a/providers/gcp/fcm/fcm.go
+++ b/providers/gcp/fcm/fcm.go
@@ -5,12 +5,12 @@ import (
"context"
"sync"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/notification/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
// Compile-time check that Mock implements driver.Notification.
diff --git a/providers/gcp/fcm/fcm_test.go b/providers/gcp/fcm/fcm_test.go
index 79a3abd..49834f7 100644
--- a/providers/gcp/fcm/fcm_test.go
+++ b/providers/gcp/fcm/fcm_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/notification/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/notification/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/firestore/firestore.go b/providers/gcp/firestore/firestore.go
index 9fdd126..ce7278c 100644
--- a/providers/gcp/firestore/firestore.go
+++ b/providers/gcp/firestore/firestore.go
@@ -9,12 +9,12 @@ import (
"sync"
"sync/atomic"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/database/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/pagination"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/internal/pagination"
+ "github.com/stackshy/cloudemu/v2/services/database/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Scan/query filter operator constants.
diff --git a/providers/gcp/firestore/firestore_test.go b/providers/gcp/firestore/firestore_test.go
index 0affbed..8ffd5ee 100644
--- a/providers/gcp/firestore/firestore_test.go
+++ b/providers/gcp/firestore/firestore_test.go
@@ -6,9 +6,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/database/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/database/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/gce/asg.go b/providers/gcp/gce/asg.go
index 348612d..45592ef 100644
--- a/providers/gcp/gce/asg.go
+++ b/providers/gcp/gce/asg.go
@@ -3,9 +3,9 @@ package gce
import (
"context"
- "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
const (
diff --git a/providers/gcp/gce/gce.go b/providers/gcp/gce/gce.go
index 4dbad1c..3028bfe 100644
--- a/providers/gcp/gce/gce.go
+++ b/providers/gcp/gce/gce.go
@@ -7,14 +7,14 @@ import (
"sync/atomic"
"time"
- "github.com/stackshy/cloudemu/compute"
- "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/statemachine"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/internal/statemachine"
+ "github.com/stackshy/cloudemu/v2/services/compute"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Compile-time check that Mock implements driver.Compute.
@@ -216,6 +216,13 @@ func (m *Mock) RunInstances(ctx context.Context, cfg driver.InstanceConfig, coun
return nil, cerrors.New(cerrors.InvalidArgument, "count must be greater than 0")
}
+ // Bound the requested count so an oversized MaxCount can't drive an
+ // unbounded slice allocation (real providers cap instances per call).
+ const maxRunInstances = 1000
+ if count > maxRunInstances {
+ return nil, cerrors.Newf(cerrors.InvalidArgument, "count %d exceeds the maximum of %d per call", count, maxRunInstances)
+ }
+
results := make([]driver.Instance, 0, count)
for i := 0; i < count; i++ {
diff --git a/providers/gcp/gce/gce_test.go b/providers/gcp/gce/gce_test.go
index a7ac654..e57f822 100644
--- a/providers/gcp/gce/gce_test.go
+++ b/providers/gcp/gce/gce_test.go
@@ -5,10 +5,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/compute"
- "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/config"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/compute"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/gce/spot.go b/providers/gcp/gce/spot.go
index 65ad078..372d73c 100644
--- a/providers/gcp/gce/spot.go
+++ b/providers/gcp/gce/spot.go
@@ -3,9 +3,9 @@ package gce
import (
"context"
- "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
const (
diff --git a/providers/gcp/gce/template.go b/providers/gcp/gce/template.go
index 6df3589..ad23b1d 100644
--- a/providers/gcp/gce/template.go
+++ b/providers/gcp/gce/template.go
@@ -4,9 +4,9 @@ import (
"context"
"sync/atomic"
- "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
//nolint:gochecknoglobals // atomic counter for template versioning
diff --git a/providers/gcp/gcp.go b/providers/gcp/gcp.go
index dd1d377..ce15f03 100644
--- a/providers/gcp/gcp.go
+++ b/providers/gcp/gcp.go
@@ -2,27 +2,27 @@
package gcp
import (
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/providers/gcp/artifactregistry"
- "github.com/stackshy/cloudemu/providers/gcp/clouddns"
- "github.com/stackshy/cloudemu/providers/gcp/cloudfunctions"
- "github.com/stackshy/cloudemu/providers/gcp/cloudlogging"
- "github.com/stackshy/cloudemu/providers/gcp/cloudmonitoring"
- "github.com/stackshy/cloudemu/providers/gcp/cloudsql"
- "github.com/stackshy/cloudemu/providers/gcp/eventarc"
- "github.com/stackshy/cloudemu/providers/gcp/fcm"
- "github.com/stackshy/cloudemu/providers/gcp/firestore"
- "github.com/stackshy/cloudemu/providers/gcp/gce"
- "github.com/stackshy/cloudemu/providers/gcp/gcpiam"
- "github.com/stackshy/cloudemu/providers/gcp/gcplb"
- "github.com/stackshy/cloudemu/providers/gcp/gcpvpc"
- "github.com/stackshy/cloudemu/providers/gcp/gcs"
- "github.com/stackshy/cloudemu/providers/gcp/gke"
- "github.com/stackshy/cloudemu/providers/gcp/memorystore"
- "github.com/stackshy/cloudemu/providers/gcp/pubsub"
- "github.com/stackshy/cloudemu/providers/gcp/secretmanager"
- "github.com/stackshy/cloudemu/providers/gcp/vertexai"
- "github.com/stackshy/cloudemu/resourcediscovery"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/artifactregistry"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/clouddns"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/cloudfunctions"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/cloudlogging"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/cloudmonitoring"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/cloudsql"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/eventarc"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/fcm"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/firestore"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/gce"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/gcpiam"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/gcplb"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/gcpvpc"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/gcs"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/gke"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/memorystore"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/pubsub"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/secretmanager"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/vertexai"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
)
// Provider holds all GCP mock services.
diff --git a/providers/gcp/gcpiam/iam.go b/providers/gcp/gcpiam/iam.go
index 9777a9d..58c97d0 100644
--- a/providers/gcp/gcpiam/iam.go
+++ b/providers/gcp/gcpiam/iam.go
@@ -8,11 +8,11 @@ import (
"strings"
"sync"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/iam/driver"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
const timeFormat = "2006-01-02T15:04:05Z"
diff --git a/providers/gcp/gcpiam/iam_test.go b/providers/gcp/gcpiam/iam_test.go
index ccab57d..e63f66c 100644
--- a/providers/gcp/gcpiam/iam_test.go
+++ b/providers/gcp/gcpiam/iam_test.go
@@ -7,9 +7,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/iam/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/iam/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/gcplb/lb.go b/providers/gcp/gcplb/lb.go
index db5f058..8f57beb 100644
--- a/providers/gcp/gcplb/lb.go
+++ b/providers/gcp/gcplb/lb.go
@@ -6,11 +6,11 @@ import (
"fmt"
"sync"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/loadbalancer/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
// Compile-time check that Mock implements driver.LoadBalancer.
diff --git a/providers/gcp/gcplb/lb_test.go b/providers/gcp/gcplb/lb_test.go
index 62cff86..016f009 100644
--- a/providers/gcp/gcplb/lb_test.go
+++ b/providers/gcp/gcplb/lb_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/loadbalancer/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/gcpvpc/acl.go b/providers/gcp/gcpvpc/acl.go
index fddc843..f7b675e 100644
--- a/providers/gcp/gcpvpc/acl.go
+++ b/providers/gcp/gcpvpc/acl.go
@@ -4,9 +4,9 @@ import (
"context"
"sort"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Default ACL rule constants.
diff --git a/providers/gcp/gcpvpc/eip.go b/providers/gcp/gcpvpc/eip.go
index 2aca4d0..5b327c8 100644
--- a/providers/gcp/gcpvpc/eip.go
+++ b/providers/gcp/gcpvpc/eip.go
@@ -3,9 +3,9 @@ package gcpvpc
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type eipData struct {
diff --git a/providers/gcp/gcpvpc/endpoint.go b/providers/gcp/gcpvpc/endpoint.go
index 7641cb3..6c2c1e7 100644
--- a/providers/gcp/gcpvpc/endpoint.go
+++ b/providers/gcp/gcpvpc/endpoint.go
@@ -3,9 +3,9 @@ package gcpvpc
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// copyStringSlice creates a shallow copy of a string slice.
diff --git a/providers/gcp/gcpvpc/flowlog.go b/providers/gcp/gcpvpc/flowlog.go
index 4340a2c..b99b51f 100644
--- a/providers/gcp/gcpvpc/flowlog.go
+++ b/providers/gcp/gcpvpc/flowlog.go
@@ -4,9 +4,9 @@ import (
"context"
"fmt"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Flow log status constants.
diff --git a/providers/gcp/gcpvpc/igw.go b/providers/gcp/gcpvpc/igw.go
index 54b598f..076f6a1 100644
--- a/providers/gcp/gcpvpc/igw.go
+++ b/providers/gcp/gcpvpc/igw.go
@@ -3,9 +3,9 @@ package gcpvpc
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Internet gateway state constants.
diff --git a/providers/gcp/gcpvpc/natgw.go b/providers/gcp/gcpvpc/natgw.go
index e0b4ba9..22c7c32 100644
--- a/providers/gcp/gcpvpc/natgw.go
+++ b/providers/gcp/gcpvpc/natgw.go
@@ -3,9 +3,9 @@ package gcpvpc
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// NAT gateway state constants.
diff --git a/providers/gcp/gcpvpc/peering.go b/providers/gcp/gcpvpc/peering.go
index 5f40106..809a5f4 100644
--- a/providers/gcp/gcpvpc/peering.go
+++ b/providers/gcp/gcpvpc/peering.go
@@ -5,9 +5,9 @@ import (
"fmt"
"net"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Peering status constants.
diff --git a/providers/gcp/gcpvpc/routetable.go b/providers/gcp/gcpvpc/routetable.go
index af04ce9..0e352e2 100644
--- a/providers/gcp/gcpvpc/routetable.go
+++ b/providers/gcp/gcpvpc/routetable.go
@@ -3,9 +3,9 @@ package gcpvpc
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Route target type constants.
diff --git a/providers/gcp/gcpvpc/rtassoc.go b/providers/gcp/gcpvpc/rtassoc.go
index 034f29e..0837af0 100644
--- a/providers/gcp/gcpvpc/rtassoc.go
+++ b/providers/gcp/gcpvpc/rtassoc.go
@@ -3,9 +3,9 @@ package gcpvpc
import (
"context"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type rtAssocData struct {
diff --git a/providers/gcp/gcpvpc/vpc.go b/providers/gcp/gcpvpc/vpc.go
index 032773c..9278cb8 100644
--- a/providers/gcp/gcpvpc/vpc.go
+++ b/providers/gcp/gcpvpc/vpc.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Time format and mock constants.
diff --git a/providers/gcp/gcpvpc/vpc_test.go b/providers/gcp/gcpvpc/vpc_test.go
index 4d68288..d0bd023 100644
--- a/providers/gcp/gcpvpc/vpc_test.go
+++ b/providers/gcp/gcpvpc/vpc_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/networking/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/gcs/gcs.go b/providers/gcp/gcs/gcs.go
index 2531b45..8ae96ce 100644
--- a/providers/gcp/gcs/gcs.go
+++ b/providers/gcp/gcs/gcs.go
@@ -10,13 +10,13 @@ import (
"strings"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/pagination"
- "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/internal/pagination"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
const (
diff --git a/providers/gcp/gcs/gcs_test.go b/providers/gcp/gcs/gcs_test.go
index d2bfad5..aab6c14 100644
--- a/providers/gcp/gcs/gcs_test.go
+++ b/providers/gcp/gcs/gcs_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/storage/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -212,13 +212,13 @@ func TestListObjects(t *testing.T) {
require.NoError(t, m.PutObject(ctx, "b1", "images/c.jpg", []byte("c"), "", nil))
tests := []struct {
- name string
- bucket string
- opts driver.ListOptions
- wantCount int
- wantPrefixes int
- wantErr bool
- wantTruncated bool
+ name string
+ bucket string
+ opts driver.ListOptions
+ wantCount int
+ wantPrefixes int
+ wantErr bool
+ wantTruncated bool
}{
{name: "all objects", bucket: "b1", opts: driver.ListOptions{}, wantCount: 3},
{name: "with prefix", bucket: "b1", opts: driver.ListOptions{Prefix: "docs/"}, wantCount: 2},
diff --git a/providers/gcp/gke/gke.go b/providers/gcp/gke/gke.go
index 097c21c..87f6936 100644
--- a/providers/gcp/gke/gke.go
+++ b/providers/gcp/gke/gke.go
@@ -17,12 +17,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/kubernetes"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/kubernetes"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Stub values used in Cluster responses until the Kubernetes data-plane
diff --git a/providers/gcp/gke/gke_k8s_test.go b/providers/gcp/gke/gke_k8s_test.go
index 73ee38f..b51d103 100644
--- a/providers/gcp/gke/gke_k8s_test.go
+++ b/providers/gcp/gke/gke_k8s_test.go
@@ -9,7 +9,7 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu/kubernetes"
+ "github.com/stackshy/cloudemu/v2/services/kubernetes"
)
func TestSetK8sAPI_CreateRegistersWithAPIServer(t *testing.T) {
diff --git a/providers/gcp/gke/gke_test.go b/providers/gcp/gke/gke_test.go
index 8b88832..50204a9 100644
--- a/providers/gcp/gke/gke_test.go
+++ b/providers/gcp/gke/gke_test.go
@@ -5,7 +5,7 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2/config"
)
func newTestMock() *Mock {
diff --git a/providers/gcp/memorystore/memorystore.go b/providers/gcp/memorystore/memorystore.go
index 88a3fca..6292b21 100644
--- a/providers/gcp/memorystore/memorystore.go
+++ b/providers/gcp/memorystore/memorystore.go
@@ -8,12 +8,12 @@ import (
"strconv"
"time"
- "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/cache/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const defaultRedisPort = 6379
diff --git a/providers/gcp/memorystore/memorystore_test.go b/providers/gcp/memorystore/memorystore_test.go
index fe032d2..f89dc59 100644
--- a/providers/gcp/memorystore/memorystore_test.go
+++ b/providers/gcp/memorystore/memorystore_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/config"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/cache/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/pubsub/pubsub.go b/providers/gcp/pubsub/pubsub.go
index 807711d..f9f8a5b 100644
--- a/providers/gcp/pubsub/pubsub.go
+++ b/providers/gcp/pubsub/pubsub.go
@@ -8,12 +8,12 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/messagequeue/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Compile-time check that Mock implements driver.MessageQueue.
diff --git a/providers/gcp/pubsub/pubsub_test.go b/providers/gcp/pubsub/pubsub_test.go
index bfbace1..6cb88e1 100644
--- a/providers/gcp/pubsub/pubsub_test.go
+++ b/providers/gcp/pubsub/pubsub_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/messagequeue/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/secretmanager/secretmanager.go b/providers/gcp/secretmanager/secretmanager.go
index 8be1c72..995f37e 100644
--- a/providers/gcp/secretmanager/secretmanager.go
+++ b/providers/gcp/secretmanager/secretmanager.go
@@ -6,11 +6,11 @@ import (
"sync"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/secrets/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
// Compile-time check that Mock implements driver.Secrets.
diff --git a/providers/gcp/secretmanager/secretmanager_test.go b/providers/gcp/secretmanager/secretmanager_test.go
index d23bf01..d59b8f8 100644
--- a/providers/gcp/secretmanager/secretmanager_test.go
+++ b/providers/gcp/secretmanager/secretmanager_test.go
@@ -5,8 +5,8 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/secrets/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/secrets/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/providers/gcp/vertexai/clone.go b/providers/gcp/vertexai/clone.go
index 9dc8236..6fb7baa 100644
--- a/providers/gcp/vertexai/clone.go
+++ b/providers/gcp/vertexai/clone.go
@@ -1,6 +1,6 @@
package vertexai
-import "github.com/stackshy/cloudemu/vertexai/driver"
+import "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
// copyLabels deep-copies a label map so stored resources never alias a caller's
// map (and vice-versa on return).
diff --git a/providers/gcp/vertexai/datasets.go b/providers/gcp/vertexai/datasets.go
index fbbb36e..e1f4fb2 100644
--- a/providers/gcp/vertexai/datasets.go
+++ b/providers/gcp/vertexai/datasets.go
@@ -4,8 +4,8 @@ import (
"context"
"strings"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// locationOf extracts the location segment from a resource name, or the
diff --git a/providers/gcp/vertexai/endpoints.go b/providers/gcp/vertexai/endpoints.go
index 64fa6f5..bf11c1b 100644
--- a/providers/gcp/vertexai/endpoints.go
+++ b/providers/gcp/vertexai/endpoints.go
@@ -3,8 +3,8 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func (m *Mock) CreateEndpoint(_ context.Context, cfg driver.EndpointConfig) (*driver.Operation, *driver.Endpoint, error) {
diff --git a/providers/gcp/vertexai/featurestore.go b/providers/gcp/vertexai/featurestore.go
index b95aa7e..deb3732 100644
--- a/providers/gcp/vertexai/featurestore.go
+++ b/providers/gcp/vertexai/featurestore.go
@@ -4,8 +4,8 @@ import (
"context"
"strings"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func (m *Mock) CreateFeatureGroup(_ context.Context, cfg driver.FeatureGroupConfig) (*driver.Operation, *driver.FeatureGroup, error) {
diff --git a/providers/gcp/vertexai/genai.go b/providers/gcp/vertexai/genai.go
index 03c31f6..88d428c 100644
--- a/providers/gcp/vertexai/genai.go
+++ b/providers/gcp/vertexai/genai.go
@@ -5,8 +5,8 @@ import (
"strings"
"time"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// approxTokens is a deterministic, whitespace-based token estimate.
diff --git a/providers/gcp/vertexai/jobs.go b/providers/gcp/vertexai/jobs.go
index a912d98..cbc2fa1 100644
--- a/providers/gcp/vertexai/jobs.go
+++ b/providers/gcp/vertexai/jobs.go
@@ -3,9 +3,9 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/memstore"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// cancelJob copy-then-Sets a job's State to canceled, or returns NotFound.
diff --git a/providers/gcp/vertexai/metadata.go b/providers/gcp/vertexai/metadata.go
index 1a0dfeb..6159d49 100644
--- a/providers/gcp/vertexai/metadata.go
+++ b/providers/gcp/vertexai/metadata.go
@@ -3,8 +3,8 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// --- Metadata stores ---
diff --git a/providers/gcp/vertexai/models.go b/providers/gcp/vertexai/models.go
index 943d806..a56ae82 100644
--- a/providers/gcp/vertexai/models.go
+++ b/providers/gcp/vertexai/models.go
@@ -4,8 +4,8 @@ import (
"context"
"strings"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
//nolint:gocritic // cfg matches the driver signature; copied on entry.
diff --git a/providers/gcp/vertexai/operations.go b/providers/gcp/vertexai/operations.go
index 4b0b206..cd15712 100644
--- a/providers/gcp/vertexai/operations.go
+++ b/providers/gcp/vertexai/operations.go
@@ -4,8 +4,8 @@ import (
"context"
"strings"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// GetOperation returns a recorded operation by name.
diff --git a/providers/gcp/vertexai/pipelines.go b/providers/gcp/vertexai/pipelines.go
index d130e58..f6afed4 100644
--- a/providers/gcp/vertexai/pipelines.go
+++ b/providers/gcp/vertexai/pipelines.go
@@ -3,8 +3,8 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// --- Training pipelines ---
diff --git a/providers/gcp/vertexai/vectorsearch.go b/providers/gcp/vertexai/vectorsearch.go
index f5912b9..9b4191b 100644
--- a/providers/gcp/vertexai/vectorsearch.go
+++ b/providers/gcp/vertexai/vectorsearch.go
@@ -4,8 +4,8 @@ import (
"context"
"strconv"
- "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func (m *Mock) CreateIndex(_ context.Context, cfg driver.IndexConfig) (*driver.Operation, *driver.Index, error) {
@@ -191,7 +191,8 @@ func (m *Mock) FindNeighbors(_ context.Context, indexEndpoint, _ string, _ []flo
count = maxNeighbors
}
- out := make([]driver.Neighbor, 0, count)
+ // count is already clamped to [0, maxNeighbors] above; grow the slice via append.
+ out := make([]driver.Neighbor, 0)
for i := 0; i < count; i++ {
out = append(out, driver.Neighbor{DatapointID: "dp-" + strconv.Itoa(i), Distance: float64(i) * 0.1})
}
diff --git a/providers/gcp/vertexai/vertexai.go b/providers/gcp/vertexai/vertexai.go
index fce7768..7a585dd 100644
--- a/providers/gcp/vertexai/vertexai.go
+++ b/providers/gcp/vertexai/vertexai.go
@@ -14,11 +14,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/internal/memstore"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/memstore"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// Compile-time check that Mock implements the full Vertex AI surface.
diff --git a/providers/gcp/vertexai/vertexai_test.go b/providers/gcp/vertexai/vertexai_test.go
index 87817ee..2698823 100644
--- a/providers/gcp/vertexai/vertexai_test.go
+++ b/providers/gcp/vertexai/vertexai_test.go
@@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func newTestMock() *Mock {
diff --git a/server/aws/aws.go b/server/aws/aws.go
index 15aaa94..da21d60 100644
--- a/server/aws/aws.go
+++ b/server/aws/aws.go
@@ -7,51 +7,54 @@
package aws
import (
- bedrockdriver "github.com/stackshy/cloudemu/bedrock/driver"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
- "github.com/stackshy/cloudemu/kubernetes"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
- mqdriver "github.com/stackshy/cloudemu/messagequeue/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- eksdriver "github.com/stackshy/cloudemu/providers/aws/eks/driver"
- rdbdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/resourcediscovery"
- sagemakerdriver "github.com/stackshy/cloudemu/sagemaker/driver"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/aws/bedrock"
- "github.com/stackshy/cloudemu/server/aws/cloudwatch"
- cloudwatchlogssrv "github.com/stackshy/cloudemu/server/aws/cloudwatchlogs"
- "github.com/stackshy/cloudemu/server/aws/dynamodb"
- "github.com/stackshy/cloudemu/server/aws/ec2"
- "github.com/stackshy/cloudemu/server/aws/ecr"
- "github.com/stackshy/cloudemu/server/aws/eks"
- "github.com/stackshy/cloudemu/server/aws/elasticache"
- "github.com/stackshy/cloudemu/server/aws/elbv2"
- "github.com/stackshy/cloudemu/server/aws/eventbridge"
- "github.com/stackshy/cloudemu/server/aws/iam"
- "github.com/stackshy/cloudemu/server/aws/lambda"
- "github.com/stackshy/cloudemu/server/aws/rds"
- "github.com/stackshy/cloudemu/server/aws/redshift"
- "github.com/stackshy/cloudemu/server/aws/resourceexplorer2"
- "github.com/stackshy/cloudemu/server/aws/resourcegroupstaggingapi"
- "github.com/stackshy/cloudemu/server/aws/route53"
- "github.com/stackshy/cloudemu/server/aws/s3"
- sagemakersrv "github.com/stackshy/cloudemu/server/aws/sagemaker"
- secretsmanagersrv "github.com/stackshy/cloudemu/server/aws/secretsmanager"
- "github.com/stackshy/cloudemu/server/aws/sns"
- "github.com/stackshy/cloudemu/server/aws/sqs"
- sdrv "github.com/stackshy/cloudemu/serverless/driver"
- storagedriver "github.com/stackshy/cloudemu/storage/driver"
+ eksdriver "github.com/stackshy/cloudemu/v2/providers/aws/eks/driver"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/aws/bedrock"
+ "github.com/stackshy/cloudemu/v2/server/aws/cloudwatch"
+ cloudwatchlogssrv "github.com/stackshy/cloudemu/v2/server/aws/cloudwatchlogs"
+ "github.com/stackshy/cloudemu/v2/server/aws/dynamodb"
+ "github.com/stackshy/cloudemu/v2/server/aws/ec2"
+ "github.com/stackshy/cloudemu/v2/server/aws/ecr"
+ "github.com/stackshy/cloudemu/v2/server/aws/eks"
+ "github.com/stackshy/cloudemu/v2/server/aws/elasticache"
+ "github.com/stackshy/cloudemu/v2/server/aws/elbv2"
+ "github.com/stackshy/cloudemu/v2/server/aws/eventbridge"
+ "github.com/stackshy/cloudemu/v2/server/aws/iam"
+ "github.com/stackshy/cloudemu/v2/server/aws/lambda"
+ "github.com/stackshy/cloudemu/v2/server/aws/rds"
+ "github.com/stackshy/cloudemu/v2/server/aws/redshift"
+ "github.com/stackshy/cloudemu/v2/server/aws/resourceexplorer2"
+ "github.com/stackshy/cloudemu/v2/server/aws/resourcegroupstaggingapi"
+ "github.com/stackshy/cloudemu/v2/server/aws/route53"
+ "github.com/stackshy/cloudemu/v2/server/aws/s3"
+ sagemakersrv "github.com/stackshy/cloudemu/v2/server/aws/sagemaker"
+ secretsmanagersrv "github.com/stackshy/cloudemu/v2/server/aws/secretsmanager"
+ "github.com/stackshy/cloudemu/v2/server/aws/sns"
+ "github.com/stackshy/cloudemu/v2/server/aws/sqs"
+ ssmsrv "github.com/stackshy/cloudemu/v2/server/aws/ssm"
+ stssrv "github.com/stackshy/cloudemu/v2/server/aws/sts"
+ bedrockdriver "github.com/stackshy/cloudemu/v2/services/bedrock/driver"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
+ "github.com/stackshy/cloudemu/v2/services/kubernetes"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
+ mqdriver "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
+ ssmdriver "github.com/stackshy/cloudemu/v2/services/parameterstore/driver"
+ rdbdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
+ sagemakerdriver "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
+ sdrv "github.com/stackshy/cloudemu/v2/services/serverless/driver"
+ storagedriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
// Drivers bundles the driver interfaces the AWS server can expose. Leave a
@@ -75,6 +78,9 @@ type Drivers struct {
// SecretsManager serves the Secrets Manager JSON 1.1 protocol against
// the secrets driver.
SecretsManager secretsdriver.Secrets
+ // SSM serves the Systems Manager Parameter Store JSON 1.1 protocol against
+ // the parameterstore driver.
+ SSM ssmdriver.ParameterStore
// CloudWatchLogs serves the CloudWatch Logs JSON 1.1 protocol against the
// logging driver.
CloudWatchLogs logdriver.Logging
@@ -91,6 +97,11 @@ type Drivers struct {
ElastiCache cachedriver.Cache
// SNS serves the SNS query protocol against the notification driver.
SNS notifdriver.Notification
+ // STS serves the AWS STS query protocol (GetCallerIdentity, AssumeRole,
+ // GetSessionToken). It has no backing driver — identity is derived from
+ // AccountID and Region — so it is gated on this bool. Enable it so SDK code
+ // paths that call sts:GetCallerIdentity or sts:AssumeRole on init succeed.
+ STS bool
// K8sAPI is the shared in-memory Kubernetes data-plane API server. It is
// shared with azureserver.Drivers.K8sAPI and gcpserver.Drivers.K8sAPI so a
// kubeconfig issued by any provider's control plane (EKS/AKS/GKE) reaches
@@ -174,6 +185,13 @@ func New(d Drivers) *server.Server {
srv.Register(secretsmanagersrv.New(d.SecretsManager))
}
+ // SSM Parameter Store matches the X-Amz-Target prefix "AmazonSSM." —
+ // disjoint from DynamoDB, SQS, ECR, SageMaker, Secrets Manager, EventBridge,
+ // CloudWatch Logs, and the tagging API.
+ if d.SSM != nil {
+ srv.Register(ssmsrv.New(d.SSM))
+ }
+
// EventBridge matches the X-Amz-Target prefix "AWSEvents." — disjoint from
// DynamoDB, SQS, ECR, SageMaker, Secrets Manager, and the tagging API.
if d.EventBridge != nil {
@@ -218,6 +236,14 @@ func New(d Drivers) *server.Server {
srv.Register(sns.New(d.SNS))
}
+ // STS also speaks the AWS query protocol; its action set (GetCallerIdentity,
+ // AssumeRole, GetSessionToken) is disjoint from RDS, Redshift, IAM, ELBv2,
+ // ElastiCache, SNS, and EC2, so no shadowing occurs. It has no driver, so
+ // it's gated on the STS bool. Registered before the EC2 catch-all.
+ if d.STS {
+ srv.Register(stssrv.New(d.AccountID, d.Region))
+ }
+
if d.EC2 != nil || d.VPC != nil {
srv.Register(ec2.New(d.EC2, d.VPC))
}
diff --git a/server/aws/aws_test.go b/server/aws/aws_test.go
index 0450593..69fcafd 100644
--- a/server/aws/aws_test.go
+++ b/server/aws/aws_test.go
@@ -13,8 +13,8 @@ import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
ddbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/server/aws/bedrock/errors.go b/server/aws/bedrock/errors.go
index c38731f..87bc932 100644
--- a/server/aws/bedrock/errors.go
+++ b/server/aws/bedrock/errors.go
@@ -4,7 +4,7 @@ import (
"encoding/json"
"net/http"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
// errorBody is the JSON body shape Bedrock returns for failures. The SDK reads
diff --git a/server/aws/bedrock/handler.go b/server/aws/bedrock/handler.go
index 2434eea..cafa802 100644
--- a/server/aws/bedrock/handler.go
+++ b/server/aws/bedrock/handler.go
@@ -26,7 +26,7 @@ import (
"net/http"
"strings"
- bedrockdriver "github.com/stackshy/cloudemu/bedrock/driver"
+ bedrockdriver "github.com/stackshy/cloudemu/v2/services/bedrock/driver"
)
const (
diff --git a/server/aws/bedrock/management.go b/server/aws/bedrock/management.go
index 429e709..d055b35 100644
--- a/server/aws/bedrock/management.go
+++ b/server/aws/bedrock/management.go
@@ -3,7 +3,7 @@ package bedrock
import (
"net/http"
- bedrockdriver "github.com/stackshy/cloudemu/bedrock/driver"
+ bedrockdriver "github.com/stackshy/cloudemu/v2/services/bedrock/driver"
)
// --- wire types ---
diff --git a/server/aws/bedrock/operations.go b/server/aws/bedrock/operations.go
index 1db349e..27b8856 100644
--- a/server/aws/bedrock/operations.go
+++ b/server/aws/bedrock/operations.go
@@ -5,7 +5,7 @@ import (
"io"
"net/http"
- bedrockdriver "github.com/stackshy/cloudemu/bedrock/driver"
+ bedrockdriver "github.com/stackshy/cloudemu/v2/services/bedrock/driver"
)
func (h *Handler) listFoundationModels(w http.ResponseWriter, r *http.Request) {
diff --git a/server/aws/bedrock/sdk_roundtrip_test.go b/server/aws/bedrock/sdk_roundtrip_test.go
index 31ee47a..08f0a76 100644
--- a/server/aws/bedrock/sdk_roundtrip_test.go
+++ b/server/aws/bedrock/sdk_roundtrip_test.go
@@ -16,8 +16,8 @@ import (
runtimetypes "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
"github.com/aws/smithy-go"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
const claudeModel = "anthropic.claude-3-sonnet-20240229-v1:0"
diff --git a/server/aws/cloudwatch/handler.go b/server/aws/cloudwatch/handler.go
index 6324d67..5943fcf 100644
--- a/server/aws/cloudwatch/handler.go
+++ b/server/aws/cloudwatch/handler.go
@@ -19,8 +19,8 @@ import (
"github.com/fxamacker/cbor/v2"
- cerrors "github.com/stackshy/cloudemu/errors"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/server/aws/cloudwatch/ops.go b/server/aws/cloudwatch/ops.go
index a129ec0..8a90eb8 100644
--- a/server/aws/cloudwatch/ops.go
+++ b/server/aws/cloudwatch/ops.go
@@ -6,7 +6,7 @@ import (
"github.com/fxamacker/cbor/v2"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// putMetricDataInput mirrors the AWS wire shape for the operation. Field
diff --git a/server/aws/cloudwatchlogs/handler.go b/server/aws/cloudwatchlogs/handler.go
index bffd65f..f336a4e 100644
--- a/server/aws/cloudwatchlogs/handler.go
+++ b/server/aws/cloudwatchlogs/handler.go
@@ -20,9 +20,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
// targetPrefix roots every CloudWatch Logs X-Amz-Target value. The API version
diff --git a/server/aws/cloudwatchlogs/operations.go b/server/aws/cloudwatchlogs/operations.go
index 9452dff..8c9142b 100644
--- a/server/aws/cloudwatchlogs/operations.go
+++ b/server/aws/cloudwatchlogs/operations.go
@@ -4,8 +4,8 @@ import (
"net/http"
"strings"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
// --- log groups ---
diff --git a/server/aws/cloudwatchlogs/sdk_roundtrip_test.go b/server/aws/cloudwatchlogs/sdk_roundtrip_test.go
index 4ab0750..0db4143 100644
--- a/server/aws/cloudwatchlogs/sdk_roundtrip_test.go
+++ b/server/aws/cloudwatchlogs/sdk_roundtrip_test.go
@@ -13,8 +13,8 @@ import (
cwl "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cwltypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newLogsClient(t *testing.T) *cwl.Client {
diff --git a/server/aws/cloudwatchlogs/types.go b/server/aws/cloudwatchlogs/types.go
index d88e265..37a4792 100644
--- a/server/aws/cloudwatchlogs/types.go
+++ b/server/aws/cloudwatchlogs/types.go
@@ -3,7 +3,7 @@ package cloudwatchlogs
import (
"time"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
// --- request envelopes ---
diff --git a/server/aws/dynamodb/advanced.go b/server/aws/dynamodb/advanced.go
index dd602c2..a665ab0 100644
--- a/server/aws/dynamodb/advanced.go
+++ b/server/aws/dynamodb/advanced.go
@@ -5,9 +5,9 @@ import (
"net/http"
"strings"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
)
// updateItem handles UpdateItem. Supports the common cases:
diff --git a/server/aws/dynamodb/handler.go b/server/aws/dynamodb/handler.go
index 52a6a00..edae96a 100644
--- a/server/aws/dynamodb/handler.go
+++ b/server/aws/dynamodb/handler.go
@@ -8,9 +8,9 @@ import (
"net/http"
"strings"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
)
const targetPrefix = "DynamoDB_20120810."
diff --git a/server/aws/ec2/auto_scaling.go b/server/aws/ec2/auto_scaling.go
index ee62ce1..6e12325 100644
--- a/server/aws/ec2/auto_scaling.go
+++ b/server/aws/ec2/auto_scaling.go
@@ -5,8 +5,8 @@ import (
"net/http"
"strconv"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// asgNamespace is the XML namespace AutoScaling responses carry. Distinct
diff --git a/server/aws/ec2/ec2_phase2_test.go b/server/aws/ec2/ec2_phase2_test.go
index 482c46b..b5be232 100644
--- a/server/aws/ec2/ec2_phase2_test.go
+++ b/server/aws/ec2/ec2_phase2_test.go
@@ -6,10 +6,10 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu/config"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- awsec2 "github.com/stackshy/cloudemu/providers/aws/ec2"
- awsvpc "github.com/stackshy/cloudemu/providers/aws/vpc"
+ "github.com/stackshy/cloudemu/v2/config"
+ awsec2 "github.com/stackshy/cloudemu/v2/providers/aws/ec2"
+ awsvpc "github.com/stackshy/cloudemu/v2/providers/aws/vpc"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// newFullHandler wires compute and VPC drivers for Phase-2 tests (no monitoring).
@@ -69,11 +69,11 @@ func TestRouteVPCDispatchesAllActions(t *testing.T) {
// Authorize an ingress rule.
rr = do(t, h, http.MethodPost, "/", url.Values{
- "Action": {"AuthorizeSecurityGroupIngress"},
- "GroupId": {sgID},
- "IpPermissions.1.IpProtocol": {"tcp"},
- "IpPermissions.1.FromPort": {"80"},
- "IpPermissions.1.ToPort": {"80"},
+ "Action": {"AuthorizeSecurityGroupIngress"},
+ "GroupId": {sgID},
+ "IpPermissions.1.IpProtocol": {"tcp"},
+ "IpPermissions.1.FromPort": {"80"},
+ "IpPermissions.1.ToPort": {"80"},
"IpPermissions.1.IpRanges.1.CidrIp": {"0.0.0.0/0"},
})
if rr.Code != http.StatusOK {
@@ -82,10 +82,10 @@ func TestRouteVPCDispatchesAllActions(t *testing.T) {
// Egress, Revoke variants use the same parser path.
rr = do(t, h, http.MethodPost, "/", url.Values{
- "Action": {"AuthorizeSecurityGroupEgress"},
- "GroupId": {sgID},
- "IpPermissions.1.IpProtocol": {"-1"},
- "IpPermissions.1.IpRanges.1.CidrIp": {"0.0.0.0/0"},
+ "Action": {"AuthorizeSecurityGroupEgress"},
+ "GroupId": {sgID},
+ "IpPermissions.1.IpProtocol": {"-1"},
+ "IpPermissions.1.IpRanges.1.CidrIp": {"0.0.0.0/0"},
})
if rr.Code != http.StatusOK {
t.Errorf("AuthorizeSecurityGroupEgress = %d", rr.Code)
@@ -187,7 +187,7 @@ func TestVPCOpsUnknownIDReturnError(t *testing.T) {
{
"AttachInternetGateway",
url.Values{
- "Action": {"AttachInternetGateway"},
+ "Action": {"AttachInternetGateway"},
"InternetGatewayId": {"igw-ghost"}, "VpcId": {"vpc-ghost"},
},
},
@@ -232,7 +232,7 @@ func TestAuthorizeSecurityGroupEmptyRulesReturns400(t *testing.T) {
vpcID := between(vpc.Body.String(), "", "")
create := do(t, h, http.MethodPost, "/", url.Values{
- "Action": {"CreateSecurityGroup"},
+ "Action": {"CreateSecurityGroup"},
"GroupName": {"x"}, "GroupDescription": {"x"}, "VpcId": {vpcID},
})
diff --git a/server/aws/ec2/ec2_phase3_test.go b/server/aws/ec2/ec2_phase3_test.go
index ca03b42..929b7c7 100644
--- a/server/aws/ec2/ec2_phase3_test.go
+++ b/server/aws/ec2/ec2_phase3_test.go
@@ -6,7 +6,7 @@ import (
"strings"
"testing"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
func TestRouteVolumesDispatch(t *testing.T) {
diff --git a/server/aws/ec2/ec2_test.go b/server/aws/ec2/ec2_test.go
index 36d0e7f..0ded106 100644
--- a/server/aws/ec2/ec2_test.go
+++ b/server/aws/ec2/ec2_test.go
@@ -10,11 +10,11 @@ import (
"strings"
"testing"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/config"
- cerrors "github.com/stackshy/cloudemu/errors"
- awsec2 "github.com/stackshy/cloudemu/providers/aws/ec2"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ awsec2 "github.com/stackshy/cloudemu/v2/providers/aws/ec2"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// newHandler returns a Handler wired to a fresh in-memory provider. Phase-1
diff --git a/server/aws/ec2/flow_log.go b/server/aws/ec2/flow_log.go
index f84fdd0..de56696 100644
--- a/server/aws/ec2/flow_log.go
+++ b/server/aws/ec2/flow_log.go
@@ -4,8 +4,8 @@ import (
"encoding/xml"
"net/http"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type flowLogXML struct {
diff --git a/server/aws/ec2/handler.go b/server/aws/ec2/handler.go
index d9a4169..cf2779c 100644
--- a/server/aws/ec2/handler.go
+++ b/server/aws/ec2/handler.go
@@ -7,10 +7,10 @@ import (
"net/http"
"strings"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// formContentType is the request Content-Type AWS SDKs use for the query
diff --git a/server/aws/ec2/image.go b/server/aws/ec2/image.go
index 1f67832..63a7b5e 100644
--- a/server/aws/ec2/image.go
+++ b/server/aws/ec2/image.go
@@ -4,8 +4,8 @@ import (
"encoding/xml"
"net/http"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
type imageXML struct {
diff --git a/server/aws/ec2/internet_gateway.go b/server/aws/ec2/internet_gateway.go
index 2fb5cfb..0af2b7e 100644
--- a/server/aws/ec2/internet_gateway.go
+++ b/server/aws/ec2/internet_gateway.go
@@ -4,8 +4,8 @@ import (
"encoding/xml"
"net/http"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type igwAttachmentXML struct {
diff --git a/server/aws/ec2/key_pair.go b/server/aws/ec2/key_pair.go
index 7d340f2..982381d 100644
--- a/server/aws/ec2/key_pair.go
+++ b/server/aws/ec2/key_pair.go
@@ -4,8 +4,8 @@ import (
"encoding/xml"
"net/http"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// keyPairSummaryXML is one - in DescribeKeyPairs responses. It omits
diff --git a/server/aws/ec2/launch_template.go b/server/aws/ec2/launch_template.go
index 2c702b8..ba44633 100644
--- a/server/aws/ec2/launch_template.go
+++ b/server/aws/ec2/launch_template.go
@@ -4,8 +4,8 @@ import (
"encoding/xml"
"net/http"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
type launchTemplateXML struct {
diff --git a/server/aws/ec2/nat_gateway.go b/server/aws/ec2/nat_gateway.go
index b5ab4c9..0e99950 100644
--- a/server/aws/ec2/nat_gateway.go
+++ b/server/aws/ec2/nat_gateway.go
@@ -4,8 +4,8 @@ import (
"encoding/xml"
"net/http"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type natGatewayXML struct {
diff --git a/server/aws/ec2/network_acl.go b/server/aws/ec2/network_acl.go
index ee1e5f3..90d2004 100644
--- a/server/aws/ec2/network_acl.go
+++ b/server/aws/ec2/network_acl.go
@@ -5,8 +5,8 @@ import (
"net/http"
"strconv"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type networkACLEntryXML struct {
diff --git a/server/aws/ec2/operations.go b/server/aws/ec2/operations.go
index 0286ac1..30399e0 100644
--- a/server/aws/ec2/operations.go
+++ b/server/aws/ec2/operations.go
@@ -4,9 +4,9 @@ import (
"net/http"
"strconv"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// reservationPrefix is our own reservation ID prefix. Real AWS uses "r-".
diff --git a/server/aws/ec2/peering.go b/server/aws/ec2/peering.go
index 14e29ab..5ffaead 100644
--- a/server/aws/ec2/peering.go
+++ b/server/aws/ec2/peering.go
@@ -4,8 +4,8 @@ import (
"encoding/xml"
"net/http"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type peeringStatusXML struct {
diff --git a/server/aws/ec2/route_table.go b/server/aws/ec2/route_table.go
index afb1e4e..d17bbd8 100644
--- a/server/aws/ec2/route_table.go
+++ b/server/aws/ec2/route_table.go
@@ -4,8 +4,8 @@ import (
"encoding/xml"
"net/http"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Route target types understood by the driver; mirror the AWS route-target
diff --git a/server/aws/ec2/security_group.go b/server/aws/ec2/security_group.go
index fb62d80..51610ce 100644
--- a/server/aws/ec2/security_group.go
+++ b/server/aws/ec2/security_group.go
@@ -8,9 +8,9 @@ import (
"net/url"
"strconv"
- cerrors "github.com/stackshy/cloudemu/errors"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
func errMissingGroupID() error {
diff --git a/server/aws/ec2/snapshot.go b/server/aws/ec2/snapshot.go
index 06db44c..3debdc4 100644
--- a/server/aws/ec2/snapshot.go
+++ b/server/aws/ec2/snapshot.go
@@ -4,8 +4,8 @@ import (
"encoding/xml"
"net/http"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
type snapshotXML struct {
diff --git a/server/aws/ec2/spot.go b/server/aws/ec2/spot.go
index 2245aab..3b8578b 100644
--- a/server/aws/ec2/spot.go
+++ b/server/aws/ec2/spot.go
@@ -5,8 +5,8 @@ import (
"net/http"
"strconv"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
type spotRequestXML struct {
diff --git a/server/aws/ec2/subnet.go b/server/aws/ec2/subnet.go
index d3db8c0..ad36171 100644
--- a/server/aws/ec2/subnet.go
+++ b/server/aws/ec2/subnet.go
@@ -4,8 +4,8 @@ import (
"encoding/xml"
"net/http"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// subnetAvailableIPs is the count reported to SDK clients. Real AWS varies
diff --git a/server/aws/ec2/volume.go b/server/aws/ec2/volume.go
index 58eb175..2aab06a 100644
--- a/server/aws/ec2/volume.go
+++ b/server/aws/ec2/volume.go
@@ -5,8 +5,8 @@ import (
"net/http"
"strconv"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// Default volume type AWS uses when the caller omits VolumeType.
diff --git a/server/aws/ec2/vpc.go b/server/aws/ec2/vpc.go
index d572299..91864c7 100644
--- a/server/aws/ec2/vpc.go
+++ b/server/aws/ec2/vpc.go
@@ -5,8 +5,8 @@ import (
"net/http"
"sort"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// stateAvailable is the "ready for use" state name shared by VPCs, subnets,
diff --git a/server/aws/ec2_test.go b/server/aws/ec2_test.go
index 2c9cdfb..c2bf0a8 100644
--- a/server/aws/ec2_test.go
+++ b/server/aws/ec2_test.go
@@ -20,8 +20,8 @@ import (
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -322,7 +322,7 @@ func TestEC2StartInstancesUnknownIDReturnsError(t *testing.T) {
// behavior: calling StartInstances on an already-running instance returns
// 200 with currentState=running rather than IncorrectInstanceState.
//
-// Issue: https://github.com/stackshy/cloudemu/issues/152
+// Issue: https://github.com/stackshy/cloudemu/v2/issues/152
func TestEC2StartInstancesIdempotentOnRunning(t *testing.T) {
client := newEC2Client(t)
ctx := context.Background()
diff --git a/server/aws/ecr/handler.go b/server/aws/ecr/handler.go
index 05c855d..cdbded0 100644
--- a/server/aws/ecr/handler.go
+++ b/server/aws/ecr/handler.go
@@ -11,9 +11,9 @@ import (
"net/http"
"strings"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
const targetPrefix = "AmazonEC2ContainerRegistry_V20150921."
diff --git a/server/aws/ecr/operations.go b/server/aws/ecr/operations.go
index d6ba4b6..682928d 100644
--- a/server/aws/ecr/operations.go
+++ b/server/aws/ecr/operations.go
@@ -3,8 +3,8 @@ package ecr
import (
"net/http"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
func (h *Handler) createRepository(w http.ResponseWriter, r *http.Request) {
diff --git a/server/aws/ecr/sdk_roundtrip_test.go b/server/aws/ecr/sdk_roundtrip_test.go
index dc73121..dfa625f 100644
--- a/server/aws/ecr/sdk_roundtrip_test.go
+++ b/server/aws/ecr/sdk_roundtrip_test.go
@@ -12,8 +12,8 @@ import (
awsecr "github.com/aws/aws-sdk-go-v2/service/ecr"
ecrtypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
const sampleManifest = `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json"}`
diff --git a/server/aws/ecr/types.go b/server/aws/ecr/types.go
index 36a83e5..00923cd 100644
--- a/server/aws/ecr/types.go
+++ b/server/aws/ecr/types.go
@@ -3,7 +3,7 @@ package ecr
import (
"time"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
type tagJSON struct {
diff --git a/server/aws/eks/errors.go b/server/aws/eks/errors.go
index 9b532cc..6ec4ca8 100644
--- a/server/aws/eks/errors.go
+++ b/server/aws/eks/errors.go
@@ -4,7 +4,7 @@ import (
"encoding/json"
"net/http"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
// errorBody is the JSON body shape EKS returns for failures. The SDK reads
diff --git a/server/aws/eks/handler.go b/server/aws/eks/handler.go
index 06264aa..a918910 100644
--- a/server/aws/eks/handler.go
+++ b/server/aws/eks/handler.go
@@ -22,7 +22,7 @@ import (
"net/http"
"strings"
- eksdriver "github.com/stackshy/cloudemu/providers/aws/eks/driver"
+ eksdriver "github.com/stackshy/cloudemu/v2/providers/aws/eks/driver"
)
const (
diff --git a/server/aws/eks/operations.go b/server/aws/eks/operations.go
index 55bac19..57b5df4 100644
--- a/server/aws/eks/operations.go
+++ b/server/aws/eks/operations.go
@@ -4,7 +4,7 @@ import (
"math"
"net/http"
- eksdriver "github.com/stackshy/cloudemu/providers/aws/eks/driver"
+ eksdriver "github.com/stackshy/cloudemu/v2/providers/aws/eks/driver"
)
// safeInt32 narrows an int to int32, clamping at math.MaxInt32 / math.MinInt32.
diff --git a/server/aws/eks/sdk_dataplane_test.go b/server/aws/eks/sdk_dataplane_test.go
index baffca5..fd0ad65 100644
--- a/server/aws/eks/sdk_dataplane_test.go
+++ b/server/aws/eks/sdk_dataplane_test.go
@@ -23,9 +23,9 @@ import (
kubescheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
- "github.com/stackshy/cloudemu"
- cloudkube "github.com/stackshy/cloudemu/kubernetes"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
+ cloudkube "github.com/stackshy/cloudemu/v2/services/kubernetes"
)
// TestSDKEKSDataPlane_NamespaceAndConfigMap drives the full path:
diff --git a/server/aws/eks/sdk_dataplane_watch_test.go b/server/aws/eks/sdk_dataplane_watch_test.go
index 01c110b..461e50a 100644
--- a/server/aws/eks/sdk_dataplane_watch_test.go
+++ b/server/aws/eks/sdk_dataplane_watch_test.go
@@ -21,9 +21,9 @@ import (
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
- "github.com/stackshy/cloudemu"
- cloudkube "github.com/stackshy/cloudemu/kubernetes"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
+ cloudkube "github.com/stackshy/cloudemu/v2/services/kubernetes"
)
// TestSDKEKSDataPlane_InformerObservesAddAndDelete drives a Phase-4
diff --git a/server/aws/eks/sdk_dataplane_workloads_test.go b/server/aws/eks/sdk_dataplane_workloads_test.go
index 42dd5ae..d9cc1b1 100644
--- a/server/aws/eks/sdk_dataplane_workloads_test.go
+++ b/server/aws/eks/sdk_dataplane_workloads_test.go
@@ -18,9 +18,9 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "github.com/stackshy/cloudemu"
- cloudkube "github.com/stackshy/cloudemu/kubernetes"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
+ cloudkube "github.com/stackshy/cloudemu/v2/services/kubernetes"
)
// TestSDKEKSDataPlane_FullWorkloadStack walks through the resources a real
diff --git a/server/aws/eks/sdk_roundtrip_test.go b/server/aws/eks/sdk_roundtrip_test.go
index 9f2bd79..f826dfd 100644
--- a/server/aws/eks/sdk_roundtrip_test.go
+++ b/server/aws/eks/sdk_roundtrip_test.go
@@ -12,8 +12,8 @@ import (
awseks "github.com/aws/aws-sdk-go-v2/service/eks"
ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newSDKClient(t *testing.T) *awseks.Client {
diff --git a/server/aws/elasticache/handler.go b/server/aws/elasticache/handler.go
index d956113..8c5b02e 100644
--- a/server/aws/elasticache/handler.go
+++ b/server/aws/elasticache/handler.go
@@ -23,9 +23,9 @@ import (
"net/http"
"strings"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
// Namespace is the XML namespace for AWS ElastiCache responses.
diff --git a/server/aws/elasticache/operations.go b/server/aws/elasticache/operations.go
index 53c0982..cce0d63 100644
--- a/server/aws/elasticache/operations.go
+++ b/server/aws/elasticache/operations.go
@@ -5,8 +5,8 @@ import (
"net/url"
"strconv"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
// parseTags parses ElastiCache-style Tags.Tag.N.{Key,Value} entries.
diff --git a/server/aws/elasticache/sdk_roundtrip_test.go b/server/aws/elasticache/sdk_roundtrip_test.go
index 6b6c23f..1179b29 100644
--- a/server/aws/elasticache/sdk_roundtrip_test.go
+++ b/server/aws/elasticache/sdk_roundtrip_test.go
@@ -14,8 +14,8 @@ import (
ectypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types"
"github.com/aws/smithy-go"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newSDKClient(t *testing.T) *awselasticache.Client {
diff --git a/server/aws/elasticache/types.go b/server/aws/elasticache/types.go
index 6c8c993..2b2cc88 100644
--- a/server/aws/elasticache/types.go
+++ b/server/aws/elasticache/types.go
@@ -5,7 +5,7 @@ import (
"strconv"
"strings"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
// All ElastiCache query-protocol responses are wrapped in with a
diff --git a/server/aws/elbv2/handler.go b/server/aws/elbv2/handler.go
index 5f8d213..8c84495 100644
--- a/server/aws/elbv2/handler.go
+++ b/server/aws/elbv2/handler.go
@@ -17,9 +17,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
// Namespace is the XML namespace for AWS ELBv2 responses.
diff --git a/server/aws/elbv2/operations.go b/server/aws/elbv2/operations.go
index 612b902..de78f6f 100644
--- a/server/aws/elbv2/operations.go
+++ b/server/aws/elbv2/operations.go
@@ -5,9 +5,9 @@ import (
"net/url"
"strconv"
- cerrors "github.com/stackshy/cloudemu/errors"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
// --- load balancers ---
diff --git a/server/aws/elbv2/sdk_roundtrip_test.go b/server/aws/elbv2/sdk_roundtrip_test.go
index f7fa9e4..02e97fc 100644
--- a/server/aws/elbv2/sdk_roundtrip_test.go
+++ b/server/aws/elbv2/sdk_roundtrip_test.go
@@ -13,8 +13,8 @@ import (
elbtypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
"github.com/aws/smithy-go"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newSDKClient(t *testing.T) *elb.Client {
diff --git a/server/aws/elbv2/types.go b/server/aws/elbv2/types.go
index de59f0f..2df9354 100644
--- a/server/aws/elbv2/types.go
+++ b/server/aws/elbv2/types.go
@@ -3,7 +3,7 @@ package elbv2
import (
"encoding/xml"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
// All ELBv2 query-protocol responses are wrapped in with a
diff --git a/server/aws/eventbridge/handler.go b/server/aws/eventbridge/handler.go
index b753f20..dcee2cf 100644
--- a/server/aws/eventbridge/handler.go
+++ b/server/aws/eventbridge/handler.go
@@ -13,9 +13,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
const targetPrefix = "AWSEvents."
diff --git a/server/aws/eventbridge/operations.go b/server/aws/eventbridge/operations.go
index d7aed2e..3c162c5 100644
--- a/server/aws/eventbridge/operations.go
+++ b/server/aws/eventbridge/operations.go
@@ -3,8 +3,8 @@ package eventbridge
import (
"net/http"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
// --- event buses ---
diff --git a/server/aws/eventbridge/sdk_roundtrip_test.go b/server/aws/eventbridge/sdk_roundtrip_test.go
index 14524db..3339920 100644
--- a/server/aws/eventbridge/sdk_roundtrip_test.go
+++ b/server/aws/eventbridge/sdk_roundtrip_test.go
@@ -12,8 +12,8 @@ import (
awseb "github.com/aws/aws-sdk-go-v2/service/eventbridge"
ebtypes "github.com/aws/aws-sdk-go-v2/service/eventbridge/types"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newEventBridgeClient(t *testing.T) *awseb.Client {
diff --git a/server/aws/eventbridge/types.go b/server/aws/eventbridge/types.go
index 5ef579e..a7563dc 100644
--- a/server/aws/eventbridge/types.go
+++ b/server/aws/eventbridge/types.go
@@ -3,7 +3,7 @@ package eventbridge
import (
"time"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
// defaultBusName is the implicit bus EventBridge routes to when a request omits
diff --git a/server/aws/iam/errors_sdk_test.go b/server/aws/iam/errors_sdk_test.go
index 8b821d5..cd9aa7e 100644
--- a/server/aws/iam/errors_sdk_test.go
+++ b/server/aws/iam/errors_sdk_test.go
@@ -12,8 +12,8 @@ import (
awsiam "github.com/aws/aws-sdk-go-v2/service/iam"
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
// newClient is a shared helper for error-path tests. Lives here (rather than
diff --git a/server/aws/iam/handler.go b/server/aws/iam/handler.go
index c943026..95d1b66 100644
--- a/server/aws/iam/handler.go
+++ b/server/aws/iam/handler.go
@@ -14,9 +14,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
// Namespace is the XML namespace for AWS IAM responses. Matches the real
diff --git a/server/aws/iam/operations.go b/server/aws/iam/operations.go
index 0eb6c03..6846259 100644
--- a/server/aws/iam/operations.go
+++ b/server/aws/iam/operations.go
@@ -6,8 +6,8 @@ import (
"net/url"
"strconv"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
// defaultPolicyVersionID is the version a freshly created policy starts with.
diff --git a/server/aws/iam/sdk_roundtrip_test.go b/server/aws/iam/sdk_roundtrip_test.go
index bf410a8..07f98d9 100644
--- a/server/aws/iam/sdk_roundtrip_test.go
+++ b/server/aws/iam/sdk_roundtrip_test.go
@@ -12,8 +12,8 @@ import (
awsiam "github.com/aws/aws-sdk-go-v2/service/iam"
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
const trustPolicy = `{
diff --git a/server/aws/iam/xml.go b/server/aws/iam/xml.go
index a4c7b08..9334d03 100644
--- a/server/aws/iam/xml.go
+++ b/server/aws/iam/xml.go
@@ -3,7 +3,7 @@ package iam
import (
"encoding/xml"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
// Every IAM query-protocol response is wrapped in with a
diff --git a/server/aws/lambda/handler.go b/server/aws/lambda/handler.go
index bcf144e..bce82ef 100644
--- a/server/aws/lambda/handler.go
+++ b/server/aws/lambda/handler.go
@@ -15,8 +15,8 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- sdrv "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ sdrv "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// pathPrefix is the Lambda API version prefix every control-plane URL starts
diff --git a/server/aws/lambda/lambda_test.go b/server/aws/lambda/lambda_test.go
index d66ff88..8003e09 100644
--- a/server/aws/lambda/lambda_test.go
+++ b/server/aws/lambda/lambda_test.go
@@ -10,10 +10,10 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu"
- awsprov "github.com/stackshy/cloudemu/providers/aws"
- "github.com/stackshy/cloudemu/server/aws/lambda"
- sdrv "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2"
+ awsprov "github.com/stackshy/cloudemu/v2/providers/aws"
+ "github.com/stackshy/cloudemu/v2/server/aws/lambda"
+ sdrv "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
func newServer(t *testing.T) (*httptest.Server, *awsprov.Provider) {
diff --git a/server/aws/lambda/sdk_roundtrip_test.go b/server/aws/lambda/sdk_roundtrip_test.go
index fa3052d..1901cce 100644
--- a/server/aws/lambda/sdk_roundtrip_test.go
+++ b/server/aws/lambda/sdk_roundtrip_test.go
@@ -12,9 +12,9 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials"
awslambda "github.com/aws/aws-sdk-go-v2/service/lambda"
lambdatypes "github.com/aws/aws-sdk-go-v2/service/lambda/types"
- "github.com/stackshy/cloudemu"
- awsprovider "github.com/stackshy/cloudemu/providers/aws"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsprovider "github.com/stackshy/cloudemu/v2/providers/aws"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newSDKClient(t *testing.T) (*awslambda.Client, *awsprovider.Provider) {
diff --git a/server/aws/rds/docdb_sdk_roundtrip_test.go b/server/aws/rds/docdb_sdk_roundtrip_test.go
index 71fd37d..af26c01 100644
--- a/server/aws/rds/docdb_sdk_roundtrip_test.go
+++ b/server/aws/rds/docdb_sdk_roundtrip_test.go
@@ -10,8 +10,8 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials"
awsdocdb "github.com/aws/aws-sdk-go-v2/service/docdb"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
// DocumentDB ships its own aws-sdk-go-v2 client but speaks the exact same
diff --git a/server/aws/rds/errors.go b/server/aws/rds/errors.go
index e13736d..f527fd4 100644
--- a/server/aws/rds/errors.go
+++ b/server/aws/rds/errors.go
@@ -1,7 +1,7 @@
package rds
import (
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
func errInstanceNotFound(id string) error {
diff --git a/server/aws/rds/handler.go b/server/aws/rds/handler.go
index 8328c9b..9347ae3 100644
--- a/server/aws/rds/handler.go
+++ b/server/aws/rds/handler.go
@@ -14,9 +14,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// Namespace is the XML namespace for AWS RDS responses.
diff --git a/server/aws/rds/neptune_sdk_roundtrip_test.go b/server/aws/rds/neptune_sdk_roundtrip_test.go
index 18a9d96..13a4574 100644
--- a/server/aws/rds/neptune_sdk_roundtrip_test.go
+++ b/server/aws/rds/neptune_sdk_roundtrip_test.go
@@ -10,8 +10,8 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials"
awsneptune "github.com/aws/aws-sdk-go-v2/service/neptune"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
// Neptune ships its own aws-sdk-go-v2 client but speaks the exact same
diff --git a/server/aws/rds/operations.go b/server/aws/rds/operations.go
index 18c6f3d..0acdefe 100644
--- a/server/aws/rds/operations.go
+++ b/server/aws/rds/operations.go
@@ -5,8 +5,8 @@ import (
"net/url"
"strconv"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// instanceFromForm pulls the common DBInstance fields out of a form. Used by
diff --git a/server/aws/rds/sdk_roundtrip_test.go b/server/aws/rds/sdk_roundtrip_test.go
index 48d2381..e215f79 100644
--- a/server/aws/rds/sdk_roundtrip_test.go
+++ b/server/aws/rds/sdk_roundtrip_test.go
@@ -11,8 +11,8 @@ import (
awsec2 "github.com/aws/aws-sdk-go-v2/service/ec2"
awsrds "github.com/aws/aws-sdk-go-v2/service/rds"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newSDKClient(t *testing.T) *awsrds.Client {
diff --git a/server/aws/rds/xml.go b/server/aws/rds/xml.go
index be1c244..94ab8c8 100644
--- a/server/aws/rds/xml.go
+++ b/server/aws/rds/xml.go
@@ -4,7 +4,7 @@ import (
"encoding/xml"
"strconv"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// All RDS query-protocol responses are wrapped in with a
diff --git a/server/aws/redshift/errors.go b/server/aws/redshift/errors.go
index effc89f..8ab2889 100644
--- a/server/aws/redshift/errors.go
+++ b/server/aws/redshift/errors.go
@@ -1,7 +1,7 @@
package redshift
import (
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
func errClusterNotFound(id string) error {
diff --git a/server/aws/redshift/handler.go b/server/aws/redshift/handler.go
index 6abf90c..806ff92 100644
--- a/server/aws/redshift/handler.go
+++ b/server/aws/redshift/handler.go
@@ -15,9 +15,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- rdbdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ rdbdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// Namespace is the XML namespace for AWS Redshift responses.
diff --git a/server/aws/redshift/operations.go b/server/aws/redshift/operations.go
index 5fea17b..01c3c6a 100644
--- a/server/aws/redshift/operations.go
+++ b/server/aws/redshift/operations.go
@@ -5,8 +5,8 @@ import (
"net/url"
"strconv"
- rdbdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ rdbdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// clusterConfigFromForm pulls the relevant Cluster fields out of a form. Used
diff --git a/server/aws/redshift/sdk_roundtrip_test.go b/server/aws/redshift/sdk_roundtrip_test.go
index ddba840..2ff3581 100644
--- a/server/aws/redshift/sdk_roundtrip_test.go
+++ b/server/aws/redshift/sdk_roundtrip_test.go
@@ -12,8 +12,8 @@ import (
awsrds "github.com/aws/aws-sdk-go-v2/service/rds"
awsredshift "github.com/aws/aws-sdk-go-v2/service/redshift"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newSDKClient(t *testing.T) *awsredshift.Client {
diff --git a/server/aws/redshift/xml.go b/server/aws/redshift/xml.go
index 2f27b96..9f258bb 100644
--- a/server/aws/redshift/xml.go
+++ b/server/aws/redshift/xml.go
@@ -4,7 +4,7 @@ import (
"encoding/xml"
"strconv"
- rdbdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ rdbdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// Redshift query-protocol responses are wrapped in with a
diff --git a/server/aws/resourceexplorer2/filter.go b/server/aws/resourceexplorer2/filter.go
index 3f8f2d9..cb8d07b 100644
--- a/server/aws/resourceexplorer2/filter.go
+++ b/server/aws/resourceexplorer2/filter.go
@@ -3,7 +3,7 @@ package resourceexplorer2
import (
"strings"
- "github.com/stackshy/cloudemu/resourcediscovery"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
)
// parseFilter parses Resource Explorer's documented query subset into a
diff --git a/server/aws/resourceexplorer2/handler.go b/server/aws/resourceexplorer2/handler.go
index f2efbc7..40e626b 100644
--- a/server/aws/resourceexplorer2/handler.go
+++ b/server/aws/resourceexplorer2/handler.go
@@ -22,10 +22,10 @@ import (
"sync"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/internal/idgen"
- "github.com/stackshy/cloudemu/resourcediscovery"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
)
// AWS service identifiers used in Resource Explorer ResourceType strings
diff --git a/server/aws/resourceexplorer2/sdk_test.go b/server/aws/resourceexplorer2/sdk_test.go
index 7e8e0fa..e5cd45e 100644
--- a/server/aws/resourceexplorer2/sdk_test.go
+++ b/server/aws/resourceexplorer2/sdk_test.go
@@ -16,10 +16,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
func TestSDKResourceExplorer2(t *testing.T) {
diff --git a/server/aws/resourcegroupstaggingapi/handler.go b/server/aws/resourcegroupstaggingapi/handler.go
index 54b5775..961fd5d 100644
--- a/server/aws/resourcegroupstaggingapi/handler.go
+++ b/server/aws/resourcegroupstaggingapi/handler.go
@@ -7,9 +7,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/resourcediscovery"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
)
const targetPrefix = "ResourceGroupsTaggingAPI_20170126."
diff --git a/server/aws/resourcegroupstaggingapi/sdk_test.go b/server/aws/resourcegroupstaggingapi/sdk_test.go
index 7375e0e..628802f 100644
--- a/server/aws/resourcegroupstaggingapi/sdk_test.go
+++ b/server/aws/resourcegroupstaggingapi/sdk_test.go
@@ -17,10 +17,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
func TestSDKResourceGroupsTagging(t *testing.T) {
diff --git a/server/aws/route53/handler.go b/server/aws/route53/handler.go
index 14361a8..8028585 100644
--- a/server/aws/route53/handler.go
+++ b/server/aws/route53/handler.go
@@ -22,7 +22,7 @@ import (
"net/http"
"strings"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
// pathPrefix roots every Route 53 REST URL. The version segment is fixed.
diff --git a/server/aws/route53/operations.go b/server/aws/route53/operations.go
index eca79ee..a2b77fe 100644
--- a/server/aws/route53/operations.go
+++ b/server/aws/route53/operations.go
@@ -5,9 +5,9 @@ import (
"net/http"
"time"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
// listMaxItems is the fixed page size echoed back to the SDK; the mock never
diff --git a/server/aws/route53/sdk_roundtrip_test.go b/server/aws/route53/sdk_roundtrip_test.go
index 8300707..fb8749e 100644
--- a/server/aws/route53/sdk_roundtrip_test.go
+++ b/server/aws/route53/sdk_roundtrip_test.go
@@ -12,8 +12,8 @@ import (
awsr53 "github.com/aws/aws-sdk-go-v2/service/route53"
r53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newRoute53Client(t *testing.T) *awsr53.Client {
diff --git a/server/aws/route53/types.go b/server/aws/route53/types.go
index c03b00b..4cef651 100644
--- a/server/aws/route53/types.go
+++ b/server/aws/route53/types.go
@@ -4,7 +4,7 @@ import (
"encoding/xml"
"strings"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
// xmlns is the Route 53 XML namespace stamped on every response root element.
diff --git a/server/aws/s3/handler.go b/server/aws/s3/handler.go
index 1c2ea0f..882dd2c 100644
--- a/server/aws/s3/handler.go
+++ b/server/aws/s3/handler.go
@@ -4,14 +4,17 @@
package s3
import (
+ "encoding/xml"
"fmt"
"io"
"net/http"
+ "sort"
+ "strconv"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire"
- "github.com/stackshy/cloudemu/storage/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
const (
@@ -108,6 +111,26 @@ func (h *Handler) listBuckets(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) bucketOp(w http.ResponseWriter, r *http.Request, bucket string) {
+ q := r.URL.Query()
+
+ switch {
+ case q.Has("versioning"):
+ h.bucketVersioningOp(w, r, bucket)
+ return
+ case q.Has("uploads"):
+ // GET /{bucket}?uploads => ListMultipartUploads.
+ if r.Method == http.MethodGet {
+ h.listMultipartUploads(w, r, bucket)
+ return
+ }
+ case q.Has("versions"):
+ // GET /{bucket}?versions => ListObjectVersions.
+ if r.Method == http.MethodGet {
+ h.listObjectVersions(w, r, bucket)
+ return
+ }
+ }
+
switch r.Method {
case http.MethodPut:
h.createBucket(w, r, bucket)
@@ -184,6 +207,23 @@ func (h *Handler) listObjects(w http.ResponseWriter, r *http.Request, bucket str
}
func (h *Handler) objectOp(w http.ResponseWriter, r *http.Request, bucket, key string) {
+ q := r.URL.Query()
+
+ switch {
+ case q.Has("tagging"):
+ h.objectTaggingOp(w, r, bucket, key)
+ return
+ case q.Has("uploads"):
+ // POST /{bucket}/{key}?uploads => CreateMultipartUpload.
+ if r.Method == http.MethodPost {
+ h.createMultipartUpload(w, r, bucket, key)
+ return
+ }
+ case q.Has("uploadId"):
+ h.multipartUploadOp(w, r, bucket, key, q.Get("uploadId"))
+ return
+ }
+
switch r.Method {
case http.MethodPut:
if r.Header.Get("X-Amz-Copy-Source") != "" {
@@ -299,6 +339,323 @@ func (h *Handler) copyObject(w http.ResponseWriter, r *http.Request, bucket, key
})
}
+// multipartUploadOp dispatches operations on an in-progress multipart upload
+// (those carrying an ?uploadId=... sub-resource).
+func (h *Handler) multipartUploadOp(w http.ResponseWriter, r *http.Request, bucket, key, uploadID string) {
+ switch r.Method {
+ case http.MethodPut:
+ h.uploadPart(w, r, bucket, key, uploadID)
+ case http.MethodPost:
+ h.completeMultipartUpload(w, r, bucket, key, uploadID)
+ case http.MethodDelete:
+ h.abortMultipartUpload(w, r, bucket, key, uploadID)
+ case http.MethodGet:
+ h.listParts(w, r, bucket, key, uploadID)
+ default:
+ writeError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+ }
+}
+
+func (h *Handler) createMultipartUpload(w http.ResponseWriter, r *http.Request, bucket, key string) {
+ contentType := r.Header.Get("Content-Type")
+ if contentType == "" {
+ contentType = "application/octet-stream"
+ }
+
+ mp, err := h.bucket.CreateMultipartUpload(r.Context(), bucket, key, contentType)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ wire.WriteXML(w, http.StatusOK, initiateMultipartUploadResult{
+ Xmlns: xmlns,
+ Bucket: bucket,
+ Key: key,
+ UploadID: mp.UploadID,
+ })
+}
+
+func (h *Handler) uploadPart(w http.ResponseWriter, r *http.Request, bucket, key, uploadID string) {
+ partNumber, err := strconv.Atoi(r.URL.Query().Get("partNumber"))
+ if err != nil || partNumber < 1 {
+ writeError(w, http.StatusBadRequest, "InvalidArgument", "invalid partNumber")
+ return
+ }
+
+ limited := http.MaxBytesReader(w, r.Body, maxPutObjectSize)
+
+ data, err := io.ReadAll(limited)
+ if err != nil {
+ writeError(w, http.StatusBadRequest, "IncompleteBody", "could not read body")
+ return
+ }
+
+ part, err := h.bucket.UploadPart(r.Context(), bucket, key, uploadID, partNumber, data)
+ if err != nil {
+ writeMultipartErr(w, err)
+ return
+ }
+
+ w.Header().Set("ETag", fmt.Sprintf("%q", part.ETag))
+ w.WriteHeader(http.StatusOK)
+}
+
+func (h *Handler) completeMultipartUpload(w http.ResponseWriter, r *http.Request, bucket, key, uploadID string) {
+ var req completeMultipartUpload
+ if err := xml.NewDecoder(r.Body).Decode(&req); err != nil {
+ writeError(w, http.StatusBadRequest, "MalformedXML", "could not parse request body")
+ return
+ }
+
+ if len(req.Parts) == 0 {
+ writeError(w, http.StatusBadRequest, "MalformedXML", "the CompleteMultipartUpload request must contain at least one part")
+ return
+ }
+
+ parts := make([]driver.UploadPart, 0, len(req.Parts))
+ for _, p := range req.Parts {
+ parts = append(parts, driver.UploadPart{
+ PartNumber: p.PartNumber,
+ ETag: strings.Trim(p.ETag, `"`),
+ })
+ }
+
+ if err := h.bucket.CompleteMultipartUpload(r.Context(), bucket, key, uploadID, parts); err != nil {
+ writeMultipartErr(w, err)
+ return
+ }
+
+ info, err := h.bucket.HeadObject(r.Context(), bucket, key)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ wire.WriteXML(w, http.StatusOK, completeMultipartUploadResult{
+ Xmlns: xmlns,
+ Location: "/" + bucket + "/" + key,
+ Bucket: bucket,
+ Key: key,
+ ETag: fmt.Sprintf("%q", info.ETag),
+ })
+}
+
+func (h *Handler) abortMultipartUpload(w http.ResponseWriter, r *http.Request, bucket, key, uploadID string) {
+ if err := h.bucket.AbortMultipartUpload(r.Context(), bucket, key, uploadID); err != nil {
+ writeMultipartErr(w, err)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+// listParts lists the parts uploaded so far for a multipart upload. The driver
+// exposes uploads but not their individual parts, so this returns the upload's
+// existence with an empty part list rather than fabricating part data.
+func (h *Handler) listParts(w http.ResponseWriter, r *http.Request, bucket, key, uploadID string) {
+ uploads, err := h.bucket.ListMultipartUploads(r.Context(), bucket)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ found := false
+ for _, u := range uploads {
+ if u.UploadID == uploadID {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ writeError(w, http.StatusNotFound, "NoSuchUpload", "the specified upload does not exist")
+ return
+ }
+
+ wire.WriteXML(w, http.StatusOK, listPartsResult{
+ Xmlns: xmlns,
+ Bucket: bucket,
+ Key: key,
+ UploadID: uploadID,
+ IsTruncated: false,
+ })
+}
+
+func (h *Handler) listMultipartUploads(w http.ResponseWriter, r *http.Request, bucket string) {
+ uploads, err := h.bucket.ListMultipartUploads(r.Context(), bucket)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ resp := listMultipartUploadsResult{Xmlns: xmlns, Bucket: bucket}
+ for _, u := range uploads {
+ resp.Uploads = append(resp.Uploads, multipartUploadXML{
+ Key: u.Key,
+ UploadID: u.UploadID,
+ Initiated: u.CreatedAt,
+ })
+ }
+
+ wire.WriteXML(w, http.StatusOK, resp)
+}
+
+// objectTaggingOp dispatches PUT/GET/DELETE for the ?tagging sub-resource.
+func (h *Handler) objectTaggingOp(w http.ResponseWriter, r *http.Request, bucket, key string) {
+ switch r.Method {
+ case http.MethodPut:
+ h.putObjectTagging(w, r, bucket, key)
+ case http.MethodGet:
+ h.getObjectTagging(w, r, bucket, key)
+ case http.MethodDelete:
+ h.deleteObjectTagging(w, r, bucket, key)
+ default:
+ writeError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+ }
+}
+
+func (h *Handler) putObjectTagging(w http.ResponseWriter, r *http.Request, bucket, key string) {
+ var body tagging
+ if err := xml.NewDecoder(r.Body).Decode(&body); err != nil {
+ writeError(w, http.StatusBadRequest, "MalformedXML", "could not parse request body")
+ return
+ }
+
+ tags := make(map[string]string, len(body.TagSet))
+ for _, t := range body.TagSet {
+ tags[t.Key] = t.Value
+ }
+
+ if err := h.bucket.PutObjectTagging(r.Context(), bucket, key, tags); err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
+
+func (h *Handler) getObjectTagging(w http.ResponseWriter, r *http.Request, bucket, key string) {
+ tags, err := h.bucket.GetObjectTagging(r.Context(), bucket, key)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ resp := tagging{Xmlns: xmlns}
+ // Sort keys for a deterministic response ordering.
+ keys := make([]string, 0, len(tags))
+ for k := range tags {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ for _, k := range keys {
+ resp.TagSet = append(resp.TagSet, tagXML{Key: k, Value: tags[k]})
+ }
+
+ wire.WriteXML(w, http.StatusOK, resp)
+}
+
+func (h *Handler) deleteObjectTagging(w http.ResponseWriter, r *http.Request, bucket, key string) {
+ if err := h.bucket.DeleteObjectTagging(r.Context(), bucket, key); err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+// bucketVersioningOp dispatches PUT/GET for the ?versioning sub-resource.
+func (h *Handler) bucketVersioningOp(w http.ResponseWriter, r *http.Request, bucket string) {
+ switch r.Method {
+ case http.MethodPut:
+ h.putBucketVersioning(w, r, bucket)
+ case http.MethodGet:
+ h.getBucketVersioning(w, r, bucket)
+ default:
+ writeError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+ }
+}
+
+func (h *Handler) putBucketVersioning(w http.ResponseWriter, r *http.Request, bucket string) {
+ var body versioningConfiguration
+ if err := xml.NewDecoder(r.Body).Decode(&body); err != nil {
+ writeError(w, http.StatusBadRequest, "MalformedXML", "could not parse request body")
+ return
+ }
+
+ enabled := body.Status == "Enabled"
+
+ if err := h.bucket.SetBucketVersioning(r.Context(), bucket, enabled); err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
+
+func (h *Handler) getBucketVersioning(w http.ResponseWriter, r *http.Request, bucket string) {
+ enabled, err := h.bucket.GetBucketVersioning(r.Context(), bucket)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ // The driver tracks versioning as a boolean only. Enabled reports "Enabled";
+ // a disabled bucket returns an empty (matching a
+ // never-versioned bucket) since "Suspended" vs. never-set isn't tracked.
+ resp := versioningConfiguration{Xmlns: xmlns}
+ if enabled {
+ resp.Status = "Enabled"
+ }
+
+ wire.WriteXML(w, http.StatusOK, resp)
+}
+
+// listObjectVersions handles GET /{bucket}?versions. The storage driver tracks
+// bucket-level versioning as a boolean flag only and does NOT retain per-object
+// version history, so no versionId-addressable versions exist. We return the
+// current objects as the sole (null) version rather than fabricating history.
+func (h *Handler) listObjectVersions(w http.ResponseWriter, r *http.Request, bucket string) {
+ opts := driver.ListOptions{
+ Prefix: r.URL.Query().Get("prefix"),
+ Delimiter: r.URL.Query().Get("delimiter"),
+ }
+
+ result, err := h.bucket.ListObjects(r.Context(), bucket, opts)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ resp := listVersionsResult{
+ Xmlns: xmlns,
+ Name: bucket,
+ Prefix: opts.Prefix,
+ Delimiter: opts.Delimiter,
+ MaxKeys: defaultMaxKeys,
+ }
+
+ for _, obj := range result.Objects {
+ resp.Versions = append(resp.Versions, objectVersionXML{
+ Key: obj.Key,
+ VersionID: "null",
+ IsLatest: true,
+ LastModified: obj.LastModified,
+ ETag: fmt.Sprintf("%q", obj.ETag),
+ Size: obj.Size,
+ StorageClass: "STANDARD",
+ })
+ }
+
+ for _, p := range result.CommonPrefixes {
+ resp.CommonPrefixes = append(resp.CommonPrefixes, prefixXML{Prefix: p})
+ }
+
+ wire.WriteXML(w, http.StatusOK, resp)
+}
+
// extractMetadata pulls x-amz-meta-* headers into a map.
func extractMetadata(h http.Header) map[string]string {
meta := make(map[string]string)
@@ -324,6 +681,16 @@ func writeError(w http.ResponseWriter, status int, code, msg string) {
}
// writeErr maps CloudEmu errors to S3 HTTP error responses.
+// writeMultipartErr maps a driver error from a multipart operation, where a
+// missing resource is the upload (NoSuchUpload), not the object key.
+func writeMultipartErr(w http.ResponseWriter, err error) {
+ if cerrors.IsNotFound(err) {
+ writeError(w, http.StatusNotFound, "NoSuchUpload", err.Error())
+ return
+ }
+ writeErr(w, err)
+}
+
func writeErr(w http.ResponseWriter, err error) {
switch {
case cerrors.IsNotFound(err):
diff --git a/server/aws/s3/sdk_roundtrip_test.go b/server/aws/s3/sdk_roundtrip_test.go
new file mode 100644
index 0000000..0cf71db
--- /dev/null
+++ b/server/aws/s3/sdk_roundtrip_test.go
@@ -0,0 +1,309 @@
+package s3_test
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsconfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
+)
+
+func newSDKClient(t *testing.T) *awss3.Client {
+ t.Helper()
+
+ cloud := cloudemu.NewAWS()
+ srv := awsserver.New(awsserver.Drivers{S3: cloud.S3})
+
+ ts := httptest.NewServer(srv)
+ t.Cleanup(ts.Close)
+
+ cfg, err := awsconfig.LoadDefaultConfig(context.Background(),
+ awsconfig.WithRegion("us-east-1"),
+ awsconfig.WithCredentialsProvider(
+ credentials.NewStaticCredentialsProvider("test", "test", ""),
+ ),
+ )
+ if err != nil {
+ t.Fatalf("aws config: %v", err)
+ }
+
+ return awss3.NewFromConfig(cfg, func(o *awss3.Options) {
+ o.BaseEndpoint = aws.String(ts.URL)
+ o.UsePathStyle = true
+ })
+}
+
+func mustCreateBucket(t *testing.T, client *awss3.Client, bucket string) {
+ t.Helper()
+
+ _, err := client.CreateBucket(context.Background(), &awss3.CreateBucketInput{
+ Bucket: aws.String(bucket),
+ })
+ if err != nil {
+ t.Fatalf("CreateBucket: %v", err)
+ }
+}
+
+// TestSDKMultipartUpload drives the real multipart flow (Create -> UploadPart x2
+// -> Complete) and asserts a subsequent GetObject returns the concatenated body.
+func TestSDKMultipartUpload(t *testing.T) {
+ client := newSDKClient(t)
+ ctx := context.Background()
+
+ const bucket = "mp-bucket"
+ const key = "big-object"
+
+ mustCreateBucket(t, client, bucket)
+
+ // Two parts. S3 requires all but the last part to be >= 5 MiB; the emulator
+ // does not enforce that, so smaller distinct payloads suffice to verify
+ // ordered concatenation.
+ part1 := bytes.Repeat([]byte("A"), 1024)
+ part2 := bytes.Repeat([]byte("B"), 2048)
+
+ created, err := client.CreateMultipartUpload(ctx, &awss3.CreateMultipartUploadInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ })
+ if err != nil {
+ t.Fatalf("CreateMultipartUpload: %v", err)
+ }
+
+ uploadID := aws.ToString(created.UploadId)
+ if uploadID == "" {
+ t.Fatal("CreateMultipartUpload returned empty UploadId")
+ }
+
+ completed := make([]types.CompletedPart, 0, 2)
+
+ for i, data := range [][]byte{part1, part2} {
+ partNum := int32(i + 1) //nolint:gosec // small loop index
+
+ up, upErr := client.UploadPart(ctx, &awss3.UploadPartInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ UploadId: aws.String(uploadID),
+ PartNumber: aws.Int32(partNum),
+ Body: bytes.NewReader(data),
+ })
+ if upErr != nil {
+ t.Fatalf("UploadPart %d: %v", partNum, upErr)
+ }
+
+ if aws.ToString(up.ETag) == "" {
+ t.Fatalf("UploadPart %d returned empty ETag", partNum)
+ }
+
+ completed = append(completed, types.CompletedPart{
+ ETag: up.ETag,
+ PartNumber: aws.Int32(partNum),
+ })
+ }
+
+ _, err = client.CompleteMultipartUpload(ctx, &awss3.CompleteMultipartUploadInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ UploadId: aws.String(uploadID),
+ MultipartUpload: &types.CompletedMultipartUpload{Parts: completed},
+ })
+ if err != nil {
+ t.Fatalf("CompleteMultipartUpload: %v", err)
+ }
+
+ got, err := client.GetObject(ctx, &awss3.GetObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ })
+ if err != nil {
+ t.Fatalf("GetObject: %v", err)
+ }
+ defer got.Body.Close()
+
+ body, err := io.ReadAll(got.Body)
+ if err != nil {
+ t.Fatalf("read body: %v", err)
+ }
+
+ want := append(append([]byte{}, part1...), part2...)
+ if !bytes.Equal(body, want) {
+ t.Fatalf("multipart body mismatch: got %d bytes, want %d bytes", len(body), len(want))
+ }
+}
+
+// TestSDKMultipartAbortAndList verifies AbortMultipartUpload removes an
+// in-progress upload from ListMultipartUploads.
+func TestSDKMultipartAbortAndList(t *testing.T) {
+ client := newSDKClient(t)
+ ctx := context.Background()
+
+ const bucket = "mp-abort-bucket"
+ const key = "obj"
+
+ mustCreateBucket(t, client, bucket)
+
+ created, err := client.CreateMultipartUpload(ctx, &awss3.CreateMultipartUploadInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ })
+ if err != nil {
+ t.Fatalf("CreateMultipartUpload: %v", err)
+ }
+
+ uploadID := aws.ToString(created.UploadId)
+
+ listed, err := client.ListMultipartUploads(ctx, &awss3.ListMultipartUploadsInput{
+ Bucket: aws.String(bucket),
+ })
+ if err != nil {
+ t.Fatalf("ListMultipartUploads: %v", err)
+ }
+
+ if len(listed.Uploads) != 1 || aws.ToString(listed.Uploads[0].UploadId) != uploadID {
+ t.Fatalf("expected 1 in-progress upload %q, got %+v", uploadID, listed.Uploads)
+ }
+
+ _, err = client.AbortMultipartUpload(ctx, &awss3.AbortMultipartUploadInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ UploadId: aws.String(uploadID),
+ })
+ if err != nil {
+ t.Fatalf("AbortMultipartUpload: %v", err)
+ }
+
+ after, err := client.ListMultipartUploads(ctx, &awss3.ListMultipartUploadsInput{
+ Bucket: aws.String(bucket),
+ })
+ if err != nil {
+ t.Fatalf("ListMultipartUploads (after abort): %v", err)
+ }
+
+ if len(after.Uploads) != 0 {
+ t.Fatalf("expected 0 uploads after abort, got %d", len(after.Uploads))
+ }
+}
+
+// TestSDKObjectTagging verifies PutObjectTagging -> GetObjectTagging round-trips
+// the tag set, and DeleteObjectTagging clears it.
+func TestSDKObjectTagging(t *testing.T) {
+ client := newSDKClient(t)
+ ctx := context.Background()
+
+ const bucket = "tag-bucket"
+ const key = "tagged-object"
+
+ mustCreateBucket(t, client, bucket)
+
+ _, err := client.PutObject(ctx, &awss3.PutObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ Body: bytes.NewReader([]byte("hello")),
+ })
+ if err != nil {
+ t.Fatalf("PutObject: %v", err)
+ }
+
+ _, err = client.PutObjectTagging(ctx, &awss3.PutObjectTaggingInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ Tagging: &types.Tagging{
+ TagSet: []types.Tag{
+ {Key: aws.String("env"), Value: aws.String("prod")},
+ {Key: aws.String("team"), Value: aws.String("platform")},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("PutObjectTagging: %v", err)
+ }
+
+ got, err := client.GetObjectTagging(ctx, &awss3.GetObjectTaggingInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ })
+ if err != nil {
+ t.Fatalf("GetObjectTagging: %v", err)
+ }
+
+ tags := map[string]string{}
+ for _, tag := range got.TagSet {
+ tags[aws.ToString(tag.Key)] = aws.ToString(tag.Value)
+ }
+
+ if tags["env"] != "prod" || tags["team"] != "platform" {
+ t.Fatalf("tag round-trip mismatch: %+v", tags)
+ }
+
+ _, err = client.DeleteObjectTagging(ctx, &awss3.DeleteObjectTaggingInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ })
+ if err != nil {
+ t.Fatalf("DeleteObjectTagging: %v", err)
+ }
+
+ afterDel, err := client.GetObjectTagging(ctx, &awss3.GetObjectTaggingInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ })
+ if err != nil {
+ t.Fatalf("GetObjectTagging (after delete): %v", err)
+ }
+
+ if len(afterDel.TagSet) != 0 {
+ t.Fatalf("expected empty tag set after delete, got %+v", afterDel.TagSet)
+ }
+}
+
+// TestSDKBucketVersioning verifies PutBucketVersioning(Enabled) ->
+// GetBucketVersioning returns Enabled.
+func TestSDKBucketVersioning(t *testing.T) {
+ client := newSDKClient(t)
+ ctx := context.Background()
+
+ const bucket = "versioned-bucket"
+
+ mustCreateBucket(t, client, bucket)
+
+ // Fresh bucket: versioning not yet configured -> empty status.
+ initial, err := client.GetBucketVersioning(ctx, &awss3.GetBucketVersioningInput{
+ Bucket: aws.String(bucket),
+ })
+ if err != nil {
+ t.Fatalf("GetBucketVersioning (initial): %v", err)
+ }
+
+ if initial.Status != "" {
+ t.Fatalf("expected empty versioning status initially, got %q", initial.Status)
+ }
+
+ _, err = client.PutBucketVersioning(ctx, &awss3.PutBucketVersioningInput{
+ Bucket: aws.String(bucket),
+ VersioningConfiguration: &types.VersioningConfiguration{
+ Status: types.BucketVersioningStatusEnabled,
+ },
+ })
+ if err != nil {
+ t.Fatalf("PutBucketVersioning: %v", err)
+ }
+
+ got, err := client.GetBucketVersioning(ctx, &awss3.GetBucketVersioningInput{
+ Bucket: aws.String(bucket),
+ })
+ if err != nil {
+ t.Fatalf("GetBucketVersioning: %v", err)
+ }
+
+ if got.Status != types.BucketVersioningStatusEnabled {
+ t.Fatalf("expected versioning Enabled, got %q", got.Status)
+ }
+}
diff --git a/server/aws/s3/xml.go b/server/aws/s3/xml.go
index 9b056e1..8349d2b 100644
--- a/server/aws/s3/xml.go
+++ b/server/aws/s3/xml.go
@@ -56,3 +56,107 @@ type copyObjectResult struct {
ETag string `xml:"ETag"`
LastModified string `xml:"LastModified"`
}
+
+// initiateMultipartUploadResult is the XML response for CreateMultipartUpload.
+type initiateMultipartUploadResult struct {
+ XMLName xml.Name `xml:"InitiateMultipartUploadResult"`
+ Xmlns string `xml:"xmlns,attr"`
+ Bucket string `xml:"Bucket"`
+ Key string `xml:"Key"`
+ UploadID string `xml:"UploadId"`
+}
+
+// completeMultipartUpload is the XML request body for CompleteMultipartUpload.
+type completeMultipartUpload struct {
+ XMLName xml.Name `xml:"CompleteMultipartUpload"`
+ Parts []completePartXML `xml:"Part"`
+}
+
+type completePartXML struct {
+ PartNumber int `xml:"PartNumber"`
+ ETag string `xml:"ETag"`
+}
+
+// completeMultipartUploadResult is the XML response for CompleteMultipartUpload.
+type completeMultipartUploadResult struct {
+ XMLName xml.Name `xml:"CompleteMultipartUploadResult"`
+ Xmlns string `xml:"xmlns,attr"`
+ Location string `xml:"Location"`
+ Bucket string `xml:"Bucket"`
+ Key string `xml:"Key"`
+ ETag string `xml:"ETag"`
+}
+
+// listPartsResult is the XML response for ListParts.
+type listPartsResult struct {
+ XMLName xml.Name `xml:"ListPartsResult"`
+ Xmlns string `xml:"xmlns,attr"`
+ Bucket string `xml:"Bucket"`
+ Key string `xml:"Key"`
+ UploadID string `xml:"UploadId"`
+ IsTruncated bool `xml:"IsTruncated"`
+ Parts []partXML `xml:"Part"`
+}
+
+type partXML struct {
+ PartNumber int `xml:"PartNumber"`
+ ETag string `xml:"ETag"`
+ Size int64 `xml:"Size"`
+}
+
+// listMultipartUploadsResult is the XML response for ListMultipartUploads.
+type listMultipartUploadsResult struct {
+ XMLName xml.Name `xml:"ListMultipartUploadsResult"`
+ Xmlns string `xml:"xmlns,attr"`
+ Bucket string `xml:"Bucket"`
+ IsTruncated bool `xml:"IsTruncated"`
+ Uploads []multipartUploadXML `xml:"Upload"`
+}
+
+type multipartUploadXML struct {
+ Key string `xml:"Key"`
+ UploadID string `xml:"UploadId"`
+ Initiated string `xml:"Initiated,omitempty"`
+}
+
+// tagging is the XML request/response body for object tagging.
+type tagging struct {
+ XMLName xml.Name `xml:"Tagging"`
+ Xmlns string `xml:"xmlns,attr,omitempty"`
+ TagSet []tagXML `xml:"TagSet>Tag"`
+}
+
+type tagXML struct {
+ Key string `xml:"Key"`
+ Value string `xml:"Value"`
+}
+
+// listVersionsResult is the XML response for ListObjectVersions.
+type listVersionsResult struct {
+ XMLName xml.Name `xml:"ListVersionsResult"`
+ Xmlns string `xml:"xmlns,attr"`
+ Name string `xml:"Name"`
+ Prefix string `xml:"Prefix"`
+ Delimiter string `xml:"Delimiter,omitempty"`
+ MaxKeys int `xml:"MaxKeys"`
+ IsTruncated bool `xml:"IsTruncated"`
+ Versions []objectVersionXML `xml:"Version"`
+ CommonPrefixes []prefixXML `xml:"CommonPrefixes,omitempty"`
+}
+
+type objectVersionXML struct {
+ Key string `xml:"Key"`
+ VersionID string `xml:"VersionId"`
+ IsLatest bool `xml:"IsLatest"`
+ LastModified string `xml:"LastModified"`
+ ETag string `xml:"ETag"`
+ Size int64 `xml:"Size"`
+ StorageClass string `xml:"StorageClass"`
+}
+
+// versioningConfiguration is the XML request/response body for bucket versioning.
+type versioningConfiguration struct {
+ XMLName xml.Name `xml:"VersioningConfiguration"`
+ Xmlns string `xml:"xmlns,attr,omitempty"`
+ Status string `xml:"Status,omitempty"`
+}
diff --git a/server/aws/sagemaker/cluster.go b/server/aws/sagemaker/cluster.go
index e788673..5241056 100644
--- a/server/aws/sagemaker/cluster.go
+++ b/server/aws/sagemaker/cluster.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"net/http"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// wireInstanceGroup is the JSON shape of a HyperPod instance group.
diff --git a/server/aws/sagemaker/errors.go b/server/aws/sagemaker/errors.go
index 96b2c33..0f34222 100644
--- a/server/aws/sagemaker/errors.go
+++ b/server/aws/sagemaker/errors.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"net/http"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
)
// writeDriverError maps a driver error to the closest SageMaker exception and
diff --git a/server/aws/sagemaker/featurestore.go b/server/aws/sagemaker/featurestore.go
index 5ccb925..df27569 100644
--- a/server/aws/sagemaker/featurestore.go
+++ b/server/aws/sagemaker/featurestore.go
@@ -4,9 +4,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// wireFeatureDef is the JSON shape of a feature definition.
diff --git a/server/aws/sagemaker/handler.go b/server/aws/sagemaker/handler.go
index a1dc89a..9c815ec 100644
--- a/server/aws/sagemaker/handler.go
+++ b/server/aws/sagemaker/handler.go
@@ -18,8 +18,8 @@ import (
"strings"
"time"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
const targetPrefix = "SageMaker."
diff --git a/server/aws/sagemaker/inference.go b/server/aws/sagemaker/inference.go
index 227f1df..8b8e1dc 100644
--- a/server/aws/sagemaker/inference.go
+++ b/server/aws/sagemaker/inference.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"net/http"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// wireContainer is the JSON shape of a model container definition.
diff --git a/server/aws/sagemaker/inference_components.go b/server/aws/sagemaker/inference_components.go
index fb339b9..b245075 100644
--- a/server/aws/sagemaker/inference_components.go
+++ b/server/aws/sagemaker/inference_components.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"net/http"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
func (h *Handler) routeInferenceComponents(w http.ResponseWriter, r *http.Request, op string) bool {
diff --git a/server/aws/sagemaker/jobs.go b/server/aws/sagemaker/jobs.go
index 1ed5738..8a57805 100644
--- a/server/aws/sagemaker/jobs.go
+++ b/server/aws/sagemaker/jobs.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"net/http"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// wireAlgorithm is the JSON shape of AlgorithmSpecification.
diff --git a/server/aws/sagemaker/jobs_more.go b/server/aws/sagemaker/jobs_more.go
index c7bcede..38b23a2 100644
--- a/server/aws/sagemaker/jobs_more.go
+++ b/server/aws/sagemaker/jobs_more.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"net/http"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
//nolint:dupl // SDK-compat decode/encode shim; the skeleton recurs but each op maps a distinct type.
diff --git a/server/aws/sagemaker/notebook.go b/server/aws/sagemaker/notebook.go
index 8f1bbe9..a42cc5e 100644
--- a/server/aws/sagemaker/notebook.go
+++ b/server/aws/sagemaker/notebook.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"net/http"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
func (h *Handler) routeNotebook(w http.ResponseWriter, r *http.Request, op string) bool {
diff --git a/server/aws/sagemaker/pipeline.go b/server/aws/sagemaker/pipeline.go
index c6f15b2..a4a4998 100644
--- a/server/aws/sagemaker/pipeline.go
+++ b/server/aws/sagemaker/pipeline.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"net/http"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
func (h *Handler) routePipelines(w http.ResponseWriter, r *http.Request, op string) bool {
diff --git a/server/aws/sagemaker/registry.go b/server/aws/sagemaker/registry.go
index 74a459c..3d16dac 100644
--- a/server/aws/sagemaker/registry.go
+++ b/server/aws/sagemaker/registry.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"net/http"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
func (h *Handler) routeRegistry(w http.ResponseWriter, r *http.Request, op string) bool {
diff --git a/server/aws/sagemaker/runtime.go b/server/aws/sagemaker/runtime.go
index 2f7913b..460e720 100644
--- a/server/aws/sagemaker/runtime.go
+++ b/server/aws/sagemaker/runtime.go
@@ -6,9 +6,9 @@ import (
"net/url"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
const maxInvokeBytes = 6 << 20 // 6 MiB, SageMaker's real-time payload limit
diff --git a/server/aws/sagemaker/sdk_roundtrip_test.go b/server/aws/sagemaker/sdk_roundtrip_test.go
index f2e8480..0ab0f41 100644
--- a/server/aws/sagemaker/sdk_roundtrip_test.go
+++ b/server/aws/sagemaker/sdk_roundtrip_test.go
@@ -16,8 +16,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newServer(t *testing.T) string {
diff --git a/server/aws/sagemaker/studio.go b/server/aws/sagemaker/studio.go
index 4f870b9..c168ba9 100644
--- a/server/aws/sagemaker/studio.go
+++ b/server/aws/sagemaker/studio.go
@@ -3,8 +3,8 @@ package sagemaker
import (
"net/http"
- "github.com/stackshy/cloudemu/sagemaker/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
func (h *Handler) routeStudio(w http.ResponseWriter, r *http.Request, op string) bool {
diff --git a/server/aws/sagemaker/tags.go b/server/aws/sagemaker/tags.go
index 38c07f5..f40e43d 100644
--- a/server/aws/sagemaker/tags.go
+++ b/server/aws/sagemaker/tags.go
@@ -3,7 +3,7 @@ package sagemaker
import (
"net/http"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
)
func (h *Handler) addTags(w http.ResponseWriter, r *http.Request) {
diff --git a/server/aws/secretsmanager/handler.go b/server/aws/secretsmanager/handler.go
index 2068071..4573ff3 100644
--- a/server/aws/secretsmanager/handler.go
+++ b/server/aws/secretsmanager/handler.go
@@ -11,9 +11,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
const targetPrefix = "secretsmanager."
diff --git a/server/aws/secretsmanager/operations.go b/server/aws/secretsmanager/operations.go
index fe216b3..2e95b3a 100644
--- a/server/aws/secretsmanager/operations.go
+++ b/server/aws/secretsmanager/operations.go
@@ -3,8 +3,8 @@ package secretsmanager
import (
"net/http"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
func (h *Handler) createSecret(w http.ResponseWriter, r *http.Request) {
diff --git a/server/aws/secretsmanager/sdk_roundtrip_test.go b/server/aws/secretsmanager/sdk_roundtrip_test.go
index b080bce..d0659e7 100644
--- a/server/aws/secretsmanager/sdk_roundtrip_test.go
+++ b/server/aws/secretsmanager/sdk_roundtrip_test.go
@@ -12,8 +12,8 @@ import (
awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
smtypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newSecretsClient(t *testing.T) *awssm.Client {
diff --git a/server/aws/secretsmanager/types.go b/server/aws/secretsmanager/types.go
index 385c19b..fc94bc8 100644
--- a/server/aws/secretsmanager/types.go
+++ b/server/aws/secretsmanager/types.go
@@ -4,7 +4,7 @@ import (
"strings"
"time"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
// Version stage labels, matching real Secrets Manager staging semantics: the
diff --git a/server/aws/sns/handler.go b/server/aws/sns/handler.go
index 8b97ad3..8012def 100644
--- a/server/aws/sns/handler.go
+++ b/server/aws/sns/handler.go
@@ -27,9 +27,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
// Namespace is the XML namespace for AWS SNS responses.
diff --git a/server/aws/sns/operations.go b/server/aws/sns/operations.go
index 567890b..ffa9938 100644
--- a/server/aws/sns/operations.go
+++ b/server/aws/sns/operations.go
@@ -5,9 +5,9 @@ import (
"net/url"
"strconv"
- cerrors "github.com/stackshy/cloudemu/errors"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
// createTopic maps CreateTopic to Notification.CreateTopic. SNS CreateTopic is
diff --git a/server/aws/sns/sdk_roundtrip_test.go b/server/aws/sns/sdk_roundtrip_test.go
index 9a38078..81b1187 100644
--- a/server/aws/sns/sdk_roundtrip_test.go
+++ b/server/aws/sns/sdk_roundtrip_test.go
@@ -12,8 +12,8 @@ import (
awssns "github.com/aws/aws-sdk-go-v2/service/sns"
snstypes "github.com/aws/aws-sdk-go-v2/service/sns/types"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newSDKClient(t *testing.T) *awssns.Client {
diff --git a/server/aws/sqs/handler.go b/server/aws/sqs/handler.go
index 684e204..c22671d 100644
--- a/server/aws/sqs/handler.go
+++ b/server/aws/sqs/handler.go
@@ -12,9 +12,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- mqdriver "github.com/stackshy/cloudemu/messagequeue/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ mqdriver "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
)
const targetPrefix = "AmazonSQS."
diff --git a/server/aws/sqs/sdk_roundtrip_test.go b/server/aws/sqs/sdk_roundtrip_test.go
index 00ee6ed..3dcbe6c 100644
--- a/server/aws/sqs/sdk_roundtrip_test.go
+++ b/server/aws/sqs/sdk_roundtrip_test.go
@@ -10,8 +10,8 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials"
awssqs "github.com/aws/aws-sdk-go-v2/service/sqs"
- "github.com/stackshy/cloudemu"
- awsserver "github.com/stackshy/cloudemu/server/aws"
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
)
func newSDKClient(t *testing.T) (*awssqs.Client, *cloudemuAWSHandle) {
diff --git a/server/aws/sqs/sqs_test.go b/server/aws/sqs/sqs_test.go
index 6f9bd3f..3e05641 100644
--- a/server/aws/sqs/sqs_test.go
+++ b/server/aws/sqs/sqs_test.go
@@ -7,8 +7,8 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu"
- "github.com/stackshy/cloudemu/server/aws/sqs"
+ "github.com/stackshy/cloudemu/v2"
+ "github.com/stackshy/cloudemu/v2/server/aws/sqs"
)
func newServer(t *testing.T) (*httptest.Server, mqHandle) {
diff --git a/server/aws/ssm/handler.go b/server/aws/ssm/handler.go
new file mode 100644
index 0000000..b3a02f5
--- /dev/null
+++ b/server/aws/ssm/handler.go
@@ -0,0 +1,85 @@
+// Package ssm implements the AWS Systems Manager (SSM) Parameter Store
+// JSON-RPC protocol as a server.Handler. Point the real aws-sdk-go-v2 SSM
+// client at a Server registered with this handler and Parameter Store
+// operations work against an in-memory parameterstore driver.
+//
+// SSM uses the AWS JSON 1.1 wire shape (POST + JSON body, dispatched on the
+// X-Amz-Target header "AmazonSSM."), the same family as DynamoDB,
+// SQS, EventBridge, CloudWatch Logs, ECR, SageMaker, and Secrets Manager. The
+// "AmazonSSM." target prefix is disjoint from all of those, so registration
+// order relative to them is unconstrained.
+package ssm
+
+import (
+ "net/http"
+ "strings"
+
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ ssmdriver "github.com/stackshy/cloudemu/v2/services/parameterstore/driver"
+)
+
+const targetPrefix = "AmazonSSM."
+
+// Handler serves Parameter Store JSON-RPC requests against a ParameterStore driver.
+type Handler struct {
+ store ssmdriver.ParameterStore
+}
+
+// New returns a Parameter Store handler backed by s.
+func New(s ssmdriver.ParameterStore) *Handler {
+ return &Handler{store: s}
+}
+
+// Matches returns true for SSM-shaped requests, identified by an X-Amz-Target
+// header of "AmazonSSM.".
+func (*Handler) Matches(r *http.Request) bool {
+ return strings.HasPrefix(r.Header.Get("X-Amz-Target"), targetPrefix)
+}
+
+// ServeHTTP dispatches Parameter Store operations based on X-Amz-Target.
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ op := strings.TrimPrefix(r.Header.Get("X-Amz-Target"), targetPrefix)
+
+ switch op {
+ case "PutParameter":
+ h.putParameter(w, r)
+ case "GetParameter":
+ h.getParameter(w, r)
+ case "GetParameters":
+ h.getParameters(w, r)
+ case "GetParametersByPath":
+ h.getParametersByPath(w, r)
+ case "DeleteParameter":
+ h.deleteParameter(w, r)
+ case "DeleteParameters":
+ h.deleteParameters(w, r)
+ case "DescribeParameters":
+ h.describeParameters(w, r)
+ case "GetParameterHistory":
+ h.getParameterHistory(w, r)
+ case "LabelParameterVersion":
+ h.labelParameterVersion(w, r)
+ default:
+ wire.WriteJSONError(w, http.StatusBadRequest,
+ "UnknownOperationException", "unknown SSM operation: "+op)
+ }
+}
+
+// writeErr maps canonical cloudemu errors to SSM JSON error responses. SSM
+// returns errors as HTTP 400 with a "__type" body the SDK maps to a typed
+// exception.
+func writeErr(w http.ResponseWriter, err error) {
+ switch {
+ case cerrors.IsNotFound(err):
+ wire.WriteJSONError(w, http.StatusBadRequest, "ParameterNotFound", err.Error())
+ case cerrors.IsAlreadyExists(err):
+ wire.WriteJSONError(w, http.StatusBadRequest, "ParameterAlreadyExists", err.Error())
+ case cerrors.IsInvalidArgument(err):
+ wire.WriteJSONError(w, http.StatusBadRequest, "ValidationException", err.Error())
+ case cerrors.GetCode(err) == cerrors.ResourceExhausted:
+ wire.WriteJSONError(w, http.StatusBadRequest, "ParameterLimitExceeded", err.Error())
+ default:
+ wire.WriteJSONError(w, http.StatusInternalServerError, "InternalServerError", err.Error())
+ }
+}
diff --git a/server/aws/ssm/operations.go b/server/aws/ssm/operations.go
new file mode 100644
index 0000000..f289c00
--- /dev/null
+++ b/server/aws/ssm/operations.go
@@ -0,0 +1,187 @@
+package ssm
+
+import (
+ "net/http"
+
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ ssmdriver "github.com/stackshy/cloudemu/v2/services/parameterstore/driver"
+)
+
+func (h *Handler) putParameter(w http.ResponseWriter, r *http.Request) {
+ var req putParameterRequest
+ if !wire.DecodeJSON(w, r, &req) {
+ return
+ }
+
+ version, tier, err := h.store.PutParameter(r.Context(), ssmdriver.PutConfig{
+ Name: req.Name,
+ Value: req.Value,
+ Type: req.Type,
+ Description: req.Description,
+ Overwrite: req.Overwrite,
+ Tier: req.Tier,
+ DataType: req.DataType,
+ })
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ wire.WriteJSON(w, putParameterResponse{Tier: tier, Version: version})
+}
+
+func (h *Handler) getParameter(w http.ResponseWriter, r *http.Request) {
+ var req getParameterRequest
+ if !wire.DecodeJSON(w, r, &req) {
+ return
+ }
+
+ p, err := h.store.GetParameter(r.Context(), req.Name, req.WithDecryption)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ wire.WriteJSON(w, getParameterResponse{Parameter: toParameterJSON(*p)})
+}
+
+func (h *Handler) getParameters(w http.ResponseWriter, r *http.Request) {
+ var req getParametersRequest
+ if !wire.DecodeJSON(w, r, &req) {
+ return
+ }
+
+ found, invalid, err := h.store.GetParameters(r.Context(), req.Names, req.WithDecryption)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ params := make([]parameterJSON, 0, len(found))
+ for _, p := range found {
+ params = append(params, toParameterJSON(p))
+ }
+
+ wire.WriteJSON(w, getParametersResponse{Parameters: params, InvalidParameters: invalid})
+}
+
+func (h *Handler) getParametersByPath(w http.ResponseWriter, r *http.Request) {
+ var req getParametersByPathRequest
+ if !wire.DecodeJSON(w, r, &req) {
+ return
+ }
+
+ found, err := h.store.GetParametersByPath(r.Context(), ssmdriver.GetByPathInput{
+ Path: req.Path,
+ Recursive: req.Recursive,
+ WithDecryption: req.WithDecryption,
+ })
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ params := make([]parameterJSON, 0, len(found))
+ for _, p := range found {
+ params = append(params, toParameterJSON(p))
+ }
+
+ wire.WriteJSON(w, getParametersByPathResponse{Parameters: params})
+}
+
+func (h *Handler) deleteParameter(w http.ResponseWriter, r *http.Request) {
+ var req nameRequest
+ if !wire.DecodeJSON(w, r, &req) {
+ return
+ }
+
+ if err := h.store.DeleteParameter(r.Context(), req.Name); err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ wire.WriteJSON(w, struct{}{})
+}
+
+func (h *Handler) deleteParameters(w http.ResponseWriter, r *http.Request) {
+ var req namesRequest
+ if !wire.DecodeJSON(w, r, &req) {
+ return
+ }
+
+ deleted, invalid, err := h.store.DeleteParameters(r.Context(), req.Names)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ wire.WriteJSON(w, deleteParametersResponse{DeletedParameters: deleted, InvalidParameters: invalid})
+}
+
+func (h *Handler) describeParameters(w http.ResponseWriter, r *http.Request) {
+ metas, err := h.store.DescribeParameters(r.Context())
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ out := make([]parameterMetadataJSON, 0, len(metas))
+ for _, md := range metas {
+ out = append(out, parameterMetadataJSON{
+ ARN: md.ARN,
+ DataType: md.DataType,
+ Description: md.Description,
+ LastModifiedDate: epochSeconds(md.LastModified),
+ LastModifiedUser: md.LastModifiedUser,
+ Name: md.Name,
+ Tier: md.Tier,
+ Type: md.Type,
+ Version: md.Version,
+ })
+ }
+
+ wire.WriteJSON(w, describeParametersResponse{Parameters: out})
+}
+
+func (h *Handler) getParameterHistory(w http.ResponseWriter, r *http.Request) {
+ var req nameRequest
+ if !wire.DecodeJSON(w, r, &req) {
+ return
+ }
+
+ history, err := h.store.GetParameterHistory(r.Context(), req.Name)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ out := make([]parameterHistoryJSON, 0, len(history))
+ for _, p := range history {
+ out = append(out, parameterHistoryJSON{
+ ARN: p.ARN,
+ DataType: p.DataType,
+ LastModifiedDate: epochSeconds(p.LastModified),
+ Name: p.Name,
+ Type: p.Type,
+ Value: p.Value,
+ Version: p.Version,
+ })
+ }
+
+ wire.WriteJSON(w, getParameterHistoryResponse{Parameters: out})
+}
+
+func (h *Handler) labelParameterVersion(w http.ResponseWriter, r *http.Request) {
+ var req labelParameterVersionRequest
+ if !wire.DecodeJSON(w, r, &req) {
+ return
+ }
+
+ applied, invalid, err := h.store.LabelParameterVersion(r.Context(), req.Name, req.ParameterVersion, req.Labels)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ wire.WriteJSON(w, labelParameterVersionResponse{InvalidLabels: invalid, ParameterVersion: applied})
+}
diff --git a/server/aws/ssm/sdk_roundtrip_test.go b/server/aws/ssm/sdk_roundtrip_test.go
new file mode 100644
index 0000000..45bc641
--- /dev/null
+++ b/server/aws/ssm/sdk_roundtrip_test.go
@@ -0,0 +1,275 @@
+package ssm_test
+
+import (
+ "context"
+ "errors"
+ "net/http/httptest"
+ "sort"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsconfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ awsssm "github.com/aws/aws-sdk-go-v2/service/ssm"
+ ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
+
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
+)
+
+func newSSMClient(t *testing.T) *awsssm.Client {
+ t.Helper()
+
+ cloud := cloudemu.NewAWS()
+ srv := awsserver.New(awsserver.Drivers{SSM: cloud.SSM})
+
+ ts := httptest.NewServer(srv)
+ t.Cleanup(ts.Close)
+
+ cfg, err := awsconfig.LoadDefaultConfig(context.Background(),
+ awsconfig.WithRegion("us-east-1"),
+ awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("test", "test", "")),
+ )
+ if err != nil {
+ t.Fatalf("aws config: %v", err)
+ }
+
+ return awsssm.NewFromConfig(cfg, func(o *awsssm.Options) {
+ o.BaseEndpoint = aws.String(ts.URL)
+ })
+}
+
+func TestSDKPutGetParameter(t *testing.T) {
+ client := newSSMClient(t)
+ ctx := context.Background()
+
+ put, err := client.PutParameter(ctx, &awsssm.PutParameterInput{
+ Name: aws.String("/app/db/host"),
+ Value: aws.String("db.internal"),
+ Type: ssmtypes.ParameterTypeString,
+ })
+ if err != nil {
+ t.Fatalf("PutParameter: %v", err)
+ }
+
+ if put.Version != 1 {
+ t.Fatalf("PutParameter version = %d, want 1", put.Version)
+ }
+
+ got, err := client.GetParameter(ctx, &awsssm.GetParameterInput{Name: aws.String("/app/db/host")})
+ if err != nil {
+ t.Fatalf("GetParameter: %v", err)
+ }
+
+ if aws.ToString(got.Parameter.Value) != "db.internal" {
+ t.Fatalf("value = %q, want db.internal", aws.ToString(got.Parameter.Value))
+ }
+
+ if got.Parameter.Version != 1 {
+ t.Fatalf("version = %d, want 1", got.Parameter.Version)
+ }
+
+ if got.Parameter.Type != ssmtypes.ParameterTypeString {
+ t.Fatalf("type = %q, want String", got.Parameter.Type)
+ }
+
+ if aws.ToString(got.Parameter.ARN) == "" {
+ t.Fatal("GetParameter returned empty ARN")
+ }
+}
+
+func TestSDKPutOverwriteVersioning(t *testing.T) {
+ client := newSSMClient(t)
+ ctx := context.Background()
+
+ if _, err := client.PutParameter(ctx, &awsssm.PutParameterInput{
+ Name: aws.String("/app/key"),
+ Value: aws.String("v1"),
+ Type: ssmtypes.ParameterTypeString,
+ }); err != nil {
+ t.Fatalf("PutParameter v1: %v", err)
+ }
+
+ // Overwrite without the flag must fail.
+ _, err := client.PutParameter(ctx, &awsssm.PutParameterInput{
+ Name: aws.String("/app/key"),
+ Value: aws.String("v2"),
+ Type: ssmtypes.ParameterTypeString,
+ })
+
+ var exists *ssmtypes.ParameterAlreadyExists
+ if !errors.As(err, &exists) {
+ t.Fatalf("Put without Overwrite: got %v, want ParameterAlreadyExists", err)
+ }
+
+ put2, err := client.PutParameter(ctx, &awsssm.PutParameterInput{
+ Name: aws.String("/app/key"),
+ Value: aws.String("v2"),
+ Type: ssmtypes.ParameterTypeString,
+ Overwrite: aws.Bool(true),
+ })
+ if err != nil {
+ t.Fatalf("PutParameter overwrite: %v", err)
+ }
+
+ if put2.Version != 2 {
+ t.Fatalf("overwrite version = %d, want 2", put2.Version)
+ }
+
+ got, err := client.GetParameter(ctx, &awsssm.GetParameterInput{Name: aws.String("/app/key")})
+ if err != nil {
+ t.Fatalf("GetParameter: %v", err)
+ }
+
+ if aws.ToString(got.Parameter.Value) != "v2" || got.Parameter.Version != 2 {
+ t.Fatalf("got value %q version %d, want v2 version 2",
+ aws.ToString(got.Parameter.Value), got.Parameter.Version)
+ }
+
+ // Fetch a specific historic version via the ":version" selector.
+ old, err := client.GetParameter(ctx, &awsssm.GetParameterInput{Name: aws.String("/app/key:1")})
+ if err != nil {
+ t.Fatalf("GetParameter(:1): %v", err)
+ }
+
+ if aws.ToString(old.Parameter.Value) != "v1" {
+ t.Fatalf("v1 selector value = %q, want v1", aws.ToString(old.Parameter.Value))
+ }
+}
+
+func TestSDKGetParametersByPath(t *testing.T) {
+ client := newSSMClient(t)
+ ctx := context.Background()
+
+ seed := map[string]string{
+ "/svc/a": "1",
+ "/svc/b": "2",
+ "/svc/deep/c": "3",
+ "/other/x": "9",
+ }
+ for name, val := range seed {
+ if _, err := client.PutParameter(ctx, &awsssm.PutParameterInput{
+ Name: aws.String(name),
+ Value: aws.String(val),
+ Type: ssmtypes.ParameterTypeString,
+ }); err != nil {
+ t.Fatalf("PutParameter %s: %v", name, err)
+ }
+ }
+
+ // Non-recursive: only direct children of /svc.
+ shallow, err := client.GetParametersByPath(ctx, &awsssm.GetParametersByPathInput{
+ Path: aws.String("/svc"),
+ })
+ if err != nil {
+ t.Fatalf("GetParametersByPath(non-recursive): %v", err)
+ }
+
+ if names := paramNames(shallow.Parameters); !equalStrings(names, []string{"/svc/a", "/svc/b"}) {
+ t.Fatalf("non-recursive names = %v, want [/svc/a /svc/b]", names)
+ }
+
+ // Recursive: whole subtree.
+ deep, err := client.GetParametersByPath(ctx, &awsssm.GetParametersByPathInput{
+ Path: aws.String("/svc"),
+ Recursive: aws.Bool(true),
+ })
+ if err != nil {
+ t.Fatalf("GetParametersByPath(recursive): %v", err)
+ }
+
+ if names := paramNames(deep.Parameters); !equalStrings(names, []string{"/svc/a", "/svc/b", "/svc/deep/c"}) {
+ t.Fatalf("recursive names = %v, want [/svc/a /svc/b /svc/deep/c]", names)
+ }
+}
+
+func TestSDKGetParameters(t *testing.T) {
+ client := newSSMClient(t)
+ ctx := context.Background()
+
+ for _, name := range []string{"/multi/one", "/multi/two"} {
+ if _, err := client.PutParameter(ctx, &awsssm.PutParameterInput{
+ Name: aws.String(name),
+ Value: aws.String("val" + name),
+ Type: ssmtypes.ParameterTypeString,
+ }); err != nil {
+ t.Fatalf("PutParameter %s: %v", name, err)
+ }
+ }
+
+ got, err := client.GetParameters(ctx, &awsssm.GetParametersInput{
+ Names: []string{"/multi/one", "/multi/two", "/multi/missing"},
+ })
+ if err != nil {
+ t.Fatalf("GetParameters: %v", err)
+ }
+
+ if len(got.Parameters) != 2 {
+ t.Fatalf("got %d parameters, want 2", len(got.Parameters))
+ }
+
+ if len(got.InvalidParameters) != 1 || got.InvalidParameters[0] != "/multi/missing" {
+ t.Fatalf("InvalidParameters = %v, want [/multi/missing]", got.InvalidParameters)
+ }
+}
+
+func TestSDKDeleteParameter(t *testing.T) {
+ client := newSSMClient(t)
+ ctx := context.Background()
+
+ if _, err := client.PutParameter(ctx, &awsssm.PutParameterInput{
+ Name: aws.String("/del/me"),
+ Value: aws.String("x"),
+ Type: ssmtypes.ParameterTypeString,
+ }); err != nil {
+ t.Fatalf("PutParameter: %v", err)
+ }
+
+ if _, err := client.DeleteParameter(ctx, &awsssm.DeleteParameterInput{Name: aws.String("/del/me")}); err != nil {
+ t.Fatalf("DeleteParameter: %v", err)
+ }
+
+ _, err := client.GetParameter(ctx, &awsssm.GetParameterInput{Name: aws.String("/del/me")})
+
+ var notFound *ssmtypes.ParameterNotFound
+ if !errors.As(err, ¬Found) {
+ t.Fatalf("GetParameter after delete: got %v, want ParameterNotFound", err)
+ }
+}
+
+func TestSDKGetParameterNotFound(t *testing.T) {
+ client := newSSMClient(t)
+ ctx := context.Background()
+
+ _, err := client.GetParameter(ctx, &awsssm.GetParameterInput{Name: aws.String("/does/not/exist")})
+
+ var notFound *ssmtypes.ParameterNotFound
+ if !errors.As(err, ¬Found) {
+ t.Fatalf("GetParameter(missing): got %v, want ParameterNotFound", err)
+ }
+}
+
+func paramNames(ps []ssmtypes.Parameter) []string {
+ out := make([]string, 0, len(ps))
+ for _, p := range ps {
+ out = append(out, aws.ToString(p.Name))
+ }
+
+ sort.Strings(out)
+
+ return out
+}
+
+func equalStrings(a, b []string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/server/aws/ssm/types.go b/server/aws/ssm/types.go
new file mode 100644
index 0000000..251d56a
--- /dev/null
+++ b/server/aws/ssm/types.go
@@ -0,0 +1,148 @@
+package ssm
+
+import (
+ "time"
+
+ ssmdriver "github.com/stackshy/cloudemu/v2/services/parameterstore/driver"
+)
+
+// parameterJSON is the wire shape for a Parameter, matching the fields the AWS
+// SDK deserializes. LastModifiedDate is epoch seconds (AWS JSON timestamp form).
+type parameterJSON struct {
+ ARN string `json:"ARN,omitempty"`
+ DataType string `json:"DataType,omitempty"`
+ LastModifiedDate float64 `json:"LastModifiedDate,omitempty"`
+ Name string `json:"Name"`
+ Selector string `json:"Selector,omitempty"`
+ Type string `json:"Type,omitempty"`
+ Value string `json:"Value,omitempty"`
+ Version int64 `json:"Version"`
+}
+
+// parameterMetadataJSON is the wire shape for ParameterMetadata (DescribeParameters).
+type parameterMetadataJSON struct {
+ ARN string `json:"ARN,omitempty"`
+ DataType string `json:"DataType,omitempty"`
+ Description string `json:"Description,omitempty"`
+ LastModifiedDate float64 `json:"LastModifiedDate,omitempty"`
+ LastModifiedUser string `json:"LastModifiedUser,omitempty"`
+ Name string `json:"Name"`
+ Tier string `json:"Tier,omitempty"`
+ Type string `json:"Type,omitempty"`
+ Version int64 `json:"Version"`
+}
+
+// --- request envelopes ---
+
+type putParameterRequest struct {
+ Name string `json:"Name"`
+ Value string `json:"Value"`
+ Type string `json:"Type"`
+ Description string `json:"Description"`
+ Overwrite bool `json:"Overwrite"`
+ Tier string `json:"Tier"`
+ DataType string `json:"DataType"`
+}
+
+type getParameterRequest struct {
+ Name string `json:"Name"`
+ WithDecryption bool `json:"WithDecryption"`
+}
+
+type getParametersRequest struct {
+ Names []string `json:"Names"`
+ WithDecryption bool `json:"WithDecryption"`
+}
+
+type getParametersByPathRequest struct {
+ Path string `json:"Path"`
+ Recursive bool `json:"Recursive"`
+ WithDecryption bool `json:"WithDecryption"`
+}
+
+type nameRequest struct {
+ Name string `json:"Name"`
+}
+
+type namesRequest struct {
+ Names []string `json:"Names"`
+}
+
+type labelParameterVersionRequest struct {
+ Name string `json:"Name"`
+ ParameterVersion int64 `json:"ParameterVersion"`
+ Labels []string `json:"Labels"`
+}
+
+// --- response envelopes ---
+
+type putParameterResponse struct {
+ Tier string `json:"Tier,omitempty"`
+ Version int64 `json:"Version"`
+}
+
+type getParameterResponse struct {
+ Parameter parameterJSON `json:"Parameter"`
+}
+
+type getParametersResponse struct {
+ Parameters []parameterJSON `json:"Parameters"`
+ InvalidParameters []string `json:"InvalidParameters,omitempty"`
+}
+
+type getParametersByPathResponse struct {
+ Parameters []parameterJSON `json:"Parameters"`
+}
+
+type deleteParametersResponse struct {
+ DeletedParameters []string `json:"DeletedParameters,omitempty"`
+ InvalidParameters []string `json:"InvalidParameters,omitempty"`
+}
+
+type describeParametersResponse struct {
+ Parameters []parameterMetadataJSON `json:"Parameters"`
+}
+
+type labelParameterVersionResponse struct {
+ InvalidLabels []string `json:"InvalidLabels,omitempty"`
+ ParameterVersion int64 `json:"ParameterVersion"`
+}
+
+type getParameterHistoryResponse struct {
+ Parameters []parameterHistoryJSON `json:"Parameters"`
+}
+
+// parameterHistoryJSON is the wire shape for a ParameterHistory entry.
+type parameterHistoryJSON struct {
+ ARN string `json:"ARN,omitempty"`
+ DataType string `json:"DataType,omitempty"`
+ LastModifiedDate float64 `json:"LastModifiedDate,omitempty"`
+ Name string `json:"Name"`
+ Type string `json:"Type,omitempty"`
+ Value string `json:"Value,omitempty"`
+ Version int64 `json:"Version"`
+}
+
+// epochSeconds converts an RFC3339 timestamp to Unix epoch seconds, the form
+// the AWS JSON protocol uses for timestamp fields. Returns 0 on parse failure.
+func epochSeconds(iso string) float64 {
+ t, err := time.Parse(time.RFC3339, iso)
+ if err != nil {
+ return 0
+ }
+
+ return float64(t.Unix())
+}
+
+func toParameterJSON(p ssmdriver.Parameter) parameterJSON {
+ return parameterJSON{
+ ARN: p.ARN,
+ DataType: p.DataType,
+ LastModifiedDate: epochSeconds(p.LastModified),
+ Name: p.Name,
+ Selector: p.Selector,
+ Type: p.Type,
+ Value: p.Value,
+ Version: p.Version,
+ }
+}
diff --git a/server/aws/sts/handler.go b/server/aws/sts/handler.go
new file mode 100644
index 0000000..7ab0b4f
--- /dev/null
+++ b/server/aws/sts/handler.go
@@ -0,0 +1,110 @@
+// Package sts implements the AWS STS query-protocol as a server.Handler.
+// Point the real aws-sdk-go-v2 STS client at a Server registered with this
+// handler and GetCallerIdentity / AssumeRole / GetSessionToken work against
+// cloudemu's configured identity.
+//
+// STS has no backing driver: identity is derived from the AccountID and Region
+// the AWS server was configured with. This exists so SDK code paths that call
+// sts:GetCallerIdentity or sts:AssumeRole on init succeed against cloudemu.
+//
+// STS shares the AWS query wire shape with EC2, RDS, Redshift, IAM, and the
+// other query-protocol handlers (POST + form-encoded body, XML response). To
+// keep dispatch unambiguous, this handler's Matches predicate parses the form
+// body once and only claims requests whose Action is one of the known STS
+// operations. The EC2 handler is the catch-all for all other query-protocol
+// actions, so this handler MUST register before EC2. Its action set
+// (GetCallerIdentity, AssumeRole, GetSessionToken) is disjoint from RDS,
+// Redshift, IAM, ELBv2, ElastiCache, SNS, and EC2, so no shadowing occurs.
+package sts
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+)
+
+// Namespace is the XML namespace for AWS STS responses.
+const Namespace = "https://sts.amazonaws.com/doc/2011-06-15/"
+
+const (
+ formContentType = "application/x-www-form-urlencoded"
+ maxFormBodyBytes = 1 << 20
+)
+
+// stsActions is the set of Action values this handler recognizes. Matches uses
+// it to decide whether to claim a request.
+var stsActions = map[string]struct{}{ //nolint:gochecknoglobals // static lookup table
+ "GetCallerIdentity": {},
+ "AssumeRole": {},
+ "GetSessionToken": {},
+}
+
+// Handler serves STS query-protocol requests. It carries the account and region
+// the AWS server was configured with; there is no backing driver.
+type Handler struct {
+ accountID string
+ region string
+}
+
+// New returns an STS handler that reports the given accountID and region.
+// Empty values fall back to sensible defaults so a well-formed identity is
+// always returned.
+func New(accountID, region string) *Handler {
+ if accountID == "" {
+ accountID = defaultAccountID
+ }
+
+ if region == "" {
+ region = defaultRegion
+ }
+
+ return &Handler{accountID: accountID, region: region}
+}
+
+const (
+ defaultAccountID = "000000000000"
+ defaultRegion = "us-east-1"
+)
+
+// Matches returns true if the request looks like an AWS STS query-protocol call
+// (POST + form-encoded body whose Action is one of the known STS operations).
+// Calling ParseForm here caches the parsed form on the request so ServeHTTP can
+// use it without re-reading the body.
+func (*Handler) Matches(r *http.Request) bool {
+ if r.Header.Get("X-Amz-Target") != "" {
+ return false
+ }
+
+ if r.Method != http.MethodPost {
+ return false
+ }
+
+ if !strings.HasPrefix(r.Header.Get("Content-Type"), formContentType) {
+ return false
+ }
+
+ r.Body = http.MaxBytesReader(nil, r.Body, maxFormBodyBytes)
+ if err := r.ParseForm(); err != nil {
+ return false
+ }
+
+ _, ok := stsActions[r.Form.Get("Action")]
+
+ return ok
+}
+
+// ServeHTTP dispatches on Action. The form has already been parsed by Matches.
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ switch r.Form.Get("Action") {
+ case "GetCallerIdentity":
+ h.getCallerIdentity(w, r)
+ case "AssumeRole":
+ h.assumeRole(w, r)
+ case "GetSessionToken":
+ h.getSessionToken(w, r)
+ default:
+ awsquery.WriteXMLError(w, http.StatusBadRequest,
+ "InvalidAction", "unknown STS action: "+r.Form.Get("Action"))
+ }
+}
diff --git a/server/aws/sts/operations.go b/server/aws/sts/operations.go
new file mode 100644
index 0000000..9549568
--- /dev/null
+++ b/server/aws/sts/operations.go
@@ -0,0 +1,109 @@
+package sts
+
+import (
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
+)
+
+// callerUserName is the synthetic IAM user name reported by GetCallerIdentity.
+const callerUserName = "cloudemu"
+
+// sessionDuration is the lifetime baked into synthetic temporary credentials.
+// Real STS defaults to 1h for AssumeRole and 12h for GetSessionToken; a fixed
+// value in the future is all any SDK requires.
+const sessionDuration = time.Hour
+
+// getCallerIdentity reports the configured account, a synthetic user ARN, and a
+// synthetic user id. This is the call most SDK init paths make.
+func (h *Handler) getCallerIdentity(w http.ResponseWriter, _ *http.Request) {
+ awsquery.WriteXMLResponse(w, getCallerIdentityResponse{
+ Xmlns: Namespace,
+ Result: getCallerIdentityResult{
+ Account: h.accountID,
+ Arn: "arn:aws:iam::" + h.accountID + ":user/" + callerUserName,
+ UserID: "AIDACLOUDEMU0000000000",
+ },
+ Metadata: responseMetadata{RequestID: awsquery.RequestID},
+ })
+}
+
+// assumeRole returns synthetic temporary credentials and an AssumedRoleUser
+// derived from the requested RoleArn and RoleSessionName.
+func (h *Handler) assumeRole(w http.ResponseWriter, r *http.Request) {
+ roleArn := r.Form.Get("RoleArn")
+ sessionName := r.Form.Get("RoleSessionName")
+
+ if sessionName == "" {
+ sessionName = "cloudemu-session"
+ }
+
+ // The assumed-role ARN AWS returns is
+ // arn:aws:sts::{account}:assumed-role/{role-name}/{session-name}
+ // where role-name is the last path segment of the requested RoleArn.
+ roleName := roleNameFromArn(roleArn)
+ assumedArn := "arn:aws:sts::" + h.accountID + ":assumed-role/" + roleName + "/" + sessionName
+
+ awsquery.WriteXMLResponse(w, assumeRoleResponse{
+ Xmlns: Namespace,
+ Result: assumeRoleResult{
+ Credentials: h.synthCredentials(),
+ AssumedRoleUser: assumedRoleUser{
+ AssumedRoleID: "AROACLOUDEMU0000000000:" + sessionName,
+ Arn: assumedArn,
+ },
+ },
+ Metadata: responseMetadata{RequestID: awsquery.RequestID},
+ })
+}
+
+// getSessionToken returns synthetic temporary credentials.
+func (h *Handler) getSessionToken(w http.ResponseWriter, _ *http.Request) {
+ awsquery.WriteXMLResponse(w, getSessionTokenResponse{
+ Xmlns: Namespace,
+ Result: getSessionTokenResult{Credentials: h.synthCredentials()},
+ Metadata: responseMetadata{RequestID: awsquery.RequestID},
+ })
+}
+
+// synthCredentials builds a deterministic set of temporary credentials with an
+// expiration in the future. cloudemu does not validate signatures, so any
+// non-empty values satisfy SDK clients.
+func (h *Handler) synthCredentials() credentials {
+ return credentials{
+ AccessKeyID: "ASIACLOUDEMU000000000",
+ SecretAccessKey: "cloudemuSecretAccessKey0000000000000000",
+ SessionToken: "cloudemu-session-token",
+ Expiration: time.Now().UTC().Add(sessionDuration).Format(time.RFC3339),
+ }
+}
+
+// roleNameFromArn extracts the role name (last path segment) from a role ARN
+// such as "arn:aws:iam::123456789012:role/path/MyRole". Falls back to a stable
+// placeholder when the ARN is missing or malformed.
+func roleNameFromArn(arn string) string {
+ if arn == "" {
+ return "cloudemu-role"
+ }
+
+ // Role ARNs are "...:role/" (name may itself contain a path with
+ // slashes); take the segment after ":role/", then its last path element.
+ name := arn
+ if _, after, ok := strings.Cut(arn, ":role/"); ok {
+ name = after
+ }
+
+ // Trim any trailing slash(es) so a stray "MyRole/" doesn't yield an empty
+ // last segment.
+ name = strings.TrimRight(name, "/")
+ if i := strings.LastIndex(name, "/"); i >= 0 {
+ name = name[i+1:]
+ }
+ if name == "" {
+ return "cloudemu-role"
+ }
+
+ return name
+}
diff --git a/server/aws/sts/sdk_roundtrip_test.go b/server/aws/sts/sdk_roundtrip_test.go
new file mode 100644
index 0000000..e51d37e
--- /dev/null
+++ b/server/aws/sts/sdk_roundtrip_test.go
@@ -0,0 +1,204 @@
+package sts_test
+
+import (
+ "context"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsconfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ awsec2 "github.com/aws/aws-sdk-go-v2/service/ec2"
+ ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
+ awssts "github.com/aws/aws-sdk-go-v2/service/sts"
+
+ "github.com/stackshy/cloudemu/v2"
+ awsserver "github.com/stackshy/cloudemu/v2/server/aws"
+)
+
+const (
+ testAccountID = "123456789012"
+ testRegion = "us-west-2"
+)
+
+func newServer(t *testing.T, d awsserver.Drivers) *httptest.Server {
+ t.Helper()
+
+ ts := httptest.NewServer(awsserver.New(d))
+ t.Cleanup(ts.Close)
+
+ return ts
+}
+
+func stsClient(t *testing.T, url string) *awssts.Client {
+ t.Helper()
+
+ cfg, err := awsconfig.LoadDefaultConfig(context.Background(),
+ awsconfig.WithRegion(testRegion),
+ awsconfig.WithCredentialsProvider(
+ credentials.NewStaticCredentialsProvider("test", "test", ""),
+ ),
+ )
+ if err != nil {
+ t.Fatalf("aws config: %v", err)
+ }
+
+ return awssts.NewFromConfig(cfg, func(o *awssts.Options) {
+ o.BaseEndpoint = aws.String(url)
+ })
+}
+
+func TestSDKGetCallerIdentity(t *testing.T) {
+ ts := newServer(t, awsserver.Drivers{
+ STS: true,
+ AccountID: testAccountID,
+ Region: testRegion,
+ })
+
+ out, err := stsClient(t, ts.URL).GetCallerIdentity(
+ context.Background(), &awssts.GetCallerIdentityInput{})
+ if err != nil {
+ t.Fatalf("GetCallerIdentity: %v", err)
+ }
+
+ if aws.ToString(out.Account) != testAccountID {
+ t.Errorf("Account = %q, want %q", aws.ToString(out.Account), testAccountID)
+ }
+
+ if arn := aws.ToString(out.Arn); !strings.Contains(arn, testAccountID) {
+ t.Errorf("Arn = %q, want it to contain account id", arn)
+ }
+
+ if aws.ToString(out.UserId) == "" {
+ t.Error("UserId is empty")
+ }
+}
+
+func TestSDKAssumeRole(t *testing.T) {
+ ts := newServer(t, awsserver.Drivers{
+ STS: true,
+ AccountID: testAccountID,
+ Region: testRegion,
+ })
+
+ const roleArn = "arn:aws:iam::123456789012:role/MyTestRole"
+
+ out, err := stsClient(t, ts.URL).AssumeRole(context.Background(), &awssts.AssumeRoleInput{
+ RoleArn: aws.String(roleArn),
+ RoleSessionName: aws.String("my-session"),
+ })
+ if err != nil {
+ t.Fatalf("AssumeRole: %v", err)
+ }
+
+ if out.AssumedRoleUser == nil {
+ t.Fatal("AssumedRoleUser is nil")
+ }
+
+ gotArn := aws.ToString(out.AssumedRoleUser.Arn)
+ wantArn := "arn:aws:sts::" + testAccountID + ":assumed-role/MyTestRole/my-session"
+
+ if gotArn != wantArn {
+ t.Errorf("AssumedRoleUser.Arn = %q, want %q", gotArn, wantArn)
+ }
+
+ if out.Credentials == nil {
+ t.Fatal("Credentials is nil")
+ }
+
+ if aws.ToString(out.Credentials.AccessKeyId) == "" {
+ t.Error("AccessKeyId is empty")
+ }
+
+ if aws.ToString(out.Credentials.SecretAccessKey) == "" {
+ t.Error("SecretAccessKey is empty")
+ }
+
+ if aws.ToString(out.Credentials.SessionToken) == "" {
+ t.Error("SessionToken is empty")
+ }
+
+ if out.Credentials.Expiration == nil {
+ t.Error("Expiration is nil")
+ }
+}
+
+func TestSDKGetSessionToken(t *testing.T) {
+ ts := newServer(t, awsserver.Drivers{
+ STS: true,
+ AccountID: testAccountID,
+ Region: testRegion,
+ })
+
+ out, err := stsClient(t, ts.URL).GetSessionToken(
+ context.Background(), &awssts.GetSessionTokenInput{})
+ if err != nil {
+ t.Fatalf("GetSessionToken: %v", err)
+ }
+
+ if out.Credentials == nil {
+ t.Fatal("Credentials is nil")
+ }
+
+ if aws.ToString(out.Credentials.AccessKeyId) == "" {
+ t.Error("AccessKeyId is empty")
+ }
+
+ if aws.ToString(out.Credentials.SessionToken) == "" {
+ t.Error("SessionToken is empty")
+ }
+}
+
+// TestSTSDoesNotShadowEC2 wires STS alongside EC2 and proves an EC2 action
+// (RunInstances) still reaches the EC2 handler: STS's Matches only claims its
+// own action set, so a query-protocol body bound for EC2 falls through.
+func TestSTSDoesNotShadowEC2(t *testing.T) {
+ cloud := cloudemu.NewAWS()
+
+ ts := newServer(t, awsserver.Drivers{
+ STS: true,
+ EC2: cloud.EC2,
+ AccountID: testAccountID,
+ Region: testRegion,
+ })
+
+ ec2cfg, err := awsconfig.LoadDefaultConfig(context.Background(),
+ awsconfig.WithRegion(testRegion),
+ awsconfig.WithCredentialsProvider(
+ credentials.NewStaticCredentialsProvider("test", "test", ""),
+ ),
+ )
+ if err != nil {
+ t.Fatalf("aws config: %v", err)
+ }
+
+ ec2c := awsec2.NewFromConfig(ec2cfg, func(o *awsec2.Options) {
+ o.BaseEndpoint = aws.String(ts.URL)
+ })
+
+ runOut, err := ec2c.RunInstances(context.Background(), &awsec2.RunInstancesInput{
+ ImageId: aws.String("ami-12345678"),
+ InstanceType: ec2types.InstanceTypeT2Micro,
+ MinCount: aws.Int32(1),
+ MaxCount: aws.Int32(1),
+ })
+ if err != nil {
+ t.Fatalf("RunInstances (should reach EC2, not STS): %v", err)
+ }
+
+ if len(runOut.Instances) != 1 {
+ t.Fatalf("expected 1 instance, got %d", len(runOut.Instances))
+ }
+
+ // And STS still works on the same server.
+ idOut, err := stsClient(t, ts.URL).GetCallerIdentity(
+ context.Background(), &awssts.GetCallerIdentityInput{})
+ if err != nil {
+ t.Fatalf("GetCallerIdentity alongside EC2: %v", err)
+ }
+
+ if aws.ToString(idOut.Account) != testAccountID {
+ t.Errorf("Account = %q, want %q", aws.ToString(idOut.Account), testAccountID)
+ }
+}
diff --git a/server/aws/sts/xml.go b/server/aws/sts/xml.go
new file mode 100644
index 0000000..e428443
--- /dev/null
+++ b/server/aws/sts/xml.go
@@ -0,0 +1,67 @@
+package sts
+
+import "encoding/xml"
+
+// responseMetadata is the
+// trailer every STS response carries.
+type responseMetadata struct {
+ RequestID string `xml:"RequestId"`
+}
+
+// credentials mirrors the STS element. The SDK deserializes
+// AccessKeyId, SecretAccessKey, SessionToken, and Expiration (ISO-8601).
+type credentials struct {
+ AccessKeyID string `xml:"AccessKeyId"`
+ SecretAccessKey string `xml:"SecretAccessKey"`
+ SessionToken string `xml:"SessionToken"`
+ Expiration string `xml:"Expiration"`
+}
+
+// assumedRoleUser mirrors the STS element.
+type assumedRoleUser struct {
+ AssumedRoleID string `xml:"AssumedRoleId"`
+ Arn string `xml:"Arn"`
+}
+
+// GetCallerIdentity ---------------------------------------------------------
+
+type getCallerIdentityResponse struct {
+ XMLName xml.Name `xml:"GetCallerIdentityResponse"`
+ Xmlns string `xml:"xmlns,attr"`
+ Result getCallerIdentityResult `xml:"GetCallerIdentityResult"`
+ Metadata responseMetadata `xml:"ResponseMetadata"`
+}
+
+type getCallerIdentityResult struct {
+ Arn string `xml:"Arn"`
+ UserID string `xml:"UserId"`
+ Account string `xml:"Account"`
+}
+
+// AssumeRole ----------------------------------------------------------------
+
+type assumeRoleResponse struct {
+ XMLName xml.Name `xml:"AssumeRoleResponse"`
+ Xmlns string `xml:"xmlns,attr"`
+ Result assumeRoleResult `xml:"AssumeRoleResult"`
+ Metadata responseMetadata `xml:"ResponseMetadata"`
+}
+
+type assumeRoleResult struct {
+ Credentials credentials `xml:"Credentials"`
+ AssumedRoleUser assumedRoleUser `xml:"AssumedRoleUser"`
+ PackedPolicySize int `xml:"PackedPolicySize"`
+}
+
+// GetSessionToken -----------------------------------------------------------
+
+type getSessionTokenResponse struct {
+ XMLName xml.Name `xml:"GetSessionTokenResponse"`
+ Xmlns string `xml:"xmlns,attr"`
+ Result getSessionTokenResult `xml:"GetSessionTokenResult"`
+ Metadata responseMetadata `xml:"ResponseMetadata"`
+}
+
+type getSessionTokenResult struct {
+ Credentials credentials `xml:"Credentials"`
+}
diff --git a/server/azure/acr/handler.go b/server/azure/acr/handler.go
index a747955..8aa3a0d 100644
--- a/server/azure/acr/handler.go
+++ b/server/azure/acr/handler.go
@@ -23,8 +23,8 @@ import (
"net/http"
"strings"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
const pathPrefix = "/acr/v1/"
diff --git a/server/azure/acr/sdk_roundtrip_test.go b/server/azure/acr/sdk_roundtrip_test.go
index 5d4eca5..69455b7 100644
--- a/server/azure/acr/sdk_roundtrip_test.go
+++ b/server/azure/acr/sdk_roundtrip_test.go
@@ -11,9 +11,9 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
azacr "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry"
- "github.com/stackshy/cloudemu"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
type fakeCred struct{}
diff --git a/server/azure/acr/types.go b/server/azure/acr/types.go
index 1fa715e..5888181 100644
--- a/server/azure/acr/types.go
+++ b/server/azure/acr/types.go
@@ -3,7 +3,7 @@ package acr
import (
"strings"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
// registryLoginServer is the synthetic login server reported for this mock
diff --git a/server/azure/aks/handler.go b/server/azure/aks/handler.go
index 62d6489..bed8fec 100644
--- a/server/azure/aks/handler.go
+++ b/server/azure/aks/handler.go
@@ -39,8 +39,8 @@ import (
"net/http"
"strings"
- "github.com/stackshy/cloudemu/providers/azure/aks"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/providers/azure/aks"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
)
const providerName = "Microsoft.ContainerService"
diff --git a/server/azure/aks/operations.go b/server/azure/aks/operations.go
index b8e70cb..975791f 100644
--- a/server/azure/aks/operations.go
+++ b/server/azure/aks/operations.go
@@ -3,8 +3,8 @@ package aks
import (
"net/http"
- "github.com/stackshy/cloudemu/providers/azure/aks"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/providers/azure/aks"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
)
// ---- Managed Cluster ops ----
diff --git a/server/azure/aks/sdk_dataplane_test.go b/server/azure/aks/sdk_dataplane_test.go
index a0cc45e..f36c19a 100644
--- a/server/azure/aks/sdk_dataplane_test.go
+++ b/server/azure/aks/sdk_dataplane_test.go
@@ -22,9 +22,9 @@ import (
kubescheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/clientcmd"
- "github.com/stackshy/cloudemu"
- cloudkube "github.com/stackshy/cloudemu/kubernetes"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
+ cloudkube "github.com/stackshy/cloudemu/v2/services/kubernetes"
)
// TestSDKAKSDataPlane_FullWorkloadStack drives the end-to-end path:
diff --git a/server/azure/aks/sdk_roundtrip_test.go b/server/azure/aks/sdk_roundtrip_test.go
index 9156621..51910b3 100644
--- a/server/azure/aks/sdk_roundtrip_test.go
+++ b/server/azure/aks/sdk_roundtrip_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
type fakeCred struct{}
diff --git a/server/azure/aks/types.go b/server/azure/aks/types.go
index e1ac7df..93164f9 100644
--- a/server/azure/aks/types.go
+++ b/server/azure/aks/types.go
@@ -1,7 +1,7 @@
package aks
import (
- "github.com/stackshy/cloudemu/providers/azure/aks"
+ "github.com/stackshy/cloudemu/v2/providers/azure/aks"
)
// ARM resource type identifiers.
diff --git a/server/azure/azure.go b/server/azure/azure.go
index a96987b..23294ca 100644
--- a/server/azure/azure.go
+++ b/server/azure/azure.go
@@ -7,67 +7,74 @@
package azure
import (
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- dbxdriver "github.com/stackshy/cloudemu/databricks/driver"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
- "github.com/stackshy/cloudemu/kubernetes"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
- mqdriver "github.com/stackshy/cloudemu/messagequeue/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- rdbdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/resourcediscovery"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/acr"
- aksserver "github.com/stackshy/cloudemu/server/azure/aks"
- "github.com/stackshy/cloudemu/server/azure/azuresql"
- "github.com/stackshy/cloudemu/server/azure/blob"
- cachesrv "github.com/stackshy/cloudemu/server/azure/cache"
- "github.com/stackshy/cloudemu/server/azure/cosmos"
- "github.com/stackshy/cloudemu/server/azure/databricks"
- "github.com/stackshy/cloudemu/server/azure/databricks/dbfs"
- "github.com/stackshy/cloudemu/server/azure/databricks/gitcredentials"
- "github.com/stackshy/cloudemu/server/azure/databricks/hostmeta"
- "github.com/stackshy/cloudemu/server/azure/databricks/pipelines"
- "github.com/stackshy/cloudemu/server/azure/databricks/queryhistory"
- "github.com/stackshy/cloudemu/server/azure/databricks/repos"
- "github.com/stackshy/cloudemu/server/azure/databricks/scim"
- "github.com/stackshy/cloudemu/server/azure/databricks/secrets"
- "github.com/stackshy/cloudemu/server/azure/databricks/serving"
- "github.com/stackshy/cloudemu/server/azure/databricks/sqlwarehouses"
- "github.com/stackshy/cloudemu/server/azure/databricks/token"
- "github.com/stackshy/cloudemu/server/azure/databricks/ucstorage"
- "github.com/stackshy/cloudemu/server/azure/databricks/unitycatalog"
- "github.com/stackshy/cloudemu/server/azure/databricks/wsfs"
- "github.com/stackshy/cloudemu/server/azure/disks"
- dnssrv "github.com/stackshy/cloudemu/server/azure/dns"
- eventgridsrv "github.com/stackshy/cloudemu/server/azure/eventgrid"
- "github.com/stackshy/cloudemu/server/azure/functions"
- "github.com/stackshy/cloudemu/server/azure/iam"
- "github.com/stackshy/cloudemu/server/azure/images"
- keyvaultsrv "github.com/stackshy/cloudemu/server/azure/keyvault"
- lbsrv "github.com/stackshy/cloudemu/server/azure/loadbalancer"
- loganalyticssrv "github.com/stackshy/cloudemu/server/azure/loganalytics"
- "github.com/stackshy/cloudemu/server/azure/monitor"
- "github.com/stackshy/cloudemu/server/azure/mysqlflex"
- "github.com/stackshy/cloudemu/server/azure/network"
- notificationhubssrv "github.com/stackshy/cloudemu/server/azure/notificationhubs"
- "github.com/stackshy/cloudemu/server/azure/postgresflex"
- "github.com/stackshy/cloudemu/server/azure/resourcegraph"
- "github.com/stackshy/cloudemu/server/azure/servicebus"
- "github.com/stackshy/cloudemu/server/azure/snapshots"
- "github.com/stackshy/cloudemu/server/azure/sshpublickeys"
- "github.com/stackshy/cloudemu/server/azure/virtualmachines"
- sdrv "github.com/stackshy/cloudemu/serverless/driver"
- storagedriver "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/acr"
+ aksserver "github.com/stackshy/cloudemu/v2/server/azure/aks"
+ azureaiserver "github.com/stackshy/cloudemu/v2/server/azure/azureai"
+ azuresearchserver "github.com/stackshy/cloudemu/v2/server/azure/azuresearch"
+ "github.com/stackshy/cloudemu/v2/server/azure/azuresql"
+ "github.com/stackshy/cloudemu/v2/server/azure/blob"
+ cachesrv "github.com/stackshy/cloudemu/v2/server/azure/cache"
+ "github.com/stackshy/cloudemu/v2/server/azure/cosmos"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/dbfs"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/gitcredentials"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/hostmeta"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/pipelines"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/queryhistory"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/repos"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/scim"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/secrets"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/serving"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/sqlwarehouses"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/token"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/ucstorage"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/unitycatalog"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/wsfs"
+ "github.com/stackshy/cloudemu/v2/server/azure/disks"
+ dnssrv "github.com/stackshy/cloudemu/v2/server/azure/dns"
+ eventgridsrv "github.com/stackshy/cloudemu/v2/server/azure/eventgrid"
+ "github.com/stackshy/cloudemu/v2/server/azure/functions"
+ "github.com/stackshy/cloudemu/v2/server/azure/iam"
+ "github.com/stackshy/cloudemu/v2/server/azure/images"
+ keyvaultsrv "github.com/stackshy/cloudemu/v2/server/azure/keyvault"
+ lbsrv "github.com/stackshy/cloudemu/v2/server/azure/loadbalancer"
+ loganalyticssrv "github.com/stackshy/cloudemu/v2/server/azure/loganalytics"
+ "github.com/stackshy/cloudemu/v2/server/azure/monitor"
+ "github.com/stackshy/cloudemu/v2/server/azure/mysqlflex"
+ "github.com/stackshy/cloudemu/v2/server/azure/network"
+ notificationhubssrv "github.com/stackshy/cloudemu/v2/server/azure/notificationhubs"
+ "github.com/stackshy/cloudemu/v2/server/azure/postgresflex"
+ "github.com/stackshy/cloudemu/v2/server/azure/queue"
+ "github.com/stackshy/cloudemu/v2/server/azure/resourcegraph"
+ "github.com/stackshy/cloudemu/v2/server/azure/servicebus"
+ "github.com/stackshy/cloudemu/v2/server/azure/snapshots"
+ "github.com/stackshy/cloudemu/v2/server/azure/sshpublickeys"
+ tablesrv "github.com/stackshy/cloudemu/v2/server/azure/table"
+ "github.com/stackshy/cloudemu/v2/server/azure/virtualmachines"
+ azureaidriver "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+ azuresearchdriver "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
+ dbxdriver "github.com/stackshy/cloudemu/v2/services/databricks/driver"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
+ "github.com/stackshy/cloudemu/v2/services/kubernetes"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
+ mqdriver "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
+ rdbdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
+ sdrv "github.com/stackshy/cloudemu/v2/services/serverless/driver"
+ storagedriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
+ tabledriver "github.com/stackshy/cloudemu/v2/services/tablestorage/driver"
)
// Drivers bundles the driver interfaces the Azure server can expose. Leave a
@@ -84,17 +91,23 @@ type Drivers struct {
Images computedriver.Compute
SSHPublicKeys computedriver.Compute
BlobStorage storagedriver.Bucket
- CosmosDB dbdriver.Database
- Network netdriver.Networking
- Monitor mondriver.Monitoring
- Functions sdrv.Serverless
- ServiceBus mqdriver.MessageQueue
- SQL rdbdriver.RelationalDB
- PostgresFlex rdbdriver.RelationalDB
- MySQLFlex rdbdriver.RelationalDB
- AKS aksserver.Backend
- IAM iamdriver.IAM
- ACR crdriver.ContainerRegistry
+ // QueueStorage serves the Azure Queue Storage data-plane REST API against
+ // the messagequeue driver.
+ QueueStorage mqdriver.MessageQueue
+ // TableStorage serves the Azure Table Storage data-plane REST API against
+ // the tablestorage driver.
+ TableStorage tabledriver.TableStorage
+ CosmosDB dbdriver.Database
+ Network netdriver.Networking
+ Monitor mondriver.Monitoring
+ Functions sdrv.Serverless
+ ServiceBus mqdriver.MessageQueue
+ SQL rdbdriver.RelationalDB
+ PostgresFlex rdbdriver.RelationalDB
+ MySQLFlex rdbdriver.RelationalDB
+ AKS aksserver.Backend
+ IAM iamdriver.IAM
+ ACR crdriver.ContainerRegistry
// KeyVault serves the Key Vault secrets data-plane API (/secrets/…)
// against the secrets driver.
KeyVault secretsdriver.Secrets
@@ -120,6 +133,11 @@ type Drivers struct {
NotificationHubs notifdriver.Notification
Databricks dbxdriver.Databricks
DatabricksDataPlane dbxdriver.DataPlane
+ CognitiveServices azureaidriver.CognitiveServices
+ MachineLearning azureaidriver.MachineLearning
+ AzureAIDataPlane azureaidriver.DataPlane
+ SearchControl azuresearchdriver.SearchControl
+ SearchDataPlane azuresearchdriver.SearchDataPlane
// K8sAPI is the shared in-memory Kubernetes data-plane API server. It is
// shared with awsserver.Drivers.K8sAPI and gcpserver.Drivers.K8sAPI so a
// kubeconfig issued by any provider's control plane (EKS/AKS/GKE) reaches
@@ -266,6 +284,35 @@ func New(d Drivers) *server.Server {
registerDatabricksDataPlane(srv, &d)
+ // Cognitive Services matches on Microsoft.CognitiveServices/accounts — a
+ // distinct ARM provider name, so registration order is unconstrained.
+ if d.CognitiveServices != nil {
+ srv.Register(azureaiserver.NewCognitiveServices(d.CognitiveServices))
+ }
+
+ // Azure ML matches on Microsoft.MachineLearningServices — a distinct ARM
+ // provider name, so registration order is unconstrained.
+ if d.MachineLearning != nil {
+ srv.Register(azureaiserver.NewMachineLearning(d.MachineLearning))
+ }
+
+ // Azure AI data plane (Azure OpenAI inference + Assistants, AML scoring).
+ // Matches on /openai/ and /score — disjoint from the ARM /subscriptions/
+ // prefix, so registration order is unconstrained.
+ if d.AzureAIDataPlane != nil {
+ srv.Register(azureaiserver.NewDataPlane(d.AzureAIDataPlane))
+ }
+
+ // Azure AI Search — ARM control plane on Microsoft.Search, plus the
+ // host/path-routed search data plane (/indexes, /indexers, …).
+ if d.SearchControl != nil {
+ srv.Register(azuresearchserver.NewControl(d.SearchControl))
+ }
+
+ if d.SearchDataPlane != nil {
+ srv.Register(azuresearchserver.NewDataPlane(d.SearchDataPlane))
+ }
+
if d.VirtualMachines != nil {
srv.Register(virtualmachines.New(d.VirtualMachines))
}
@@ -303,6 +350,24 @@ func New(d Drivers) *server.Server {
srv.Register(keyvaultsrv.New(d.KeyVault))
}
+ // Table Storage matches the OData table surface (/Tables, /Tables('name'),
+ // /{table}(…) entity predicates, and POST /{table} inserts) — path shapes
+ // that contain parentheses or a bare JSON POST, disjoint from Blob's
+ // container/blob paths and Queue's /messages surface. Registered before the
+ // permissive Blob fallback.
+ if d.TableStorage != nil {
+ srv.Register(tablesrv.New(d.TableStorage))
+ }
+
+ // Queue Storage matches the queue data-plane surface (/{queue}/messages,
+ // bare PUT/DELETE /{queue} without restype=container). These shapes are
+ // disjoint from Blob (which carries restype=container) and Table (which
+ // carries OData parentheses). Registered before the permissive Blob
+ // fallback.
+ if d.QueueStorage != nil {
+ srv.Register(queue.New(d.QueueStorage))
+ }
+
// BlobStorage handler is the data-plane fallback for non-ARM URLs. It
// must register last so its permissive Matches() doesn't shadow the
// ARM-specific resource handlers.
diff --git a/server/azure/azureai/accounts.go b/server/azure/azureai/accounts.go
new file mode 100644
index 0000000..8dc57ea
--- /dev/null
+++ b/server/azure/azureai/accounts.go
@@ -0,0 +1,257 @@
+package azureai
+
+import (
+ "net/http"
+
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ csdriver "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+// armAccountBody is the ARM request body for an account PUT.
+type armAccountBody struct {
+ Location string `json:"location"`
+ Kind string `json:"kind"`
+ SKU *struct {
+ Name string `json:"name"`
+ } `json:"sku"`
+ Tags map[string]string `json:"tags"`
+ Properties *struct {
+ CustomSubDomainName string `json:"customSubDomainName"`
+ } `json:"properties"`
+}
+
+func accountJSON(a *csdriver.Account) map[string]any {
+ return map[string]any{
+ "id": a.ID,
+ "name": a.Name,
+ "type": csProvider + "/accounts",
+ "location": a.Location,
+ "kind": a.Kind,
+ "sku": map[string]any{"name": a.SKUName},
+ "tags": a.Tags,
+ "properties": map[string]any{
+ "endpoint": a.Endpoint,
+ "provisioningState": a.ProvisioningState,
+ "customSubDomainName": a.CustomDomain,
+ },
+ }
+}
+
+func (h *CognitiveServicesHandler) createOrUpdateAccount(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ var body armAccountBody
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ cfg := csdriver.AccountConfig{
+ Name: rp.ResourceName,
+ ResourceGroup: rp.ResourceGroup,
+ Location: body.Location,
+ Kind: body.Kind,
+ Tags: body.Tags,
+ }
+
+ if body.SKU != nil {
+ cfg.SKUName = body.SKU.Name
+ }
+
+ if body.Properties != nil {
+ cfg.CustomDomain = body.Properties.CustomSubDomainName
+ }
+
+ a, err := h.svc.CreateAccount(r.Context(), cfg)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, accountJSON(a))
+}
+
+func (h *CognitiveServicesHandler) getAccount(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ a, err := h.svc.GetAccount(r.Context(), rp.ResourceGroup, rp.ResourceName)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, accountJSON(a))
+}
+
+func (h *CognitiveServicesHandler) patchAccount(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ var body struct {
+ Tags map[string]string `json:"tags"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ a, err := h.svc.UpdateAccountTags(r.Context(), rp.ResourceGroup, rp.ResourceName, body.Tags)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, accountJSON(a))
+}
+
+func (h *CognitiveServicesHandler) deleteAccount(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ if err := h.svc.DeleteAccount(r.Context(), rp.ResourceGroup, rp.ResourceName); err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{})
+}
+
+//nolint:dupl // list-then-map shape mirrors the workspaces handler.
+func (h *CognitiveServicesHandler) listAccounts(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ var (
+ accts []csdriver.Account
+ err error
+ )
+
+ if rp.ResourceGroup == "" {
+ accts, err = h.svc.ListAccounts(r.Context())
+ } else {
+ accts, err = h.svc.ListAccountsByResourceGroup(r.Context(), rp.ResourceGroup)
+ }
+
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(accts))
+ for i := range accts {
+ out = append(out, accountJSON(&accts[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
+
+// accountActionMethods is the HTTP method each account action requires, matching
+// the real Microsoft.CognitiveServices REST contract.
+//
+//nolint:gochecknoglobals // immutable routing set
+var accountActionMethods = map[string]string{
+ "listKeys": http.MethodPost,
+ "regenerateKey": http.MethodPost,
+ "models": http.MethodGet,
+ "skus": http.MethodGet,
+ "usages": http.MethodGet,
+}
+
+// serveAccountAction handles the account-level verbs and read-only catalogs.
+func (h *CognitiveServicesHandler) serveAccountAction(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ if want, ok := accountActionMethods[rp.SubResource]; ok && r.Method != want {
+ writeMethodNotAllowed(w)
+
+ return
+ }
+
+ switch rp.SubResource {
+ case "listKeys":
+ h.listKeys(w, r, rp)
+ case "regenerateKey":
+ h.regenerateKey(w, r, rp)
+ case "models":
+ h.listModels(w, r, rp)
+ case "skus":
+ h.listSkus(w, r, rp)
+ case "usages":
+ h.listUsages(w, r, rp)
+ default:
+ azurearm.WriteError(w, http.StatusNotFound, "NotFound", "unknown account action: "+rp.SubResource)
+ }
+}
+
+func (h *CognitiveServicesHandler) listKeys(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ keys, err := h.svc.ListAccountKeys(r.Context(), rp.ResourceGroup, rp.ResourceName)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"key1": keys.Key1, "key2": keys.Key2})
+}
+
+func (h *CognitiveServicesHandler) regenerateKey(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ var body struct {
+ KeyName string `json:"keyName"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ keys, err := h.svc.RegenerateAccountKey(r.Context(), rp.ResourceGroup, rp.ResourceName, body.KeyName)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"key1": keys.Key1, "key2": keys.Key2})
+}
+
+func (h *CognitiveServicesHandler) listModels(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ models, err := h.svc.ListAccountModels(r.Context(), rp.ResourceGroup, rp.ResourceName)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(models))
+ for _, mdl := range models {
+ out = append(out, map[string]any{
+ "name": mdl.Name, "format": mdl.Format, "kind": mdl.Kind,
+ "model": map[string]any{"name": mdl.Name, "version": mdl.Version, "format": mdl.Format},
+ })
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
+
+func (h *CognitiveServicesHandler) listSkus(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ skus, err := h.svc.ListAccountSkus(r.Context(), rp.ResourceGroup, rp.ResourceName)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(skus))
+ for _, s := range skus {
+ out = append(out, map[string]any{"resourceType": "accounts", "sku": map[string]any{"name": s.Name, "tier": s.Tier}})
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
+
+func (h *CognitiveServicesHandler) listUsages(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ usages, err := h.svc.ListAccountUsages(r.Context(), rp.ResourceGroup, rp.ResourceName)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(usages))
+ for _, u := range usages {
+ out = append(out, map[string]any{
+ "name": map[string]any{"value": u.Name, "localizedValue": u.Name},
+ "currentValue": u.CurrentValue, "limit": u.Limit, "unit": u.Unit,
+ })
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
diff --git a/server/azure/azureai/assistants.go b/server/azure/azureai/assistants.go
new file mode 100644
index 0000000..ba12990
--- /dev/null
+++ b/server/azure/azureai/assistants.go
@@ -0,0 +1,227 @@
+package azureai
+
+import (
+ "net/http"
+
+ csdriver "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+func assistantJSON(a *csdriver.Assistant) map[string]any {
+ return map[string]any{
+ "id": a.ID, "object": "assistant", "model": a.Model, "name": a.Name,
+ "instructions": a.Instructions, "created_at": a.CreatedAt,
+ }
+}
+
+func threadJSON(t *csdriver.Thread) map[string]any {
+ return map[string]any{"id": t.ID, "object": "thread", "created_at": t.CreatedAt}
+}
+
+func messageJSON(m *csdriver.ThreadMessage) map[string]any {
+ return map[string]any{
+ "id": m.ID, "object": "thread.message", "thread_id": m.ThreadID,
+ "role": m.Role, "content": m.Content, "created_at": m.CreatedAt,
+ }
+}
+
+func runJSON(r *csdriver.Run) map[string]any {
+ return map[string]any{
+ "id": r.ID, "object": "thread.run", "thread_id": r.ThreadID,
+ "assistant_id": r.AssistantID, "status": r.Status, "created_at": r.CreatedAt,
+ }
+}
+
+// serveAssistants handles /openai/assistants[/{id}].
+func (h *DataPlaneHandler) serveAssistants(w http.ResponseWriter, r *http.Request, account string, parts []string) {
+ if len(parts) == 1 {
+ switch r.Method {
+ case http.MethodPost:
+ h.createAssistant(w, r, account)
+ case http.MethodGet:
+ h.listAssistants(w, r, account)
+ default:
+ dpErr(w, http.StatusMethodNotAllowed, "method not allowed")
+ }
+
+ return
+ }
+
+ id := parts[1]
+
+ switch r.Method {
+ case http.MethodGet:
+ a, err := h.dp.GetAssistant(r.Context(), account, id)
+ writeDP(w, assistantJSON, a, err)
+ case http.MethodDelete:
+ if err := h.dp.DeleteAssistant(r.Context(), account, id); err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ dpJSON(w, map[string]any{"id": id, "object": "assistant.deleted", "deleted": true})
+ default:
+ dpErr(w, http.StatusMethodNotAllowed, "method not allowed")
+ }
+}
+
+func (h *DataPlaneHandler) createAssistant(w http.ResponseWriter, r *http.Request, account string) {
+ var body struct {
+ Model string `json:"model"`
+ Name string `json:"name"`
+ Instructions string `json:"instructions"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ a, err := h.dp.CreateAssistant(r.Context(), csdriver.AssistantConfig{
+ Account: account, Model: body.Model, Name: body.Name, Instructions: body.Instructions,
+ })
+ writeDP(w, assistantJSON, a, err)
+}
+
+func (h *DataPlaneHandler) listAssistants(w http.ResponseWriter, r *http.Request, account string) {
+ as, err := h.dp.ListAssistants(r.Context(), account)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ data := make([]map[string]any, 0, len(as))
+ for i := range as {
+ data = append(data, assistantJSON(&as[i]))
+ }
+
+ dpJSON(w, map[string]any{"object": "list", "data": data})
+}
+
+// serveThreads handles /openai/threads[/{id}[/messages|/runs[/{id}]]].
+func (h *DataPlaneHandler) serveThreads(w http.ResponseWriter, r *http.Request, account string, parts []string) {
+ if len(parts) == 1 {
+ if r.Method != http.MethodPost {
+ dpErr(w, http.StatusMethodNotAllowed, "method not allowed")
+
+ return
+ }
+
+ _ = r.Body.Close()
+
+ t, err := h.dp.CreateThread(r.Context(), account)
+ writeDP(w, threadJSON, t, err)
+
+ return
+ }
+
+ threadID := parts[1]
+
+ const threadSubResIdx = 2
+ if len(parts) > threadSubResIdx {
+ switch parts[threadSubResIdx] {
+ case "messages":
+ h.serveMessages(w, r, account, threadID)
+ case "runs":
+ h.serveRuns(w, r, account, threadID, parts)
+ default:
+ dpErr(w, http.StatusNotFound, "unknown thread sub-resource: "+parts[2])
+ }
+
+ return
+ }
+
+ switch r.Method {
+ case http.MethodGet:
+ t, err := h.dp.GetThread(r.Context(), account, threadID)
+ writeDP(w, threadJSON, t, err)
+ case http.MethodDelete:
+ if err := h.dp.DeleteThread(r.Context(), account, threadID); err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ dpJSON(w, map[string]any{"id": threadID, "object": "thread.deleted", "deleted": true})
+ default:
+ dpErr(w, http.StatusMethodNotAllowed, "method not allowed")
+ }
+}
+
+func (h *DataPlaneHandler) serveMessages(w http.ResponseWriter, r *http.Request, account, threadID string) {
+ switch r.Method {
+ case http.MethodPost:
+ var body struct {
+ Role string `json:"role"`
+ Content string `json:"content"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ msg, err := h.dp.CreateMessage(r.Context(), account, threadID, body.Role, body.Content)
+ writeDP(w, messageJSON, msg, err)
+ case http.MethodGet:
+ msgs, err := h.dp.ListMessages(r.Context(), account, threadID)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ data := make([]map[string]any, 0, len(msgs))
+ for i := range msgs {
+ data = append(data, messageJSON(&msgs[i]))
+ }
+
+ dpJSON(w, map[string]any{"object": "list", "data": data})
+ default:
+ dpErr(w, http.StatusMethodNotAllowed, "method not allowed")
+ }
+}
+
+func (h *DataPlaneHandler) serveRuns(w http.ResponseWriter, r *http.Request, account, threadID string, parts []string) {
+ // /openai/threads/{id}/runs/{runID}
+ const runIDIdx = 3
+ if len(parts) > runIDIdx {
+ if r.Method != http.MethodGet {
+ dpErr(w, http.StatusMethodNotAllowed, "method not allowed")
+
+ return
+ }
+
+ run, err := h.dp.GetRun(r.Context(), account, threadID, parts[runIDIdx])
+ writeDP(w, runJSON, run, err)
+
+ return
+ }
+
+ if r.Method != http.MethodPost {
+ dpErr(w, http.StatusMethodNotAllowed, "method not allowed")
+
+ return
+ }
+
+ var body struct {
+ AssistantID string `json:"assistant_id"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ run, err := h.dp.CreateRun(r.Context(), account, threadID, body.AssistantID)
+ writeDP(w, runJSON, run, err)
+}
+
+// writeDP writes a created/fetched data-plane resource or maps the error.
+func writeDP[T any](w http.ResponseWriter, toJSON func(*T) map[string]any, res *T, err error) {
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ dpJSON(w, toJSON(res))
+}
diff --git a/server/azure/azureai/children.go b/server/azure/azureai/children.go
new file mode 100644
index 0000000..9e243a4
--- /dev/null
+++ b/server/azure/azureai/children.go
@@ -0,0 +1,317 @@
+package azureai
+
+import (
+ "net/http"
+
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ csdriver "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+// serveChild dispatches account sub-resource requests by collection, method,
+// and whether a child name is present.
+func (h *CognitiveServicesHandler) serveChild(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ if rp.SubResourceName == "" {
+ if r.Method != http.MethodGet {
+ writeMethodNotAllowed(w)
+
+ return
+ }
+
+ h.listChildren(w, r, rp)
+
+ return
+ }
+
+ switch r.Method {
+ case http.MethodPut:
+ h.putChild(w, r, rp)
+ case http.MethodGet:
+ h.getChild(w, r, rp)
+ case http.MethodDelete:
+ h.deleteChild(w, r, rp)
+ default:
+ writeMethodNotAllowed(w)
+ }
+}
+
+func deploymentJSON(d *csdriver.Deployment) map[string]any {
+ return map[string]any{
+ "id": d.ID, "name": d.Name, "type": csProvider + "/accounts/deployments",
+ "sku": map[string]any{"name": d.SKUName, "capacity": d.SKUCapacity},
+ "properties": map[string]any{
+ "provisioningState": d.ProvisioningState,
+ "model": map[string]any{
+ "name": d.ModelName, "version": d.ModelVersion, "format": d.ModelFormat,
+ },
+ },
+ }
+}
+
+func projectJSON(p *csdriver.Project) map[string]any {
+ return map[string]any{
+ "id": p.ID, "name": p.Name, "type": csProvider + "/accounts/projects",
+ "location": p.Location, "tags": p.Tags,
+ "properties": map[string]any{
+ "displayName": p.DisplayName, "description": p.Description,
+ "provisioningState": p.ProvisioningState,
+ },
+ }
+}
+
+func raiPolicyJSON(p *csdriver.RaiPolicy) map[string]any {
+ return map[string]any{
+ "id": p.ID, "name": p.Name, "type": csProvider + "/accounts/raiPolicies",
+ "properties": map[string]any{"mode": p.Mode, "basePolicyName": p.BasePolicy},
+ }
+}
+
+func commitmentPlanJSON(p *csdriver.CommitmentPlan) map[string]any {
+ return map[string]any{
+ "id": p.ID, "name": p.Name, "type": csProvider + "/accounts/commitmentPlans",
+ "properties": map[string]any{
+ "planType": p.PlanType, "autoRenew": p.AutoRenew,
+ "provisioningState": p.ProvisioningState,
+ },
+ }
+}
+
+func pecJSON(c *csdriver.PrivateEndpointConnection) map[string]any {
+ return map[string]any{
+ "id": c.ID, "name": c.Name, "type": csProvider + "/accounts/privateEndpointConnections",
+ "properties": map[string]any{
+ "provisioningState": c.ProvisioningState,
+ "privateLinkServiceConnectionState": map[string]any{
+ "status": c.Status, "description": c.Description,
+ },
+ },
+ }
+}
+
+//nolint:gocyclo,funlen // flat per-collection dispatch; one decode+case per child type.
+func (h *CognitiveServicesHandler) putChild(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ rg, account, name := rp.ResourceGroup, rp.ResourceName, rp.SubResourceName
+
+ switch rp.SubResource {
+ case collDeployments:
+ var body struct {
+ SKU *struct {
+ Name string `json:"name"`
+ Capacity int `json:"capacity"`
+ } `json:"sku"`
+ Properties struct {
+ Model struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Format string `json:"format"`
+ } `json:"model"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ cfg := csdriver.DeploymentConfig{
+ Account: account, ResourceGroup: rg, Name: name,
+ ModelName: body.Properties.Model.Name,
+ ModelVersion: body.Properties.Model.Version,
+ ModelFormat: body.Properties.Model.Format,
+ }
+ if body.SKU != nil {
+ cfg.SKUName = body.SKU.Name
+ cfg.SKUCapacity = body.SKU.Capacity
+ }
+
+ d, err := h.svc.CreateDeployment(r.Context(), cfg)
+ writeChild(w, deploymentJSON, d, err)
+ case collProjects:
+ var body struct {
+ Location string `json:"location"`
+ Tags map[string]string `json:"tags"`
+ Properties struct {
+ DisplayName string `json:"displayName"`
+ Description string `json:"description"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ p, err := h.svc.CreateProject(r.Context(), csdriver.ProjectConfig{
+ Account: account, ResourceGroup: rg, Name: name, Location: body.Location,
+ DisplayName: body.Properties.DisplayName, Description: body.Properties.Description, Tags: body.Tags,
+ })
+ writeChild(w, projectJSON, p, err)
+ case collRaiPolicies:
+ var body struct {
+ Properties struct {
+ Mode string `json:"mode"`
+ BasePolicyName string `json:"basePolicyName"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ p, err := h.svc.CreateRaiPolicy(r.Context(), csdriver.RaiPolicyConfig{
+ Account: account, ResourceGroup: rg, Name: name,
+ Mode: body.Properties.Mode, BasePolicy: body.Properties.BasePolicyName,
+ })
+ writeChild(w, raiPolicyJSON, p, err)
+ case collCommitmentPlans:
+ var body struct {
+ Properties struct {
+ PlanType string `json:"planType"`
+ AutoRenew bool `json:"autoRenew"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ p, err := h.svc.CreateCommitmentPlan(r.Context(), csdriver.CommitmentPlanConfig{
+ Account: account, ResourceGroup: rg, Name: name,
+ PlanType: body.Properties.PlanType, AutoRenew: body.Properties.AutoRenew,
+ })
+ writeChild(w, commitmentPlanJSON, p, err)
+ case collPEC:
+ var body struct {
+ Properties struct {
+ State struct {
+ Status string `json:"status"`
+ Description string `json:"description"`
+ } `json:"privateLinkServiceConnectionState"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ c, err := h.svc.PutPrivateEndpointConnection(r.Context(), rg, account, name, body.Properties.State.Status)
+ writeChild(w, pecJSON, c, err)
+ }
+}
+
+func (h *CognitiveServicesHandler) getChild(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ rg, account, name := rp.ResourceGroup, rp.ResourceName, rp.SubResourceName
+
+ switch rp.SubResource {
+ case collDeployments:
+ d, err := h.svc.GetDeployment(r.Context(), rg, account, name)
+ writeChild(w, deploymentJSON, d, err)
+ case collProjects:
+ p, err := h.svc.GetProject(r.Context(), rg, account, name)
+ writeChild(w, projectJSON, p, err)
+ case collRaiPolicies:
+ p, err := h.svc.GetRaiPolicy(r.Context(), rg, account, name)
+ writeChild(w, raiPolicyJSON, p, err)
+ case collCommitmentPlans:
+ p, err := h.svc.GetCommitmentPlan(r.Context(), rg, account, name)
+ writeChild(w, commitmentPlanJSON, p, err)
+ case collPEC:
+ c, err := h.svc.GetPrivateEndpointConnection(r.Context(), rg, account, name)
+ writeChild(w, pecJSON, c, err)
+ }
+}
+
+func (h *CognitiveServicesHandler) deleteChild(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ rg, account, name := rp.ResourceGroup, rp.ResourceName, rp.SubResourceName
+
+ var err error
+
+ switch rp.SubResource {
+ case collDeployments:
+ err = h.svc.DeleteDeployment(r.Context(), rg, account, name)
+ case collProjects:
+ err = h.svc.DeleteProject(r.Context(), rg, account, name)
+ case collRaiPolicies:
+ err = h.svc.DeleteRaiPolicy(r.Context(), rg, account, name)
+ case collCommitmentPlans:
+ err = h.svc.DeleteCommitmentPlan(r.Context(), rg, account, name)
+ case collPEC:
+ err = h.svc.DeletePrivateEndpointConnection(r.Context(), rg, account, name)
+ }
+
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{})
+}
+
+//nolint:gocyclo // flat per-collection dispatch; one case per child type.
+func (h *CognitiveServicesHandler) listChildren(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ rg, account := rp.ResourceGroup, rp.ResourceName
+
+ var (
+ out []map[string]any
+ err error
+ )
+
+ switch rp.SubResource {
+ case collDeployments:
+ var ds []csdriver.Deployment
+
+ ds, err = h.svc.ListDeployments(r.Context(), rg, account)
+ for i := range ds {
+ out = append(out, deploymentJSON(&ds[i]))
+ }
+ case collProjects:
+ var ps []csdriver.Project
+
+ ps, err = h.svc.ListProjects(r.Context(), rg, account)
+ for i := range ps {
+ out = append(out, projectJSON(&ps[i]))
+ }
+ case collRaiPolicies:
+ var ps []csdriver.RaiPolicy
+
+ ps, err = h.svc.ListRaiPolicies(r.Context(), rg, account)
+ for i := range ps {
+ out = append(out, raiPolicyJSON(&ps[i]))
+ }
+ case collCommitmentPlans:
+ var ps []csdriver.CommitmentPlan
+
+ ps, err = h.svc.ListCommitmentPlans(r.Context(), rg, account)
+ for i := range ps {
+ out = append(out, commitmentPlanJSON(&ps[i]))
+ }
+ case collPEC:
+ var cs []csdriver.PrivateEndpointConnection
+
+ cs, err = h.svc.ListPrivateEndpointConnections(r.Context(), rg, account)
+ for i := range cs {
+ out = append(out, pecJSON(&cs[i]))
+ }
+ }
+
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ if out == nil {
+ out = []map[string]any{}
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
+
+// writeChild writes a created/fetched child resource or maps the error.
+func writeChild[T any](w http.ResponseWriter, toJSON func(*T) map[string]any, res *T, err error) {
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, toJSON(res))
+}
diff --git a/server/azure/azureai/dataplane.go b/server/azure/azureai/dataplane.go
new file mode 100644
index 0000000..eb5cbf8
--- /dev/null
+++ b/server/azure/azureai/dataplane.go
@@ -0,0 +1,351 @@
+package azureai
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/stackshy/cloudemu/v2/errors"
+ csdriver "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+const (
+ maxDPBody = 4 << 20
+ scorePath = "/score"
+ openaiRoot = "/openai/"
+)
+
+// DataPlaneHandler serves the Azure OpenAI inference + Assistants data plane and
+// AML online-endpoint scoring. Routing is path-based; the account scope is
+// derived from the request Host subdomain (e.g. {account}.openai.azure.com).
+type DataPlaneHandler struct {
+ dp csdriver.DataPlane
+}
+
+// NewDataPlane returns a data-plane handler backed by drv.
+func NewDataPlane(drv csdriver.DataPlane) *DataPlaneHandler {
+ return &DataPlaneHandler{dp: drv}
+}
+
+// Matches claims the /openai/ data-plane surface and the /score AML scoring
+// endpoint.
+func (*DataPlaneHandler) Matches(r *http.Request) bool {
+ p := r.URL.Path
+
+ return strings.HasPrefix(p, openaiRoot) || p == scorePath
+}
+
+// ServeHTTP routes by path.
+func (h *DataPlaneHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == scorePath {
+ h.score(w, r)
+
+ return
+ }
+
+ parts := strings.Split(strings.Trim(strings.TrimPrefix(r.URL.Path, "/openai"), "/"), "/")
+ account := accountFromHost(r.Host)
+
+ switch parts[0] {
+ case collDeployments:
+ h.serveInference(w, r, account, parts)
+ case "assistants":
+ h.serveAssistants(w, r, account, parts)
+ case "threads":
+ h.serveThreads(w, r, account, parts)
+ default:
+ dpErr(w, http.StatusNotFound, "unknown data-plane resource: "+parts[0])
+ }
+}
+
+// accountFromHost extracts the account label from an Azure AI data-plane host
+// such as {account}.openai.azure.com. Falls back to "default" for hosts that
+// don't carry an account subdomain (e.g. a bare httptest 127.0.0.1).
+func accountFromHost(host string) string {
+ host, _, _ = strings.Cut(host, ":")
+ for _, infix := range []string{".openai.", ".services.ai.", ".cognitiveservices."} {
+ if i := strings.Index(host, infix); i > 0 {
+ return host[:i]
+ }
+ }
+
+ return "default"
+}
+
+// endpointFromHost extracts the AML online-endpoint name from an inference host
+// such as {endpoint}.{region}.inference.ml.azure.com (region optional), taking
+// the leading DNS label. Falls back to accountFromHost for other hosts.
+func endpointFromHost(host string) string {
+ host, _, _ = strings.Cut(host, ":")
+ if i := strings.Index(host, ".inference.ml.azure.com"); i > 0 {
+ label, _, _ := strings.Cut(host[:i], ".")
+
+ return label
+ }
+
+ return accountFromHost(host)
+}
+
+func (h *DataPlaneHandler) serveInference(w http.ResponseWriter, r *http.Request, account string, parts []string) {
+ // /openai/deployments/{deployment}/{action}
+ const minSegs = 3
+ if len(parts) < minSegs || r.Method != http.MethodPost {
+ dpErr(w, http.StatusNotFound, "unsupported inference path")
+
+ return
+ }
+
+ deployment, action := parts[1], parts[2]
+
+ switch action {
+ case "chat": // .../chat/completions
+ h.chatCompletions(w, r, account, deployment)
+ case "completions":
+ h.completions(w, r, account, deployment)
+ case "embeddings":
+ h.embeddings(w, r, account, deployment)
+ default:
+ dpErr(w, http.StatusNotFound, "unknown inference action: "+action)
+ }
+}
+
+func (h *DataPlaneHandler) chatCompletions(w http.ResponseWriter, r *http.Request, account, deployment string) {
+ var body struct {
+ Messages []struct {
+ Role string `json:"role"`
+ Content string `json:"content"`
+ } `json:"messages"`
+ Temperature *float64 `json:"temperature"`
+ MaxTokens *int `json:"max_tokens"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ req := csdriver.ChatCompletionRequest{Temperature: body.Temperature, MaxTokens: body.MaxTokens}
+ for _, msg := range body.Messages {
+ req.Messages = append(req.Messages, csdriver.ChatMessage{Role: msg.Role, Content: msg.Content})
+ }
+
+ resp, err := h.dp.ChatCompletions(r.Context(), account, deployment, req)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ choices := make([]map[string]any, 0, len(resp.Choices))
+ for _, c := range resp.Choices {
+ choices = append(choices, map[string]any{
+ "index": c.Index,
+ "message": map[string]any{"role": c.Message.Role, "content": c.Message.Content},
+ "finish_reason": c.FinishReason,
+ })
+ }
+
+ dpJSON(w, map[string]any{
+ "id": resp.ID, "object": "chat.completion", "model": resp.Model, "created": resp.Created,
+ "choices": choices, "usage": usageJSON(resp.Usage),
+ })
+}
+
+func (h *DataPlaneHandler) completions(w http.ResponseWriter, r *http.Request, account, deployment string) {
+ var body struct {
+ Prompt string `json:"prompt"`
+ MaxTokens *int `json:"max_tokens"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ resp, err := h.dp.Completions(r.Context(), account, deployment,
+ csdriver.CompletionsRequest{Prompt: body.Prompt, MaxTokens: body.MaxTokens})
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ choices := make([]map[string]any, 0, len(resp.Choices))
+ for _, c := range resp.Choices {
+ choices = append(choices, map[string]any{"text": c.Text, "index": c.Index, "finish_reason": c.FinishReason})
+ }
+
+ dpJSON(w, map[string]any{
+ "id": resp.ID, "object": "text_completion", "model": resp.Model, "created": resp.Created,
+ "choices": choices, "usage": usageJSON(resp.Usage),
+ })
+}
+
+func (h *DataPlaneHandler) embeddings(w http.ResponseWriter, r *http.Request, account, deployment string) {
+ var body struct {
+ Input json.RawMessage `json:"input"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ resp, err := h.dp.Embeddings(r.Context(), account, deployment,
+ csdriver.EmbeddingsRequest{Input: parseEmbeddingInput(body.Input)})
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ data := make([]map[string]any, 0, len(resp.Data))
+ for _, d := range resp.Data {
+ data = append(data, map[string]any{"object": "embedding", "index": d.Index, "embedding": d.Embedding})
+ }
+
+ dpJSON(w, map[string]any{
+ "object": "list", "model": resp.Model, "data": data, "usage": usageJSON(resp.Usage),
+ })
+}
+
+// parseEmbeddingInput accepts any of the shapes the embeddings API allows: a
+// string, an array of strings, a single token array ([]int), or an array of
+// token arrays ([][]int). Token arrays are rendered to a synthetic string so
+// each input still yields exactly one embedding row.
+func parseEmbeddingInput(raw json.RawMessage) []string {
+ var single string
+ if json.Unmarshal(raw, &single) == nil {
+ return []string{single}
+ }
+
+ var many []string
+ if json.Unmarshal(raw, &many) == nil {
+ return many
+ }
+
+ var tokens []int
+ if json.Unmarshal(raw, &tokens) == nil {
+ return []string{tokenKey(tokens)}
+ }
+
+ var tokenLists [][]int
+ if json.Unmarshal(raw, &tokenLists) == nil {
+ out := make([]string, 0, len(tokenLists))
+ for _, t := range tokenLists {
+ out = append(out, tokenKey(t))
+ }
+
+ return out
+ }
+
+ return nil
+}
+
+// tokenKey renders a token-ID array to a stable string so it can be embedded.
+func tokenKey(tokens []int) string {
+ parts := make([]string, len(tokens))
+ for i, n := range tokens {
+ parts[i] = strconv.Itoa(n)
+ }
+
+ return strings.Join(parts, " ")
+}
+
+func usageJSON(u csdriver.TokenUsage) map[string]any {
+ return map[string]any{
+ "prompt_tokens": u.PromptTokens, "completion_tokens": u.CompletionTokens, "total_tokens": u.TotalTokens,
+ }
+}
+
+func (h *DataPlaneHandler) score(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ dpErr(w, http.StatusMethodNotAllowed, "method not allowed")
+
+ return
+ }
+
+ body, err := io.ReadAll(io.LimitReader(r.Body, maxDPBody))
+ if err != nil {
+ dpErr(w, http.StatusBadRequest, "failed to read body")
+
+ return
+ }
+
+ out, cerr := h.dp.ScoreOnlineEndpoint(r.Context(), endpointFromHost(r.Host), body)
+ if cerr != nil {
+ dpCErr(w, cerr)
+
+ return
+ }
+
+ // Re-encode through json (breaks request->response taint; keeps valid JSON).
+ var payload any
+ if json.Unmarshal(out, &payload) != nil {
+ payload = map[string]any{"raw": string(out)}
+ }
+
+ dpJSON(w, payload)
+}
+
+// --- wire helpers ---
+
+func dpDecode(w http.ResponseWriter, r *http.Request, v any) bool {
+ body, err := io.ReadAll(io.LimitReader(r.Body, maxDPBody))
+ if err != nil {
+ dpErr(w, http.StatusBadRequest, "failed to read body")
+
+ return false
+ }
+
+ if err := json.Unmarshal(body, v); err != nil {
+ dpErr(w, http.StatusBadRequest, "invalid JSON body")
+
+ return false
+ }
+
+ return true
+}
+
+func dpJSON(w http.ResponseWriter, v any) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _ = json.NewEncoder(w).Encode(v)
+}
+
+func dpErr(w http.ResponseWriter, status int, msg string) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ // Azure OpenAI / azcore.ResponseError treat error.code as a string identifier.
+ _ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": msg, "code": codeForStatus(status)}})
+}
+
+// codeForStatus maps an HTTP status to the string error-code identifier Azure
+// data-plane clients expect in error.code.
+func codeForStatus(status int) string {
+ switch status {
+ case http.StatusBadRequest:
+ return "BadRequest"
+ case http.StatusNotFound:
+ return "NotFound"
+ case http.StatusMethodNotAllowed:
+ return "MethodNotAllowed"
+ case http.StatusConflict:
+ return "Conflict"
+ default:
+ return "InternalServerError"
+ }
+}
+
+// dpCErr maps a typed cloud error to the OpenAI-style error envelope.
+func dpCErr(w http.ResponseWriter, err error) {
+ switch {
+ case errors.IsNotFound(err):
+ dpErr(w, http.StatusNotFound, err.Error())
+ case errors.IsInvalidArgument(err):
+ dpErr(w, http.StatusBadRequest, err.Error())
+ case errors.IsAlreadyExists(err), errors.IsFailedPrecondition(err):
+ dpErr(w, http.StatusConflict, err.Error())
+ default:
+ dpErr(w, http.StatusInternalServerError, err.Error())
+ }
+}
diff --git a/server/azure/azureai/dataplane_roundtrip_test.go b/server/azure/azureai/dataplane_roundtrip_test.go
new file mode 100644
index 0000000..fd2343d
--- /dev/null
+++ b/server/azure/azureai/dataplane_roundtrip_test.go
@@ -0,0 +1,192 @@
+package azureai_test
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
+)
+
+func newDPServer(t *testing.T) string {
+ t.Helper()
+
+ cloud := cloudemu.NewAzure()
+ srv := azureserver.New(azureserver.Drivers{AzureAIDataPlane: cloud.AzureAI})
+ ts := httptest.NewServer(srv)
+ t.Cleanup(ts.Close)
+
+ return ts.URL
+}
+
+func TestChatCompletions(t *testing.T) {
+ url := newDPServer(t)
+
+ resp := do(t, http.MethodPost, url+"/openai/deployments/gpt4o/chat/completions", map[string]any{
+ "messages": []any{
+ map[string]any{"role": "system", "content": "be brief"},
+ map[string]any{"role": "user", "content": "hello there world"},
+ },
+ })
+
+ assert.Equal(t, "chat.completion", resp["object"])
+ choices := resp["choices"].([]any)
+ require.Len(t, choices, 1)
+ msg := choices[0].(map[string]any)["message"].(map[string]any)
+ assert.Equal(t, "assistant", msg["role"])
+ assert.Contains(t, msg["content"], "hello there world")
+
+ usage := resp["usage"].(map[string]any)
+ assert.Greater(t, usage["total_tokens"], float64(0))
+}
+
+func TestEmbeddings(t *testing.T) {
+ url := newDPServer(t)
+
+ resp := do(t, http.MethodPost, url+"/openai/deployments/embed/embeddings", map[string]any{
+ "input": []any{"alpha", "beta"},
+ })
+
+ data := resp["data"].([]any)
+ require.Len(t, data, 2)
+ vec := data[0].(map[string]any)["embedding"].([]any)
+ assert.NotEmpty(t, vec)
+}
+
+func TestCompletions(t *testing.T) {
+ url := newDPServer(t)
+
+ resp := do(t, http.MethodPost, url+"/openai/deployments/gpt35/completions", map[string]any{
+ "prompt": "say hi",
+ })
+
+ choices := resp["choices"].([]any)
+ require.Len(t, choices, 1)
+ assert.Contains(t, choices[0].(map[string]any)["text"], "say hi")
+}
+
+func TestAssistantsThreadsRuns(t *testing.T) {
+ url := newDPServer(t)
+
+ asst := do(t, http.MethodPost, url+"/openai/assistants", map[string]any{
+ "model": "gpt4o", "name": "helper", "instructions": "help",
+ })
+ asstID := asst["id"].(string)
+ require.NotEmpty(t, asstID)
+
+ list := do(t, http.MethodGet, url+"/openai/assistants", nil)
+ assert.Len(t, list["data"], 1)
+
+ thread := do(t, http.MethodPost, url+"/openai/threads", map[string]any{})
+ threadID := thread["id"].(string)
+ require.NotEmpty(t, threadID)
+
+ msg := do(t, http.MethodPost, url+"/openai/threads/"+threadID+"/messages", map[string]any{
+ "role": "user", "content": "question?",
+ })
+ assert.Equal(t, "question?", msg["content"])
+
+ msgs := do(t, http.MethodGet, url+"/openai/threads/"+threadID+"/messages", nil)
+ assert.Len(t, msgs["data"], 1)
+
+ run := do(t, http.MethodPost, url+"/openai/threads/"+threadID+"/runs", map[string]any{
+ "assistant_id": asstID,
+ })
+ runID := run["id"].(string)
+ assert.Equal(t, "completed", run["status"])
+
+ gotRun := do(t, http.MethodGet, url+"/openai/threads/"+threadID+"/runs/"+runID, nil)
+ assert.Equal(t, runID, gotRun["id"])
+
+ // Cleanup paths.
+ do(t, http.MethodDelete, url+"/openai/assistants/"+asstID, nil)
+ do(t, http.MethodDelete, url+"/openai/threads/"+threadID, nil)
+}
+
+func TestScore(t *testing.T) {
+ url := newDPServer(t)
+
+ resp := do(t, http.MethodPost, url+"/score", map[string]any{
+ "input_data": map[string]any{"data": []any{[]any{1, 2, 3}}},
+ })
+ require.NotNil(t, resp["input_data"])
+}
+
+func TestListMessagesOrdered(t *testing.T) {
+ url := newDPServer(t)
+
+ thread := do(t, http.MethodPost, url+"/openai/threads", map[string]any{})
+ threadID := thread["id"].(string)
+
+ want := []string{"m0", "m1", "m2", "m3", "m4"}
+ for _, c := range want {
+ do(t, http.MethodPost, url+"/openai/threads/"+threadID+"/messages", map[string]any{"role": "user", "content": c})
+ }
+
+ // Repeated listings must return a stable creation-ordered sequence.
+ for range 3 {
+ msgs := do(t, http.MethodGet, url+"/openai/threads/"+threadID+"/messages", nil)
+ data := msgs["data"].([]any)
+ require.Len(t, data, len(want))
+
+ got := make([]string, len(data))
+ for i, d := range data {
+ got[i] = d.(map[string]any)["content"].(string)
+ }
+
+ assert.Equal(t, want, got)
+ }
+}
+
+// --- real azopenai SDK roundtrip ---
+
+func newAOAIClient(t *testing.T) *azopenai.Client {
+ t.Helper()
+
+ cloudP := cloudemu.NewAzure()
+ srv := azureserver.New(azureserver.Drivers{AzureAIDataPlane: cloudP.AzureAI})
+ ts := httptest.NewTLSServer(srv)
+ t.Cleanup(ts.Close)
+
+ c, err := azopenai.NewClient(ts.URL, fakeCred{}, &azopenai.ClientOptions{
+ ClientOptions: azcore.ClientOptions{Transport: ts.Client(), Retry: policy.RetryOptions{MaxRetries: -1}},
+ })
+ require.NoError(t, err)
+
+ return c
+}
+
+func TestSDKAzureOpenAIInference(t *testing.T) {
+ c := newAOAIClient(t)
+ ctx := context.Background()
+
+ chat, err := c.GetChatCompletions(ctx, azopenai.ChatCompletionsOptions{
+ DeploymentName: to.Ptr("gpt4o"),
+ Messages: []azopenai.ChatRequestMessageClassification{
+ &azopenai.ChatRequestUserMessage{Content: azopenai.NewChatRequestUserMessageContent("hello there world")},
+ },
+ MaxTokens: to.Ptr(int32(16)),
+ }, nil)
+ require.NoError(t, err)
+ require.NotEmpty(t, chat.Choices)
+ require.NotNil(t, chat.Choices[0].Message)
+ require.NotNil(t, chat.Choices[0].Message.Content)
+ assert.NotEmpty(t, *chat.Choices[0].Message.Content)
+
+ emb, err := c.GetEmbeddings(ctx, azopenai.EmbeddingsOptions{
+ DeploymentName: to.Ptr("text-embedding-3-large"),
+ Input: []string{"alpha", "beta"},
+ }, nil)
+ require.NoError(t, err)
+ require.Len(t, emb.Data, 2)
+ assert.NotEmpty(t, emb.Data[0].Embedding)
+}
diff --git a/server/azure/azureai/handler.go b/server/azure/azureai/handler.go
new file mode 100644
index 0000000..8a3d2f6
--- /dev/null
+++ b/server/azure/azureai/handler.go
@@ -0,0 +1,118 @@
+// Package azureai implements the Azure AI ARM REST APIs as server.Handlers.
+// This file serves Microsoft.CognitiveServices (AI Foundry / AI Studio / the AI
+// Services resource / Azure OpenAI). Real
+// github.com/Azure/azure-sdk-for-go/.../armcognitiveservices clients configured
+// with a custom endpoint hit this handler the same way they hit
+// management.azure.com.
+//
+// Coverage (Microsoft.CognitiveServices):
+//
+// accounts CRUD, list by RG / subscription
+// accounts/{a}:listKeys|regenerateKey POST key management
+// accounts/{a}/models|skus|usages GET account catalogs/quota
+// accounts/{a}/deployments CRUD, list (model deployments)
+// accounts/{a}/projects CRUD, list (AI Foundry projects)
+// accounts/{a}/raiPolicies CRUD, list (content filters)
+// accounts/{a}/commitmentPlans CRUD, list
+// accounts/{a}/privateEndpointConnections CRUD, list
+//
+// ARM PUT returns the resource inline with a terminal provisioningState so the
+// SDK LRO poller terminates on the first response.
+package azureai
+
+import (
+ "net/http"
+
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ csdriver "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+const csProvider = "Microsoft.CognitiveServices"
+
+// Account sub-resource collection names.
+const (
+ collDeployments = "deployments"
+ collProjects = "projects"
+ collRaiPolicies = "raiPolicies"
+ collCommitmentPlans = "commitmentPlans"
+ collPEC = "privateEndpointConnections"
+)
+
+// CognitiveServicesHandler serves Microsoft.CognitiveServices ARM requests.
+type CognitiveServicesHandler struct {
+ svc csdriver.CognitiveServices
+}
+
+// NewCognitiveServices returns a handler backed by svc.
+func NewCognitiveServices(svc csdriver.CognitiveServices) *CognitiveServicesHandler {
+ return &CognitiveServicesHandler{svc: svc}
+}
+
+// Matches returns true for ARM Microsoft.CognitiveServices/accounts paths.
+func (*CognitiveServicesHandler) Matches(r *http.Request) bool {
+ rp, ok := azurearm.ParsePath(r.URL.Path)
+ if !ok {
+ return false
+ }
+
+ return rp.Provider == csProvider && rp.ResourceType == "accounts"
+}
+
+// childCollections are the account sub-resource collections (as opposed to
+// account-level action verbs).
+//
+//nolint:gochecknoglobals // immutable routing set
+var childCollections = map[string]bool{
+ collDeployments: true, collProjects: true, collRaiPolicies: true,
+ collCommitmentPlans: true, collPEC: true,
+}
+
+// ServeHTTP routes the request by path shape and method.
+func (h *CognitiveServicesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ rp, ok := azurearm.ParsePath(r.URL.Path)
+ if !ok {
+ azurearm.WriteError(w, http.StatusBadRequest, "InvalidPath", "malformed ARM path")
+
+ return
+ }
+
+ switch {
+ case rp.ResourceName == "":
+ h.serveAccountCollection(w, r, &rp)
+ case rp.SubResource == "":
+ h.serveAccount(w, r, &rp)
+ case childCollections[rp.SubResource]:
+ h.serveChild(w, r, &rp)
+ default:
+ h.serveAccountAction(w, r, &rp)
+ }
+}
+
+func (h *CognitiveServicesHandler) serveAccountCollection(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ if r.Method != http.MethodGet {
+ writeMethodNotAllowed(w)
+
+ return
+ }
+
+ h.listAccounts(w, r, rp)
+}
+
+func (h *CognitiveServicesHandler) serveAccount(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ switch r.Method {
+ case http.MethodPut:
+ h.createOrUpdateAccount(w, r, rp)
+ case http.MethodGet:
+ h.getAccount(w, r, rp)
+ case http.MethodPatch:
+ h.patchAccount(w, r, rp)
+ case http.MethodDelete:
+ h.deleteAccount(w, r, rp)
+ default:
+ writeMethodNotAllowed(w)
+ }
+}
+
+func writeMethodNotAllowed(w http.ResponseWriter) {
+ azurearm.WriteError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+}
diff --git a/server/azure/azureai/machinelearning.go b/server/azure/azureai/machinelearning.go
new file mode 100644
index 0000000..2d1b8ed
--- /dev/null
+++ b/server/azure/azureai/machinelearning.go
@@ -0,0 +1,356 @@
+// This file serves Microsoft.MachineLearningServices (Azure ML) ARM requests.
+//
+// Coverage (Microsoft.MachineLearningServices):
+//
+// workspaces CRUD, list by RG / subscription
+// workspaces/{w}/computes[/{c}[/start|stop|restart]]
+// workspaces/{w}/{online,batch}Endpoints[/{e}[/deployments[/{d}]]]
+// workspaces/{w}/jobs[/{j}[/cancel]]
+// workspaces/{w}/{models,data,environments,components,featuresets}[/{n}[/versions[/{v}]]]
+// workspaces/{w}/datastores[/{d}]
+// workspaces/{w}/connections[/{c}]
+// workspaces/{w}/schedules[/{s}]
+// registries[/{r}]
+//
+// AML paths nest deeper than the shared azurearm.ParsePath captures, so this
+// handler parses the trailing segments itself.
+package azureai
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ mldriver "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+const mlProvider = "Microsoft.MachineLearningServices"
+
+// Workspace-child path-segment lengths (p.rest), e.g.
+// workspaces/{w}/{coll}/{name}/{sub}/{subName}.
+const (
+ mlLenWorkspace = 2 // workspaces/{w}
+ mlLenCollection = 3 // .../{coll}
+ mlLenChild = 4 // .../{coll}/{name}
+ mlLenSub = 5 // .../{coll}/{name}/{sub}
+)
+
+// Endpoint collection names and the batch-kind label.
+const (
+ collOnlineEndpoints = "onlineEndpoints"
+ collBatchEndpoints = "batchEndpoints"
+ kindBatch = "batch"
+)
+
+// assetTypes are the workspace versioned-asset collections.
+//
+//nolint:gochecknoglobals // immutable routing set
+var assetTypes = map[string]bool{
+ "models": true, "data": true, "environments": true, "components": true, "featuresets": true,
+}
+
+// MachineLearningHandler serves Microsoft.MachineLearningServices ARM requests.
+type MachineLearningHandler struct {
+ svc mldriver.MachineLearning
+}
+
+// NewMachineLearning returns a handler backed by svc.
+func NewMachineLearning(svc mldriver.MachineLearning) *MachineLearningHandler {
+ return &MachineLearningHandler{svc: svc}
+}
+
+// mlPath is a parsed Azure ML ARM path.
+type mlPath struct {
+ subscription string
+ resourceGroup string
+ rest []string // segments after the provider name
+}
+
+// parseMLPath splits an ARM URL, returning the subscription, optional resource
+// group, and the resource segments after Microsoft.MachineLearningServices.
+func parseMLPath(urlPath string) (mlPath, bool) {
+ parts := strings.Split(strings.Trim(urlPath, "/"), "/")
+ if len(parts) < 2 || parts[0] != "subscriptions" {
+ return mlPath{}, false
+ }
+
+ p := mlPath{subscription: parts[1]}
+ i := 2
+
+ if i+1 < len(parts) && parts[i] == "resourceGroups" {
+ p.resourceGroup = parts[i+1]
+ i += 2
+ }
+
+ if i+1 >= len(parts) || parts[i] != "providers" || parts[i+1] != mlProvider {
+ return mlPath{}, false
+ }
+
+ p.rest = parts[i+2:]
+
+ return p, true
+}
+
+// Matches claims Microsoft.MachineLearningServices ARM paths.
+func (*MachineLearningHandler) Matches(r *http.Request) bool {
+ p, ok := parseMLPath(r.URL.Path)
+
+ return ok && len(p.rest) >= 1
+}
+
+// ServeHTTP routes by the top-level resource type.
+func (h *MachineLearningHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ p, ok := parseMLPath(r.URL.Path)
+ if !ok || len(p.rest) == 0 {
+ azurearm.WriteError(w, http.StatusBadRequest, "InvalidPath", "malformed ARM path")
+
+ return
+ }
+
+ switch p.rest[0] {
+ case "workspaces":
+ h.serveWorkspaces(w, r, &p)
+ case "registries":
+ h.serveRegistries(w, r, &p)
+ default:
+ azurearm.WriteError(w, http.StatusNotFound, "NotFound", "unsupported resource: "+p.rest[0])
+ }
+}
+
+func writeMLMethodNotAllowed(w http.ResponseWriter) {
+ azurearm.WriteError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+}
+
+// --- Workspaces ---
+
+func mlWorkspaceJSON(w *mldriver.MLWorkspace) map[string]any {
+ return map[string]any{
+ "id": w.ID, "name": w.Name, "type": mlProvider + "/workspaces",
+ "location": w.Location, "kind": w.Kind, "tags": w.Tags,
+ "properties": map[string]any{
+ "friendlyName": w.FriendlyName, "description": w.Description,
+ "discoveryUrl": w.DiscoveryURL, "provisioningState": w.ProvisioningState,
+ },
+ }
+}
+
+func (h *MachineLearningHandler) serveWorkspaces(w http.ResponseWriter, r *http.Request, p *mlPath) {
+ // Collection: list by RG or subscription.
+ if len(p.rest) == 1 {
+ if r.Method != http.MethodGet {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ h.listWorkspaces(w, r, p)
+
+ return
+ }
+
+ wsName := p.rest[1]
+
+ // Nested child resources.
+ if len(p.rest) > mlLenWorkspace {
+ h.serveWorkspaceChild(w, r, p, wsName)
+
+ return
+ }
+
+ switch r.Method {
+ case http.MethodPut:
+ h.createWorkspace(w, r, p, wsName)
+ case http.MethodGet:
+ h.getWorkspace(w, r, p, wsName)
+ case http.MethodPatch:
+ h.patchWorkspace(w, r, p, wsName)
+ case http.MethodDelete:
+ h.deleteWorkspace(w, r, p, wsName)
+ default:
+ writeMLMethodNotAllowed(w)
+ }
+}
+
+func (h *MachineLearningHandler) createWorkspace(w http.ResponseWriter, r *http.Request, p *mlPath, name string) {
+ var body struct {
+ Location string `json:"location"`
+ Kind string `json:"kind"`
+ Tags map[string]string `json:"tags"`
+ Properties struct {
+ FriendlyName string `json:"friendlyName"`
+ Description string `json:"description"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ ws, err := h.svc.CreateMLWorkspace(r.Context(), mldriver.MLWorkspaceConfig{
+ Name: name, ResourceGroup: p.resourceGroup, Location: body.Location, Kind: body.Kind,
+ FriendlyName: body.Properties.FriendlyName, Description: body.Properties.Description, Tags: body.Tags,
+ })
+ writeML(w, mlWorkspaceJSON, ws, err)
+}
+
+func (h *MachineLearningHandler) getWorkspace(w http.ResponseWriter, r *http.Request, p *mlPath, name string) {
+ ws, err := h.svc.GetMLWorkspace(r.Context(), p.resourceGroup, name)
+ writeML(w, mlWorkspaceJSON, ws, err)
+}
+
+func (h *MachineLearningHandler) patchWorkspace(w http.ResponseWriter, r *http.Request, p *mlPath, name string) {
+ var body struct {
+ Tags map[string]string `json:"tags"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ ws, err := h.svc.UpdateMLWorkspaceTags(r.Context(), p.resourceGroup, name, body.Tags)
+ writeML(w, mlWorkspaceJSON, ws, err)
+}
+
+func (h *MachineLearningHandler) deleteWorkspace(w http.ResponseWriter, r *http.Request, p *mlPath, name string) {
+ if err := h.svc.DeleteMLWorkspace(r.Context(), p.resourceGroup, name); err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{})
+}
+
+//nolint:dupl // list-then-map shape mirrors the accounts handler.
+func (h *MachineLearningHandler) listWorkspaces(w http.ResponseWriter, r *http.Request, p *mlPath) {
+ var (
+ wss []mldriver.MLWorkspace
+ err error
+ )
+
+ if p.resourceGroup == "" {
+ wss, err = h.svc.ListMLWorkspaces(r.Context())
+ } else {
+ wss, err = h.svc.ListMLWorkspacesByResourceGroup(r.Context(), p.resourceGroup)
+ }
+
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(wss))
+ for i := range wss {
+ out = append(out, mlWorkspaceJSON(&wss[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
+
+// serveWorkspaceChild dispatches the nested workspace collections.
+func (h *MachineLearningHandler) serveWorkspaceChild(w http.ResponseWriter, r *http.Request, p *mlPath, ws string) {
+ coll := p.rest[2]
+
+ switch {
+ case coll == "computes":
+ h.serveComputes(w, r, p, ws)
+ case coll == collOnlineEndpoints || coll == collBatchEndpoints:
+ h.serveEndpoints(w, r, p, ws, coll)
+ case coll == "jobs":
+ h.serveJobs(w, r, p, ws)
+ case coll == "datastores":
+ h.serveDatastores(w, r, p, ws)
+ case coll == "connections":
+ h.serveConnections(w, r, p, ws)
+ case coll == "schedules":
+ h.serveSchedules(w, r, p, ws)
+ case assetTypes[coll]:
+ h.serveAssets(w, r, p, ws, coll)
+ default:
+ azurearm.WriteError(w, http.StatusNotFound, "NotFound", "unsupported workspace child: "+coll)
+ }
+}
+
+// --- Registries ---
+
+func registryJSON(r *mldriver.Registry) map[string]any {
+ return map[string]any{
+ "id": r.ID, "name": r.Name, "type": mlProvider + "/registries",
+ "location": r.Location, "tags": r.Tags,
+ "properties": map[string]any{"description": r.Description, "provisioningState": r.ProvisioningState},
+ }
+}
+
+func (h *MachineLearningHandler) serveRegistries(w http.ResponseWriter, r *http.Request, p *mlPath) {
+ if len(p.rest) == 1 {
+ if r.Method != http.MethodGet {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ regs, err := h.svc.ListRegistries(r.Context(), p.resourceGroup)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(regs))
+ for i := range regs {
+ out = append(out, registryJSON(®s[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+
+ return
+ }
+
+ name := p.rest[1]
+
+ switch r.Method {
+ case http.MethodPut:
+ var body struct {
+ Location string `json:"location"`
+ Tags map[string]string `json:"tags"`
+ Properties struct {
+ Description string `json:"description"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ reg, err := h.svc.CreateRegistry(r.Context(), mldriver.RegistryConfig{
+ Name: name, ResourceGroup: p.resourceGroup, Location: body.Location,
+ Description: body.Properties.Description, Tags: body.Tags,
+ })
+ writeML(w, registryJSON, reg, err)
+ case http.MethodGet:
+ reg, err := h.svc.GetRegistry(r.Context(), p.resourceGroup, name)
+ writeML(w, registryJSON, reg, err)
+ case http.MethodDelete:
+ if err := h.svc.DeleteRegistry(r.Context(), p.resourceGroup, name); err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{})
+ default:
+ writeMLMethodNotAllowed(w)
+ }
+}
+
+// writeML writes a created/fetched resource or maps the error.
+func writeML[T any](w http.ResponseWriter, toJSON func(*T) map[string]any, res *T, err error) {
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, toJSON(res))
+}
diff --git a/server/azure/azureai/machinelearning_children.go b/server/azure/azureai/machinelearning_children.go
new file mode 100644
index 0000000..36ad0d4
--- /dev/null
+++ b/server/azure/azureai/machinelearning_children.go
@@ -0,0 +1,490 @@
+package azureai
+
+import (
+ "net/http"
+
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ mldriver "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+func endpointKind(collection string) string {
+ if collection == collBatchEndpoints {
+ return kindBatch
+ }
+
+ return "online"
+}
+
+// --- Computes ---
+
+func computeJSON(c *mldriver.Compute) map[string]any {
+ return map[string]any{
+ "id": c.ID, "name": c.Name, "type": mlProvider + "/workspaces/computes",
+ "properties": map[string]any{
+ "computeType": c.ComputeType, "provisioningState": c.ProvisioningState,
+ "properties": map[string]any{
+ "vmSize": c.VMSize, "state": c.State,
+ "scaleSettings": map[string]any{"minNodeCount": c.MinNodes, "maxNodeCount": c.MaxNodes},
+ },
+ },
+ }
+}
+
+func (h *MachineLearningHandler) serveComputes(w http.ResponseWriter, r *http.Request, p *mlPath, ws string) {
+ if len(p.rest) == mlLenCollection {
+ h.listComputesOrMethod(w, r, p, ws)
+
+ return
+ }
+
+ name := p.rest[3]
+
+ // .../computes/{c}/{action}
+ if len(p.rest) > mlLenChild {
+ h.computeAction(w, r, p, ws, name, p.rest[4])
+
+ return
+ }
+
+ switch r.Method {
+ case http.MethodPut:
+ var body struct {
+ Properties struct {
+ ComputeType string `json:"computeType"`
+ Properties struct {
+ VMSize string `json:"vmSize"`
+ ScaleSettings struct {
+ MinNodeCount int `json:"minNodeCount"`
+ MaxNodeCount int `json:"maxNodeCount"`
+ } `json:"scaleSettings"`
+ } `json:"properties"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ c, err := h.svc.CreateCompute(r.Context(), mldriver.ComputeConfig{
+ Workspace: ws, ResourceGroup: p.resourceGroup, Name: name,
+ ComputeType: body.Properties.ComputeType, VMSize: body.Properties.Properties.VMSize,
+ MinNodes: body.Properties.Properties.ScaleSettings.MinNodeCount,
+ MaxNodes: body.Properties.Properties.ScaleSettings.MaxNodeCount,
+ })
+ writeML(w, computeJSON, c, err)
+ case http.MethodGet:
+ c, err := h.svc.GetCompute(r.Context(), p.resourceGroup, ws, name)
+ writeML(w, computeJSON, c, err)
+ case http.MethodDelete:
+ writeMLDelete(w, h.svc.DeleteCompute(r.Context(), p.resourceGroup, ws, name))
+ default:
+ writeMLMethodNotAllowed(w)
+ }
+}
+
+func (h *MachineLearningHandler) listComputesOrMethod(w http.ResponseWriter, r *http.Request, p *mlPath, ws string) {
+ if r.Method != http.MethodGet {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ cs, err := h.svc.ListComputes(r.Context(), p.resourceGroup, ws)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(cs))
+ for i := range cs {
+ out = append(out, computeJSON(&cs[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
+
+func (h *MachineLearningHandler) computeAction(w http.ResponseWriter, r *http.Request, p *mlPath, ws, name, action string) {
+ if r.Method != http.MethodPost {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ var err error
+
+ switch action {
+ case "start":
+ err = h.svc.StartCompute(r.Context(), p.resourceGroup, ws, name)
+ case "stop":
+ err = h.svc.StopCompute(r.Context(), p.resourceGroup, ws, name)
+ case "restart":
+ err = h.svc.RestartCompute(r.Context(), p.resourceGroup, ws, name)
+ default:
+ azurearm.WriteError(w, http.StatusNotFound, "NotFound", "unknown compute action: "+action)
+
+ return
+ }
+
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{})
+}
+
+// --- Endpoints + deployments ---
+
+func endpointJSON(e *mldriver.Endpoint) map[string]any {
+ return map[string]any{
+ "id": e.ID, "name": e.Name, "type": mlProvider + "/workspaces/" + endpointCollection(e.Kind),
+ "properties": map[string]any{
+ "authMode": e.AuthMode, "description": e.Description,
+ "scoringUri": e.ScoringURI, "provisioningState": e.ProvisioningState, "traffic": e.Traffic,
+ },
+ }
+}
+
+func endpointCollection(kind string) string {
+ if kind == kindBatch {
+ return collBatchEndpoints
+ }
+
+ return collOnlineEndpoints
+}
+
+func endpointDeploymentJSON(d *mldriver.EndpointDeployment) map[string]any {
+ return map[string]any{
+ "id": d.ID, "name": d.Name,
+ "properties": map[string]any{
+ "model": d.Model, "instanceType": d.InstanceType,
+ "provisioningState": d.ProvisioningState,
+ },
+ "sku": map[string]any{"capacity": d.InstanceCount},
+ }
+}
+
+func (h *MachineLearningHandler) serveEndpoints(w http.ResponseWriter, r *http.Request, p *mlPath, ws, coll string) {
+ kind := endpointKind(coll)
+
+ if len(p.rest) == mlLenCollection {
+ h.listEndpoints(w, r, p, ws, kind)
+
+ return
+ }
+
+ ep := p.rest[3]
+
+ // Nested deployments: .../{online,batch}Endpoints/{e}/deployments[/{d}]
+ if len(p.rest) > mlLenChild && p.rest[4] == "deployments" {
+ h.serveEndpointDeployments(w, r, p, ws, kind, ep)
+
+ return
+ }
+
+ switch r.Method {
+ case http.MethodPut:
+ var body struct {
+ Properties struct {
+ AuthMode string `json:"authMode"`
+ Description string `json:"description"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ e, err := h.svc.CreateEndpoint(r.Context(), mldriver.EndpointConfig{
+ Workspace: ws, ResourceGroup: p.resourceGroup, Name: ep, Kind: kind,
+ AuthMode: body.Properties.AuthMode, Description: body.Properties.Description,
+ })
+ writeML(w, endpointJSON, e, err)
+ case http.MethodGet:
+ e, err := h.svc.GetEndpoint(r.Context(), p.resourceGroup, ws, kind, ep)
+ writeML(w, endpointJSON, e, err)
+ case http.MethodDelete:
+ writeMLDelete(w, h.svc.DeleteEndpoint(r.Context(), p.resourceGroup, ws, kind, ep))
+ default:
+ writeMLMethodNotAllowed(w)
+ }
+}
+
+func (h *MachineLearningHandler) listEndpoints(w http.ResponseWriter, r *http.Request, p *mlPath, ws, kind string) {
+ if r.Method != http.MethodGet {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ eps, err := h.svc.ListEndpoints(r.Context(), p.resourceGroup, ws, kind)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(eps))
+ for i := range eps {
+ out = append(out, endpointJSON(&eps[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
+
+func (h *MachineLearningHandler) serveEndpointDeployments(w http.ResponseWriter, r *http.Request, p *mlPath, ws, kind, ep string) {
+ // .../deployments[/{d}]
+ if len(p.rest) == mlLenSub {
+ if r.Method != http.MethodGet {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ ds, err := h.svc.ListEndpointDeployments(r.Context(), p.resourceGroup, ws, kind, ep)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(ds))
+ for i := range ds {
+ out = append(out, endpointDeploymentJSON(&ds[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+
+ return
+ }
+
+ name := p.rest[5]
+
+ switch r.Method {
+ case http.MethodPut:
+ var body struct {
+ Properties struct {
+ Model string `json:"model"`
+ InstanceType string `json:"instanceType"`
+ } `json:"properties"`
+ SKU struct {
+ Capacity int `json:"capacity"`
+ } `json:"sku"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ d, err := h.svc.CreateEndpointDeployment(r.Context(), mldriver.EndpointDeploymentConfig{
+ Workspace: ws, ResourceGroup: p.resourceGroup, Endpoint: ep, EndpointKind: kind, Name: name,
+ Model: body.Properties.Model, InstanceType: body.Properties.InstanceType, InstanceCount: body.SKU.Capacity,
+ })
+ writeML(w, endpointDeploymentJSON, d, err)
+ case http.MethodGet:
+ d, err := h.svc.GetEndpointDeployment(r.Context(), p.resourceGroup, ws, kind, ep, name)
+ writeML(w, endpointDeploymentJSON, d, err)
+ case http.MethodDelete:
+ writeMLDelete(w, h.svc.DeleteEndpointDeployment(r.Context(), p.resourceGroup, ws, kind, ep, name))
+ default:
+ writeMLMethodNotAllowed(w)
+ }
+}
+
+// --- Jobs ---
+
+func jobJSON(j *mldriver.Job) map[string]any {
+ return map[string]any{
+ "id": j.ID, "name": j.Name, "type": mlProvider + "/workspaces/jobs",
+ "properties": map[string]any{"jobType": j.JobType, "displayName": j.DisplayName, "status": j.Status},
+ }
+}
+
+//nolint:gocyclo // flat dispatch over list/cancel/CRUD job paths.
+func (h *MachineLearningHandler) serveJobs(w http.ResponseWriter, r *http.Request, p *mlPath, ws string) {
+ if len(p.rest) == mlLenCollection {
+ if r.Method != http.MethodGet {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ js, err := h.svc.ListJobs(r.Context(), p.resourceGroup, ws)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(js))
+ for i := range js {
+ out = append(out, jobJSON(&js[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+
+ return
+ }
+
+ name := p.rest[3]
+
+ if len(p.rest) > mlLenChild && p.rest[4] == "cancel" {
+ if r.Method != http.MethodPost {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ writeMLDelete(w, h.svc.CancelJob(r.Context(), p.resourceGroup, ws, name))
+
+ return
+ }
+
+ switch r.Method {
+ case http.MethodPut:
+ var body struct {
+ Properties struct {
+ JobType string `json:"jobType"`
+ DisplayName string `json:"displayName"`
+ ComputeID string `json:"computeId"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ j, err := h.svc.CreateJob(r.Context(), mldriver.JobConfig{
+ Workspace: ws, ResourceGroup: p.resourceGroup, Name: name,
+ JobType: body.Properties.JobType, DisplayName: body.Properties.DisplayName, ComputeID: body.Properties.ComputeID,
+ })
+ writeML(w, jobJSON, j, err)
+ case http.MethodGet:
+ j, err := h.svc.GetJob(r.Context(), p.resourceGroup, ws, name)
+ writeML(w, jobJSON, j, err)
+ default:
+ writeMLMethodNotAllowed(w)
+ }
+}
+
+// --- Versioned assets ---
+
+func assetJSON(a *mldriver.Asset) map[string]any {
+ return map[string]any{
+ "id": a.ID, "name": a.Version, "type": mlProvider + "/workspaces/" + a.AssetType + "/versions",
+ "properties": map[string]any{
+ "description": a.Description, "path": a.Path, "properties": a.Properties,
+ },
+ }
+}
+
+//nolint:gocyclo // flat dispatch over container/version path shapes.
+func (h *MachineLearningHandler) serveAssets(w http.ResponseWriter, r *http.Request, p *mlPath, ws, assetType string) {
+ // .../{assetType} -> list containers
+ // .../{assetType}/{name}/versions -> list versions
+ // .../{assetType}/{name}/versions/{version} -> version CRUD
+ if len(p.rest) == mlLenCollection {
+ h.listAssetContainers(w, r, p, ws, assetType)
+
+ return
+ }
+
+ name := p.rest[3]
+
+ if len(p.rest) < mlLenSub || p.rest[4] != "versions" {
+ azurearm.WriteError(w, http.StatusNotFound, "NotFound", "expected /versions under asset container")
+
+ return
+ }
+
+ if len(p.rest) == mlLenSub {
+ if r.Method != http.MethodGet {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ vs, err := h.svc.ListAssetVersions(r.Context(), p.resourceGroup, ws, assetType, name)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(vs))
+ for i := range vs {
+ out = append(out, assetJSON(&vs[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+
+ return
+ }
+
+ version := p.rest[5]
+
+ switch r.Method {
+ case http.MethodPut:
+ var body struct {
+ Properties struct {
+ Description string `json:"description"`
+ Path string `json:"path"`
+ Properties map[string]string `json:"properties"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ a, err := h.svc.CreateAsset(r.Context(), mldriver.AssetConfig{
+ Workspace: ws, ResourceGroup: p.resourceGroup, AssetType: assetType, Name: name, Version: version,
+ Description: body.Properties.Description, Path: body.Properties.Path, Properties: body.Properties.Properties,
+ })
+ writeML(w, assetJSON, a, err)
+ case http.MethodGet:
+ a, err := h.svc.GetAsset(r.Context(), p.resourceGroup, ws, assetType, name, version)
+ writeML(w, assetJSON, a, err)
+ case http.MethodDelete:
+ writeMLDelete(w, h.svc.DeleteAsset(r.Context(), p.resourceGroup, ws, assetType, name, version))
+ default:
+ writeMLMethodNotAllowed(w)
+ }
+}
+
+func (h *MachineLearningHandler) listAssetContainers(w http.ResponseWriter, r *http.Request, p *mlPath, ws, assetType string) {
+ if r.Method != http.MethodGet {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ cs, err := h.svc.ListAssetContainers(r.Context(), p.resourceGroup, ws, assetType)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(cs))
+ for i := range cs {
+ out = append(out, map[string]any{
+ "id": cs[i].ID, "name": cs[i].Name, "type": mlProvider + "/workspaces/" + assetType,
+ "properties": map[string]any{"latestVersion": cs[i].Version, "description": cs[i].Description},
+ })
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
+
+func writeMLDelete(w http.ResponseWriter, err error) {
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{})
+}
diff --git a/server/azure/azureai/machinelearning_roundtrip_test.go b/server/azure/azureai/machinelearning_roundtrip_test.go
new file mode 100644
index 0000000..c44c496
--- /dev/null
+++ b/server/azure/azureai/machinelearning_roundtrip_test.go
@@ -0,0 +1,228 @@
+package azureai_test
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/machinelearning/armmachinelearning/v4"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
+)
+
+func mlBase() string {
+ return "/subscriptions/" + sub + "/resourceGroups/" + rg + "/providers/Microsoft.MachineLearningServices"
+}
+
+func newMLServer(t *testing.T) string {
+ t.Helper()
+
+ cloud := cloudemu.NewAzure()
+ srv := azureserver.New(azureserver.Drivers{MachineLearning: cloud.AzureAI})
+ ts := httptest.NewServer(srv)
+ t.Cleanup(ts.Close)
+
+ return ts.URL
+}
+
+func TestMLWorkspaceLifecycle(t *testing.T) {
+ url := newMLServer(t)
+ ws := mlBase() + "/workspaces/ws1"
+
+ created := do(t, http.MethodPut, url+ws, map[string]any{
+ "location": "eastus", "kind": "Hub",
+ "properties": map[string]any{"friendlyName": "Hub WS"},
+ })
+ assert.Equal(t, "ws1", created["name"])
+ assert.Equal(t, "Hub", created["kind"])
+ assert.Equal(t, "Succeeded", props(created)["provisioningState"])
+
+ got := do(t, http.MethodGet, url+ws, nil)
+ assert.Equal(t, "Hub WS", props(got)["friendlyName"])
+
+ list := do(t, http.MethodGet, url+mlBase()+"/workspaces", nil)
+ assert.Len(t, list["value"], 1)
+}
+
+func TestMLComputeLifecycle(t *testing.T) {
+ url := newMLServer(t)
+ do(t, http.MethodPut, url+mlBase()+"/workspaces/ws1", map[string]any{"location": "eastus"})
+
+ cBase := mlBase() + "/workspaces/ws1/computes/cpu"
+ c := do(t, http.MethodPut, url+cBase, map[string]any{
+ "properties": map[string]any{"computeType": "AmlCompute", "properties": map[string]any{"vmSize": "STANDARD_DS3_V2"}},
+ })
+ assert.Equal(t, "cpu", c["name"])
+
+ // stop then start lifecycle.
+ do(t, http.MethodPost, url+cBase+"/stop", map[string]any{})
+ stopped := do(t, http.MethodGet, url+cBase, nil)
+ assert.Equal(t, "Stopped", props(props(stopped))["state"])
+ do(t, http.MethodPost, url+cBase+"/start", map[string]any{})
+
+ list := do(t, http.MethodGet, url+mlBase()+"/workspaces/ws1/computes", nil)
+ assert.Len(t, list["value"], 1)
+}
+
+func TestMLOnlineEndpointAndDeployment(t *testing.T) {
+ url := newMLServer(t)
+ do(t, http.MethodPut, url+mlBase()+"/workspaces/ws1", map[string]any{"location": "eastus"})
+
+ epBase := mlBase() + "/workspaces/ws1/onlineEndpoints/ep1"
+ ep := do(t, http.MethodPut, url+epBase, map[string]any{"properties": map[string]any{"authMode": "Key"}})
+ assert.Equal(t, "ep1", ep["name"])
+ assert.NotEmpty(t, props(ep)["scoringUri"])
+
+ dep := do(t, http.MethodPut, url+epBase+"/deployments/blue", map[string]any{
+ "properties": map[string]any{"model": "azureml:m:1", "instanceType": "Standard_DS3_v2"},
+ "sku": map[string]any{"capacity": 3},
+ })
+ assert.Equal(t, "blue", dep["name"])
+
+ deps := do(t, http.MethodGet, url+epBase+"/deployments", nil)
+ assert.Len(t, deps["value"], 1)
+
+ eps := do(t, http.MethodGet, url+mlBase()+"/workspaces/ws1/onlineEndpoints", nil)
+ assert.Len(t, eps["value"], 1)
+}
+
+func TestMLJobsAndAssets(t *testing.T) {
+ url := newMLServer(t)
+ do(t, http.MethodPut, url+mlBase()+"/workspaces/ws1", map[string]any{"location": "eastus"})
+
+ job := do(t, http.MethodPut, url+mlBase()+"/workspaces/ws1/jobs/j1", map[string]any{
+ "properties": map[string]any{"jobType": "Command", "displayName": "train"},
+ })
+ assert.Equal(t, "Completed", props(job)["status"])
+ do(t, http.MethodPost, url+mlBase()+"/workspaces/ws1/jobs/j1/cancel", map[string]any{})
+
+ // Versioned model asset.
+ mv := mlBase() + "/workspaces/ws1/models/m/versions/1"
+ asset := do(t, http.MethodPut, url+mv, map[string]any{
+ "properties": map[string]any{"description": "v1", "path": "azureml://models/m/1"},
+ })
+ assert.Equal(t, "1", asset["name"])
+
+ versions := do(t, http.MethodGet, url+mlBase()+"/workspaces/ws1/models/m/versions", nil)
+ assert.Len(t, versions["value"], 1)
+
+ containers := do(t, http.MethodGet, url+mlBase()+"/workspaces/ws1/models", nil)
+ assert.Len(t, containers["value"], 1)
+}
+
+func TestMLAssetLatestVersionNumeric(t *testing.T) {
+ url := newMLServer(t)
+ do(t, http.MethodPut, url+mlBase()+"/workspaces/ws1", map[string]any{"location": "eastus"})
+
+ // Register versions 1..10; lexical compare would report "9" as latest.
+ for _, v := range []string{"1", "2", "9", "10"} {
+ do(t, http.MethodPut, url+mlBase()+"/workspaces/ws1/models/m/versions/"+v, map[string]any{
+ "properties": map[string]any{"path": "azureml://models/m/" + v},
+ })
+ }
+
+ containers := do(t, http.MethodGet, url+mlBase()+"/workspaces/ws1/models", nil)
+ require.Len(t, containers["value"], 1)
+ c := containers["value"].([]any)[0].(map[string]any)
+ assert.Equal(t, "10", c["properties"].(map[string]any)["latestVersion"])
+}
+
+func TestMLDatastoresConnectionsSchedulesRegistries(t *testing.T) {
+ url := newMLServer(t)
+ do(t, http.MethodPut, url+mlBase()+"/workspaces/ws1", map[string]any{"location": "eastus"})
+
+ ds := do(t, http.MethodPut, url+mlBase()+"/workspaces/ws1/datastores/store1", map[string]any{
+ "properties": map[string]any{"datastoreType": "AzureBlob", "accountName": "acct", "containerName": "data"},
+ })
+ assert.Equal(t, "store1", ds["name"])
+
+ conn := do(t, http.MethodPut, url+mlBase()+"/workspaces/ws1/connections/aoai", map[string]any{
+ "properties": map[string]any{"category": "AzureOpenAI", "target": "https://x.openai.azure.com"},
+ })
+ assert.Equal(t, "aoai", conn["name"])
+
+ sched := do(t, http.MethodPut, url+mlBase()+"/workspaces/ws1/schedules/nightly", map[string]any{
+ "properties": map[string]any{"displayName": "nightly", "trigger": map[string]any{"expression": "0 0 * * *"}},
+ })
+ assert.Equal(t, "nightly", sched["name"])
+
+ reg := do(t, http.MethodPut, url+mlBase()+"/registries/reg1", map[string]any{"location": "eastus"})
+ assert.Equal(t, "reg1", reg["name"])
+
+ regs := do(t, http.MethodGet, url+mlBase()+"/registries", nil)
+ assert.Len(t, regs["value"], 1)
+
+ require.NotEmpty(t, ds["id"])
+}
+
+// --- real armmachinelearning SDK roundtrip ---
+
+func newMLClientFactory(t *testing.T) *armmachinelearning.ClientFactory {
+ t.Helper()
+
+ cloudP := cloudemu.NewAzure()
+ srv := azureserver.New(azureserver.Drivers{MachineLearning: cloudP.AzureAI})
+ ts := httptest.NewTLSServer(srv)
+ t.Cleanup(ts.Close)
+
+ cf, err := armmachinelearning.NewClientFactory(sub, fakeCred{}, armClientOptions(ts))
+ require.NoError(t, err)
+
+ return cf
+}
+
+func TestSDKMachineLearningWorkspaceAndAssets(t *testing.T) {
+ cf := newMLClientFactory(t)
+ ctx := context.Background()
+ ws := cf.NewWorkspacesClient()
+
+ poller, err := ws.BeginCreateOrUpdate(ctx, rg, "ws1", armmachinelearning.Workspace{
+ Location: to.Ptr("eastus"),
+ }, nil)
+ require.NoError(t, err)
+
+ created, err := poller.PollUntilDone(ctx, nil)
+ require.NoError(t, err)
+ require.NotNil(t, created.Workspace.Name)
+ assert.Equal(t, "ws1", *created.Workspace.Name)
+
+ got, err := ws.Get(ctx, rg, "ws1", nil)
+ require.NoError(t, err)
+ assert.Equal(t, "eastus", *got.Workspace.Location)
+
+ // Register versions 1, 2, 10; the latest must be the numeric max, not "9"/lexical.
+ mv := cf.NewModelVersionsClient()
+ for _, v := range []string{"1", "2", "10"} {
+ _, cerr := mv.CreateOrUpdate(ctx, rg, "ws1", "m", v, armmachinelearning.ModelVersion{
+ Properties: &armmachinelearning.ModelVersionProperties{Description: to.Ptr("v" + v)},
+ }, nil)
+ require.NoError(t, cerr)
+ }
+
+ versions := mv.NewListPager(rg, "ws1", "m", nil)
+ vpage, err := versions.NextPage(ctx)
+ require.NoError(t, err)
+ assert.Len(t, vpage.Value, 3)
+
+ mc := cf.NewModelContainersClient()
+ cpage, err := mc.NewListPager(rg, "ws1", nil).NextPage(ctx)
+ require.NoError(t, err)
+ require.Len(t, cpage.Value, 1)
+ require.NotNil(t, cpage.Value[0].Properties)
+ require.NotNil(t, cpage.Value[0].Properties.LatestVersion)
+ assert.Equal(t, "10", *cpage.Value[0].Properties.LatestVersion)
+
+ page, err := ws.NewListByResourceGroupPager(rg, nil).NextPage(ctx)
+ require.NoError(t, err)
+ assert.GreaterOrEqual(t, len(page.Value), 1)
+
+ delPoller, err := ws.BeginDelete(ctx, rg, "ws1", nil)
+ require.NoError(t, err)
+ _, err = delPoller.PollUntilDone(ctx, nil)
+ require.NoError(t, err)
+}
diff --git a/server/azure/azureai/machinelearning_simple.go b/server/azure/azureai/machinelearning_simple.go
new file mode 100644
index 0000000..d8c26b8
--- /dev/null
+++ b/server/azure/azureai/machinelearning_simple.go
@@ -0,0 +1,189 @@
+package azureai
+
+import (
+ "net/http"
+
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ mldriver "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+func datastoreJSON(d *mldriver.Datastore) map[string]any {
+ return map[string]any{
+ "id": d.ID, "name": d.Name, "type": mlProvider + "/workspaces/datastores",
+ "properties": map[string]any{
+ "datastoreType": d.StoreType, "accountName": d.AccountName, "containerName": d.Container,
+ },
+ }
+}
+
+//nolint:dupl // uniform workspace-child CRUD; shape recurs across collections.
+func (h *MachineLearningHandler) serveDatastores(w http.ResponseWriter, r *http.Request, p *mlPath, ws string) {
+ if len(p.rest) == mlLenCollection {
+ if r.Method != http.MethodGet {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ ds, err := h.svc.ListDatastores(r.Context(), p.resourceGroup, ws)
+ writeMLList(w, datastoreJSON, ds, err)
+
+ return
+ }
+
+ name := p.rest[3]
+
+ switch r.Method {
+ case http.MethodPut:
+ var body struct {
+ Properties struct {
+ DatastoreType string `json:"datastoreType"`
+ AccountName string `json:"accountName"`
+ ContainerName string `json:"containerName"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ d, err := h.svc.CreateDatastore(r.Context(), mldriver.DatastoreConfig{
+ Workspace: ws, ResourceGroup: p.resourceGroup, Name: name,
+ StoreType: body.Properties.DatastoreType, AccountName: body.Properties.AccountName,
+ Container: body.Properties.ContainerName,
+ })
+ writeML(w, datastoreJSON, d, err)
+ case http.MethodGet:
+ d, err := h.svc.GetDatastore(r.Context(), p.resourceGroup, ws, name)
+ writeML(w, datastoreJSON, d, err)
+ case http.MethodDelete:
+ writeMLDelete(w, h.svc.DeleteDatastore(r.Context(), p.resourceGroup, ws, name))
+ default:
+ writeMLMethodNotAllowed(w)
+ }
+}
+
+func connectionJSON(c *mldriver.Connection) map[string]any {
+ return map[string]any{
+ "id": c.ID, "name": c.Name, "type": mlProvider + "/workspaces/connections",
+ "properties": map[string]any{"category": c.Category, "target": c.Target, "authType": c.AuthType},
+ }
+}
+
+//nolint:dupl // uniform workspace-child CRUD; shape recurs across collections.
+func (h *MachineLearningHandler) serveConnections(w http.ResponseWriter, r *http.Request, p *mlPath, ws string) {
+ if len(p.rest) == mlLenCollection {
+ if r.Method != http.MethodGet {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ cs, err := h.svc.ListConnections(r.Context(), p.resourceGroup, ws)
+ writeMLList(w, connectionJSON, cs, err)
+
+ return
+ }
+
+ name := p.rest[3]
+
+ switch r.Method {
+ case http.MethodPut:
+ var body struct {
+ Properties struct {
+ Category string `json:"category"`
+ Target string `json:"target"`
+ AuthType string `json:"authType"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ c, err := h.svc.CreateConnection(r.Context(), mldriver.ConnectionConfig{
+ Workspace: ws, ResourceGroup: p.resourceGroup, Name: name,
+ Category: body.Properties.Category, Target: body.Properties.Target, AuthType: body.Properties.AuthType,
+ })
+ writeML(w, connectionJSON, c, err)
+ case http.MethodGet:
+ c, err := h.svc.GetConnection(r.Context(), p.resourceGroup, ws, name)
+ writeML(w, connectionJSON, c, err)
+ case http.MethodDelete:
+ writeMLDelete(w, h.svc.DeleteConnection(r.Context(), p.resourceGroup, ws, name))
+ default:
+ writeMLMethodNotAllowed(w)
+ }
+}
+
+func mlScheduleJSON(s *mldriver.MLSchedule) map[string]any {
+ return map[string]any{
+ "id": s.ID, "name": s.Name, "type": mlProvider + "/workspaces/schedules",
+ "properties": map[string]any{
+ "displayName": s.DisplayName, "isEnabled": s.IsEnabled,
+ "trigger": map[string]any{"expression": s.Cron},
+ },
+ }
+}
+
+func (h *MachineLearningHandler) serveSchedules(w http.ResponseWriter, r *http.Request, p *mlPath, ws string) {
+ if len(p.rest) == mlLenCollection {
+ if r.Method != http.MethodGet {
+ writeMLMethodNotAllowed(w)
+
+ return
+ }
+
+ ss, err := h.svc.ListMLSchedules(r.Context(), p.resourceGroup, ws)
+ writeMLList(w, mlScheduleJSON, ss, err)
+
+ return
+ }
+
+ name := p.rest[3]
+
+ switch r.Method {
+ case http.MethodPut:
+ var body struct {
+ Properties struct {
+ DisplayName string `json:"displayName"`
+ Trigger struct {
+ Expression string `json:"expression"`
+ } `json:"trigger"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ s, err := h.svc.CreateMLSchedule(r.Context(), mldriver.MLScheduleConfig{
+ Workspace: ws, ResourceGroup: p.resourceGroup, Name: name,
+ Cron: body.Properties.Trigger.Expression, DisplayName: body.Properties.DisplayName,
+ })
+ writeML(w, mlScheduleJSON, s, err)
+ case http.MethodGet:
+ s, err := h.svc.GetMLSchedule(r.Context(), p.resourceGroup, ws, name)
+ writeML(w, mlScheduleJSON, s, err)
+ case http.MethodDelete:
+ writeMLDelete(w, h.svc.DeleteMLSchedule(r.Context(), p.resourceGroup, ws, name))
+ default:
+ writeMLMethodNotAllowed(w)
+ }
+}
+
+// writeMLList writes a "value"-wrapped list or maps the error.
+func writeMLList[T any](w http.ResponseWriter, toJSON func(*T) map[string]any, items []T, err error) {
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(items))
+ for i := range items {
+ out = append(out, toJSON(&items[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
diff --git a/server/azure/azureai/sdk_roundtrip_test.go b/server/azure/azureai/sdk_roundtrip_test.go
new file mode 100644
index 0000000..b42bcbb
--- /dev/null
+++ b/server/azure/azureai/sdk_roundtrip_test.go
@@ -0,0 +1,278 @@
+package azureai_test
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
+)
+
+// fakeCred is a no-op bearer-token credential for driving real ARM SDK clients
+// against the in-memory server.
+type fakeCred struct{}
+
+func (fakeCred) GetToken(_ context.Context, _ policy.TokenRequestOptions) (azcore.AccessToken, error) {
+ return azcore.AccessToken{Token: "fake", ExpiresOn: time.Now().Add(time.Hour)}, nil
+}
+
+// armClientOptions points a real ARM client at the in-memory TLS server.
+func armClientOptions(ts *httptest.Server) *arm.ClientOptions {
+ return &arm.ClientOptions{
+ ClientOptions: azcore.ClientOptions{
+ Cloud: cloud.Configuration{
+ ActiveDirectoryAuthorityHost: "https://login.microsoftonline.com/",
+ Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
+ cloud.ResourceManager: {Endpoint: ts.URL, Audience: "https://management.azure.com"},
+ },
+ },
+ Transport: ts.Client(),
+ Retry: policy.RetryOptions{MaxRetries: -1},
+ },
+ }
+}
+
+const (
+ sub = "sub-1"
+ rg = "rg-1"
+ acct = "my-ai"
+)
+
+func base() string {
+ return "/subscriptions/" + sub + "/resourceGroups/" + rg + "/providers/Microsoft.CognitiveServices/accounts"
+}
+
+func newServer(t *testing.T) string {
+ t.Helper()
+
+ cloud := cloudemu.NewAzure()
+ srv := azureserver.New(azureserver.Drivers{CognitiveServices: cloud.AzureAI})
+ ts := httptest.NewServer(srv)
+ t.Cleanup(ts.Close)
+
+ return ts.URL
+}
+
+func do(t *testing.T, method, url string, body any) map[string]any {
+ t.Helper()
+
+ var rdr io.Reader
+
+ if body != nil {
+ b, err := json.Marshal(body)
+ require.NoError(t, err)
+ rdr = bytes.NewReader(b)
+ }
+
+ req, err := http.NewRequest(method, url, rdr)
+ require.NoError(t, err)
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ raw, _ := io.ReadAll(resp.Body)
+ require.Equalf(t, http.StatusOK, resp.StatusCode, "method=%s url=%s body=%s", method, url, raw)
+
+ out := map[string]any{}
+ if len(bytes.TrimSpace(raw)) > 0 {
+ require.NoError(t, json.Unmarshal(raw, &out), "body=%s", raw)
+ }
+
+ return out
+}
+
+func props(m map[string]any) map[string]any {
+ p, _ := m["properties"].(map[string]any)
+
+ return p
+}
+
+func TestAccountLifecycle(t *testing.T) {
+ url := newServer(t)
+
+ acc := do(t, http.MethodPut, url+base()+"/"+acct, map[string]any{
+ "location": "eastus", "kind": "AIServices", "sku": map[string]any{"name": "S0"},
+ "tags": map[string]any{"env": "test"},
+ })
+ assert.Equal(t, acct, acc["name"])
+ assert.NotEmpty(t, props(acc)["endpoint"])
+ assert.Equal(t, "Succeeded", props(acc)["provisioningState"])
+
+ got := do(t, http.MethodGet, url+base()+"/"+acct, nil)
+ assert.Equal(t, "eastus", got["location"])
+
+ patched := do(t, http.MethodPatch, url+base()+"/"+acct, map[string]any{"tags": map[string]any{"env": "prod"}})
+ assert.Equal(t, "prod", patched["tags"].(map[string]any)["env"])
+
+ list := do(t, http.MethodGet, url+base(), nil)
+ assert.Len(t, list["value"], 1)
+
+ subList := do(t, http.MethodGet,
+ url+"/subscriptions/"+sub+"/providers/Microsoft.CognitiveServices/accounts", nil)
+ assert.Len(t, subList["value"], 1)
+}
+
+func TestAccountKeysAndCatalogs(t *testing.T) {
+ url := newServer(t)
+ do(t, http.MethodPut, url+base()+"/"+acct, map[string]any{"location": "eastus", "kind": "OpenAI"})
+
+ keys := do(t, http.MethodPost, url+base()+"/"+acct+"/listKeys", map[string]any{})
+ require.NotEmpty(t, keys["key1"])
+ require.NotEmpty(t, keys["key2"])
+
+ regen := do(t, http.MethodPost, url+base()+"/"+acct+"/regenerateKey", map[string]any{"keyName": "Key1"})
+ assert.NotEqual(t, keys["key1"], regen["key1"], "regenerated key1 must change")
+ assert.Equal(t, keys["key2"], regen["key2"], "key2 must be stable")
+
+ // The rotation must persist: a subsequent listKeys returns the new key1.
+ after := do(t, http.MethodPost, url+base()+"/"+acct+"/listKeys", map[string]any{})
+ assert.Equal(t, regen["key1"], after["key1"], "rotated key1 must persist")
+ assert.Equal(t, keys["key2"], after["key2"])
+
+ models := do(t, http.MethodGet, url+base()+"/"+acct+"/models", nil)
+ assert.NotEmpty(t, models["value"])
+
+ skus := do(t, http.MethodGet, url+base()+"/"+acct+"/skus", nil)
+ assert.NotEmpty(t, skus["value"])
+
+ usages := do(t, http.MethodGet, url+base()+"/"+acct+"/usages", nil)
+ assert.NotEmpty(t, usages["value"])
+}
+
+func TestDeploymentsAndChildren(t *testing.T) {
+ url := newServer(t)
+ do(t, http.MethodPut, url+base()+"/"+acct, map[string]any{"location": "eastus", "kind": "OpenAI"})
+
+ dep := do(t, http.MethodPut, url+base()+"/"+acct+"/deployments/gpt4o", map[string]any{
+ "sku": map[string]any{"name": "Standard", "capacity": 10},
+ "properties": map[string]any{"model": map[string]any{"name": "gpt-4o", "version": "2024-08-06", "format": "OpenAI"}},
+ })
+ assert.Equal(t, "gpt4o", dep["name"])
+ assert.Equal(t, "gpt-4o", props(dep)["model"].(map[string]any)["name"])
+
+ got := do(t, http.MethodGet, url+base()+"/"+acct+"/deployments/gpt4o", nil)
+ assert.Equal(t, "gpt4o", got["name"])
+
+ depList := do(t, http.MethodGet, url+base()+"/"+acct+"/deployments", nil)
+ assert.Len(t, depList["value"], 1)
+
+ // Project (AI Foundry).
+ proj := do(t, http.MethodPut, url+base()+"/"+acct+"/projects/proj1", map[string]any{
+ "location": "eastus", "properties": map[string]any{"displayName": "My Project"},
+ })
+ assert.Equal(t, "My Project", props(proj)["displayName"])
+
+ // RAI policy.
+ rai := do(t, http.MethodPut, url+base()+"/"+acct+"/raiPolicies/strict", map[string]any{
+ "properties": map[string]any{"mode": "Blocking"},
+ })
+ assert.Equal(t, "Blocking", props(rai)["mode"])
+
+ // Commitment plan.
+ cp := do(t, http.MethodPut, url+base()+"/"+acct+"/commitmentPlans/plan1", map[string]any{
+ "properties": map[string]any{"planType": "PTU", "autoRenew": true},
+ })
+ assert.Equal(t, "PTU", props(cp)["planType"])
+
+ // Private endpoint connection.
+ pec := do(t, http.MethodPut, url+base()+"/"+acct+"/privateEndpointConnections/pec1", map[string]any{
+ "properties": map[string]any{"privateLinkServiceConnectionState": map[string]any{"status": "Approved"}},
+ })
+ assert.Equal(t, "Approved", props(pec)["privateLinkServiceConnectionState"].(map[string]any)["status"])
+
+ do(t, http.MethodDelete, url+base()+"/"+acct+"/deployments/gpt4o", nil)
+ after := do(t, http.MethodGet, url+base()+"/"+acct+"/deployments", nil)
+ assert.Empty(t, after["value"])
+}
+
+func TestAccountNotFound(t *testing.T) {
+ url := newServer(t)
+
+ req, _ := http.NewRequest(http.MethodGet, url+base()+"/ghost", nil)
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusNotFound, resp.StatusCode)
+}
+
+// --- real armcognitiveservices SDK roundtrip ---
+
+func newCSAccountsClient(t *testing.T) *armcognitiveservices.AccountsClient {
+ t.Helper()
+
+ cloudP := cloudemu.NewAzure()
+ srv := azureserver.New(azureserver.Drivers{CognitiveServices: cloudP.AzureAI})
+ ts := httptest.NewTLSServer(srv)
+ t.Cleanup(ts.Close)
+
+ c, err := armcognitiveservices.NewAccountsClient(sub, fakeCred{}, armClientOptions(ts))
+ require.NoError(t, err)
+
+ return c
+}
+
+func TestSDKCognitiveServicesAccountLifecycle(t *testing.T) {
+ c := newCSAccountsClient(t)
+ ctx := context.Background()
+
+ poller, err := c.BeginCreate(ctx, rg, acct, armcognitiveservices.Account{
+ Location: to.Ptr("eastus"),
+ Kind: to.Ptr("AIServices"),
+ SKU: &armcognitiveservices.SKU{Name: to.Ptr("S0")},
+ }, nil)
+ require.NoError(t, err)
+
+ created, err := poller.PollUntilDone(ctx, nil)
+ require.NoError(t, err)
+ require.NotNil(t, created.Account.Name)
+ assert.Equal(t, acct, *created.Account.Name)
+
+ got, err := c.Get(ctx, rg, acct, nil)
+ require.NoError(t, err)
+ assert.Equal(t, "eastus", *got.Account.Location)
+ require.NotNil(t, got.Account.Properties)
+ require.NotNil(t, got.Account.Properties.Endpoint)
+ assert.NotEmpty(t, *got.Account.Properties.Endpoint)
+
+ keys, err := c.ListKeys(ctx, rg, acct, nil)
+ require.NoError(t, err)
+ require.NotNil(t, keys.Key1)
+
+ regen, err := c.RegenerateKey(ctx, rg, acct, armcognitiveservices.RegenerateKeyParameters{
+ KeyName: to.Ptr(armcognitiveservices.KeyNameKey1),
+ }, nil)
+ require.NoError(t, err)
+ require.NotNil(t, regen.Key1)
+ assert.NotEqual(t, *keys.Key1, *regen.Key1, "rotated key must differ")
+
+ pager := c.NewListByResourceGroupPager(rg, nil)
+ page, err := pager.NextPage(ctx)
+ require.NoError(t, err)
+ assert.Len(t, page.Value, 1)
+
+ delPoller, err := c.BeginDelete(ctx, rg, acct, nil)
+ require.NoError(t, err)
+ _, err = delPoller.PollUntilDone(ctx, nil)
+ require.NoError(t, err)
+
+ _, err = c.Get(ctx, rg, acct, nil)
+ assert.Error(t, err, "account should be gone after delete")
+}
diff --git a/server/azure/azuresearch/control_actions.go b/server/azure/azuresearch/control_actions.go
new file mode 100644
index 0000000..7046d22
--- /dev/null
+++ b/server/azure/azuresearch/control_actions.go
@@ -0,0 +1,237 @@
+package azuresearch
+
+import (
+ "net/http"
+
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ srchdriver "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+// actionMethods is the HTTP method each key action requires, matching the real
+// Microsoft.Search REST contract.
+//
+//nolint:gochecknoglobals // immutable routing set
+var actionMethods = map[string]string{
+ subAdminKeys: http.MethodPost,
+ subRegenerate: http.MethodPost,
+ subListQuery: http.MethodPost,
+ subCreateQuery: http.MethodPost,
+ subDeleteQuery: http.MethodDelete,
+}
+
+// serveServiceAction handles the key verbs under a service.
+func (h *ControlHandler) serveServiceAction(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ rg, name := rp.ResourceGroup, rp.ResourceName
+
+ if want, ok := actionMethods[rp.SubResource]; ok && r.Method != want {
+ writeMethodNotAllowed(w)
+
+ return
+ }
+
+ switch rp.SubResource {
+ case subAdminKeys:
+ keys, err := h.svc.ListAdminKeys(r.Context(), rg, name)
+ writeAdminKeys(w, keys, err)
+ case subRegenerate:
+ // .../regenerateAdminKey/{primary|secondary}
+ keys, err := h.svc.RegenerateAdminKey(r.Context(), rg, name, rp.SubResourceName)
+ writeAdminKeys(w, keys, err)
+ case subListQuery:
+ keys, err := h.svc.ListQueryKeys(r.Context(), rg, name)
+ writeQueryKeys(w, keys, err)
+ case subCreateQuery:
+ // .../createQueryKey/{keyName}
+ qk, err := h.svc.CreateQueryKey(r.Context(), rg, name, rp.SubResourceName)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, queryKeyJSON(qk))
+ case subDeleteQuery:
+ // .../deleteQueryKey/{key}
+ if err := h.svc.DeleteQueryKey(r.Context(), rg, name, rp.SubResourceName); err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{})
+ default:
+ azurearm.WriteError(w, http.StatusNotFound, "NotFound", "unknown action: "+rp.SubResource)
+ }
+}
+
+func writeAdminKeys(w http.ResponseWriter, keys *srchdriver.AdminKeys, err error) {
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"primaryKey": keys.Primary, "secondaryKey": keys.Secondary})
+}
+
+func queryKeyJSON(qk *srchdriver.QueryKey) map[string]any {
+ return map[string]any{"name": qk.Name, "key": qk.Key}
+}
+
+func writeQueryKeys(w http.ResponseWriter, keys []srchdriver.QueryKey, err error) {
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(keys))
+ for i := range keys {
+ out = append(out, queryKeyJSON(&keys[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
+
+func sharedLinkJSON(l *srchdriver.SharedPrivateLink) map[string]any {
+ return map[string]any{
+ "id": l.ID, "name": l.Name, "type": searchProvider + "/searchServices/sharedPrivateLinkResources",
+ "properties": map[string]any{
+ "groupId": l.GroupID, "privateLinkResourceId": l.PrivateLinkID,
+ "status": l.Status, "provisioningState": l.ProvisioningState,
+ },
+ }
+}
+
+func (h *ControlHandler) serveSharedLink(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ rg, name := rp.ResourceGroup, rp.ResourceName
+
+ if rp.SubResourceName == "" {
+ if r.Method != http.MethodGet {
+ writeMethodNotAllowed(w)
+
+ return
+ }
+
+ links, err := h.svc.ListSharedPrivateLinks(r.Context(), rg, name)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(links))
+ for i := range links {
+ out = append(out, sharedLinkJSON(&links[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+
+ return
+ }
+
+ link := rp.SubResourceName
+
+ switch r.Method {
+ case http.MethodPut:
+ var body struct {
+ Properties struct {
+ GroupID string `json:"groupId"`
+ PrivateLinkResourceID string `json:"privateLinkResourceId"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ l, err := h.svc.PutSharedPrivateLink(r.Context(), rg, name, link, body.Properties.GroupID, body.Properties.PrivateLinkResourceID)
+ writeRes(w, sharedLinkJSON, l, err)
+ case http.MethodGet:
+ l, err := h.svc.GetSharedPrivateLink(r.Context(), rg, name, link)
+ writeRes(w, sharedLinkJSON, l, err)
+ case http.MethodDelete:
+ if err := h.svc.DeleteSharedPrivateLink(r.Context(), rg, name, link); err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{})
+ default:
+ writeMethodNotAllowed(w)
+ }
+}
+
+func pecJSON(c *srchdriver.PrivateEndpointConnection) map[string]any {
+ return map[string]any{
+ "id": c.ID, "name": c.Name, "type": searchProvider + "/searchServices/privateEndpointConnections",
+ "properties": map[string]any{
+ "provisioningState": c.ProvisioningState,
+ "privateLinkServiceConnectionState": map[string]any{
+ "status": c.Status, "description": c.Description,
+ },
+ },
+ }
+}
+
+func (h *ControlHandler) servePEC(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ rg, name := rp.ResourceGroup, rp.ResourceName
+
+ if rp.SubResourceName == "" {
+ if r.Method != http.MethodGet {
+ writeMethodNotAllowed(w)
+
+ return
+ }
+
+ conns, err := h.svc.ListPrivateEndpointConnections(r.Context(), rg, name)
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(conns))
+ for i := range conns {
+ out = append(out, pecJSON(&conns[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+
+ return
+ }
+
+ conn := rp.SubResourceName
+
+ switch r.Method {
+ case http.MethodPut:
+ var body struct {
+ Properties struct {
+ State struct {
+ Status string `json:"status"`
+ } `json:"privateLinkServiceConnectionState"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ c, err := h.svc.PutPrivateEndpointConnection(r.Context(), rg, name, conn, body.Properties.State.Status)
+ writeRes(w, pecJSON, c, err)
+ case http.MethodGet:
+ c, err := h.svc.GetPrivateEndpointConnection(r.Context(), rg, name, conn)
+ writeRes(w, pecJSON, c, err)
+ case http.MethodDelete:
+ if err := h.svc.DeletePrivateEndpointConnection(r.Context(), rg, name, conn); err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{})
+ default:
+ writeMethodNotAllowed(w)
+ }
+}
diff --git a/server/azure/azuresearch/dataplane.go b/server/azure/azuresearch/dataplane.go
new file mode 100644
index 0000000..5b00d92
--- /dev/null
+++ b/server/azure/azuresearch/dataplane.go
@@ -0,0 +1,312 @@
+package azuresearch
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/stackshy/cloudemu/v2/errors"
+ srchdriver "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+const maxDPBody = 8 << 20
+
+// dataPlaneRoots are the path prefixes the data-plane handler claims.
+//
+//nolint:gochecknoglobals // immutable routing set
+var dataPlaneRoots = map[string]bool{
+ "indexes": true, "indexers": true, "datasources": true,
+ "skillsets": true, "synonymmaps": true, "aliases": true, "servicestats": true,
+}
+
+// DataPlaneHandler serves the {service}.search.windows.net data plane. The
+// service scope is derived from the request Host subdomain.
+type DataPlaneHandler struct {
+ dp srchdriver.SearchDataPlane
+}
+
+// NewDataPlane returns a data-plane handler backed by drv.
+func NewDataPlane(drv srchdriver.SearchDataPlane) *DataPlaneHandler {
+ return &DataPlaneHandler{dp: drv}
+}
+
+// Matches claims the search data-plane roots.
+func (*DataPlaneHandler) Matches(r *http.Request) bool {
+ parts := splitPath(r.URL.Path)
+
+ return len(parts) > 0 && dataPlaneRoots[parts[0]]
+}
+
+func splitPath(p string) []string {
+ p = strings.Trim(p, "/")
+ if p == "" {
+ return nil
+ }
+
+ raw := strings.Split(p, "/")
+ out := make([]string, 0, len(raw))
+
+ for _, seg := range raw {
+ out = append(out, expandKeySegment(seg)...)
+ }
+
+ return out
+}
+
+// expandKeySegment normalizes the OData key-form the search SDKs emit, so the
+// fused segments route identically to their slash-separated equivalents:
+//
+// indexes('products') → [indexes products]
+// docs('42') → [docs 42]
+// ('42') → [42]
+func expandKeySegment(seg string) []string {
+ i := strings.Index(seg, "('")
+ if i < 0 || !strings.HasSuffix(seg, "')") {
+ return []string{seg}
+ }
+
+ coll, docKey := seg[:i], seg[i+2:len(seg)-2]
+ if coll == "" {
+ return []string{docKey}
+ }
+
+ return []string{coll, docKey}
+}
+
+// serviceFromHost extracts the service name from {service}.search.windows.net,
+// falling back to "default" for hosts without that suffix (e.g. httptest).
+func serviceFromHost(host string) string {
+ host, _, _ = strings.Cut(host, ":")
+ if i := strings.Index(host, ".search."); i > 0 {
+ return host[:i]
+ }
+
+ return "default"
+}
+
+// ServeHTTP routes by the top-level data-plane resource.
+func (h *DataPlaneHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ parts := splitPath(r.URL.Path)
+ service := serviceFromHost(r.Host)
+
+ switch parts[0] {
+ case "indexes":
+ h.serveIndexes(w, r, service, parts)
+ case "indexers":
+ h.serveIndexers(w, r, service, parts)
+ case "datasources":
+ h.serveDataSources(w, r, service, parts)
+ case "skillsets":
+ h.serveSkillsets(w, r, service, parts)
+ case "synonymmaps":
+ h.serveSynonymMaps(w, r, service, parts)
+ case "aliases":
+ h.serveAliases(w, r, service, parts)
+ case "servicestats":
+ h.serveServiceStats(w, r, service)
+ default:
+ dpErr(w, http.StatusNotFound, "unknown data-plane resource: "+parts[0])
+ }
+}
+
+// --- Indexes + documents ---
+
+func indexJSON(i *srchdriver.Index) map[string]any {
+ fields := make([]map[string]any, 0, len(i.Fields))
+ for _, f := range i.Fields {
+ fields = append(fields, map[string]any{
+ "name": f.Name, "type": f.Type, "key": f.Key,
+ "searchable": f.Searchable, "filterable": f.Filterable,
+ "sortable": f.Sortable, "facetable": f.Facetable,
+ "retrievable": f.Retrievable, "dimensions": f.Dimensions,
+ })
+ }
+
+ return map[string]any{"name": i.Name, "fields": fields, "@odata.etag": i.ETag}
+}
+
+func (h *DataPlaneHandler) serveIndexes(w http.ResponseWriter, r *http.Request, service string, parts []string) {
+ // /indexes/{name}/docs/...
+ const docsIdx = 2
+ if len(parts) > docsIdx && parts[docsIdx] == "docs" {
+ h.serveDocs(w, r, service, parts[1], parts)
+
+ return
+ }
+
+ if len(parts) == 1 {
+ switch r.Method {
+ case http.MethodPost:
+ h.putIndex(w, r, service, "")
+ case http.MethodGet:
+ h.listIndexes(w, r, service)
+ default:
+ dpMethodNotAllowed(w)
+ }
+
+ return
+ }
+
+ name := parts[1]
+
+ switch r.Method {
+ case http.MethodPut:
+ h.putIndex(w, r, service, name)
+ case http.MethodGet:
+ idx, err := h.dp.GetIndex(r.Context(), service, name)
+ writeDP(w, indexJSON, idx, err)
+ case http.MethodDelete:
+ dpDelete(w, h.dp.DeleteIndex(r.Context(), service, name))
+ default:
+ dpMethodNotAllowed(w)
+ }
+}
+
+func (h *DataPlaneHandler) putIndex(w http.ResponseWriter, r *http.Request, service, name string) {
+ var body struct {
+ Name string `json:"name"`
+ Fields []struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Key bool `json:"key"`
+ Searchable bool `json:"searchable"`
+ Filterable bool `json:"filterable"`
+ Sortable bool `json:"sortable"`
+ Facetable bool `json:"facetable"`
+ Retrievable bool `json:"retrievable"`
+ Dimensions int `json:"dimensions"`
+ } `json:"fields"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ if body.Name == "" {
+ body.Name = name
+ }
+
+ idx := srchdriver.Index{Name: body.Name}
+ for _, f := range body.Fields {
+ idx.Fields = append(idx.Fields, srchdriver.Field{
+ Name: f.Name, Type: f.Type, Key: f.Key, Searchable: f.Searchable,
+ Filterable: f.Filterable, Sortable: f.Sortable, Facetable: f.Facetable,
+ Retrievable: f.Retrievable, Dimensions: f.Dimensions,
+ })
+ }
+
+ out, err := h.dp.CreateOrUpdateIndex(r.Context(), service, idx)
+ writeDP(w, indexJSON, out, err)
+}
+
+func (h *DataPlaneHandler) listIndexes(w http.ResponseWriter, r *http.Request, service string) {
+ idxs, err := h.dp.ListIndexes(r.Context(), service)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(idxs))
+ for i := range idxs {
+ out = append(out, indexJSON(&idxs[i]))
+ }
+
+ dpJSON(w, map[string]any{"value": out})
+}
+
+// --- wire helpers ---
+
+func dpDecode(w http.ResponseWriter, r *http.Request, v any) bool {
+ body, err := io.ReadAll(io.LimitReader(r.Body, maxDPBody))
+ if err != nil {
+ dpErr(w, http.StatusBadRequest, "failed to read body")
+
+ return false
+ }
+
+ if strings.TrimSpace(string(body)) == "" {
+ return true
+ }
+
+ if err := json.Unmarshal(body, v); err != nil {
+ dpErr(w, http.StatusBadRequest, "invalid JSON body")
+
+ return false
+ }
+
+ return true
+}
+
+func dpJSON(w http.ResponseWriter, v any) {
+ dpJSONStatus(w, http.StatusOK, v)
+}
+
+func dpJSONStatus(w http.ResponseWriter, status int, v any) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ _ = json.NewEncoder(w).Encode(v)
+}
+
+func dpErr(w http.ResponseWriter, status int, msg string) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ // Azure returns error.code as a string identifier, not the numeric HTTP status.
+ _ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"code": codeForStatus(status), "message": msg}})
+}
+
+// codeForStatus maps an HTTP status to the string error-code identifier Azure
+// data-plane clients expect in error.code.
+func codeForStatus(status int) string {
+ switch status {
+ case http.StatusBadRequest:
+ return "BadRequest"
+ case http.StatusNotFound:
+ return "NotFound"
+ case http.StatusMethodNotAllowed:
+ return "MethodNotAllowed"
+ case http.StatusConflict:
+ return "Conflict"
+ default:
+ return "InternalServerError"
+ }
+}
+
+func dpMethodNotAllowed(w http.ResponseWriter) {
+ dpErr(w, http.StatusMethodNotAllowed, "method not allowed")
+}
+
+func dpCErr(w http.ResponseWriter, err error) {
+ switch {
+ case errors.IsNotFound(err):
+ dpErr(w, http.StatusNotFound, err.Error())
+ case errors.IsInvalidArgument(err):
+ dpErr(w, http.StatusBadRequest, err.Error())
+ case errors.IsAlreadyExists(err), errors.IsFailedPrecondition(err):
+ dpErr(w, http.StatusConflict, err.Error())
+ default:
+ dpErr(w, http.StatusInternalServerError, err.Error())
+ }
+}
+
+func dpDelete(w http.ResponseWriter, err error) {
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ dpJSON(w, map[string]any{})
+}
+
+// writeDP writes a created/fetched data-plane resource or maps the error.
+func writeDP[T any](w http.ResponseWriter, toJSON func(*T) map[string]any, res *T, err error) {
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ dpJSON(w, toJSON(res))
+}
diff --git a/server/azure/azuresearch/dataplane_docs.go b/server/azure/azuresearch/dataplane_docs.go
new file mode 100644
index 0000000..86ce7b5
--- /dev/null
+++ b/server/azure/azuresearch/dataplane_docs.go
@@ -0,0 +1,253 @@
+package azuresearch
+
+import (
+ "net/http"
+ "strconv"
+ "strings"
+
+ srchdriver "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+// atoiOr parses s as an int, returning def on failure or empty input.
+func atoiOr(s string, def int) int {
+ if n, err := strconv.Atoi(strings.TrimSpace(s)); err == nil {
+ return n
+ }
+
+ return def
+}
+
+// serveDocs handles /indexes/{index}/docs/... actions.
+func (h *DataPlaneHandler) serveDocs(w http.ResponseWriter, r *http.Request, service, index string, parts []string) {
+ // parts: [indexes {index} docs {action?}]
+ const actionIdx = 3
+ if len(parts) <= actionIdx {
+ // GET /indexes/{index}/docs → simple search via query params.
+ h.searchDocs(w, r, service, index, true)
+
+ return
+ }
+
+ action := parts[actionIdx]
+
+ switch action {
+ case "search":
+ h.searchDocs(w, r, service, index, false)
+ case "index":
+ h.indexDocs(w, r, service, index)
+ case "suggest":
+ h.suggestDocs(w, r, service, index)
+ case "autocomplete":
+ h.autocompleteDocs(w, r, service, index)
+ case "$count":
+ h.countDocs(w, r, service, index)
+ default:
+ // Treat any other trailing segment as a document key:
+ // /indexes/{index}/docs/{key} or /docs('{key}').
+ h.getDoc(w, r, service, index, docKey(action))
+ }
+}
+
+func docKey(seg string) string {
+ if strings.HasPrefix(seg, "('") && strings.HasSuffix(seg, "')") {
+ return seg[2 : len(seg)-2]
+ }
+
+ return seg
+}
+
+func (h *DataPlaneHandler) searchDocs(w http.ResponseWriter, r *http.Request, service, index string, fromQuery bool) {
+ req := srchdriver.SearchRequest{Count: false}
+
+ if fromQuery {
+ q := r.URL.Query()
+ req.Search = q.Get("search")
+ req.Filter = q.Get("$filter")
+ req.OrderBy = q.Get("$orderby")
+ req.Count = q.Get("$count") == "true"
+ req.Top = atoiOr(q.Get("$top"), 0)
+ req.Skip = atoiOr(q.Get("$skip"), 0)
+
+ if sel := q.Get("$select"); sel != "" {
+ req.Select = strings.Split(sel, ",")
+ }
+ } else {
+ var body struct {
+ Search string `json:"search"`
+ Filter string `json:"filter"`
+ Top int `json:"top"`
+ Skip int `json:"skip"`
+ Select string `json:"select"`
+ Count bool `json:"count"`
+ OrderBy string `json:"orderby"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ req.Search, req.Filter, req.Top, req.Skip = body.Search, body.Filter, body.Top, body.Skip
+ req.Count, req.OrderBy = body.Count, body.OrderBy
+
+ if body.Select != "" {
+ req.Select = strings.Split(body.Select, ",")
+ }
+ }
+
+ resp, err := h.dp.SearchDocuments(r.Context(), service, index, req)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ value := make([]map[string]any, 0, len(resp.Results))
+
+ for _, res := range resp.Results {
+ doc := map[string]any{"@search.score": res.Score}
+ for k, v := range res.Document {
+ doc[k] = v
+ }
+
+ value = append(value, doc)
+ }
+
+ out := map[string]any{"value": value}
+ if resp.Count >= 0 {
+ out["@odata.count"] = resp.Count
+ }
+
+ dpJSON(w, out)
+}
+
+func (h *DataPlaneHandler) indexDocs(w http.ResponseWriter, r *http.Request, service, index string) {
+ var body struct {
+ Value []map[string]any `json:"value"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ actions := make([]srchdriver.IndexAction, 0, len(body.Value))
+
+ for _, raw := range body.Value {
+ act := "upload"
+ if a, ok := raw["@search.action"].(string); ok && a != "" {
+ act = a
+ }
+
+ doc := make(map[string]any, len(raw))
+
+ for k, v := range raw {
+ if k != "@search.action" {
+ doc[k] = v
+ }
+ }
+
+ actions = append(actions, srchdriver.IndexAction{Action: act, Document: doc})
+ }
+
+ results, err := h.dp.IndexDocuments(r.Context(), service, index, actions)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(results))
+ status := http.StatusOK
+
+ for _, res := range results {
+ if !res.Status {
+ // Azure returns 207 Multi-Status when any document in the batch fails.
+ status = http.StatusMultiStatus
+ }
+
+ out = append(out, map[string]any{
+ "key": res.Key, "status": res.Status, "statusCode": res.StatusCode, "errorMessage": res.ErrorMsg,
+ })
+ }
+
+ dpJSONStatus(w, status, map[string]any{"value": out})
+}
+
+func (h *DataPlaneHandler) suggestDocs(w http.ResponseWriter, r *http.Request, service, index string) {
+ var body struct {
+ Search string `json:"search"`
+ SuggesterName string `json:"suggesterName"`
+ Top int `json:"top"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ res, err := h.dp.SuggestDocuments(r.Context(), service, index, body.Search, body.SuggesterName, body.Top)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ value := make([]map[string]any, 0, len(res))
+
+ for _, s := range res {
+ doc := map[string]any{"@search.text": s.Text}
+ for k, v := range s.Document {
+ doc[k] = v
+ }
+
+ value = append(value, doc)
+ }
+
+ dpJSON(w, map[string]any{"value": value})
+}
+
+func (h *DataPlaneHandler) autocompleteDocs(w http.ResponseWriter, r *http.Request, service, index string) {
+ var body struct {
+ Search string `json:"search"`
+ SuggesterName string `json:"suggesterName"`
+ Top int `json:"top"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ res, err := h.dp.AutocompleteDocuments(r.Context(), service, index, body.Search, body.SuggesterName, body.Top)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ value := make([]map[string]any, 0, len(res))
+ for _, t := range res {
+ value = append(value, map[string]any{"text": t, "queryPlusText": t})
+ }
+
+ dpJSON(w, map[string]any{"value": value})
+}
+
+func (h *DataPlaneHandler) countDocs(w http.ResponseWriter, r *http.Request, service, index string) {
+ n, err := h.dp.CountDocuments(r.Context(), service, index)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ dpJSON(w, n)
+}
+
+func (h *DataPlaneHandler) getDoc(w http.ResponseWriter, r *http.Request, service, index, key string) {
+ doc, err := h.dp.GetDocument(r.Context(), service, index, key)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ dpJSON(w, doc)
+}
diff --git a/server/azure/azuresearch/dataplane_resources.go b/server/azure/azuresearch/dataplane_resources.go
new file mode 100644
index 0000000..a780761
--- /dev/null
+++ b/server/azure/azuresearch/dataplane_resources.go
@@ -0,0 +1,388 @@
+package azuresearch
+
+import (
+ "net/http"
+
+ srchdriver "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+// --- Indexers ---
+
+func indexerJSON(i *srchdriver.Indexer) map[string]any {
+ return map[string]any{
+ "name": i.Name, "dataSourceName": i.DataSourceName, "targetIndexName": i.TargetIndex,
+ "skillsetName": i.SkillsetName, "schedule": map[string]any{"interval": i.Schedule}, "@odata.etag": i.ETag,
+ }
+}
+
+func (h *DataPlaneHandler) serveIndexers(w http.ResponseWriter, r *http.Request, service string, parts []string) {
+ if len(parts) == 1 {
+ switch r.Method {
+ case http.MethodGet:
+ ixs, err := h.dp.ListIndexers(r.Context(), service)
+ writeDPList(w, indexerJSON, ixs, err)
+ case http.MethodPost:
+ h.putIndexer(w, r, service, "")
+ default:
+ dpMethodNotAllowed(w)
+ }
+
+ return
+ }
+
+ name := parts[1]
+
+ // /indexers/{name}/{run|reset|status}
+ const actionIdx = 2
+ if len(parts) > actionIdx {
+ h.indexerAction(w, r, service, name, parts[actionIdx])
+
+ return
+ }
+
+ switch r.Method {
+ case http.MethodPut:
+ h.putIndexer(w, r, service, name)
+ case http.MethodGet:
+ ix, err := h.dp.GetIndexer(r.Context(), service, name)
+ writeDP(w, indexerJSON, ix, err)
+ case http.MethodDelete:
+ dpDelete(w, h.dp.DeleteIndexer(r.Context(), service, name))
+ default:
+ dpMethodNotAllowed(w)
+ }
+}
+
+func (h *DataPlaneHandler) putIndexer(w http.ResponseWriter, r *http.Request, service, name string) {
+ var body struct {
+ Name string `json:"name"`
+ DataSourceName string `json:"dataSourceName"`
+ TargetIndexName string `json:"targetIndexName"`
+ SkillsetName string `json:"skillsetName"`
+ Schedule struct {
+ Interval string `json:"interval"`
+ } `json:"schedule"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ if body.Name == "" {
+ body.Name = name
+ }
+
+ ix, err := h.dp.CreateOrUpdateIndexer(r.Context(), service, srchdriver.IndexerConfig{
+ Name: body.Name, DataSourceName: body.DataSourceName, TargetIndex: body.TargetIndexName,
+ SkillsetName: body.SkillsetName, Schedule: body.Schedule.Interval,
+ })
+ writeDP(w, indexerJSON, ix, err)
+}
+
+func (h *DataPlaneHandler) indexerAction(w http.ResponseWriter, r *http.Request, service, name, action string) {
+ switch action {
+ case "run":
+ dpDelete(w, h.dp.RunIndexer(r.Context(), service, name))
+ case "reset":
+ dpDelete(w, h.dp.ResetIndexer(r.Context(), service, name))
+ case "status":
+ st, err := h.dp.GetIndexerStatus(r.Context(), service, name)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ dpJSON(w, map[string]any{
+ "name": st.Name, "status": st.Status,
+ "lastResult": map[string]any{
+ "status": st.LastResult, "itemsProcessed": st.ItemsProcessed, "itemsFailed": st.ItemsFailed,
+ },
+ })
+ default:
+ dpErr(w, http.StatusNotFound, "unknown indexer action: "+action)
+ }
+}
+
+// --- Data sources ---
+
+func dataSourceJSON(d *srchdriver.DataSource) map[string]any {
+ return map[string]any{
+ "name": d.Name, "type": d.Type,
+ "container": map[string]any{"name": d.Container},
+ "@odata.etag": d.ETag,
+ }
+}
+
+//nolint:dupl // uniform data-plane collection CRUD; shape recurs across resources.
+func (h *DataPlaneHandler) serveDataSources(w http.ResponseWriter, r *http.Request, service string, parts []string) {
+ if len(parts) == 1 {
+ switch r.Method {
+ case http.MethodGet:
+ ds, err := h.dp.ListDataSources(r.Context(), service)
+ writeDPList(w, dataSourceJSON, ds, err)
+ case http.MethodPost:
+ h.putDataSource(w, r, service, "")
+ default:
+ dpMethodNotAllowed(w)
+ }
+
+ return
+ }
+
+ name := parts[1]
+
+ switch r.Method {
+ case http.MethodPut:
+ h.putDataSource(w, r, service, name)
+ case http.MethodGet:
+ ds, err := h.dp.GetDataSource(r.Context(), service, name)
+ writeDP(w, dataSourceJSON, ds, err)
+ case http.MethodDelete:
+ dpDelete(w, h.dp.DeleteDataSource(r.Context(), service, name))
+ default:
+ dpMethodNotAllowed(w)
+ }
+}
+
+func (h *DataPlaneHandler) putDataSource(w http.ResponseWriter, r *http.Request, service, name string) {
+ var body struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Container struct {
+ Name string `json:"name"`
+ } `json:"container"`
+ Credentials struct {
+ ConnectionString string `json:"connectionString"`
+ } `json:"credentials"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ if body.Name == "" {
+ body.Name = name
+ }
+
+ ds, err := h.dp.CreateOrUpdateDataSource(r.Context(), service, srchdriver.DataSource{
+ Name: body.Name, Type: body.Type, Container: body.Container.Name, ConnString: body.Credentials.ConnectionString,
+ })
+ writeDP(w, dataSourceJSON, ds, err)
+}
+
+// --- Skillsets ---
+
+func skillsetJSON(s *srchdriver.Skillset) map[string]any {
+ return map[string]any{
+ "name": s.Name, "description": s.Description, "skills": make([]any, s.SkillCount), "@odata.etag": s.ETag,
+ }
+}
+
+//nolint:dupl // uniform data-plane collection CRUD; shape recurs across resources.
+func (h *DataPlaneHandler) serveSkillsets(w http.ResponseWriter, r *http.Request, service string, parts []string) {
+ if len(parts) == 1 {
+ switch r.Method {
+ case http.MethodGet:
+ sk, err := h.dp.ListSkillsets(r.Context(), service)
+ writeDPList(w, skillsetJSON, sk, err)
+ case http.MethodPost:
+ h.putSkillset(w, r, service, "")
+ default:
+ dpMethodNotAllowed(w)
+ }
+
+ return
+ }
+
+ name := parts[1]
+
+ switch r.Method {
+ case http.MethodPut:
+ h.putSkillset(w, r, service, name)
+ case http.MethodGet:
+ sk, err := h.dp.GetSkillset(r.Context(), service, name)
+ writeDP(w, skillsetJSON, sk, err)
+ case http.MethodDelete:
+ dpDelete(w, h.dp.DeleteSkillset(r.Context(), service, name))
+ default:
+ dpMethodNotAllowed(w)
+ }
+}
+
+func (h *DataPlaneHandler) putSkillset(w http.ResponseWriter, r *http.Request, service, name string) {
+ var body struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Skills []map[string]any `json:"skills"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ if body.Name == "" {
+ body.Name = name
+ }
+
+ sk, err := h.dp.CreateOrUpdateSkillset(r.Context(), service, srchdriver.Skillset{
+ Name: body.Name, Description: body.Description, SkillCount: len(body.Skills),
+ })
+ writeDP(w, skillsetJSON, sk, err)
+}
+
+// --- Synonym maps ---
+
+func synonymMapJSON(s *srchdriver.SynonymMap) map[string]any {
+ return map[string]any{"name": s.Name, "format": s.Format, "synonyms": s.Synonyms, "@odata.etag": s.ETag}
+}
+
+//nolint:dupl // uniform data-plane collection CRUD; shape recurs across resources.
+func (h *DataPlaneHandler) serveSynonymMaps(w http.ResponseWriter, r *http.Request, service string, parts []string) {
+ if len(parts) == 1 {
+ switch r.Method {
+ case http.MethodGet:
+ sm, err := h.dp.ListSynonymMaps(r.Context(), service)
+ writeDPList(w, synonymMapJSON, sm, err)
+ case http.MethodPost:
+ h.putSynonymMap(w, r, service, "")
+ default:
+ dpMethodNotAllowed(w)
+ }
+
+ return
+ }
+
+ name := parts[1]
+
+ switch r.Method {
+ case http.MethodPut:
+ h.putSynonymMap(w, r, service, name)
+ case http.MethodGet:
+ sm, err := h.dp.GetSynonymMap(r.Context(), service, name)
+ writeDP(w, synonymMapJSON, sm, err)
+ case http.MethodDelete:
+ dpDelete(w, h.dp.DeleteSynonymMap(r.Context(), service, name))
+ default:
+ dpMethodNotAllowed(w)
+ }
+}
+
+func (h *DataPlaneHandler) putSynonymMap(w http.ResponseWriter, r *http.Request, service, name string) {
+ var body struct {
+ Name string `json:"name"`
+ Format string `json:"format"`
+ Synonyms string `json:"synonyms"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ if body.Name == "" {
+ body.Name = name
+ }
+
+ sm, err := h.dp.CreateOrUpdateSynonymMap(r.Context(), service, srchdriver.SynonymMap{
+ Name: body.Name, Format: body.Format, Synonyms: body.Synonyms,
+ })
+ writeDP(w, synonymMapJSON, sm, err)
+}
+
+// --- Aliases ---
+
+func aliasJSON(a *srchdriver.Alias) map[string]any {
+ return map[string]any{"name": a.Name, "indexes": a.Indexes, "@odata.etag": a.ETag}
+}
+
+//nolint:dupl // uniform data-plane collection CRUD; shape recurs across resources.
+func (h *DataPlaneHandler) serveAliases(w http.ResponseWriter, r *http.Request, service string, parts []string) {
+ if len(parts) == 1 {
+ switch r.Method {
+ case http.MethodGet:
+ al, err := h.dp.ListAliases(r.Context(), service)
+ writeDPList(w, aliasJSON, al, err)
+ case http.MethodPost:
+ h.putAlias(w, r, service, "")
+ default:
+ dpMethodNotAllowed(w)
+ }
+
+ return
+ }
+
+ name := parts[1]
+
+ switch r.Method {
+ case http.MethodPut:
+ h.putAlias(w, r, service, name)
+ case http.MethodGet:
+ al, err := h.dp.GetAlias(r.Context(), service, name)
+ writeDP(w, aliasJSON, al, err)
+ case http.MethodDelete:
+ dpDelete(w, h.dp.DeleteAlias(r.Context(), service, name))
+ default:
+ dpMethodNotAllowed(w)
+ }
+}
+
+func (h *DataPlaneHandler) putAlias(w http.ResponseWriter, r *http.Request, service, name string) {
+ var body struct {
+ Name string `json:"name"`
+ Indexes []string `json:"indexes"`
+ }
+
+ if !dpDecode(w, r, &body) {
+ return
+ }
+
+ if body.Name == "" {
+ body.Name = name
+ }
+
+ al, err := h.dp.CreateOrUpdateAlias(r.Context(), service, srchdriver.Alias{Name: body.Name, Indexes: body.Indexes})
+ writeDP(w, aliasJSON, al, err)
+}
+
+// --- Service statistics ---
+
+func (h *DataPlaneHandler) serveServiceStats(w http.ResponseWriter, r *http.Request, service string) {
+ if r.Method != http.MethodGet {
+ dpMethodNotAllowed(w)
+
+ return
+ }
+
+ st, err := h.dp.GetServiceStatistics(r.Context(), service)
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ dpJSON(w, map[string]any{
+ "counters": map[string]any{
+ "documentCount": map[string]any{"usage": st.DocumentCount},
+ "indexesCount": map[string]any{"usage": st.IndexCount},
+ "indexersCount": map[string]any{"usage": st.IndexerCount},
+ "dataSourcesCount": map[string]any{"usage": st.DataSourceCount},
+ "storageSize": map[string]any{"usage": st.StorageBytes},
+ },
+ })
+}
+
+// writeDPList writes a "value"-wrapped list or maps the error.
+func writeDPList[T any](w http.ResponseWriter, toJSON func(*T) map[string]any, items []T, err error) {
+ if err != nil {
+ dpCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(items))
+ for i := range items {
+ out = append(out, toJSON(&items[i]))
+ }
+
+ dpJSON(w, map[string]any{"value": out})
+}
diff --git a/server/azure/azuresearch/dataplane_roundtrip_test.go b/server/azure/azuresearch/dataplane_roundtrip_test.go
new file mode 100644
index 0000000..f47080a
--- /dev/null
+++ b/server/azure/azuresearch/dataplane_roundtrip_test.go
@@ -0,0 +1,230 @@
+package azuresearch_test
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// rawStatus posts body to url and returns the HTTP status code plus decoded body.
+func rawStatus(t *testing.T, method, url string, body any) (int, map[string]any) {
+ t.Helper()
+
+ b, err := json.Marshal(body)
+ require.NoError(t, err)
+
+ req, err := http.NewRequest(method, url, bytes.NewReader(b))
+ require.NoError(t, err)
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ var out map[string]any
+ _ = json.NewDecoder(resp.Body).Decode(&out)
+
+ return resp.StatusCode, out
+}
+
+func makeProductsIndex(t *testing.T, url string) {
+ t.Helper()
+
+ do(t, http.MethodPut, url+"/indexes/products", map[string]any{
+ "name": "products",
+ "fields": []any{
+ map[string]any{"name": "id", "type": "Edm.String", "key": true, "retrievable": true},
+ map[string]any{"name": "name", "type": "Edm.String", "searchable": true, "retrievable": true},
+ },
+ })
+}
+
+func TestMergeOrUploadPreservesFields(t *testing.T) {
+ url := newServer(t)
+ makeProductsIndex(t, url)
+
+ do(t, http.MethodPost, url+"/indexes/products/docs/index", map[string]any{
+ "value": []any{map[string]any{"@search.action": "upload", "id": "1", "name": "widget", "price": 9}},
+ })
+
+ // mergeOrUpload with only price must preserve the untouched name field.
+ do(t, http.MethodPost, url+"/indexes/products/docs/index", map[string]any{
+ "value": []any{map[string]any{"@search.action": "mergeOrUpload", "id": "1", "price": 12}},
+ })
+
+ doc := do(t, http.MethodGet, url+"/indexes/products/docs/1", nil)
+ assert.Equal(t, "widget", doc["name"])
+ assert.EqualValues(t, 12, doc["price"])
+}
+
+func TestSearchFilterTopSelect(t *testing.T) {
+ url := newServer(t)
+ makeProductsIndex(t, url)
+
+ do(t, http.MethodPost, url+"/indexes/products/docs/index", map[string]any{
+ "value": []any{
+ map[string]any{"@search.action": "upload", "id": "1", "name": "a", "price": 50},
+ map[string]any{"@search.action": "upload", "id": "2", "name": "b", "price": 150},
+ map[string]any{"@search.action": "upload", "id": "3", "name": "c", "price": 250},
+ },
+ })
+
+ // $filter must be honored, not ignored.
+ res := do(t, http.MethodPost, url+"/indexes/products/docs/search", map[string]any{
+ "search": "*", "filter": "price gt 100", "count": true, "orderby": "price asc",
+ })
+ require.Len(t, res["value"], 2)
+ assert.EqualValues(t, 2, res["@odata.count"])
+ assert.EqualValues(t, 150, res["value"].([]any)[0].(map[string]any)["price"])
+
+ // GET path honors $top/$select.
+ get := do(t, http.MethodGet, url+"/indexes/products/docs?search=*&$top=1&$select=id", nil)
+ require.Len(t, get["value"], 1)
+ first := get["value"].([]any)[0].(map[string]any)
+ _, hasName := first["name"]
+ assert.False(t, hasName, "$select=id should drop name")
+}
+
+func TestODataParenFormRouting(t *testing.T) {
+ url := newServer(t)
+ makeProductsIndex(t, url)
+
+ do(t, http.MethodPost, url+"/indexes/products/docs/index", map[string]any{
+ "value": []any{map[string]any{"@search.action": "upload", "id": "1", "name": "red widget"}},
+ })
+
+ // The fused OData key forms the real search SDKs emit must route identically
+ // to their slash-separated equivalents.
+ doc := do(t, http.MethodGet, url+"/indexes('products')/docs('1')", nil)
+ assert.Equal(t, "red widget", doc["name"])
+
+ res := do(t, http.MethodPost, url+"/indexes('products')/docs/search", map[string]any{"search": "*", "count": true})
+ assert.EqualValues(t, 1, res["@odata.count"])
+}
+
+func TestKeylessDocRejectedAndPartialBatch207(t *testing.T) {
+ url := newServer(t)
+ makeProductsIndex(t, url)
+
+ status, out := rawStatus(t, http.MethodPost, url+"/indexes/products/docs/index", map[string]any{
+ "value": []any{
+ map[string]any{"@search.action": "upload", "id": "1", "name": "ok"},
+ map[string]any{"@search.action": "upload", "name": "no-key"}, // missing key
+ map[string]any{"@search.action": "merge", "id": "99", "name": "x"}, // merge missing doc
+ },
+ })
+
+ assert.Equal(t, http.StatusMultiStatus, status, "partial failure must return 207")
+
+ results := out["value"].([]any)
+ require.Len(t, results, 3)
+ assert.Equal(t, true, results[0].(map[string]any)["status"])
+ assert.Equal(t, false, results[1].(map[string]any)["status"], "keyless doc must fail")
+ assert.Equal(t, false, results[2].(map[string]any)["status"], "merge of missing doc must 404")
+}
+
+func TestIndexAndDocumentSearch(t *testing.T) {
+ url := newServer(t)
+
+ // Create an index.
+ idx := do(t, http.MethodPut, url+"/indexes/products", map[string]any{
+ "name": "products",
+ "fields": []any{
+ map[string]any{"name": "id", "type": "Edm.String", "key": true, "retrievable": true},
+ map[string]any{"name": "name", "type": "Edm.String", "searchable": true, "retrievable": true},
+ },
+ })
+ assert.Equal(t, "products", idx["name"])
+
+ list := do(t, http.MethodGet, url+"/indexes", nil)
+ assert.Len(t, list["value"], 1)
+
+ // Upload documents.
+ up := do(t, http.MethodPost, url+"/indexes/products/docs/index", map[string]any{
+ "value": []any{
+ map[string]any{"@search.action": "upload", "id": "1", "name": "red widget"},
+ map[string]any{"@search.action": "upload", "id": "2", "name": "blue gadget"},
+ },
+ })
+ assert.Len(t, up["value"], 2)
+
+ // Count.
+ cnt := do(t, http.MethodGet, url+"/indexes/products/docs/$count", nil)
+ _ = cnt // count returns a bare number; just assert the call succeeded (200)
+
+ // Search with count.
+ res := do(t, http.MethodPost, url+"/indexes/products/docs/search", map[string]any{
+ "search": "widget", "count": true,
+ })
+ require.Len(t, res["value"], 1)
+ assert.EqualValues(t, 1, res["@odata.count"])
+ assert.Equal(t, "red widget", res["value"].([]any)[0].(map[string]any)["name"])
+
+ // Search all.
+ all := do(t, http.MethodPost, url+"/indexes/products/docs/search", map[string]any{"search": "*", "count": true})
+ assert.EqualValues(t, 2, all["@odata.count"])
+
+ // Get document by key.
+ doc := do(t, http.MethodGet, url+"/indexes/products/docs/2", nil)
+ assert.Equal(t, "blue gadget", doc["name"])
+
+ // Suggest / autocomplete.
+ sug := do(t, http.MethodPost, url+"/indexes/products/docs/suggest", map[string]any{"search": "blue", "suggesterName": "sg"})
+ assert.Len(t, sug["value"], 1)
+
+ // Delete a doc via merge action then verify count drops.
+ do(t, http.MethodPost, url+"/indexes/products/docs/index", map[string]any{
+ "value": []any{map[string]any{"@search.action": "delete", "id": "1"}},
+ })
+ after := do(t, http.MethodPost, url+"/indexes/products/docs/search", map[string]any{"search": "*", "count": true})
+ assert.EqualValues(t, 1, after["@odata.count"])
+}
+
+func TestIndexersDataSourcesSkillsetsSynonymsAliases(t *testing.T) {
+ url := newServer(t)
+ do(t, http.MethodPut, url+"/indexes/products", map[string]any{"name": "products"})
+
+ ds := do(t, http.MethodPut, url+"/datasources/blob1", map[string]any{
+ "name": "blob1", "type": "azureblob", "container": map[string]any{"name": "docs"},
+ "credentials": map[string]any{"connectionString": "x"},
+ })
+ assert.Equal(t, "blob1", ds["name"])
+
+ ss := do(t, http.MethodPut, url+"/skillsets/enrich", map[string]any{
+ "name": "enrich", "skills": []any{map[string]any{"x": 1}},
+ })
+ assert.Equal(t, "enrich", ss["name"])
+
+ ix := do(t, http.MethodPut, url+"/indexers/idxr", map[string]any{
+ "name": "idxr", "dataSourceName": "blob1", "targetIndexName": "products", "skillsetName": "enrich",
+ })
+ assert.Equal(t, "idxr", ix["name"])
+
+ do(t, http.MethodPost, url+"/indexers/idxr/run", map[string]any{})
+ st := do(t, http.MethodGet, url+"/indexers/idxr/status", nil)
+ assert.Equal(t, "running", st["status"])
+
+ sm := do(t, http.MethodPut, url+"/synonymmaps/syn", map[string]any{
+ "name": "syn", "format": "solr", "synonyms": "usa, united states",
+ })
+ assert.Equal(t, "syn", sm["name"])
+
+ al := do(t, http.MethodPut, url+"/aliases/prod-alias", map[string]any{
+ "name": "prod-alias", "indexes": []any{"products"},
+ })
+ assert.Equal(t, "prod-alias", al["name"])
+
+ assert.Len(t, do(t, http.MethodGet, url+"/indexers", nil)["value"], 1)
+ assert.Len(t, do(t, http.MethodGet, url+"/datasources", nil)["value"], 1)
+ assert.Len(t, do(t, http.MethodGet, url+"/skillsets", nil)["value"], 1)
+ assert.Len(t, do(t, http.MethodGet, url+"/synonymmaps", nil)["value"], 1)
+ assert.Len(t, do(t, http.MethodGet, url+"/aliases", nil)["value"], 1)
+
+ stats := do(t, http.MethodGet, url+"/servicestats", nil)
+ counters := stats["counters"].(map[string]any)
+ assert.EqualValues(t, 1, counters["indexesCount"].(map[string]any)["usage"])
+}
diff --git a/server/azure/azuresearch/handler.go b/server/azure/azuresearch/handler.go
new file mode 100644
index 0000000..a03fa8f
--- /dev/null
+++ b/server/azure/azuresearch/handler.go
@@ -0,0 +1,220 @@
+// Package azuresearch implements the Azure AI Search REST APIs as
+// server.Handlers: the Microsoft.Search ARM control plane (service lifecycle,
+// admin/query keys, private links) and the {service}.search.windows.net data
+// plane (indexes, documents, indexers, data sources, skillsets, synonym maps,
+// aliases, service statistics).
+//
+// Real armsearch clients (and the azsearch data-plane client) configured with a
+// custom endpoint hit these handlers the same way they hit management.azure.com
+// and {service}.search.windows.net.
+package azuresearch
+
+import (
+ "net/http"
+
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ srchdriver "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+const searchProvider = "Microsoft.Search"
+
+// Account sub-resource collections under a search service.
+const (
+ subAdminKeys = "listAdminKeys"
+ subRegenerate = "regenerateAdminKey"
+ subListQuery = "listQueryKeys"
+ subCreateQuery = "createQueryKey"
+ subDeleteQuery = "deleteQueryKey"
+ collSharedLink = "sharedPrivateLinkResources"
+ collPEC = "privateEndpointConnections"
+)
+
+// ControlHandler serves Microsoft.Search/searchServices ARM requests.
+type ControlHandler struct {
+ svc srchdriver.SearchControl
+}
+
+// NewControl returns a control-plane handler backed by svc.
+func NewControl(svc srchdriver.SearchControl) *ControlHandler {
+ return &ControlHandler{svc: svc}
+}
+
+// Matches claims Microsoft.Search/searchServices ARM paths.
+func (*ControlHandler) Matches(r *http.Request) bool {
+ rp, ok := azurearm.ParsePath(r.URL.Path)
+ if !ok {
+ return false
+ }
+
+ return rp.Provider == searchProvider && rp.ResourceType == "searchServices"
+}
+
+// ServeHTTP routes by path shape and method.
+func (h *ControlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ rp, ok := azurearm.ParsePath(r.URL.Path)
+ if !ok {
+ azurearm.WriteError(w, http.StatusBadRequest, "InvalidPath", "malformed ARM path")
+
+ return
+ }
+
+ switch {
+ case rp.ResourceName == "":
+ if r.Method != http.MethodGet {
+ writeMethodNotAllowed(w)
+
+ return
+ }
+
+ h.listServices(w, r, &rp)
+ case rp.SubResource == "":
+ h.serveService(w, r, &rp)
+ case rp.SubResource == collSharedLink:
+ h.serveSharedLink(w, r, &rp)
+ case rp.SubResource == collPEC:
+ h.servePEC(w, r, &rp)
+ default:
+ h.serveServiceAction(w, r, &rp)
+ }
+}
+
+func writeMethodNotAllowed(w http.ResponseWriter) {
+ azurearm.WriteError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+}
+
+func serviceJSON(s *srchdriver.Service) map[string]any {
+ return map[string]any{
+ "id": s.ID, "name": s.Name, "type": searchProvider + "/searchServices",
+ "location": s.Location, "tags": s.Tags,
+ "sku": map[string]any{"name": s.SKUName},
+ "properties": map[string]any{
+ "replicaCount": s.ReplicaCount, "partitionCount": s.PartitionCount,
+ "hostingMode": s.HostingMode, "status": s.Status,
+ "provisioningState": s.ProvisioningState,
+ "endpoint": s.Endpoint,
+ },
+ }
+}
+
+func (h *ControlHandler) serveService(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ switch r.Method {
+ case http.MethodPut:
+ h.createService(w, r, rp)
+ case http.MethodGet:
+ h.getService(w, r, rp)
+ case http.MethodPatch:
+ h.patchService(w, r, rp)
+ case http.MethodDelete:
+ h.deleteService(w, r, rp)
+ default:
+ writeMethodNotAllowed(w)
+ }
+}
+
+func (h *ControlHandler) createService(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ var body struct {
+ Location string `json:"location"`
+ SKU *struct {
+ Name string `json:"name"`
+ } `json:"sku"`
+ Tags map[string]string `json:"tags"`
+ Properties *struct {
+ ReplicaCount int `json:"replicaCount"`
+ PartitionCount int `json:"partitionCount"`
+ HostingMode string `json:"hostingMode"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ cfg := srchdriver.ServiceConfig{Name: rp.ResourceName, ResourceGroup: rp.ResourceGroup, Location: body.Location, Tags: body.Tags}
+ if body.SKU != nil {
+ cfg.SKUName = body.SKU.Name
+ }
+
+ if body.Properties != nil {
+ cfg.ReplicaCount = body.Properties.ReplicaCount
+ cfg.PartitionCount = body.Properties.PartitionCount
+ cfg.HostingMode = body.Properties.HostingMode
+ }
+
+ s, err := h.svc.CreateService(r.Context(), cfg)
+ writeRes(w, serviceJSON, s, err)
+}
+
+func (h *ControlHandler) getService(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ s, err := h.svc.GetService(r.Context(), rp.ResourceGroup, rp.ResourceName)
+ writeRes(w, serviceJSON, s, err)
+}
+
+func (h *ControlHandler) patchService(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ var body struct {
+ Tags map[string]string `json:"tags"`
+ Properties *struct {
+ ReplicaCount int `json:"replicaCount"`
+ PartitionCount int `json:"partitionCount"`
+ } `json:"properties"`
+ }
+
+ if !azurearm.DecodeJSON(w, r, &body) {
+ return
+ }
+
+ var replicas, partitions int
+ if body.Properties != nil {
+ replicas = body.Properties.ReplicaCount
+ partitions = body.Properties.PartitionCount
+ }
+
+ s, err := h.svc.UpdateService(r.Context(), rp.ResourceGroup, rp.ResourceName, replicas, partitions, body.Tags)
+ writeRes(w, serviceJSON, s, err)
+}
+
+func (h *ControlHandler) deleteService(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ if err := h.svc.DeleteService(r.Context(), rp.ResourceGroup, rp.ResourceName); err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{})
+}
+
+func (h *ControlHandler) listServices(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
+ var (
+ svcs []srchdriver.Service
+ err error
+ )
+
+ if rp.ResourceGroup == "" {
+ svcs, err = h.svc.ListServices(r.Context())
+ } else {
+ svcs, err = h.svc.ListServicesByResourceGroup(r.Context(), rp.ResourceGroup)
+ }
+
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ out := make([]map[string]any, 0, len(svcs))
+ for i := range svcs {
+ out = append(out, serviceJSON(&svcs[i]))
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, map[string]any{"value": out})
+}
+
+// writeRes writes a created/fetched resource or maps the error.
+func writeRes[T any](w http.ResponseWriter, toJSON func(*T) map[string]any, res *T, err error) {
+ if err != nil {
+ azurearm.WriteCErr(w, err)
+
+ return
+ }
+
+ azurearm.WriteJSON(w, http.StatusOK, toJSON(res))
+}
diff --git a/server/azure/azuresearch/sdk_roundtrip_test.go b/server/azure/azuresearch/sdk_roundtrip_test.go
new file mode 100644
index 0000000..68c5421
--- /dev/null
+++ b/server/azure/azuresearch/sdk_roundtrip_test.go
@@ -0,0 +1,271 @@
+package azuresearch_test
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/search/armsearch"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
+)
+
+// fakeCred is a no-op bearer-token credential for driving real ARM SDK clients.
+type fakeCred struct{}
+
+func (fakeCred) GetToken(_ context.Context, _ policy.TokenRequestOptions) (azcore.AccessToken, error) {
+ return azcore.AccessToken{Token: "fake", ExpiresOn: time.Now().Add(time.Hour)}, nil
+}
+
+func armClientOptions(ts *httptest.Server) *arm.ClientOptions {
+ return &arm.ClientOptions{
+ ClientOptions: azcore.ClientOptions{
+ Cloud: cloud.Configuration{
+ ActiveDirectoryAuthorityHost: "https://login.microsoftonline.com/",
+ Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
+ cloud.ResourceManager: {Endpoint: ts.URL, Audience: "https://management.azure.com"},
+ },
+ },
+ Transport: ts.Client(),
+ Retry: policy.RetryOptions{MaxRetries: -1},
+ },
+ }
+}
+
+const (
+ sub = "sub-1"
+ rg = "rg-1"
+ svcN = "mysearch"
+)
+
+func armBase() string {
+ return "/subscriptions/" + sub + "/resourceGroups/" + rg + "/providers/Microsoft.Search/searchServices"
+}
+
+func newServer(t *testing.T) string {
+ t.Helper()
+
+ cloud := cloudemu.NewAzure()
+ srv := azureserver.New(azureserver.Drivers{
+ SearchControl: cloud.AzureSearch,
+ SearchDataPlane: cloud.AzureSearch,
+ })
+ ts := httptest.NewServer(srv)
+ t.Cleanup(ts.Close)
+
+ return ts.URL
+}
+
+func do(t *testing.T, method, url string, body any) map[string]any {
+ t.Helper()
+
+ var rdr io.Reader
+
+ if body != nil {
+ b, err := json.Marshal(body)
+ require.NoError(t, err)
+ rdr = bytes.NewReader(b)
+ }
+
+ req, err := http.NewRequest(method, url, rdr)
+ require.NoError(t, err)
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ raw, _ := io.ReadAll(resp.Body)
+ require.Equalf(t, http.StatusOK, resp.StatusCode, "method=%s url=%s body=%s", method, url, raw)
+
+ out := map[string]any{}
+ if len(bytes.TrimSpace(raw)) > 0 && bytes.HasPrefix(bytes.TrimSpace(raw), []byte("{")) {
+ require.NoError(t, json.Unmarshal(raw, &out), "body=%s", raw)
+ }
+
+ return out
+}
+
+func props(m map[string]any) map[string]any {
+ p, _ := m["properties"].(map[string]any)
+
+ return p
+}
+
+func TestServiceLifecycleAndKeys(t *testing.T) {
+ url := newServer(t)
+
+ svc := do(t, http.MethodPut, url+armBase()+"/"+svcN, map[string]any{
+ "location": "eastus", "sku": map[string]any{"name": "standard"},
+ "properties": map[string]any{"replicaCount": 2, "partitionCount": 1},
+ })
+ assert.Equal(t, svcN, svc["name"])
+ assert.NotEmpty(t, props(svc)["endpoint"])
+
+ got := do(t, http.MethodGet, url+armBase()+"/"+svcN, nil)
+ assert.EqualValues(t, 2, props(got)["replicaCount"])
+
+ list := do(t, http.MethodGet, url+armBase(), nil)
+ assert.Len(t, list["value"], 1)
+
+ keys := do(t, http.MethodPost, url+armBase()+"/"+svcN+"/listAdminKeys", map[string]any{})
+ require.NotEmpty(t, keys["primaryKey"])
+
+ regen := do(t, http.MethodPost, url+armBase()+"/"+svcN+"/regenerateAdminKey/primary", map[string]any{})
+ assert.NotEqual(t, keys["primaryKey"], regen["primaryKey"])
+
+ qk := do(t, http.MethodPost, url+armBase()+"/"+svcN+"/createQueryKey/reader", map[string]any{})
+ require.NotEmpty(t, qk["key"])
+
+ qkeys := do(t, http.MethodPost, url+armBase()+"/"+svcN+"/listQueryKeys", map[string]any{})
+ assert.GreaterOrEqual(t, len(qkeys["value"].([]any)), 2)
+}
+
+func TestSharedPrivateLinkAndPEC(t *testing.T) {
+ url := newServer(t)
+ do(t, http.MethodPut, url+armBase()+"/"+svcN, map[string]any{"location": "eastus"})
+
+ spl := do(t, http.MethodPut, url+armBase()+"/"+svcN+"/sharedPrivateLinkResources/blob", map[string]any{
+ "properties": map[string]any{"groupId": "blob", "privateLinkResourceId": "/sub/x"},
+ })
+ assert.Equal(t, "blob", spl["name"])
+
+ pec := do(t, http.MethodPut, url+armBase()+"/"+svcN+"/privateEndpointConnections/pec1", map[string]any{
+ "properties": map[string]any{"privateLinkServiceConnectionState": map[string]any{"status": "Approved"}},
+ })
+ assert.Equal(t, "pec1", pec["name"])
+
+ assert.Len(t, do(t, http.MethodGet, url+armBase()+"/"+svcN+"/sharedPrivateLinkResources", nil)["value"], 1)
+ assert.Len(t, do(t, http.MethodGet, url+armBase()+"/"+svcN+"/privateEndpointConnections", nil)["value"], 1)
+}
+
+func TestAdminKeyRegenerationPersists(t *testing.T) {
+ url := newServer(t)
+ do(t, http.MethodPut, url+armBase()+"/"+svcN, map[string]any{"location": "eastus"})
+
+ orig := do(t, http.MethodPost, url+armBase()+"/"+svcN+"/listAdminKeys", map[string]any{})
+ regen := do(t, http.MethodPost, url+armBase()+"/"+svcN+"/regenerateAdminKey/primary", map[string]any{})
+ require.NotEqual(t, orig["primaryKey"], regen["primaryKey"])
+
+ // A subsequent list must return the rotated key, not the original.
+ after := do(t, http.MethodPost, url+armBase()+"/"+svcN+"/listAdminKeys", map[string]any{})
+ assert.Equal(t, regen["primaryKey"], after["primaryKey"])
+ assert.Equal(t, orig["secondaryKey"], after["secondaryKey"], "untouched key stays stable")
+}
+
+func TestKeyActionMethodEnforced(t *testing.T) {
+ url := newServer(t)
+ do(t, http.MethodPut, url+armBase()+"/"+svcN, map[string]any{"location": "eastus"})
+
+ // listAdminKeys requires POST; a GET must not leak keys.
+ req, _ := http.NewRequest(http.MethodGet, url+armBase()+"/"+svcN+"/listAdminKeys", nil)
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ resp.Body.Close()
+ assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
+
+ // deleteQueryKey requires DELETE; a stray GET must not delete.
+ req2, _ := http.NewRequest(http.MethodGet, url+armBase()+"/"+svcN+"/deleteQueryKey/somekey", nil)
+ resp2, err := http.DefaultClient.Do(req2)
+ require.NoError(t, err)
+ resp2.Body.Close()
+ assert.Equal(t, http.StatusMethodNotAllowed, resp2.StatusCode)
+}
+
+func TestServiceNotFound(t *testing.T) {
+ url := newServer(t)
+
+ req, _ := http.NewRequest(http.MethodGet, url+armBase()+"/ghost", nil)
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusNotFound, resp.StatusCode)
+}
+
+// --- real armsearch (Microsoft.Search control-plane) SDK roundtrip ---
+//
+// Azure ships no Go data-plane search SDK (no azsearch/azsearchindex module),
+// so wire compatibility for the {service}.search.windows.net data plane is
+// covered by the explicit paren-form tests in dataplane_roundtrip_test.go.
+
+func newSearchClientFactory(t *testing.T) *armsearch.ClientFactory {
+ t.Helper()
+
+ cloudP := cloudemu.NewAzure()
+ srv := azureserver.New(azureserver.Drivers{SearchControl: cloudP.AzureSearch})
+ ts := httptest.NewTLSServer(srv)
+ t.Cleanup(ts.Close)
+
+ cf, err := armsearch.NewClientFactory(sub, fakeCred{}, armClientOptions(ts))
+ require.NoError(t, err)
+
+ return cf
+}
+
+func TestSDKSearchServiceAndKeys(t *testing.T) {
+ cf := newSearchClientFactory(t)
+ ctx := context.Background()
+ services := cf.NewServicesClient()
+
+ poller, err := services.BeginCreateOrUpdate(ctx, rg, svcN, armsearch.Service{
+ Location: to.Ptr("eastus"),
+ SKU: &armsearch.SKU{Name: to.Ptr(armsearch.SKUNameStandard)},
+ }, nil, nil)
+ require.NoError(t, err)
+
+ created, err := poller.PollUntilDone(ctx, nil)
+ require.NoError(t, err)
+ require.NotNil(t, created.Name)
+ assert.Equal(t, svcN, *created.Name)
+
+ got, err := services.Get(ctx, rg, svcN, nil, nil)
+ require.NoError(t, err)
+ assert.Equal(t, "eastus", *got.Location)
+
+ // Admin-key rotation must be observable via the real client.
+ admin := cf.NewAdminKeysClient()
+
+ orig, err := admin.Get(ctx, rg, svcN, nil, nil)
+ require.NoError(t, err)
+ require.NotNil(t, orig.PrimaryKey)
+
+ regen, err := admin.Regenerate(ctx, rg, svcN, armsearch.AdminKeyKindPrimary, nil, nil)
+ require.NoError(t, err)
+ assert.NotEqual(t, *orig.PrimaryKey, *regen.PrimaryKey)
+
+ after, err := admin.Get(ctx, rg, svcN, nil, nil)
+ require.NoError(t, err)
+ assert.Equal(t, *regen.PrimaryKey, *after.PrimaryKey, "rotation must persist")
+
+ // Query keys create + list.
+ query := cf.NewQueryKeysClient()
+
+ qk, err := query.Create(ctx, rg, svcN, "reader", nil, nil)
+ require.NoError(t, err)
+ require.NotNil(t, qk.Key)
+
+ qpage, err := query.NewListBySearchServicePager(rg, svcN, nil, nil).NextPage(ctx)
+ require.NoError(t, err)
+ assert.GreaterOrEqual(t, len(qpage.Value), 2)
+
+ page, err := services.NewListByResourceGroupPager(rg, nil, nil).NextPage(ctx)
+ require.NoError(t, err)
+ assert.Len(t, page.Value, 1)
+
+ _, err = services.Delete(ctx, rg, svcN, nil, nil)
+ require.NoError(t, err)
+}
diff --git a/server/azure/azuresql/handler.go b/server/azure/azuresql/handler.go
index 2474d4c..27f207b 100644
--- a/server/azure/azuresql/handler.go
+++ b/server/azure/azuresql/handler.go
@@ -23,8 +23,8 @@ package azuresql
import (
"net/http"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
const (
diff --git a/server/azure/azuresql/operations.go b/server/azure/azuresql/operations.go
index d8d6ba7..2d31475 100644
--- a/server/azure/azuresql/operations.go
+++ b/server/azure/azuresql/operations.go
@@ -3,8 +3,8 @@ package azuresql
import (
"net/http"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// ---- Server (logical) ops ----
diff --git a/server/azure/azuresql/sdk_roundtrip_test.go b/server/azure/azuresql/sdk_roundtrip_test.go
index 55b6c6c..3fcc236 100644
--- a/server/azure/azuresql/sdk_roundtrip_test.go
+++ b/server/azure/azuresql/sdk_roundtrip_test.go
@@ -13,8 +13,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
type fakeCred struct{}
diff --git a/server/azure/azuresql/types.go b/server/azure/azuresql/types.go
index 0732d14..a400d9e 100644
--- a/server/azure/azuresql/types.go
+++ b/server/azure/azuresql/types.go
@@ -1,7 +1,7 @@
package azuresql
import (
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// Azure SQL Database.status enum values used in ARM responses.
diff --git a/server/azure/blob/blob_test.go b/server/azure/blob/blob_test.go
index caea423..8fa997d 100644
--- a/server/azure/blob/blob_test.go
+++ b/server/azure/blob/blob_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/service"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
// TestSDKBlobRoundTrip drives Azure Blob storage operations with the real
diff --git a/server/azure/blob/handler.go b/server/azure/blob/handler.go
index 95be7a3..b249ef1 100644
--- a/server/azure/blob/handler.go
+++ b/server/azure/blob/handler.go
@@ -29,8 +29,8 @@ import (
"strings"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- storagedriver "github.com/stackshy/cloudemu/storage/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ storagedriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
const (
diff --git a/server/azure/cache/handler.go b/server/azure/cache/handler.go
index 6bcd040..e6afa39 100644
--- a/server/azure/cache/handler.go
+++ b/server/azure/cache/handler.go
@@ -26,8 +26,8 @@ package cache
import (
"net/http"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
const (
diff --git a/server/azure/cache/operations.go b/server/azure/cache/operations.go
index e32e58f..932c9d9 100644
--- a/server/azure/cache/operations.go
+++ b/server/azure/cache/operations.go
@@ -4,8 +4,8 @@ import (
"net/http"
"strconv"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
// createOrUpdateCache handles PUT — Redis.BeginCreate. The LRO completes inline:
diff --git a/server/azure/cache/sdk_roundtrip_test.go b/server/azure/cache/sdk_roundtrip_test.go
index cf43490..2c40d3f 100644
--- a/server/azure/cache/sdk_roundtrip_test.go
+++ b/server/azure/cache/sdk_roundtrip_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v3"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
const (
diff --git a/server/azure/cache/types.go b/server/azure/cache/types.go
index d05e8a8..3b672c7 100644
--- a/server/azure/cache/types.go
+++ b/server/azure/cache/types.go
@@ -4,8 +4,8 @@ import (
"strconv"
"strings"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
const (
diff --git a/server/azure/cosmos/cosmos_test.go b/server/azure/cosmos/cosmos_test.go
index 764d157..246ebf6 100644
--- a/server/azure/cosmos/cosmos_test.go
+++ b/server/azure/cosmos/cosmos_test.go
@@ -12,8 +12,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
// fakeKey is the dummy master key our test client uses. The handler ignores
diff --git a/server/azure/cosmos/handler.go b/server/azure/cosmos/handler.go
index 177224e..436af03 100644
--- a/server/azure/cosmos/handler.go
+++ b/server/azure/cosmos/handler.go
@@ -24,8 +24,8 @@ import (
"strings"
"time"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
)
const (
diff --git a/server/azure/databricks/dataplane.go b/server/azure/databricks/dataplane.go
index cdc525a..66a5ffb 100644
--- a/server/azure/databricks/dataplane.go
+++ b/server/azure/databricks/dataplane.go
@@ -18,8 +18,8 @@ import (
"net/http"
"strings"
- dbxdriver "github.com/stackshy/cloudemu/databricks/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ dbxdriver "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
const dpMaxBodyBytes = 5 << 20
diff --git a/server/azure/databricks/dataplane_more_ops.go b/server/azure/databricks/dataplane_more_ops.go
index b369c4c..f559343 100644
--- a/server/azure/databricks/dataplane_more_ops.go
+++ b/server/azure/databricks/dataplane_more_ops.go
@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
- dbxdriver "github.com/stackshy/cloudemu/databricks/driver"
+ dbxdriver "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
// --- wire types: runs ---
diff --git a/server/azure/databricks/dataplane_ops.go b/server/azure/databricks/dataplane_ops.go
index f83f5b3..c120ded 100644
--- a/server/azure/databricks/dataplane_ops.go
+++ b/server/azure/databricks/dataplane_ops.go
@@ -6,7 +6,7 @@ import (
"net/http"
"strconv"
- dbxdriver "github.com/stackshy/cloudemu/databricks/driver"
+ dbxdriver "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
// --- wire types ---
diff --git a/server/azure/databricks/dataplane_roundtrip_test.go b/server/azure/databricks/dataplane_roundtrip_test.go
index b5b1972..f903da2 100644
--- a/server/azure/databricks/dataplane_roundtrip_test.go
+++ b/server/azure/databricks/dataplane_roundtrip_test.go
@@ -11,8 +11,8 @@ import (
"github.com/databricks/databricks-sdk-go/service/iam"
"github.com/databricks/databricks-sdk-go/service/jobs"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
func newWorkspace(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/dbfs/dbfs_roundtrip_test.go b/server/azure/databricks/dbfs/dbfs_roundtrip_test.go
index a9c6311..4be3637 100644
--- a/server/azure/databricks/dbfs/dbfs_roundtrip_test.go
+++ b/server/azure/databricks/dbfs/dbfs_roundtrip_test.go
@@ -11,8 +11,8 @@ import (
"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/files"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/dbfs"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/dbfs"
)
func newWorkspace(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/gitcredentials/gitcredentials_roundtrip_test.go b/server/azure/databricks/gitcredentials/gitcredentials_roundtrip_test.go
index 5b03878..007b39c 100644
--- a/server/azure/databricks/gitcredentials/gitcredentials_roundtrip_test.go
+++ b/server/azure/databricks/gitcredentials/gitcredentials_roundtrip_test.go
@@ -9,8 +9,8 @@ import (
"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/workspace"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/gitcredentials"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/gitcredentials"
)
func newWorkspace(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/handler.go b/server/azure/databricks/handler.go
index 68d434c..e0cb53a 100644
--- a/server/azure/databricks/handler.go
+++ b/server/azure/databricks/handler.go
@@ -20,8 +20,8 @@ package databricks
import (
"net/http"
- dbxdriver "github.com/stackshy/cloudemu/databricks/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ dbxdriver "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
const (
diff --git a/server/azure/databricks/hostmeta/hostmeta_roundtrip_test.go b/server/azure/databricks/hostmeta/hostmeta_roundtrip_test.go
index 29068a0..b3763d2 100644
--- a/server/azure/databricks/hostmeta/hostmeta_roundtrip_test.go
+++ b/server/azure/databricks/hostmeta/hostmeta_roundtrip_test.go
@@ -9,8 +9,8 @@ import (
"github.com/databricks/databricks-sdk-go/common/environment"
"github.com/databricks/databricks-sdk-go/config"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/hostmeta"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/hostmeta"
)
func newServer(t *testing.T) *httptest.Server {
diff --git a/server/azure/databricks/operations.go b/server/azure/databricks/operations.go
index 1db992a..149ec1f 100644
--- a/server/azure/databricks/operations.go
+++ b/server/azure/databricks/operations.go
@@ -3,8 +3,8 @@ package databricks
import (
"net/http"
- dbxdriver "github.com/stackshy/cloudemu/databricks/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ dbxdriver "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
func (h *Handler) createOrUpdateWorkspace(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
diff --git a/server/azure/databricks/pipelines/pipelines_roundtrip_test.go b/server/azure/databricks/pipelines/pipelines_roundtrip_test.go
index 5432ba7..0cd03a5 100644
--- a/server/azure/databricks/pipelines/pipelines_roundtrip_test.go
+++ b/server/azure/databricks/pipelines/pipelines_roundtrip_test.go
@@ -9,8 +9,8 @@ import (
"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/pipelines"
- "github.com/stackshy/cloudemu/server"
- pipelineshandler "github.com/stackshy/cloudemu/server/azure/databricks/pipelines"
+ "github.com/stackshy/cloudemu/v2/server"
+ pipelineshandler "github.com/stackshy/cloudemu/v2/server/azure/databricks/pipelines"
)
func newPipelineClient(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/queryhistory/queryhistory_roundtrip_test.go b/server/azure/databricks/queryhistory/queryhistory_roundtrip_test.go
index cb4d622..4f535c6 100644
--- a/server/azure/databricks/queryhistory/queryhistory_roundtrip_test.go
+++ b/server/azure/databricks/queryhistory/queryhistory_roundtrip_test.go
@@ -9,8 +9,8 @@ import (
"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/sql"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/queryhistory"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/queryhistory"
)
func newClient(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/repos/repos_roundtrip_test.go b/server/azure/databricks/repos/repos_roundtrip_test.go
index db68870..7daa9f6 100644
--- a/server/azure/databricks/repos/repos_roundtrip_test.go
+++ b/server/azure/databricks/repos/repos_roundtrip_test.go
@@ -11,8 +11,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/repos"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/repos"
)
func newWorkspace(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/scim/scim_roundtrip_test.go b/server/azure/databricks/scim/scim_roundtrip_test.go
index ca8bf42..488643f 100644
--- a/server/azure/databricks/scim/scim_roundtrip_test.go
+++ b/server/azure/databricks/scim/scim_roundtrip_test.go
@@ -11,8 +11,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/scim"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/scim"
)
func newWorkspace(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/sdk_roundtrip_test.go b/server/azure/databricks/sdk_roundtrip_test.go
index 02c6367..6b4e69e 100644
--- a/server/azure/databricks/sdk_roundtrip_test.go
+++ b/server/azure/databricks/sdk_roundtrip_test.go
@@ -13,8 +13,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/databricks/armdatabricks"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
const (
diff --git a/server/azure/databricks/secrets/secrets_roundtrip_test.go b/server/azure/databricks/secrets/secrets_roundtrip_test.go
index 9508c5d..62cfcd0 100644
--- a/server/azure/databricks/secrets/secrets_roundtrip_test.go
+++ b/server/azure/databricks/secrets/secrets_roundtrip_test.go
@@ -9,8 +9,8 @@ import (
"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/workspace"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/secrets"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/secrets"
)
func newWorkspace(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/serving/serving_roundtrip_test.go b/server/azure/databricks/serving/serving_roundtrip_test.go
index dc5a2c6..cade681 100644
--- a/server/azure/databricks/serving/serving_roundtrip_test.go
+++ b/server/azure/databricks/serving/serving_roundtrip_test.go
@@ -9,8 +9,8 @@ import (
"github.com/databricks/databricks-sdk-go/config"
dbserving "github.com/databricks/databricks-sdk-go/service/serving"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/serving"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/serving"
)
func newServingClient(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/sqlwarehouses/sqlwarehouses_roundtrip_test.go b/server/azure/databricks/sqlwarehouses/sqlwarehouses_roundtrip_test.go
index 0ba9e4c..3f0736a 100644
--- a/server/azure/databricks/sqlwarehouses/sqlwarehouses_roundtrip_test.go
+++ b/server/azure/databricks/sqlwarehouses/sqlwarehouses_roundtrip_test.go
@@ -9,8 +9,8 @@ import (
"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/sql"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/sqlwarehouses"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/sqlwarehouses"
)
func newWarehouseClient(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/token/token_roundtrip_test.go b/server/azure/databricks/token/token_roundtrip_test.go
index 47c0b85..047f419 100644
--- a/server/azure/databricks/token/token_roundtrip_test.go
+++ b/server/azure/databricks/token/token_roundtrip_test.go
@@ -9,8 +9,8 @@ import (
"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/settings"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/token"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/token"
)
func newWorkspace(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/types.go b/server/azure/databricks/types.go
index abc5a3d..dca3025 100644
--- a/server/azure/databricks/types.go
+++ b/server/azure/databricks/types.go
@@ -1,6 +1,6 @@
package databricks
-import dbxdriver "github.com/stackshy/cloudemu/databricks/driver"
+import dbxdriver "github.com/stackshy/cloudemu/v2/services/databricks/driver"
// JSON wire shapes for the Microsoft.Databricks ARM REST API. Field names match
// what the real armdatabricks client emits and expects.
diff --git a/server/azure/databricks/ucstorage/ucstorage_roundtrip_test.go b/server/azure/databricks/ucstorage/ucstorage_roundtrip_test.go
index ff481af..cb60a6a 100644
--- a/server/azure/databricks/ucstorage/ucstorage_roundtrip_test.go
+++ b/server/azure/databricks/ucstorage/ucstorage_roundtrip_test.go
@@ -11,8 +11,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/ucstorage"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/ucstorage"
)
func newWorkspace(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/unitycatalog/unitycatalog_roundtrip_test.go b/server/azure/databricks/unitycatalog/unitycatalog_roundtrip_test.go
index 818e37a..b1c0134 100644
--- a/server/azure/databricks/unitycatalog/unitycatalog_roundtrip_test.go
+++ b/server/azure/databricks/unitycatalog/unitycatalog_roundtrip_test.go
@@ -11,8 +11,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/unitycatalog"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/unitycatalog"
)
func newWorkspace(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/databricks/wsfs/wsfs_roundtrip_test.go b/server/azure/databricks/wsfs/wsfs_roundtrip_test.go
index e8f32a6..44b975f 100644
--- a/server/azure/databricks/wsfs/wsfs_roundtrip_test.go
+++ b/server/azure/databricks/wsfs/wsfs_roundtrip_test.go
@@ -10,8 +10,8 @@ import (
"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/workspace"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/azure/databricks/wsfs"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/azure/databricks/wsfs"
)
func newWorkspace(t *testing.T) *databricks.WorkspaceClient {
diff --git a/server/azure/disks/disks_test.go b/server/azure/disks/disks_test.go
index cc1f7a1..51ee563 100644
--- a/server/azure/disks/disks_test.go
+++ b/server/azure/disks/disks_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
type fakeCred struct{}
diff --git a/server/azure/disks/handler.go b/server/azure/disks/handler.go
index a229bec..97b7a08 100644
--- a/server/azure/disks/handler.go
+++ b/server/azure/disks/handler.go
@@ -13,9 +13,9 @@ import (
"context"
"net/http"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
const (
diff --git a/server/azure/dns/handler.go b/server/azure/dns/handler.go
index 355e9a6..acb33a3 100644
--- a/server/azure/dns/handler.go
+++ b/server/azure/dns/handler.go
@@ -33,8 +33,8 @@ import (
"net/http"
"strings"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
const (
diff --git a/server/azure/dns/operations.go b/server/azure/dns/operations.go
index 1385ffa..03d78c3 100644
--- a/server/azure/dns/operations.go
+++ b/server/azure/dns/operations.go
@@ -3,9 +3,9 @@ package dns
import (
"net/http"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
// --- zones ---
diff --git a/server/azure/dns/sdk_roundtrip_test.go b/server/azure/dns/sdk_roundtrip_test.go
index 4231491..98821ac 100644
--- a/server/azure/dns/sdk_roundtrip_test.go
+++ b/server/azure/dns/sdk_roundtrip_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
const (
diff --git a/server/azure/dns/types.go b/server/azure/dns/types.go
index 34d16bc..bdeb892 100644
--- a/server/azure/dns/types.go
+++ b/server/azure/dns/types.go
@@ -4,9 +4,9 @@ import (
"context"
"strings"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
// ARM resource type strings stamped on responses.
diff --git a/server/azure/eventgrid/handler.go b/server/azure/eventgrid/handler.go
index f299032..2c52099 100644
--- a/server/azure/eventgrid/handler.go
+++ b/server/azure/eventgrid/handler.go
@@ -27,8 +27,8 @@ package eventgrid
import (
"net/http"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
const (
diff --git a/server/azure/eventgrid/operations.go b/server/azure/eventgrid/operations.go
index 0b0a0d8..4e102c5 100644
--- a/server/azure/eventgrid/operations.go
+++ b/server/azure/eventgrid/operations.go
@@ -3,8 +3,8 @@ package eventgrid
import (
"net/http"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
func (h *Handler) createOrUpdateTopic(w http.ResponseWriter, r *http.Request, rp *azurearm.ResourcePath) {
diff --git a/server/azure/eventgrid/sdk_roundtrip_test.go b/server/azure/eventgrid/sdk_roundtrip_test.go
index f124e8d..a21816e 100644
--- a/server/azure/eventgrid/sdk_roundtrip_test.go
+++ b/server/azure/eventgrid/sdk_roundtrip_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventgrid/armeventgrid/v2"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
const (
diff --git a/server/azure/eventgrid/types.go b/server/azure/eventgrid/types.go
index 64bcc8b..ef737f1 100644
--- a/server/azure/eventgrid/types.go
+++ b/server/azure/eventgrid/types.go
@@ -1,8 +1,8 @@
package eventgrid
import (
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
const (
diff --git a/server/azure/functions/functions_test.go b/server/azure/functions/functions_test.go
index d50a8c7..5096fbe 100644
--- a/server/azure/functions/functions_test.go
+++ b/server/azure/functions/functions_test.go
@@ -9,8 +9,8 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
const (
@@ -267,4 +267,3 @@ func readBody(t *testing.T, resp *http.Response) string {
return strings.TrimSpace(string(b))
}
-
diff --git a/server/azure/functions/handler.go b/server/azure/functions/handler.go
index 1c9308c..f7b2a1d 100644
--- a/server/azure/functions/handler.go
+++ b/server/azure/functions/handler.go
@@ -23,9 +23,9 @@ import (
"strings"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
- sdrv "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ sdrv "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
const (
diff --git a/server/azure/functions/sdk_roundtrip_test.go b/server/azure/functions/sdk_roundtrip_test.go
index 81e80b0..2b3e776 100644
--- a/server/azure/functions/sdk_roundtrip_test.go
+++ b/server/azure/functions/sdk_roundtrip_test.go
@@ -13,8 +13,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v3"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
// fakeCred is a static-token credential for tests. The handler ignores the
diff --git a/server/azure/iam/handler.go b/server/azure/iam/handler.go
index c7dad60..e029c3d 100644
--- a/server/azure/iam/handler.go
+++ b/server/azure/iam/handler.go
@@ -28,7 +28,7 @@ import (
"net/http"
"strings"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
const (
diff --git a/server/azure/iam/operations.go b/server/azure/iam/operations.go
index 32297f0..a253d49 100644
--- a/server/azure/iam/operations.go
+++ b/server/azure/iam/operations.go
@@ -7,8 +7,8 @@ import (
"strings"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
const maxBodyBytes = 1 << 20
diff --git a/server/azure/iam/sdk_roundtrip_test.go b/server/azure/iam/sdk_roundtrip_test.go
index ea1defe..f164163 100644
--- a/server/azure/iam/sdk_roundtrip_test.go
+++ b/server/azure/iam/sdk_roundtrip_test.go
@@ -13,8 +13,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
type fakeCred struct{}
diff --git a/server/azure/images/handler.go b/server/azure/images/handler.go
index 55f51d6..60f7d4a 100644
--- a/server/azure/images/handler.go
+++ b/server/azure/images/handler.go
@@ -6,9 +6,9 @@ import (
"net/http"
"strings"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
const (
diff --git a/server/azure/images/images_test.go b/server/azure/images/images_test.go
index fc4b9ef..ef96c95 100644
--- a/server/azure/images/images_test.go
+++ b/server/azure/images/images_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
type fakeCred struct{}
diff --git a/server/azure/keyvault/handler.go b/server/azure/keyvault/handler.go
index a1ca8de..e707e2c 100644
--- a/server/azure/keyvault/handler.go
+++ b/server/azure/keyvault/handler.go
@@ -26,8 +26,8 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
const pathPrefix = "/secrets"
diff --git a/server/azure/keyvault/operations.go b/server/azure/keyvault/operations.go
index ce3e75e..bd1eec3 100644
--- a/server/azure/keyvault/operations.go
+++ b/server/azure/keyvault/operations.go
@@ -3,9 +3,9 @@ package keyvault
import (
"net/http"
- cerrors "github.com/stackshy/cloudemu/errors"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
- "github.com/stackshy/cloudemu/server/wire"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
// setSecret creates the secret on first PUT and adds a new version on
diff --git a/server/azure/keyvault/sdk_roundtrip_test.go b/server/azure/keyvault/sdk_roundtrip_test.go
index 3904311..ab16453 100644
--- a/server/azure/keyvault/sdk_roundtrip_test.go
+++ b/server/azure/keyvault/sdk_roundtrip_test.go
@@ -12,8 +12,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
type fakeCred struct{}
diff --git a/server/azure/keyvault/types.go b/server/azure/keyvault/types.go
index 7ee965c..9608165 100644
--- a/server/azure/keyvault/types.go
+++ b/server/azure/keyvault/types.go
@@ -4,7 +4,7 @@ import (
"net/http"
"time"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
type attributesJSON struct {
diff --git a/server/azure/loadbalancer/handler.go b/server/azure/loadbalancer/handler.go
index c507961..04261c6 100644
--- a/server/azure/loadbalancer/handler.go
+++ b/server/azure/loadbalancer/handler.go
@@ -40,8 +40,8 @@ import (
"net/http"
"strings"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
// Handler serves Microsoft.Network/loadBalancers ARM requests against a
diff --git a/server/azure/loadbalancer/operations.go b/server/azure/loadbalancer/operations.go
index b72aab2..9a7011d 100644
--- a/server/azure/loadbalancer/operations.go
+++ b/server/azure/loadbalancer/operations.go
@@ -6,9 +6,9 @@ import (
"strconv"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
// Internal tags scope a driver target group to its Azure parent load balancer
diff --git a/server/azure/loadbalancer/sdk_roundtrip_test.go b/server/azure/loadbalancer/sdk_roundtrip_test.go
index 319e72e..54dce89 100644
--- a/server/azure/loadbalancer/sdk_roundtrip_test.go
+++ b/server/azure/loadbalancer/sdk_roundtrip_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
const (
diff --git a/server/azure/loganalytics/handler.go b/server/azure/loganalytics/handler.go
index 046e252..2a88b1e 100644
--- a/server/azure/loganalytics/handler.go
+++ b/server/azure/loganalytics/handler.go
@@ -30,8 +30,8 @@ import (
"net/http"
"strings"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
const (
diff --git a/server/azure/loganalytics/operations.go b/server/azure/loganalytics/operations.go
index 761d147..19562c8 100644
--- a/server/azure/loganalytics/operations.go
+++ b/server/azure/loganalytics/operations.go
@@ -3,8 +3,8 @@ package loganalytics
import (
"net/http"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
// createOrUpdateWorkspace maps Workspaces.CreateOrUpdate onto the logging
diff --git a/server/azure/loganalytics/sdk_roundtrip_test.go b/server/azure/loganalytics/sdk_roundtrip_test.go
index 7a30b64..e36a953 100644
--- a/server/azure/loganalytics/sdk_roundtrip_test.go
+++ b/server/azure/loganalytics/sdk_roundtrip_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
// NOTE: This slice covers the Log Analytics workspace lifecycle
diff --git a/server/azure/loganalytics/types.go b/server/azure/loganalytics/types.go
index fa48d49..2febaa9 100644
--- a/server/azure/loganalytics/types.go
+++ b/server/azure/loganalytics/types.go
@@ -1,8 +1,8 @@
package loganalytics
import (
- logdriver "github.com/stackshy/cloudemu/logging/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
// provisioningSucceeded is the terminal provisioning state. The azcore body
diff --git a/server/azure/monitor/handler.go b/server/azure/monitor/handler.go
index 6997f37..07046ae 100644
--- a/server/azure/monitor/handler.go
+++ b/server/azure/monitor/handler.go
@@ -7,9 +7,9 @@ import (
"context"
"net/http"
- cerrors "github.com/stackshy/cloudemu/errors"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/server/azure/monitor/monitor_test.go b/server/azure/monitor/monitor_test.go
index 1f84535..ee85f7f 100644
--- a/server/azure/monitor/monitor_test.go
+++ b/server/azure/monitor/monitor_test.go
@@ -7,8 +7,8 @@ import (
"net/http/httptest"
"testing"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
// HTTP-level tests for the Azure Monitor handler. The real armmonitor SDK
diff --git a/server/azure/mysqlflex/handler.go b/server/azure/mysqlflex/handler.go
index a992d9e..98f4a05 100644
--- a/server/azure/mysqlflex/handler.go
+++ b/server/azure/mysqlflex/handler.go
@@ -22,8 +22,8 @@ package mysqlflex
import (
"net/http"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
const (
diff --git a/server/azure/mysqlflex/operations.go b/server/azure/mysqlflex/operations.go
index 0771da3..4c3c138 100644
--- a/server/azure/mysqlflex/operations.go
+++ b/server/azure/mysqlflex/operations.go
@@ -3,8 +3,8 @@ package mysqlflex
import (
"net/http"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// ---- Server lifecycle ----
diff --git a/server/azure/mysqlflex/sdk_roundtrip_test.go b/server/azure/mysqlflex/sdk_roundtrip_test.go
index 3f5d89b..57952db 100644
--- a/server/azure/mysqlflex/sdk_roundtrip_test.go
+++ b/server/azure/mysqlflex/sdk_roundtrip_test.go
@@ -13,8 +13,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
type fakeCred struct{}
diff --git a/server/azure/mysqlflex/types.go b/server/azure/mysqlflex/types.go
index 1cbebe7..25bb839 100644
--- a/server/azure/mysqlflex/types.go
+++ b/server/azure/mysqlflex/types.go
@@ -1,7 +1,7 @@
package mysqlflex
import (
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// MySQL Flexible Server state enum values surfaced in ARM responses.
diff --git a/server/azure/network/handler.go b/server/azure/network/handler.go
index 47a5076..703b0fd 100644
--- a/server/azure/network/handler.go
+++ b/server/azure/network/handler.go
@@ -19,9 +19,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
const (
diff --git a/server/azure/network/network_test.go b/server/azure/network/network_test.go
index ff99a9c..bd13b17 100644
--- a/server/azure/network/network_test.go
+++ b/server/azure/network/network_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
type fakeCred struct{}
diff --git a/server/azure/notificationhubs/handler.go b/server/azure/notificationhubs/handler.go
index d283d7d..7a5be41 100644
--- a/server/azure/notificationhubs/handler.go
+++ b/server/azure/notificationhubs/handler.go
@@ -30,8 +30,8 @@ import (
"net/http"
"strings"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
const (
diff --git a/server/azure/notificationhubs/operations.go b/server/azure/notificationhubs/operations.go
index a82d43a..3f08a8f 100644
--- a/server/azure/notificationhubs/operations.go
+++ b/server/azure/notificationhubs/operations.go
@@ -4,8 +4,8 @@ import (
"net/http"
"strings"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
// --- namespaces ---
diff --git a/server/azure/notificationhubs/sdk_roundtrip_test.go b/server/azure/notificationhubs/sdk_roundtrip_test.go
index 348d8fa..92a36f1 100644
--- a/server/azure/notificationhubs/sdk_roundtrip_test.go
+++ b/server/azure/notificationhubs/sdk_roundtrip_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/notificationhubs/armnotificationhubs"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
const (
diff --git a/server/azure/notificationhubs/types.go b/server/azure/notificationhubs/types.go
index 3ea49eb..f0a38a1 100644
--- a/server/azure/notificationhubs/types.go
+++ b/server/azure/notificationhubs/types.go
@@ -1,8 +1,8 @@
package notificationhubs
import (
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
// ARM resource type strings stamped on responses.
diff --git a/server/azure/postgresflex/handler.go b/server/azure/postgresflex/handler.go
index b54ec69..afc10a5 100644
--- a/server/azure/postgresflex/handler.go
+++ b/server/azure/postgresflex/handler.go
@@ -23,8 +23,8 @@ package postgresflex
import (
"net/http"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
const (
diff --git a/server/azure/postgresflex/operations.go b/server/azure/postgresflex/operations.go
index c5b7185..26ab654 100644
--- a/server/azure/postgresflex/operations.go
+++ b/server/azure/postgresflex/operations.go
@@ -3,8 +3,8 @@ package postgresflex
import (
"net/http"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// instanceFromBody decodes a Postgres Flex create/update body and converts it
diff --git a/server/azure/postgresflex/sdk_roundtrip_test.go b/server/azure/postgresflex/sdk_roundtrip_test.go
index 426be64..63ed1f9 100644
--- a/server/azure/postgresflex/sdk_roundtrip_test.go
+++ b/server/azure/postgresflex/sdk_roundtrip_test.go
@@ -13,8 +13,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
type fakeCred struct{}
diff --git a/server/azure/postgresflex/types.go b/server/azure/postgresflex/types.go
index 7d140ac..d0c6904 100644
--- a/server/azure/postgresflex/types.go
+++ b/server/azure/postgresflex/types.go
@@ -1,8 +1,8 @@
package postgresflex
import (
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// Postgres Flex ServerState enum values used in ARM responses. Real Azure
diff --git a/server/azure/queue/dispatch_test.go b/server/azure/queue/dispatch_test.go
new file mode 100644
index 0000000..484a279
--- /dev/null
+++ b/server/azure/queue/dispatch_test.go
@@ -0,0 +1,96 @@
+package queue_test
+
+import (
+ "context"
+ "encoding/json"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/data/aztables"
+ "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+ "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue"
+
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
+)
+
+// TestNoShadowing registers Blob, Queue and Table on the same server and
+// confirms each service's SDK routes to its own handler — i.e. the permissive
+// Blob fallback does not swallow Queue or Table requests, and Queue and Table
+// do not claim each other's or Blob's paths.
+func TestNoShadowing(t *testing.T) {
+ cloudP := cloudemu.NewAzure()
+ srv := azureserver.New(azureserver.Drivers{
+ BlobStorage: cloudP.BlobStorage,
+ QueueStorage: cloudP.QueueStorage,
+ TableStorage: cloudP.TableStorage,
+ })
+
+ ts := httptest.NewServer(srv)
+ t.Cleanup(ts.Close)
+
+ ctx := context.Background()
+ transport := policy.ClientOptions{Transport: ts.Client(), Retry: policy.RetryOptions{MaxRetries: -1}}
+
+ // Blob: create a container.
+ blobClient, err := azblob.NewClientWithNoCredential(ts.URL+"/", &azblob.ClientOptions{ClientOptions: transport})
+ if err != nil {
+ t.Fatalf("azblob client: %v", err)
+ }
+
+ if _, err := blobClient.CreateContainer(ctx, "cont", nil); err != nil {
+ t.Fatalf("Blob CreateContainer routed wrong: %v", err)
+ }
+
+ // Note: the root GET /?comp=list shape (list-containers vs list-queues) is
+ // byte-for-byte identical on the wire — Azure disambiguates it only by the
+ // {account}.blob vs {account}.queue hostname, which a single test server
+ // can't see. When Blob and Queue coexist behind one endpoint, that one
+ // shape is inherently ambiguous; the Queue handler owns it (registered
+ // first). Every other surface is disjoint, which is what this test asserts:
+ // container/blob paths, /{queue}/messages, and OData table paths never
+ // collide.
+
+ // Blob: put+get a blob (a two-segment /{container}/{blob} path) — must
+ // reach the Blob handler, not Queue or Table.
+ if _, err := blobClient.UploadBuffer(ctx, "cont", "obj", []byte("data"), nil); err != nil {
+ t.Fatalf("Blob UploadBuffer routed wrong: %v", err)
+ }
+
+ // Queue: create a queue named "cont" (same name as the container) and
+ // enqueue — must hit the queue handler, not blob.
+ qSvc, err := azqueue.NewServiceClientWithNoCredential(ts.URL+"/", &azqueue.ClientOptions{ClientOptions: transport})
+ if err != nil {
+ t.Fatalf("azqueue client: %v", err)
+ }
+
+ if _, err := qSvc.CreateQueue(ctx, "cont", nil); err != nil {
+ t.Fatalf("Queue CreateQueue routed wrong: %v", err)
+ }
+
+ qClient, err := azqueue.NewQueueClientWithNoCredential(ts.URL+"/cont", &azqueue.ClientOptions{ClientOptions: transport})
+ if err != nil {
+ t.Fatalf("azqueue queue client: %v", err)
+ }
+
+ if _, err := qClient.EnqueueMessage(ctx, "m", nil); err != nil {
+ t.Fatalf("Queue EnqueueMessage routed wrong: %v", err)
+ }
+
+ // Table: create a table named "cont" and insert — must hit the table
+ // handler.
+ tSvc, err := aztables.NewServiceClientWithNoCredential(ts.URL+"/", &aztables.ClientOptions{ClientOptions: transport})
+ if err != nil {
+ t.Fatalf("aztables client: %v", err)
+ }
+
+ if _, err := tSvc.CreateTable(ctx, "cont", nil); err != nil {
+ t.Fatalf("Table CreateTable routed wrong: %v", err)
+ }
+
+ ent, _ := json.Marshal(map[string]any{"PartitionKey": "p", "RowKey": "r", "V": "1"})
+ if _, err := tSvc.NewClient("cont").AddEntity(ctx, ent, nil); err != nil {
+ t.Fatalf("Table AddEntity routed wrong: %v", err)
+ }
+}
diff --git a/server/azure/queue/handler.go b/server/azure/queue/handler.go
new file mode 100644
index 0000000..1960960
--- /dev/null
+++ b/server/azure/queue/handler.go
@@ -0,0 +1,418 @@
+// Package queue implements the Azure Queue Storage REST+XML wire protocol as a
+// server.Handler. Real azure-sdk-for-go azqueue clients configured with a
+// custom service URL hit this handler the same way they hit
+// {account}.queue.core.windows.net.
+//
+// It maps the Azure Queue REST surface onto the shared messagequeue driver:
+//
+// PUT /{queue} — create queue
+// DELETE /{queue} — delete queue
+// GET /?comp=list — list queues
+// POST /{queue}/messages — enqueue message
+// GET /{queue}/messages — dequeue messages
+// DELETE /{queue}/messages/{messageid}?popreceipt=… — delete message
+//
+// Message bodies are XML … envelopes; the SDK
+// base64-encodes application payloads into MessageText, so this handler treats
+// MessageText as an opaque string and round-trips it verbatim.
+//
+// Less-used surfaces (metadata, ACLs, message update/peek/clear, service
+// properties) are not yet wired and return 501.
+package queue
+
+import (
+ "encoding/xml"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ mqdriver "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+)
+
+const (
+ contentTypeXML = "application/xml"
+
+ // xmsVersion is the Queue Storage service version we report.
+ xmsVersion = "2018-03-28"
+
+ compList = "list"
+
+ // maxEnqueueBodyBytes caps a single enqueued message body. Azure allows up
+ // to 64 KiB; we use a generous 1 MiB cap.
+ maxEnqueueBodyBytes = 1 << 20
+)
+
+// Handler serves Azure Queue Storage REST requests against a messagequeue
+// driver.
+type Handler struct {
+ mq mqdriver.MessageQueue
+}
+
+// New returns a Queue handler backed by mq.
+func New(mq mqdriver.MessageQueue) *Handler {
+ return &Handler{mq: mq}
+}
+
+// Matches returns true for requests that look like Azure Queue Storage calls.
+// These are non-ARM data-plane URLs; the detection signals are disjoint from
+// the Blob fallback:
+//
+// - /{queue}/messages and /{queue}/messages/{id} — the "/messages" segment
+// is the queue data-plane marker. NOTE: this is not strictly disjoint from
+// Blob — a blob literally named "messages" (or under a "messages/" prefix)
+// has the same shape. Azure separates them only by the {account}.queue vs
+// {account}.blob hostname, invisible behind a shared endpoint. When both
+// handlers are wired, the Queue handler owns this shape; a blob named
+// "messages" is a known collision.
+// - PUT|DELETE /{queue} with no restype=container query — Blob container ops
+// always carry restype=container, so a bare PUT/DELETE on a single path
+// segment is a queue create/delete. Disjoint from Blob container ops.
+// - GET /?comp=list (list queues) — this shape is byte-for-byte identical to
+// Blob's list-containers; Azure disambiguates only by hostname. When both
+// handlers are registered, the Queue handler (registered first) owns it.
+//
+// The two shared-hostname shapes above are inherent to serving Queue and Blob
+// on one endpoint (real Azure uses distinct hostnames); documented, not fixable
+// without host-based routing.
+//
+// Registered before the permissive Blob fallback so these shapes win.
+func (*Handler) Matches(r *http.Request) bool {
+ if strings.HasPrefix(r.URL.Path, "/subscriptions/") {
+ return false
+ }
+
+ queue, sub, msgID := parseQueuePath(r.URL.Path)
+ q := r.URL.Query()
+
+ // /{queue}/messages[/{id}] — the unambiguous queue message surface.
+ if sub == "messages" {
+ _ = msgID
+
+ return true
+ }
+
+ // GET /?comp=list — list queues (see Matches doc: shares Blob's shape).
+ if queue == "" {
+ return r.Method == http.MethodGet && q.Get("comp") == compList && q.Get("restype") == ""
+ }
+
+ // Bare /{queue} create/delete: PUT or DELETE with no container/blob query
+ // markers. Blob container ops carry restype=container.
+ if sub == "" {
+ switch r.Method {
+ case http.MethodPut, http.MethodDelete:
+ return q.Get("restype") == ""
+ }
+ }
+
+ return false
+}
+
+// ServeHTTP routes on the parsed path shape.
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("X-Ms-Version", xmsVersion)
+
+ queue, sub, msgID := parseQueuePath(r.URL.Path)
+ q := r.URL.Query()
+
+ switch {
+ case queue == "" && r.Method == http.MethodGet && q.Get("comp") == compList:
+ h.listQueues(w, r)
+ case queue == "":
+ writeError(w, http.StatusNotImplemented, "NotImplemented", "operation not supported on root")
+ case sub == "messages" && msgID == "":
+ h.messagesOp(w, r, queue)
+ case sub == "messages":
+ h.messageIDOp(w, r, queue, msgID)
+ case sub == "":
+ h.queueOp(w, r, queue)
+ default:
+ writeError(w, http.StatusBadRequest, "InvalidUri", "unrecognized queue path")
+ }
+}
+
+// parseQueuePath splits "/queue/messages/id" into ("queue", "messages", "id").
+// For "/queue" it returns ("queue", "", ""); for "/queue/messages" it returns
+// ("queue", "messages", "").
+func parseQueuePath(path string) (queue, sub, msgID string) {
+ path = strings.TrimPrefix(path, "/")
+ if path == "" {
+ return "", "", ""
+ }
+
+ const maxParts = 3
+ parts := strings.SplitN(path, "/", maxParts)
+ queue = parts[0]
+
+ if len(parts) > 1 {
+ sub = parts[1]
+ }
+
+ if len(parts) > 2 {
+ msgID = parts[2]
+ }
+
+ return queue, sub, msgID
+}
+
+func (h *Handler) queueOp(w http.ResponseWriter, r *http.Request, queue string) {
+ switch r.Method {
+ case http.MethodPut:
+ h.createQueue(w, r, queue)
+ case http.MethodDelete:
+ h.deleteQueue(w, r, queue)
+ default:
+ writeError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+ }
+}
+
+func (h *Handler) createQueue(w http.ResponseWriter, r *http.Request, queue string) {
+ _, err := h.mq.CreateQueue(r.Context(), mqdriver.QueueConfig{Name: queue})
+ if err != nil {
+ // Azure returns 204 No Content when the queue already exists with the
+ // same metadata (idempotent create).
+ if cerrors.IsAlreadyExists(err) {
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ writeErr(w, err)
+
+ return
+ }
+
+ w.WriteHeader(http.StatusCreated)
+}
+
+func (h *Handler) deleteQueue(w http.ResponseWriter, r *http.Request, queue string) {
+ url, err := h.resolveQueueURL(r, queue)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ if err := h.mq.DeleteQueue(r.Context(), url); err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+func (h *Handler) listQueues(w http.ResponseWriter, r *http.Request) {
+ prefix := r.URL.Query().Get("prefix")
+
+ queues, err := h.mq.ListQueues(r.Context(), prefix)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ out := listQueuesResult{Prefix: prefix}
+ for _, qi := range queues {
+ out.Queues.Queues = append(out.Queues.Queues, queueXML{Name: qi.Name})
+ }
+
+ writeXML(w, http.StatusOK, out)
+}
+
+// messagesOp handles POST (enqueue) and GET (dequeue) on /{queue}/messages.
+func (h *Handler) messagesOp(w http.ResponseWriter, r *http.Request, queue string) {
+ switch r.Method {
+ case http.MethodPost:
+ h.enqueue(w, r, queue)
+ case http.MethodGet:
+ h.dequeue(w, r, queue)
+ case http.MethodDelete:
+ // DELETE /{queue}/messages with no id = clear queue.
+ h.clearQueue(w, r, queue)
+ default:
+ writeError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+ }
+}
+
+func (h *Handler) enqueue(w http.ResponseWriter, r *http.Request, queue string) {
+ url, err := h.resolveQueueURL(r, queue)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxEnqueueBodyBytes))
+ if err != nil {
+ writeError(w, http.StatusBadRequest, "InvalidInput", "could not read body")
+ return
+ }
+
+ var req enqueueRequest
+ if err := xml.Unmarshal(body, &req); err != nil {
+ writeError(w, http.StatusBadRequest, "InvalidXmlDocument", "malformed request body")
+ return
+ }
+
+ visTimeout := atoiDefault(r.URL.Query().Get("visibilitytimeout"), 0)
+
+ out, err := h.mq.SendMessage(r.Context(), mqdriver.SendMessageInput{
+ QueueURL: url,
+ Body: req.MessageText,
+ DelaySeconds: visTimeout,
+ })
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ now := time.Now().UTC()
+ resp := messagesList{Messages: []messageXML{{
+ MessageID: out.MessageID,
+ InsertionTime: now.Format(time.RFC1123),
+ ExpirationTime: now.Add(7 * 24 * time.Hour).Format(time.RFC1123),
+ PopReceipt: out.MessageID,
+ TimeNextVisible: now.Add(time.Duration(visTimeout) * time.Second).Format(time.RFC1123),
+ }}}
+
+ writeXML(w, http.StatusCreated, resp)
+}
+
+func (h *Handler) dequeue(w http.ResponseWriter, r *http.Request, queue string) {
+ url, err := h.resolveQueueURL(r, queue)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ q := r.URL.Query()
+ maxMsgs := atoiDefault(q.Get("numofmessages"), 1)
+ visTimeout := atoiDefault(q.Get("visibilitytimeout"), 0)
+
+ msgs, err := h.mq.ReceiveMessages(r.Context(), mqdriver.ReceiveMessageInput{
+ QueueURL: url,
+ MaxMessages: maxMsgs,
+ VisibilityTimeout: visTimeout,
+ })
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ now := time.Now().UTC()
+ out := messagesList{}
+
+ for _, m := range msgs {
+ out.Messages = append(out.Messages, messageXML{
+ MessageID: m.MessageID,
+ InsertionTime: now.Format(time.RFC1123),
+ ExpirationTime: now.Add(7 * 24 * time.Hour).Format(time.RFC1123),
+ PopReceipt: m.ReceiptHandle,
+ TimeNextVisible: now.Add(time.Duration(visTimeout) * time.Second).Format(time.RFC1123),
+ DequeueCount: 1,
+ MessageText: m.Body,
+ })
+ }
+
+ writeXML(w, http.StatusOK, out)
+}
+
+func (h *Handler) clearQueue(w http.ResponseWriter, r *http.Request, queue string) {
+ url, err := h.resolveQueueURL(r, queue)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ if err := h.mq.PurgeQueue(r.Context(), url); err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+// messageIDOp handles DELETE /{queue}/messages/{messageid}?popreceipt=….
+func (h *Handler) messageIDOp(w http.ResponseWriter, r *http.Request, queue, _ string) {
+ if r.Method != http.MethodDelete {
+ writeError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+ return
+ }
+
+ url, err := h.resolveQueueURL(r, queue)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ popReceipt := r.URL.Query().Get("popreceipt")
+ if popReceipt == "" {
+ writeError(w, http.StatusBadRequest, "InvalidQueryParameterValue", "popreceipt is required")
+ return
+ }
+
+ if err := h.mq.DeleteMessage(r.Context(), url, popReceipt); err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+// resolveQueueURL maps an Azure queue name to the driver's internal queue URL
+// via ListQueues. The messagequeue driver keys queues by an opaque URL rather
+// than by name, so we look the name up.
+func (h *Handler) resolveQueueURL(r *http.Request, queue string) (string, error) {
+ queues, err := h.mq.ListQueues(r.Context(), "")
+ if err != nil {
+ return "", err
+ }
+
+ for _, qi := range queues {
+ if qi.Name == queue {
+ return qi.URL, nil
+ }
+ }
+
+ return "", cerrors.Newf(cerrors.NotFound, "queue %q not found", queue)
+}
+
+func atoiDefault(s string, def int) int {
+ if s == "" {
+ return def
+ }
+
+ if n, err := strconv.Atoi(s); err == nil {
+ return n
+ }
+
+ return def
+}
+
+func writeXML(w http.ResponseWriter, status int, v any) {
+ w.Header().Set("Content-Type", contentTypeXML)
+ w.WriteHeader(status)
+ _, _ = w.Write([]byte(xml.Header))
+ _ = xml.NewEncoder(w).Encode(v)
+}
+
+func writeError(w http.ResponseWriter, status int, code, msg string) {
+ w.Header().Set("Content-Type", contentTypeXML)
+ w.Header().Set("X-Ms-Error-Code", code)
+ w.WriteHeader(status)
+ _, _ = w.Write([]byte(xml.Header))
+ _ = xml.NewEncoder(w).Encode(errorXML{Code: code, Message: msg})
+}
+
+// writeErr maps CloudEmu canonical errors to Azure Queue HTTP errors.
+func writeErr(w http.ResponseWriter, err error) {
+ switch {
+ case cerrors.IsNotFound(err):
+ writeError(w, http.StatusNotFound, "QueueNotFound", err.Error())
+ case cerrors.IsAlreadyExists(err):
+ writeError(w, http.StatusConflict, "QueueAlreadyExists", err.Error())
+ case cerrors.IsInvalidArgument(err):
+ writeError(w, http.StatusBadRequest, "InvalidInput", err.Error())
+ default:
+ writeError(w, http.StatusInternalServerError, "InternalError", err.Error())
+ }
+}
diff --git a/server/azure/queue/sdk_roundtrip_test.go b/server/azure/queue/sdk_roundtrip_test.go
new file mode 100644
index 0000000..7af1c19
--- /dev/null
+++ b/server/azure/queue/sdk_roundtrip_test.go
@@ -0,0 +1,121 @@
+package queue_test
+
+import (
+ "context"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue"
+
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
+)
+
+// TestSDKQueueRoundTrip drives Azure Queue Storage operations with the real
+// azqueue client against our handler. azqueue supports anonymous access, so
+// the test doesn't have to forge SharedKey signatures.
+func TestSDKQueueRoundTrip(t *testing.T) {
+ cloudP := cloudemu.NewAzure()
+ srv := azureserver.New(azureserver.Drivers{QueueStorage: cloudP.QueueStorage})
+
+ ts := httptest.NewServer(srv)
+ t.Cleanup(ts.Close)
+
+ ctx := context.Background()
+
+ svcOpts := &azqueue.ClientOptions{
+ ClientOptions: policy.ClientOptions{
+ Transport: ts.Client(),
+ Retry: policy.RetryOptions{MaxRetries: -1},
+ },
+ }
+
+ svcClient, err := azqueue.NewServiceClientWithNoCredential(ts.URL+"/", svcOpts)
+ if err != nil {
+ t.Fatalf("NewServiceClientWithNoCredential: %v", err)
+ }
+
+ // Create queue.
+ if _, err := svcClient.CreateQueue(ctx, "q1", nil); err != nil {
+ t.Fatalf("CreateQueue: %v", err)
+ }
+
+ // List queues and confirm q1 is present.
+ {
+ pager := svcClient.NewListQueuesPager(nil)
+
+ seen := map[string]bool{}
+ for pager.More() {
+ page, err := pager.NextPage(ctx)
+ if err != nil {
+ t.Fatalf("ListQueues: %v", err)
+ }
+
+ for _, q := range page.Queues {
+ if q.Name != nil {
+ seen[*q.Name] = true
+ }
+ }
+ }
+
+ if !seen["q1"] {
+ t.Errorf("q1 not in queue list: %v", seen)
+ }
+ }
+
+ qClient, err := azqueue.NewQueueClientWithNoCredential(ts.URL+"/q1", &azqueue.ClientOptions{
+ ClientOptions: policy.ClientOptions{Transport: ts.Client(), Retry: policy.RetryOptions{MaxRetries: -1}},
+ })
+ if err != nil {
+ t.Fatalf("NewQueueClientWithNoCredential: %v", err)
+ }
+
+ // Enqueue a message.
+ const payload = "hello, azure queue"
+
+ enq, err := qClient.EnqueueMessage(ctx, payload, nil)
+ if err != nil {
+ t.Fatalf("EnqueueMessage: %v", err)
+ }
+
+ if len(enq.Messages) == 0 || enq.Messages[0].MessageID == nil {
+ t.Fatalf("EnqueueMessage returned no message id: %+v", enq)
+ }
+
+ // Dequeue and verify the message text round-trips.
+ deq, err := qClient.DequeueMessage(ctx, nil)
+ if err != nil {
+ t.Fatalf("DequeueMessage: %v", err)
+ }
+
+ if len(deq.Messages) != 1 {
+ t.Fatalf("DequeueMessage returned %d messages, want 1", len(deq.Messages))
+ }
+
+ msg := deq.Messages[0]
+ if msg.MessageText == nil || *msg.MessageText != payload {
+ t.Errorf("message text mismatch: got=%v want=%q", msg.MessageText, payload)
+ }
+
+ if msg.MessageID == nil || msg.PopReceipt == nil {
+ t.Fatalf("dequeued message missing id/popreceipt: %+v", msg)
+ }
+
+ // Delete the message using its id + pop receipt.
+ if _, err := qClient.DeleteMessage(ctx, *msg.MessageID, *msg.PopReceipt, nil); err != nil {
+ t.Fatalf("DeleteMessage: %v", err)
+ }
+
+ // Delete the queue.
+ if _, err := qClient.Delete(ctx, nil); err != nil {
+ t.Fatalf("Delete queue: %v", err)
+ }
+
+ // Confirm the queue is gone: enqueue should now fail with QueueNotFound.
+ if _, err := qClient.EnqueueMessage(ctx, "x", nil); err == nil ||
+ !strings.Contains(err.Error(), "QueueNotFound") {
+ t.Errorf("expected QueueNotFound after delete, got %v", err)
+ }
+}
diff --git a/server/azure/queue/types.go b/server/azure/queue/types.go
new file mode 100644
index 0000000..d43daa8
--- /dev/null
+++ b/server/azure/queue/types.go
@@ -0,0 +1,56 @@
+package queue
+
+import "encoding/xml"
+
+// Azure Queue Storage XML wire shapes. Child element names match the
+// azqueue SDK's generated models exactly; the SDK unmarshals into structs and
+// ignores the root element name, so only child names are load-bearing.
+
+// enqueueRequest is the body of POST /{queue}/messages.
+type enqueueRequest struct {
+ XMLName xml.Name `xml:"QueueMessage"`
+ MessageText string `xml:"MessageText"`
+}
+
+// messagesList is the QueueMessagesList body returned by enqueue and dequeue.
+type messagesList struct {
+ XMLName xml.Name `xml:"QueueMessagesList"`
+ Messages []messageXML `xml:"QueueMessage"`
+}
+
+// messageXML carries the union of fields the SDK reads for enqueued and
+// dequeued messages. Empty fields are omitted so enqueue responses stay lean.
+type messageXML struct {
+ MessageID string `xml:"MessageId"`
+ InsertionTime string `xml:"InsertionTime"`
+ ExpirationTime string `xml:"ExpirationTime"`
+ PopReceipt string `xml:"PopReceipt"`
+ TimeNextVisible string `xml:"TimeNextVisible"`
+ DequeueCount int64 `xml:"DequeueCount,omitempty"`
+ MessageText string `xml:"MessageText,omitempty"`
+}
+
+// listQueuesResult is the EnumerationResults body for GET /?comp=list.
+type listQueuesResult struct {
+ XMLName xml.Name `xml:"EnumerationResults"`
+ Prefix string `xml:"Prefix,omitempty"`
+ Marker string `xml:"Marker,omitempty"`
+ MaxResults int `xml:"MaxResults,omitempty"`
+ Queues queuesList `xml:"Queues"`
+ NextMarker string `xml:"NextMarker"`
+}
+
+type queuesList struct {
+ Queues []queueXML `xml:"Queue"`
+}
+
+type queueXML struct {
+ Name string `xml:"Name"`
+}
+
+// errorXML is the Azure Storage error envelope.
+type errorXML struct {
+ XMLName xml.Name `xml:"Error"`
+ Code string `xml:"Code"`
+ Message string `xml:"Message"`
+}
diff --git a/server/azure/resourcegraph/handler.go b/server/azure/resourcegraph/handler.go
index aa507ca..f5cd54a 100644
--- a/server/azure/resourcegraph/handler.go
+++ b/server/azure/resourcegraph/handler.go
@@ -4,8 +4,8 @@ import (
"net/http"
"strings"
- "github.com/stackshy/cloudemu/resourcediscovery"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
)
// Path prefixes this handler serves. The Resource Graph provider sits at a
diff --git a/server/azure/resourcegraph/kql.go b/server/azure/resourcegraph/kql.go
index dbf56a9..2ba1f95 100644
--- a/server/azure/resourcegraph/kql.go
+++ b/server/azure/resourcegraph/kql.go
@@ -28,7 +28,7 @@ import (
"strconv"
"strings"
- "github.com/stackshy/cloudemu/resourcediscovery"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
)
// Canonical Azure resource type strings used by Resource Graph query syntax
diff --git a/server/azure/resourcegraph/sdk_test.go b/server/azure/resourcegraph/sdk_test.go
index b5c8437..397c5be 100644
--- a/server/azure/resourcegraph/sdk_test.go
+++ b/server/azure/resourcegraph/sdk_test.go
@@ -18,11 +18,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- dbxdriver "github.com/stackshy/cloudemu/databricks/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
+ dbxdriver "github.com/stackshy/cloudemu/v2/services/databricks/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
type fakeCred struct{}
diff --git a/server/azure/servicebus/handler.go b/server/azure/servicebus/handler.go
index ff9179e..6239b5a 100644
--- a/server/azure/servicebus/handler.go
+++ b/server/azure/servicebus/handler.go
@@ -30,9 +30,9 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- mqdriver "github.com/stackshy/cloudemu/messagequeue/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ mqdriver "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
)
const (
diff --git a/server/azure/servicebus/sdk_roundtrip_test.go b/server/azure/servicebus/sdk_roundtrip_test.go
index 227b1e0..6bfb23b 100644
--- a/server/azure/servicebus/sdk_roundtrip_test.go
+++ b/server/azure/servicebus/sdk_roundtrip_test.go
@@ -13,8 +13,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicebus/armservicebus/v2"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
// fakeCred is a static-token credential for tests.
diff --git a/server/azure/servicebus/servicebus_test.go b/server/azure/servicebus/servicebus_test.go
index 99166ba..754f109 100644
--- a/server/azure/servicebus/servicebus_test.go
+++ b/server/azure/servicebus/servicebus_test.go
@@ -9,8 +9,8 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
const (
diff --git a/server/azure/snapshots/handler.go b/server/azure/snapshots/handler.go
index 1e17b37..9146ff0 100644
--- a/server/azure/snapshots/handler.go
+++ b/server/azure/snapshots/handler.go
@@ -6,9 +6,9 @@ import (
"net/http"
"strings"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// diskARMNameTag mirrors the constant in server/azure/disks. We resolve
diff --git a/server/azure/snapshots/snapshots_test.go b/server/azure/snapshots/snapshots_test.go
index c075007..c99a45a 100644
--- a/server/azure/snapshots/snapshots_test.go
+++ b/server/azure/snapshots/snapshots_test.go
@@ -14,8 +14,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
type fakeCred struct{}
diff --git a/server/azure/sshpublickeys/handler.go b/server/azure/sshpublickeys/handler.go
index cddeac4..5e1caf0 100644
--- a/server/azure/sshpublickeys/handler.go
+++ b/server/azure/sshpublickeys/handler.go
@@ -7,9 +7,9 @@ import (
"net/http"
"strings"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
const (
diff --git a/server/azure/sshpublickeys/sshpublickeys_test.go b/server/azure/sshpublickeys/sshpublickeys_test.go
index 6bb8a18..b11de68 100644
--- a/server/azure/sshpublickeys/sshpublickeys_test.go
+++ b/server/azure/sshpublickeys/sshpublickeys_test.go
@@ -13,8 +13,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
type fakeCred struct{}
diff --git a/server/azure/table/handler.go b/server/azure/table/handler.go
new file mode 100644
index 0000000..745143f
--- /dev/null
+++ b/server/azure/table/handler.go
@@ -0,0 +1,359 @@
+// Package table implements the Azure Table Storage data-plane REST protocol
+// (OData/JSON) as a server.Handler. Real azure-sdk-for-go aztables clients
+// configured with a custom service URL hit this handler the same way they hit
+// {account}.table.core.windows.net.
+//
+// Supported operations:
+//
+// POST /Tables — create table
+// GET /Tables — list tables
+// DELETE /Tables('name') — delete table
+// POST /{table} — insert entity
+// GET /{table}(PartitionKey='p',RowKey='r') — get entity
+// PUT /{table}(PartitionKey='p',RowKey='r') — replace entity
+// MERGE|PATCH /{table}(PartitionKey='p',RowKey='r') — merge entity
+// DELETE /{table}(PartitionKey='p',RowKey='r') — delete entity
+// GET /{table}()?$filter=… — query entities
+//
+// Access policies and transactional batches are out of scope. Upsert (a PUT or
+// MERGE with no If-Match header against a missing row) is supported as an
+// insert-or-replace/merge, matching aztables UpsertEntity.
+package table
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ driver "github.com/stackshy/cloudemu/v2/services/tablestorage/driver"
+)
+
+const (
+ contentTypeJSON = "application/json"
+
+ // xmsVersion is the Table Storage service version we report.
+ xmsVersion = "2019-02-02"
+
+ // maxBodyBytes caps request bodies (entities / table-create payloads).
+ maxBodyBytes = 1 << 20
+)
+
+// Handler serves Azure Table Storage REST requests against a TableStorage
+// driver.
+type Handler struct {
+ ts driver.TableStorage
+}
+
+// New returns a Table handler backed by ts.
+func New(ts driver.TableStorage) *Handler {
+ return &Handler{ts: ts}
+}
+
+// Matches returns true for Azure Table Storage data-plane requests. The
+// detection signals are disjoint from every other Azure handler:
+//
+// - The path is /Tables or /Tables('name') — the table lifecycle surface,
+// which no other service uses.
+// - The path's first segment carries an OData key predicate: it contains a
+// "(" — either "()" (query entities) or
+// "(PartitionKey='…',RowKey='…')" (entity CRUD). Blob/Queue paths never
+// contain parentheses, and ARM paths start with /subscriptions/, so this
+// is unambiguous.
+// - POST /{table} (insert entity) is a bare single segment with a JSON body.
+// Blob and Queue never use a bare POST on a single path segment, so the
+// method+content-type discriminates it from their PUT/DELETE ops.
+//
+// Registered before the permissive Blob fallback so these shapes win.
+func (*Handler) Matches(r *http.Request) bool {
+ if strings.HasPrefix(r.URL.Path, "/subscriptions/") {
+ return false
+ }
+
+ path := strings.TrimPrefix(r.URL.Path, "/")
+
+ // /Tables and /Tables('name').
+ if path == "Tables" || strings.HasPrefix(path, "Tables(") {
+ return true
+ }
+
+ first := path
+ if i := strings.IndexByte(path, '/'); i >= 0 {
+ first = path[:i]
+ }
+
+ // Entity CRUD / query: first path segment contains an OData "(" predicate.
+ if strings.ContainsRune(first, '(') {
+ return true
+ }
+
+ // Insert entity: POST /{table} with a JSON body.
+ if r.Method == http.MethodPost && first != "" && !strings.Contains(first, "/") {
+ return isJSONContentType(r.Header.Get("Content-Type"))
+ }
+
+ return false
+}
+
+func isJSONContentType(ct string) bool {
+ return strings.Contains(strings.ToLower(ct), "application/json")
+}
+
+// ServeHTTP routes on the parsed path shape.
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("X-Ms-Version", xmsVersion)
+ w.Header().Set("Dataserviceversion", "3.0")
+
+ path := strings.TrimPrefix(r.URL.Path, "/")
+
+ switch {
+ case path == "Tables":
+ h.tablesCollectionOp(w, r)
+ case strings.HasPrefix(path, "Tables("):
+ h.deleteTable(w, r, tableNameFromDelete(path))
+ case r.Method == http.MethodPost && !strings.ContainsRune(path, '('):
+ // POST /{table} — insert entity into a bare table path.
+ h.insertEntity(w, r, path)
+ default:
+ h.entityOp(w, r, path)
+ }
+}
+
+// tablesCollectionOp handles POST (create) and GET (list) on /Tables.
+func (h *Handler) tablesCollectionOp(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case http.MethodPost:
+ h.createTable(w, r)
+ case http.MethodGet:
+ h.listTables(w, r)
+ default:
+ writeError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+ }
+}
+
+func (h *Handler) createTable(w http.ResponseWriter, r *http.Request) {
+ var body struct {
+ TableName string `json:"TableName"`
+ }
+
+ if err := decodeJSON(w, r, &body); err != nil {
+ writeError(w, http.StatusBadRequest, "InvalidInput", "malformed request body")
+ return
+ }
+
+ if err := h.ts.CreateTable(r.Context(), body.TableName); err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ resp := map[string]any{
+ "odata.metadata": fmt.Sprintf("%s://%s/$metadata#Tables/@Element", scheme(r), r.Host),
+ "TableName": body.TableName,
+ }
+
+ writeJSON(w, http.StatusCreated, resp)
+}
+
+func (h *Handler) listTables(w http.ResponseWriter, r *http.Request) {
+ names, err := h.ts.ListTables(r.Context())
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ value := make([]map[string]any, 0, len(names))
+ for _, n := range names {
+ value = append(value, map[string]any{"TableName": n})
+ }
+
+ resp := map[string]any{
+ "odata.metadata": fmt.Sprintf("%s://%s/$metadata#Tables", scheme(r), r.Host),
+ "value": value,
+ }
+
+ writeJSON(w, http.StatusOK, resp)
+}
+
+func (h *Handler) deleteTable(w http.ResponseWriter, r *http.Request, name string) {
+ if r.Method != http.MethodDelete {
+ writeError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+ return
+ }
+
+ if err := h.ts.DeleteTable(r.Context(), name); err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+// entityOp routes entity-level requests: /{table}() (query) and
+// /{table}(PartitionKey='p',RowKey='r') (CRUD).
+func (h *Handler) entityOp(w http.ResponseWriter, r *http.Request, path string) {
+ table, predicate, ok := splitEntityPath(path)
+ if !ok {
+ writeError(w, http.StatusBadRequest, "InvalidUri", "unrecognized table path")
+ return
+ }
+
+ // Query: /{table}() with an empty predicate.
+ if strings.TrimSpace(predicate) == "" {
+ h.queryEntities(w, r, table)
+ return
+ }
+
+ pk, rk, ok := parseKeyPredicate(predicate)
+ if !ok {
+ writeError(w, http.StatusBadRequest, "InvalidInput", "malformed entity key predicate")
+ return
+ }
+
+ switch r.Method {
+ case http.MethodGet:
+ h.getEntity(w, r, table, pk, rk)
+ case http.MethodPut:
+ h.updateEntity(w, r, table, pk, rk, driver.UpdateModeReplace)
+ case http.MethodPatch, "MERGE":
+ h.updateEntity(w, r, table, pk, rk, driver.UpdateModeMerge)
+ case http.MethodDelete:
+ h.deleteEntity(w, r, table, pk, rk)
+ default:
+ writeError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+ }
+}
+
+func (h *Handler) queryEntities(w http.ResponseWriter, r *http.Request, table string) {
+ if r.Method != http.MethodGet {
+ writeError(w, http.StatusMethodNotAllowed, "MethodNotAllowed", "method not allowed")
+ return
+ }
+
+ entities, err := h.ts.QueryEntities(r.Context(), table, driver.QueryOptions{
+ Filter: r.URL.Query().Get("$filter"),
+ })
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ value := make([]map[string]any, 0, len(entities))
+ for _, e := range entities {
+ value = append(value, entityToJSON(e))
+ }
+
+ resp := map[string]any{
+ "odata.metadata": fmt.Sprintf("%s://%s/$metadata#%s", scheme(r), r.Host, table),
+ "value": value,
+ }
+
+ writeJSON(w, http.StatusOK, resp)
+}
+
+func (h *Handler) getEntity(w http.ResponseWriter, r *http.Request, table, pk, rk string) {
+ ent, err := h.ts.GetEntity(r.Context(), table, pk, rk)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ out := entityToJSON(ent)
+ out["odata.metadata"] = fmt.Sprintf("%s://%s/$metadata#%s/@Element", scheme(r), r.Host, table)
+
+ w.Header().Set("ETag", entityETag())
+ writeJSON(w, http.StatusOK, out)
+}
+
+func (h *Handler) insertEntity(w http.ResponseWriter, r *http.Request, table string) {
+ ent, err := readEntity(w, r)
+ if err != nil {
+ writeError(w, http.StatusBadRequest, "InvalidInput", "malformed entity body")
+ return
+ }
+
+ pk := asString(ent["PartitionKey"])
+ rk := asString(ent["RowKey"])
+
+ if err := h.ts.InsertEntity(r.Context(), table, pk, rk, driver.Entity(ent)); err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ // Default (no Prefer header): return-content — echo the entity with 201.
+ out := entityToJSON(ent)
+ out["odata.metadata"] = fmt.Sprintf("%s://%s/$metadata#%s/@Element", scheme(r), r.Host, table)
+
+ w.Header().Set("ETag", entityETag())
+
+ if strings.EqualFold(r.Header.Get("Prefer"), "return-no-content") {
+ w.Header().Set("Preference-Applied", "return-no-content")
+ w.WriteHeader(http.StatusNoContent)
+
+ return
+ }
+
+ w.Header().Set("Preference-Applied", "return-content")
+ writeJSON(w, http.StatusCreated, out)
+}
+
+func (h *Handler) updateEntity(
+ w http.ResponseWriter, r *http.Request, table, pk, rk string, mode driver.UpdateMode,
+) {
+ ent, err := readEntity(w, r)
+ if err != nil {
+ writeError(w, http.StatusBadRequest, "InvalidInput", "malformed entity body")
+ return
+ }
+
+ // The key comes from the URL; ensure it's present in the stored entity.
+ ent["PartitionKey"] = pk
+ ent["RowKey"] = rk
+
+ if err := h.ts.UpdateEntity(r.Context(), table, pk, rk, driver.Entity(ent), mode); err != nil {
+ // aztables UpdateEntity sends an If-Match header; UpsertEntity does not.
+ // With no If-Match, a PUT/MERGE against a missing row is an insert
+ // (insert-or-replace/merge), so create it instead of returning 404.
+ if cerrors.IsNotFound(err) && r.Header.Get("If-Match") == "" {
+ if ierr := h.ts.InsertEntity(r.Context(), table, pk, rk, driver.Entity(ent)); ierr != nil {
+ writeErr(w, ierr)
+ return
+ }
+
+ w.Header().Set("ETag", entityETag())
+ w.WriteHeader(http.StatusNoContent)
+
+ return
+ }
+
+ writeErr(w, err)
+ return
+ }
+
+ w.Header().Set("ETag", entityETag())
+ w.WriteHeader(http.StatusNoContent)
+}
+
+func (h *Handler) deleteEntity(w http.ResponseWriter, r *http.Request, table, pk, rk string) {
+ if err := h.ts.DeleteEntity(r.Context(), table, pk, rk); err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+func readEntity(w http.ResponseWriter, r *http.Request) (map[string]any, error) {
+ data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxBodyBytes))
+ if err != nil {
+ return nil, err
+ }
+
+ var ent map[string]any
+ if err := json.Unmarshal(data, &ent); err != nil {
+ return nil, err
+ }
+
+ return ent, nil
+}
diff --git a/server/azure/table/helpers.go b/server/azure/table/helpers.go
new file mode 100644
index 0000000..bf20f45
--- /dev/null
+++ b/server/azure/table/helpers.go
@@ -0,0 +1,186 @@
+package table
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ driver "github.com/stackshy/cloudemu/v2/services/tablestorage/driver"
+)
+
+// tableNameFromDelete extracts "name" from a "Tables('name')" path.
+func tableNameFromDelete(path string) string {
+ open := strings.IndexByte(path, '(')
+ closeIdx := strings.LastIndexByte(path, ')')
+
+ if open < 0 || closeIdx < 0 || closeIdx <= open {
+ return ""
+ }
+
+ inner := path[open+1 : closeIdx]
+
+ return strings.Trim(inner, "'")
+}
+
+// splitEntityPath splits "table(predicate)" into ("table", "predicate", true).
+// A bare "table" with no parentheses returns ok=false.
+func splitEntityPath(path string) (table, predicate string, ok bool) {
+ open := strings.IndexByte(path, '(')
+ if open < 0 {
+ return "", "", false
+ }
+
+ closeIdx := strings.LastIndexByte(path, ')')
+ if closeIdx < 0 || closeIdx <= open {
+ return "", "", false
+ }
+
+ return path[:open], path[open+1 : closeIdx], true
+}
+
+// parseKeyPredicate parses "PartitionKey='p',RowKey='r'" into ("p", "r").
+// The two clauses may appear in either order.
+func parseKeyPredicate(predicate string) (partitionKey, rowKey string, ok bool) {
+ for _, clause := range splitTopLevel(predicate) {
+ key, val, found := strings.Cut(clause, "=")
+ if !found {
+ continue
+ }
+
+ key = strings.TrimSpace(key)
+ val = unquote(strings.TrimSpace(val))
+
+ switch key {
+ case "PartitionKey":
+ partitionKey = val
+ case "RowKey":
+ rowKey = val
+ }
+ }
+
+ if partitionKey == "" && rowKey == "" {
+ return "", "", false
+ }
+
+ return partitionKey, rowKey, true
+}
+
+// splitTopLevel splits on commas that are not inside single-quoted strings, so
+// a key value containing a comma survives.
+func splitTopLevel(s string) []string {
+ var (
+ parts []string
+ start int
+ inQuote bool
+ )
+
+ for i := 0; i < len(s); i++ {
+ switch s[i] {
+ case '\'':
+ inQuote = !inQuote
+ case ',':
+ if !inQuote {
+ parts = append(parts, s[start:i])
+ start = i + 1
+ }
+ }
+ }
+
+ parts = append(parts, s[start:])
+
+ return parts
+}
+
+// unquote strips surrounding single quotes and unescapes doubled quotes, which
+// is how OData escapes a literal apostrophe.
+func unquote(s string) string {
+ if len(s) >= 2 && s[0] == '\'' && s[len(s)-1] == '\'' {
+ s = s[1 : len(s)-1]
+ }
+
+ return strings.ReplaceAll(s, "''", "'")
+}
+
+// entityToJSON copies an entity's properties into a fresh JSON map. The
+// PartitionKey/RowKey and user properties round-trip verbatim.
+func entityToJSON(e driver.Entity) map[string]any {
+ out := make(map[string]any, len(e)+1)
+ for k, v := range e {
+ out[k] = v
+ }
+
+ return out
+}
+
+func entityETag() string {
+ return fmt.Sprintf("W/\"datetime'%s'\"", time.Now().UTC().Format(time.RFC3339Nano))
+}
+
+func scheme(r *http.Request) string {
+ if r.TLS != nil {
+ return "https"
+ }
+
+ return "http"
+}
+
+func asString(v any) string {
+ s, _ := v.(string)
+ return s
+}
+
+func decodeJSON(w http.ResponseWriter, r *http.Request, v any) error {
+ data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxBodyBytes))
+ if err != nil {
+ return err
+ }
+
+ return json.Unmarshal(data, v)
+}
+
+func writeJSON(w http.ResponseWriter, status int, v any) {
+ w.Header().Set("Content-Type", contentTypeJSON+";odata=minimalmetadata;streaming=true;charset=utf-8")
+ w.WriteHeader(status)
+ _ = json.NewEncoder(w).Encode(v)
+}
+
+// tableError is the Azure Table Storage OData JSON error envelope.
+type tableError struct {
+ ODataError struct {
+ Code string `json:"code"`
+ Message struct {
+ Lang string `json:"lang"`
+ Value string `json:"value"`
+ } `json:"message"`
+ } `json:"odata.error"`
+}
+
+func writeError(w http.ResponseWriter, status int, code, msg string) {
+ var e tableError
+ e.ODataError.Code = code
+ e.ODataError.Message.Lang = "en-US"
+ e.ODataError.Message.Value = msg
+
+ w.Header().Set("Content-Type", contentTypeJSON)
+ w.Header().Set("X-Ms-Error-Code", code)
+ w.WriteHeader(status)
+ _ = json.NewEncoder(w).Encode(e)
+}
+
+// writeErr maps CloudEmu canonical errors to Azure Table HTTP errors.
+func writeErr(w http.ResponseWriter, err error) {
+ switch {
+ case cerrors.IsNotFound(err):
+ writeError(w, http.StatusNotFound, "ResourceNotFound", err.Error())
+ case cerrors.IsAlreadyExists(err):
+ writeError(w, http.StatusConflict, "EntityAlreadyExists", err.Error())
+ case cerrors.IsInvalidArgument(err):
+ writeError(w, http.StatusBadRequest, "InvalidInput", err.Error())
+ default:
+ writeError(w, http.StatusInternalServerError, "InternalError", err.Error())
+ }
+}
diff --git a/server/azure/table/sdk_roundtrip_test.go b/server/azure/table/sdk_roundtrip_test.go
new file mode 100644
index 0000000..66083ef
--- /dev/null
+++ b/server/azure/table/sdk_roundtrip_test.go
@@ -0,0 +1,130 @@
+package table_test
+
+import (
+ "context"
+ "encoding/json"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/data/aztables"
+
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
+)
+
+// TestSDKTableRoundTrip drives Azure Table Storage operations with the real
+// aztables client against our handler. aztables supports anonymous access, so
+// the test doesn't have to forge SharedKey signatures.
+func TestSDKTableRoundTrip(t *testing.T) {
+ cloudP := cloudemu.NewAzure()
+ srv := azureserver.New(azureserver.Drivers{TableStorage: cloudP.TableStorage})
+
+ ts := httptest.NewServer(srv)
+ t.Cleanup(ts.Close)
+
+ ctx := context.Background()
+
+ svcOpts := &aztables.ClientOptions{
+ ClientOptions: policy.ClientOptions{
+ Transport: ts.Client(),
+ Retry: policy.RetryOptions{MaxRetries: -1},
+ },
+ }
+
+ svc, err := aztables.NewServiceClientWithNoCredential(ts.URL+"/", svcOpts)
+ if err != nil {
+ t.Fatalf("NewServiceClientWithNoCredential: %v", err)
+ }
+
+ const tableName = "people"
+
+ // Create table.
+ if _, err := svc.CreateTable(ctx, tableName, nil); err != nil {
+ t.Fatalf("CreateTable: %v", err)
+ }
+
+ client := svc.NewClient(tableName)
+
+ // Insert entity.
+ entity := map[string]any{
+ "PartitionKey": "org",
+ "RowKey": "alice",
+ "Email": "alice@example.com",
+ "Age": int64(30),
+ }
+
+ marshalled, err := json.Marshal(entity)
+ if err != nil {
+ t.Fatalf("marshal entity: %v", err)
+ }
+
+ if _, err := client.AddEntity(ctx, marshalled, nil); err != nil {
+ t.Fatalf("AddEntity: %v", err)
+ }
+
+ // Get entity and verify properties round-trip.
+ got, err := client.GetEntity(ctx, "org", "alice", nil)
+ if err != nil {
+ t.Fatalf("GetEntity: %v", err)
+ }
+
+ var props map[string]any
+ if err := json.Unmarshal(got.Value, &props); err != nil {
+ t.Fatalf("unmarshal entity: %v", err)
+ }
+
+ if props["Email"] != "alice@example.com" {
+ t.Errorf("Email mismatch: got=%v want=alice@example.com", props["Email"])
+ }
+
+ if props["PartitionKey"] != "org" || props["RowKey"] != "alice" {
+ t.Errorf("key mismatch: pk=%v rk=%v", props["PartitionKey"], props["RowKey"])
+ }
+
+ // Query by partition and confirm alice is returned.
+ {
+ filter := "PartitionKey eq 'org'"
+ pager := client.NewListEntitiesPager(&aztables.ListEntitiesOptions{Filter: &filter})
+
+ seen := map[string]bool{}
+ for pager.More() {
+ page, err := pager.NextPage(ctx)
+ if err != nil {
+ t.Fatalf("ListEntities: %v", err)
+ }
+
+ for _, raw := range page.Entities {
+ var e map[string]any
+ if err := json.Unmarshal(raw, &e); err != nil {
+ t.Fatalf("unmarshal query entity: %v", err)
+ }
+
+ if rk, ok := e["RowKey"].(string); ok {
+ seen[rk] = true
+ }
+ }
+ }
+
+ if !seen["alice"] {
+ t.Errorf("alice not returned by partition query: %v", seen)
+ }
+ }
+
+ // Delete entity.
+ if _, err := client.DeleteEntity(ctx, "org", "alice", nil); err != nil {
+ t.Fatalf("DeleteEntity: %v", err)
+ }
+
+ // Confirm the entity is gone.
+ if _, err := client.GetEntity(ctx, "org", "alice", nil); err == nil ||
+ !strings.Contains(err.Error(), "ResourceNotFound") {
+ t.Errorf("expected ResourceNotFound after delete, got %v", err)
+ }
+
+ // Delete table.
+ if _, err := client.Delete(ctx, nil); err != nil {
+ t.Fatalf("Delete table: %v", err)
+ }
+}
diff --git a/server/azure/virtualmachines/handler.go b/server/azure/virtualmachines/handler.go
index 1e3b14a..3936c7b 100644
--- a/server/azure/virtualmachines/handler.go
+++ b/server/azure/virtualmachines/handler.go
@@ -22,8 +22,8 @@ import (
"net/http"
"strings"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// providerName is the ARM provider this handler serves.
diff --git a/server/azure/virtualmachines/instances.go b/server/azure/virtualmachines/instances.go
index 5922f0a..528a7f5 100644
--- a/server/azure/virtualmachines/instances.go
+++ b/server/azure/virtualmachines/instances.go
@@ -5,9 +5,9 @@ import (
"net/http"
"strings"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// armNameTag is the tag key we use to round-trip the ARM resource name
diff --git a/server/azure/virtualmachines/sdk_roundtrip_test.go b/server/azure/virtualmachines/sdk_roundtrip_test.go
index 04c4838..df63995 100644
--- a/server/azure/virtualmachines/sdk_roundtrip_test.go
+++ b/server/azure/virtualmachines/sdk_roundtrip_test.go
@@ -13,8 +13,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
// fakeCred is a static-token credential for tests. The real ARM endpoint
diff --git a/server/azure/virtualmachines/virtualmachines_test.go b/server/azure/virtualmachines/virtualmachines_test.go
index 7a0c44d..6693b65 100644
--- a/server/azure/virtualmachines/virtualmachines_test.go
+++ b/server/azure/virtualmachines/virtualmachines_test.go
@@ -9,8 +9,8 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu"
- azureserver "github.com/stackshy/cloudemu/server/azure"
+ "github.com/stackshy/cloudemu/v2"
+ azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)
// armBasePath returns the per-test ARM resource URL for a given VM name.
diff --git a/server/gcp/artifactregistry/handler.go b/server/gcp/artifactregistry/handler.go
index cc427ad..dd51eb6 100644
--- a/server/gcp/artifactregistry/handler.go
+++ b/server/gcp/artifactregistry/handler.go
@@ -19,8 +19,8 @@ import (
"net/http"
"strings"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
const (
diff --git a/server/gcp/artifactregistry/operations.go b/server/gcp/artifactregistry/operations.go
index 7348a12..8cefd50 100644
--- a/server/gcp/artifactregistry/operations.go
+++ b/server/gcp/artifactregistry/operations.go
@@ -3,8 +3,8 @@ package artifactregistry
import (
"net/http"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
func (h *Handler) createRepository(w http.ResponseWriter, r *http.Request, rt *route) {
diff --git a/server/gcp/artifactregistry/sdk_roundtrip_test.go b/server/gcp/artifactregistry/sdk_roundtrip_test.go
index 75ec192..08134ad 100644
--- a/server/gcp/artifactregistry/sdk_roundtrip_test.go
+++ b/server/gcp/artifactregistry/sdk_roundtrip_test.go
@@ -6,9 +6,9 @@ import (
"net/http/httptest"
"testing"
- "github.com/stackshy/cloudemu"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
ar "google.golang.org/api/artifactregistry/v1"
"google.golang.org/api/googleapi"
"google.golang.org/api/option"
diff --git a/server/gcp/artifactregistry/types.go b/server/gcp/artifactregistry/types.go
index c70cb57..4061ca8 100644
--- a/server/gcp/artifactregistry/types.go
+++ b/server/gcp/artifactregistry/types.go
@@ -4,7 +4,7 @@ import (
"strconv"
"strings"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
// repositoryJSON is the artifactregistry.googleapis.com v1 Repository shape.
diff --git a/server/gcp/cloudasset/filter.go b/server/gcp/cloudasset/filter.go
index 2bf5ba5..7639953 100644
--- a/server/gcp/cloudasset/filter.go
+++ b/server/gcp/cloudasset/filter.go
@@ -21,7 +21,7 @@ package cloudasset
import (
"strings"
- "github.com/stackshy/cloudemu/resourcediscovery"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
)
// GCP service identifiers as they appear in Cloud Asset assetType strings.
diff --git a/server/gcp/cloudasset/handler.go b/server/gcp/cloudasset/handler.go
index dc20bb6..a40439f 100644
--- a/server/gcp/cloudasset/handler.go
+++ b/server/gcp/cloudasset/handler.go
@@ -11,8 +11,8 @@ import (
"sync"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/resourcediscovery"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
)
// Path prefix shared by every Cloud Asset operation.
diff --git a/server/gcp/cloudasset/sdk_test.go b/server/gcp/cloudasset/sdk_test.go
index 774969c..b5de395 100644
--- a/server/gcp/cloudasset/sdk_test.go
+++ b/server/gcp/cloudasset/sdk_test.go
@@ -14,10 +14,10 @@ import (
"google.golang.org/api/cloudasset/v1"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
func TestSDKCloudAsset(t *testing.T) {
diff --git a/server/gcp/clouddns/handler.go b/server/gcp/clouddns/handler.go
index 585c665..8bbf115 100644
--- a/server/gcp/clouddns/handler.go
+++ b/server/gcp/clouddns/handler.go
@@ -22,8 +22,8 @@ import (
"net/http"
"strings"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
const (
diff --git a/server/gcp/clouddns/operations.go b/server/gcp/clouddns/operations.go
index 716757c..607a770 100644
--- a/server/gcp/clouddns/operations.go
+++ b/server/gcp/clouddns/operations.go
@@ -3,9 +3,9 @@ package clouddns
import (
"net/http"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
func (h *Handler) createZone(w http.ResponseWriter, r *http.Request, _ route) {
diff --git a/server/gcp/clouddns/sdk_roundtrip_test.go b/server/gcp/clouddns/sdk_roundtrip_test.go
index 4c6663a..12de18c 100644
--- a/server/gcp/clouddns/sdk_roundtrip_test.go
+++ b/server/gcp/clouddns/sdk_roundtrip_test.go
@@ -11,8 +11,8 @@ import (
"google.golang.org/api/googleapi"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const testProject = "demo"
diff --git a/server/gcp/clouddns/types.go b/server/gcp/clouddns/types.go
index 63cb3cf..939eb65 100644
--- a/server/gcp/clouddns/types.go
+++ b/server/gcp/clouddns/types.go
@@ -5,8 +5,8 @@ import (
"hash/fnv"
"strconv"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
// Kind values Cloud DNS stamps on its resources; the SDK tolerates them being
diff --git a/server/gcp/cloudfunctions/cloudfunctions_test.go b/server/gcp/cloudfunctions/cloudfunctions_test.go
index c698068..9e7675d 100644
--- a/server/gcp/cloudfunctions/cloudfunctions_test.go
+++ b/server/gcp/cloudfunctions/cloudfunctions_test.go
@@ -9,8 +9,8 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const (
diff --git a/server/gcp/cloudfunctions/handler.go b/server/gcp/cloudfunctions/handler.go
index 2a607a2..8d56472 100644
--- a/server/gcp/cloudfunctions/handler.go
+++ b/server/gcp/cloudfunctions/handler.go
@@ -23,8 +23,8 @@ import (
"strings"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- sdrv "github.com/stackshy/cloudemu/serverless/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ sdrv "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
const (
diff --git a/server/gcp/cloudfunctions/sdk_roundtrip_test.go b/server/gcp/cloudfunctions/sdk_roundtrip_test.go
index e812849..24d507a 100644
--- a/server/gcp/cloudfunctions/sdk_roundtrip_test.go
+++ b/server/gcp/cloudfunctions/sdk_roundtrip_test.go
@@ -6,8 +6,8 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
"google.golang.org/api/cloudfunctions/v1"
"google.golang.org/api/option"
)
diff --git a/server/gcp/cloudlogging/handler.go b/server/gcp/cloudlogging/handler.go
index 109349e..9c60f11 100644
--- a/server/gcp/cloudlogging/handler.go
+++ b/server/gcp/cloudlogging/handler.go
@@ -29,8 +29,8 @@ import (
"net/http"
"strings"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
const (
diff --git a/server/gcp/cloudlogging/operations.go b/server/gcp/cloudlogging/operations.go
index da05830..58c510d 100644
--- a/server/gcp/cloudlogging/operations.go
+++ b/server/gcp/cloudlogging/operations.go
@@ -5,9 +5,9 @@ import (
"net/http"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
// writeEntries maps WriteLogEntries onto the driver. Cloud Logging creates a log
diff --git a/server/gcp/cloudlogging/sdk_roundtrip_test.go b/server/gcp/cloudlogging/sdk_roundtrip_test.go
index 2bfe16a..aa0a39c 100644
--- a/server/gcp/cloudlogging/sdk_roundtrip_test.go
+++ b/server/gcp/cloudlogging/sdk_roundtrip_test.go
@@ -9,8 +9,8 @@ import (
logging "google.golang.org/api/logging/v2"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const testProject = "demo"
diff --git a/server/gcp/cloudlogging/types.go b/server/gcp/cloudlogging/types.go
index 5cffa73..6ea3856 100644
--- a/server/gcp/cloudlogging/types.go
+++ b/server/gcp/cloudlogging/types.go
@@ -5,7 +5,7 @@ import (
"strings"
"time"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
// logEntryJSON is the subset of the Cloud Logging LogEntry resource we model:
diff --git a/server/gcp/cloudsql/handler.go b/server/gcp/cloudsql/handler.go
index 94eac34..16290f7 100644
--- a/server/gcp/cloudsql/handler.go
+++ b/server/gcp/cloudsql/handler.go
@@ -33,7 +33,7 @@ import (
"net/http"
"strings"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
const (
diff --git a/server/gcp/cloudsql/operations.go b/server/gcp/cloudsql/operations.go
index f8407fe..66f717f 100644
--- a/server/gcp/cloudsql/operations.go
+++ b/server/gcp/cloudsql/operations.go
@@ -5,7 +5,7 @@ import (
"strconv"
"time"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// instanceFromBody decodes a Cloud SQL instance request and converts it to
diff --git a/server/gcp/cloudsql/sdk_roundtrip_test.go b/server/gcp/cloudsql/sdk_roundtrip_test.go
index 21b2b63..8840321 100644
--- a/server/gcp/cloudsql/sdk_roundtrip_test.go
+++ b/server/gcp/cloudsql/sdk_roundtrip_test.go
@@ -8,8 +8,8 @@ import (
"google.golang.org/api/option"
sqladmin "google.golang.org/api/sqladmin/v1"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
func newSDKClient(t *testing.T) (*sqladmin.Service, string) {
diff --git a/server/gcp/cloudsql/types.go b/server/gcp/cloudsql/types.go
index 31760a8..88a7ea6 100644
--- a/server/gcp/cloudsql/types.go
+++ b/server/gcp/cloudsql/types.go
@@ -4,8 +4,8 @@ import (
"encoding/json"
"net/http"
- cerrors "github.com/stackshy/cloudemu/errors"
- rdsdriver "github.com/stackshy/cloudemu/relationaldb/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ rdsdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// Cloud SQL activation policy values. Real Cloud SQL exposes a third
diff --git a/server/gcp/compute/compute_test.go b/server/gcp/compute/compute_test.go
index 0934a25..9340e22 100644
--- a/server/gcp/compute/compute_test.go
+++ b/server/gcp/compute/compute_test.go
@@ -9,8 +9,8 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const (
diff --git a/server/gcp/compute/disks.go b/server/gcp/compute/disks.go
index b34cb55..30fedb8 100644
--- a/server/gcp/compute/disks.go
+++ b/server/gcp/compute/disks.go
@@ -6,9 +6,9 @@ import (
"strconv"
"strings"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// gcpDiskNameTag is the tag key used to round-trip the GCP disk name through
diff --git a/server/gcp/compute/disks_sdk_test.go b/server/gcp/compute/disks_sdk_test.go
index 1c00450..3ca97e3 100644
--- a/server/gcp/compute/disks_sdk_test.go
+++ b/server/gcp/compute/disks_sdk_test.go
@@ -10,8 +10,8 @@ import (
"cloud.google.com/go/compute/apiv1/computepb"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
// newDisksSDKClient builds a real google-cloud-go DisksRESTClient pointing
diff --git a/server/gcp/compute/handler.go b/server/gcp/compute/handler.go
index be4252c..35aae8a 100644
--- a/server/gcp/compute/handler.go
+++ b/server/gcp/compute/handler.go
@@ -22,8 +22,8 @@ import (
"net/http"
"strings"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// Resource type names used in URL routing.
diff --git a/server/gcp/compute/images.go b/server/gcp/compute/images.go
index 397f1ee..e59dad1 100644
--- a/server/gcp/compute/images.go
+++ b/server/gcp/compute/images.go
@@ -5,9 +5,9 @@ import (
"net/http"
"strings"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// gcpImageNameTag is the tag we round-trip the image name through.
diff --git a/server/gcp/compute/images_sdk_test.go b/server/gcp/compute/images_sdk_test.go
index 1d0226d..add10b3 100644
--- a/server/gcp/compute/images_sdk_test.go
+++ b/server/gcp/compute/images_sdk_test.go
@@ -10,8 +10,8 @@ import (
"cloud.google.com/go/compute/apiv1/computepb"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
func newImagesSDKClient(t *testing.T, ts *httptest.Server) *gcpcompute.ImagesClient {
diff --git a/server/gcp/compute/instances.go b/server/gcp/compute/instances.go
index 22acea5..121b7ea 100644
--- a/server/gcp/compute/instances.go
+++ b/server/gcp/compute/instances.go
@@ -6,9 +6,9 @@ import (
"strconv"
"strings"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// gcpNameTag is the tag key we use to round-trip the GCP instance name
diff --git a/server/gcp/compute/sdk_roundtrip_test.go b/server/gcp/compute/sdk_roundtrip_test.go
index e596947..b28865e 100644
--- a/server/gcp/compute/sdk_roundtrip_test.go
+++ b/server/gcp/compute/sdk_roundtrip_test.go
@@ -10,8 +10,8 @@ import (
"cloud.google.com/go/compute/apiv1/computepb"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
// newSDKInstancesClient builds a real google-cloud-go InstancesRESTClient
diff --git a/server/gcp/compute/snapshots.go b/server/gcp/compute/snapshots.go
index 3ca062b..769d722 100644
--- a/server/gcp/compute/snapshots.go
+++ b/server/gcp/compute/snapshots.go
@@ -6,9 +6,9 @@ import (
"strconv"
"strings"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// gcpSnapshotNameTag is the tag we round-trip the snapshot name through.
diff --git a/server/gcp/compute/snapshots_sdk_test.go b/server/gcp/compute/snapshots_sdk_test.go
index e15d574..bcf3a94 100644
--- a/server/gcp/compute/snapshots_sdk_test.go
+++ b/server/gcp/compute/snapshots_sdk_test.go
@@ -9,8 +9,8 @@ import (
"cloud.google.com/go/compute/apiv1/computepb"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
func newSnapshotsSDKClient(t *testing.T, ts *httptest.Server) *gcpcompute.SnapshotsClient {
diff --git a/server/gcp/eventarc/handler.go b/server/gcp/eventarc/handler.go
index 0e2258b..3bb9994 100644
--- a/server/gcp/eventarc/handler.go
+++ b/server/gcp/eventarc/handler.go
@@ -42,8 +42,8 @@ import (
"net/http"
"strings"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
const (
diff --git a/server/gcp/eventarc/operations.go b/server/gcp/eventarc/operations.go
index 760200f..1a7a7a1 100644
--- a/server/gcp/eventarc/operations.go
+++ b/server/gcp/eventarc/operations.go
@@ -3,9 +3,9 @@ package eventarc
import (
"net/http"
- cerrors "github.com/stackshy/cloudemu/errors"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
func (h *Handler) createTrigger(w http.ResponseWriter, r *http.Request, rt *route) {
diff --git a/server/gcp/eventarc/sdk_roundtrip_test.go b/server/gcp/eventarc/sdk_roundtrip_test.go
index dbc3ba1..86ec5aa 100644
--- a/server/gcp/eventarc/sdk_roundtrip_test.go
+++ b/server/gcp/eventarc/sdk_roundtrip_test.go
@@ -10,8 +10,8 @@ import (
"google.golang.org/api/googleapi"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const (
diff --git a/server/gcp/eventarc/types.go b/server/gcp/eventarc/types.go
index 058a729..b815459 100644
--- a/server/gcp/eventarc/types.go
+++ b/server/gcp/eventarc/types.go
@@ -3,7 +3,7 @@ package eventarc
import (
"encoding/json"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
// destinationTargetID is the fixed target id under which a trigger's
diff --git a/server/gcp/fcm/handler.go b/server/gcp/fcm/handler.go
index 60d98a2..cfa16b6 100644
--- a/server/gcp/fcm/handler.go
+++ b/server/gcp/fcm/handler.go
@@ -28,8 +28,8 @@ import (
"net/http"
"strings"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
const (
diff --git a/server/gcp/fcm/operations.go b/server/gcp/fcm/operations.go
index 10b811b..361006a 100644
--- a/server/gcp/fcm/operations.go
+++ b/server/gcp/fcm/operations.go
@@ -3,9 +3,9 @@ package fcm
import (
"net/http"
- cerrors "github.com/stackshy/cloudemu/errors"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
// sendMessageRequest is the FCM messages:send request body. Only the fields the
diff --git a/server/gcp/fcm/sdk_roundtrip_test.go b/server/gcp/fcm/sdk_roundtrip_test.go
index 63c9282..3daa71d 100644
--- a/server/gcp/fcm/sdk_roundtrip_test.go
+++ b/server/gcp/fcm/sdk_roundtrip_test.go
@@ -11,8 +11,8 @@ import (
"google.golang.org/api/googleapi"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const testProject = "demo"
diff --git a/server/gcp/firestore/firestore_test.go b/server/gcp/firestore/firestore_test.go
index e918ae3..f6a0cb2 100644
--- a/server/gcp/firestore/firestore_test.go
+++ b/server/gcp/firestore/firestore_test.go
@@ -9,9 +9,9 @@ import (
"google.golang.org/api/iterator"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
)
const testProject = "p1"
@@ -50,8 +50,8 @@ func TestSDKFirestoreRoundTrip(t *testing.T) {
// Set (create) a document.
docRef := coll.Doc("u1")
if _, err := docRef.Set(ctx, map[string]any{
- "name": "Alice",
- "age": 30,
+ "name": "Alice",
+ "age": 30,
"active": true,
}); err != nil {
t.Fatalf("Set: %v", err)
diff --git a/server/gcp/firestore/handler.go b/server/gcp/firestore/handler.go
index 5b751a7..bc21507 100644
--- a/server/gcp/firestore/handler.go
+++ b/server/gcp/firestore/handler.go
@@ -20,8 +20,8 @@ import (
"strings"
"time"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
)
// Path-segment values used in Firestore REST URLs.
diff --git a/server/gcp/gcp.go b/server/gcp/gcp.go
index 5cbd4f5..808ef7c 100644
--- a/server/gcp/gcp.go
+++ b/server/gcp/gcp.go
@@ -7,48 +7,48 @@
package gcp
import (
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- crdriver "github.com/stackshy/cloudemu/containerregistry/driver"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- dnsdriver "github.com/stackshy/cloudemu/dns/driver"
- ebdriver "github.com/stackshy/cloudemu/eventbus/driver"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
- "github.com/stackshy/cloudemu/kubernetes"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
- logdriver "github.com/stackshy/cloudemu/logging/driver"
- mqdriver "github.com/stackshy/cloudemu/messagequeue/driver"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- notifdriver "github.com/stackshy/cloudemu/notification/driver"
- gkeprov "github.com/stackshy/cloudemu/providers/gcp/gke"
- rdbdriver "github.com/stackshy/cloudemu/relationaldb/driver"
- "github.com/stackshy/cloudemu/resourcediscovery"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
- "github.com/stackshy/cloudemu/server"
- "github.com/stackshy/cloudemu/server/gcp/artifactregistry"
- "github.com/stackshy/cloudemu/server/gcp/cloudasset"
- "github.com/stackshy/cloudemu/server/gcp/clouddns"
- "github.com/stackshy/cloudemu/server/gcp/cloudfunctions"
- cloudloggingsrv "github.com/stackshy/cloudemu/server/gcp/cloudlogging"
- "github.com/stackshy/cloudemu/server/gcp/cloudsql"
- "github.com/stackshy/cloudemu/server/gcp/compute"
- "github.com/stackshy/cloudemu/server/gcp/eventarc"
- fcmsrv "github.com/stackshy/cloudemu/server/gcp/fcm"
- "github.com/stackshy/cloudemu/server/gcp/firestore"
- "github.com/stackshy/cloudemu/server/gcp/gcs"
- "github.com/stackshy/cloudemu/server/gcp/gke"
- "github.com/stackshy/cloudemu/server/gcp/iam"
- lbsrv "github.com/stackshy/cloudemu/server/gcp/loadbalancer"
- memorystoresrv "github.com/stackshy/cloudemu/server/gcp/memorystore"
- "github.com/stackshy/cloudemu/server/gcp/monitoring"
- "github.com/stackshy/cloudemu/server/gcp/networks"
- "github.com/stackshy/cloudemu/server/gcp/pubsub"
- secretmanagersrv "github.com/stackshy/cloudemu/server/gcp/secretmanager"
- vertexaisrv "github.com/stackshy/cloudemu/server/gcp/vertexai"
- sdrv "github.com/stackshy/cloudemu/serverless/driver"
- storagedriver "github.com/stackshy/cloudemu/storage/driver"
- vertexaidriver "github.com/stackshy/cloudemu/vertexai/driver"
+ gkeprov "github.com/stackshy/cloudemu/v2/providers/gcp/gke"
+ "github.com/stackshy/cloudemu/v2/server"
+ "github.com/stackshy/cloudemu/v2/server/gcp/artifactregistry"
+ "github.com/stackshy/cloudemu/v2/server/gcp/cloudasset"
+ "github.com/stackshy/cloudemu/v2/server/gcp/clouddns"
+ "github.com/stackshy/cloudemu/v2/server/gcp/cloudfunctions"
+ cloudloggingsrv "github.com/stackshy/cloudemu/v2/server/gcp/cloudlogging"
+ "github.com/stackshy/cloudemu/v2/server/gcp/cloudsql"
+ "github.com/stackshy/cloudemu/v2/server/gcp/compute"
+ "github.com/stackshy/cloudemu/v2/server/gcp/eventarc"
+ fcmsrv "github.com/stackshy/cloudemu/v2/server/gcp/fcm"
+ "github.com/stackshy/cloudemu/v2/server/gcp/firestore"
+ "github.com/stackshy/cloudemu/v2/server/gcp/gcs"
+ "github.com/stackshy/cloudemu/v2/server/gcp/gke"
+ "github.com/stackshy/cloudemu/v2/server/gcp/iam"
+ lbsrv "github.com/stackshy/cloudemu/v2/server/gcp/loadbalancer"
+ memorystoresrv "github.com/stackshy/cloudemu/v2/server/gcp/memorystore"
+ "github.com/stackshy/cloudemu/v2/server/gcp/monitoring"
+ "github.com/stackshy/cloudemu/v2/server/gcp/networks"
+ "github.com/stackshy/cloudemu/v2/server/gcp/pubsub"
+ secretmanagersrv "github.com/stackshy/cloudemu/v2/server/gcp/secretmanager"
+ vertexaisrv "github.com/stackshy/cloudemu/v2/server/gcp/vertexai"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ crdriver "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
+ dnsdriver "github.com/stackshy/cloudemu/v2/services/dns/driver"
+ ebdriver "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
+ "github.com/stackshy/cloudemu/v2/services/kubernetes"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
+ logdriver "github.com/stackshy/cloudemu/v2/services/logging/driver"
+ mqdriver "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
+ notifdriver "github.com/stackshy/cloudemu/v2/services/notification/driver"
+ rdbdriver "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/services/resourcediscovery"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
+ sdrv "github.com/stackshy/cloudemu/v2/services/serverless/driver"
+ storagedriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
+ vertexaidriver "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// Drivers bundles the driver interfaces the GCP server can expose.
diff --git a/server/gcp/gcs/gcs_test.go b/server/gcp/gcs/gcs_test.go
index 82f15db..7ea3e37 100644
--- a/server/gcp/gcs/gcs_test.go
+++ b/server/gcp/gcs/gcs_test.go
@@ -12,8 +12,8 @@ import (
"google.golang.org/api/iterator"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
// TestSDKGCSRoundTrip drives Google Cloud Storage operations through the real
diff --git a/server/gcp/gcs/handler.go b/server/gcp/gcs/handler.go
index dd1f2e2..3db5a3f 100644
--- a/server/gcp/gcs/handler.go
+++ b/server/gcp/gcs/handler.go
@@ -26,8 +26,8 @@ import (
"strings"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
- storagedriver "github.com/stackshy/cloudemu/storage/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ storagedriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
const (
diff --git a/server/gcp/gke/handler.go b/server/gcp/gke/handler.go
index 6c73168..e507d1e 100644
--- a/server/gcp/gke/handler.go
+++ b/server/gcp/gke/handler.go
@@ -46,7 +46,7 @@ import (
"net/http"
"strings"
- "github.com/stackshy/cloudemu/providers/gcp/gke"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/gke"
)
const (
diff --git a/server/gcp/gke/operations.go b/server/gcp/gke/operations.go
index 98a87e0..cb07b38 100644
--- a/server/gcp/gke/operations.go
+++ b/server/gcp/gke/operations.go
@@ -3,7 +3,7 @@ package gke
import (
"net/http"
- "github.com/stackshy/cloudemu/providers/gcp/gke"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/gke"
)
func (h *Handler) createCluster(w http.ResponseWriter, r *http.Request, p *gkePath) {
diff --git a/server/gcp/gke/sdk_dataplane_test.go b/server/gcp/gke/sdk_dataplane_test.go
index 48ba28a..4e8976c 100644
--- a/server/gcp/gke/sdk_dataplane_test.go
+++ b/server/gcp/gke/sdk_dataplane_test.go
@@ -20,9 +20,9 @@ import (
kubescheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
- "github.com/stackshy/cloudemu"
- cloudkube "github.com/stackshy/cloudemu/kubernetes"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
+ cloudkube "github.com/stackshy/cloudemu/v2/services/kubernetes"
)
// TestSDKGKEDataPlane_FullWorkloadStack drives the full path:
diff --git a/server/gcp/gke/sdk_roundtrip_test.go b/server/gcp/gke/sdk_roundtrip_test.go
index 857a784..64447bc 100644
--- a/server/gcp/gke/sdk_roundtrip_test.go
+++ b/server/gcp/gke/sdk_roundtrip_test.go
@@ -9,9 +9,9 @@ import (
"google.golang.org/api/container/v1"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gkeprov "github.com/stackshy/cloudemu/providers/gcp/gke"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gkeprov "github.com/stackshy/cloudemu/v2/providers/gcp/gke"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
func newSDKClient(t *testing.T) (*container.Service, string) {
diff --git a/server/gcp/gke/types.go b/server/gcp/gke/types.go
index a360c6a..1fa3023 100644
--- a/server/gcp/gke/types.go
+++ b/server/gcp/gke/types.go
@@ -4,8 +4,8 @@ import (
"encoding/json"
"net/http"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/providers/gcp/gke"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/providers/gcp/gke"
)
// gkeCluster mirrors google.golang.org/api/container/v1.Cluster's wire shape
diff --git a/server/gcp/iam/handler.go b/server/gcp/iam/handler.go
index ebd28bb..7470789 100644
--- a/server/gcp/iam/handler.go
+++ b/server/gcp/iam/handler.go
@@ -37,7 +37,7 @@ import (
"net/http"
"strings"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
const (
diff --git a/server/gcp/iam/operations.go b/server/gcp/iam/operations.go
index 54b920a..546ebe4 100644
--- a/server/gcp/iam/operations.go
+++ b/server/gcp/iam/operations.go
@@ -6,8 +6,8 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- iamdriver "github.com/stackshy/cloudemu/iam/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ iamdriver "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
const maxBodyBytes = 1 << 20
diff --git a/server/gcp/iam/sdk_roundtrip_test.go b/server/gcp/iam/sdk_roundtrip_test.go
index 4c33e0c..bfb361a 100644
--- a/server/gcp/iam/sdk_roundtrip_test.go
+++ b/server/gcp/iam/sdk_roundtrip_test.go
@@ -6,8 +6,8 @@ import (
"net/http/httptest"
"testing"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
"google.golang.org/api/googleapi"
iamv1 "google.golang.org/api/iam/v1"
"google.golang.org/api/option"
diff --git a/server/gcp/loadbalancer/handler.go b/server/gcp/loadbalancer/handler.go
index 5540efe..419140f 100644
--- a/server/gcp/loadbalancer/handler.go
+++ b/server/gcp/loadbalancer/handler.go
@@ -35,8 +35,8 @@ package loadbalancer
import (
"net/http"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
const (
diff --git a/server/gcp/loadbalancer/operations.go b/server/gcp/loadbalancer/operations.go
index 0a8d6b3..fabaa66 100644
--- a/server/gcp/loadbalancer/operations.go
+++ b/server/gcp/loadbalancer/operations.go
@@ -6,9 +6,9 @@ import (
"strconv"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
// --- backend services (target groups) ---
diff --git a/server/gcp/loadbalancer/sdk_roundtrip_test.go b/server/gcp/loadbalancer/sdk_roundtrip_test.go
index fa5d1d4..f33c6be 100644
--- a/server/gcp/loadbalancer/sdk_roundtrip_test.go
+++ b/server/gcp/loadbalancer/sdk_roundtrip_test.go
@@ -10,8 +10,8 @@ import (
"google.golang.org/api/iterator"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const testProject = "proj-1"
diff --git a/server/gcp/memorystore/handler.go b/server/gcp/memorystore/handler.go
index 6ac488c..0316029 100644
--- a/server/gcp/memorystore/handler.go
+++ b/server/gcp/memorystore/handler.go
@@ -31,8 +31,8 @@ import (
"net/http"
"strings"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
const (
diff --git a/server/gcp/memorystore/operations.go b/server/gcp/memorystore/operations.go
index 2337a6d..8683ab6 100644
--- a/server/gcp/memorystore/operations.go
+++ b/server/gcp/memorystore/operations.go
@@ -4,8 +4,8 @@ import (
"encoding/json"
"net/http"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
// createInstance handles POST .../instances?instanceId={i} — Create. The
diff --git a/server/gcp/memorystore/sdk_roundtrip_test.go b/server/gcp/memorystore/sdk_roundtrip_test.go
index 2f2c63f..fe3b96f 100644
--- a/server/gcp/memorystore/sdk_roundtrip_test.go
+++ b/server/gcp/memorystore/sdk_roundtrip_test.go
@@ -10,8 +10,8 @@ import (
"google.golang.org/api/option"
redis "google.golang.org/api/redis/v1"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const (
diff --git a/server/gcp/memorystore/types.go b/server/gcp/memorystore/types.go
index 3e91f12..6b86851 100644
--- a/server/gcp/memorystore/types.go
+++ b/server/gcp/memorystore/types.go
@@ -5,7 +5,7 @@ import (
"strconv"
"strings"
- cachedriver "github.com/stackshy/cloudemu/cache/driver"
+ cachedriver "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
const (
diff --git a/server/gcp/monitoring/handler.go b/server/gcp/monitoring/handler.go
index 3dc1467..048c0ae 100644
--- a/server/gcp/monitoring/handler.go
+++ b/server/gcp/monitoring/handler.go
@@ -16,8 +16,8 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- mondriver "github.com/stackshy/cloudemu/monitoring/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ mondriver "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
const (
diff --git a/server/gcp/monitoring/monitoring_test.go b/server/gcp/monitoring/monitoring_test.go
index 960e574..f22bc73 100644
--- a/server/gcp/monitoring/monitoring_test.go
+++ b/server/gcp/monitoring/monitoring_test.go
@@ -7,8 +7,8 @@ import (
"net/http/httptest"
"testing"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
// HTTP-level test for the GCP Cloud Monitoring handler. Real
diff --git a/server/gcp/networks/handler.go b/server/gcp/networks/handler.go
index 5b62862..0f92cb4 100644
--- a/server/gcp/networks/handler.go
+++ b/server/gcp/networks/handler.go
@@ -26,9 +26,9 @@ import (
"net/http"
"strconv"
- cerrors "github.com/stackshy/cloudemu/errors"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
const (
diff --git a/server/gcp/networks/networks_test.go b/server/gcp/networks/networks_test.go
index f0d3ca2..c3dbd85 100644
--- a/server/gcp/networks/networks_test.go
+++ b/server/gcp/networks/networks_test.go
@@ -9,8 +9,8 @@ import (
"cloud.google.com/go/compute/apiv1/computepb"
"google.golang.org/api/option"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const (
diff --git a/server/gcp/pubsub/handler.go b/server/gcp/pubsub/handler.go
index 4fd730a..47c09f7 100644
--- a/server/gcp/pubsub/handler.go
+++ b/server/gcp/pubsub/handler.go
@@ -30,8 +30,8 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
- mqdriver "github.com/stackshy/cloudemu/messagequeue/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ mqdriver "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
)
const (
diff --git a/server/gcp/pubsub/pubsub_test.go b/server/gcp/pubsub/pubsub_test.go
index ae2ec63..5fb0aed 100644
--- a/server/gcp/pubsub/pubsub_test.go
+++ b/server/gcp/pubsub/pubsub_test.go
@@ -10,8 +10,8 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const project = "demo"
diff --git a/server/gcp/pubsub/sdk_roundtrip_test.go b/server/gcp/pubsub/sdk_roundtrip_test.go
index fccef7b..fab4a9b 100644
--- a/server/gcp/pubsub/sdk_roundtrip_test.go
+++ b/server/gcp/pubsub/sdk_roundtrip_test.go
@@ -7,8 +7,8 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
"google.golang.org/api/option"
pubsubv1 "google.golang.org/api/pubsub/v1"
)
diff --git a/server/gcp/secretmanager/handler.go b/server/gcp/secretmanager/handler.go
index 1a8649f..63d87e5 100644
--- a/server/gcp/secretmanager/handler.go
+++ b/server/gcp/secretmanager/handler.go
@@ -23,8 +23,8 @@ import (
"net/http"
"strings"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
const (
diff --git a/server/gcp/secretmanager/operations.go b/server/gcp/secretmanager/operations.go
index 9abe310..1f75b8b 100644
--- a/server/gcp/secretmanager/operations.go
+++ b/server/gcp/secretmanager/operations.go
@@ -3,8 +3,8 @@ package secretmanager
import (
"net/http"
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
func (h *Handler) createSecret(w http.ResponseWriter, r *http.Request, rt route) {
diff --git a/server/gcp/secretmanager/sdk_roundtrip_test.go b/server/gcp/secretmanager/sdk_roundtrip_test.go
index 1669771..3cfac48 100644
--- a/server/gcp/secretmanager/sdk_roundtrip_test.go
+++ b/server/gcp/secretmanager/sdk_roundtrip_test.go
@@ -11,8 +11,8 @@ import (
"google.golang.org/api/option"
sm "google.golang.org/api/secretmanager/v1"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const testParent = "projects/demo"
diff --git a/server/gcp/secretmanager/types.go b/server/gcp/secretmanager/types.go
index 3c06756..f409535 100644
--- a/server/gcp/secretmanager/types.go
+++ b/server/gcp/secretmanager/types.go
@@ -1,7 +1,7 @@
package secretmanager
import (
- secretsdriver "github.com/stackshy/cloudemu/secrets/driver"
+ secretsdriver "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
// stateEnabled is the lifecycle state reported for every version — the mock
diff --git a/server/gcp/vertexai/cachedcontents.go b/server/gcp/vertexai/cachedcontents.go
index 4daf783..51027de 100644
--- a/server/gcp/vertexai/cachedcontents.go
+++ b/server/gcp/vertexai/cachedcontents.go
@@ -3,7 +3,7 @@ package vertexai
import (
"net/http"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func cachedContentJSON(c *driver.CachedContent) map[string]any {
diff --git a/server/gcp/vertexai/datasets.go b/server/gcp/vertexai/datasets.go
index 29d2f82..96feebe 100644
--- a/server/gcp/vertexai/datasets.go
+++ b/server/gcp/vertexai/datasets.go
@@ -3,7 +3,7 @@ package vertexai
import (
"net/http"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func datasetJSON(d *driver.Dataset) map[string]any {
diff --git a/server/gcp/vertexai/endpoints.go b/server/gcp/vertexai/endpoints.go
index 4a3540b..97e8170 100644
--- a/server/gcp/vertexai/endpoints.go
+++ b/server/gcp/vertexai/endpoints.go
@@ -5,7 +5,7 @@ import (
"io"
"net/http"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func endpointJSON(e *driver.Endpoint) map[string]any {
diff --git a/server/gcp/vertexai/featureregistry.go b/server/gcp/vertexai/featureregistry.go
index 11aef88..e86ed1c 100644
--- a/server/gcp/vertexai/featureregistry.go
+++ b/server/gcp/vertexai/featureregistry.go
@@ -3,7 +3,7 @@ package vertexai
import (
"net/http"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// --- Feature groups + features (Feature Registry) ---
diff --git a/server/gcp/vertexai/featurestore.go b/server/gcp/vertexai/featurestore.go
index bb8bb77..eb9dfc8 100644
--- a/server/gcp/vertexai/featurestore.go
+++ b/server/gcp/vertexai/featurestore.go
@@ -3,7 +3,7 @@ package vertexai
import (
"net/http"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// --- Featurestores ---
diff --git a/server/gcp/vertexai/genai.go b/server/gcp/vertexai/genai.go
index 33ce34b..f9f6a95 100644
--- a/server/gcp/vertexai/genai.go
+++ b/server/gcp/vertexai/genai.go
@@ -4,7 +4,7 @@ import (
"net/http"
"strings"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// wireContent is the JSON shape of a generateContent content turn.
diff --git a/server/gcp/vertexai/handler.go b/server/gcp/vertexai/handler.go
index c7ae103..439b7f9 100644
--- a/server/gcp/vertexai/handler.go
+++ b/server/gcp/vertexai/handler.go
@@ -28,7 +28,7 @@ import (
"net/http"
"strings"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
const (
diff --git a/server/gcp/vertexai/jobs.go b/server/gcp/vertexai/jobs.go
index cab231d..6402cd1 100644
--- a/server/gcp/vertexai/jobs.go
+++ b/server/gcp/vertexai/jobs.go
@@ -3,7 +3,7 @@ package vertexai
import (
"net/http"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// --- Custom jobs ---
diff --git a/server/gcp/vertexai/metadata.go b/server/gcp/vertexai/metadata.go
index 3b20899..36177e6 100644
--- a/server/gcp/vertexai/metadata.go
+++ b/server/gcp/vertexai/metadata.go
@@ -3,7 +3,7 @@ package vertexai
import (
"net/http"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// --- Metadata stores ---
diff --git a/server/gcp/vertexai/models.go b/server/gcp/vertexai/models.go
index ad16955..feb1fdf 100644
--- a/server/gcp/vertexai/models.go
+++ b/server/gcp/vertexai/models.go
@@ -3,7 +3,7 @@ package vertexai
import (
"net/http"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func modelJSON(m *driver.Model) map[string]any {
diff --git a/server/gcp/vertexai/pipelines.go b/server/gcp/vertexai/pipelines.go
index 841d29a..e732c89 100644
--- a/server/gcp/vertexai/pipelines.go
+++ b/server/gcp/vertexai/pipelines.go
@@ -3,7 +3,7 @@ package vertexai
import (
"net/http"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// --- Training pipelines ---
diff --git a/server/gcp/vertexai/responses.go b/server/gcp/vertexai/responses.go
index fd39476..bd62548 100644
--- a/server/gcp/vertexai/responses.go
+++ b/server/gcp/vertexai/responses.go
@@ -3,8 +3,8 @@ package vertexai
import (
"net/http"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func writeJSON(w http.ResponseWriter, v any) {
diff --git a/server/gcp/vertexai/sdk_roundtrip_test.go b/server/gcp/vertexai/sdk_roundtrip_test.go
index c5967e4..71c63ee 100644
--- a/server/gcp/vertexai/sdk_roundtrip_test.go
+++ b/server/gcp/vertexai/sdk_roundtrip_test.go
@@ -12,8 +12,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu"
- gcpserver "github.com/stackshy/cloudemu/server/gcp"
+ "github.com/stackshy/cloudemu/v2"
+ gcpserver "github.com/stackshy/cloudemu/v2/server/gcp"
)
const base = "/v1/projects/mock-project/locations/us-central1"
diff --git a/server/gcp/vertexai/tuning.go b/server/gcp/vertexai/tuning.go
index 56dcbbe..aa97a52 100644
--- a/server/gcp/vertexai/tuning.go
+++ b/server/gcp/vertexai/tuning.go
@@ -3,7 +3,7 @@ package vertexai
import (
"net/http"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// --- Hyperparameter tuning jobs ---
diff --git a/server/gcp/vertexai/vectorsearch.go b/server/gcp/vertexai/vectorsearch.go
index f4920b0..ac2f619 100644
--- a/server/gcp/vertexai/vectorsearch.go
+++ b/server/gcp/vertexai/vectorsearch.go
@@ -3,7 +3,7 @@ package vertexai
import (
"net/http"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// --- Indexes ---
diff --git a/server/wire/awsquery/awsquery_test.go b/server/wire/awsquery/awsquery_test.go
index 283ca4c..375bb5a 100644
--- a/server/wire/awsquery/awsquery_test.go
+++ b/server/wire/awsquery/awsquery_test.go
@@ -8,7 +8,7 @@ import (
"strings"
"testing"
- "github.com/stackshy/cloudemu/server/wire/awsquery"
+ "github.com/stackshy/cloudemu/v2/server/wire/awsquery"
)
func TestListStrings(t *testing.T) {
@@ -112,13 +112,13 @@ func TestFlatTags(t *testing.T) {
func TestCollectIndices(t *testing.T) {
form := url.Values{
- "Foo.1": {"a"},
- "Foo.2.Bar": {"b"},
- "Foo.10": {"j"},
- "Foo.3.Baz.1": {"c"},
- "Other.1": {"x"},
- "NotIndexed": {"y"},
- "Foo.notanint": {"z"},
+ "Foo.1": {"a"},
+ "Foo.2.Bar": {"b"},
+ "Foo.10": {"j"},
+ "Foo.3.Baz.1": {"c"},
+ "Other.1": {"x"},
+ "NotIndexed": {"y"},
+ "Foo.notanint": {"z"},
}
got := awsquery.CollectIndices(form, "Foo")
diff --git a/server/wire/azurearm/azurearm.go b/server/wire/azurearm/azurearm.go
index 976ac21..9f35083 100644
--- a/server/wire/azurearm/azurearm.go
+++ b/server/wire/azurearm/azurearm.go
@@ -16,7 +16,7 @@ import (
"net/http"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
// ContentType is the JSON content type used by all ARM responses.
diff --git a/server/wire/azurearm/azurearm_test.go b/server/wire/azurearm/azurearm_test.go
index 878876f..df2c864 100644
--- a/server/wire/azurearm/azurearm_test.go
+++ b/server/wire/azurearm/azurearm_test.go
@@ -8,8 +8,8 @@ import (
"strings"
"testing"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/azurearm"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/azurearm"
)
func TestParsePathSubscriptionOnly(t *testing.T) {
diff --git a/server/wire/gcprest/gcprest.go b/server/wire/gcprest/gcprest.go
index eaa9ec9..0839434 100644
--- a/server/wire/gcprest/gcprest.go
+++ b/server/wire/gcprest/gcprest.go
@@ -20,7 +20,7 @@ import (
"strings"
"time"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
// ContentType is the JSON content type used by all REST responses.
diff --git a/server/wire/gcprest/gcprest_test.go b/server/wire/gcprest/gcprest_test.go
index a2ca078..21b2387 100644
--- a/server/wire/gcprest/gcprest_test.go
+++ b/server/wire/gcprest/gcprest_test.go
@@ -8,8 +8,8 @@ import (
"strings"
"testing"
- cerrors "github.com/stackshy/cloudemu/errors"
- "github.com/stackshy/cloudemu/server/wire/gcprest"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/server/wire/gcprest"
)
func TestParsePathRejectsNonComputePrefix(t *testing.T) {
diff --git a/services/azureai/azureai.go b/services/azureai/azureai.go
new file mode 100644
index 0000000..9973b06
--- /dev/null
+++ b/services/azureai/azureai.go
@@ -0,0 +1,129 @@
+// Package azureai provides a portable Azure AI API with cross-cutting concerns.
+// It wraps a driver.AzureAI (both ARM providers plus the data planes) with
+// recording, metrics, rate limiting, error injection, and latency simulation —
+// the same middle layer every other service ships (see bedrock/bedrock.go,
+// sagemaker/sagemaker.go, vertexai/vertexai.go) so Azure AI participates in the
+// three-layer design.
+//
+// The wrapper implements driver.AzureAI, so it is a drop-in replacement for the
+// raw provider mock when constructing the SDK-compat server.
+package azureai
+
+import (
+ "context"
+ "time"
+
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+// Compile-time check that the portable type implements the full service.
+var _ driver.AzureAI = (*AzureAI)(nil)
+
+// AzureAI is the portable Azure AI type wrapping a driver with cross-cutting
+// concerns.
+type AzureAI struct {
+ drv driver.AzureAI
+ recorder *recorder.Recorder
+ metrics *metrics.Collector
+ limiter *ratelimit.Limiter
+ injector *inject.Injector
+ latency time.Duration
+}
+
+// New creates a portable AzureAI wrapping the given driver.
+func New(d driver.AzureAI, opts ...Option) *AzureAI {
+ a := &AzureAI{drv: d}
+ for _, opt := range opts {
+ opt(a)
+ }
+
+ return a
+}
+
+// Option configures a portable AzureAI.
+type Option func(*AzureAI)
+
+// WithRecorder sets the call recorder.
+func WithRecorder(r *recorder.Recorder) Option { return func(a *AzureAI) { a.recorder = r } }
+
+// WithMetrics sets the metrics collector.
+func WithMetrics(m *metrics.Collector) Option { return func(a *AzureAI) { a.metrics = m } }
+
+// WithRateLimiter sets the rate limiter.
+func WithRateLimiter(l *ratelimit.Limiter) Option { return func(a *AzureAI) { a.limiter = l } }
+
+// WithErrorInjection sets the error injector.
+func WithErrorInjection(i *inject.Injector) Option { return func(a *AzureAI) { a.injector = i } }
+
+// WithLatency sets simulated latency applied to every call.
+func WithLatency(d time.Duration) Option { return func(a *AzureAI) { a.latency = d } }
+
+// do applies the cross-cutting concerns around a single driver call.
+func (a *AzureAI) do(_ context.Context, op string, input any, fn func() (any, error)) (any, error) {
+ start := time.Now()
+
+ if a.injector != nil {
+ if err := a.injector.Check("azureai", op); err != nil {
+ a.rec(op, input, nil, err, time.Since(start))
+
+ return nil, err
+ }
+ }
+
+ if a.limiter != nil {
+ if err := a.limiter.Allow(); err != nil {
+ a.rec(op, input, nil, err, time.Since(start))
+
+ return nil, err
+ }
+ }
+
+ if a.latency > 0 {
+ time.Sleep(a.latency)
+ }
+
+ out, err := fn()
+ dur := time.Since(start)
+
+ if a.metrics != nil {
+ labels := map[string]string{"service": "azureai", "operation": op}
+ a.metrics.Counter("calls_total", 1, labels)
+ a.metrics.Histogram("call_duration", dur, labels)
+
+ if err != nil {
+ a.metrics.Counter("errors_total", 1, labels)
+ }
+ }
+
+ a.rec(op, input, out, err, dur)
+
+ return out, err
+}
+
+func (a *AzureAI) rec(op string, input, output any, err error, dur time.Duration) {
+ if a.recorder != nil {
+ a.recorder.Record("azureai", op, input, output, err, dur)
+ }
+}
+
+// cast converts the do() result to T, short-circuiting on error.
+func cast[T any](out any, err error) (T, error) {
+ if err != nil {
+ var zero T
+
+ return zero, err
+ }
+
+ return out.(T), nil //nolint:forcetypeassert // do() returns exactly the driver's typed result
+}
+
+// act runs an error-only driver call through the pipeline.
+func (a *AzureAI) act(ctx context.Context, op string, input any, fn func() error) error {
+ _, err := a.do(ctx, op, input, func() (any, error) { return nil, fn() })
+
+ return err
+}
diff --git a/services/azureai/azureai_test.go b/services/azureai/azureai_test.go
new file mode 100644
index 0000000..256d85e
--- /dev/null
+++ b/services/azureai/azureai_test.go
@@ -0,0 +1,79 @@
+package azureai_test
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ provazureai "github.com/stackshy/cloudemu/v2/providers/azure/azureai"
+ "github.com/stackshy/cloudemu/v2/services/azureai"
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+func newPortable(opts ...azureai.Option) *azureai.AzureAI {
+ o := config.NewOptions(config.WithAccountID("sub-1"))
+
+ return azureai.New(provazureai.New(o), opts...)
+}
+
+func TestPortableRecordsAndCountsCalls(t *testing.T) {
+ rec := recorder.New()
+ col := metrics.NewCollector()
+ a := newPortable(azureai.WithRecorder(rec), azureai.WithMetrics(col))
+
+ _, err := a.CreateAccount(context.Background(), driver.AccountConfig{
+ Name: "ai", ResourceGroup: "rg", Location: "eastus", Kind: "AIServices",
+ })
+ require.NoError(t, err)
+
+ assert.Equal(t, 1, rec.CallCountFor("azureai", "CreateAccount"))
+ assert.NotEmpty(t, col.All())
+}
+
+func TestPortableErrorInjection(t *testing.T) {
+ inj := inject.NewInjector()
+ boom := errors.New("injected")
+ inj.Set("azureai", "CreateMLWorkspace", boom, inject.Always{})
+
+ a := newPortable(azureai.WithErrorInjection(inj))
+
+ _, err := a.CreateMLWorkspace(context.Background(), driver.MLWorkspaceConfig{
+ Name: "ws", ResourceGroup: "rg", Location: "eastus",
+ })
+ require.ErrorIs(t, err, boom)
+}
+
+func TestPortableLatencyApplied(t *testing.T) {
+ a := newPortable(azureai.WithLatency(20 * time.Millisecond))
+
+ start := time.Now()
+ _, err := a.ListAccounts(context.Background())
+ require.NoError(t, err)
+ assert.GreaterOrEqual(t, time.Since(start), 20*time.Millisecond)
+}
+
+func TestPortableForwardsResults(t *testing.T) {
+ a := newPortable()
+ ctx := context.Background()
+
+ _, err := a.CreateAccount(ctx, driver.AccountConfig{Name: "ai", ResourceGroup: "rg", Location: "eastus", Kind: "OpenAI"})
+ require.NoError(t, err)
+
+ got, err := a.GetAccount(ctx, "rg", "ai")
+ require.NoError(t, err)
+ assert.Equal(t, "ai", got.Name)
+
+ resp, err := a.ChatCompletions(ctx, "ai", "gpt4o", driver.ChatCompletionRequest{
+ Messages: []driver.ChatMessage{{Role: "user", Content: "hi"}},
+ })
+ require.NoError(t, err)
+ require.Len(t, resp.Choices, 1)
+}
diff --git a/services/azureai/driver/cognitiveservices.go b/services/azureai/driver/cognitiveservices.go
new file mode 100644
index 0000000..63d6233
--- /dev/null
+++ b/services/azureai/driver/cognitiveservices.go
@@ -0,0 +1,202 @@
+package driver
+
+import "context"
+
+// AccountConfig describes a Cognitive Services account (the "Azure AI resource"
+// / AI Foundry / Azure OpenAI account) to create or update.
+type AccountConfig struct {
+ Name string
+ ResourceGroup string
+ Location string
+ Kind string // AIServices | OpenAI | CognitiveServices | ...
+ SKUName string // S0, F0, ...
+ CustomDomain string
+ Tags map[string]string
+}
+
+// Account is a Microsoft.CognitiveServices/accounts resource.
+type Account struct {
+ ID string
+ Name string
+ ResourceGroup string
+ Location string
+ Kind string
+ SKUName string
+ Endpoint string
+ CustomDomain string
+ ProvisioningState string
+ Tags map[string]string
+ CreatedAt string
+}
+
+// DeploymentConfig describes a model deployment under an account.
+type DeploymentConfig struct {
+ Account string
+ ResourceGroup string
+ Name string
+ ModelName string
+ ModelVersion string
+ ModelFormat string // OpenAI | Microsoft | ...
+ SKUName string // Standard | GlobalStandard | ProvisionedManaged | ...
+ SKUCapacity int
+}
+
+// Deployment is a Microsoft.CognitiveServices/accounts/deployments resource.
+type Deployment struct {
+ ID string
+ Name string
+ ModelName string
+ ModelVersion string
+ ModelFormat string
+ SKUName string
+ SKUCapacity int
+ ProvisioningState string
+ CreatedAt string
+}
+
+// ProjectConfig describes an AI Foundry project under an account.
+type ProjectConfig struct {
+ Account string
+ ResourceGroup string
+ Name string
+ Location string
+ DisplayName string
+ Description string
+ Tags map[string]string
+}
+
+// Project is a Microsoft.CognitiveServices/accounts/projects resource (AI
+// Foundry project).
+type Project struct {
+ ID string
+ Name string
+ Location string
+ DisplayName string
+ Description string
+ ProvisioningState string
+ Tags map[string]string
+ CreatedAt string
+}
+
+// RaiPolicyConfig describes a Responsible AI content-filter policy.
+type RaiPolicyConfig struct {
+ Account string
+ ResourceGroup string
+ Name string
+ Mode string // Default | Deferred | Blocking | Asynchronous_filter
+ BasePolicy string
+}
+
+// RaiPolicy is a Microsoft.CognitiveServices/accounts/raiPolicies resource.
+type RaiPolicy struct {
+ ID string
+ Name string
+ Mode string
+ BasePolicy string
+ CreatedAt string
+}
+
+// CommitmentPlanConfig describes a provisioned-throughput commitment plan.
+type CommitmentPlanConfig struct {
+ Account string
+ ResourceGroup string
+ Name string
+ PlanType string
+ Tier string
+ AutoRenew bool
+}
+
+// CommitmentPlan is a Microsoft.CognitiveServices/accounts/commitmentPlans
+// resource.
+type CommitmentPlan struct {
+ ID string
+ Name string
+ PlanType string
+ Tier string
+ AutoRenew bool
+ ProvisioningState string
+ CreatedAt string
+}
+
+// PrivateEndpointConnection is a private-endpoint connection under an account.
+type PrivateEndpointConnection struct {
+ ID string
+ Name string
+ Status string // Approved | Pending | Rejected
+ Description string
+ ProvisioningState string
+}
+
+// AccountKeys holds the two access keys for an account.
+type AccountKeys struct {
+ Key1 string
+ Key2 string
+}
+
+// AccountModel is an entry returned by the account's listModels surface.
+type AccountModel struct {
+ Name string
+ Version string
+ Format string
+ Kind string
+}
+
+// AccountSKU is an entry returned by the account's listSkus surface.
+type AccountSKU struct {
+ Name string
+ Tier string
+}
+
+// Usage is a quota/usage datum returned by the account's listUsages surface.
+type Usage struct {
+ Name string
+ CurrentValue float64
+ Limit float64
+ Unit string
+}
+
+// CognitiveServices is the Microsoft.CognitiveServices control-plane surface.
+type CognitiveServices interface {
+ // Accounts.
+ CreateAccount(ctx context.Context, cfg AccountConfig) (*Account, error)
+ GetAccount(ctx context.Context, resourceGroup, name string) (*Account, error)
+ DeleteAccount(ctx context.Context, resourceGroup, name string) error
+ UpdateAccountTags(ctx context.Context, resourceGroup, name string, tags map[string]string) (*Account, error)
+ ListAccountsByResourceGroup(ctx context.Context, resourceGroup string) ([]Account, error)
+ ListAccounts(ctx context.Context) ([]Account, error)
+ ListAccountKeys(ctx context.Context, resourceGroup, name string) (*AccountKeys, error)
+ RegenerateAccountKey(ctx context.Context, resourceGroup, name, keyName string) (*AccountKeys, error)
+ ListAccountUsages(ctx context.Context, resourceGroup, name string) ([]Usage, error)
+ ListAccountModels(ctx context.Context, resourceGroup, name string) ([]AccountModel, error)
+ ListAccountSkus(ctx context.Context, resourceGroup, name string) ([]AccountSKU, error)
+
+ // Deployments (accounts/deployments).
+ CreateDeployment(ctx context.Context, cfg DeploymentConfig) (*Deployment, error)
+ GetDeployment(ctx context.Context, resourceGroup, account, name string) (*Deployment, error)
+ DeleteDeployment(ctx context.Context, resourceGroup, account, name string) error
+ ListDeployments(ctx context.Context, resourceGroup, account string) ([]Deployment, error)
+
+ // Projects (accounts/projects).
+ CreateProject(ctx context.Context, cfg ProjectConfig) (*Project, error)
+ GetProject(ctx context.Context, resourceGroup, account, name string) (*Project, error)
+ DeleteProject(ctx context.Context, resourceGroup, account, name string) error
+ ListProjects(ctx context.Context, resourceGroup, account string) ([]Project, error)
+
+ // RAI policies (accounts/raiPolicies).
+ CreateRaiPolicy(ctx context.Context, cfg RaiPolicyConfig) (*RaiPolicy, error)
+ GetRaiPolicy(ctx context.Context, resourceGroup, account, name string) (*RaiPolicy, error)
+ DeleteRaiPolicy(ctx context.Context, resourceGroup, account, name string) error
+ ListRaiPolicies(ctx context.Context, resourceGroup, account string) ([]RaiPolicy, error)
+
+ // Commitment plans (accounts/commitmentPlans).
+ CreateCommitmentPlan(ctx context.Context, cfg CommitmentPlanConfig) (*CommitmentPlan, error)
+ GetCommitmentPlan(ctx context.Context, resourceGroup, account, name string) (*CommitmentPlan, error)
+ DeleteCommitmentPlan(ctx context.Context, resourceGroup, account, name string) error
+ ListCommitmentPlans(ctx context.Context, resourceGroup, account string) ([]CommitmentPlan, error)
+
+ // Private-endpoint connections (accounts/privateEndpointConnections).
+ PutPrivateEndpointConnection(ctx context.Context, resourceGroup, account, name, status string) (*PrivateEndpointConnection, error)
+ GetPrivateEndpointConnection(ctx context.Context, resourceGroup, account, name string) (*PrivateEndpointConnection, error)
+ DeletePrivateEndpointConnection(ctx context.Context, resourceGroup, account, name string) error
+ ListPrivateEndpointConnections(ctx context.Context, resourceGroup, account string) ([]PrivateEndpointConnection, error)
+}
diff --git a/services/azureai/driver/dataplane.go b/services/azureai/driver/dataplane.go
new file mode 100644
index 0000000..1a7418a
--- /dev/null
+++ b/services/azureai/driver/dataplane.go
@@ -0,0 +1,146 @@
+package driver
+
+import "context"
+
+// ChatMessage is one turn in a chat-completions conversation.
+type ChatMessage struct {
+ Role string // system | user | assistant | tool
+ Content string
+}
+
+// ChatCompletionRequest is an Azure OpenAI chat-completions request.
+type ChatCompletionRequest struct {
+ Messages []ChatMessage
+ Temperature *float64
+ MaxTokens *int
+}
+
+// ChatChoice is one completion choice.
+type ChatChoice struct {
+ Index int
+ Message ChatMessage
+ FinishReason string
+}
+
+// TokenUsage is the prompt/completion/total token accounting.
+type TokenUsage struct {
+ PromptTokens int
+ CompletionTokens int
+ TotalTokens int
+}
+
+// ChatCompletionResponse is an Azure OpenAI chat-completions response.
+type ChatCompletionResponse struct {
+ ID string
+ Model string
+ Created int64
+ Choices []ChatChoice
+ Usage TokenUsage
+}
+
+// EmbeddingsRequest is an Azure OpenAI embeddings request.
+type EmbeddingsRequest struct {
+ Input []string
+}
+
+// EmbeddingData is one input's embedding vector.
+type EmbeddingData struct {
+ Index int
+ Embedding []float64
+}
+
+// EmbeddingsResponse is an Azure OpenAI embeddings response.
+type EmbeddingsResponse struct {
+ Model string
+ Data []EmbeddingData
+ Usage TokenUsage
+}
+
+// CompletionsRequest is a (legacy) Azure OpenAI completions request.
+type CompletionsRequest struct {
+ Prompt string
+ MaxTokens *int
+}
+
+// CompletionChoice is one legacy-completion choice.
+type CompletionChoice struct {
+ Text string
+ Index int
+ FinishReason string
+}
+
+// CompletionsResponse is a legacy Azure OpenAI completions response.
+type CompletionsResponse struct {
+ ID string
+ Model string
+ Created int64
+ Choices []CompletionChoice
+ Usage TokenUsage
+}
+
+// AssistantConfig describes an AI Foundry / Azure OpenAI assistant to create.
+type AssistantConfig struct {
+ Account string
+ Model string
+ Name string
+ Instructions string
+}
+
+// Assistant is an AI Foundry / Azure OpenAI assistant.
+type Assistant struct {
+ ID string
+ Model string
+ Name string
+ Instructions string
+ CreatedAt int64
+}
+
+// Thread is an assistants-API conversation thread.
+type Thread struct {
+ ID string
+ CreatedAt int64
+}
+
+// ThreadMessage is a message within a thread.
+type ThreadMessage struct {
+ ID string
+ ThreadID string
+ Role string
+ Content string
+ CreatedAt int64
+}
+
+// Run is an assistant run over a thread.
+type Run struct {
+ ID string
+ ThreadID string
+ AssistantID string
+ Status string // queued | in_progress | completed | failed
+ CreatedAt int64
+}
+
+// DataPlane is the Azure AI data-plane surface: Azure OpenAI inference, the AI
+// Foundry Agents/Assistants API, and AML online-endpoint scoring. The account
+// scopes inference and assistant state; deployment names the model deployment.
+type DataPlane interface {
+ // Azure OpenAI inference.
+ ChatCompletions(ctx context.Context, account, deployment string, req ChatCompletionRequest) (*ChatCompletionResponse, error)
+ Embeddings(ctx context.Context, account, deployment string, req EmbeddingsRequest) (*EmbeddingsResponse, error)
+ Completions(ctx context.Context, account, deployment string, req CompletionsRequest) (*CompletionsResponse, error)
+
+ // Agents / Assistants.
+ CreateAssistant(ctx context.Context, cfg AssistantConfig) (*Assistant, error)
+ GetAssistant(ctx context.Context, account, id string) (*Assistant, error)
+ ListAssistants(ctx context.Context, account string) ([]Assistant, error)
+ DeleteAssistant(ctx context.Context, account, id string) error
+ CreateThread(ctx context.Context, account string) (*Thread, error)
+ GetThread(ctx context.Context, account, id string) (*Thread, error)
+ DeleteThread(ctx context.Context, account, id string) error
+ CreateMessage(ctx context.Context, account, thread, role, content string) (*ThreadMessage, error)
+ ListMessages(ctx context.Context, account, thread string) ([]ThreadMessage, error)
+ CreateRun(ctx context.Context, account, thread, assistant string) (*Run, error)
+ GetRun(ctx context.Context, account, thread, id string) (*Run, error)
+
+ // AML online-endpoint scoring.
+ ScoreOnlineEndpoint(ctx context.Context, endpoint string, body []byte) ([]byte, error)
+}
diff --git a/services/azureai/driver/driver.go b/services/azureai/driver/driver.go
new file mode 100644
index 0000000..a0d37cf
--- /dev/null
+++ b/services/azureai/driver/driver.go
@@ -0,0 +1,32 @@
+// Package driver defines the interfaces for Azure AI emulation, spanning the
+// two ARM providers that make up "Azure AI": Microsoft.CognitiveServices (Azure
+// AI Foundry / AI Studio, the AI Services resource, and Azure OpenAI) and
+// Microsoft.MachineLearningServices (Azure Machine Learning). It also defines
+// the data-plane surfaces: Azure OpenAI inference, the AI Foundry
+// Agents/Assistants API, and AML online-endpoint scoring.
+//
+// The interfaces use plain Go types only (no cloud SDK dependencies). ARM
+// resource IDs follow the standard
+// /subscriptions/{s}/resourceGroups/{rg}/providers/{provider}/{type}/{name}
+// convention. Control-plane mutations complete synchronously (ARM PUT returns
+// the resource inline with a terminal provisioningState).
+package driver
+
+// Provisioning-state values shared by ARM resources across both providers.
+const (
+ StateSucceeded = "Succeeded"
+ StateCreating = "Creating"
+ StateUpdating = "Updating"
+ StateDeleting = "Deleting"
+ StateFailed = "Failed"
+ StateCanceled = "Canceled"
+)
+
+// AzureAI is the full Azure AI surface: both ARM providers plus the data
+// planes. Implementations (the in-memory Mock) satisfy this; individual server
+// handlers take the narrower per-area interface they need.
+type AzureAI interface {
+ CognitiveServices
+ MachineLearning
+ DataPlane
+}
diff --git a/services/azureai/driver/machinelearning.go b/services/azureai/driver/machinelearning.go
new file mode 100644
index 0000000..a304ed0
--- /dev/null
+++ b/services/azureai/driver/machinelearning.go
@@ -0,0 +1,291 @@
+package driver
+
+import "context"
+
+// MLWorkspaceConfig describes an Azure ML workspace to create or update.
+type MLWorkspaceConfig struct {
+ Name string
+ ResourceGroup string
+ Location string
+ Kind string // Default | Hub | Project | FeatureStore
+ FriendlyName string
+ Description string
+ Tags map[string]string
+}
+
+// MLWorkspace is a Microsoft.MachineLearningServices/workspaces resource.
+type MLWorkspace struct {
+ ID string
+ Name string
+ ResourceGroup string
+ Location string
+ Kind string
+ FriendlyName string
+ Description string
+ DiscoveryURL string
+ ProvisioningState string
+ Tags map[string]string
+ CreatedAt string
+}
+
+// ComputeConfig describes a workspace compute to create.
+type ComputeConfig struct {
+ Workspace string
+ ResourceGroup string
+ Name string
+ ComputeType string // AmlCompute | ComputeInstance | Kubernetes | ...
+ VMSize string
+ MinNodes int
+ MaxNodes int
+}
+
+// Compute is a workspaces/computes resource.
+type Compute struct {
+ ID string
+ Name string
+ ComputeType string
+ VMSize string
+ MinNodes int
+ MaxNodes int
+ State string // Running | Stopped | Resizing | ...
+ ProvisioningState string
+ CreatedAt string
+}
+
+// EndpointConfig describes an online/batch endpoint to create.
+type EndpointConfig struct {
+ Workspace string
+ ResourceGroup string
+ Name string
+ Kind string // online | batch
+ AuthMode string // Key | AMLToken | AADToken
+ Description string
+}
+
+// Endpoint is a workspaces/{online,batch}Endpoints resource.
+type Endpoint struct {
+ ID string
+ Name string
+ Kind string
+ AuthMode string
+ Description string
+ ScoringURI string
+ ProvisioningState string
+ Traffic map[string]int
+ CreatedAt string
+}
+
+// EndpointDeploymentConfig describes a deployment under an endpoint.
+type EndpointDeploymentConfig struct {
+ Workspace string
+ ResourceGroup string
+ Endpoint string
+ EndpointKind string
+ Name string
+ Model string
+ InstanceType string
+ InstanceCount int
+}
+
+// EndpointDeployment is a {online,batch}Endpoints/{e}/deployments resource.
+type EndpointDeployment struct {
+ ID string
+ Name string
+ Model string
+ InstanceType string
+ InstanceCount int
+ ProvisioningState string
+ CreatedAt string
+}
+
+// JobConfig describes a workspace job to submit.
+type JobConfig struct {
+ Workspace string
+ ResourceGroup string
+ Name string
+ JobType string // Command | Sweep | Pipeline | AutoML
+ DisplayName string
+ ComputeID string
+}
+
+// Job is a workspaces/jobs resource.
+type Job struct {
+ ID string
+ Name string
+ JobType string
+ DisplayName string
+ Status string // NotStarted | Running | Completed | Failed | Canceled
+ CreatedAt string
+}
+
+// AssetConfig describes a versioned asset (model/data/environment/component)
+// or a featureset to create. AssetType selects the collection.
+type AssetConfig struct {
+ Workspace string
+ ResourceGroup string
+ AssetType string // models | data | environments | components | featuresets
+ Name string
+ Version string
+ Description string
+ Path string // assetUri / image / etc.
+ Properties map[string]string
+}
+
+// Asset is one version of a versioned asset.
+type Asset struct {
+ ID string
+ Name string
+ Version string
+ AssetType string
+ Description string
+ Path string
+ Properties map[string]string
+ CreatedAt string
+}
+
+// DatastoreConfig describes a workspace datastore.
+type DatastoreConfig struct {
+ Workspace string
+ ResourceGroup string
+ Name string
+ StoreType string // AzureBlob | AzureFile | AzureDataLakeGen2 | ...
+ AccountName string
+ Container string
+}
+
+// Datastore is a workspaces/datastores resource.
+type Datastore struct {
+ ID string
+ Name string
+ StoreType string
+ AccountName string
+ Container string
+ CreatedAt string
+}
+
+// ConnectionConfig describes a workspace connection.
+type ConnectionConfig struct {
+ Workspace string
+ ResourceGroup string
+ Name string
+ Category string // AzureOpenAI | CognitiveSearch | Git | ...
+ Target string
+ AuthType string
+}
+
+// Connection is a workspaces/connections resource.
+type Connection struct {
+ ID string
+ Name string
+ Category string
+ Target string
+ AuthType string
+ CreatedAt string
+}
+
+// MLScheduleConfig describes a workspace schedule.
+type MLScheduleConfig struct {
+ Workspace string
+ ResourceGroup string
+ Name string
+ Cron string
+ DisplayName string
+}
+
+// MLSchedule is a workspaces/schedules resource.
+type MLSchedule struct {
+ ID string
+ Name string
+ Cron string
+ DisplayName string
+ IsEnabled bool
+ CreatedAt string
+}
+
+// RegistryConfig describes a cross-workspace registry.
+type RegistryConfig struct {
+ Name string
+ ResourceGroup string
+ Location string
+ Description string
+ Tags map[string]string
+}
+
+// Registry is a Microsoft.MachineLearningServices/registries resource.
+type Registry struct {
+ ID string
+ Name string
+ Location string
+ Description string
+ ProvisioningState string
+ Tags map[string]string
+ CreatedAt string
+}
+
+// MachineLearning is the Microsoft.MachineLearningServices control-plane
+// surface (Azure ML).
+type MachineLearning interface {
+ // Workspaces.
+ CreateMLWorkspace(ctx context.Context, cfg MLWorkspaceConfig) (*MLWorkspace, error)
+ GetMLWorkspace(ctx context.Context, resourceGroup, name string) (*MLWorkspace, error)
+ DeleteMLWorkspace(ctx context.Context, resourceGroup, name string) error
+ UpdateMLWorkspaceTags(ctx context.Context, resourceGroup, name string, tags map[string]string) (*MLWorkspace, error)
+ ListMLWorkspacesByResourceGroup(ctx context.Context, resourceGroup string) ([]MLWorkspace, error)
+ ListMLWorkspaces(ctx context.Context) ([]MLWorkspace, error)
+
+ // Computes.
+ CreateCompute(ctx context.Context, cfg ComputeConfig) (*Compute, error)
+ GetCompute(ctx context.Context, resourceGroup, workspace, name string) (*Compute, error)
+ DeleteCompute(ctx context.Context, resourceGroup, workspace, name string) error
+ ListComputes(ctx context.Context, resourceGroup, workspace string) ([]Compute, error)
+ StartCompute(ctx context.Context, resourceGroup, workspace, name string) error
+ StopCompute(ctx context.Context, resourceGroup, workspace, name string) error
+ RestartCompute(ctx context.Context, resourceGroup, workspace, name string) error
+
+ // Endpoints (online + batch) and their deployments.
+ CreateEndpoint(ctx context.Context, cfg EndpointConfig) (*Endpoint, error)
+ GetEndpoint(ctx context.Context, resourceGroup, workspace, kind, name string) (*Endpoint, error)
+ DeleteEndpoint(ctx context.Context, resourceGroup, workspace, kind, name string) error
+ ListEndpoints(ctx context.Context, resourceGroup, workspace, kind string) ([]Endpoint, error)
+ CreateEndpointDeployment(ctx context.Context, cfg EndpointDeploymentConfig) (*EndpointDeployment, error)
+ GetEndpointDeployment(ctx context.Context, resourceGroup, workspace, kind, endpoint, name string) (*EndpointDeployment, error)
+ DeleteEndpointDeployment(ctx context.Context, resourceGroup, workspace, kind, endpoint, name string) error
+ ListEndpointDeployments(ctx context.Context, resourceGroup, workspace, kind, endpoint string) ([]EndpointDeployment, error)
+
+ // Jobs.
+ CreateJob(ctx context.Context, cfg JobConfig) (*Job, error)
+ GetJob(ctx context.Context, resourceGroup, workspace, name string) (*Job, error)
+ ListJobs(ctx context.Context, resourceGroup, workspace string) ([]Job, error)
+ CancelJob(ctx context.Context, resourceGroup, workspace, name string) error
+
+ // Versioned assets (models / data / environments / components / featuresets).
+ CreateAsset(ctx context.Context, cfg AssetConfig) (*Asset, error)
+ GetAsset(ctx context.Context, resourceGroup, workspace, assetType, name, version string) (*Asset, error)
+ DeleteAsset(ctx context.Context, resourceGroup, workspace, assetType, name, version string) error
+ ListAssetVersions(ctx context.Context, resourceGroup, workspace, assetType, name string) ([]Asset, error)
+ ListAssetContainers(ctx context.Context, resourceGroup, workspace, assetType string) ([]Asset, error)
+
+ // Datastores.
+ CreateDatastore(ctx context.Context, cfg DatastoreConfig) (*Datastore, error)
+ GetDatastore(ctx context.Context, resourceGroup, workspace, name string) (*Datastore, error)
+ DeleteDatastore(ctx context.Context, resourceGroup, workspace, name string) error
+ ListDatastores(ctx context.Context, resourceGroup, workspace string) ([]Datastore, error)
+
+ // Connections.
+ CreateConnection(ctx context.Context, cfg ConnectionConfig) (*Connection, error)
+ GetConnection(ctx context.Context, resourceGroup, workspace, name string) (*Connection, error)
+ DeleteConnection(ctx context.Context, resourceGroup, workspace, name string) error
+ ListConnections(ctx context.Context, resourceGroup, workspace string) ([]Connection, error)
+
+ // Schedules.
+ CreateMLSchedule(ctx context.Context, cfg MLScheduleConfig) (*MLSchedule, error)
+ GetMLSchedule(ctx context.Context, resourceGroup, workspace, name string) (*MLSchedule, error)
+ DeleteMLSchedule(ctx context.Context, resourceGroup, workspace, name string) error
+ ListMLSchedules(ctx context.Context, resourceGroup, workspace string) ([]MLSchedule, error)
+
+ // Registries (subscription/RG-scoped, cross-workspace).
+ CreateRegistry(ctx context.Context, cfg RegistryConfig) (*Registry, error)
+ GetRegistry(ctx context.Context, resourceGroup, name string) (*Registry, error)
+ DeleteRegistry(ctx context.Context, resourceGroup, name string) error
+ ListRegistries(ctx context.Context, resourceGroup string) ([]Registry, error)
+}
diff --git a/services/azureai/wrappers_cognitiveservices.go b/services/azureai/wrappers_cognitiveservices.go
new file mode 100644
index 0000000..681fda1
--- /dev/null
+++ b/services/azureai/wrappers_cognitiveservices.go
@@ -0,0 +1,182 @@
+package azureai
+
+import (
+ "context"
+
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateAccount(ctx context.Context, cfg driver.AccountConfig) (*driver.Account, error) {
+ return cast[*driver.Account](a.do(ctx, "CreateAccount", cfg, func() (any, error) { return a.drv.CreateAccount(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetAccount(ctx context.Context, rg, name string) (*driver.Account, error) {
+ return cast[*driver.Account](a.do(ctx, "GetAccount", name, func() (any, error) { return a.drv.GetAccount(ctx, rg, name) }))
+}
+
+func (a *AzureAI) DeleteAccount(ctx context.Context, rg, name string) error {
+ return a.act(ctx, "DeleteAccount", name, func() error { return a.drv.DeleteAccount(ctx, rg, name) })
+}
+
+func (a *AzureAI) UpdateAccountTags(ctx context.Context, rg, name string, tags map[string]string) (*driver.Account, error) {
+ return cast[*driver.Account](a.do(ctx, "UpdateAccountTags", name, func() (any, error) {
+ return a.drv.UpdateAccountTags(ctx, rg, name, tags)
+ }))
+}
+
+func (a *AzureAI) ListAccountsByResourceGroup(ctx context.Context, rg string) ([]driver.Account, error) {
+ return cast[[]driver.Account](a.do(ctx, "ListAccountsByResourceGroup", rg, func() (any, error) {
+ return a.drv.ListAccountsByResourceGroup(ctx, rg)
+ }))
+}
+
+func (a *AzureAI) ListAccounts(ctx context.Context) ([]driver.Account, error) {
+ return cast[[]driver.Account](a.do(ctx, "ListAccounts", nil, func() (any, error) { return a.drv.ListAccounts(ctx) }))
+}
+
+func (a *AzureAI) ListAccountKeys(ctx context.Context, rg, name string) (*driver.AccountKeys, error) {
+ return cast[*driver.AccountKeys](a.do(ctx, "ListAccountKeys", name, func() (any, error) {
+ return a.drv.ListAccountKeys(ctx, rg, name)
+ }))
+}
+
+func (a *AzureAI) RegenerateAccountKey(ctx context.Context, rg, name, keyName string) (*driver.AccountKeys, error) {
+ return cast[*driver.AccountKeys](a.do(ctx, "RegenerateAccountKey", name, func() (any, error) {
+ return a.drv.RegenerateAccountKey(ctx, rg, name, keyName)
+ }))
+}
+
+func (a *AzureAI) ListAccountUsages(ctx context.Context, rg, name string) ([]driver.Usage, error) {
+ return cast[[]driver.Usage](a.do(ctx, "ListAccountUsages", name, func() (any, error) {
+ return a.drv.ListAccountUsages(ctx, rg, name)
+ }))
+}
+
+func (a *AzureAI) ListAccountModels(ctx context.Context, rg, name string) ([]driver.AccountModel, error) {
+ return cast[[]driver.AccountModel](a.do(ctx, "ListAccountModels", name, func() (any, error) {
+ return a.drv.ListAccountModels(ctx, rg, name)
+ }))
+}
+
+func (a *AzureAI) ListAccountSkus(ctx context.Context, rg, name string) ([]driver.AccountSKU, error) {
+ return cast[[]driver.AccountSKU](a.do(ctx, "ListAccountSkus", name, func() (any, error) {
+ return a.drv.ListAccountSkus(ctx, rg, name)
+ }))
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateDeployment(ctx context.Context, cfg driver.DeploymentConfig) (*driver.Deployment, error) {
+ return cast[*driver.Deployment](a.do(ctx, "CreateDeployment", cfg, func() (any, error) { return a.drv.CreateDeployment(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetDeployment(ctx context.Context, rg, account, name string) (*driver.Deployment, error) {
+ return cast[*driver.Deployment](a.do(ctx, "GetDeployment", name, func() (any, error) {
+ return a.drv.GetDeployment(ctx, rg, account, name)
+ }))
+}
+
+func (a *AzureAI) DeleteDeployment(ctx context.Context, rg, account, name string) error {
+ return a.act(ctx, "DeleteDeployment", name, func() error { return a.drv.DeleteDeployment(ctx, rg, account, name) })
+}
+
+func (a *AzureAI) ListDeployments(ctx context.Context, rg, account string) ([]driver.Deployment, error) {
+ return cast[[]driver.Deployment](a.do(ctx, "ListDeployments", account, func() (any, error) {
+ return a.drv.ListDeployments(ctx, rg, account)
+ }))
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateProject(ctx context.Context, cfg driver.ProjectConfig) (*driver.Project, error) {
+ return cast[*driver.Project](a.do(ctx, "CreateProject", cfg, func() (any, error) { return a.drv.CreateProject(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetProject(ctx context.Context, rg, account, name string) (*driver.Project, error) {
+ return cast[*driver.Project](a.do(ctx, "GetProject", name, func() (any, error) {
+ return a.drv.GetProject(ctx, rg, account, name)
+ }))
+}
+
+func (a *AzureAI) DeleteProject(ctx context.Context, rg, account, name string) error {
+ return a.act(ctx, "DeleteProject", name, func() error { return a.drv.DeleteProject(ctx, rg, account, name) })
+}
+
+func (a *AzureAI) ListProjects(ctx context.Context, rg, account string) ([]driver.Project, error) {
+ return cast[[]driver.Project](a.do(ctx, "ListProjects", account, func() (any, error) {
+ return a.drv.ListProjects(ctx, rg, account)
+ }))
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateRaiPolicy(ctx context.Context, cfg driver.RaiPolicyConfig) (*driver.RaiPolicy, error) {
+ return cast[*driver.RaiPolicy](a.do(ctx, "CreateRaiPolicy", cfg, func() (any, error) { return a.drv.CreateRaiPolicy(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetRaiPolicy(ctx context.Context, rg, account, name string) (*driver.RaiPolicy, error) {
+ return cast[*driver.RaiPolicy](a.do(ctx, "GetRaiPolicy", name, func() (any, error) {
+ return a.drv.GetRaiPolicy(ctx, rg, account, name)
+ }))
+}
+
+func (a *AzureAI) DeleteRaiPolicy(ctx context.Context, rg, account, name string) error {
+ return a.act(ctx, "DeleteRaiPolicy", name, func() error { return a.drv.DeleteRaiPolicy(ctx, rg, account, name) })
+}
+
+func (a *AzureAI) ListRaiPolicies(ctx context.Context, rg, account string) ([]driver.RaiPolicy, error) {
+ return cast[[]driver.RaiPolicy](a.do(ctx, "ListRaiPolicies", account, func() (any, error) {
+ return a.drv.ListRaiPolicies(ctx, rg, account)
+ }))
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateCommitmentPlan(ctx context.Context, cfg driver.CommitmentPlanConfig) (*driver.CommitmentPlan, error) {
+ return cast[*driver.CommitmentPlan](a.do(ctx, "CreateCommitmentPlan", cfg, func() (any, error) {
+ return a.drv.CreateCommitmentPlan(ctx, cfg)
+ }))
+}
+
+func (a *AzureAI) GetCommitmentPlan(ctx context.Context, rg, account, name string) (*driver.CommitmentPlan, error) {
+ return cast[*driver.CommitmentPlan](a.do(ctx, "GetCommitmentPlan", name, func() (any, error) {
+ return a.drv.GetCommitmentPlan(ctx, rg, account, name)
+ }))
+}
+
+func (a *AzureAI) DeleteCommitmentPlan(ctx context.Context, rg, account, name string) error {
+ return a.act(ctx, "DeleteCommitmentPlan", name, func() error { return a.drv.DeleteCommitmentPlan(ctx, rg, account, name) })
+}
+
+func (a *AzureAI) ListCommitmentPlans(ctx context.Context, rg, account string) ([]driver.CommitmentPlan, error) {
+ return cast[[]driver.CommitmentPlan](a.do(ctx, "ListCommitmentPlans", account, func() (any, error) {
+ return a.drv.ListCommitmentPlans(ctx, rg, account)
+ }))
+}
+
+func (a *AzureAI) PutPrivateEndpointConnection(
+ ctx context.Context, rg, account, name, status string,
+) (*driver.PrivateEndpointConnection, error) {
+ return cast[*driver.PrivateEndpointConnection](a.do(ctx, "PutPrivateEndpointConnection", name, func() (any, error) {
+ return a.drv.PutPrivateEndpointConnection(ctx, rg, account, name, status)
+ }))
+}
+
+func (a *AzureAI) GetPrivateEndpointConnection(
+ ctx context.Context, rg, account, name string,
+) (*driver.PrivateEndpointConnection, error) {
+ return cast[*driver.PrivateEndpointConnection](a.do(ctx, "GetPrivateEndpointConnection", name, func() (any, error) {
+ return a.drv.GetPrivateEndpointConnection(ctx, rg, account, name)
+ }))
+}
+
+func (a *AzureAI) DeletePrivateEndpointConnection(ctx context.Context, rg, account, name string) error {
+ return a.act(ctx, "DeletePrivateEndpointConnection", name, func() error {
+ return a.drv.DeletePrivateEndpointConnection(ctx, rg, account, name)
+ })
+}
+
+func (a *AzureAI) ListPrivateEndpointConnections(
+ ctx context.Context, rg, account string,
+) ([]driver.PrivateEndpointConnection, error) {
+ return cast[[]driver.PrivateEndpointConnection](a.do(ctx, "ListPrivateEndpointConnections", account, func() (any, error) {
+ return a.drv.ListPrivateEndpointConnections(ctx, rg, account)
+ }))
+}
diff --git a/services/azureai/wrappers_dataplane.go b/services/azureai/wrappers_dataplane.go
new file mode 100644
index 0000000..87744bb
--- /dev/null
+++ b/services/azureai/wrappers_dataplane.go
@@ -0,0 +1,89 @@
+package azureai
+
+import (
+ "context"
+
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+func (a *AzureAI) ChatCompletions(
+ ctx context.Context, account, deployment string, req driver.ChatCompletionRequest,
+) (*driver.ChatCompletionResponse, error) {
+ return cast[*driver.ChatCompletionResponse](a.do(ctx, "ChatCompletions", deployment, func() (any, error) {
+ return a.drv.ChatCompletions(ctx, account, deployment, req)
+ }))
+}
+
+func (a *AzureAI) Embeddings(
+ ctx context.Context, account, deployment string, req driver.EmbeddingsRequest,
+) (*driver.EmbeddingsResponse, error) {
+ return cast[*driver.EmbeddingsResponse](a.do(ctx, "Embeddings", deployment, func() (any, error) {
+ return a.drv.Embeddings(ctx, account, deployment, req)
+ }))
+}
+
+func (a *AzureAI) Completions(
+ ctx context.Context, account, deployment string, req driver.CompletionsRequest,
+) (*driver.CompletionsResponse, error) {
+ return cast[*driver.CompletionsResponse](a.do(ctx, "Completions", deployment, func() (any, error) {
+ return a.drv.Completions(ctx, account, deployment, req)
+ }))
+}
+
+func (a *AzureAI) CreateAssistant(ctx context.Context, cfg driver.AssistantConfig) (*driver.Assistant, error) {
+ return cast[*driver.Assistant](a.do(ctx, "CreateAssistant", cfg, func() (any, error) { return a.drv.CreateAssistant(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetAssistant(ctx context.Context, account, id string) (*driver.Assistant, error) {
+ return cast[*driver.Assistant](a.do(ctx, "GetAssistant", id, func() (any, error) { return a.drv.GetAssistant(ctx, account, id) }))
+}
+
+func (a *AzureAI) ListAssistants(ctx context.Context, account string) ([]driver.Assistant, error) {
+ return cast[[]driver.Assistant](a.do(ctx, "ListAssistants", account, func() (any, error) {
+ return a.drv.ListAssistants(ctx, account)
+ }))
+}
+
+func (a *AzureAI) DeleteAssistant(ctx context.Context, account, id string) error {
+ return a.act(ctx, "DeleteAssistant", id, func() error { return a.drv.DeleteAssistant(ctx, account, id) })
+}
+
+func (a *AzureAI) CreateThread(ctx context.Context, account string) (*driver.Thread, error) {
+ return cast[*driver.Thread](a.do(ctx, "CreateThread", account, func() (any, error) { return a.drv.CreateThread(ctx, account) }))
+}
+
+func (a *AzureAI) GetThread(ctx context.Context, account, id string) (*driver.Thread, error) {
+ return cast[*driver.Thread](a.do(ctx, "GetThread", id, func() (any, error) { return a.drv.GetThread(ctx, account, id) }))
+}
+
+func (a *AzureAI) DeleteThread(ctx context.Context, account, id string) error {
+ return a.act(ctx, "DeleteThread", id, func() error { return a.drv.DeleteThread(ctx, account, id) })
+}
+
+func (a *AzureAI) CreateMessage(ctx context.Context, account, thread, role, content string) (*driver.ThreadMessage, error) {
+ return cast[*driver.ThreadMessage](a.do(ctx, "CreateMessage", thread, func() (any, error) {
+ return a.drv.CreateMessage(ctx, account, thread, role, content)
+ }))
+}
+
+func (a *AzureAI) ListMessages(ctx context.Context, account, thread string) ([]driver.ThreadMessage, error) {
+ return cast[[]driver.ThreadMessage](a.do(ctx, "ListMessages", thread, func() (any, error) {
+ return a.drv.ListMessages(ctx, account, thread)
+ }))
+}
+
+func (a *AzureAI) CreateRun(ctx context.Context, account, thread, assistant string) (*driver.Run, error) {
+ return cast[*driver.Run](a.do(ctx, "CreateRun", thread, func() (any, error) {
+ return a.drv.CreateRun(ctx, account, thread, assistant)
+ }))
+}
+
+func (a *AzureAI) GetRun(ctx context.Context, account, thread, id string) (*driver.Run, error) {
+ return cast[*driver.Run](a.do(ctx, "GetRun", id, func() (any, error) { return a.drv.GetRun(ctx, account, thread, id) }))
+}
+
+func (a *AzureAI) ScoreOnlineEndpoint(ctx context.Context, endpoint string, body []byte) ([]byte, error) {
+ return cast[[]byte](a.do(ctx, "ScoreOnlineEndpoint", endpoint, func() (any, error) {
+ return a.drv.ScoreOnlineEndpoint(ctx, endpoint, body)
+ }))
+}
diff --git a/services/azureai/wrappers_machinelearning.go b/services/azureai/wrappers_machinelearning.go
new file mode 100644
index 0000000..b7e8092
--- /dev/null
+++ b/services/azureai/wrappers_machinelearning.go
@@ -0,0 +1,236 @@
+package azureai
+
+import (
+ "context"
+
+ "github.com/stackshy/cloudemu/v2/services/azureai/driver"
+)
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateMLWorkspace(ctx context.Context, cfg driver.MLWorkspaceConfig) (*driver.MLWorkspace, error) {
+ return cast[*driver.MLWorkspace](a.do(ctx, "CreateMLWorkspace", cfg, func() (any, error) {
+ return a.drv.CreateMLWorkspace(ctx, cfg)
+ }))
+}
+
+func (a *AzureAI) GetMLWorkspace(ctx context.Context, rg, name string) (*driver.MLWorkspace, error) {
+ return cast[*driver.MLWorkspace](a.do(ctx, "GetMLWorkspace", name, func() (any, error) {
+ return a.drv.GetMLWorkspace(ctx, rg, name)
+ }))
+}
+
+func (a *AzureAI) DeleteMLWorkspace(ctx context.Context, rg, name string) error {
+ return a.act(ctx, "DeleteMLWorkspace", name, func() error { return a.drv.DeleteMLWorkspace(ctx, rg, name) })
+}
+
+func (a *AzureAI) UpdateMLWorkspaceTags(ctx context.Context, rg, name string, tags map[string]string) (*driver.MLWorkspace, error) {
+ return cast[*driver.MLWorkspace](a.do(ctx, "UpdateMLWorkspaceTags", name, func() (any, error) {
+ return a.drv.UpdateMLWorkspaceTags(ctx, rg, name, tags)
+ }))
+}
+
+func (a *AzureAI) ListMLWorkspacesByResourceGroup(ctx context.Context, rg string) ([]driver.MLWorkspace, error) {
+ return cast[[]driver.MLWorkspace](a.do(ctx, "ListMLWorkspacesByResourceGroup", rg, func() (any, error) {
+ return a.drv.ListMLWorkspacesByResourceGroup(ctx, rg)
+ }))
+}
+
+func (a *AzureAI) ListMLWorkspaces(ctx context.Context) ([]driver.MLWorkspace, error) {
+ return cast[[]driver.MLWorkspace](a.do(ctx, "ListMLWorkspaces", nil, func() (any, error) { return a.drv.ListMLWorkspaces(ctx) }))
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateCompute(ctx context.Context, cfg driver.ComputeConfig) (*driver.Compute, error) {
+ return cast[*driver.Compute](a.do(ctx, "CreateCompute", cfg, func() (any, error) { return a.drv.CreateCompute(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetCompute(ctx context.Context, rg, ws, name string) (*driver.Compute, error) {
+ return cast[*driver.Compute](a.do(ctx, "GetCompute", name, func() (any, error) { return a.drv.GetCompute(ctx, rg, ws, name) }))
+}
+
+func (a *AzureAI) DeleteCompute(ctx context.Context, rg, ws, name string) error {
+ return a.act(ctx, "DeleteCompute", name, func() error { return a.drv.DeleteCompute(ctx, rg, ws, name) })
+}
+
+func (a *AzureAI) ListComputes(ctx context.Context, rg, ws string) ([]driver.Compute, error) {
+ return cast[[]driver.Compute](a.do(ctx, "ListComputes", ws, func() (any, error) { return a.drv.ListComputes(ctx, rg, ws) }))
+}
+
+func (a *AzureAI) StartCompute(ctx context.Context, rg, ws, name string) error {
+ return a.act(ctx, "StartCompute", name, func() error { return a.drv.StartCompute(ctx, rg, ws, name) })
+}
+
+func (a *AzureAI) StopCompute(ctx context.Context, rg, ws, name string) error {
+ return a.act(ctx, "StopCompute", name, func() error { return a.drv.StopCompute(ctx, rg, ws, name) })
+}
+
+func (a *AzureAI) RestartCompute(ctx context.Context, rg, ws, name string) error {
+ return a.act(ctx, "RestartCompute", name, func() error { return a.drv.RestartCompute(ctx, rg, ws, name) })
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateEndpoint(ctx context.Context, cfg driver.EndpointConfig) (*driver.Endpoint, error) {
+ return cast[*driver.Endpoint](a.do(ctx, "CreateEndpoint", cfg, func() (any, error) { return a.drv.CreateEndpoint(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetEndpoint(ctx context.Context, rg, ws, kind, name string) (*driver.Endpoint, error) {
+ return cast[*driver.Endpoint](a.do(ctx, "GetEndpoint", name, func() (any, error) {
+ return a.drv.GetEndpoint(ctx, rg, ws, kind, name)
+ }))
+}
+
+func (a *AzureAI) DeleteEndpoint(ctx context.Context, rg, ws, kind, name string) error {
+ return a.act(ctx, "DeleteEndpoint", name, func() error { return a.drv.DeleteEndpoint(ctx, rg, ws, kind, name) })
+}
+
+func (a *AzureAI) ListEndpoints(ctx context.Context, rg, ws, kind string) ([]driver.Endpoint, error) {
+ return cast[[]driver.Endpoint](a.do(ctx, "ListEndpoints", ws, func() (any, error) {
+ return a.drv.ListEndpoints(ctx, rg, ws, kind)
+ }))
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateEndpointDeployment(ctx context.Context, cfg driver.EndpointDeploymentConfig) (*driver.EndpointDeployment, error) {
+ return cast[*driver.EndpointDeployment](a.do(ctx, "CreateEndpointDeployment", cfg, func() (any, error) {
+ return a.drv.CreateEndpointDeployment(ctx, cfg)
+ }))
+}
+
+func (a *AzureAI) GetEndpointDeployment(ctx context.Context, rg, ws, kind, ep, name string) (*driver.EndpointDeployment, error) {
+ return cast[*driver.EndpointDeployment](a.do(ctx, "GetEndpointDeployment", name, func() (any, error) {
+ return a.drv.GetEndpointDeployment(ctx, rg, ws, kind, ep, name)
+ }))
+}
+
+func (a *AzureAI) DeleteEndpointDeployment(ctx context.Context, rg, ws, kind, ep, name string) error {
+ return a.act(ctx, "DeleteEndpointDeployment", name, func() error {
+ return a.drv.DeleteEndpointDeployment(ctx, rg, ws, kind, ep, name)
+ })
+}
+
+func (a *AzureAI) ListEndpointDeployments(ctx context.Context, rg, ws, kind, ep string) ([]driver.EndpointDeployment, error) {
+ return cast[[]driver.EndpointDeployment](a.do(ctx, "ListEndpointDeployments", ep, func() (any, error) {
+ return a.drv.ListEndpointDeployments(ctx, rg, ws, kind, ep)
+ }))
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateJob(ctx context.Context, cfg driver.JobConfig) (*driver.Job, error) {
+ return cast[*driver.Job](a.do(ctx, "CreateJob", cfg, func() (any, error) { return a.drv.CreateJob(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetJob(ctx context.Context, rg, ws, name string) (*driver.Job, error) {
+ return cast[*driver.Job](a.do(ctx, "GetJob", name, func() (any, error) { return a.drv.GetJob(ctx, rg, ws, name) }))
+}
+
+func (a *AzureAI) ListJobs(ctx context.Context, rg, ws string) ([]driver.Job, error) {
+ return cast[[]driver.Job](a.do(ctx, "ListJobs", ws, func() (any, error) { return a.drv.ListJobs(ctx, rg, ws) }))
+}
+
+func (a *AzureAI) CancelJob(ctx context.Context, rg, ws, name string) error {
+ return a.act(ctx, "CancelJob", name, func() error { return a.drv.CancelJob(ctx, rg, ws, name) })
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateAsset(ctx context.Context, cfg driver.AssetConfig) (*driver.Asset, error) {
+ return cast[*driver.Asset](a.do(ctx, "CreateAsset", cfg, func() (any, error) { return a.drv.CreateAsset(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetAsset(ctx context.Context, rg, ws, assetType, name, version string) (*driver.Asset, error) {
+ return cast[*driver.Asset](a.do(ctx, "GetAsset", name, func() (any, error) {
+ return a.drv.GetAsset(ctx, rg, ws, assetType, name, version)
+ }))
+}
+
+func (a *AzureAI) DeleteAsset(ctx context.Context, rg, ws, assetType, name, version string) error {
+ return a.act(ctx, "DeleteAsset", name, func() error { return a.drv.DeleteAsset(ctx, rg, ws, assetType, name, version) })
+}
+
+func (a *AzureAI) ListAssetVersions(ctx context.Context, rg, ws, assetType, name string) ([]driver.Asset, error) {
+ return cast[[]driver.Asset](a.do(ctx, "ListAssetVersions", name, func() (any, error) {
+ return a.drv.ListAssetVersions(ctx, rg, ws, assetType, name)
+ }))
+}
+
+func (a *AzureAI) ListAssetContainers(ctx context.Context, rg, ws, assetType string) ([]driver.Asset, error) {
+ return cast[[]driver.Asset](a.do(ctx, "ListAssetContainers", assetType, func() (any, error) {
+ return a.drv.ListAssetContainers(ctx, rg, ws, assetType)
+ }))
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateDatastore(ctx context.Context, cfg driver.DatastoreConfig) (*driver.Datastore, error) {
+ return cast[*driver.Datastore](a.do(ctx, "CreateDatastore", cfg, func() (any, error) { return a.drv.CreateDatastore(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetDatastore(ctx context.Context, rg, ws, name string) (*driver.Datastore, error) {
+ return cast[*driver.Datastore](a.do(ctx, "GetDatastore", name, func() (any, error) {
+ return a.drv.GetDatastore(ctx, rg, ws, name)
+ }))
+}
+
+func (a *AzureAI) DeleteDatastore(ctx context.Context, rg, ws, name string) error {
+ return a.act(ctx, "DeleteDatastore", name, func() error { return a.drv.DeleteDatastore(ctx, rg, ws, name) })
+}
+
+func (a *AzureAI) ListDatastores(ctx context.Context, rg, ws string) ([]driver.Datastore, error) {
+ return cast[[]driver.Datastore](a.do(ctx, "ListDatastores", ws, func() (any, error) { return a.drv.ListDatastores(ctx, rg, ws) }))
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateConnection(ctx context.Context, cfg driver.ConnectionConfig) (*driver.Connection, error) {
+ return cast[*driver.Connection](a.do(ctx, "CreateConnection", cfg, func() (any, error) { return a.drv.CreateConnection(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetConnection(ctx context.Context, rg, ws, name string) (*driver.Connection, error) {
+ return cast[*driver.Connection](a.do(ctx, "GetConnection", name, func() (any, error) {
+ return a.drv.GetConnection(ctx, rg, ws, name)
+ }))
+}
+
+func (a *AzureAI) DeleteConnection(ctx context.Context, rg, ws, name string) error {
+ return a.act(ctx, "DeleteConnection", name, func() error { return a.drv.DeleteConnection(ctx, rg, ws, name) })
+}
+
+func (a *AzureAI) ListConnections(ctx context.Context, rg, ws string) ([]driver.Connection, error) {
+ return cast[[]driver.Connection](a.do(ctx, "ListConnections", ws, func() (any, error) {
+ return a.drv.ListConnections(ctx, rg, ws)
+ }))
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureAI) CreateMLSchedule(ctx context.Context, cfg driver.MLScheduleConfig) (*driver.MLSchedule, error) {
+ return cast[*driver.MLSchedule](a.do(ctx, "CreateMLSchedule", cfg, func() (any, error) { return a.drv.CreateMLSchedule(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetMLSchedule(ctx context.Context, rg, ws, name string) (*driver.MLSchedule, error) {
+ return cast[*driver.MLSchedule](a.do(ctx, "GetMLSchedule", name, func() (any, error) {
+ return a.drv.GetMLSchedule(ctx, rg, ws, name)
+ }))
+}
+
+func (a *AzureAI) DeleteMLSchedule(ctx context.Context, rg, ws, name string) error {
+ return a.act(ctx, "DeleteMLSchedule", name, func() error { return a.drv.DeleteMLSchedule(ctx, rg, ws, name) })
+}
+
+func (a *AzureAI) ListMLSchedules(ctx context.Context, rg, ws string) ([]driver.MLSchedule, error) {
+ return cast[[]driver.MLSchedule](a.do(ctx, "ListMLSchedules", ws, func() (any, error) {
+ return a.drv.ListMLSchedules(ctx, rg, ws)
+ }))
+}
+
+func (a *AzureAI) CreateRegistry(ctx context.Context, cfg driver.RegistryConfig) (*driver.Registry, error) {
+ return cast[*driver.Registry](a.do(ctx, "CreateRegistry", cfg, func() (any, error) { return a.drv.CreateRegistry(ctx, cfg) }))
+}
+
+func (a *AzureAI) GetRegistry(ctx context.Context, rg, name string) (*driver.Registry, error) {
+ return cast[*driver.Registry](a.do(ctx, "GetRegistry", name, func() (any, error) { return a.drv.GetRegistry(ctx, rg, name) }))
+}
+
+func (a *AzureAI) DeleteRegistry(ctx context.Context, rg, name string) error {
+ return a.act(ctx, "DeleteRegistry", name, func() error { return a.drv.DeleteRegistry(ctx, rg, name) })
+}
+
+func (a *AzureAI) ListRegistries(ctx context.Context, rg string) ([]driver.Registry, error) {
+ return cast[[]driver.Registry](a.do(ctx, "ListRegistries", rg, func() (any, error) { return a.drv.ListRegistries(ctx, rg) }))
+}
diff --git a/services/azuresearch/azuresearch.go b/services/azuresearch/azuresearch.go
new file mode 100644
index 0000000..e2a3eca
--- /dev/null
+++ b/services/azuresearch/azuresearch.go
@@ -0,0 +1,125 @@
+// Package azuresearch provides a portable Azure AI Search API with
+// cross-cutting concerns. It wraps a driver.AzureSearch (ARM control plane +
+// search data plane) with recording, metrics, rate limiting, error injection,
+// and latency simulation — the same middle layer every other service ships, so
+// Azure AI Search participates in the three-layer design.
+package azuresearch
+
+import (
+ "context"
+ "time"
+
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+// Compile-time check that the portable type implements the full service.
+var _ driver.AzureSearch = (*AzureSearch)(nil)
+
+// AzureSearch is the portable Azure AI Search type wrapping a driver with
+// cross-cutting concerns.
+type AzureSearch struct {
+ drv driver.AzureSearch
+ recorder *recorder.Recorder
+ metrics *metrics.Collector
+ limiter *ratelimit.Limiter
+ injector *inject.Injector
+ latency time.Duration
+}
+
+// New creates a portable AzureSearch wrapping the given driver.
+func New(d driver.AzureSearch, opts ...Option) *AzureSearch {
+ a := &AzureSearch{drv: d}
+ for _, opt := range opts {
+ opt(a)
+ }
+
+ return a
+}
+
+// Option configures a portable AzureSearch.
+type Option func(*AzureSearch)
+
+// WithRecorder sets the call recorder.
+func WithRecorder(r *recorder.Recorder) Option { return func(a *AzureSearch) { a.recorder = r } }
+
+// WithMetrics sets the metrics collector.
+func WithMetrics(m *metrics.Collector) Option { return func(a *AzureSearch) { a.metrics = m } }
+
+// WithRateLimiter sets the rate limiter.
+func WithRateLimiter(l *ratelimit.Limiter) Option { return func(a *AzureSearch) { a.limiter = l } }
+
+// WithErrorInjection sets the error injector.
+func WithErrorInjection(i *inject.Injector) Option { return func(a *AzureSearch) { a.injector = i } }
+
+// WithLatency sets simulated latency applied to every call.
+func WithLatency(d time.Duration) Option { return func(a *AzureSearch) { a.latency = d } }
+
+// do applies the cross-cutting concerns around a single driver call.
+func (a *AzureSearch) do(_ context.Context, op string, input any, fn func() (any, error)) (any, error) {
+ start := time.Now()
+
+ if a.injector != nil {
+ if err := a.injector.Check("azuresearch", op); err != nil {
+ a.rec(op, input, nil, err, time.Since(start))
+
+ return nil, err
+ }
+ }
+
+ if a.limiter != nil {
+ if err := a.limiter.Allow(); err != nil {
+ a.rec(op, input, nil, err, time.Since(start))
+
+ return nil, err
+ }
+ }
+
+ if a.latency > 0 {
+ time.Sleep(a.latency)
+ }
+
+ out, err := fn()
+ dur := time.Since(start)
+
+ if a.metrics != nil {
+ labels := map[string]string{"service": "azuresearch", "operation": op}
+ a.metrics.Counter("calls_total", 1, labels)
+ a.metrics.Histogram("call_duration", dur, labels)
+
+ if err != nil {
+ a.metrics.Counter("errors_total", 1, labels)
+ }
+ }
+
+ a.rec(op, input, out, err, dur)
+
+ return out, err
+}
+
+func (a *AzureSearch) rec(op string, input, output any, err error, dur time.Duration) {
+ if a.recorder != nil {
+ a.recorder.Record("azuresearch", op, input, output, err, dur)
+ }
+}
+
+// cast converts the do() result to T, short-circuiting on error.
+func cast[T any](out any, err error) (T, error) {
+ if err != nil {
+ var zero T
+
+ return zero, err
+ }
+
+ return out.(T), nil //nolint:forcetypeassert // do() returns exactly the driver's typed result
+}
+
+// act runs an error-only driver call through the pipeline.
+func (a *AzureSearch) act(ctx context.Context, op string, input any, fn func() error) error {
+ _, err := a.do(ctx, op, input, func() (any, error) { return nil, fn() })
+
+ return err
+}
diff --git a/services/azuresearch/azuresearch_test.go b/services/azuresearch/azuresearch_test.go
new file mode 100644
index 0000000..f5bbe74
--- /dev/null
+++ b/services/azuresearch/azuresearch_test.go
@@ -0,0 +1,80 @@
+package azuresearch_test
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ provsearch "github.com/stackshy/cloudemu/v2/providers/azure/azuresearch"
+ "github.com/stackshy/cloudemu/v2/services/azuresearch"
+ "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+func newPortable(opts ...azuresearch.Option) *azuresearch.AzureSearch {
+ o := config.NewOptions(config.WithAccountID("sub-1"))
+
+ return azuresearch.New(provsearch.New(o), opts...)
+}
+
+func TestPortableRecordsAndCountsCalls(t *testing.T) {
+ rec := recorder.New()
+ col := metrics.NewCollector()
+ a := newPortable(azuresearch.WithRecorder(rec), azuresearch.WithMetrics(col))
+
+ _, err := a.CreateService(context.Background(), driver.ServiceConfig{Name: "s", ResourceGroup: "rg", Location: "eastus"})
+ require.NoError(t, err)
+
+ assert.Equal(t, 1, rec.CallCountFor("azuresearch", "CreateService"))
+ assert.NotEmpty(t, col.All())
+}
+
+func TestPortableErrorInjection(t *testing.T) {
+ inj := inject.NewInjector()
+ boom := errors.New("injected")
+ inj.Set("azuresearch", "CreateOrUpdateIndex", boom, inject.Always{})
+
+ a := newPortable(azuresearch.WithErrorInjection(inj))
+
+ _, err := a.CreateOrUpdateIndex(context.Background(), "s", driver.Index{Name: "i"})
+ require.ErrorIs(t, err, boom)
+}
+
+func TestPortableLatencyApplied(t *testing.T) {
+ a := newPortable(azuresearch.WithLatency(20 * time.Millisecond))
+
+ start := time.Now()
+ _, err := a.ListServices(context.Background())
+ require.NoError(t, err)
+ assert.GreaterOrEqual(t, time.Since(start), 20*time.Millisecond)
+}
+
+func TestPortableForwardsResults(t *testing.T) {
+ a := newPortable()
+ ctx := context.Background()
+
+ _, err := a.CreateService(ctx, driver.ServiceConfig{Name: "s", ResourceGroup: "rg", Location: "eastus"})
+ require.NoError(t, err)
+
+ _, err = a.CreateOrUpdateIndex(ctx, "s", driver.Index{
+ Name: "products", Fields: []driver.Field{{Name: "id", Type: "Edm.String", Key: true}},
+ })
+ require.NoError(t, err)
+
+ _, err = a.IndexDocuments(ctx, "s", "products", []driver.IndexAction{
+ {Action: "upload", Document: map[string]any{"id": "1", "name": "alpha widget"}},
+ })
+ require.NoError(t, err)
+
+ resp, err := a.SearchDocuments(ctx, "s", "products", driver.SearchRequest{Search: "widget", Count: true})
+ require.NoError(t, err)
+ assert.EqualValues(t, 1, resp.Count)
+ require.Len(t, resp.Results, 1)
+}
diff --git a/services/azuresearch/driver/control.go b/services/azuresearch/driver/control.go
new file mode 100644
index 0000000..22b7c93
--- /dev/null
+++ b/services/azuresearch/driver/control.go
@@ -0,0 +1,90 @@
+package driver
+
+import "context"
+
+// ServiceConfig describes a search service to create or update.
+type ServiceConfig struct {
+ Name string
+ ResourceGroup string
+ Location string
+ SKUName string // free | basic | standard | standard2 | standard3 | storage_optimized_l1 | ...
+ ReplicaCount int
+ PartitionCount int
+ HostingMode string // default | highDensity
+ Tags map[string]string
+}
+
+// Service is a Microsoft.Search/searchServices resource.
+type Service struct {
+ ID string
+ Name string
+ ResourceGroup string
+ Location string
+ SKUName string
+ ReplicaCount int
+ PartitionCount int
+ HostingMode string
+ Endpoint string
+ Status string
+ ProvisioningState string
+ Tags map[string]string
+ CreatedAt string
+}
+
+// AdminKeys holds the primary/secondary admin keys for a service.
+type AdminKeys struct {
+ Primary string
+ Secondary string
+}
+
+// QueryKey is a read-only query key for a service.
+type QueryKey struct {
+ Name string
+ Key string
+}
+
+// SharedPrivateLink is a shared private-link resource under a service.
+type SharedPrivateLink struct {
+ ID string
+ Name string
+ GroupID string
+ PrivateLinkID string
+ RequestMessage string
+ Status string // Pending | Approved | Rejected | Disconnected
+ ProvisioningState string
+}
+
+// PrivateEndpointConnection is a private-endpoint connection under a service.
+type PrivateEndpointConnection struct {
+ ID string
+ Name string
+ Status string
+ Description string
+ ProvisioningState string
+}
+
+// SearchControl is the Microsoft.Search ARM control-plane surface.
+type SearchControl interface {
+ CreateService(ctx context.Context, cfg ServiceConfig) (*Service, error)
+ GetService(ctx context.Context, resourceGroup, name string) (*Service, error)
+ DeleteService(ctx context.Context, resourceGroup, name string) error
+ UpdateService(ctx context.Context, resourceGroup, name string, replicas, partitions int, tags map[string]string) (*Service, error)
+ ListServicesByResourceGroup(ctx context.Context, resourceGroup string) ([]Service, error)
+ ListServices(ctx context.Context) ([]Service, error)
+
+ ListAdminKeys(ctx context.Context, resourceGroup, name string) (*AdminKeys, error)
+ RegenerateAdminKey(ctx context.Context, resourceGroup, name, which string) (*AdminKeys, error)
+ ListQueryKeys(ctx context.Context, resourceGroup, name string) ([]QueryKey, error)
+ CreateQueryKey(ctx context.Context, resourceGroup, name, keyName string) (*QueryKey, error)
+ DeleteQueryKey(ctx context.Context, resourceGroup, name, key string) error
+
+ PutSharedPrivateLink(ctx context.Context, resourceGroup, name, linkName, groupID, privateLinkID string) (*SharedPrivateLink, error)
+ GetSharedPrivateLink(ctx context.Context, resourceGroup, name, linkName string) (*SharedPrivateLink, error)
+ DeleteSharedPrivateLink(ctx context.Context, resourceGroup, name, linkName string) error
+ ListSharedPrivateLinks(ctx context.Context, resourceGroup, name string) ([]SharedPrivateLink, error)
+
+ PutPrivateEndpointConnection(ctx context.Context, resourceGroup, name, connName, status string) (*PrivateEndpointConnection, error)
+ GetPrivateEndpointConnection(ctx context.Context, resourceGroup, name, connName string) (*PrivateEndpointConnection, error)
+ DeletePrivateEndpointConnection(ctx context.Context, resourceGroup, name, connName string) error
+ ListPrivateEndpointConnections(ctx context.Context, resourceGroup, name string) ([]PrivateEndpointConnection, error)
+}
diff --git a/services/azuresearch/driver/dataplane.go b/services/azuresearch/driver/dataplane.go
new file mode 100644
index 0000000..86b6f00
--- /dev/null
+++ b/services/azuresearch/driver/dataplane.go
@@ -0,0 +1,189 @@
+package driver
+
+import "context"
+
+// Index is a search index definition (data plane).
+type Index struct {
+ Name string
+ Fields []Field
+ DefaultScoring string
+ ETag string
+}
+
+// Field is one index field.
+type Field struct {
+ Name string
+ Type string // Edm.String | Edm.Int32 | Collection(Edm.Single) | ...
+ Key bool
+ Searchable bool
+ Filterable bool
+ Sortable bool
+ Facetable bool
+ Retrievable bool
+ Dimensions int // vector field width (0 = non-vector)
+}
+
+// IndexerConfig describes an indexer to create or update.
+type IndexerConfig struct {
+ Name string
+ DataSourceName string
+ TargetIndex string
+ SkillsetName string
+ Schedule string
+}
+
+// Indexer is a search indexer.
+type Indexer struct {
+ Name string
+ DataSourceName string
+ TargetIndex string
+ SkillsetName string
+ Schedule string
+ ETag string
+}
+
+// IndexerStatus is the execution status of an indexer.
+type IndexerStatus struct {
+ Name string
+ Status string // running | success | error | reset
+ LastResult string
+ ItemsProcessed int
+ ItemsFailed int
+}
+
+// DataSource is an indexer data source.
+type DataSource struct {
+ Name string
+ Type string // azureblob | azuresql | cosmosdb | ...
+ Container string
+ ConnString string
+ ETag string
+}
+
+// Skillset is an AI enrichment skillset.
+type Skillset struct {
+ Name string
+ Description string
+ SkillCount int
+ ETag string
+}
+
+// SynonymMap is a synonym map.
+type SynonymMap struct {
+ Name string
+ Format string // solr
+ Synonyms string
+ ETag string
+}
+
+// Alias maps an alias name to an index.
+type Alias struct {
+ Name string
+ Indexes []string
+ ETag string
+}
+
+// IndexAction is one document mutation in a batch.
+type IndexAction struct {
+ Action string // upload | merge | mergeOrUpload | delete
+ Document map[string]any
+}
+
+// IndexResult is the per-document outcome of a batch index call.
+type IndexResult struct {
+ Key string
+ Status bool
+ StatusCode int
+ ErrorMsg string
+}
+
+// SearchRequest is a document search query.
+type SearchRequest struct {
+ Search string
+ Filter string
+ Top int
+ Skip int
+ OrderBy string
+ Select []string
+ Count bool
+}
+
+// SearchResult is a single matched document with its score.
+type SearchResult struct {
+ Score float64
+ Document map[string]any
+}
+
+// SearchResponse is the result of a document search.
+type SearchResponse struct {
+ Count int64 // -1 when not requested
+ Results []SearchResult
+}
+
+// SuggestResult is one suggestion / autocomplete hit.
+type SuggestResult struct {
+ Text string
+ Document map[string]any
+}
+
+// ServiceStatistics summarizes counts and usage for a service's data plane.
+type ServiceStatistics struct {
+ DocumentCount int64
+ IndexCount int
+ IndexerCount int
+ DataSourceCount int
+ StorageBytes int64
+}
+
+// SearchDataPlane is the {service}.search.windows.net data-plane surface.
+type SearchDataPlane interface {
+ // Indexes.
+ CreateOrUpdateIndex(ctx context.Context, service string, idx Index) (*Index, error)
+ GetIndex(ctx context.Context, service, name string) (*Index, error)
+ ListIndexes(ctx context.Context, service string) ([]Index, error)
+ DeleteIndex(ctx context.Context, service, name string) error
+
+ // Documents.
+ IndexDocuments(ctx context.Context, service, index string, actions []IndexAction) ([]IndexResult, error)
+ SearchDocuments(ctx context.Context, service, index string, req SearchRequest) (*SearchResponse, error)
+ SuggestDocuments(ctx context.Context, service, index, searchText, suggester string, top int) ([]SuggestResult, error)
+ AutocompleteDocuments(ctx context.Context, service, index, searchText, suggester string, top int) ([]string, error)
+ CountDocuments(ctx context.Context, service, index string) (int64, error)
+ GetDocument(ctx context.Context, service, index, key string) (map[string]any, error)
+
+ // Indexers.
+ CreateOrUpdateIndexer(ctx context.Context, service string, cfg IndexerConfig) (*Indexer, error)
+ GetIndexer(ctx context.Context, service, name string) (*Indexer, error)
+ ListIndexers(ctx context.Context, service string) ([]Indexer, error)
+ DeleteIndexer(ctx context.Context, service, name string) error
+ RunIndexer(ctx context.Context, service, name string) error
+ ResetIndexer(ctx context.Context, service, name string) error
+ GetIndexerStatus(ctx context.Context, service, name string) (*IndexerStatus, error)
+
+ // Data sources.
+ CreateOrUpdateDataSource(ctx context.Context, service string, ds DataSource) (*DataSource, error)
+ GetDataSource(ctx context.Context, service, name string) (*DataSource, error)
+ ListDataSources(ctx context.Context, service string) ([]DataSource, error)
+ DeleteDataSource(ctx context.Context, service, name string) error
+
+ // Skillsets.
+ CreateOrUpdateSkillset(ctx context.Context, service string, sk Skillset) (*Skillset, error)
+ GetSkillset(ctx context.Context, service, name string) (*Skillset, error)
+ ListSkillsets(ctx context.Context, service string) ([]Skillset, error)
+ DeleteSkillset(ctx context.Context, service, name string) error
+
+ // Synonym maps.
+ CreateOrUpdateSynonymMap(ctx context.Context, service string, sm SynonymMap) (*SynonymMap, error)
+ GetSynonymMap(ctx context.Context, service, name string) (*SynonymMap, error)
+ ListSynonymMaps(ctx context.Context, service string) ([]SynonymMap, error)
+ DeleteSynonymMap(ctx context.Context, service, name string) error
+
+ // Aliases.
+ CreateOrUpdateAlias(ctx context.Context, service string, alias Alias) (*Alias, error)
+ GetAlias(ctx context.Context, service, name string) (*Alias, error)
+ ListAliases(ctx context.Context, service string) ([]Alias, error)
+ DeleteAlias(ctx context.Context, service, name string) error
+
+ // Service statistics.
+ GetServiceStatistics(ctx context.Context, service string) (*ServiceStatistics, error)
+}
diff --git a/services/azuresearch/driver/driver.go b/services/azuresearch/driver/driver.go
new file mode 100644
index 0000000..e3bf3bf
--- /dev/null
+++ b/services/azuresearch/driver/driver.go
@@ -0,0 +1,29 @@
+// Package driver defines the interface for Azure AI Search
+// (Microsoft.Search/searchServices) — both the ARM control plane (service
+// lifecycle, admin/query keys, private links) and the search data plane
+// (indexes, documents, indexers, data sources, skillsets, synonym maps,
+// aliases, service statistics).
+//
+// The interface uses plain Go types only (no cloud SDK dependencies). ARM
+// resource IDs follow the standard
+// /subscriptions/{s}/resourceGroups/{rg}/providers/Microsoft.Search/searchServices/{name}
+// convention. The data plane is hosted at {service}.search.windows.net.
+package driver
+
+// Provisioning / status values for a search service.
+const (
+ StateSucceeded = "succeeded"
+ StateProvisioning = "provisioning"
+ StateFailed = "failed"
+
+ StatusRunning = "running"
+ StatusDegraded = "degraded"
+ StatusError = "error"
+)
+
+// AzureSearch is the full Azure AI Search surface: the ARM control plane plus
+// the search data plane.
+type AzureSearch interface {
+ SearchControl
+ SearchDataPlane
+}
diff --git a/services/azuresearch/wrappers_control.go b/services/azuresearch/wrappers_control.go
new file mode 100644
index 0000000..c678b1f
--- /dev/null
+++ b/services/azuresearch/wrappers_control.go
@@ -0,0 +1,118 @@
+package azuresearch
+
+import (
+ "context"
+
+ "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureSearch) CreateService(ctx context.Context, cfg driver.ServiceConfig) (*driver.Service, error) {
+ return cast[*driver.Service](a.do(ctx, "CreateService", cfg, func() (any, error) { return a.drv.CreateService(ctx, cfg) }))
+}
+
+func (a *AzureSearch) GetService(ctx context.Context, rg, name string) (*driver.Service, error) {
+ return cast[*driver.Service](a.do(ctx, "GetService", name, func() (any, error) { return a.drv.GetService(ctx, rg, name) }))
+}
+
+func (a *AzureSearch) DeleteService(ctx context.Context, rg, name string) error {
+ return a.act(ctx, "DeleteService", name, func() error { return a.drv.DeleteService(ctx, rg, name) })
+}
+
+func (a *AzureSearch) UpdateService(
+ ctx context.Context, rg, name string, replicas, partitions int, tags map[string]string,
+) (*driver.Service, error) {
+ return cast[*driver.Service](a.do(ctx, "UpdateService", name, func() (any, error) {
+ return a.drv.UpdateService(ctx, rg, name, replicas, partitions, tags)
+ }))
+}
+
+func (a *AzureSearch) ListServicesByResourceGroup(ctx context.Context, rg string) ([]driver.Service, error) {
+ return cast[[]driver.Service](a.do(ctx, "ListServicesByResourceGroup", rg, func() (any, error) {
+ return a.drv.ListServicesByResourceGroup(ctx, rg)
+ }))
+}
+
+func (a *AzureSearch) ListServices(ctx context.Context) ([]driver.Service, error) {
+ return cast[[]driver.Service](a.do(ctx, "ListServices", nil, func() (any, error) { return a.drv.ListServices(ctx) }))
+}
+
+func (a *AzureSearch) ListAdminKeys(ctx context.Context, rg, name string) (*driver.AdminKeys, error) {
+ return cast[*driver.AdminKeys](a.do(ctx, "ListAdminKeys", name, func() (any, error) { return a.drv.ListAdminKeys(ctx, rg, name) }))
+}
+
+func (a *AzureSearch) RegenerateAdminKey(ctx context.Context, rg, name, which string) (*driver.AdminKeys, error) {
+ return cast[*driver.AdminKeys](a.do(ctx, "RegenerateAdminKey", name, func() (any, error) {
+ return a.drv.RegenerateAdminKey(ctx, rg, name, which)
+ }))
+}
+
+func (a *AzureSearch) ListQueryKeys(ctx context.Context, rg, name string) ([]driver.QueryKey, error) {
+ return cast[[]driver.QueryKey](a.do(ctx, "ListQueryKeys", name, func() (any, error) { return a.drv.ListQueryKeys(ctx, rg, name) }))
+}
+
+func (a *AzureSearch) CreateQueryKey(ctx context.Context, rg, name, keyName string) (*driver.QueryKey, error) {
+ return cast[*driver.QueryKey](a.do(ctx, "CreateQueryKey", name, func() (any, error) {
+ return a.drv.CreateQueryKey(ctx, rg, name, keyName)
+ }))
+}
+
+func (a *AzureSearch) DeleteQueryKey(ctx context.Context, rg, name, key string) error {
+ return a.act(ctx, "DeleteQueryKey", name, func() error { return a.drv.DeleteQueryKey(ctx, rg, name, key) })
+}
+
+func (a *AzureSearch) PutSharedPrivateLink(
+ ctx context.Context, rg, name, linkName, groupID, privateLinkID string,
+) (*driver.SharedPrivateLink, error) {
+ return cast[*driver.SharedPrivateLink](a.do(ctx, "PutSharedPrivateLink", linkName, func() (any, error) {
+ return a.drv.PutSharedPrivateLink(ctx, rg, name, linkName, groupID, privateLinkID)
+ }))
+}
+
+func (a *AzureSearch) GetSharedPrivateLink(ctx context.Context, rg, name, linkName string) (*driver.SharedPrivateLink, error) {
+ return cast[*driver.SharedPrivateLink](a.do(ctx, "GetSharedPrivateLink", linkName, func() (any, error) {
+ return a.drv.GetSharedPrivateLink(ctx, rg, name, linkName)
+ }))
+}
+
+func (a *AzureSearch) DeleteSharedPrivateLink(ctx context.Context, rg, name, linkName string) error {
+ return a.act(ctx, "DeleteSharedPrivateLink", linkName, func() error {
+ return a.drv.DeleteSharedPrivateLink(ctx, rg, name, linkName)
+ })
+}
+
+func (a *AzureSearch) ListSharedPrivateLinks(ctx context.Context, rg, name string) ([]driver.SharedPrivateLink, error) {
+ return cast[[]driver.SharedPrivateLink](a.do(ctx, "ListSharedPrivateLinks", name, func() (any, error) {
+ return a.drv.ListSharedPrivateLinks(ctx, rg, name)
+ }))
+}
+
+func (a *AzureSearch) PutPrivateEndpointConnection(
+ ctx context.Context, rg, name, connName, status string,
+) (*driver.PrivateEndpointConnection, error) {
+ return cast[*driver.PrivateEndpointConnection](a.do(ctx, "PutPrivateEndpointConnection", connName, func() (any, error) {
+ return a.drv.PutPrivateEndpointConnection(ctx, rg, name, connName, status)
+ }))
+}
+
+func (a *AzureSearch) GetPrivateEndpointConnection(
+ ctx context.Context, rg, name, connName string,
+) (*driver.PrivateEndpointConnection, error) {
+ return cast[*driver.PrivateEndpointConnection](a.do(ctx, "GetPrivateEndpointConnection", connName, func() (any, error) {
+ return a.drv.GetPrivateEndpointConnection(ctx, rg, name, connName)
+ }))
+}
+
+func (a *AzureSearch) DeletePrivateEndpointConnection(ctx context.Context, rg, name, connName string) error {
+ return a.act(ctx, "DeletePrivateEndpointConnection", connName, func() error {
+ return a.drv.DeletePrivateEndpointConnection(ctx, rg, name, connName)
+ })
+}
+
+func (a *AzureSearch) ListPrivateEndpointConnections(
+ ctx context.Context, rg, name string,
+) ([]driver.PrivateEndpointConnection, error) {
+ return cast[[]driver.PrivateEndpointConnection](a.do(ctx, "ListPrivateEndpointConnections", name, func() (any, error) {
+ return a.drv.ListPrivateEndpointConnections(ctx, rg, name)
+ }))
+}
diff --git a/services/azuresearch/wrappers_dataplane.go b/services/azuresearch/wrappers_dataplane.go
new file mode 100644
index 0000000..a6d7147
--- /dev/null
+++ b/services/azuresearch/wrappers_dataplane.go
@@ -0,0 +1,178 @@
+package azuresearch
+
+import (
+ "context"
+
+ "github.com/stackshy/cloudemu/v2/services/azuresearch/driver"
+)
+
+func (a *AzureSearch) CreateOrUpdateIndex(ctx context.Context, service string, idx driver.Index) (*driver.Index, error) {
+ return cast[*driver.Index](a.do(ctx, "CreateOrUpdateIndex", idx.Name, func() (any, error) {
+ return a.drv.CreateOrUpdateIndex(ctx, service, idx)
+ }))
+}
+
+func (a *AzureSearch) GetIndex(ctx context.Context, service, name string) (*driver.Index, error) {
+ return cast[*driver.Index](a.do(ctx, "GetIndex", name, func() (any, error) { return a.drv.GetIndex(ctx, service, name) }))
+}
+
+func (a *AzureSearch) ListIndexes(ctx context.Context, service string) ([]driver.Index, error) {
+ return cast[[]driver.Index](a.do(ctx, "ListIndexes", service, func() (any, error) { return a.drv.ListIndexes(ctx, service) }))
+}
+
+func (a *AzureSearch) DeleteIndex(ctx context.Context, service, name string) error {
+ return a.act(ctx, "DeleteIndex", name, func() error { return a.drv.DeleteIndex(ctx, service, name) })
+}
+
+func (a *AzureSearch) IndexDocuments(
+ ctx context.Context, service, index string, actions []driver.IndexAction,
+) ([]driver.IndexResult, error) {
+ return cast[[]driver.IndexResult](a.do(ctx, "IndexDocuments", index, func() (any, error) {
+ return a.drv.IndexDocuments(ctx, service, index, actions)
+ }))
+}
+
+//nolint:gocritic // req matches the driver signature; forwarded unchanged.
+func (a *AzureSearch) SearchDocuments(
+ ctx context.Context, service, index string, req driver.SearchRequest,
+) (*driver.SearchResponse, error) {
+ return cast[*driver.SearchResponse](a.do(ctx, "SearchDocuments", index, func() (any, error) {
+ return a.drv.SearchDocuments(ctx, service, index, req)
+ }))
+}
+
+func (a *AzureSearch) SuggestDocuments(
+ ctx context.Context, service, index, searchText, suggester string, top int,
+) ([]driver.SuggestResult, error) {
+ return cast[[]driver.SuggestResult](a.do(ctx, "SuggestDocuments", index, func() (any, error) {
+ return a.drv.SuggestDocuments(ctx, service, index, searchText, suggester, top)
+ }))
+}
+
+func (a *AzureSearch) AutocompleteDocuments(
+ ctx context.Context, service, index, searchText, suggester string, top int,
+) ([]string, error) {
+ return cast[[]string](a.do(ctx, "AutocompleteDocuments", index, func() (any, error) {
+ return a.drv.AutocompleteDocuments(ctx, service, index, searchText, suggester, top)
+ }))
+}
+
+func (a *AzureSearch) CountDocuments(ctx context.Context, service, index string) (int64, error) {
+ return cast[int64](a.do(ctx, "CountDocuments", index, func() (any, error) { return a.drv.CountDocuments(ctx, service, index) }))
+}
+
+func (a *AzureSearch) GetDocument(ctx context.Context, service, index, key string) (map[string]any, error) {
+ return cast[map[string]any](a.do(ctx, "GetDocument", key, func() (any, error) { return a.drv.GetDocument(ctx, service, index, key) }))
+}
+
+//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
+func (a *AzureSearch) CreateOrUpdateIndexer(ctx context.Context, service string, cfg driver.IndexerConfig) (*driver.Indexer, error) {
+ return cast[*driver.Indexer](a.do(ctx, "CreateOrUpdateIndexer", cfg.Name, func() (any, error) {
+ return a.drv.CreateOrUpdateIndexer(ctx, service, cfg)
+ }))
+}
+
+func (a *AzureSearch) GetIndexer(ctx context.Context, service, name string) (*driver.Indexer, error) {
+ return cast[*driver.Indexer](a.do(ctx, "GetIndexer", name, func() (any, error) { return a.drv.GetIndexer(ctx, service, name) }))
+}
+
+func (a *AzureSearch) ListIndexers(ctx context.Context, service string) ([]driver.Indexer, error) {
+ return cast[[]driver.Indexer](a.do(ctx, "ListIndexers", service, func() (any, error) { return a.drv.ListIndexers(ctx, service) }))
+}
+
+func (a *AzureSearch) DeleteIndexer(ctx context.Context, service, name string) error {
+ return a.act(ctx, "DeleteIndexer", name, func() error { return a.drv.DeleteIndexer(ctx, service, name) })
+}
+
+func (a *AzureSearch) RunIndexer(ctx context.Context, service, name string) error {
+ return a.act(ctx, "RunIndexer", name, func() error { return a.drv.RunIndexer(ctx, service, name) })
+}
+
+func (a *AzureSearch) ResetIndexer(ctx context.Context, service, name string) error {
+ return a.act(ctx, "ResetIndexer", name, func() error { return a.drv.ResetIndexer(ctx, service, name) })
+}
+
+func (a *AzureSearch) GetIndexerStatus(ctx context.Context, service, name string) (*driver.IndexerStatus, error) {
+ return cast[*driver.IndexerStatus](a.do(ctx, "GetIndexerStatus", name, func() (any, error) {
+ return a.drv.GetIndexerStatus(ctx, service, name)
+ }))
+}
+
+//nolint:gocritic // ds matches the driver signature; forwarded unchanged.
+func (a *AzureSearch) CreateOrUpdateDataSource(ctx context.Context, service string, ds driver.DataSource) (*driver.DataSource, error) {
+ return cast[*driver.DataSource](a.do(ctx, "CreateOrUpdateDataSource", ds.Name, func() (any, error) {
+ return a.drv.CreateOrUpdateDataSource(ctx, service, ds)
+ }))
+}
+
+func (a *AzureSearch) GetDataSource(ctx context.Context, service, name string) (*driver.DataSource, error) {
+ return cast[*driver.DataSource](a.do(ctx, "GetDataSource", name, func() (any, error) { return a.drv.GetDataSource(ctx, service, name) }))
+}
+
+func (a *AzureSearch) ListDataSources(ctx context.Context, service string) ([]driver.DataSource, error) {
+ return cast[[]driver.DataSource](a.do(ctx, "ListDataSources", service, func() (any, error) { return a.drv.ListDataSources(ctx, service) }))
+}
+
+func (a *AzureSearch) DeleteDataSource(ctx context.Context, service, name string) error {
+ return a.act(ctx, "DeleteDataSource", name, func() error { return a.drv.DeleteDataSource(ctx, service, name) })
+}
+
+func (a *AzureSearch) CreateOrUpdateSkillset(ctx context.Context, service string, sk driver.Skillset) (*driver.Skillset, error) {
+ return cast[*driver.Skillset](a.do(ctx, "CreateOrUpdateSkillset", sk.Name, func() (any, error) {
+ return a.drv.CreateOrUpdateSkillset(ctx, service, sk)
+ }))
+}
+
+func (a *AzureSearch) GetSkillset(ctx context.Context, service, name string) (*driver.Skillset, error) {
+ return cast[*driver.Skillset](a.do(ctx, "GetSkillset", name, func() (any, error) { return a.drv.GetSkillset(ctx, service, name) }))
+}
+
+func (a *AzureSearch) ListSkillsets(ctx context.Context, service string) ([]driver.Skillset, error) {
+ return cast[[]driver.Skillset](a.do(ctx, "ListSkillsets", service, func() (any, error) { return a.drv.ListSkillsets(ctx, service) }))
+}
+
+func (a *AzureSearch) DeleteSkillset(ctx context.Context, service, name string) error {
+ return a.act(ctx, "DeleteSkillset", name, func() error { return a.drv.DeleteSkillset(ctx, service, name) })
+}
+
+func (a *AzureSearch) CreateOrUpdateSynonymMap(ctx context.Context, service string, sm driver.SynonymMap) (*driver.SynonymMap, error) {
+ return cast[*driver.SynonymMap](a.do(ctx, "CreateOrUpdateSynonymMap", sm.Name, func() (any, error) {
+ return a.drv.CreateOrUpdateSynonymMap(ctx, service, sm)
+ }))
+}
+
+func (a *AzureSearch) GetSynonymMap(ctx context.Context, service, name string) (*driver.SynonymMap, error) {
+ return cast[*driver.SynonymMap](a.do(ctx, "GetSynonymMap", name, func() (any, error) { return a.drv.GetSynonymMap(ctx, service, name) }))
+}
+
+func (a *AzureSearch) ListSynonymMaps(ctx context.Context, service string) ([]driver.SynonymMap, error) {
+ return cast[[]driver.SynonymMap](a.do(ctx, "ListSynonymMaps", service, func() (any, error) { return a.drv.ListSynonymMaps(ctx, service) }))
+}
+
+func (a *AzureSearch) DeleteSynonymMap(ctx context.Context, service, name string) error {
+ return a.act(ctx, "DeleteSynonymMap", name, func() error { return a.drv.DeleteSynonymMap(ctx, service, name) })
+}
+
+func (a *AzureSearch) CreateOrUpdateAlias(ctx context.Context, service string, alias driver.Alias) (*driver.Alias, error) {
+ return cast[*driver.Alias](a.do(ctx, "CreateOrUpdateAlias", alias.Name, func() (any, error) {
+ return a.drv.CreateOrUpdateAlias(ctx, service, alias)
+ }))
+}
+
+func (a *AzureSearch) GetAlias(ctx context.Context, service, name string) (*driver.Alias, error) {
+ return cast[*driver.Alias](a.do(ctx, "GetAlias", name, func() (any, error) { return a.drv.GetAlias(ctx, service, name) }))
+}
+
+func (a *AzureSearch) ListAliases(ctx context.Context, service string) ([]driver.Alias, error) {
+ return cast[[]driver.Alias](a.do(ctx, "ListAliases", service, func() (any, error) { return a.drv.ListAliases(ctx, service) }))
+}
+
+func (a *AzureSearch) DeleteAlias(ctx context.Context, service, name string) error {
+ return a.act(ctx, "DeleteAlias", name, func() error { return a.drv.DeleteAlias(ctx, service, name) })
+}
+
+func (a *AzureSearch) GetServiceStatistics(ctx context.Context, service string) (*driver.ServiceStatistics, error) {
+ return cast[*driver.ServiceStatistics](a.do(ctx, "GetServiceStatistics", service, func() (any, error) {
+ return a.drv.GetServiceStatistics(ctx, service)
+ }))
+}
diff --git a/bedrock/bedrock.go b/services/bedrock/bedrock.go
similarity index 97%
rename from bedrock/bedrock.go
rename to services/bedrock/bedrock.go
index 8bd4fe6..348f2b3 100644
--- a/bedrock/bedrock.go
+++ b/services/bedrock/bedrock.go
@@ -7,11 +7,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/bedrock/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/bedrock/driver"
)
// Bedrock is the portable foundation-model type wrapping a driver with
diff --git a/bedrock/bedrock_test.go b/services/bedrock/bedrock_test.go
similarity index 93%
rename from bedrock/bedrock_test.go
rename to services/bedrock/bedrock_test.go
index 8d7a228..ba0cf5f 100644
--- a/bedrock/bedrock_test.go
+++ b/services/bedrock/bedrock_test.go
@@ -6,13 +6,13 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/bedrock/driver"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- awsbedrock "github.com/stackshy/cloudemu/providers/aws/bedrock"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ awsbedrock "github.com/stackshy/cloudemu/v2/providers/aws/bedrock"
+ "github.com/stackshy/cloudemu/v2/services/bedrock/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/bedrock/driver/driver.go b/services/bedrock/driver/driver.go
similarity index 100%
rename from bedrock/driver/driver.go
rename to services/bedrock/driver/driver.go
diff --git a/cache/cache.go b/services/cache/cache.go
similarity index 96%
rename from cache/cache.go
rename to services/cache/cache.go
index 1081448..54053bf 100644
--- a/cache/cache.go
+++ b/services/cache/cache.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/cache/driver"
)
// Cache is the portable cache type wrapping a driver with cross-cutting concerns.
diff --git a/cache/cache_test.go b/services/cache/cache_test.go
similarity index 98%
rename from cache/cache_test.go
rename to services/cache/cache_test.go
index 3107934..ddf3cd6 100644
--- a/cache/cache_test.go
+++ b/services/cache/cache_test.go
@@ -6,12 +6,12 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/cache/driver"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/providers/aws/elasticache"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/providers/aws/elasticache"
+ "github.com/stackshy/cloudemu/v2/services/cache/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/cache/driver/driver.go b/services/cache/driver/driver.go
similarity index 100%
rename from cache/driver/driver.go
rename to services/cache/driver/driver.go
diff --git a/compute/compute.go b/services/compute/compute.go
similarity index 98%
rename from compute/compute.go
rename to services/compute/compute.go
index a512b1e..65c4c26 100644
--- a/compute/compute.go
+++ b/services/compute/compute.go
@@ -4,11 +4,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
)
// Compute is the portable compute type wrapping a driver with cross-cutting concerns.
diff --git a/compute/compute_test.go b/services/compute/compute_test.go
similarity index 99%
rename from compute/compute_test.go
rename to services/compute/compute_test.go
index 3c71464..c316ff8 100644
--- a/compute/compute_test.go
+++ b/services/compute/compute_test.go
@@ -7,12 +7,12 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/compute/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -1789,7 +1789,7 @@ func (mc *mockCompute) CreateKeyPair(_ context.Context, cfg driver.KeyPairConfig
Name: cfg.Name,
Fingerprint: "fp-" + cfg.Name,
KeyType: keyType,
- PublicKey: "mock-public-key-" + cfg.Name,
+ PublicKey: "mock-public-key-" + cfg.Name,
PrivateKey: "mock-private-key-" + cfg.Name,
Tags: cfg.Tags,
}
diff --git a/compute/driver/driver.go b/services/compute/driver/driver.go
similarity index 100%
rename from compute/driver/driver.go
rename to services/compute/driver/driver.go
diff --git a/compute/states.go b/services/compute/states.go
similarity index 93%
rename from compute/states.go
rename to services/compute/states.go
index bcd7235..e3433e5 100644
--- a/compute/states.go
+++ b/services/compute/states.go
@@ -1,7 +1,7 @@
// Package compute provides a portable compute API with cross-cutting concerns.
package compute
-import "github.com/stackshy/cloudemu/statemachine"
+import "github.com/stackshy/cloudemu/v2/internal/statemachine"
// VM states.
const (
diff --git a/containerregistry/containerregistry.go b/services/containerregistry/containerregistry.go
similarity index 96%
rename from containerregistry/containerregistry.go
rename to services/containerregistry/containerregistry.go
index 542097e..ddb4d69 100644
--- a/containerregistry/containerregistry.go
+++ b/services/containerregistry/containerregistry.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/containerregistry/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
)
const serviceName = "containerregistry"
diff --git a/containerregistry/containerregistry_test.go b/services/containerregistry/containerregistry_test.go
similarity index 97%
rename from containerregistry/containerregistry_test.go
rename to services/containerregistry/containerregistry_test.go
index ca059bb..e337c6f 100644
--- a/containerregistry/containerregistry_test.go
+++ b/services/containerregistry/containerregistry_test.go
@@ -6,13 +6,13 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/containerregistry/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/providers/aws/ecr"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/providers/aws/ecr"
+ "github.com/stackshy/cloudemu/v2/services/containerregistry/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/containerregistry/driver/driver.go b/services/containerregistry/driver/driver.go
similarity index 100%
rename from containerregistry/driver/driver.go
rename to services/containerregistry/driver/driver.go
diff --git a/cost/cost.go b/services/cost/cost.go
similarity index 81%
rename from cost/cost.go
rename to services/cost/cost.go
index 80b162f..3e1c076 100644
--- a/cost/cost.go
+++ b/services/cost/cost.go
@@ -104,6 +104,31 @@ func defaultRates() map[string]float64 {
"vertexai:AssignNotebookRuntime": 0.15, // managed notebook node-hour
"vertexai:CreateModel": 0.0,
"vertexai:CreateEndpoint": 0.0,
+
+ // Azure AI — Cognitive Services (AI Foundry / Azure OpenAI): inference
+ // per call, deployments/accounts free (billed via consumed tokens).
+ "azureai:ChatCompletions": 0.00001, // per-call proxy for token usage
+ "azureai:Completions": 0.00001,
+ "azureai:Embeddings": 0.000001,
+ "azureai:CreateDeployment": 0.0,
+ "azureai:CreateAccount": 0.0,
+
+ // Azure AI — Machine Learning: compute/jobs/endpoints per node-hour,
+ // assets and workspaces free.
+ "azureai:CreateCompute": 0.19, // STANDARD_DS3_v2-equivalent node-hour
+ "azureai:CreateJob": 0.19,
+ "azureai:CreateEndpointDeployment": 0.19, // online-endpoint instance-hour
+ "azureai:ScoreOnlineEndpoint": 0.0, // bundled into instance-hours
+ "azureai:CreateMLWorkspace": 0.0,
+ "azureai:CreateEndpoint": 0.0,
+
+ // Azure AI Search: service billed per SU-hour (proxied at create),
+ // queries/indexing per operation, indexes/data-sources free.
+ "azuresearch:CreateService": 0.336, // standard tier search-unit-hour proxy
+ "azuresearch:SearchDocuments": 0.0000004,
+ "azuresearch:IndexDocuments": 0.0000004,
+ "azuresearch:CreateOrUpdateIndex": 0.0,
+ "azuresearch:CreateOrUpdateIndexer": 0.0,
}
}
diff --git a/cost/cost_test.go b/services/cost/cost_test.go
similarity index 94%
rename from cost/cost_test.go
rename to services/cost/cost_test.go
index 09ef687..6c524c6 100644
--- a/cost/cost_test.go
+++ b/services/cost/cost_test.go
@@ -9,20 +9,29 @@ import (
func TestTracker_Record_And_TotalCost(t *testing.T) {
tests := []struct {
- name string
- records []struct{ svc, op string; qty int }
- expectGt float64
+ name string
+ records []struct {
+ svc, op string
+ qty int
+ }
+ expectGt float64
}{
{
name: "single storage put",
- records: []struct{ svc, op string; qty int }{
+ records: []struct {
+ svc, op string
+ qty int
+ }{
{"storage", "PutObject", 1000},
},
expectGt: 0,
},
{
name: "unknown operation has zero cost",
- records: []struct{ svc, op string; qty int }{
+ records: []struct {
+ svc, op string
+ qty int
+ }{
{"unknown", "DoSomething", 100},
},
expectGt: -1, // total should be 0
diff --git a/database/database.go b/services/database/database.go
similarity index 97%
rename from database/database.go
rename to services/database/database.go
index 523398d..6095bf8 100644
--- a/database/database.go
+++ b/services/database/database.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/database/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/database/driver"
)
// Database is the portable database type wrapping a driver.
diff --git a/database/database_test.go b/services/database/database_test.go
similarity index 98%
rename from database/database_test.go
rename to services/database/database_test.go
index 9f4b7fc..fdc009a 100644
--- a/database/database_test.go
+++ b/services/database/database_test.go
@@ -6,13 +6,13 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/database/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/providers/aws/dynamodb"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/providers/aws/dynamodb"
+ "github.com/stackshy/cloudemu/v2/services/database/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/database/driver/driver.go b/services/database/driver/driver.go
similarity index 100%
rename from database/driver/driver.go
rename to services/database/driver/driver.go
diff --git a/databricks/databricks.go b/services/databricks/databricks.go
similarity index 94%
rename from databricks/databricks.go
rename to services/databricks/databricks.go
index a519541..e467e9e 100644
--- a/databricks/databricks.go
+++ b/services/databricks/databricks.go
@@ -7,11 +7,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/databricks/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
// Databricks is the portable workspace type wrapping a driver with
diff --git a/databricks/databricks_test.go b/services/databricks/databricks_test.go
similarity index 89%
rename from databricks/databricks_test.go
rename to services/databricks/databricks_test.go
index 248d9fc..3c9bc12 100644
--- a/databricks/databricks_test.go
+++ b/services/databricks/databricks_test.go
@@ -6,13 +6,13 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/databricks/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- azuredbx "github.com/stackshy/cloudemu/providers/azure/databricks"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ azuredbx "github.com/stackshy/cloudemu/v2/providers/azure/databricks"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/databricks/dataplane.go b/services/databricks/dataplane.go
similarity index 98%
rename from databricks/dataplane.go
rename to services/databricks/dataplane.go
index 7bbf1f0..85a6d1c 100644
--- a/databricks/dataplane.go
+++ b/services/databricks/dataplane.go
@@ -4,11 +4,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/databricks/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
)
// DataPlane is the portable wrapper for the Databricks workspace data plane
diff --git a/databricks/dataplane_test.go b/services/databricks/dataplane_test.go
similarity index 90%
rename from databricks/dataplane_test.go
rename to services/databricks/dataplane_test.go
index 98592ab..2db6f6f 100644
--- a/databricks/dataplane_test.go
+++ b/services/databricks/dataplane_test.go
@@ -6,12 +6,12 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/databricks/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- azuredbx "github.com/stackshy/cloudemu/providers/azure/databricks"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ azuredbx "github.com/stackshy/cloudemu/v2/providers/azure/databricks"
+ "github.com/stackshy/cloudemu/v2/services/databricks/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/databricks/driver/dataplane.go b/services/databricks/driver/dataplane.go
similarity index 100%
rename from databricks/driver/dataplane.go
rename to services/databricks/driver/dataplane.go
diff --git a/databricks/driver/driver.go b/services/databricks/driver/driver.go
similarity index 100%
rename from databricks/driver/driver.go
rename to services/databricks/driver/driver.go
diff --git a/dns/dns.go b/services/dns/dns.go
similarity index 95%
rename from dns/dns.go
rename to services/dns/dns.go
index a8e11bb..a6e2f3b 100644
--- a/dns/dns.go
+++ b/services/dns/dns.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/dns/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/dns/driver"
)
type DNS struct {
diff --git a/dns/dns_test.go b/services/dns/dns_test.go
similarity index 98%
rename from dns/dns_test.go
rename to services/dns/dns_test.go
index d6158ce..4ae24fd 100644
--- a/dns/dns_test.go
+++ b/services/dns/dns_test.go
@@ -6,10 +6,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/dns/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/dns/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/dns/driver/driver.go b/services/dns/driver/driver.go
similarity index 100%
rename from dns/driver/driver.go
rename to services/dns/driver/driver.go
diff --git a/eventbus/driver/driver.go b/services/eventbus/driver/driver.go
similarity index 100%
rename from eventbus/driver/driver.go
rename to services/eventbus/driver/driver.go
diff --git a/eventbus/eventbus.go b/services/eventbus/eventbus.go
similarity index 96%
rename from eventbus/eventbus.go
rename to services/eventbus/eventbus.go
index d705992..0e1c61c 100644
--- a/eventbus/eventbus.go
+++ b/services/eventbus/eventbus.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
)
// EventBus is the portable event bus type wrapping a driver with cross-cutting concerns.
diff --git a/eventbus/eventbus_test.go b/services/eventbus/eventbus_test.go
similarity index 97%
rename from eventbus/eventbus_test.go
rename to services/eventbus/eventbus_test.go
index 7f7ff58..d45f267 100644
--- a/eventbus/eventbus_test.go
+++ b/services/eventbus/eventbus_test.go
@@ -6,13 +6,13 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/eventbus/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/providers/aws/eventbridge"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/providers/aws/eventbridge"
+ "github.com/stackshy/cloudemu/v2/services/eventbus/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/iam/driver/driver.go b/services/iam/driver/driver.go
similarity index 100%
rename from iam/driver/driver.go
rename to services/iam/driver/driver.go
diff --git a/iam/iam.go b/services/iam/iam.go
similarity index 97%
rename from iam/iam.go
rename to services/iam/iam.go
index cc3d8d5..fe2c8c0 100644
--- a/iam/iam.go
+++ b/services/iam/iam.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/iam/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/iam/driver"
)
// IAM is the portable IAM type wrapping a driver.
diff --git a/iam/iam_test.go b/services/iam/iam_test.go
similarity index 99%
rename from iam/iam_test.go
rename to services/iam/iam_test.go
index 642b821..b0d8ee7 100644
--- a/iam/iam_test.go
+++ b/services/iam/iam_test.go
@@ -6,10 +6,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/iam/driver"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/iam/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/kubernetes/apiserver.go b/services/kubernetes/apiserver.go
similarity index 100%
rename from kubernetes/apiserver.go
rename to services/kubernetes/apiserver.go
diff --git a/kubernetes/apiserver_test.go b/services/kubernetes/apiserver_test.go
similarity index 99%
rename from kubernetes/apiserver_test.go
rename to services/kubernetes/apiserver_test.go
index 6ebb20a..1451f2d 100644
--- a/kubernetes/apiserver_test.go
+++ b/services/kubernetes/apiserver_test.go
@@ -12,7 +12,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "github.com/stackshy/cloudemu/kubernetes"
+ "github.com/stackshy/cloudemu/v2/services/kubernetes"
)
func TestAPIServer_RegisterAndLookup(t *testing.T) {
diff --git a/kubernetes/cascade_test.go b/services/kubernetes/cascade_test.go
similarity index 99%
rename from kubernetes/cascade_test.go
rename to services/kubernetes/cascade_test.go
index dea97b1..c639e5c 100644
--- a/kubernetes/cascade_test.go
+++ b/services/kubernetes/cascade_test.go
@@ -177,4 +177,3 @@ func mustHaveItems(t *testing.T, base, path string, want int) {
t.Fatalf("mustHaveItems: unknown path suffix %s", path)
}
}
-
diff --git a/kubernetes/configmap.go b/services/kubernetes/configmap.go
similarity index 100%
rename from kubernetes/configmap.go
rename to services/kubernetes/configmap.go
diff --git a/kubernetes/deployment.go b/services/kubernetes/deployment.go
similarity index 100%
rename from kubernetes/deployment.go
rename to services/kubernetes/deployment.go
diff --git a/kubernetes/deployment_test.go b/services/kubernetes/deployment_test.go
similarity index 100%
rename from kubernetes/deployment_test.go
rename to services/kubernetes/deployment_test.go
diff --git a/kubernetes/endpoints.go b/services/kubernetes/endpoints.go
similarity index 100%
rename from kubernetes/endpoints.go
rename to services/kubernetes/endpoints.go
diff --git a/kubernetes/endpoints_test.go b/services/kubernetes/endpoints_test.go
similarity index 100%
rename from kubernetes/endpoints_test.go
rename to services/kubernetes/endpoints_test.go
diff --git a/kubernetes/handlers_http_test.go b/services/kubernetes/handlers_http_test.go
similarity index 97%
rename from kubernetes/handlers_http_test.go
rename to services/kubernetes/handlers_http_test.go
index 9b0e2b2..f5f3fa8 100644
--- a/kubernetes/handlers_http_test.go
+++ b/services/kubernetes/handlers_http_test.go
@@ -15,7 +15,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "github.com/stackshy/cloudemu/kubernetes"
+ "github.com/stackshy/cloudemu/v2/services/kubernetes"
)
// newFixture spins up an APIServer with one registered cluster and returns
@@ -496,7 +496,10 @@ func TestConfigMap_PatchBadContentTypeReturns400(t *testing.T) {
base+"/api/v1/namespaces/default/configmaps/p", bytes.NewReader([]byte(`[]`)))
req.Header.Set("Content-Type", "application/strategic-merge-patch+json")
- resp, _ := http.DefaultClient.Do(req)
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("request: %v", err)
+ }
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
@@ -595,7 +598,10 @@ func TestConfigMap_PatchBadJSONReturns400(t *testing.T) {
)
req.Header.Set("Content-Type", "application/merge-patch+json")
- resp, _ := http.DefaultClient.Do(req)
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("request: %v", err)
+ }
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
@@ -618,7 +624,10 @@ func TestConfigMap_PatchTypeIncompatibleReturns400(t *testing.T) {
)
req.Header.Set("Content-Type", "application/merge-patch+json")
- resp, _ := http.DefaultClient.Do(req)
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("request: %v", err)
+ }
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
@@ -636,7 +645,10 @@ func TestNamespace_PatchOnMissingReturns404(t *testing.T) {
)
req.Header.Set("Content-Type", "application/merge-patch+json")
- resp, _ := http.DefaultClient.Do(req)
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("request: %v", err)
+ }
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
diff --git a/kubernetes/internal_test.go b/services/kubernetes/internal_test.go
similarity index 100%
rename from kubernetes/internal_test.go
rename to services/kubernetes/internal_test.go
diff --git a/kubernetes/kubeconfig.go b/services/kubernetes/kubeconfig.go
similarity index 100%
rename from kubernetes/kubeconfig.go
rename to services/kubernetes/kubeconfig.go
diff --git a/kubernetes/namespace.go b/services/kubernetes/namespace.go
similarity index 100%
rename from kubernetes/namespace.go
rename to services/kubernetes/namespace.go
diff --git a/kubernetes/patch.go b/services/kubernetes/patch.go
similarity index 100%
rename from kubernetes/patch.go
rename to services/kubernetes/patch.go
diff --git a/kubernetes/pod.go b/services/kubernetes/pod.go
similarity index 100%
rename from kubernetes/pod.go
rename to services/kubernetes/pod.go
diff --git a/kubernetes/pod_test.go b/services/kubernetes/pod_test.go
similarity index 100%
rename from kubernetes/pod_test.go
rename to services/kubernetes/pod_test.go
diff --git a/kubernetes/route.go b/services/kubernetes/route.go
similarity index 100%
rename from kubernetes/route.go
rename to services/kubernetes/route.go
diff --git a/kubernetes/secret.go b/services/kubernetes/secret.go
similarity index 100%
rename from kubernetes/secret.go
rename to services/kubernetes/secret.go
diff --git a/kubernetes/secret_test.go b/services/kubernetes/secret_test.go
similarity index 100%
rename from kubernetes/secret_test.go
rename to services/kubernetes/secret_test.go
diff --git a/kubernetes/service.go b/services/kubernetes/service.go
similarity index 100%
rename from kubernetes/service.go
rename to services/kubernetes/service.go
diff --git a/kubernetes/service_test.go b/services/kubernetes/service_test.go
similarity index 100%
rename from kubernetes/service_test.go
rename to services/kubernetes/service_test.go
diff --git a/kubernetes/serviceaccount.go b/services/kubernetes/serviceaccount.go
similarity index 100%
rename from kubernetes/serviceaccount.go
rename to services/kubernetes/serviceaccount.go
diff --git a/kubernetes/serviceaccount_test.go b/services/kubernetes/serviceaccount_test.go
similarity index 100%
rename from kubernetes/serviceaccount_test.go
rename to services/kubernetes/serviceaccount_test.go
diff --git a/kubernetes/state.go b/services/kubernetes/state.go
similarity index 100%
rename from kubernetes/state.go
rename to services/kubernetes/state.go
diff --git a/kubernetes/watch.go b/services/kubernetes/watch.go
similarity index 100%
rename from kubernetes/watch.go
rename to services/kubernetes/watch.go
diff --git a/kubernetes/watch_test.go b/services/kubernetes/watch_test.go
similarity index 99%
rename from kubernetes/watch_test.go
rename to services/kubernetes/watch_test.go
index ed8dfbe..e23a08c 100644
--- a/kubernetes/watch_test.go
+++ b/services/kubernetes/watch_test.go
@@ -43,7 +43,7 @@ type nonFlushingWriter struct {
rec *httptest.ResponseRecorder
}
-func (n *nonFlushingWriter) Header() http.Header { return n.rec.Header() }
+func (n *nonFlushingWriter) Header() http.Header { return n.rec.Header() }
func (n *nonFlushingWriter) Write(b []byte) (int, error) { return n.rec.Write(b) }
func (n *nonFlushingWriter) WriteHeader(code int) { n.rec.WriteHeader(code) }
@@ -446,4 +446,3 @@ func tryReceive(sub *subscriber, deadline time.Duration) (watchEvent, bool) {
return watchEvent{}, false
}
}
-
diff --git a/kubernetes/wire.go b/services/kubernetes/wire.go
similarity index 100%
rename from kubernetes/wire.go
rename to services/kubernetes/wire.go
diff --git a/loadbalancer/driver/driver.go b/services/loadbalancer/driver/driver.go
similarity index 100%
rename from loadbalancer/driver/driver.go
rename to services/loadbalancer/driver/driver.go
diff --git a/loadbalancer/loadbalancer.go b/services/loadbalancer/loadbalancer.go
similarity index 96%
rename from loadbalancer/loadbalancer.go
rename to services/loadbalancer/loadbalancer.go
index c5fcd9f..ec987ba 100644
--- a/loadbalancer/loadbalancer.go
+++ b/services/loadbalancer/loadbalancer.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/loadbalancer/driver"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
)
type LB struct {
diff --git a/loadbalancer/loadbalancer_test.go b/services/loadbalancer/loadbalancer_test.go
similarity index 98%
rename from loadbalancer/loadbalancer_test.go
rename to services/loadbalancer/loadbalancer_test.go
index 674b65d..b1d8de2 100644
--- a/loadbalancer/loadbalancer_test.go
+++ b/services/loadbalancer/loadbalancer_test.go
@@ -6,12 +6,12 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/loadbalancer/driver"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/logging/driver/driver.go b/services/logging/driver/driver.go
similarity index 100%
rename from logging/driver/driver.go
rename to services/logging/driver/driver.go
diff --git a/logging/logging.go b/services/logging/logging.go
similarity index 96%
rename from logging/logging.go
rename to services/logging/logging.go
index a528d75..735143d 100644
--- a/logging/logging.go
+++ b/services/logging/logging.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/logging/driver"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/logging/driver"
)
// Logging is the portable logging type wrapping a driver with cross-cutting concerns.
diff --git a/logging/logging_test.go b/services/logging/logging_test.go
similarity index 97%
rename from logging/logging_test.go
rename to services/logging/logging_test.go
index 61840d5..06115f7 100644
--- a/logging/logging_test.go
+++ b/services/logging/logging_test.go
@@ -6,13 +6,13 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/logging/driver"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/providers/aws/cloudwatchlogs"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/providers/aws/cloudwatchlogs"
+ "github.com/stackshy/cloudemu/v2/services/logging/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/messagequeue/driver/driver.go b/services/messagequeue/driver/driver.go
similarity index 100%
rename from messagequeue/driver/driver.go
rename to services/messagequeue/driver/driver.go
diff --git a/messagequeue/messagequeue.go b/services/messagequeue/messagequeue.go
similarity index 95%
rename from messagequeue/messagequeue.go
rename to services/messagequeue/messagequeue.go
index 68be83a..03535d7 100644
--- a/messagequeue/messagequeue.go
+++ b/services/messagequeue/messagequeue.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/messagequeue/driver"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
)
type MQ struct {
diff --git a/messagequeue/messagequeue_test.go b/services/messagequeue/messagequeue_test.go
similarity index 98%
rename from messagequeue/messagequeue_test.go
rename to services/messagequeue/messagequeue_test.go
index 35bb389..f9ba644 100644
--- a/messagequeue/messagequeue_test.go
+++ b/services/messagequeue/messagequeue_test.go
@@ -6,10 +6,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/messagequeue/driver"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/messagequeue/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/monitoring/driver/driver.go b/services/monitoring/driver/driver.go
similarity index 100%
rename from monitoring/driver/driver.go
rename to services/monitoring/driver/driver.go
diff --git a/monitoring/monitoring.go b/services/monitoring/monitoring.go
similarity index 95%
rename from monitoring/monitoring.go
rename to services/monitoring/monitoring.go
index f88e499..67bbc98 100644
--- a/monitoring/monitoring.go
+++ b/services/monitoring/monitoring.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
)
// Monitoring is the portable monitoring type wrapping a driver.
diff --git a/monitoring/monitoring_test.go b/services/monitoring/monitoring_test.go
similarity index 96%
rename from monitoring/monitoring_test.go
rename to services/monitoring/monitoring_test.go
index d95aa2b..4c67df0 100644
--- a/monitoring/monitoring_test.go
+++ b/services/monitoring/monitoring_test.go
@@ -6,13 +6,13 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/monitoring/driver"
- "github.com/stackshy/cloudemu/providers/aws/cloudwatch"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/providers/aws/cloudwatch"
+ "github.com/stackshy/cloudemu/v2/services/monitoring/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/networking/driver/driver.go b/services/networking/driver/driver.go
similarity index 100%
rename from networking/driver/driver.go
rename to services/networking/driver/driver.go
diff --git a/networking/networking.go b/services/networking/networking.go
similarity index 98%
rename from networking/networking.go
rename to services/networking/networking.go
index 1e672f8..3c4abed 100644
--- a/networking/networking.go
+++ b/services/networking/networking.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
)
// Networking is the portable networking type wrapping a driver.
diff --git a/networking/networking_test.go b/services/networking/networking_test.go
similarity index 99%
rename from networking/networking_test.go
rename to services/networking/networking_test.go
index d2615ef..be60aba 100644
--- a/networking/networking_test.go
+++ b/services/networking/networking_test.go
@@ -6,12 +6,12 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/networking/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/notification/driver/driver.go b/services/notification/driver/driver.go
similarity index 100%
rename from notification/driver/driver.go
rename to services/notification/driver/driver.go
diff --git a/notification/notification.go b/services/notification/notification.go
similarity index 94%
rename from notification/notification.go
rename to services/notification/notification.go
index 1c3c28d..ff533f4 100644
--- a/notification/notification.go
+++ b/services/notification/notification.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/notification/driver"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/notification/driver"
)
// Notification is the portable notification type wrapping a driver with cross-cutting concerns.
diff --git a/notification/notification_test.go b/services/notification/notification_test.go
similarity index 97%
rename from notification/notification_test.go
rename to services/notification/notification_test.go
index caac10a..4a1511a 100644
--- a/notification/notification_test.go
+++ b/services/notification/notification_test.go
@@ -6,12 +6,12 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/notification/driver"
- "github.com/stackshy/cloudemu/providers/aws/sns"
- "github.com/stackshy/cloudemu/recorder"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/providers/aws/sns"
+ "github.com/stackshy/cloudemu/v2/services/notification/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/services/parameterstore/driver/driver.go b/services/parameterstore/driver/driver.go
new file mode 100644
index 0000000..ecdd237
--- /dev/null
+++ b/services/parameterstore/driver/driver.go
@@ -0,0 +1,77 @@
+// Package driver defines the interface for SSM Parameter Store service implementations.
+package driver
+
+import "context"
+
+// Parameter types, matching AWS SSM Parameter Store.
+const (
+ // TypeString is a plain single-value string parameter.
+ TypeString = "String"
+ // TypeStringList is a comma-separated list parameter.
+ TypeStringList = "StringList"
+ // TypeSecureString is an (nominally) encrypted parameter. cloudemu stores
+ // the value as-is: there is no real KMS integration.
+ TypeSecureString = "SecureString"
+)
+
+// PutConfig describes a PutParameter request.
+type PutConfig struct {
+ Name string
+ Value string
+ Type string
+ Description string
+ Overwrite bool
+ Tier string
+ DataType string
+}
+
+// Parameter is a single version of a stored parameter.
+type Parameter struct {
+ Name string
+ Type string
+ Value string
+ Version int64
+ ARN string
+ DataType string
+ LastModified string
+ // Selector records how this value was addressed (e.g. ":3" for a version
+ // or ":prod" for a label), for the SDK's Selector response field.
+ Selector string
+}
+
+// ParameterMetadata describes a parameter without its value.
+type ParameterMetadata struct {
+ Name string
+ Type string
+ Description string
+ Version int64
+ ARN string
+ Tier string
+ DataType string
+ LastModified string
+ LastModifiedUser string
+}
+
+// GetByPathInput describes a GetParametersByPath request.
+type GetByPathInput struct {
+ Path string
+ Recursive bool
+ WithDecryption bool
+}
+
+// ParameterStore is the interface SSM Parameter Store provider implementations
+// must satisfy.
+type ParameterStore interface {
+ PutParameter(ctx context.Context, cfg PutConfig) (version int64, tier string, err error)
+ GetParameter(ctx context.Context, name string, withDecryption bool) (*Parameter, error)
+ GetParameters(ctx context.Context, names []string, withDecryption bool) (found []Parameter, invalid []string, err error)
+ GetParametersByPath(ctx context.Context, in GetByPathInput) ([]Parameter, error)
+ DeleteParameter(ctx context.Context, name string) error
+ DeleteParameters(ctx context.Context, names []string) (deleted, invalid []string, err error)
+ DescribeParameters(ctx context.Context) ([]ParameterMetadata, error)
+
+ // GetParameterHistory returns every version of a parameter, oldest first.
+ GetParameterHistory(ctx context.Context, name string) ([]Parameter, error)
+ // LabelParameterVersion attaches labels to a specific version (0 = latest).
+ LabelParameterVersion(ctx context.Context, name string, version int64, labels []string) (appliedVersion int64, invalid []string, err error)
+}
diff --git a/services/parameterstore/parameterstore.go b/services/parameterstore/parameterstore.go
new file mode 100644
index 0000000..8317809
--- /dev/null
+++ b/services/parameterstore/parameterstore.go
@@ -0,0 +1,236 @@
+// Package parameterstore provides a portable SSM Parameter Store API with
+// cross-cutting concerns (recorder, metrics, rate limiting, error injection,
+// simulated latency) wrapping a driver.
+package parameterstore
+
+import (
+ "context"
+ "time"
+
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/parameterstore/driver"
+)
+
+// ParameterStore is the portable Parameter Store type wrapping a driver with
+// cross-cutting concerns.
+type ParameterStore struct {
+ driver driver.ParameterStore
+ recorder *recorder.Recorder
+ metrics *metrics.Collector
+ limiter *ratelimit.Limiter
+ injector *inject.Injector
+ latency time.Duration
+}
+
+// New creates a new portable ParameterStore wrapping the given driver.
+func New(d driver.ParameterStore, opts ...Option) *ParameterStore {
+ p := &ParameterStore{driver: d}
+ for _, opt := range opts {
+ opt(p)
+ }
+
+ return p
+}
+
+// Option configures a portable ParameterStore.
+type Option func(*ParameterStore)
+
+// WithRecorder sets the recorder.
+func WithRecorder(r *recorder.Recorder) Option { return func(p *ParameterStore) { p.recorder = r } }
+
+// WithMetrics sets the metrics collector.
+func WithMetrics(m *metrics.Collector) Option { return func(p *ParameterStore) { p.metrics = m } }
+
+// WithRateLimiter sets the rate limiter.
+func WithRateLimiter(l *ratelimit.Limiter) Option { return func(p *ParameterStore) { p.limiter = l } }
+
+// WithErrorInjection sets the error injector.
+func WithErrorInjection(i *inject.Injector) Option { return func(p *ParameterStore) { p.injector = i } }
+
+// WithLatency sets simulated latency.
+func WithLatency(d time.Duration) Option { return func(p *ParameterStore) { p.latency = d } }
+
+func (p *ParameterStore) do(_ context.Context, op string, input any, fn func() (any, error)) (any, error) {
+ start := time.Now()
+
+ if p.injector != nil {
+ if err := p.injector.Check("ssm", op); err != nil {
+ p.rec(op, input, nil, err, time.Since(start))
+ return nil, err
+ }
+ }
+
+ if p.limiter != nil {
+ if err := p.limiter.Allow(); err != nil {
+ p.rec(op, input, nil, err, time.Since(start))
+ return nil, err
+ }
+ }
+
+ if p.latency > 0 {
+ time.Sleep(p.latency)
+ }
+
+ out, err := fn()
+ dur := time.Since(start)
+
+ if p.metrics != nil {
+ labels := map[string]string{"service": "ssm", "operation": op}
+ p.metrics.Counter("calls_total", 1, labels)
+ p.metrics.Histogram("call_duration", dur, labels)
+
+ if err != nil {
+ p.metrics.Counter("errors_total", 1, labels)
+ }
+ }
+
+ p.rec(op, input, out, err, dur)
+
+ return out, err
+}
+
+func (p *ParameterStore) rec(op string, input, output any, err error, dur time.Duration) {
+ if p.recorder != nil {
+ p.recorder.Record("ssm", op, input, output, err, dur)
+ }
+}
+
+// PutParameter creates or updates a parameter, returning the new version and tier.
+func (p *ParameterStore) PutParameter(ctx context.Context, cfg driver.PutConfig) (int64, string, error) {
+ type result struct {
+ version int64
+ tier string
+ }
+
+ out, err := p.do(ctx, "PutParameter", cfg, func() (any, error) {
+ v, tier, err := p.driver.PutParameter(ctx, cfg)
+ return result{v, tier}, err
+ })
+ if err != nil {
+ return 0, "", err
+ }
+
+ r := out.(result)
+
+ return r.version, r.tier, nil
+}
+
+// GetParameter retrieves a parameter by name (optionally with a version or label selector).
+func (p *ParameterStore) GetParameter(ctx context.Context, name string, withDecryption bool) (*driver.Parameter, error) {
+ out, err := p.do(ctx, "GetParameter", name, func() (any, error) {
+ return p.driver.GetParameter(ctx, name, withDecryption)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return out.(*driver.Parameter), nil
+}
+
+// GetParameters retrieves multiple parameters by name.
+func (p *ParameterStore) GetParameters(ctx context.Context, names []string, withDecryption bool) ([]driver.Parameter, []string, error) {
+ type result struct {
+ found []driver.Parameter
+ invalid []string
+ }
+
+ out, err := p.do(ctx, "GetParameters", names, func() (any, error) {
+ found, invalid, err := p.driver.GetParameters(ctx, names, withDecryption)
+ return result{found, invalid}, err
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+
+ r := out.(result)
+
+ return r.found, r.invalid, nil
+}
+
+// GetParametersByPath retrieves parameters under a hierarchical path.
+func (p *ParameterStore) GetParametersByPath(ctx context.Context, in driver.GetByPathInput) ([]driver.Parameter, error) {
+ out, err := p.do(ctx, "GetParametersByPath", in, func() (any, error) {
+ return p.driver.GetParametersByPath(ctx, in)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return out.([]driver.Parameter), nil
+}
+
+// DeleteParameter deletes a parameter by name.
+func (p *ParameterStore) DeleteParameter(ctx context.Context, name string) error {
+ _, err := p.do(ctx, "DeleteParameter", name, func() (any, error) {
+ return nil, p.driver.DeleteParameter(ctx, name)
+ })
+
+ return err
+}
+
+// DeleteParameters deletes multiple parameters, returning deleted and invalid names.
+func (p *ParameterStore) DeleteParameters(ctx context.Context, names []string) ([]string, []string, error) {
+ type result struct {
+ deleted []string
+ invalid []string
+ }
+
+ out, err := p.do(ctx, "DeleteParameters", names, func() (any, error) {
+ deleted, invalid, err := p.driver.DeleteParameters(ctx, names)
+ return result{deleted, invalid}, err
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+
+ r := out.(result)
+
+ return r.deleted, r.invalid, nil
+}
+
+// DescribeParameters lists parameter metadata.
+func (p *ParameterStore) DescribeParameters(ctx context.Context) ([]driver.ParameterMetadata, error) {
+ out, err := p.do(ctx, "DescribeParameters", nil, func() (any, error) {
+ return p.driver.DescribeParameters(ctx)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return out.([]driver.ParameterMetadata), nil
+}
+
+// GetParameterHistory returns every version of a parameter, oldest first.
+func (p *ParameterStore) GetParameterHistory(ctx context.Context, name string) ([]driver.Parameter, error) {
+ out, err := p.do(ctx, "GetParameterHistory", name, func() (any, error) {
+ return p.driver.GetParameterHistory(ctx, name)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return out.([]driver.Parameter), nil
+}
+
+// LabelParameterVersion attaches labels to a specific version (0 = latest).
+func (p *ParameterStore) LabelParameterVersion(ctx context.Context, name string, version int64, labels []string) (int64, []string, error) {
+ type result struct {
+ applied int64
+ invalid []string
+ }
+
+ out, err := p.do(ctx, "LabelParameterVersion", map[string]any{"name": name, "version": version}, func() (any, error) {
+ applied, invalid, err := p.driver.LabelParameterVersion(ctx, name, version, labels)
+ return result{applied, invalid}, err
+ })
+ if err != nil {
+ return 0, nil, err
+ }
+
+ r := out.(result)
+
+ return r.applied, r.invalid, nil
+}
diff --git a/relationaldb/driver/driver.go b/services/relationaldb/driver/driver.go
similarity index 100%
rename from relationaldb/driver/driver.go
rename to services/relationaldb/driver/driver.go
diff --git a/relationaldb/relationaldb.go b/services/relationaldb/relationaldb.go
similarity index 97%
rename from relationaldb/relationaldb.go
rename to services/relationaldb/relationaldb.go
index d4d5102..082681f 100644
--- a/relationaldb/relationaldb.go
+++ b/services/relationaldb/relationaldb.go
@@ -9,11 +9,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/relationaldb/driver"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/relationaldb/driver"
)
// DB is the portable handle around a relational-database driver.
diff --git a/resourcediscovery/arn.go b/services/resourcediscovery/arn.go
similarity index 98%
rename from resourcediscovery/arn.go
rename to services/resourcediscovery/arn.go
index 681cbe7..9a5b5d6 100644
--- a/resourcediscovery/arn.go
+++ b/services/resourcediscovery/arn.go
@@ -3,7 +3,7 @@ package resourcediscovery
import (
"fmt"
- "github.com/stackshy/cloudemu/internal/idgen"
+ "github.com/stackshy/cloudemu/v2/internal/idgen"
)
// Per-provider ARN/URN builders. These produce best-effort canonical
diff --git a/resourcediscovery/engine.go b/services/resourcediscovery/engine.go
similarity index 88%
rename from resourcediscovery/engine.go
rename to services/resourcediscovery/engine.go
index c9e6724..e707c88 100644
--- a/resourcediscovery/engine.go
+++ b/services/resourcediscovery/engine.go
@@ -3,12 +3,12 @@ package resourcediscovery
import (
"context"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- dbxdriver "github.com/stackshy/cloudemu/databricks/driver"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- serverlessdriver "github.com/stackshy/cloudemu/serverless/driver"
- storagedriver "github.com/stackshy/cloudemu/storage/driver"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
+ dbxdriver "github.com/stackshy/cloudemu/v2/services/databricks/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
+ serverlessdriver "github.com/stackshy/cloudemu/v2/services/serverless/driver"
+ storagedriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
// Drivers bundles the per-service drivers the engine reads from. Any field
diff --git a/resourcediscovery/engine_test.go b/services/resourcediscovery/engine_test.go
similarity index 93%
rename from resourcediscovery/engine_test.go
rename to services/resourcediscovery/engine_test.go
index 57f701d..0048295 100644
--- a/resourcediscovery/engine_test.go
+++ b/services/resourcediscovery/engine_test.go
@@ -7,18 +7,18 @@ import (
"testing"
"time"
- computedriver "github.com/stackshy/cloudemu/compute/driver"
- "github.com/stackshy/cloudemu/config"
- dbdriver "github.com/stackshy/cloudemu/database/driver"
- cerrors "github.com/stackshy/cloudemu/errors"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
- "github.com/stackshy/cloudemu/providers/aws/dynamodb"
- "github.com/stackshy/cloudemu/providers/aws/ec2"
- "github.com/stackshy/cloudemu/providers/aws/lambda"
- "github.com/stackshy/cloudemu/providers/aws/s3"
- "github.com/stackshy/cloudemu/providers/aws/vpc"
- serverlessdriver "github.com/stackshy/cloudemu/serverless/driver"
- storagedriver "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ "github.com/stackshy/cloudemu/v2/providers/aws/dynamodb"
+ "github.com/stackshy/cloudemu/v2/providers/aws/ec2"
+ "github.com/stackshy/cloudemu/v2/providers/aws/lambda"
+ "github.com/stackshy/cloudemu/v2/providers/aws/s3"
+ "github.com/stackshy/cloudemu/v2/providers/aws/vpc"
+ computedriver "github.com/stackshy/cloudemu/v2/services/compute/driver"
+ dbdriver "github.com/stackshy/cloudemu/v2/services/database/driver"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
+ serverlessdriver "github.com/stackshy/cloudemu/v2/services/serverless/driver"
+ storagedriver "github.com/stackshy/cloudemu/v2/services/storage/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/resourcediscovery/search.go b/services/resourcediscovery/search.go
similarity index 100%
rename from resourcediscovery/search.go
rename to services/resourcediscovery/search.go
diff --git a/resourcediscovery/tagging.go b/services/resourcediscovery/tagging.go
similarity index 99%
rename from resourcediscovery/tagging.go
rename to services/resourcediscovery/tagging.go
index 57d3de5..bcdc1c8 100644
--- a/resourcediscovery/tagging.go
+++ b/services/resourcediscovery/tagging.go
@@ -5,7 +5,7 @@ import (
"fmt"
"strings"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
// AWS service identifiers as they appear in ARNs.
diff --git a/resourcediscovery/tagging_test.go b/services/resourcediscovery/tagging_test.go
similarity index 98%
rename from resourcediscovery/tagging_test.go
rename to services/resourcediscovery/tagging_test.go
index 2f77b02..2bb734e 100644
--- a/resourcediscovery/tagging_test.go
+++ b/services/resourcediscovery/tagging_test.go
@@ -5,8 +5,8 @@ import (
"strings"
"testing"
- cerrors "github.com/stackshy/cloudemu/errors"
- netdriver "github.com/stackshy/cloudemu/networking/driver"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
+ netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/resourcediscovery/types.go b/services/resourcediscovery/types.go
similarity index 100%
rename from resourcediscovery/types.go
rename to services/resourcediscovery/types.go
diff --git a/resourcediscovery/walkers.go b/services/resourcediscovery/walkers.go
similarity index 99%
rename from resourcediscovery/walkers.go
rename to services/resourcediscovery/walkers.go
index be5d109..0f0c353 100644
--- a/resourcediscovery/walkers.go
+++ b/services/resourcediscovery/walkers.go
@@ -4,7 +4,7 @@ import (
"context"
"fmt"
- cerrors "github.com/stackshy/cloudemu/errors"
+ cerrors "github.com/stackshy/cloudemu/v2/errors"
)
// Provider name constants used for routing per-provider ARN construction.
diff --git a/sagemaker/driver/cluster.go b/services/sagemaker/driver/cluster.go
similarity index 100%
rename from sagemaker/driver/cluster.go
rename to services/sagemaker/driver/cluster.go
diff --git a/sagemaker/driver/driver.go b/services/sagemaker/driver/driver.go
similarity index 100%
rename from sagemaker/driver/driver.go
rename to services/sagemaker/driver/driver.go
diff --git a/sagemaker/driver/featurestore.go b/services/sagemaker/driver/featurestore.go
similarity index 100%
rename from sagemaker/driver/featurestore.go
rename to services/sagemaker/driver/featurestore.go
diff --git a/sagemaker/driver/inference.go b/services/sagemaker/driver/inference.go
similarity index 100%
rename from sagemaker/driver/inference.go
rename to services/sagemaker/driver/inference.go
diff --git a/sagemaker/driver/jobs.go b/services/sagemaker/driver/jobs.go
similarity index 100%
rename from sagemaker/driver/jobs.go
rename to services/sagemaker/driver/jobs.go
diff --git a/sagemaker/driver/notebook.go b/services/sagemaker/driver/notebook.go
similarity index 100%
rename from sagemaker/driver/notebook.go
rename to services/sagemaker/driver/notebook.go
diff --git a/sagemaker/driver/pipeline.go b/services/sagemaker/driver/pipeline.go
similarity index 100%
rename from sagemaker/driver/pipeline.go
rename to services/sagemaker/driver/pipeline.go
diff --git a/sagemaker/driver/registry.go b/services/sagemaker/driver/registry.go
similarity index 100%
rename from sagemaker/driver/registry.go
rename to services/sagemaker/driver/registry.go
diff --git a/sagemaker/driver/studio.go b/services/sagemaker/driver/studio.go
similarity index 100%
rename from sagemaker/driver/studio.go
rename to services/sagemaker/driver/studio.go
diff --git a/sagemaker/sagemaker.go b/services/sagemaker/sagemaker.go
similarity index 92%
rename from sagemaker/sagemaker.go
rename to services/sagemaker/sagemaker.go
index 6b5eae5..7809e82 100644
--- a/sagemaker/sagemaker.go
+++ b/services/sagemaker/sagemaker.go
@@ -12,11 +12,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// Compile-time check that the portable type implements the full service.
diff --git a/sagemaker/sagemaker_test.go b/services/sagemaker/sagemaker_test.go
similarity index 81%
rename from sagemaker/sagemaker_test.go
rename to services/sagemaker/sagemaker_test.go
index f322aa1..fd07583 100644
--- a/sagemaker/sagemaker_test.go
+++ b/services/sagemaker/sagemaker_test.go
@@ -9,13 +9,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- awssm "github.com/stackshy/cloudemu/providers/aws/sagemaker"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/sagemaker"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ awssm "github.com/stackshy/cloudemu/v2/providers/aws/sagemaker"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
func newPortable(opts ...sagemaker.Option) *sagemaker.SageMaker {
diff --git a/sagemaker/wrappers_inference.go b/services/sagemaker/wrappers_inference.go
similarity index 99%
rename from sagemaker/wrappers_inference.go
rename to services/sagemaker/wrappers_inference.go
index ba77c1f..59002a7 100644
--- a/sagemaker/wrappers_inference.go
+++ b/services/sagemaker/wrappers_inference.go
@@ -3,7 +3,7 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
//nolint:gocritic // cfg/in matches the driver signature; forwarded unchanged.
diff --git a/sagemaker/wrappers_jobs.go b/services/sagemaker/wrappers_jobs.go
similarity index 99%
rename from sagemaker/wrappers_jobs.go
rename to services/sagemaker/wrappers_jobs.go
index e1eb051..620258d 100644
--- a/sagemaker/wrappers_jobs.go
+++ b/services/sagemaker/wrappers_jobs.go
@@ -3,7 +3,7 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
//nolint:gocritic // cfg/in matches the driver signature; forwarded unchanged.
diff --git a/sagemaker/wrappers_rest.go b/services/sagemaker/wrappers_rest.go
similarity index 99%
rename from sagemaker/wrappers_rest.go
rename to services/sagemaker/wrappers_rest.go
index ecd4038..a2bc388 100644
--- a/sagemaker/wrappers_rest.go
+++ b/services/sagemaker/wrappers_rest.go
@@ -3,7 +3,7 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
// --- Cluster ---
diff --git a/sagemaker/wrappers_studio_notebook.go b/services/sagemaker/wrappers_studio_notebook.go
similarity index 99%
rename from sagemaker/wrappers_studio_notebook.go
rename to services/sagemaker/wrappers_studio_notebook.go
index 2f46c56..360c689 100644
--- a/sagemaker/wrappers_studio_notebook.go
+++ b/services/sagemaker/wrappers_studio_notebook.go
@@ -3,7 +3,7 @@ package sagemaker
import (
"context"
- "github.com/stackshy/cloudemu/sagemaker/driver"
+ "github.com/stackshy/cloudemu/v2/services/sagemaker/driver"
)
//nolint:gocritic // cfg/in matches the driver signature; forwarded unchanged.
diff --git a/secrets/driver/driver.go b/services/secrets/driver/driver.go
similarity index 100%
rename from secrets/driver/driver.go
rename to services/secrets/driver/driver.go
diff --git a/secrets/secrets.go b/services/secrets/secrets.go
similarity index 94%
rename from secrets/secrets.go
rename to services/secrets/secrets.go
index 82a9283..246437e 100644
--- a/secrets/secrets.go
+++ b/services/secrets/secrets.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/secrets/driver"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/secrets/driver"
)
// Secrets is the portable secrets type wrapping a driver with cross-cutting concerns.
diff --git a/secrets/secrets_test.go b/services/secrets/secrets_test.go
similarity index 96%
rename from secrets/secrets_test.go
rename to services/secrets/secrets_test.go
index c103096..391c035 100644
--- a/secrets/secrets_test.go
+++ b/services/secrets/secrets_test.go
@@ -6,13 +6,13 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/providers/aws/secretsmanager"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/secrets/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/providers/aws/secretsmanager"
+ "github.com/stackshy/cloudemu/v2/services/secrets/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/serverless/aliases.go b/services/serverless/aliases.go
similarity index 96%
rename from serverless/aliases.go
rename to services/serverless/aliases.go
index 160641f..ff82632 100644
--- a/serverless/aliases.go
+++ b/services/serverless/aliases.go
@@ -3,7 +3,7 @@ package serverless
import (
"context"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// CreateAlias creates a new alias pointing to a specific function version.
diff --git a/serverless/concurrency.go b/services/serverless/concurrency.go
similarity index 94%
rename from serverless/concurrency.go
rename to services/serverless/concurrency.go
index 06255fd..447f319 100644
--- a/serverless/concurrency.go
+++ b/services/serverless/concurrency.go
@@ -3,7 +3,7 @@ package serverless
import (
"context"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// PutFunctionConcurrency sets reserved concurrency for a function.
diff --git a/serverless/driver/driver.go b/services/serverless/driver/driver.go
similarity index 100%
rename from serverless/driver/driver.go
rename to services/serverless/driver/driver.go
diff --git a/serverless/esm.go b/services/serverless/esm.go
similarity index 97%
rename from serverless/esm.go
rename to services/serverless/esm.go
index 0cecedf..1bb4248 100644
--- a/serverless/esm.go
+++ b/services/serverless/esm.go
@@ -3,7 +3,7 @@ package serverless
import (
"context"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// CreateEventSourceMapping creates a new event source mapping.
diff --git a/serverless/layers.go b/services/serverless/layers.go
similarity index 96%
rename from serverless/layers.go
rename to services/serverless/layers.go
index 5113970..c78e264 100644
--- a/serverless/layers.go
+++ b/services/serverless/layers.go
@@ -3,7 +3,7 @@ package serverless
import (
"context"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// PublishLayerVersion publishes a new version of a layer.
diff --git a/serverless/serverless.go b/services/serverless/serverless.go
similarity index 93%
rename from serverless/serverless.go
rename to services/serverless/serverless.go
index 1eeeee9..8f94fc5 100644
--- a/serverless/serverless.go
+++ b/services/serverless/serverless.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// Serverless is the portable serverless type wrapping a driver.
diff --git a/serverless/serverless_test.go b/services/serverless/serverless_test.go
similarity index 99%
rename from serverless/serverless_test.go
rename to services/serverless/serverless_test.go
index 6e31711..a8f84b3 100644
--- a/serverless/serverless_test.go
+++ b/services/serverless/serverless_test.go
@@ -6,10 +6,10 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/serverless/versions.go b/services/serverless/versions.go
similarity index 93%
rename from serverless/versions.go
rename to services/serverless/versions.go
index 1cc8f7d..b61a6fa 100644
--- a/serverless/versions.go
+++ b/services/serverless/versions.go
@@ -3,7 +3,7 @@ package serverless
import (
"context"
- "github.com/stackshy/cloudemu/serverless/driver"
+ "github.com/stackshy/cloudemu/v2/services/serverless/driver"
)
// PublishVersion publishes a new immutable version of a function.
diff --git a/storage/driver/driver.go b/services/storage/driver/driver.go
similarity index 100%
rename from storage/driver/driver.go
rename to services/storage/driver/driver.go
diff --git a/storage/storage.go b/services/storage/storage.go
similarity index 98%
rename from storage/storage.go
rename to services/storage/storage.go
index 562bc7a..c7c2a3d 100644
--- a/storage/storage.go
+++ b/services/storage/storage.go
@@ -5,11 +5,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/storage/driver"
)
// Bucket is the portable storage type wrapping a driver with cross-cutting concerns.
diff --git a/storage/storage_test.go b/services/storage/storage_test.go
similarity index 98%
rename from storage/storage_test.go
rename to services/storage/storage_test.go
index beb5a35..ce83f3e 100644
--- a/storage/storage_test.go
+++ b/services/storage/storage_test.go
@@ -6,13 +6,13 @@ import (
"testing"
"time"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/providers/aws/s3"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/storage/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/providers/aws/s3"
+ "github.com/stackshy/cloudemu/v2/services/storage/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/services/tablestorage/driver/driver.go b/services/tablestorage/driver/driver.go
new file mode 100644
index 0000000..74ae849
--- /dev/null
+++ b/services/tablestorage/driver/driver.go
@@ -0,0 +1,51 @@
+// Package driver defines the interface for Azure Table Storage-style
+// key/value entity stores. Entities live inside named tables and are addressed
+// by a (PartitionKey, RowKey) pair; each entity is a flat bag of named
+// properties.
+//
+// The interface is intentionally small — it mirrors the operations the Azure
+// Table data-plane REST API (aztables) exercises, nothing more.
+package driver
+
+import "context"
+
+// UpdateMode selects how UpdateEntity combines the supplied properties with an
+// existing entity.
+type UpdateMode int
+
+const (
+ // UpdateModeMerge merges the supplied properties into the existing entity,
+ // leaving unmentioned properties untouched.
+ UpdateModeMerge UpdateMode = iota
+ // UpdateModeReplace replaces the entity wholesale with the supplied
+ // properties.
+ UpdateModeReplace
+)
+
+// Entity is a single Table Storage row: a flat map of property name to value.
+// PartitionKey and RowKey are stored as ordinary properties (keys
+// "PartitionKey" and "RowKey") so callers get them back verbatim.
+type Entity map[string]any
+
+// QueryOptions filters a QueryEntities call.
+type QueryOptions struct {
+ // PartitionKey, when non-empty, restricts results to a single partition.
+ PartitionKey string
+ // Filter is an OData $filter expression. Only a small, common subset is
+ // supported (PartitionKey/RowKey/property eq comparisons joined by "and");
+ // an unsupported filter is ignored rather than rejected.
+ Filter string
+}
+
+// TableStorage is the interface a Table Storage backend implements.
+type TableStorage interface {
+ CreateTable(ctx context.Context, name string) error
+ DeleteTable(ctx context.Context, name string) error
+ ListTables(ctx context.Context) ([]string, error)
+
+ InsertEntity(ctx context.Context, table, partitionKey, rowKey string, entity Entity) error
+ GetEntity(ctx context.Context, table, partitionKey, rowKey string) (Entity, error)
+ UpdateEntity(ctx context.Context, table, partitionKey, rowKey string, entity Entity, mode UpdateMode) error
+ DeleteEntity(ctx context.Context, table, partitionKey, rowKey string) error
+ QueryEntities(ctx context.Context, table string, opts QueryOptions) ([]Entity, error)
+}
diff --git a/vertexai/driver/datasets.go b/services/vertexai/driver/datasets.go
similarity index 100%
rename from vertexai/driver/datasets.go
rename to services/vertexai/driver/datasets.go
diff --git a/vertexai/driver/driver.go b/services/vertexai/driver/driver.go
similarity index 100%
rename from vertexai/driver/driver.go
rename to services/vertexai/driver/driver.go
diff --git a/vertexai/driver/endpoints.go b/services/vertexai/driver/endpoints.go
similarity index 100%
rename from vertexai/driver/endpoints.go
rename to services/vertexai/driver/endpoints.go
diff --git a/vertexai/driver/featurestore.go b/services/vertexai/driver/featurestore.go
similarity index 100%
rename from vertexai/driver/featurestore.go
rename to services/vertexai/driver/featurestore.go
diff --git a/vertexai/driver/genai.go b/services/vertexai/driver/genai.go
similarity index 100%
rename from vertexai/driver/genai.go
rename to services/vertexai/driver/genai.go
diff --git a/vertexai/driver/jobs.go b/services/vertexai/driver/jobs.go
similarity index 100%
rename from vertexai/driver/jobs.go
rename to services/vertexai/driver/jobs.go
diff --git a/vertexai/driver/metadata.go b/services/vertexai/driver/metadata.go
similarity index 100%
rename from vertexai/driver/metadata.go
rename to services/vertexai/driver/metadata.go
diff --git a/vertexai/driver/models.go b/services/vertexai/driver/models.go
similarity index 100%
rename from vertexai/driver/models.go
rename to services/vertexai/driver/models.go
diff --git a/vertexai/driver/operations.go b/services/vertexai/driver/operations.go
similarity index 100%
rename from vertexai/driver/operations.go
rename to services/vertexai/driver/operations.go
diff --git a/vertexai/driver/pipelines.go b/services/vertexai/driver/pipelines.go
similarity index 100%
rename from vertexai/driver/pipelines.go
rename to services/vertexai/driver/pipelines.go
diff --git a/vertexai/driver/vectorsearch.go b/services/vertexai/driver/vectorsearch.go
similarity index 100%
rename from vertexai/driver/vectorsearch.go
rename to services/vertexai/driver/vectorsearch.go
diff --git a/vertexai/vertexai.go b/services/vertexai/vertexai.go
similarity index 93%
rename from vertexai/vertexai.go
rename to services/vertexai/vertexai.go
index f449d8a..46223b3 100644
--- a/vertexai/vertexai.go
+++ b/services/vertexai/vertexai.go
@@ -13,11 +13,11 @@ import (
"context"
"time"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- "github.com/stackshy/cloudemu/ratelimit"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/ratelimit"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// Compile-time check that the portable type implements the full service.
diff --git a/vertexai/vertexai_test.go b/services/vertexai/vertexai_test.go
similarity index 82%
rename from vertexai/vertexai_test.go
rename to services/vertexai/vertexai_test.go
index 02d7e3c..4df4ce5 100644
--- a/vertexai/vertexai_test.go
+++ b/services/vertexai/vertexai_test.go
@@ -9,13 +9,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/stackshy/cloudemu/config"
- "github.com/stackshy/cloudemu/inject"
- "github.com/stackshy/cloudemu/metrics"
- gcpvertex "github.com/stackshy/cloudemu/providers/gcp/vertexai"
- "github.com/stackshy/cloudemu/recorder"
- "github.com/stackshy/cloudemu/vertexai"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/config"
+ "github.com/stackshy/cloudemu/v2/features/inject"
+ "github.com/stackshy/cloudemu/v2/features/metrics"
+ "github.com/stackshy/cloudemu/v2/features/recorder"
+ gcpvertex "github.com/stackshy/cloudemu/v2/providers/gcp/vertexai"
+ "github.com/stackshy/cloudemu/v2/services/vertexai"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func newPortable(opts ...vertexai.Option) *vertexai.VertexAI {
diff --git a/vertexai/wrappers_datasets.go b/services/vertexai/wrappers_datasets.go
similarity index 96%
rename from vertexai/wrappers_datasets.go
rename to services/vertexai/wrappers_datasets.go
index d38cac8..093e739 100644
--- a/vertexai/wrappers_datasets.go
+++ b/services/vertexai/wrappers_datasets.go
@@ -3,7 +3,7 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func (v *VertexAI) CreateDataset(ctx context.Context, cfg driver.DatasetConfig) (*driver.Operation, *driver.Dataset, error) {
diff --git a/vertexai/wrappers_endpoints.go b/services/vertexai/wrappers_endpoints.go
similarity index 97%
rename from vertexai/wrappers_endpoints.go
rename to services/vertexai/wrappers_endpoints.go
index 66c15a2..dd18c74 100644
--- a/vertexai/wrappers_endpoints.go
+++ b/services/vertexai/wrappers_endpoints.go
@@ -3,7 +3,7 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func (v *VertexAI) CreateEndpoint(ctx context.Context, cfg driver.EndpointConfig) (*driver.Operation, *driver.Endpoint, error) {
diff --git a/vertexai/wrappers_featurestore.go b/services/vertexai/wrappers_featurestore.go
similarity index 99%
rename from vertexai/wrappers_featurestore.go
rename to services/vertexai/wrappers_featurestore.go
index eeff17e..5af4c3a 100644
--- a/vertexai/wrappers_featurestore.go
+++ b/services/vertexai/wrappers_featurestore.go
@@ -3,7 +3,7 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// --- Legacy Featurestore / EntityType / Feature values ---
diff --git a/vertexai/wrappers_genai.go b/services/vertexai/wrappers_genai.go
similarity index 96%
rename from vertexai/wrappers_genai.go
rename to services/vertexai/wrappers_genai.go
index fa83a30..108a43e 100644
--- a/vertexai/wrappers_genai.go
+++ b/services/vertexai/wrappers_genai.go
@@ -3,7 +3,7 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func (v *VertexAI) GenerateContent(
diff --git a/vertexai/wrappers_jobs.go b/services/vertexai/wrappers_jobs.go
similarity index 98%
rename from vertexai/wrappers_jobs.go
rename to services/vertexai/wrappers_jobs.go
index 4244f44..6dd7d61 100644
--- a/vertexai/wrappers_jobs.go
+++ b/services/vertexai/wrappers_jobs.go
@@ -3,7 +3,7 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func (v *VertexAI) CreateCustomJob(ctx context.Context, cfg driver.CustomJobConfig) (*driver.CustomJob, error) {
diff --git a/vertexai/wrappers_metadata.go b/services/vertexai/wrappers_metadata.go
similarity index 99%
rename from vertexai/wrappers_metadata.go
rename to services/vertexai/wrappers_metadata.go
index a854ce6..d7a8e57 100644
--- a/vertexai/wrappers_metadata.go
+++ b/services/vertexai/wrappers_metadata.go
@@ -3,7 +3,7 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
// --- Metadata stores ---
diff --git a/vertexai/wrappers_models.go b/services/vertexai/wrappers_models.go
similarity index 97%
rename from vertexai/wrappers_models.go
rename to services/vertexai/wrappers_models.go
index c91a38c..5c598d8 100644
--- a/vertexai/wrappers_models.go
+++ b/services/vertexai/wrappers_models.go
@@ -3,7 +3,7 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
//nolint:gocritic // cfg matches the driver signature; forwarded unchanged.
diff --git a/vertexai/wrappers_pipelines.go b/services/vertexai/wrappers_pipelines.go
similarity index 98%
rename from vertexai/wrappers_pipelines.go
rename to services/vertexai/wrappers_pipelines.go
index 12b5343..46ab858 100644
--- a/vertexai/wrappers_pipelines.go
+++ b/services/vertexai/wrappers_pipelines.go
@@ -3,7 +3,7 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func (v *VertexAI) CreateTrainingPipeline(
diff --git a/vertexai/wrappers_vectorsearch.go b/services/vertexai/wrappers_vectorsearch.go
similarity index 98%
rename from vertexai/wrappers_vectorsearch.go
rename to services/vertexai/wrappers_vectorsearch.go
index f568d4b..6b748a6 100644
--- a/vertexai/wrappers_vectorsearch.go
+++ b/services/vertexai/wrappers_vectorsearch.go
@@ -3,7 +3,7 @@ package vertexai
import (
"context"
- "github.com/stackshy/cloudemu/vertexai/driver"
+ "github.com/stackshy/cloudemu/v2/services/vertexai/driver"
)
func (v *VertexAI) CreateIndex(ctx context.Context, cfg driver.IndexConfig) (*driver.Operation, *driver.Index, error) {