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:
- Go to Actions → API Tests
- Click Run workflow
- Check which environments to test:
- Run Dev tests (checked by default)
- Run UAT tests (unchecked by default)
- Can run one, both, or neither
- 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.
- Set skip_slack_notifications to
true to suppress Slack messages for the
run.
- 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
qa-testaccount02@nscale.com.
| Environment |
Primary token secret |
Test account |
| Dev |
DEV_API_AUTH_TOKEN |
qa-testaccount02@nscale.com |
| UAT/QA |
UAT_API_AUTH_TOKEN |
qa-testaccount02@nscale.com |
Contract Testing
Contract tests verify that the provider service meets consumer expectations defined in the Pact Broker.
Prerequisites
-
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/
-
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:
- Consumer (e.g., uni-compute) publishes a new pact to Pact Broker
- Pact Broker webhook triggers this repository's GitHub Actions workflow
- Provider verification runs automatically against the new contract
- Results are published back to Pact Broker
- 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:
-
Test Setup:
mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{
Consumer: "uni-region",
Provider: "uni-identity",
PactDir: "./pacts",
})
-
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
})
-
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:
-
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()
-
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)
-
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)
}
-
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
- Apply the
skip-contract-tests label to your pull request.
- The workflow re-triggers automatically (via the
labeled event) — no new commit needed.
ConsumerContractTests and CanIDeploy are both skipped. Skipped jobs count as neutral in GitHub and satisfy branch protection required-status checks.
- 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.