Skip to content

nscaledev/uni-region

Repository files navigation

Region

Region is the platform's cloud and region abstraction service. It exposes provider-backed regions, networks, images, servers, load balancers, storage, and related project-scoped infrastructure through one API and lifecycle model.

Developers

Developer Hub

Package Documentation

Implementation-level package documentation lives in pkg/README.md. Use that as the drill-down entry point for the service internals, especially for the API model, provider bindings, handlers, controller lifecycle, and monitoring behaviour.

Architecture

We provide a composable suite of different micro-services that provide different functionality.

Hardware provisioning can come in a number of different flavors, namely bare-metal, managed Kubernetes etc. These services have a common requirement on a compute cloud/region to provision projects, users, roles, networking etc. in order to function.

The current region service is therefore more than simple discovery. It is the shared layer that:

  • maps platform resources onto real cloud/provider resources
  • maintains project-scoped service-principal and resource lifecycle
  • exposes a common API shape across different provider substrates
  • coordinates both desired-state reconciliation and observed-state monitoring

The preferred API direction is v2. Older v1 shapes remain as deprecated compatibility surface and should be migrated away from as quickly as practical.

A Note on Security

At present this service is still monolithic. It combines region discovery and routing with the provider-facing lifecycle work needed to provision and deprovision the aforementioned hardware prerequisites.

Given this service holds elevated-privilege credentials to those clouds, it is somewhat of a honey pot.

Longer term, the goal is to move toward a model where this service is primarily discovery and routing, and platform-specific region controllers live within the target platforms alongside their credentials.

That would reduce blast radius, because compromise of one provider-local controller would not automatically affect the others, and it would avoid having to disseminate provider credentials across the internet.

Supported Providers

OpenStack

OpenStack is an open source cloud provider that allows on premise provisioning of virtual and physical infrastructure. It allows a vertically integrated stack from server to application, so you have full control over the platform. This obviously entails a support crew to keep it up and running!

For further info see the OpenStack provider documentation.

Kubernetes

Kubernetes regions allow existing Kubernetes clusters from any cloud provider to be consumed as region substrates without the hassle of physical infrastructure.

They are not limited to one workload model. A Kubernetes-backed region can be used for Kubernetes-on-Kubernetes patterns such as virtual clusters, but the important contract is broader than that: the backing cluster must expose a region shape that higher-level services can consume in roughly the same way as other clouds. That could include virtual clusters, VM-style provisioning, or other region-shaped services.

For further info see the Kubernetes provider documentation.

Installation

Prerequisites

To use the region service you first need to install:

Installing the Service

The region service is typically installed with Helm as follows:

region:
  ingress:
    host: region.unikorn-cloud.org
    clusterIssuer: letsencrypt-production
    externalDns: true
  oidc:
    issuer: https://identity.unikorn-cloud.org
regions:
- name: gb-north-1
  provider: openstack
  openstack:
    endpoint: https://my-openstack-endpoint.com:5000
    serviceAccountSecret:
      namespace: unikorn-region
      name: gb-north-1-credentials # See the provider setup section

This configures the service to be exposed on the specified host using an ingress with TLS and DDNS.

The OIDC configuration allows token validation at the API.

Regions define cloud instances to expose to clients. The provider field in the Helm values remains required installation configuration. The test fixture provider inference described below only controls which existing Region CR make integration-fixtures uses for generated test data.

Running Tests

Local Testing

The API test targets load test/.env when it exists.

For tests against an existing deployment, copy the example config and update it with your service URLs, tokens, and fixture IDs:

cp test/.env.example test/.env

The required values are:

  • API_BASE_URL - Region API server URL
  • API_AUTH_TOKEN - service token from console
  • REGION_BASE_URL - Direct Region API server URL for hidden endpoints
  • TEST_ORG_ID, TEST_PROJECT_ID, TEST_REGION_ID - test data IDs

For a Helm-based local install in the current kubectl context, generate the install environment and fixtures instead of hand-editing test/.env:

hack/local-install-env --output test/.env.install
make integration-fixtures

hack/local-install-env discovers the identity and region Helm releases, their ingress hosts, and the local CA bundle. make integration-fixtures creates the identity fixtures and writes test/.env.

Internal API tests are local-only by default. The integration fixture setup can generate short-lived mTLS client certificate files and write INTERNAL_API_CLIENT_CERT and INTERNAL_API_CLIENT_KEY into test/.env. Do not store those system-account credentials as shared workflow secrets.

Server lifecycle tests also require TEST_SERVER_FLAVOR_ID and TEST_SERVER_IMAGE_ID to identify a known-compatible flavor/image pair in the target region. The tests skip rather than selecting arbitrary inventory when those values are absent.

The infrastructure placement test additionally requires TEST_SERVER_INFRASTRUCTURE_REF. For local OpenStack or DevStack runs, use hack/openstack/configure-server to create or reuse a fake baremetal node and emit that node UUID as the test infrastructure reference before running make integration-fixtures. Set TEST_SERVER_INFRASTRUCTURE_REF yourself only when you have prepared the target outside the repository helper scripts.

To run API tests against an OpenStack-backed region, generate test/.env.install first, then register or reference the region. When TEST_REGION_ID points at an OpenStack-backed Region CR, the public test region is that existing Region and the simulated private region fixture is still created. OPENSTACK_REGION_ID is accepted as a fallback alias for the value printed by register-region, but TEST_REGION_ID takes precedence when both are set. Register the Region in the same Kubernetes namespace that test/.env.install records for the Region service. register-region prints the environment entries the fixture setup needs:

provider_env="${TMPDIR:-/tmp}/gb-north-1.openstack.env" # From hack/openstack/configure.

. test/.env.install

hack/openstack/register-region \
    --provider-env "${provider_env}" \
    --namespace "${REGION_NAMESPACE}" \
    --region-id c7e8492f-c320-4278-8201-48cd38fed38b \
    --display-name gb-north-1 \
    --secret-name gb-north-1-openstack-credentials \
    --create-secret \
    > test/.env.openstack

hack/openstack/configure-server \
    --provider-env "${provider_env}" \
    --output test/.env.openstack-server

set -a
. test/.env.openstack
. test/.env.openstack-server
set +a
make integration-fixtures

The emitted REGION_PROVIDER=openstack value is a safeguard. The fixture setup infers the provider from TEST_REGION_ID and fails if REGION_PROVIDER disagrees with the Region CR.

If the provider env contains UNIKORN_OPENSTACK_FLAVOR_ID and UNIKORN_OPENSTACK_IMAGE_ID, register-region emits the corresponding TEST_SERVER_FLAVOR_ID and TEST_SERVER_IMAGE_ID values and make integration-fixtures preserves them in test/.env. Otherwise export those TEST_SERVER_* values before running fixtures if you want server lifecycle tests to run rather than skip. For infrastructure placement coverage, run hack/openstack/configure-server with the provider env generated by hack/openstack/configure; make integration-fixtures then preserves the emitted TEST_SERVER_INFRASTRUCTURE_REF in test/.env. configure-server defaults to the fake-hardware Ironic driver. If your DevStack exposes a different driver in openstack baremetal driver list, pass that value with --driver.

For persistent regions, create or sync the OpenStack credential Secret from a password manager and omit --create-secret; see the OpenStack provider documentation for details.

Run tests with:

make test-api                                             # Run all tests
make test-api-verbose                                     # Verbose output
make test-api-focus FOCUS="should return all available"   # Run focused tests

Note: Local env files and CA bundles under test/ are gitignored and may contain sensitive credentials. They should never be committed to the repository.

Integration Fixtures

make integration-fixtures creates the mTLS bootstrap certificate used to seed test data through the API. The fixture generator reuses an existing certificate Secret only when the certificate and key parse correctly, the certificate Common Name matches ci-fixtures, and the certificate is currently valid with at least 15 minutes remaining. The certificate lifetime must also cover the requested fixture certificate duration. Otherwise it removes the stale Secret so cert-manager issues a replacement before writing test/.env.

Fixture certificates last one hour by default. For longer local sessions, override the duration when generating fixtures:

make integration-fixtures FIXTURE_CERT_DURATION=24h

GitHub Actions

Trigger the workflow manually from the Actions tab:

  1. Go to ActionsAPI Tests
  2. Click Run workflow
  3. Check which environments to test:
    • Run Dev tests (checked by default)
    • Run UAT tests (unchecked by default)
    • Can run one, both, or neither
  4. Scheduled UAT runs check out the staged constellation tag resolved by the workflow. Manual UAT runs use the same staged constellation lookup by default. To run UAT against the branch or tag selected in GitHub's manual workflow picker instead, set use_staging_constellation to false. Disabling use_staging_constellation is enough to trigger the UAT job for that selected ref.
  5. Set skip_slack_notifications to true to suppress Slack messages for the run.
  6. View results in the workflow run and download test artifacts

The workflow maps GitHub environment variables and secrets into the test suite configuration. The primary API test tokens for both Dev and UAT/QA are owned by the shared test account [email protected].

Environment Primary token secret Test account
Dev DEV_API_AUTH_TOKEN [email protected]
UAT/QA UAT_API_AUTH_TOKEN [email protected]

Contract Testing

Contract tests verify that the provider service meets consumer expectations defined in the Pact Broker.

Prerequisites

  1. Install Pact FFI library (macOS):

    brew tap pact-foundation/pact-ruby-standalone
    brew install pact-ruby-standalone
    mkdir -p $HOME/Library/pact
    cp /usr/local/opt/pact-ruby-standalone/libexec/lib/*.dylib $HOME/Library/pact/
  2. Start Pact Broker (optional, for local testing):

Download the Uni-core repo and run the following command from its root dir:

make pact-broker-start

Running Consumer Contract Tests

Run consumer tests locally:

make test-contracts-consumer

Run with verbose output:

make test-contracts-consumer-verbose

Publish consumer pact files to Pact Broker (requires Docker):

make publish-contracts-consumer

Run consumer tests and publish in CI:

make test-contracts-consumer-ci

Check if a version can be safely deployed:

make can-i-deploy

Record a deployment to an environment:

make record-deployment

Running Provider Contract Tests

Run verification against pacts from the Pact Broker (this assumes you have already run and published the consumer tests to the broker):

make test-contracts-provider

Run verification against a local pact file (pact for the consumer when testing without a broker):

make test-contracts-provider-local PACT_FILE=/path/to/pact.json

Run with verbose output:

make test-contracts-provider-verbose

Automated Provider Verification (Webhook)

The repository includes a webhook-triggered workflow (.github/workflows/pact-verification.yaml) that automatically verifies contracts when consumers publish new pacts.

How it works:

  1. Consumer (e.g., uni-compute) publishes a new pact to Pact Broker
  2. Pact Broker webhook triggers this repository's GitHub Actions workflow
  3. Provider verification runs automatically against the new contract
  4. Results are published back to Pact Broker
  5. Consumer's can-i-deploy check can now validate compatibility

Setup: The webhook is configured in the Pact Broker by the consumer service. See uni-compute's README for webhook setup instructions.

Workflow trigger:

on:
  repository_dispatch:
    types: [pact_verification]

This workflow receives metadata about which pact to verify and runs make test-contracts-provider-ci to verify and publish results.

Writing Consumer Tests

Consumer tests define uni-region's expectations when calling external APIs (like uni-identity). Tests are located in test/contracts/consumer/{provider}/.

Structure:

  • suite_test.go - Ginkgo test suite setup
  • {feature}_test.go - Consumer contract tests for specific features (e.g., rbac_test.go, allocations_test.go)

Basic Pattern:

  1. Test Setup:

    mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{
        Consumer: "uni-region",
        Provider: "uni-identity",
        PactDir:  "./pacts",
    })
  2. Define Interactions:

    err := mockProvider.
        AddInteraction().
        Given("organization exists with global read permission").
        UponReceiving("a request to get organization ACL").
        WithRequest(http.MethodGet, "/api/v1/organizations/test-org/acl").
        WillRespondWith(http.StatusOK, func(b *consumer.V2ResponseBuilder) {
            b.JSONBody(matchers.StructMatcher{
                "scopes": matchers.EachLike(map[string]interface{}{
                    "name": matchers.String("global"),
                    // ... more fields
                }, 1),
            })
        }).
        ExecuteTest(nil, func(config consumer.MockServerConfig) error {
            // Execute actual API call here
            return nil
        })
  3. Test Organization:

    • Group related tests by feature (RBAC, allocations, etc.)
    • Use descriptive "Given", "UponReceiving" phrases
    • Test both success and error scenarios
    • Use Pact matchers for flexible matching

Example: See test/contracts/consumer/identity/ for complete examples of RBAC and allocation consumer tests.

Writing Provider Tests

Provider tests are located in test/contracts/provider/{consumer}/. Each consumer has:

  • verify_test.go - Main test setup and verification
  • states.go - State handlers for setting up test data
  • middleware.go - Test-specific middleware (e.g., mock ACL)

Basic Pattern:

  1. Test Structure (verify_test.go):

    • Uses Ginkgo/Gomega for BDD-style tests
    • Starts a test server in BeforeEach
    • Creates state handlers mapping Pact states to setup functions
    • Runs verification using provider.NewVerifier()
  2. State Handlers (states.go):

    • Implement parameterized state handlers that accept organization ID and other parameters
    • Use StateManager to create/cleanup Kubernetes resources
    • Follow the builder pattern for creating test resources (see RegionBuilder)
  3. Example State Handler:

    func (sm *StateManager) HandleOrganizationState(ctx context.Context, setup bool, params map[string]interface{}) error {
        orgID := getStringParam(params, ParamOrganizationID, "test-org")
        regionType := getStringParam(params, ParamRegionType, "")
        
        if setup {
            return sm.setupRegions(ctx, orgID, regionType)
        }
        return sm.cleanupAllRegions(ctx)
    }
  4. State Constants:

    • Define state names as constants (must match consumer contract states)
    • Use parameter keys for passing data to state handlers

See test/contracts/provider/compute/ for a complete example following this pattern.

Running All Contract Tests

Run both consumer and provider tests together:

make test-contracts

This is useful for ensuring both your consumer expectations and provider implementations are working correctly before publishing to the Pact Broker.

Contract Test Escape Hatch

In emergencies (e.g. a hotfix that can't wait for contract tests to be updated), you can skip the ConsumerContractTests and CanIDeploy jobs without permanently weakening the CI gate.

How to use it

  1. Apply the skip-contract-tests label to your pull request.
  2. The workflow re-triggers automatically (via the labeled event) — no new commit needed.
  3. ConsumerContractTests and CanIDeploy are both skipped. Skipped jobs count as neutral in GitHub and satisfy branch protection required-status checks.
  4. Remove the label once the contracts are updated.

The label is recorded in the PR timeline, providing a full audit trail of when the escape hatch was used and by whom.

Behaviour

Scenario ConsumerContractTests CanIDeploy
Normal PR (no label) Runs normally Runs if tests pass
PR has skip-contract-tests label Skipped Skipped

One-time setup

Create the label in GitHub → Settings → Labels:

  • Name: skip-contract-tests
  • Description: Use only when contract tests need updating but can't block a hotfix.
  • Colour: your choice (red is a good reminder it's a bypass)

What Next?

The region controller is useless as it is, and requires a service provider to use it to yield a consumable resource. Try out the Kubernetes service.

About

UNI Region Controller

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors