mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-07 06:22:17 -04:00
ci: add e2e-upgrade test
The test is implemented as a go test. It can be executed as a bazel target. The general workflow is to setup a cluster, point the test to the workspace in which to find the kubeconfig and the constellation config and specify a target image, k8s and service version. The test will succeed if it detects all target versions in the cluster within the configured timeout. The CI automates the above steps. A separate workflow is introduced as there are multiple input fields to the test. Adding all of these to the manual e2e test seemed confusing. Co-authored-by: Fabian Kammel <fk@edgeless.systems>
This commit is contained in:
parent
18661ced48
commit
cac43a1dd0
25 changed files with 920 additions and 58 deletions
16
.github/actions/e2e_test/action.yml
vendored
16
.github/actions/e2e_test/action.yml
vendored
|
@ -23,6 +23,9 @@ inputs:
|
||||||
description: "Is OS img a debug img?"
|
description: "Is OS img a debug img?"
|
||||||
default: "true"
|
default: "true"
|
||||||
required: true
|
required: true
|
||||||
|
cliVersion:
|
||||||
|
description: "Version of a released CLI to download, e.g. 'v2.3.0', leave empty to build it."
|
||||||
|
required: false
|
||||||
kubernetesVersion:
|
kubernetesVersion:
|
||||||
description: "Kubernetes version to create the cluster from."
|
description: "Kubernetes version to create the cluster from."
|
||||||
required: false
|
required: false
|
||||||
|
@ -114,6 +117,7 @@ runs:
|
||||||
buildBuddyApiKey: ${{ inputs.buildBuddyApiKey }}
|
buildBuddyApiKey: ${{ inputs.buildBuddyApiKey }}
|
||||||
|
|
||||||
- name: Build CLI
|
- name: Build CLI
|
||||||
|
if: inputs.cliVersion == ''
|
||||||
uses: ./.github/actions/build_cli
|
uses: ./.github/actions/build_cli
|
||||||
with:
|
with:
|
||||||
targetOS: ${{ steps.determine-build-target.outputs.hostOS }}
|
targetOS: ${{ steps.determine-build-target.outputs.hostOS }}
|
||||||
|
@ -121,6 +125,18 @@ runs:
|
||||||
enterpriseCLI: ${{ inputs.keepMeasurements }}
|
enterpriseCLI: ${{ inputs.keepMeasurements }}
|
||||||
outputPath: "build/constellation"
|
outputPath: "build/constellation"
|
||||||
|
|
||||||
|
- name: Fetch CLI
|
||||||
|
if: inputs.cliVersion != ''
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o constellation https://github.com/edgelesssys/constellation/releases/download/${{ inputs.cliVersion }}/constellation-linux-amd64
|
||||||
|
chmod u+x constellation
|
||||||
|
echo "$(pwd)" >> $GITHUB_PATH
|
||||||
|
export PATH="$PATH:$(pwd)"
|
||||||
|
constellation version
|
||||||
|
# Do not spam license server from pipeline
|
||||||
|
sudo sh -c 'echo "127.0.0.1 license.confidential.cloud" >> /etc/hosts'
|
||||||
|
|
||||||
- name: Build the bootstrapper
|
- name: Build the bootstrapper
|
||||||
id: build-bootstrapper
|
id: build-bootstrapper
|
||||||
if: inputs.isDebugImage == 'true'
|
if: inputs.isDebugImage == 'true'
|
||||||
|
|
20
.github/workflows/e2e-test-weekly.yml
vendored
20
.github/workflows/e2e-test-weekly.yml
vendored
|
@ -188,3 +188,23 @@ jobs:
|
||||||
--force-deletion-types Microsoft.Compute/virtualMachines \
|
--force-deletion-types Microsoft.Compute/virtualMachines \
|
||||||
--no-wait \
|
--no-wait \
|
||||||
--yes
|
--yes
|
||||||
|
|
||||||
|
e2e-upgrade:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 1
|
||||||
|
matrix:
|
||||||
|
fromVersion:
|
||||||
|
["2.6"]
|
||||||
|
cloudProvider: ["gcp", "azure"]
|
||||||
|
name: Run upgrade tests
|
||||||
|
secrets: inherit
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
uses: ./.github/workflows/e2e-upgrade.yml
|
||||||
|
with:
|
||||||
|
fromVersion: ${{ matrix.fromVersion }}
|
||||||
|
cloudProvider: ${{ matrix.cloudProvider }}
|
||||||
|
workerNodesCount: 2
|
||||||
|
controlNodesCount: 3
|
||||||
|
|
224
.github/workflows/e2e-upgrade.yml
vendored
Normal file
224
.github/workflows/e2e-upgrade.yml
vendored
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
name: e2e test upgrade
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
cloudProvider:
|
||||||
|
description: "Which cloud provider to use."
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- "gcp"
|
||||||
|
- "azure"
|
||||||
|
default: "azure"
|
||||||
|
workerNodesCount:
|
||||||
|
description: "Number of worker nodes to spawn."
|
||||||
|
default: "2"
|
||||||
|
controlNodesCount:
|
||||||
|
description: "Number of control-plane nodes to spawn."
|
||||||
|
default: "3"
|
||||||
|
fromVersion:
|
||||||
|
description: CLI version to create a new cluster with. This has to be a released version, e.g., 'v2.1.3'.
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
toCLI:
|
||||||
|
description: CLI version to execute upgrade with, e.g., 'v2.1.3', or empty to build HEAD.
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
toImage:
|
||||||
|
description: Image (shortpath) the cluster is upgraded to, or empty for main/nightly.
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
toKubernetes:
|
||||||
|
description: Kubernetes version to target for the upgrade, empty for target's default version.
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
toMicroservices:
|
||||||
|
description: Microservice version to target for the upgrade, empty for target's default version.
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
cloudProvider:
|
||||||
|
description: "Which cloud provider to use."
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
workerNodesCount:
|
||||||
|
description: "Number of worker nodes to spawn."
|
||||||
|
type: number
|
||||||
|
required: true
|
||||||
|
controlNodesCount:
|
||||||
|
description: "Number of control-plane nodes to spawn."
|
||||||
|
type: number
|
||||||
|
required: true
|
||||||
|
fromVersion:
|
||||||
|
description: CLI version to create a new cluster with. This has to be a released version, e.g., 'v2.1.3'.
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
toCLI:
|
||||||
|
description: CLI version to execute upgrade with, e.g., 'v2.1.3', or empty to build HEAD.
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
toImage:
|
||||||
|
description: Image (shortpath) the cluster is upgraded to, or empty for main/nightly.
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
toKubernetes:
|
||||||
|
description: Kubernetes version to target for the upgrade, empty for target's default version.
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
toMicroservices:
|
||||||
|
description: Kubernetes version to target for the upgrade, empty for target's default version.
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
|
||||||
|
|
||||||
|
env:
|
||||||
|
ARM_CLIENT_ID: ${{ secrets.AZURE_E2E_CLIENT_ID }}
|
||||||
|
ARM_CLIENT_SECRET: ${{ secrets.AZURE_E2E_CLIENT_SECRET }}
|
||||||
|
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_E2E_SUBSCRIPTION_ID }}
|
||||||
|
ARM_TENANT_ID: ${{ secrets.AZURE_E2E_TENANT_ID }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e-upgrade:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3.2.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || '' }}
|
||||||
|
|
||||||
|
- name: Setup Go environment
|
||||||
|
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
|
||||||
|
with:
|
||||||
|
go-version: "1.20.2"
|
||||||
|
|
||||||
|
- name: Login to Azure
|
||||||
|
if: inputs.cloudProvider == 'azure'
|
||||||
|
uses: ./.github/actions/login_azure
|
||||||
|
with:
|
||||||
|
azure_credentials: ${{ secrets.AZURE_E2E_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Login to AWS
|
||||||
|
uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 # v1.7.0
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::795746500882:role/GithubConstellationVersionsAPIRead
|
||||||
|
aws-region: eu-central-1
|
||||||
|
|
||||||
|
- name: Create Azure resource group
|
||||||
|
if: inputs.cloudProvider == 'azure'
|
||||||
|
id: az_resource_group_gen
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
uuid=$(cat /proc/sys/kernel/random/uuid)
|
||||||
|
name=e2e-test-${uuid%%-*}
|
||||||
|
az group create --location northeurope --name "$name" --tags e2e
|
||||||
|
echo "res_group_name=$name" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Find latest nightly image
|
||||||
|
id: find-image
|
||||||
|
if: inputs.toImage == ''
|
||||||
|
uses: ./.github/actions/versionsapi
|
||||||
|
with:
|
||||||
|
command: latest
|
||||||
|
ref: main
|
||||||
|
stream: nightly
|
||||||
|
|
||||||
|
- name: Create cluster with 'fromVersion' CLI.
|
||||||
|
id: e2e_test
|
||||||
|
uses: ./.github/actions/e2e_test
|
||||||
|
with:
|
||||||
|
workerNodesCount: ${{ inputs.workerNodesCount }}
|
||||||
|
controlNodesCount: ${{ inputs.controlNodesCount }}
|
||||||
|
cloudProvider: ${{ inputs.cloudProvider }}
|
||||||
|
osImage: ${{ inputs.fromVersion }}
|
||||||
|
cliVersion: ${{ inputs.fromVersion }}
|
||||||
|
isDebugImage: "false"
|
||||||
|
azureSubscription: ${{ secrets.AZURE_E2E_SUBSCRIPTION_ID }}
|
||||||
|
azureTenant: ${{ secrets.AZURE_E2E_TENANT_ID }}
|
||||||
|
azureClientID: ${{ secrets.AZURE_E2E_CLIENT_ID }}
|
||||||
|
azureClientSecret: ${{ secrets.AZURE_E2E_CLIENT_SECRET }}
|
||||||
|
azureUserAssignedIdentity: ${{ secrets.AZURE_E2E_USER_ASSIGNED_IDENTITY }}
|
||||||
|
azureResourceGroup: ${{ steps.az_resource_group_gen.outputs.res_group_name }}
|
||||||
|
gcpProject: ${{ secrets.GCP_E2E_PROJECT }}
|
||||||
|
gcp_service_account: "constellation-e2e@constellation-331613.iam.gserviceaccount.com"
|
||||||
|
gcpClusterServiceAccountKey: ${{ secrets.GCP_CLUSTER_SERVICE_ACCOUNT }}
|
||||||
|
test: "nop"
|
||||||
|
buildBuddyApiKey: ${{ secrets.BUILDBUDDY_ORG_API_KEY }}
|
||||||
|
|
||||||
|
- name: Run upgrade test
|
||||||
|
run: |
|
||||||
|
echo "Image target: $IMAGE"
|
||||||
|
echo "K8s target: $KUBERNETES"
|
||||||
|
echo "Microservice target: $MICROSERVICES"
|
||||||
|
|
||||||
|
if [[ -n ${MICROSERVICES} ]]; then
|
||||||
|
MICROSERVICES_FLAG="--target-microservices $MICROSERVICES"
|
||||||
|
fi
|
||||||
|
if [[ -n ${KUBERNETES} ]]; then
|
||||||
|
KUBERNETES_FLAG="--target-kubernetes $KUBERNETES"
|
||||||
|
fi
|
||||||
|
|
||||||
|
bazelisk run //e2e/internal/upgrade:upgrade_test -- --want-worker "$WORKERNODES" --want-control "$CONTROLNODES" --target-image "$IMAGE" "$KUBERNETES_FLAG" "$MICROSERVICES_FLAG"
|
||||||
|
env:
|
||||||
|
KUBECONFIG: ${{ steps.e2e_test.outputs.kubeconfig }}
|
||||||
|
IMAGE: ${{ inputs.toImage && inputs.toImage || steps.find-image.outputs.output }}
|
||||||
|
KUBERNETES: ${{ inputs.toKubernetes }}
|
||||||
|
MICROSERVICES: ${{ inputs.toMicroservices }}
|
||||||
|
WORKERNODES: ${{ inputs.workerNodesCount }}
|
||||||
|
CONTROLNODES: ${{ inputs.controlNodesCount }}
|
||||||
|
|
||||||
|
- name: Always fetch logs
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
kubectl logs -n kube-system -l "app.kubernetes.io/name=node-maintenance-operator" --tail=-1 > node-maintenance-operator.logs
|
||||||
|
kubectl get nodeversions.update.edgeless.systems constellation-version -o yaml > constellation-version.yaml
|
||||||
|
env:
|
||||||
|
KUBECONFIG: ${{ steps.e2e_test.outputs.kubeconfig }}
|
||||||
|
|
||||||
|
- name: Always upload logs
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3.1.1
|
||||||
|
with:
|
||||||
|
name: upgrade-logs
|
||||||
|
path: |
|
||||||
|
node-maintenance-operator.logs
|
||||||
|
constellation-version.yaml
|
||||||
|
|
||||||
|
- name: Always terminate cluster
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
uses: ./.github/actions/constellation_destroy
|
||||||
|
with:
|
||||||
|
kubeconfig: ${{ steps.e2e_test.outputs.kubeconfig }}
|
||||||
|
|
||||||
|
- name: Notify teams channel
|
||||||
|
if: failure() && github.ref == 'refs/heads/main'
|
||||||
|
continue-on-error: true
|
||||||
|
shell: bash
|
||||||
|
working-directory: .github/actions/e2e_test
|
||||||
|
run: |
|
||||||
|
sudo apt-get install gettext-base -y
|
||||||
|
export TEAMS_JOB_NAME="upgrade-${{ inputs.cloudProvider }}"
|
||||||
|
export TEAMS_RUN_ID=${{ github.run_id }}
|
||||||
|
envsubst < teams-payload.json > to-be-send.json
|
||||||
|
curl \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @to-be-send.json \
|
||||||
|
"${{ secrets.MS_TEAMS_WEBHOOK_URI }}"
|
||||||
|
|
||||||
|
- name: Always destroy Azure resource group
|
||||||
|
if: always() && inputs.cloudProvider == 'azure'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
az group delete \
|
||||||
|
--name ${{ steps.az_resource_group_gen.outputs.res_group_name }} \
|
||||||
|
--force-deletion-types Microsoft.Compute/virtualMachineScaleSets \
|
||||||
|
--force-deletion-types Microsoft.Compute/virtualMachines \
|
||||||
|
--no-wait \
|
||||||
|
--yes
|
|
@ -2,6 +2,7 @@ run:
|
||||||
timeout: 10m
|
timeout: 10m
|
||||||
build-tags:
|
build-tags:
|
||||||
- integration
|
- integration
|
||||||
|
- e2e
|
||||||
modules-download-mode: readonly
|
modules-download-mode: readonly
|
||||||
skip-dirs:
|
skip-dirs:
|
||||||
- 3rdparty/node-maintenance-operator
|
- 3rdparty/node-maintenance-operator
|
||||||
|
|
|
@ -3,10 +3,16 @@ load("@com_github_ash2k_bazel_tools//multirun:def.bzl", "multirun")
|
||||||
load("@com_github_bazelbuild_buildtools//buildifier:def.bzl", "buildifier", "buildifier_test")
|
load("@com_github_bazelbuild_buildtools//buildifier:def.bzl", "buildifier", "buildifier_test")
|
||||||
load("//bazel/sh:def.bzl", "noop_warn", "repo_command", "sh_template")
|
load("//bazel/sh:def.bzl", "noop_warn", "repo_command", "sh_template")
|
||||||
|
|
||||||
gazelle(name = "gazelle_generate")
|
required_tags = ["e2e"]
|
||||||
|
|
||||||
|
gazelle(
|
||||||
|
name = "gazelle_generate",
|
||||||
|
build_tags = required_tags,
|
||||||
|
)
|
||||||
|
|
||||||
gazelle(
|
gazelle(
|
||||||
name = "gazelle_check",
|
name = "gazelle_check",
|
||||||
|
build_tags = required_tags,
|
||||||
command = "fix",
|
command = "fix",
|
||||||
mode = "diff",
|
mode = "diff",
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,7 +4,7 @@ This module contains rules and macros for building and testing Go code.
|
||||||
|
|
||||||
load("@io_bazel_rules_go//go:def.bzl", _go_test = "go_test")
|
load("@io_bazel_rules_go//go:def.bzl", _go_test = "go_test")
|
||||||
|
|
||||||
def go_test(ld = None, **kwargs):
|
def go_test(ld = None, count = 3, **kwargs):
|
||||||
"""go_test is a wrapper for go_test that uses default settings for Constellation.
|
"""go_test is a wrapper for go_test that uses default settings for Constellation.
|
||||||
|
|
||||||
It adds the following:
|
It adds the following:
|
||||||
|
@ -14,12 +14,13 @@ def go_test(ld = None, **kwargs):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ld: path to interpreter to that will be written into the elf header.
|
ld: path to interpreter to that will be written into the elf header.
|
||||||
|
count: number of times each test should be executed. defaults to 3.
|
||||||
**kwargs: all other arguments are passed to go_test.
|
**kwargs: all other arguments are passed to go_test.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Sets test count to 3.
|
# Sets test count to 3.
|
||||||
kwargs.setdefault("args", [])
|
kwargs.setdefault("args", [])
|
||||||
kwargs["args"].append("--test.count=3")
|
kwargs["args"].append("--test.count={}".format(count))
|
||||||
|
|
||||||
# enable race detector by default
|
# enable race detector by default
|
||||||
kwargs.setdefault("race", "on")
|
kwargs.setdefault("race", "on")
|
||||||
|
|
|
@ -12,12 +12,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||||
|
@ -181,34 +178,21 @@ func (cfm *configFetchMeasurementsCmd) parseFetchMeasurementsFlags(cmd *cobra.Co
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error {
|
func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error {
|
||||||
|
ver, err := versionsapi.NewVersionFromShortPath(conf.Image, versionsapi.VersionKindImage)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating version from image name: %w", err)
|
||||||
|
}
|
||||||
|
measurementsURL, signatureURL, err := versionsapi.MeasurementURL(ver, conf.GetProvider())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if f.measurementsURL == nil {
|
if f.measurementsURL == nil {
|
||||||
url, err := measurementURL(conf.GetProvider(), conf.Image, "measurements.json")
|
f.measurementsURL = measurementsURL
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.measurementsURL = url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.signatureURL == nil {
|
if f.signatureURL == nil {
|
||||||
url, err := measurementURL(conf.GetProvider(), conf.Image, "measurements.json.sig")
|
f.signatureURL = signatureURL
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.signatureURL = url
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func measurementURL(provider cloudprovider.Provider, image, file string) (*url.URL, error) {
|
|
||||||
ver, err := versionsapi.NewVersionFromShortPath(image, versionsapi.VersionKindImage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("creating version from image name: %w", err)
|
|
||||||
}
|
|
||||||
artifactBaseURL := ver.ArtifactURL()
|
|
||||||
url, err := url.Parse(artifactBaseURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing artifact base URL %s: %w", artifactBaseURL, err)
|
|
||||||
}
|
|
||||||
url.Path = path.Join(url.Path, "image", "csp", strings.ToLower(provider.String()), file)
|
|
||||||
return url, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -119,11 +119,13 @@ func TestUpdateURLs(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
flags: &fetchMeasurementsFlags{},
|
flags: &fetchMeasurementsFlags{},
|
||||||
wantMeasurementsURL: ver.ArtifactURL() + "/image/csp/gcp/measurements.json",
|
wantMeasurementsURL: ver.ArtifactsURL() + "/image/csp/gcp/measurements.json",
|
||||||
wantMeasurementsSigURL: ver.ArtifactURL() + "/image/csp/gcp/measurements.json.sig",
|
wantMeasurementsSigURL: ver.ArtifactsURL() + "/image/csp/gcp/measurements.json.sig",
|
||||||
},
|
},
|
||||||
"both set by user": {
|
"both set by user": {
|
||||||
conf: &config.Config{},
|
conf: &config.Config{
|
||||||
|
Image: ver.ShortPath(),
|
||||||
|
},
|
||||||
flags: &fetchMeasurementsFlags{
|
flags: &fetchMeasurementsFlags{
|
||||||
measurementsURL: urlMustParse("get.my/measurements.json"),
|
measurementsURL: urlMustParse("get.my/measurements.json"),
|
||||||
signatureURL: urlMustParse("get.my/measurements.json.sig"),
|
signatureURL: urlMustParse("get.my/measurements.json.sig"),
|
||||||
|
|
|
@ -147,11 +147,11 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler
|
||||||
|
|
||||||
spinner.Start("Creating", false)
|
spinner.Start("Creating", false)
|
||||||
idFile, err := creator.Create(cmd.Context(), provider, conf, instanceType, flags.controllerCount, flags.workerCount)
|
idFile, err := creator.Create(cmd.Context(), provider, conf, instanceType, flags.controllerCount, flags.workerCount)
|
||||||
c.log.Debugf("Successfully created the cloud resources for the cluster")
|
|
||||||
spinner.Stop()
|
spinner.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return translateCreateErrors(cmd, err)
|
return translateCreateErrors(cmd, err)
|
||||||
}
|
}
|
||||||
|
c.log.Debugf("Successfully created the cloud resources for the cluster")
|
||||||
|
|
||||||
if err := fileHandler.WriteJSON(constants.ClusterIDsFileName, idFile, file.OptNone); err != nil {
|
if err := fileHandler.WriteJSON(constants.ClusterIDsFileName, idFile, file.OptNone); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -470,12 +470,7 @@ func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, clien
|
||||||
for _, version := range versions {
|
for _, version := range versions {
|
||||||
log.Debugf("Fetching measurements for image: %s", version)
|
log.Debugf("Fetching measurements for image: %s", version)
|
||||||
shortPath := version.ShortPath()
|
shortPath := version.ShortPath()
|
||||||
measurementsURL, err := measurementURL(csp, shortPath, "measurements.json")
|
measurementsURL, signatureURL, err := versionsapi.MeasurementURL(version, csp)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
signatureURL, err := measurementURL(csp, shortPath, "measurements.json.sig")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,6 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"examples": ["{'1':{'expected':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','warnOnly':true},'15':{'expected':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=','warnOnly':true}}"]
|
"examples": ["{'1':{'expected':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA','warnOnly':true},'15':{'expected':'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=','warnOnly':true}}"]
|
||||||
},
|
},
|
||||||
"enforceIdKeyDigest": {
|
|
||||||
"description": "ID Key Digest enforcement policy.",
|
|
||||||
"enum": ["StrictChecking", "MAAFallback", "WarnOnly"]
|
|
||||||
},
|
|
||||||
"idKeyConfig": {
|
"idKeyConfig": {
|
||||||
"description": "Configuration for validating the ID Key Digest of the SEV-SNP attestation.",
|
"description": "Configuration for validating the ID Key Digest of the SEV-SNP attestation.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
|
@ -110,7 +110,7 @@ func (c *Client) Upgrade(ctx context.Context, config *config.Config, timeout tim
|
||||||
case errors.As(err, &invalidUpgrade):
|
case errors.As(err, &invalidUpgrade):
|
||||||
upgradeErrs = append(upgradeErrs, fmt.Errorf("skipping %s upgrade: %w", info.releaseName, err))
|
upgradeErrs = append(upgradeErrs, fmt.Errorf("skipping %s upgrade: %w", info.releaseName, err))
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return fmt.Errorf("upgrading %s: %w", info.releaseName, err)
|
return fmt.Errorf("should upgrade %s: %w", info.releaseName, err)
|
||||||
case err == nil:
|
case err == nil:
|
||||||
upgradeReleases = append(upgradeReleases, chart)
|
upgradeReleases = append(upgradeReleases, chart)
|
||||||
}
|
}
|
||||||
|
|
|
@ -464,10 +464,7 @@ func (i *ChartLoader) loadConstellationServicesValues() (map[string]any, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// extendConstellationServicesValues extends the given values map by some values depending on user input.
|
// extendConstellationServicesValues extends the given values map by some values depending on user input.
|
||||||
// This extra step of separating the application of user input is necessary since service upgrades should
|
// Values set inside this function are only applied during init, not during upgrade.
|
||||||
// reuse user input from the init step. However, we can't rely on reuse-values, because
|
|
||||||
// during upgrades all values need to be set locally as they might have changed.
|
|
||||||
// Also, the charts are not rendered correctly without all of these values.
|
|
||||||
func extendConstellationServicesValues(
|
func extendConstellationServicesValues(
|
||||||
in map[string]any, config *config.Config, masterSecret, salt []byte, maaURL string,
|
in map[string]any, config *config.Config, masterSecret, salt []byte, maaURL string,
|
||||||
) error {
|
) error {
|
||||||
|
|
50
e2e/internal/upgrade/BUILD.bazel
Normal file
50
e2e/internal/upgrade/BUILD.bazel
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
load("//bazel/go:go_test.bzl", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "upgrade",
|
||||||
|
srcs = [
|
||||||
|
"helm.go",
|
||||||
|
"image.go",
|
||||||
|
"upgrade.go",
|
||||||
|
],
|
||||||
|
importpath = "github.com/edgelesssys/constellation/v2/e2e/internal/upgrade",
|
||||||
|
visibility = ["//e2e:__subpackages__"],
|
||||||
|
deps = [
|
||||||
|
"//internal/attestation/measurements",
|
||||||
|
"//internal/cloud/cloudprovider",
|
||||||
|
"//internal/constants",
|
||||||
|
"//internal/logger",
|
||||||
|
"//internal/versionsapi",
|
||||||
|
"//internal/versionsapi/fetcher",
|
||||||
|
"@sh_helm_helm_v3//pkg/action",
|
||||||
|
"@sh_helm_helm_v3//pkg/cli",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "upgrade_test",
|
||||||
|
srcs = ["upgrade_test.go"],
|
||||||
|
count = 1,
|
||||||
|
data = [
|
||||||
|
"//cli:cli_oss_linux_amd64",
|
||||||
|
],
|
||||||
|
embed = [":upgrade"],
|
||||||
|
env = {
|
||||||
|
"PATH_CLI": "$(location //cli:cli_oss_linux_amd64)",
|
||||||
|
},
|
||||||
|
gotags = ["e2e"],
|
||||||
|
tags = ["manual"],
|
||||||
|
deps = [
|
||||||
|
"//e2e/internal/kubectl",
|
||||||
|
"//internal/config",
|
||||||
|
"//internal/constants",
|
||||||
|
"//internal/file",
|
||||||
|
"//internal/semver",
|
||||||
|
"@com_github_spf13_afero//:afero",
|
||||||
|
"@com_github_stretchr_testify//require",
|
||||||
|
"@io_k8s_api//core/v1:core",
|
||||||
|
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
||||||
|
"@io_k8s_client_go//kubernetes",
|
||||||
|
],
|
||||||
|
)
|
54
e2e/internal/upgrade/helm.go
Normal file
54
e2e/internal/upgrade/helm.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
//go:build e2e
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package upgrade
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||||
|
"helm.sh/helm/v3/pkg/action"
|
||||||
|
"helm.sh/helm/v3/pkg/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func servicesVersion(t *testing.T) (string, error) {
|
||||||
|
t.Helper()
|
||||||
|
log := logger.NewTest(t)
|
||||||
|
settings := cli.New()
|
||||||
|
settings.KubeConfig = "constellation-admin.conf"
|
||||||
|
actionConfig := &action.Configuration{}
|
||||||
|
if err := actionConfig.Init(settings.RESTClientGetter(), constants.HelmNamespace, "secret", log.Infof); err != nil {
|
||||||
|
return "", fmt.Errorf("initializing config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentVersion(actionConfig, "constellation-services")
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentVersion(cfg *action.Configuration, release string) (string, error) {
|
||||||
|
action := action.NewList(cfg)
|
||||||
|
action.Filter = release
|
||||||
|
rel, err := action.Run()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rel) == 0 {
|
||||||
|
return "", fmt.Errorf("release %s not found", release)
|
||||||
|
}
|
||||||
|
if len(rel) > 1 {
|
||||||
|
return "", fmt.Errorf("multiple releases found for %s", release)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rel[0] == nil || rel[0].Chart == nil || rel[0].Chart.Metadata == nil {
|
||||||
|
return "", fmt.Errorf("received invalid release %s", release)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rel[0].Chart.Metadata.Version, nil
|
||||||
|
}
|
91
e2e/internal/upgrade/image.go
Normal file
91
e2e/internal/upgrade/image.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
//go:build e2e
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package upgrade
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/versionsapi/fetcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
type upgradeInfo struct {
|
||||||
|
measurements measurements.M
|
||||||
|
shortPath string
|
||||||
|
wantImage string
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchUpgradeInfo(ctx context.Context, csp cloudprovider.Provider, toImage string) (upgradeInfo, error) {
|
||||||
|
info := upgradeInfo{
|
||||||
|
measurements: make(measurements.M),
|
||||||
|
shortPath: toImage,
|
||||||
|
}
|
||||||
|
versionsClient := fetcher.NewFetcher()
|
||||||
|
|
||||||
|
ver, err := versionsapi.NewVersionFromShortPath(toImage, versionsapi.VersionKindImage)
|
||||||
|
if err != nil {
|
||||||
|
return upgradeInfo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
measurementsURL, signatureURL, err := versionsapi.MeasurementURL(ver, csp)
|
||||||
|
if err != nil {
|
||||||
|
return upgradeInfo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var fetchedMeasurements measurements.M
|
||||||
|
_, err = fetchedMeasurements.FetchAndVerify(
|
||||||
|
ctx, http.DefaultClient,
|
||||||
|
measurementsURL,
|
||||||
|
signatureURL,
|
||||||
|
[]byte(constants.CosignPublicKey),
|
||||||
|
measurements.WithMetadata{
|
||||||
|
CSP: csp,
|
||||||
|
Image: toImage,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return upgradeInfo{}, err
|
||||||
|
}
|
||||||
|
info.measurements = fetchedMeasurements
|
||||||
|
|
||||||
|
wantImage, err := fetchWantImage(ctx, versionsClient, csp, versionsapi.ImageInfo{
|
||||||
|
Ref: ver.Ref,
|
||||||
|
Stream: ver.Stream,
|
||||||
|
Version: ver.Version,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return upgradeInfo{}, err
|
||||||
|
}
|
||||||
|
info.wantImage = wantImage
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchWantImage(ctx context.Context, client *fetcher.Fetcher, csp cloudprovider.Provider, imageInfo versionsapi.ImageInfo) (string, error) {
|
||||||
|
imageInfo, err := client.FetchImageInfo(ctx, imageInfo)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch csp {
|
||||||
|
case cloudprovider.GCP:
|
||||||
|
return imageInfo.GCP["sev-es"], nil
|
||||||
|
case cloudprovider.Azure:
|
||||||
|
return imageInfo.Azure["cvm"], nil
|
||||||
|
case cloudprovider.AWS:
|
||||||
|
return imageInfo.AWS["eu-central-1"], nil
|
||||||
|
default:
|
||||||
|
return "", errors.New("finding wanted image")
|
||||||
|
}
|
||||||
|
}
|
19
e2e/internal/upgrade/upgrade.go
Normal file
19
e2e/internal/upgrade/upgrade.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package upgrade tests that the CLI's upgrade apply command works as expected and
|
||||||
|
// the operators eventually upgrade all nodes inside the cluster.
|
||||||
|
// The test is written as a go test because:
|
||||||
|
// 1. the helm cli does not directly provide the chart version of a release
|
||||||
|
//
|
||||||
|
// 2. the image patch needs to be parsed from the image-api's info.json
|
||||||
|
//
|
||||||
|
// 3. there is some logic required to setup the test correctly:
|
||||||
|
//
|
||||||
|
// - set microservice, k8s version correctly depending on input
|
||||||
|
//
|
||||||
|
// - set or fetch measurements depending on target image
|
||||||
|
package upgrade
|
327
e2e/internal/upgrade/upgrade_test.go
Normal file
327
e2e/internal/upgrade/upgrade_test.go
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
//go:build e2e
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package upgrade
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/e2e/internal/kubectl"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/semver"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
coreV1 "k8s.io/api/core/v1"
|
||||||
|
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Flags are defined globally as `go test` implicitly calls flag.Parse() before executing a testcase.
|
||||||
|
// Thus defining and parsing flags inside a testcase would result in a panic.
|
||||||
|
// See https://groups.google.com/g/golang-nuts/c/P6EdEdgvDuc/m/5-Dg6bPxmvQJ.
|
||||||
|
var (
|
||||||
|
targetImage = flag.String("target-image", "", "Image (shortversion) to upgrade to.")
|
||||||
|
targetKubernetes = flag.String("target-kubernetes", "", "Kubernetes version (MAJOR.MINOR.PATCH) to upgrade to. Defaults to default version of target CLI.")
|
||||||
|
targetMicroservices = flag.String("target-microservices", "", "Microservice version (MAJOR.MINOR.PATCH) to upgrade to. Defaults to default version of target CLI.")
|
||||||
|
// When executing the test as a bazel target the workspace path is supplied through an env variable that bazel sets.
|
||||||
|
workspace = flag.String("workspace", "", "Constellation workspace in which to run the tests.")
|
||||||
|
// When executing the test as a bazel target the CLI path is supplied through an env variable that bazel sets.
|
||||||
|
// When executing via `go test` extra care should be taken that the supplied CLI is built on the same commit as this test.
|
||||||
|
cliPath = flag.String("cli", "", "Constellation CLI to run the tests.")
|
||||||
|
wantWorker = flag.Int("want-worker", 0, "Number of wanted worker nodes.")
|
||||||
|
wantControl = flag.Int("want-control", 0, "Number of wanted control nodes.")
|
||||||
|
timeout = flag.Duration("timeout", 3*time.Hour, "Timeout after which the cluster should have converged to the target version.")
|
||||||
|
)
|
||||||
|
|
||||||
|
// setup checks that the prerequisites for the test are met:
|
||||||
|
// - a workspace is set
|
||||||
|
// - a CLI path is set
|
||||||
|
// - the constellation-upgrade folder does not exist.
|
||||||
|
func setup() error {
|
||||||
|
workingDir, err := workingDir(*workspace)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting working directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(workingDir); err != nil {
|
||||||
|
return fmt.Errorf("changing working directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := getCLIPath(*cliPath); err != nil {
|
||||||
|
return fmt.Errorf("getting CLI path: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("constellation-upgrade"); err == nil {
|
||||||
|
return errors.New("please remove the existing constellation-upgrade folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUpgrade checks that the workspace's kubeconfig points to a healthy cluster,
|
||||||
|
// we can write an upgrade config, we can trigger an upgrade
|
||||||
|
// and the cluster eventually upgrades to the target version.
|
||||||
|
func TestUpgrade(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
err := setup()
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
k, err := kubectl.New()
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(k)
|
||||||
|
|
||||||
|
require.NotEqual(*targetImage, "", "--target-image needs to be specified")
|
||||||
|
|
||||||
|
testNodesEventuallyAvailable(t, k, *wantControl, *wantWorker)
|
||||||
|
testPodsEventuallyReady(t, k, "kube-system")
|
||||||
|
|
||||||
|
targetVersions := writeUpgradeConfig(require, *targetImage, *targetKubernetes, *targetMicroservices)
|
||||||
|
|
||||||
|
cli, err := getCLIPath(*cliPath)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
data, err := os.ReadFile("./constellation-conf.yaml")
|
||||||
|
require.NoError(err)
|
||||||
|
log.Println(string(data))
|
||||||
|
|
||||||
|
log.Println("Triggering upgrade.")
|
||||||
|
cmd := exec.CommandContext(context.Background(), cli, "upgrade", "apply", "--force", "--debug")
|
||||||
|
msg, err := cmd.CombinedOutput()
|
||||||
|
require.NoErrorf(err, "%s", string(msg))
|
||||||
|
require.NoError(containsUnexepectedMsg(string(msg)))
|
||||||
|
log.Println(string(msg))
|
||||||
|
|
||||||
|
testMicroservicesEventuallyHaveVersion(t, targetVersions.microservices, *timeout)
|
||||||
|
testNodesEventuallyHaveVersion(t, k, targetVersions, *wantControl+*wantWorker, *timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// workingDir returns the path to the workspace.
|
||||||
|
func workingDir(workspace string) (string, error) {
|
||||||
|
workingDir := os.Getenv("BUILD_WORKING_DIRECTORY")
|
||||||
|
switch {
|
||||||
|
case workingDir != "":
|
||||||
|
return workingDir, nil
|
||||||
|
case workspace != "":
|
||||||
|
return workspace, nil
|
||||||
|
default:
|
||||||
|
return "", errors.New("neither 'BUILD_WORKING_DIRECTORY' nor 'workspace' flag set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCLIPath returns the path to the CLI.
|
||||||
|
func getCLIPath(cliPath string) (string, error) {
|
||||||
|
pathCLI := os.Getenv("PATH_CLI")
|
||||||
|
switch {
|
||||||
|
case pathCLI != "":
|
||||||
|
return pathCLI, nil
|
||||||
|
case cliPath != "":
|
||||||
|
return cliPath, nil
|
||||||
|
default:
|
||||||
|
return "", errors.New("neither 'PATH_CLI' nor 'cli' flag set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsUnexepectedMsg checks if the given input contains any unexpected messages.
|
||||||
|
// unexepcted messages are:
|
||||||
|
// "Skipping image & Kubernetes upgrades. Another upgrade is in progress".
|
||||||
|
func containsUnexepectedMsg(input string) error {
|
||||||
|
if strings.Contains(input, "Skipping image & Kubernetes upgrades. Another upgrade is in progress") {
|
||||||
|
return errors.New("unexpected upgrade in progress")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeUpgradeConfig(require *require.Assertions, image string, kubernetes string, microservices string) versionContainer {
|
||||||
|
fileHandler := file.NewHandler(afero.NewOsFs())
|
||||||
|
cfg, err := config.New(fileHandler, constants.ConfigFilename, true)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
info, err := fetchUpgradeInfo(context.Background(), cfg.GetProvider(), image)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
log.Printf("Setting image version: %s\n", info.shortPath)
|
||||||
|
cfg.Image = info.shortPath
|
||||||
|
cfg.UpdateMeasurements(info.measurements)
|
||||||
|
|
||||||
|
defaultConfig := config.Default()
|
||||||
|
var kubernetesVersion semver.Semver
|
||||||
|
if kubernetes == "" {
|
||||||
|
kubernetesVersion, err = semver.New(defaultConfig.KubernetesVersion)
|
||||||
|
require.NoError(err)
|
||||||
|
} else {
|
||||||
|
kubernetesVersion, err = semver.New(kubernetes)
|
||||||
|
require.NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var microserviceVersion string
|
||||||
|
if microservices == "" {
|
||||||
|
microserviceVersion = defaultConfig.MicroserviceVersion
|
||||||
|
} else {
|
||||||
|
microserviceVersion = microservices
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Setting K8s version: %s\n", kubernetesVersion.String())
|
||||||
|
cfg.KubernetesVersion = kubernetesVersion.String()
|
||||||
|
log.Printf("Setting microservice version: %s\n", microserviceVersion)
|
||||||
|
cfg.MicroserviceVersion = microserviceVersion
|
||||||
|
|
||||||
|
err = fileHandler.WriteYAML(constants.ConfigFilename, cfg, file.OptOverwrite)
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
return versionContainer{image: info.wantImage, kubernetes: kubernetesVersion, microservices: microserviceVersion}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMicroservicesEventuallyHaveVersion(t *testing.T, wantMicroserviceVersion string, timeout time.Duration) {
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
version, err := servicesVersion(t)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to fetch microservice version: %v\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if version != wantMicroserviceVersion {
|
||||||
|
log.Printf("Microservices still at version: %v\n", version)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, timeout, time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNodesEventuallyHaveVersion(t *testing.T, k *kubernetes.Clientset, targetVersions versionContainer, totalNodeCount int, timeout time.Duration) {
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
nodes, err := k.CoreV1().Nodes().List(context.Background(), metaV1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
require.False(t, len(nodes.Items) < totalNodeCount, "expected at least %v nodes, got %v", totalNodeCount, len(nodes.Items))
|
||||||
|
|
||||||
|
allUpdated := true
|
||||||
|
log.Printf("Node status (%v):", time.Now())
|
||||||
|
for _, node := range nodes.Items {
|
||||||
|
for key, value := range node.Annotations {
|
||||||
|
if key == "constellation.edgeless.systems/node-image" {
|
||||||
|
log.Printf("\t%s: Image %s\n", node.Name, value)
|
||||||
|
if value != targetVersions.image {
|
||||||
|
allUpdated = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kubeletVersion := node.Status.NodeInfo.KubeletVersion
|
||||||
|
if kubeletVersion != targetVersions.kubernetes.String() {
|
||||||
|
log.Printf("\t%s: K8s (Kubelet) %s\n", node.Name, kubeletVersion)
|
||||||
|
allUpdated = false
|
||||||
|
}
|
||||||
|
kubeProxyVersion := node.Status.NodeInfo.KubeProxyVersion
|
||||||
|
if kubeProxyVersion != targetVersions.kubernetes.String() {
|
||||||
|
log.Printf("\t%s: K8s (Proxy) %s\n", node.Name, kubeProxyVersion)
|
||||||
|
allUpdated = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allUpdated
|
||||||
|
}, timeout, time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testPodsEventuallyReady checks that:
|
||||||
|
// 1) all pods are running.
|
||||||
|
// 2) all pods have good status conditions.
|
||||||
|
func testPodsEventuallyReady(t *testing.T, k *kubernetes.Clientset, namespace string) {
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
pods, err := k.CoreV1().Pods(namespace).List(context.Background(), metaV1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pod := range pods.Items {
|
||||||
|
if pod.Status.Phase != coreV1.PodRunning {
|
||||||
|
log.Printf("Pod %s is not running, but %s\n", pod.Name, pod.Status.Phase)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, condition := range pod.Status.Conditions {
|
||||||
|
switch condition.Type {
|
||||||
|
case coreV1.ContainersReady, coreV1.PodInitialized, coreV1.PodReady, coreV1.PodScheduled:
|
||||||
|
if condition.Status != coreV1.ConditionTrue {
|
||||||
|
log.Printf("Pod %s's status %s is false\n", pod.Name, coreV1.ContainersReady)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, time.Minute*30, time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testNodesEventuallyAvailable checks that:
|
||||||
|
// 1) all nodes only have good status conditions.
|
||||||
|
// 2) the expected number of nodes have joined the cluster.
|
||||||
|
func testNodesEventuallyAvailable(t *testing.T, k *kubernetes.Clientset, wantControlNodeCount, wantWorkerNodeCount int) {
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
nodes, err := k.CoreV1().Nodes().List(context.Background(), metaV1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var controlNodeCount, workerNodeCount int
|
||||||
|
for _, node := range nodes.Items {
|
||||||
|
if _, ok := node.Labels["node-role.kubernetes.io/control-plane"]; ok {
|
||||||
|
controlNodeCount++
|
||||||
|
} else {
|
||||||
|
workerNodeCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, condition := range node.Status.Conditions {
|
||||||
|
switch condition.Type {
|
||||||
|
case coreV1.NodeReady:
|
||||||
|
if condition.Status != coreV1.ConditionTrue {
|
||||||
|
fmt.Printf("Status %s for node %s is %s\n", condition.Type, node.Name, condition.Status)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case coreV1.NodeMemoryPressure, coreV1.NodeDiskPressure, coreV1.NodePIDPressure, coreV1.NodeNetworkUnavailable:
|
||||||
|
if condition.Status != coreV1.ConditionFalse {
|
||||||
|
fmt.Printf("Status %s for node %s is %s\n", condition.Type, node.Name, condition.Status)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if controlNodeCount != wantControlNodeCount {
|
||||||
|
log.Printf("Want %d control nodes but got %d\n", wantControlNodeCount, controlNodeCount)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if workerNodeCount != wantWorkerNodeCount {
|
||||||
|
log.Printf("Want %d worker nodes but got %d\n", wantWorkerNodeCount, workerNodeCount)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, time.Minute*30, time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
type versionContainer struct {
|
||||||
|
image string
|
||||||
|
kubernetes semver.Semver
|
||||||
|
microservices string
|
||||||
|
}
|
|
@ -143,7 +143,7 @@ func measurementURL(provider cloudprovider.Provider, image, file string) (*url.U
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.Parse(
|
return url.Parse(
|
||||||
version.ArtifactURL() + path.Join("/image", "csp", strings.ToLower(provider.String()), file),
|
version.ArtifactsURL() + path.Join("/image", "csp", strings.ToLower(provider.String()), file),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -181,6 +181,10 @@ const (
|
||||||
CDNRepositoryURL = "https://cdn.confidential.cloud"
|
CDNRepositoryURL = "https://cdn.confidential.cloud"
|
||||||
// CDNAPIPrefix is the prefix of the Constellation API.
|
// CDNAPIPrefix is the prefix of the Constellation API.
|
||||||
CDNAPIPrefix = "constellation/v1"
|
CDNAPIPrefix = "constellation/v1"
|
||||||
|
// CDNMeasurementsFile is name of file containing image measurements.
|
||||||
|
CDNMeasurementsFile = "measurements.json"
|
||||||
|
// CDNMeasurementsSignature is name of file containing signature for CDNMeasurementsFile.
|
||||||
|
CDNMeasurementsSignature = "measurements.json.sig"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VersionInfo returns the version of a binary.
|
// VersionInfo returns the version of a binary.
|
||||||
|
|
|
@ -25,8 +25,8 @@ type Semver struct {
|
||||||
Patch int
|
Patch int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSemver returns a Version from a string.
|
// New returns a Version from a string.
|
||||||
func NewSemver(version string) (Semver, error) {
|
func New(version string) (Semver, error) {
|
||||||
// ensure that semver has "v" prefix
|
// ensure that semver has "v" prefix
|
||||||
if !strings.HasPrefix(version, "v") {
|
if !strings.HasPrefix(version, "v") {
|
||||||
version = "v" + version
|
version = "v" + version
|
||||||
|
@ -70,7 +70,7 @@ func (v Semver) IsUpgradeTo(other Semver) bool {
|
||||||
// CompatibleWithBinary returns if a version is compatible version of the current built binary.
|
// CompatibleWithBinary returns if a version is compatible version of the current built binary.
|
||||||
// It checks if the version of the binary is equal or greater than the current version and allows a drift of at most one minor version.
|
// It checks if the version of the binary is equal or greater than the current version and allows a drift of at most one minor version.
|
||||||
func (v Semver) CompatibleWithBinary() bool {
|
func (v Semver) CompatibleWithBinary() bool {
|
||||||
binaryVersion, err := NewSemver(constants.VersionInfo())
|
binaryVersion, err := New(constants.VersionInfo())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ func (v *Semver) UnmarshalJSON(data []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
version, err := NewSemver(s)
|
version, err := New(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func TestNewVersion(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
_, err := NewSemver(tc.version)
|
_, err := New(tc.version)
|
||||||
if tc.wantErr {
|
if tc.wantErr {
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -14,6 +14,7 @@ go_library(
|
||||||
importpath = "github.com/edgelesssys/constellation/v2/internal/versionsapi",
|
importpath = "github.com/edgelesssys/constellation/v2/internal/versionsapi",
|
||||||
visibility = ["//:__subpackages__"],
|
visibility = ["//:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//internal/cloud/cloudprovider",
|
||||||
"//internal/constants",
|
"//internal/constants",
|
||||||
"@org_golang_x_mod//semver",
|
"@org_golang_x_mod//semver",
|
||||||
],
|
],
|
||||||
|
@ -30,6 +31,7 @@ go_test(
|
||||||
],
|
],
|
||||||
embed = [":versionsapi"],
|
embed = [":versionsapi"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//internal/cloud/cloudprovider",
|
||||||
"//internal/constants",
|
"//internal/constants",
|
||||||
"@com_github_stretchr_testify//assert",
|
"@com_github_stretchr_testify//assert",
|
||||||
"@com_github_stretchr_testify//require",
|
"@com_github_stretchr_testify//require",
|
||||||
|
|
|
@ -10,10 +10,12 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
"golang.org/x/mod/semver"
|
"golang.org/x/mod/semver"
|
||||||
)
|
)
|
||||||
|
@ -142,9 +144,9 @@ func (v Version) ListPath(gran Granularity) string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArtifactURL returns the URL to the artifacts stored for this version.
|
// ArtifactsURL returns the URL to the artifacts stored for this version.
|
||||||
// The URL points to a directory.
|
// The URL points to a directory.
|
||||||
func (v Version) ArtifactURL() string {
|
func (v Version) ArtifactsURL() string {
|
||||||
return constants.CDNRepositoryURL + "/" + v.ArtifactPath()
|
return constants.CDNRepositoryURL + "/" + v.ArtifactPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,6 +325,33 @@ func ValidateStream(ref, stream string) error {
|
||||||
return fmt.Errorf("stream %q is unknown or not supported on ref %q", stream, ref)
|
return fmt.Errorf("stream %q is unknown or not supported on ref %q", stream, ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MeasurementURL builds the measurement and signature URLs for the given version and CSP.
|
||||||
|
func MeasurementURL(version Version, csp cloudprovider.Provider) (measurementURL, signatureURL *url.URL, err error) {
|
||||||
|
if version.Kind != VersionKindImage {
|
||||||
|
return &url.URL{}, &url.URL{}, fmt.Errorf("kind %q is not supported", version.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
measurementPath, err := url.JoinPath(version.ArtifactsURL(), "image", "csp", strings.ToLower(csp.String()), constants.CDNMeasurementsFile)
|
||||||
|
if err != nil {
|
||||||
|
return &url.URL{}, &url.URL{}, fmt.Errorf("joining path for measurement: %w", err)
|
||||||
|
}
|
||||||
|
signaturePath, err := url.JoinPath(version.ArtifactsURL(), "image", "csp", strings.ToLower(csp.String()), constants.CDNMeasurementsSignature)
|
||||||
|
if err != nil {
|
||||||
|
return &url.URL{}, &url.URL{}, fmt.Errorf("joining path for signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
measurementURL, err = url.Parse(measurementPath)
|
||||||
|
if err != nil {
|
||||||
|
return &url.URL{}, &url.URL{}, fmt.Errorf("parsing path for measurement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureURL, err = url.Parse(signaturePath)
|
||||||
|
if err != nil {
|
||||||
|
return &url.URL{}, &url.URL{}, fmt.Errorf("parsing path for signature: %w", err)
|
||||||
|
}
|
||||||
|
return measurementURL, signatureURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
shortPathRegex = regexp.MustCompile(`^ref/([a-zA-Z0-9-]+)/stream/([a-zA-Z0-9-]+)/([a-zA-Z0-9.-]+)$`)
|
shortPathRegex = regexp.MustCompile(`^ref/([a-zA-Z0-9-]+)/stream/([a-zA-Z0-9-]+)/([a-zA-Z0-9.-]+)$`)
|
||||||
shortPathReleaseRegex = regexp.MustCompile(`^stream/([a-zA-Z0-9-]+)/([a-zA-Z0-9.-]+)$`)
|
shortPathReleaseRegex = regexp.MustCompile(`^stream/([a-zA-Z0-9-]+)/([a-zA-Z0-9.-]+)$`)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
@ -451,6 +452,49 @@ func TestVersionListPathURL(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVersionArtifactURL(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
ver Version
|
||||||
|
csp cloudprovider.Provider
|
||||||
|
wantMeasurementURL string
|
||||||
|
wantSignatureURL string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"nightly-feature": {
|
||||||
|
ver: Version{
|
||||||
|
Ref: "feat-some-feature",
|
||||||
|
Stream: "nightly",
|
||||||
|
Version: "v2.6.0-pre.0.20230217095603-193dd48ca19f",
|
||||||
|
Kind: VersionKindImage,
|
||||||
|
},
|
||||||
|
csp: cloudprovider.GCP,
|
||||||
|
wantMeasurementURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/feat-some-feature/stream/nightly/v2.6.0-pre.0.20230217095603-193dd48ca19f/image/csp/gcp/measurements.json",
|
||||||
|
wantSignatureURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/feat-some-feature/stream/nightly/v2.6.0-pre.0.20230217095603-193dd48ca19f/image/csp/gcp/measurements.json.sig",
|
||||||
|
},
|
||||||
|
"fail for wrong kind": {
|
||||||
|
ver: Version{
|
||||||
|
Kind: VersionKindCLI,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
measurementURL, signatureURL, err := MeasurementURL(tc.ver, tc.csp)
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(tc.wantMeasurementURL, measurementURL.String())
|
||||||
|
assert.Equal(tc.wantSignatureURL, signatureURL.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVersionArtifactPathURL(t *testing.T) {
|
func TestVersionArtifactPathURL(t *testing.T) {
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
ver Version
|
ver Version
|
||||||
|
@ -518,7 +562,7 @@ func TestVersionArtifactPathURL(t *testing.T) {
|
||||||
|
|
||||||
path := tc.ver.ArtifactPath()
|
path := tc.ver.ArtifactPath()
|
||||||
assert.Equal(tc.wantPath, path)
|
assert.Equal(tc.wantPath, path)
|
||||||
url := tc.ver.ArtifactURL()
|
url := tc.ver.ArtifactsURL()
|
||||||
assert.Equal(constants.CDNRepositoryURL+"/"+tc.wantPath, url)
|
assert.Equal(constants.CDNRepositoryURL+"/"+tc.wantPath, url)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue