Skip to main content

Port Controller (Preview)

Preview

This integration is in Preview mode and is not covered by the Syntasso SLA. If you are evaluating or using it, contact Syntasso to discuss applicability and expectations.

Kratix supports configuring and maintaining both day 1 and day 2 integrations with Port.



This page provides information on how to:

  1. Install the SKE Port Controller
  2. Setup GitHub Actions backend for Port Actions
  3. Configure Kratix GitOps for requests from Port Actions
  4. Expose a Promise in Port

This documentation assumes both a Port account as well as a GitHub Repository with enough permissions to manage GitHub Actions. If required, please refer to the Port and GitHub sign-up pages to create accounts.

Install the SKE Port Controller

Port provides a generic controller for pushing content from Kubernetes to the Port UI. By installing this via the SKE Controller you will get configuration and management of Kratix Promises and Resources automatically managed for you, including managing the backend of your choosing for the self-service actions in Port.

Prerequisites

To install the SKE Port Controller, you will need:

Configure the integration

To configure the controller, you will need to create a configmap and a secret in the ske-port-controller-system namespace.

Start by creating this namespace if you have not previously installed the controller.

Then create local environment variables for the four required values:

  • PORT_CLIENT_ID
  • PORT_CLIENT_SECRET
  • PORT_GITHUB_ORG
  • GITHUB_INSTALLATION_ID — the installation ID of your GitHub Ocean integration in Port (see below for how to find this)

And review the remaining default configurations. If you require overriding any, set those local environment variables as well.

  • PORT_GITHUB_REPO (default: port-syncer)
  • PORT_GITHUB_WORKFLOW_FILE (default: port-action.yml)
  • PORT_CM_NAME (default: ske-port-k8s-exporter)
  • PORT_CM_NAMESPACE (default: ske-port-controller-system)
  • PORT_SKIP_DELETE_BLUEPRINT (default: false)

Once environment variables have been set, you can create the secret using the following command:

kubectl create secret generic -n ske-port-controller-system ske-port-exporter-config \
--from-literal="PORT_CLIENT_ID=${PORT_CLIENT_ID}" \
--from-literal="PORT_CLIENT_SECRET=${PORT_CLIENT_SECRET}" \
--from-literal="PORT_GITHUB_ORG=${PORT_GITHUB_ORG}" \
--from-literal="GITHUB_INSTALLATION_ID=${GITHUB_INSTALLATION_ID}" \
--from-literal="PORT_GITHUB_REPO=${PORT_GITHUB_REPO:-}" \
--from-literal="PORT_GITHUB_WORKFLOW_FILE=${PORT_GITHUB_WORKFLOW_FILE:-}" \
--from-literal="PORT_CM_NAME=${PORT_CM_NAME:-}" \
--from-literal="PORT_CM_NAMESPACE=${PORT_CM_NAMESPACE:-}" \
--from-literal="PORT_SKIP_DELETE_BLUEPRINT=${PORT_SKIP_DELETE_BLUEPRINT:-}"

Access the enterprise controller image

Next you can either pre-load the required image into your cluster, or define your authentication credentials via a docker-registry secret and refer to this secret in Pipeline definition in your Workflow.

The release is set to use imagePullSecret, to access the enterprise image. To create the secret, replacing YOUR_TOKEN with the Syntasso registry token you have been provided with:

kubectl create secret docker-registry syntasso-registry \
--namespace ske-port-controller-system \
--docker-server=ghcr.io \
--docker-username=syntasso-pkg \
--docker-password=YOUR_TOKEN
tip

If you prefer, you can load the image onto the Kubernetes nodes for local access.

To pull the image, select the image at the correct version from the releases page, then pull and load that image.

docker pull ghcr.io/syntasso/ske-port-controller:<VERSION>

Install the controller

Finally, to install the controller, run the command below, replacing VERSION with the target version from the releases page:

kubectl apply -f http://s3.eu-west-2.amazonaws.com/syntasso-enterprise-releases/ske-port-controller/<VERSION>/install.yaml

Setup GitHub Ocean integration

The SKE Port Controller uses Port's GitHub Ocean integration to trigger GitHub Actions workflows as the backend for self-service actions.

To set up the integration:

  1. In your Port account, go to Settings > Data Sources and install the GitHub Ocean integration. Follow the Port documentation for setup instructions.
  2. Once installed, note the installation ID of your GitHub Ocean integration. It's the name of the integration when you deployed it in port. This is the value you set as GITHUB_INSTALLATION_ID.

Setup GitHub Actions backend for Port Actions

First, your repo needs its Actions to be configured to allow commits. Follow GitHub instructions on how to manage this setting here.

Next, your Actions need both the PORT_CLIENT_ID and PORT_CLIENT_SECRET secrets set. Follow GitHub instructions on how to set these up here.

Finally, the repository must have a GitHub Action workflow file that can generate resource requests that match the Promise CRD.

Below is an example action stored in a workflow file called port-action.yml (the file name is configurable using the PORT_GITHUB_WORKFLOW environment variable). This example will support create, update, and delete flows as well as sharing log data back to Port. It stores each request as <folder>/<namespace>/<kind>/<name>.yaml:

Example port-action.yml contents
name: Sync control plane gitops
permissions:
contents: write
pull-requests: write
on:
workflow_dispatch:
inputs:
operation:
required: true
description: "Delete, Update or create"
type: string
triggeringUser:
required: true
description: "The email of the triggering user"
type: string
runId:
required: true
description: "Port's Run ID"
type: string
manifest:
required: true
description: "The K8s manifest generated by Port"
type: string
folder:
required: true
description: Folder where the resource will be stored
default: "./kratix/platform/resources"
type: string

jobs:
commit-to-main:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Update manifests folder
run: |
echo '${{ inputs.manifest }}' | jq . > temp.json
yaml_data=$(yq -p json -o yaml temp.json)
name=$(echo '${{ inputs.manifest }}' | jq -r .metadata.name)
namespace=$(echo '${{ inputs.manifest }}' | jq -r '.metadata.namespace // "default"')
kind=$(echo '${{ inputs.manifest }}' | jq -r '.kind | ascii_downcase')
mkdir -p "${{ inputs.folder }}/$namespace/$kind"
if [ "${{ inputs.operation }}" = "DELETE" ]; then
rm -f "${{ inputs.folder }}/$namespace/$kind/$name.yaml"
else
echo "$yaml_data" > "${{ inputs.folder }}/$namespace/$kind/$name.yaml"
fi
rm -f temp.json
- uses: port-labs/port-github-action@v1
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
operation: PATCH_RUN
runId: ${{inputs.runId}}
icon: GithubActions
logMessage: "Creating a commit with the changes of the new resource in folder ${{ inputs.folder }} 🚀"
- name: create commit to main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "GitHub Action"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
if ! git diff-index --quiet HEAD; then
git commit -m "chore: ${{ inputs.operation }} resource by ${{ inputs.triggeringUser }}"
git push origin main
fi
- uses: port-labs/port-github-action@v1
if: success()
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
operation: PATCH_RUN
status: "SUCCESS"
runId: ${{inputs.runId}}
icon: GithubActions
logMessage: |
Successfully committed request to main ✅
- uses: port-labs/port-github-action@v1
if: failure()
with:
clientId: ${{ secrets.PORT_CLIENT_ID }}
clientSecret: ${{ secrets.PORT_CLIENT_SECRET }}
operation: PATCH_RUN
status: "FAILURE"
runId: ${{inputs.runId}}
icon: GithubActions
logMessage: |
Failed to commit request to main ❌
Resource may have been created outside of Port 🕵
Check the job run logs in Github to learn more 📜:
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

Configure GitOps for requests from Port Actions

So far you have configured Port to use GitHub Actions as a backend for the Port self-serve actions and automations. You have then configured the GitHub Action to commit user requests to the defined repository.

Now you will need to configure a GitOps agent to pull the repository documents into the cluster where Kratix runs. This will enable Kratix to respond to resources that are created via Port.

Please refer to the guide documentation to use ArgoCD, Flux, or any other agent you prefer.

Part of the configuration is making sure that the GitOps agent is reading from the repository that Port is writing to. An example set up when using FluxCD and the defaults from above can be found below.

Example Flux Resources for port-syncer repository
info

This example resource depends on a secret called git-credentials to exist in the flux-system namespace. This secret should be configured based on the FluxCD docs.

---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: kratix-port-resources
namespace: flux-system
spec:
interval: 5s
url: https://github.com/${PORT_GITHUB_ORG}/${PORT_GITHUB_REPO}
ref:
branch: main
secretRef:
name: git-credentials
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: kratix-port-resources
namespace: flux-system
spec:
interval: 3s
sourceRef:
kind: GitRepository
name: kratix-port-resources
path: "kratix/platform/resources/"
prune: true

Expose a Promise in Port

While many platform APIs benefit from being available, you may not want all platform APIs in the Port UI. For this reason, Kratix uses an opt-in methodology.

To expose a Promise via the Port UI, make sure to add a label kratix.io/get-port: "true" in the Kratix Promise API. For example, to expose the marketplace namespace promise, it would look like this:

apiVersion: platform.kratix.io/v1alpha1
kind: Promise
metadata:
name: namespace
namespace: default
spec:
api:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: namespaces.marketplace.kratix.io
labels:
kratix.io/get-port: "true"
spec:
group: marketplace.kratix.io
names:
kind: namespace
plural: namespaces
singular: namespace
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
spec:
properties:
clusterName:
description: |
The name of the cluster where the namespace should be created
type: string
namespaceName:
description: |
The name of the namespace to be created
type: string
required:
- namespaceName
type: object
type: object
served: true
storage: true
workflows:
resource:
configure:
- apiVersion: platform.kratix.io/v1alpha1
kind: Pipeline
metadata:
name: instance-configure
namespace: default
spec:
containers:
- image: ghcr.io/syntasso/kratix-marketplace/namespace-configure-pipeline:v0.1.0
name: namespace-configure-pipeline

Promise API validations in Port

When the controller generates self-service actions for a Promise, it carries the validation rules from the Promise API into the Port action form. Users get immediate feedback on invalid input, before a request reaches the platform. Default values defined in the Promise API are pre-filled in the form.

The following validations are supported:

Promise API field typeValidations shown and enforced in Port
stringpattern, minLength, maxLength, enum
integer / numberminimum, maximum, exclusiveMinimum, exclusiveMaximum, enum
arrayminItems, maxItems, plus any of the above on items
objectNested fields become individual form inputs (named parent__child), each validated with the rules above. Nested required fields are required in the form.

Limitations

Port cannot express every validation a Promise API can. Any rule that Port cannot enforce is still enforced by Kubernetes when the request is applied to the Kubernetes cluster.

  • Integers are treated as numbers. Port has no integer type, so integer fields become number inputs. For integers, range rules still apply, but a decimal value such as 2.5 is accepted by the form and rejected later by Kubernetes.
  • multipleOf, allOf/anyOf, and CEL rules (x-kubernetes-validations) are not supported by Port and are enforced only when the request is applied. A selector-based oneOf is supported (see Conditional fields with oneOf); other oneOf shapes are not translated to the form.
  • Numeric enums are not shown on catalogue entities. A numeric enum (for example enum: [1, 2, 3]) works in the action form, but the entity view in the Port catalogue displays the field as a plain number.

Conditional fields with oneOf

A Promise API can use oneOf to offer a choice between mutually exclusive sets of fields. The controller translates this into a conditional form when a top-level string field (the selector) chooses between object blocks: each branch pins the selector to one enum value and requires the matching block.

spec:
properties:
engine:
type: string
enum: [postgres, mysql]
postgres:
type: object
# ...postgres fields...
mysql:
type: object
# ...mysql fields...
oneOf:
- properties:
engine:
enum: [postgres]
required: [postgres]
- properties:
engine:
enum: [mysql]
required: [mysql]

Choosing a selector value shows and requires only that branch's fields; the other is hidden and dropped from the request, so it satisfies the API's oneOf when applied.

Only this selector-based shape is supported. A oneOf without a selector field is not translated: all fields are shown and the constraint is enforced only on apply.

Control the order of form fields

By default, Port decides the order of the inputs in an action form. To set an explicit order, add the kratix.io/port-input-order annotation to the Promise with a comma-separated list of input identifiers:

metadata:
name: deployment
annotations:
kratix.io/port-input-order: "target,repositoryUrl,gitSecretRef"

The list is applied as given; any input left out keeps Port's default position. Use the flattened parent__child name for nested fields. If you are unsure of the exact identifiers, install the Promise and inspect the generated action.