Port Controller (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:
- Install the SKE Port Controller
- Setup GitHub Actions backend for Port Actions
- Configure Kratix GitOps for requests from Port Actions
- 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:
- A Syntasso registry token. To request one, visit Syntasso and click "Try SKE Today".
- A Kubernetes cluster with Kratix running. The Kratix Quick Start is an easy way to get started.
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_IDPORT_CLIENT_SECRETPORT_GITHUB_ORGGITHUB_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
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:
- In your Port account, go to Settings > Data Sources and install the GitHub Ocean integration. Follow the Port documentation for setup instructions.
- 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
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 type | Validations shown and enforced in Port |
|---|---|
string | pattern, minLength, maxLength, enum |
integer / number | minimum, maximum, exclusiveMinimum, exclusiveMaximum, enum |
array | minItems, maxItems, plus any of the above on items |
object | Nested 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.5is 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-basedoneOfis supported (see Conditional fields withoneOf); otheroneOfshapes are not translated to the form.- Numeric enums are not shown on catalogue entities. A numeric
enum(for exampleenum: [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.
