Create a Kubernetes namespace
Overviewโ
In the following guide, you are going to create a self-service action in Port that executes a GitLab pipeline to create a Kubernetes namespace.
Prerequisitesโ
- Ensure you have a working Kubernetes cluster and a GitLab project.
Don't have a Kubernetes cluster?
Quickly create a local one using kind.
- In the same GitLab project, register and install the GitLab agent.
- In your GitLab project, go to the Settingsmenu at the sidebar on the left, selectCI/CDand create the followingVariables:- PORT_CLIENT_ID- Port Client ID learn more
- PORT_CLIENT_SECRET- Port Client Secret learn more
- KUBE_CONTEXT- This is obtained after registering and installing the GitLab agent.- Find Your Project URL: It will look like this:  https://gitlab.com/your-group/your-project
- Compose Your Context: Use this format: your-group/your-project:agent-name
- Set KUBE_CONTEXT
 
- Find Your Project URL: It will look like this:  
 
GitLab Pipelineโ
Create a .gitlab-ci.yaml file in the root of your gitlab project with the following content:
GitLab workflow
gitlab-ci.yaml
stages:
  - prerequisites
  - deploy
  - port-update
image: alpine:latest
variables:
  PORT_CLIENT_ID: ${PORT_CLIENT_ID}
  PORT_CLIENT_SECRET: ${PORT_CLIENT_SECRET}
  KUBE_CONTEXT: ${KUBE_CONTEXT}
before_script:
  - apk update
  - apk add --upgrade curl jq -q
  
fetch-port-access-token:
  stage: prerequisites
  except:
    - pushes
  script:
    - |
      echo "Getting access token from Port API"
      accessToken=$(curl -X POST \
        -H 'Content-Type: application/json' \
        -d '{"clientId": "'"$PORT_CLIENT_ID"'", "clientSecret": "'"$PORT_CLIENT_SECRET"'"}' \
        -s 'https://api.getport.io/v1/auth/access_token' | jq -r '.accessToken')
      echo "ACCESS_TOKEN=$accessToken" >> fetch-port-access-token-data.env
      runId=$(cat $TRIGGER_PAYLOAD | jq -r '.context.runId')
      curl -X POST \
        -H 'Content-Type: application/json' \
        -H "Authorization: Bearer $accessToken" \
        -d '{"message":"๐โโ๏ธ Starting action to create a k8s namespace"}' \
        "https://api.getport.io/v1/actions/runs/$runId/logs"
      curl -X PATCH \
        -H 'Content-Type: application/json' \
        -H "Authorization: Bearer $accessToken" \
        -d '{"link":"'"$CI_PIPELINE_URL"'"}' \
        "https://api.getport.io/v1/actions/runs/$runId"
  artifacts:
    reports:
      dotenv: fetch-port-access-token-data.env
create-manifest:
  stage: deploy
  needs:
    - job: fetch-port-access-token
      artifacts: true
  except:
    - pushes
  script:
    - echo "Creating k8s namespace manifest"
    - |
      NAMESPACE_NAME=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.name')
      MIN_CPU=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.min_cpu')
      MAX_CPU=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.max_cpu')
      MIN_MEMORY=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.min_memory')
      MAX_MEMORY=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.max_memory')
      MIN_STORAGE=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.min_storage')
      MAX_STORAGE=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.max_storage')
      cat <<EOF > manifest.yml
      apiVersion: v1
      kind: ResourceQuota
      metadata:
        name: $NAMESPACE_NAME-quota
        namespace: $NAMESPACE_NAME  # Consider if this should be dynamic too
      spec:
        hard:
          requests.cpu: $MIN_CPU
          requests.memory: ${MIN_MEMORY}Gi
          requests.storage: ${MIN_STORAGE}Gi
          limits.cpu: $MAX_CPU
          limits.memory: ${MAX_MEMORY}Gi
      EOF
      # log the manifest
      cat manifest.yml
      echo "NAMESPACE_NAME=$NAMESPACE_NAME" >> create-manifest-data.env
  artifacts:
    paths:
      - manifest.yml
    reports:
      dotenv: create-manifest-data.env
log-pre-create:
  stage: deploy
  needs:
    - job: fetch-port-access-token
      artifacts: true
  except:
    - pushes
  script:
    - |
      echo "Logging pre-create action"
      runId=$(cat $TRIGGER_PAYLOAD | jq -r '.context.runId')
      curl -X POST \
        -H 'Content-Type: application/json' \
        -H "Authorization: Bearer $ACCESS_TOKEN" \
        -d '{"statusLabel": "Creating Namespace", "message":"๐ง Creating the namespace in k8s!"}' \
        "https://api.getport.io/v1/actions/runs/$runId/logs"
create-k8s-namespace:
  stage: deploy
  before_script: [] # Empty before_script to override the global one
  needs:
    - job: log-pre-create
    - job: create-manifest
      artifacts: true
  except:
    - pushes
  image:
    name: bitnami/kubectl:latest
    entrypoint: [""]
  script:
    - |
      echo "Creating k8s namespace"
      cat manifest.yml
      kubectl config get-contexts
      kubectl config use-context $KUBE_CONTEXT
      kubectl create namespace $NAMESPACE_NAME
      kubectl apply -f manifest.yml
  
create-port-entity:
  stage: port-update
  needs:
    - job: fetch-port-access-token
      artifacts: true
    - job: create-k8s-namespace
      artifacts: true
    - job: create-manifest
      artifacts: true
  except:
    - pushes
  before_script:
    - apk update
    - apk add --upgrade curl jq -q
  script:
    - |
      echo "Creating Port entity to match new k8s namespace"
      NAMESPACE_NAME=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.name')
      MIN_CPU=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.min_cpu')
      MAX_CPU=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.max_cpu')
      MIN_MEMORY=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.min_memory')
      MAX_MEMORY=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.max_memory')
      MIN_STORAGE=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.min_storage')
      MAX_STORAGE=$(cat $TRIGGER_PAYLOAD | jq -r '.payload.properties.max_storage')
      
      BLUEPRINT=$(cat $TRIGGER_PAYLOAD | jq -r '.context.blueprint')
      runId=$(cat $TRIGGER_PAYLOAD | jq -r '.context.runId')
      curl -X POST \
          -H 'Content-Type: application/json' \
          -H "Authorization: Bearer $ACCESS_TOKEN" \
          -d '{"statusLabel": "Creating Entity", "message":"๐ง Creating the namespace entity in Port!"}' \
          "https://api.getport.io/v1/actions/runs/$runId/logs"
      log='{
         "identifier": "'"$NAMESPACE_NAME"'",
         "title": "'"$NAMESPACE_NAME"'",
         "blueprint": "'"$BLUEPRINT"'",
         "properties": {
            "min_cpu": "'"$MIN_CPU"'",
            "max_cpu": "'"$MAX_CPU"'",
            "min_memory": "'"$MIN_MEMORY"'",
            "max_memory": "'"$MAX_MEMORY"'",
            "min_storage": "'"$MIN_STORAGE"'"
         },
         "relations": {}
        }'
      echo "$log"
      curl --location --request POST "https://api.getport.io/v1/blueprints/$BLUEPRINT/entities?create_missing_related_entities=false&run_id=$runId" \
        -H "Authorization: Bearer $ACCESS_TOKEN" \
        -H "Content-Type: application/json" \
        -d "$log"
update-run-status:
  stage: port-update
  needs:
    - job: create-port-entity
      artifacts: true
    - job: fetch-port-access-token
      artifacts: true
  except:
    - pushes
  before_script:
    - apk update
    - apk add --upgrade curl jq -q
  script:
    - |
      echo "Updating Port action run status and final logs"
      runId=$(cat $TRIGGER_PAYLOAD | jq -r '.context.runId')
      curl -X POST \
        -H 'Content-Type: application/json' \
        -H "Authorization: Bearer $ACCESS_TOKEN" \
        -d '{"terminationStatus":"SUCCESS", "message":"โ
 Created new k8s namespace!"}' \
        "https://api.getport.io/v1/actions/runs/$runId/logs"
Port Configurationโ
- Head over to the Builder page to create the following blueprint:
- Click on the + Blueprintbutton.
- Click on the {...} Edit JSONbutton.
- Copy and paste the following JSON configuration into the editor.
- Click Save.
 
- Click on the 
Namespace Blueprint
{
  "identifier": "namespace",
  "description": "Kubernetes Namespace",
  "title": "Namespace",
  "icon": "AmazonEKS",
  "schema": {
    "properties": {
      "min_cpu": {
        "icon": "AmazonEKS",
        "type": "number",
        "title": "Min CPU",
        "description": "The minimum CPU resource guaranteed for containers within the namespace. ",
        "default": 1
      },
      "max_cpu": {
        "type": "number",
        "title": "Max CPU",
        "description": "Maximum CPU",
        "icon": "AmazonEKS",
        "default": 2
      },
      "min_memory": {
        "type": "number",
        "title": "Min Memory",
        "description": "The minimum memory resource guaranteed for containers within the namespace",
        "icon": "AmazonEKS",
        "default": 0.5
      },
      "min_storage": {
        "type": "number",
        "title": "Min Storage",
        "description": "The minimum storage resource guaranteed for persistent volumes within the namespace",
        "icon": "AmazonEKS",
        "default": 0.5
      },
      "max_memory": {
        "type": "number",
        "title": "Max Memory",
        "description": " The maximum memory that containers in the namespace are allowed to use",
        "icon": "AmazonEKS",
        "default": 2
      },
      "project_name": {
        "type": "string",
        "title": "Project Name",
        "description": "The AWS Account ID",
        "icon": "AWS"
      }
    },
    "required": [
      "min_cpu",
      "max_cpu",
      "min_memory",
      "min_storage",
      "max_memory"
    ]
  },
  "mirrorProperties": {},
  "calculationProperties": {},
  "aggregationProperties": {},
  "relations": {}
}
- To create the Port action, go to the self-service page:
- Click on the + New Actionbutton.
- Choose the Namespaceblueprint and clickNext.
- Click on the {...} Edit JSONbutton.
- Copy and paste the following JSON configuration into the editor.
 
- Click on the 
Port Action: Create a Namespace
tip
{
  "identifier": "namespace_create_namespace",
  "title": "Create Namespace",
  "icon": "AmazonEKS",
  "description": "Create a Kubernetes Namespace",
  "trigger": {
    "type": "self-service",
    "operation": "CREATE",
    "userInputs": {
      "properties": {
        "project_name": {
          "type": "string",
          "title": "Project Name"
        },
        "name": {
          "icon": "DefaultProperty",
          "type": "string",
          "title": "Name"
        },
        "min_cpu": {
          "type": "number",
          "title": "Min CPU",
          "default": 0.5
        },
        "max_cpu": {
          "icon": "DefaultProperty",
          "type": "number",
          "title": "Max CPU",
          "description": "The maximum number of CPU cores a container can use in the namespace",
          "default": 1.5
        },
        "min_memory": {
          "type": "number",
          "title": "Min Memory",
          "default": 0.5,
          "description": "The minimum memory resource guaranteed for containers within the namespace"
        },
        "max_memory": {
          "type": "number",
          "title": "Max Memory",
          "description": "The maximum memory that containers in the namespace are allowed to use",
          "default": 2
        },
        "min_storage": {
          "type": "number",
          "title": "Min Storage",
          "description": "The minimum storage resource guaranteed for persistent volumes within the namespace ",
          "default": 0.5
        }
      },
      "required": [
        "project_name",
        "min_cpu",
        "max_cpu",
        "min_memory",
        "max_memory",
        "min_storage",
        "name"
      ],
      "order": [
        "project_name",
        "name",
        "min_cpu",
        "max_cpu",
        "min_memory",
        "max_memory",
        "min_storage"
      ]
    },
    "blueprintIdentifier": "namespace"
  },
  "invocationMethod": {
    "type": "WEBHOOK",
    "url": "https://gitlab.com/api/v4/projects/<PROJECT_ID>/ref/main/trigger/pipeline?token=<PIPELINE_TRIGGER_TOKEN>",
    "agent": false,
    "synchronized": false,
    "method": "POST",
    "body": {
      "action": "{{ .action.identifier[(\"namespace_\" | length):] }}",
      "resourceType": "run",
      "status": "TRIGGERED",
      "trigger": "{{ .trigger | {by, origin, at} }}",
      "context": {
        "entity": "{{.entity.identifier}}",
        "blueprint": "{{.action.blueprint}}",
        "runId": "{{.run.id}}"
      },
      "payload": {
        "entity": "{{ (if .entity == {} then null else .entity end) }}",
        "action": {
          "invocationMethod": {
            "type": "WEBHOOK",
            "url": "https://gitlab.com/api/v4/projects/<PROJECT_ID>/ref/main/trigger/pipeline?token=<PIPELINE_TRIGGER_TOKEN>",
            "agent": false,
            "synchronized": false,
            "method": "POST"
          },
          "trigger": "{{.trigger.operation}}"
        },
        "properties": {
          "{{if (.inputs | has(\"project_name\")) then \"project_name\" else null end}}": "{{.inputs.\"project_name\"}}",
          "{{if (.inputs | has(\"name\")) then \"name\" else null end}}": "{{.inputs.\"name\"}}",
          "{{if (.inputs | has(\"min_cpu\")) then \"min_cpu\" else null end}}": "{{.inputs.\"min_cpu\"}}",
          "{{if (.inputs | has(\"max_cpu\")) then \"max_cpu\" else null end}}": "{{.inputs.\"max_cpu\"}}",
          "{{if (.inputs | has(\"min_memory\")) then \"min_memory\" else null end}}": "{{.inputs.\"min_memory\"}}",
          "{{if (.inputs | has(\"max_memory\")) then \"max_memory\" else null end}}": "{{.inputs.\"max_memory\"}}",
          "{{if (.inputs | has(\"min_storage\")) then \"min_storage\" else null end}}": "{{.inputs.\"min_storage\"}}"
        },
        "censoredProperties": "{{.action.encryptedProperties}}"
      }
    }
  },
  "requiredApproval": false
  
}
- Fill out the backendform section with your values:- For the Endpoint URL you need to add a URL in the following format:
- To create a pipeline trigger token, follow this guide.
 
 https://gitlab.com/api/v4/projects/{PROJECT_ID}/ref/main/trigger/pipeline?token={PIPELINE_TRIGGER_TOKEN}- Set HTTP method to POST.
- Set Request type to Async.
- Set Use self-hosted agent to No.
 
- For the Endpoint URL you need to add a URL in the following format:
 
Let's test it!โ
 
- Head to the Namespace catalog page
- Click on the + Namespacebutton
- Choose the Create Namespaceoption
- Fill in the form
- Click on Execute
- Wait for the namespace to be created.
Done ๐ You've created an namespace using an action in Port ๐ฅ
