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.
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.
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.
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.
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 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.
To use the region service you first need to install:
- The identity service to provide API authentication and authorization.
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 sectionThis 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.
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/.envThe required values are:
API_BASE_URL- Region API server URLAPI_AUTH_TOKEN- service token from consoleREGION_BASE_URL- Direct Region API server URL for hidden endpointsTEST_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-fixtureshack/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-fixturesThe 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 testsNote: Local env files and CA bundles under test/ are gitignored and may
contain sensitive credentials. They should never be committed to the repository.
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=24hTrigger 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
trueto 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
[email protected].
| Environment | Primary token secret | Test account |
|---|---|---|
| Dev | DEV_API_AUTH_TOKEN |
[email protected] |
| UAT/QA | UAT_API_AUTH_TOKEN |
[email protected] |
Contract tests verify that the provider service meets consumer expectations defined in the Pact Broker.
-
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-startRun consumer tests locally:
make test-contracts-consumerRun with verbose output:
make test-contracts-consumer-verbosePublish consumer pact files to Pact Broker (requires Docker):
make publish-contracts-consumerRun consumer tests and publish in CI:
make test-contracts-consumer-ciCheck if a version can be safely deployed:
make can-i-deployRecord a deployment to an environment:
make record-deploymentRun verification against pacts from the Pact Broker (this assumes you have already run and published the consumer tests to the broker):
make test-contracts-providerRun 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.jsonRun with verbose output:
make test-contracts-provider-verboseThe 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-deploycheck 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.
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.
Provider tests are located in test/contracts/provider/{consumer}/. Each consumer has:
verify_test.go- Main test setup and verificationstates.go- State handlers for setting up test datamiddleware.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
StateManagerto 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.
Run both consumer and provider tests together:
make test-contractsThis is useful for ensuring both your consumer expectations and provider implementations are working correctly before publishing to the Pact Broker.
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.
- Apply the
skip-contract-testslabel to your pull request. - The workflow re-triggers automatically (via the
labeledevent) — no new commit needed. ConsumerContractTestsandCanIDeployare 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.
| Scenario | ConsumerContractTests | CanIDeploy |
|---|---|---|
| Normal PR (no label) | Runs normally | Runs if tests pass |
PR has skip-contract-tests label |
Skipped | Skipped |
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)
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.