Installation
Requirements
| Component | Minimum version | Needed for |
|---|---|---|
| SKE Operator | v0.32.1 | Installing the portal-controller integration type |
| Portal Controller | v0.2.0 | This integration |
| Portal Patch | 0.7.0 | Declarative customization, optional |
@syntasso/plugin-ske-backend | v0.21.0 | Backstage portals |
@syntasso/plugin-ske-frontend | v0.18.0 | Backstage portals |
Install the Portal Controller
The Portal Controller is installed and upgraded by the
SKE Operator from a single
SKEIntegration resource of
spec.type: portal-controller. Only one SKEIntegration of this type may exist on a platform at
a time.
apiVersion: platform.syntasso.io/v1alpha1
kind: SKEIntegration
metadata:
name: portal-controller
spec:
type: portal-controller
version: v0.2.0
portals: []
Applying the resource installs the Portal Controller Deployment, the PortalCustomization Custom
Resource Definition, and the RBAC and Service Account it runs as, into the
kratix-platform-system namespace.
Connect a portal
Each entry under spec.portals describes one portal:
| Field | Required | Meaning |
|---|---|---|
name | yes | Identifies the portal. Must be unique within type, so you can have two Backstage portals, but not two both named prod. |
type | yes | The portal type this entry targets, for example backstage or cortex. |
url | depends on type | The portal's base URL. Required for Cortex, unused by Backstage. |
secretRef | depends on type | Names the portal's credential Secret. Required for Cortex, unused by Backstage. |
config | depends on type | Settings for this portal only. The keys differ per type. |
adapterConfig | no | Adapter image, pull secrets and generate flags. |
You can declare more than one portal, of the same or different types, on the same
SKEIntegration. What each field needs varies by type:
Credential Secret handling
The Portal Controller does not create or manage a portal's credential Secret, and not every portal needs one. Backstage does not, since it never calls a live API.
Where a portal's secretRef is set, create the Secret yourself in kratix-platform-system before
installing or upgrading, with the credential under a token key:
apiVersion: v1
kind: Secret
metadata:
name: cortex-token
namespace: kratix-platform-system
type: Opaque
stringData:
token: <cortex-api-token>
The operator validates only that a referenced Secret exists, not that it contains a usable
credential. If it is missing, the SKEIntegration reports IntegrationDeploymentReady: False
until you create it.
Once installed, the credential is injected directly into the sync pipeline's writer container, and for Cortex the status exporter, using a secret key reference. It is never written to a shared volume and never reaches the generate or configure stages.
Bind a Promise to a portal
A Promise, or a Resource request, opts into a portal with a label:
metadata:
labels:
kratix.io/portal-backstage: "true" # opt in to the default portal of type "backstage"
If you have more than one portal of the same type, name the one you want with an instance suffix:
metadata:
labels:
kratix.io/portal-backstage.prod: "true" # opt in to the portal named "prod"
On a Promise, only the presence of the label matters. Its value is not read, so
kratix.io/portal-backstage: "false" still opts the Promise in. To opt a Promise out, remove the
label.
Opting a single Resource out
Resource requests inherit their Promise's labels. To keep one request out of the portal, set the
same label to the string "false" on that request:
metadata:
labels:
kratix.io/portal-backstage: "false" # this request only, on an opted-in Promise
The request is skipped silently, with no entity generated and no condition reported. Use the same key the Promise resolved to, including the instance suffix if it used one.
Binding status
A Promise reports how its binding resolved on the PortalBindingReady condition:
| Reason | Meaning |
|---|---|
BindingResolved | The label resolved to exactly one portal |
AmbiguousPortalInstance | More than one portal of the type exists and no instance was named |
PortalConnectionNotFound | The named instance does not match a configured portal |
PortalIntegrationNotFound | No portal-controller SKEIntegration is installed |
CustomizationNotFound | The named PortalCustomization does not exist |
AmbiguousCustomization | More than one PortalCustomization label is present |
Once bound, each sync reports on a per-type condition, BackstagePortalSynced or
CortexPortalSynced:
| Reason | Meaning |
|---|---|
EntitySynced | The entity shipped successfully |
SyncInProgress | The sync Job is running |
GenerateOrWriteFailed | The Job failed before shipping the entity |
PromiseHasNoAPI | The Promise has no spec.api to render into an entity |
On success the Promise is annotated with kratix.io/portal-<type>-entity-url, pointing at the
entity in the portal.
Adapter image resolution
By default, a backstage or cortex portal uses the adapter bundled inside the Portal
Controller's own image, at the version set on spec.version. Most platforms never change this.
To pin a different adapter image for one portal, for example to roll a fix out to a single portal
before the rest, set adapterConfig.image to a single image reference (repo:tag or
repo@sha256, there is no separate version field):
portals:
- name: prod
type: backstage
adapterConfig:
image: ghcr.io/syntasso/ske-portal-controller:v0.2.1
Other adapterConfig fields
| Field | Applies to | Meaning |
|---|---|---|
image | all portals | Adapter image override |
imagePullSecrets | all portals | Extra pull secret names, always merged with syntasso-registry |
generate.args | all portals | Flags passed verbatim to the generate stage |
exporter.deploymentSpec | Cortex portals | Overlay merged into the status exporter's Deployment |
Portal-wide generate flags
adapterConfig.generate.args is a list of flags passed verbatim to the adapter's generate stage
for that portal. Every Promise and Resource routed through the portal gets the same behaviour, so
this is a portal-wide setting, not a per-Promise one:
| Flag | Portal type | Effect |
|---|---|---|
--single-spec-page | backstage | Merges the generated Template's spec form pages into one, instead of one page per top-level spec property |
--fixed-namespace=<value> | cortex | Hard-codes the namespace on the generated create workflow and hides the namespace input. Must be a valid DNS-1123 label |
portals:
- name: prod
type: cortex
# ... url, secretRef, config as below
adapterConfig:
generate:
args:
- "--fixed-namespace=team-a"
Portal Patch offers per-Promise equivalents of both, through the
singleSpecPage and hideNamespaceManifest recipes. Use the flags when you want the behaviour
everywhere on a portal, and a recipe when you want it for particular Promises.
Do not put credentials in generate.args. Unlike a secretRef credential, which is injected into
the writer container as a secret key reference, these flags are passed as container arguments and
are visible to anyone with read access to the Job in kratix-platform-system.
Making the controller's Jobs follow your cluster's rules
The Portal Controller creates Kubernetes Jobs to do its work. It creates one when a Promise or Resource syncs to a portal, and one when a portal connection is first set up.
Those Jobs run in your cluster, so they need to comply with your cluster's policies. For example, if an admission policy requires specific labels on every workload, Jobs that don't include those labels will be rejected, preventing the portal integration from running. You may also want them on particular nodes, inside a resource quota, or cleaned up on your usual schedule.
You configure these settings in the ske-portal-workloads-config ConfigMap in the kratix-platform-system namespace, under the workloads.yaml key:
apiVersion: v1
kind: ConfigMap
metadata:
name: ske-portal-workloads-config
namespace: kratix-platform-system
data:
workloads.yaml: |
jobTemplate:
metadata:
labels:
app.kubernetes.io/name: ske-portal
acs.customer.com/securityZone: dmz
annotations:
acs.customer.com/owner: platform-team
spec:
backoffLimit: 5
ttlSecondsAfterFinished: 3600
template:
spec:
nodeSelector:
kubernetes.io/os: linux
jobTemplate is a partial Kubernetes
JobTemplateSpec.
The controller merges it into each Job it builds: Promise sync Jobs, Resource sync Jobs, and portal
setup Jobs.
This ConfigMap is the only place these settings live. There is no matching field on the
SKEIntegration. You create it, edit it and delete it yourself, and it applies to every portal
connection.
Kubernetes Jobs cannot be changed after they are created, so the controller replaces them. When you
edit workloads.yaml, the Jobs it affects are recreated with the new configuration. Delete the ConfigMap
and they go back to the controller's defaults.
Labels and annotations reach the pods
Anything under jobTemplate.metadata lands on the Job and on the pods it creates, so you declare it
once. Admission policies usually check pods, so this is normally what you want.
If you want something on the pods only, put it under jobTemplate.spec.template.metadata. When the
same key appears in both places, the pod-level value wins.
In YAML, 4711 is a number and yes is a boolean, but Kubernetes labels, annotations and
nodeSelector values all have to be text. Put quote marks around anything that is not obviously a
word:
labels:
acme.com/cost-centre: "4711" # not 4711
audited: "yes" # not yes
This is the most common mistake. An unquoted value is rejected, and When the file is wrong shows where the error appears.
What you cannot set
Everything in the template is yours except the following. The controller sets these back to its own values after merging, because it reads them to find and clean up its own Jobs:
| Field | Why the controller keeps it |
|---|---|
metadata.name, metadata.namespace | It finds and cleans up its Jobs by name |
metadata.ownerReferences | This is how a Promise notices its Job has finished |
metadata.finalizers | A finalizer would stop the controller deleting its own Job |
| Which containers exist | You can change a container it builds, but not add one. The default containers are reader, generate, work-writer, status-writer |
Labels and annotations starting kratix.io/, and app.kubernetes.io/managed-by | These are how the controller finds its Jobs and protects them from being cleaned up early |
Labels such as app.kubernetes.io/name, app.kubernetes.io/component and
app.kubernetes.io/version are yours to set.
You cannot add a container, but you can change one the controller builds, by naming it. This is how you set CPU, memory or ephemeral storage limits:
jobTemplate:
spec:
template:
spec:
containers:
- name: generate
resources:
limits:
memory: 512Mi
ephemeral-storage: 1Gi
When the file is wrong
If workloads.yaml cannot be read, the controller creates no new Jobs until you fix it, and records
a warning on the ConfigMap itself. Existing Jobs are left alone.
kubectl -n kratix-platform-system describe configmap ske-portal-workloads-config
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning PortalWorkloadConfigInvalid 110s portal-setup-controller could not parse the portal
workloads configuration: jobTemplate: json: cannot unmarshal number into Go struct field
ObjectMeta.metadata.labels of type string
That message is the unquoted number from the tip above.
The controller rejects anything it cannot make sense of: unknown fields, misspelled fields, values of
the wrong type, and an empty (null) metadata or spec. It does not ignore them. That way a typo
cannot quietly drop the labels your policy needs.
Two things keep working while the file is broken. Cleaning up after a portal connection you have removed from your configuration still runs. So does deleting a Resource, because its Job has to run for the Resource to finish being removed.
The warning appears on the ConfigMap, not on the Promises whose Jobs were not created. If a Promise has stopped syncing to its portal and you cannot see why, check this ConfigMap's events.
Installing without the SKE Operator
The Portal Controller gets the portal configuration from a ConfigMap. If you use the SKE Operator,
the ConfigMap is automatically generated by it. If you want to deploy the Portal Controller
directly, please create a ske-portal-config ConfigMap in the namespace where the controller is running:
apiVersion: v1
kind: ConfigMap
metadata:
name: ske-portal-config
namespace: kratix-platform-system
data:
connections.yaml: | # required key name
- name: prod
type: backstage
- name: cortex-eu
type: cortex
url: https://api.getcortexapp.com
secretRef:
name: cortex-token
Each entry takes the fields described in Connect a portal, and credential Secrets work the same as a SKE operator managed Portal Controller.
Editing a connection reruns setup for that connection only. When the controller cannot find the portal configuration ConfigMap or when the configurations cannot be parsed successfully by the controller, a warning event is published.
If the SKE Operator manages your platform, you are not expected to create or edit this ConfigMap.
Configure portals through SKEIntegration.spec.portals instead.
Air-gapped environments
Mirror the following images into your own registry:
| Image | Needed for |
|---|---|
ghcr.io/syntasso/ske-portal-controller:<version> | The controller and its bundled adapter, one image |
ghcr.io/syntasso/portal-patch:<version> | Only if you use Portal Patch recipes |
Any adapterConfig.image | Only if you override the adapter image |
Any PortalCustomization container image | Your own configure containers |
The Kratix pipeline adapter image is also required, but it is part of the SKE platform install rather than this integration.
Point the operator at your registry with its image registry configuration, the same way as other
SKE components. Pull secrets come from adapterConfig.imagePullSecrets, which is always merged
with syntasso-registry.
