devfactory-core-control
AWS provisioning automation API for the Dev Factory. A C# ASP.NET Core
Endpoints HTTP 227
| Método | Rota | Arquivo |
|---|---|---|
| GET | / | Program.cs |
| GET | /.metadata | Program.cs |
| GET | /admin/activity | ActivityEndpoints.cs |
| GET | / | AdminEntraAppsEndpoints.cs |
| DELETE | /{appId} | AdminEntraAppsEndpoints.cs |
| POST | /admin/git/reconcile-teams | AdminGitAccessEndpoints.cs |
| GET | / | AdminGitAccessEndpoints.cs |
| PUT | / | AdminGitAccessEndpoints.cs |
| GET | /{subject} | AdminGitAccessEndpoints.cs |
| PUT | /{subject}/profile | AdminGitAccessEndpoints.cs |
| DELETE | /{subject} | AdminGitAccessEndpoints.cs |
| POST | /{subject}/git/sync | AdminGitAccessEndpoints.cs |
| DELETE | /{subject}/orphan-groups/{projectId} | AdminGitAccessEndpoints.cs |
| GET | / | AdminGitAccessEndpoints.cs |
| PUT | / | AdminGitAccessEndpoints.cs |
| GET | /validation | AdminGitAccessEndpoints.cs |
| POST | /sync | AdminGitAccessEndpoints.cs |
| GET | / | AdminSecretsEndpoints.cs |
| GET | /{collection} | AdminSecretsEndpoints.cs |
| POST | /{collection} | AdminSecretsEndpoints.cs |
| GET | /{collection}/{id} | AdminSecretsEndpoints.cs |
| PUT | /{collection}/{id} | AdminSecretsEndpoints.cs |
| PATCH | /{collection}/{id} | AdminSecretsEndpoints.cs |
| DELETE | /{collection}/{id} | AdminSecretsEndpoints.cs |
| POST | /{collection}/{id}/rotate | AdminSecretsEndpoints.cs |
| GET | /health | ApiEndpoints.cs |
| GET | / | ApiEndpoints.cs |
| GET | /overview | ApiEndpoints.cs |
| POST | / | ApiEndpoints.cs |
| GET | /{id} | ApiEndpoints.cs |
| PATCH | /{id} | ApiEndpoints.cs |
| POST | /{id}/on-demand | ApiEndpoints.cs |
| DELETE | /{id} | ApiEndpoints.cs |
| GET | /{id}/activity | ApiEndpoints.cs |
| GET | /{id}/resources | ApiEndpoints.cs |
| POST | /{id}/resources | ApiEndpoints.cs |
| GET | /{id} | ApiEndpoints.cs |
| DELETE | /{id} | ApiEndpoints.cs |
| PATCH | /{id} | ApiEndpoints.cs |
| POST | /{id}/retry | ApiEndpoints.cs |
| POST | /{id}/disable | ApiEndpoints.cs |
| GET | /catalog | ApiEndpoints.cs |
| POST | / | ApplicationEndpoints.cs |
| GET | / | ApplicationEndpoints.cs |
| GET | /{appId} | ApplicationEndpoints.cs |
| PATCH | /{appId} | ApplicationEndpoints.cs |
| DELETE | /{appId} | ApplicationEndpoints.cs |
| POST | /{appId}/unarchive | ApplicationEndpoints.cs |
| POST | /{appId}/retry | ApplicationEndpoints.cs |
| PUT | /{appId}/default | ApplicationEndpoints.cs |
| DELETE | /{appId}/default | ApplicationEndpoints.cs |
| GET | / | ApplicationTemplateEndpoints.cs |
| PUT | / | ApplicationTemplateEndpoints.cs |
| GET | /validation | ApplicationTemplateEndpoints.cs |
| GET | /catalog/applications | ApplicationTemplateEndpoints.cs |
| POST | / | ArtifactEndpoints.cs |
| POST | /{releaseId}/complete | ArtifactEndpoints.cs |
| GET | / | ArtifactEndpoints.cs |
| GET | /{releaseId}/download | ArtifactEndpoints.cs |
| GET | /projects/{id}/applications/{appId}/publish-guide | ArtifactEndpoints.cs |
| GET | /admin/aws-cost | AwsCostEndpoints.cs |
| GET | /admin/aws-cost/projects/{projectId} | AwsCostEndpoints.cs |
| GET | /admin/aws-cost/shared | AwsCostEndpoints.cs |
| GET | /admin/bedrock-keys | BedrockKeysEndpoints.cs |
| POST | /admin/bedrock-keys/{userName}/credentials/{credentialId}/invalidate | BedrockKeysEndpoints.cs |
| POST | /admin/bedrock-keys/{userName}/credentials/{credentialId}/reactivate | BedrockKeysEndpoints.cs |
| GET | /admin/bedrock-keys/{userName}/timeline | BedrockKeysEndpoints.cs |
| GET | /admin/bedrock-usage | BedrockUsageEndpoints.cs |
| GET | /admin/bedrock-usage/summary | BedrockUsageEndpoints.cs |
| GET | /admin/bedrock-usage/users/{user} | BedrockUsageEndpoints.cs |
| GET | /me/bedrock-usage | BedrockUsageEndpoints.cs |
| GET | /admin/bedrock-usage/installment | BedrockUsageEndpoints.cs |
| GET | /admin/capacity | CapacityEndpoints.cs |
| POST | /ecr-login | CiEndpoints.cs |
| GET | /latest | ClarityAnalyticsEndpoints.cs |
| GET | /trend | ClarityAnalyticsEndpoints.cs |
| GET | /config | ClarityAnalyticsEndpoints.cs |
| PUT | /config | ClarityAnalyticsEndpoints.cs |
| POST | /sync | ClarityAnalyticsEndpoints.cs |
| GET | /deep-link | ClarityAnalyticsEndpoints.cs |
Estrutura
README
devfactory-apis-aws
AWS provisioning automation API for the Dev Factory. A C# ASP.NET Core (net10.0) minimal-API service that receives REST calls (from the apis-console YARP gateway) and provisions AWS infrastructure — S3 / ECR / DynamoDB / EKS — via the AWS SDK for .NET.
It runs in two modes: a mock mode (zero AWS calls, for local dev and CI) and an aws mode (real SDK calls under the default credential chain). Provisioning is asynchronous: the create endpoint returns 202 immediately and a background worker drives each resource through its lifecycle.
- Listens on
http://0.0.0.0:8080. - Published as an OCI container via
dotnet publish -t:PublishContainer(no Dockerfile), - AWS account
992151248006, regionus-east-1.
base image mcr.microsoft.com/dotnet/aspnet:10.0, deployed on AWS App Runner.
Endpoints
All paths are forwarded 1:1 from the gateway. JSON is camelCase.
| Method | Path | Auth | Description | |
|---|---|---|---|---|
GET | /health | public | `{ status:"ok", mode:"mock\ | aws", time }` |
GET | /projects | api-key | List projects | |
POST | /projects | api-key | Create a project { name, description? } → 201 | |
GET | /projects/{id} | api-key | Get a project (includes its resources[]) — 404 if absent | |
DELETE | /projects/{id} | api-key | Delete a project and deprovision its resources → 204 | |
GET | /projects/{id}/resources | api-key | List a project's resources | |
POST | /projects/{id}/resources | api-key | Provision a resource { type, name, config? } → 202 | |
GET | /projects/{id}/activity | api-key | List a project's activity events (newest first) | |
GET | /resources/{id} | api-key | Get a resource | |
DELETE | /resources/{id} | api-key | Deprovision a resource → 204 | |
GET | /catalog | api-key | Provisionable types + their config form fields | |
PUT | /projects/{id}/tags | project.update | Replace a project's tags { tags:{k:v} } | |
PATCH | /projects/{id}/tags | project.update | Merge tags (a null value removes the key) | |
GET | /tags | api-key | Aggregated tag facets (keys → values + counts) over visible projects | |
GET | /tags/taxonomy | api-key | Known tag keys, drill-down order, suggestions | |
PUT | /tags/taxonomy | platform.admin | Replace the platform tag taxonomy | |
GET | /saved-views | api-key | Global views + the caller's personal saved filters | |
POST | /saved-views | api-key/platform.admin | Create a saved view (global ⇒ admin) → 201 | |
PUT | /saved-views/{id} | owner/admin | Update a saved view (name/filters/pin/icon/color) | |
DELETE | /saved-views/{id} | owner/admin | Delete a saved view → 204 | |
POST | /projects/{id}/observability/signals | project-key / project.update | Register telemetry signals (batch) → 202 | |
GET | /projects/{id}/observability/signals | project.read | Recent signals (filter: env/kind/level/since/q/top) | |
GET | /projects/{id}/observability/summary | project.read | Counts by level/kind, error rate, 24h buckets |
Swagger UI is served at /swagger (the root / redirects there); the OpenAPI document is at /swagger/v1/swagger.json.
Models (camelCase JSON)
// Project — id like prj_<8hex>
{ "id", "name", "description", "status", "createdAt", "resourceCount",
"tags": { "organization": "energia", "workspace": "ops" } /* AWS-style key/value */ }
// Resource — id like res_<8hex>; status ∈ pending|provisioning|active|failed|deleting
{ "id", "projectId", "type", "name", "status", "arn", "endpoint",
"region", "config", "createdAt", "message" }
// ActivityEvent — id like evt_<8hex>
{ "id", "projectId", "resourceId", "action", "status", "message", "at" }
// ResourceTypeSpec (GET /catalog)
{ "type", "label", "icon",
"fields": [ { "key", "type" /* bool|string|number|select */, "label", "default", "options?" } ] }
type ∈ s3 | ecr | dynamodb | eks.
Project tags & saved views
Projects carry AWS-style key/value tags (Dictionary<string,string>, validated by TagPolicy: ≤50 tags, key ≤128 / value ≤256 chars, devfactory: prefix reserved). They are console-only today (organize + filter; not yet stamped onto provisioned AWS resources). The console builds an *emergent* organization → workspace → team → … hierarchy from them via faceted, drill-down filtering — there are no first-class org/workspace entities; the levels are just tag keys.
- Taxonomy (
/tags/taxonomy) — platform-admin-managed list of *known* keys with a label, color, - Facets (
/tags) — keys → values with per-value project counts, over the projects the caller - Saved views (
/saved-views) — named tag-filters.scopeispersonal(only the owner sees
icon, suggested values and an order that drives the default drill-down. Tagging stays free-form ("conhecidas e livres"): unknown keys still work; they just sort after the known ones. Seeded with organization, workspace, team, environment, cost-center.
can see (same membership filter as GET /projects).
them) or global (everyone; only platform admins may write). pinned views surface in the console sidebar. filters is { [tagKey]: string[] } — OR within a key, AND across keys.
Tags, the taxonomy and saved views all persist through the reserved registry partition (devfactory-platform-registry, unreachable from the public data-plane route grammar), like the rest of the control-plane registry, so they survive pod restarts.
// SavedView — id like view_<8hex>
{ "id", "name", "scope": "personal|global", "ownerSubject", "pinned",
"filters": { "organization": ["energia"], "workspace": ["ops"] },
"icon", "color", "order", "createdAt", "updatedAt", "createdBy" }
Observability (telemetry)
Apps register observability signals — event / log / metric / trace — and the console explores them (the data explorer, but for app telemetry). Ingest accepts a project API key (x-project-key, app→platform) or an Entra user with project.update; reads require project.read.
- Storage: a bounded per-project in-memory ring buffer (last 5 000) powers the fast explorer.
- AWS sink (aws mode, best-effort): every signal is mirrored to Amazon CloudWatch Logs
(group /devfactory/observability/{env}, stream {projectId}) and metric signals also to CloudWatch Metrics (namespace DevFactory/Observability, dimensions Project/Env). Forwarding is fire-and-forget and swallows errors — missing IAM degrades durability, not ingest.
// TelemetrySignal — id like sig_<8hex>
{ "id", "projectId", "env", "kind": "event|log|metric|trace", "name", "level": "debug|info|warn|error",
"message", "value", "unit", "attributes": {"route":"/orders"}, "timestamp", "receivedAt" }
The IAM role apis-aws runs under needs logs:CreateLogGroup|CreateLogStream|PutLogEvents and cloudwatch:PutMetricData for the sink (ingest still works without them).
Modes
Selected by the PROVISIONING_MODE env var (default mock).
mock (default)
No AWS calls whatsoever. The worker simulates a realistic delay (≈1–3 s for s3/ecr/dynamodb, longer for eks) then marks the resource active with a synthesized, obviously-fake arn (it contains mock) and endpoint. This lets the whole API run and be exercised with zero AWS access.
aws
Uses the AWS SDK for .NET. Credentials resolve from the default chain (env vars / shared config / instance or App Runner instance role). Per-type provisioners:
| Type | What it does |
|---|---|
s3 | PutBucketAsync (us-east-1), applies tags, optional versioning. arn:aws:s3:::<bucket>, endpoint https://<bucket>.s3.us-east-1.amazonaws.com. |
ecr | CreateRepositoryAsync + tags (honours scanOnPush, tagMutability). Returns the repo arn + repositoryUri as endpoint. |
dynamodb | CreateTableAsync (PK id S, PAY_PER_REQUEST) + tags, then polls until ACTIVE (bounded by a 2 min timeout). |
eks | Guarded. Only calls CreateClusterAsync if both EKS_ROLE_ARN and EKS_SUBNET_IDS are set — and then returns immediately with status provisioning (it never blocks for the ~15 min creation). Otherwise it returns active with message *"EKS provisioning delegated to Terraform IaC (devfactory-iac)"* and a synthesized arn. |
On any AWS exception in aws-mode the resource is set to failed, the AWS error message is captured in message + an ActivityEvent, and the worker keeps running (it never crashes).
Resource naming & tags
Resources are named <short>-<env>-devfactory-df-<project>-<name>, sanitized per service (lowercase, dns-safe, length-capped). S3 additionally appends the account id for global uniqueness — mirroring the existing devfactory-files-992151248006 bucket, e.g. s3-dev-devfactory-df-myproj-assets-992151248006. env comes from ENVIRONMENT (default dev).
Every AWS resource is tagged: Project, Resource, Environment, Tenant=devfactory, ManagedBy=devfactory-apis-aws.
Environment variables
| Var | Default | Purpose |
|---|---|---|
PROVISIONING_MODE | mock | mock (no AWS) or aws (real SDK calls). |
ENVIRONMENT | dev | Logical env slug embedded in resource names. |
SERVICE_API_KEY | _(unset)_ | If set, every route except /health requires a matching api-key header. Unset ⇒ open (local dev). The gateway injects this header. |
EKS_ROLE_ARN | _(unset)_ | IAM role ARN for the EKS control plane. Required to actually create an EKS cluster. |
EKS_SUBNET_IDS | _(unset)_ | Comma-separated subnet ids for EKS. Required to actually create an EKS cluster. |
AWS_REGION | us-east-1 | Region for the SDK clients and synthesized arns/endpoints. |
AWS_ACCOUNT_ID | 992151248006 | Appended to S3 bucket names for global uniqueness. |
GITHUB_TOKEN | _(unset)_ | PAT with repo-creation rights. Unset ⇒ GitHub provisioning runs in mock mode. |
GITHUB_ORG | devfactory | Platform org: source of generate-from-template + connection-kit reference links. |
GITHUB_APPS_ORG | devfactory-apps | Apps org: where created project repos (app/docs) are created and release CI is wired. |
ASPNETCORE_URLS | http://0.0.0.0:8080 | Bind address. Keep :8080 for App Runner. |
Auth model: the Entra JWT is validated upstream at the gateway. This service trusts the
gateway and only checks the sharedSERVICE_API_KEY. CORS is wide-open here for dev — the
gateway handles real CORS.
Run it locally
Requires the .NET 10 SDK (dotnet --version → 10.0.x).
# mock mode (default) — no AWS credentials needed
cd src/DevFactory.ApisAws
PROVISIONING_MODE=mock dotnet run
# then, in another shell:
curl http://localhost:8080/health
curl -X POST http://localhost:8080/projects \
-H 'content-type: application/json' -d '{"name":"demo"}'
Open <http://localhost:8080/swagger> for the interactive UI.
To run against real AWS (creates real resources, costs money):
PROVISIONING_MODE=aws AWS_REGION=us-east-1 \
AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... \
dotnet run
Smoke test
smoke.sh (bash + curl + jq) and smoke.ps1 (PowerShell) start the app in mock mode, wait for /health, create a project + an S3 resource, poll the resource until active, print the activity feed, and exercise tags + saved views end-to-end (create a tagged project, PATCH tags, read facets/taxonomy, create→list→delete a saved view).
./smoke.sh # or: pwsh ./smoke.ps1
Build
dotnet build -c Release # 0 warnings, 0 errors
Publish the container (no Dockerfile / no daemon)
The csproj carries the container props (ContainerRepository, base image, exposed port 8080, default env). The SDK's built-in container tooling builds the OCI image directly:
cd src/DevFactory.ApisAws
# A) push straight to a registry (e.g. the DevFactory ECR repo) — no Docker daemon required:
dotnet publish -c Release --os linux --arch x64 -t:PublishContainer \
-p:ContainerRegistry=992151248006.dkr.ecr.us-east-1.amazonaws.com \
-p:ContainerRepository=devfactory-apis-aws \
-p:ContainerImageTag=latest
# (authenticate first: aws ecr get-login-password | docker login ... — or use the registry's
# credential helper; the SDK reads ~/.docker/config.json auths)
# B) or emit a portable tarball (handy where no daemon/registry is reachable):
dotnet publish -c Release --os linux --arch x64 -t:PublishContainer \
-p:ContainerArchiveOutputPath=./apis-aws-image.tar.gz
# C) or, on a machine WITH Docker, the default target loads it into the local daemon:
dotnet publish -c Release --os linux --arch x64 -t:PublishContainer
docker run --rm -p 8080:8080 -e PROVISIONING_MODE=mock devfactory-apis-aws:latest
The produced image runs DevFactory.ApisAws on top of mcr.microsoft.com/dotnet/aspnet:10.0 and listens on :8080.
Deploy (AWS App Runner)
Same shape as the sibling APIs that are already live. Push the image to ECR (step A above), then point an App Runner service at it:
- Image:
992151248006.dkr.ecr.us-east-1.amazonaws.com/devfactory-apis-aws:latest - Port:
8080 - Health check:
GET /health(public) - Instance role: an IAM role granting the S3 / ECR / DynamoDB (and optionally EKS) actions
- Runtime env vars:
PROVISIONING_MODE=aws,ENVIRONMENT=dev|prd,
the provisioners call — this is how aws-mode resolves credentials (no keys in env).
SERVICE_API_KEY=<shared key the gateway injects>, and (only if you want this service to create EKS clusters directly) EKS_ROLE_ARN + EKS_SUBNET_IDS.
The apis-console gateway's AwsProvisioning cluster should point at the resulting App Runner URL in production (it defaults to http://apis-aws:8080 for local compose).
Deployment itself is handled by the orchestrator / IaC — this repo does not deploy.
Project layout
src/DevFactory.ApisAws/
DevFactory.ApisAws.csproj net10.0 web app + AWS SDK pkgs + container publish props
Program.cs DI wiring, JSON, Swagger, CORS, middleware, endpoints, seed
appsettings.json logging config
Models/ Project, Resource, ActivityEvent, ResourceTypeSpec, Catalog, Requests
Stores/ IRegistryStore + InMemoryRegistryStore, ActivityRecorder, Ids, DemoSeeder
Provisioning/ IResourceProvisioner + S3/Ecr/DynamoDb/Eks/Mock provisioners,
ProvisionerDispatcher, ProvisioningQueue, ProvisioningWorker (BackgroundService),
ProvisioningOptions, ResourceNaming, ResourceTags, ConfigReader
Endpoints/ ApiEndpoints (minimal-API mapping), ApiKeyMiddleware
smoke.sh / smoke.ps1 end-to-end mock-mode smoke test
Design notes
- State store is an in-memory, thread-safe
ConcurrentDictionaryregistry behind an - Async provisioning uses
System.Threading.Channels(an unbounded queue) feeding a single - Minimal APIs throughout (no MVC controllers).
IRegistryStore interface, so a DynamoDB-backed store could be dropped in later without touching the endpoints or worker. A single demo project (default) is seeded at startup.
BackgroundService consumer. Each lifecycle transition appends an ActivityEvent. The worker is exception-safe — provisioning failures become failed resources, never crashes.