Skip to main content

Installation

Requirements

ComponentMinimum versionNeeded for
SKE Operatorv0.32.1Installing the portal-controller integration type
Portal Controllerv0.2.0This integration
Portal Patch0.7.0Declarative customization, optional
@syntasso/plugin-ske-backendv0.21.0Backstage portals
@syntasso/plugin-ske-frontendv0.18.0Backstage 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:

FieldRequiredMeaning
nameyesIdentifies the portal. Must be unique within type, so you can have two Backstage portals, but not two both named prod.
typeyesThe portal type this entry targets, for example backstage or cortex.
urldepends on typeThe portal's base URL. Required for Cortex, unused by Backstage.
secretRefdepends on typeNames the portal's credential Secret. Required for Cortex, unused by Backstage.
configdepends on typeSettings for this portal only. The keys differ per type.
adapterConfignoAdapter 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"
note

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:

ReasonMeaning
BindingResolvedThe label resolved to exactly one portal
AmbiguousPortalInstanceMore than one portal of the type exists and no instance was named
PortalConnectionNotFoundThe named instance does not match a configured portal
PortalIntegrationNotFoundNo portal-controller SKEIntegration is installed
CustomizationNotFoundThe named PortalCustomization does not exist
AmbiguousCustomizationMore than one PortalCustomization label is present

Once bound, each sync reports on a per-type condition, BackstagePortalSynced or CortexPortalSynced:

ReasonMeaning
EntitySyncedThe entity shipped successfully
SyncInProgressThe sync Job is running
GenerateOrWriteFailedThe Job failed before shipping the entity
PromiseHasNoAPIThe 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

FieldApplies toMeaning
imageall portalsAdapter image override
imagePullSecretsall portalsExtra pull secret names, always merged with syntasso-registry
generate.argsall portalsFlags passed verbatim to the generate stage
exporter.deploymentSpecCortex portalsOverlay 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:

FlagPortal typeEffect
--single-spec-pagebackstageMerges the generated Template's spec form pages into one, instead of one page per top-level spec property
--fixed-namespace=<value>cortexHard-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.

warning

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.

note

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.

Quote your values

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:

FieldWhy the controller keeps it
metadata.name, metadata.namespaceIt finds and cleans up its Jobs by name
metadata.ownerReferencesThis is how a Promise notices its Job has finished
metadata.finalizersA finalizer would stop the controller deleting its own Job
Which containers existYou 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-byThese 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.

Setting resources on a container

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.

note

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.

warning

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:

ImageNeeded 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.imageOnly if you override the adapter image
Any PortalCustomization container imageYour 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.