name: Constellation create
description: Create a new Constellation cluster using latest OS image.

inputs:
  workerNodesCount:
    description: "Number of worker nodes to spawn."
    required: true
  controlNodesCount:
    description: "Number of control-plane nodes to spawn."
    required: true
  cloudProvider:
    description: "Either 'gcp' or 'azure'."
    required: true
  machineType:
    description: "Machine type of VM to spawn."
    required: false
  osImage:
    description: "OS image to use."
    required: true
  isDebugImage:
    description: "Is OS img a debug img?"
    required: true
  kubernetesVersion:
    description: "Kubernetes version to create the cluster from."
    required: false
  artifactNameSuffix:
    description: "Suffix for artifact naming."
    required: true
  keepMeasurements:
    default: "false"
    description: "Keep measurements embedded in the CLI."
  existingConfig:
    default: "false"
    description: "Use existing config file."
  #
  # GCP specific inputs
  #
  gcpProject:
    description: "The GCP project to deploy Constellation in."
    required: false
  gcpInClusterServiceAccountKey:
    description: "The GCP Service account to use inside the created Constellation cluster."
    required: false
  #
  # Azure specific inputs
  #
  azureSubscription:
    description: "The Azure subscription ID to deploy Constellation in."
    required: false
  azureTenant:
    description: "The Azure tenant ID to deploy Constellation in."
    required: false
  azureClientID:
    description: "The Azure client ID of the application registration created for Constellation."
    required: false
  azureClientSecret:
    description: "The Azure client secret value of the used secret."
    required: false
  azureUserAssignedIdentity:
    description: "The Azure user assigned identity to use for Constellation."
    required: false
  azureResourceGroup:
    description: "The Azure resource group to use for Constellation cluster"
    required: false

outputs:
  kubeconfig:
    description: "The kubeconfig for the cluster."
    value: ${{ steps.constellation-init.outputs.KUBECONFIG }}
  masterSecret:
    description: "The master-secret for the cluster."
    value: ${{ steps.constellation-init.outputs.MASTERSECRET }}
  osImageUsed:
    description: "The OS image used in the cluster."
    value: ${{ steps.setImage.outputs.image }}

runs:
  using: "composite"
  steps:
    - name: Constellation config generate
      shell: bash
      if: inputs.existingConfig != 'true'
      run: |
        if [[ -n "${{ inputs.kubernetesVersion }}" ]]; then
          constellation config generate ${{ inputs.cloudProvider }} --kubernetes="${{ inputs.kubernetesVersion }}" --debug
        else
          constellation config generate ${{ inputs.cloudProvider }} --debug
        fi

        yq eval -i "(.name) = \"e2e-test\"" constellation-conf.yaml

        yq eval -i \
          "(.provider | select(. | has(\"azure\")).azure.subscription) = \"${{ inputs.azureSubscription }}\" |
            (.provider | select(. | has(\"azure\")).azure.tenant) = \"${{ inputs.azureTenant }}\" |
            (.provider | select(. | has(\"azure\")).azure.location) = \"West US\" |
            (.provider | select(. | has(\"azure\")).azure.userAssignedIdentity) = \"${{ inputs.azureUserAssignedIdentity }}\" |
            (.provider | select(. | has(\"azure\")).azure.resourceGroup) = \"${{ inputs.azureResourceGroup }}\" |
          constellation-conf.yaml

        yq eval -i \
          "(.provider | select(. | has(\"gcp\")).gcp.project) = \"${{ inputs.gcpProject }}\" |
            (.provider | select(. | has(\"gcp\")).gcp.region) = \"europe-west3\" |
            (.provider | select(. | has(\"gcp\")).gcp.zone) = \"europe-west3-b\" |
            (.provider | select(. | has(\"gcp\")).gcp.serviceAccountKeyPath) = \"serviceAccountKey.json\"" \
          constellation-conf.yaml

        yq eval -i \
          "(.provider | select(. | has(\"aws\")).aws.region) = \"eu-west-1\" |
            (.provider | select(. | has(\"aws\")).aws.zone) = \"eu-west-1c\" |
            (.provider | select(. | has(\"aws\")).aws.iamProfileControlPlane) = \"e2e_test_control_plane_instance_profile\" |
            (.provider | select(. | has(\"aws\")).aws.iamProfileWorkerNodes) = \"e2e_test_worker_node_instance_profile\"" \
          constellation-conf.yaml

        if [[ -n "${{ inputs.kubernetesVersion }}" ]]; then
          yq eval -i "(.kubernetesVersion) = \"${{ inputs.kubernetesVersion }}\"" constellation-conf.yaml
        fi

    - name: Remove embedded measurements
      if: inputs.keepMeasurements == 'false'
      shell: bash
      run: |
        if [[ $(yq '.version' constellation-conf.yaml) == "v2" ]]
        then
          yq eval -i \
            "(.provider | select(. | has(\"aws\")).aws.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}" \
            constellation-conf.yaml

          yq eval -i \
            "(.provider | select(. | has(\"azure\")).azure.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}" \
            constellation-conf.yaml

          yq eval -i \
            "(.provider | select(. | has(\"gcp\")).gcp.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}"\
            constellation-conf.yaml

          yq eval -i \
            "(.provider | select(. | has(\"qemu\")).qemu.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}" \
            constellation-conf.yaml
        else
          yq eval -i \
            "(.attestation | select(. | has(\"awsNitroTPM\")).awsNitroTPM.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}" \
            constellation-conf.yaml

          yq eval -i \
            "(.attestation | select(. | has(\"awsSEVSNP\")).awsSEVSNP.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}" \
            constellation-conf.yaml

          yq eval -i \
            "(.attestation | select(. | has(\"azureSEVSNP\")).azureSEVSNP.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}" \
            constellation-conf.yaml

          yq eval -i \
            "(.attestation | select(. | has(\"azureTrustedLaunch\")).azureTrustedLaunch.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}" \
            constellation-conf.yaml

          yq eval -i \
            "(.attestation | select(. | has(\"gcpSEVES\")).gcpSEVES.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}"\
            constellation-conf.yaml

          yq eval -i \
            "(.attestation | select(. | has(\"qemuVTPM\")).qemuVTPM.measurements) = {15:{\"expected\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"warnOnly\":false}}" \
            constellation-conf.yaml
        fi

    - name: Set image
      id: setImage
      shell: bash
      env:
        imageInput: ${{ inputs.osImage }}
      run: |
        if [[ -z "${imageInput}" ]]; then
          echo "No image specified. Using default image from config."
          image=$(yq eval ".image" constellation-conf.yaml)
          echo "image=${image}" | tee -a "$GITHUB_OUTPUT"
          exit 0
        fi

        yq eval -i "(.image) = \"${imageInput}\"" constellation-conf.yaml
        echo "image=${imageInput}" | tee -a "$GITHUB_OUTPUT"

    - name: Set instanceType
      if: inputs.machineType && inputs.machineType != 'default'
      shell: bash
      run: |
        yq eval -i "(.provider | select(. | has(\"azure\")).azure.instanceType) = \"${{ inputs.machineType }}\"" constellation-conf.yaml
        yq eval -i "(.provider | select(. | has(\"gcp\")).gcp.instanceType) = \"${{ inputs.machineType }}\"" constellation-conf.yaml
        yq eval -i "(.provider | select(. | has(\"aws\")).aws.instanceType) = \"${{ inputs.machineType }}\"" constellation-conf.yaml

    - name: Create serviceAccountKey.json
      if: inputs.cloudProvider == 'gcp' && !inputs.existingConfig # Skip if using existing config. serviceAccountKey.json is already present in that case.
      shell: bash
      env:
        GCP_CLUSTER_SERVICE_ACCOUNT_KEY: ${{ inputs.gcpInClusterServiceAccountKey }}
      run: |
        echo "$GCP_CLUSTER_SERVICE_ACCOUNT_KEY" > serviceAccountKey.json

    - name: Enable debugCluster flag
      if: inputs.isDebugImage == 'true'
      shell: bash
      run: |
        yq eval -i '(.debugCluster) = true' constellation-conf.yaml

    # Uses --force flag since the CLI currently does not have a pre-release version and is always on the latest released version.
    # However, many of our pipelines work on prerelease images. Thus the used images are newer than the CLI's version.
    # This makes the version validation in the CLI fail.
    - name: Constellation create
      shell: bash
      run: |
        echo "Creating cluster using config:"
        cat constellation-conf.yaml
        sudo sh -c 'echo "127.0.0.1 license.confidential.cloud" >> /etc/hosts' || true

        output=$(constellation --help)
        if [[ $output == *"tf-log"* ]]; then
          TFFLAG="--tf-log=DEBUG"
        fi
        constellation create -c ${{ inputs.controlNodesCount }} -w ${{ inputs.workerNodesCount }} -y --force --debug ${TFFLAG:-}

    - name: Cdbg deploy
      if: inputs.isDebugImage == 'true'
      shell: bash
      run: |
        echo "::group::cdbg deploy"
        chmod +x $GITHUB_WORKSPACE/build/cdbg
        cdbg deploy \
          --bootstrapper "${{ github.workspace }}/build/bootstrapper" \
          --upgrade-agent "${{ github.workspace }}/build/upgrade-agent" \
          --info logcollect=true \
          --info logcollect.github.actor="${{ github.triggering_actor }}" \
          --info logcollect.github.workflow="${{ github.workflow }}" \
          --info logcollect.github.run-id="${{ github.run_id }}" \
          --info logcollect.github.run-attempt="${{ github.run_attempt }}" \
          --info logcollect.github.ref-name="${{ github.ref_name }}" \
          --info logcollect.github.sha="${{ github.sha }}" \
          --info logcollect.github.runner-os="${{ runner.os }}" \
          --force
        echo "::endgroup::"

    - name: Constellation init
      id: constellation-init
      shell: bash
      run: |
        constellation init --force --debug
        echo "KUBECONFIG=$(pwd)/constellation-admin.conf" >> $GITHUB_OUTPUT
        echo "MASTERSECRET=$(pwd)/constellation-mastersecret.json" >> $GITHUB_OUTPUT

    - name: Wait for nodes to join and become ready
      shell: bash
      env:
        KUBECONFIG: "${{ steps.constellation-init.outputs.KUBECONFIG }}"
        JOINTIMEOUT: "1200" # 20 minutes timeout for all nodes to join
      run: |
        echo "::group::Wait for nodes"
        NODES_COUNT=$((${{ inputs.controlNodesCount }} + ${{ inputs.workerNodesCount }}))
        JOINWAIT=0
        until [[ "$(kubectl get nodes -o json | jq '.items | length')" == "${NODES_COUNT}" ]] || [[ $JOINWAIT -gt $JOINTIMEOUT ]];
        do
            echo "$(kubectl get nodes -o json | jq '.items | length')/"${NODES_COUNT}" nodes have joined.. waiting.."
            JOINWAIT=$((JOINWAIT+30))
            sleep 30
        done
        if [[ $JOINWAIT -gt $JOINTIMEOUT ]]; then
            echo "Timed out waiting for nodes to join"
            exit 1
        fi
        echo "$(kubectl get nodes -o json | jq '.items | length')/"${NODES_COUNT}" nodes have joined"
        if ! kubectl wait --for=condition=ready --all nodes --timeout=20m; then
          kubectl get pods -n kube-system
          kubectl get events -n kube-system
          echo "::error::kubectl wait timed out before all nodes became ready"
          echo "::endgroup::"
          exit 1
        fi
        echo "::endgroup::"

    - name: Download boot logs
      if: always()
      continue-on-error: true
      shell: bash
      env:
        CSP: ${{ inputs.cloudProvider }}
      run: |
        echo "::group::Download boot logs"
        case $CSP in
          azure)
            AZURE_RESOURCE_GROUP=$(yq eval ".provider.azure.resourceGroup" constellation-conf.yaml)
            ./.github/actions/constellation_create/az-logs.sh ${AZURE_RESOURCE_GROUP}
            ;;
          gcp)
            ./.github/actions/constellation_create/gcp-logs.sh
            ;;
          aws)
            ./.github/actions/constellation_create/aws-logs.sh eu-central-1
            ;;
        esac
        echo "::endgroup::"

    - name: Upload boot logs
      if: always() && !env.ACT
      continue-on-error: true
      uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
      with:
        name: serial-logs-${{ inputs.artifactNameSuffix }}
        path: "*.log"