Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/actions/setup-e2e-toolchain/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ runs:

- name: Restore d8 cache
id: d8-cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: /opt/deckhouse/bin/d8
key: d8-${{ inputs.d8-version }}-${{ runner.os }}
Expand All @@ -57,7 +57,7 @@ runs:

- name: Install kubectl CLI
if: inputs.install-kubectl == 'true'
uses: azure/setup-kubectl@v4
uses: azure/setup-kubectl@v5

- name: Install htpasswd utility
if: inputs.install-htpasswd == 'true'
Expand Down
31 changes: 31 additions & 0 deletions .github/scripts/bash/e2e/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,37 @@ modules_repo_for_registry() {
fi
}

# Echoes the virtualization feature gates supported by every given release, one
# per line. A gate the pulled module does not know fails ModulePullOverride
# validation and leaves the module uninstalled, so a gate is only listed when
# none of the releases predate it. In-place resize never shipped in the 1.9
# line. Anything that is not a release tag (a PR reference, a build off main)
# carries the current gates.
# Usage: virtualization_feature_gates [release]...
virtualization_feature_gates() {
local release

echo "HotplugCPUWithLiveMigration"
echo "HotplugMemoryWithLiveMigration"

for release in "$@"; do
if [[ "${release}" =~ ^v([0-9]+)\.([0-9]+)\. ]] && (( BASH_REMATCH[1] == 1 && BASH_REMATCH[2] < 10 )); then
return 0
fi
done

echo "HotplugCPUAndMemoryWithInPlaceResize"
}

# Echoes images_digests.json packaged in the module image of a given release.
# Usage: module_images_digests <module_source> <release>
module_images_digests() {
local module_source="$1"
local release="$2"

crane export "${module_source}/virtualization:${release}" - | tar -Oxf - images_digests.json
}

# Reads a manifest from stdin and applies it with retries.
# Usage: kubectl_apply_with_retry [count] [delay] [diag_fn]
# diag_fn is an optional function name invoked on each failed attempt.
Expand Down
11 changes: 8 additions & 3 deletions .github/scripts/bash/e2e/configure-virtualization-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ current_release="$(required_env_value CURRENT_RELEASE)"

REGISTRY="$(registry_host_from_docker_cfg "${dev_registry_docker_cfg}")"

# Only the gates this release knows: a gate it does not support fails
# ModulePullOverride validation and the module never installs. The upgrade
# revisits the list for the new release (patch-virtualization-feature-gates.sh).
feature_gates_yaml="$(virtualization_feature_gates "${current_release}" | sed 's/^/ - /')"
echo "[INFO] Feature gates for ${current_release}:"
echo "${feature_gates_yaml}"

echo "[INFO] Apply ModuleSource prod config"
kubectl_apply_with_retry 20 10 show_deckhouse_state <<EOF
apiVersion: deckhouse.io/v1alpha1
Expand Down Expand Up @@ -71,9 +78,7 @@ spec:
virtualMachineCIDRs:
- 192.168.10.0/24
featureGates:
- HotplugCPUWithLiveMigration
- HotplugMemoryWithLiveMigration
- HotplugCPUAndMemoryWithInPlaceResize
${feature_gates_yaml}
source: deckhouse-dev
version: 1
---
Expand Down
58 changes: 58 additions & 0 deletions .github/scripts/bash/e2e/patch-virtualization-feature-gates.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env bash

# Copyright 2026 Flant JSC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Sets the virtualization feature gates of the module config to those supported
# by every given release, and verifies the result.
#
# Usage: patch-virtualization-feature-gates.sh <release>...
#
# During a release upgrade this runs twice. Before the image tag is patched it
# is called with both releases, which drops the gates the new release does not
# know - otherwise the new module fails validation and never installs. After the
# upgrade it is called with the new release alone, which enables the gates only
# that release supports.

set -Eeuo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=.github/scripts/bash/e2e/common.sh
source "${SCRIPT_DIR}/common.sh"

if [ "$#" -eq 0 ]; then
echo "[ERROR] Usage: $(basename -- "${BASH_SOURCE[0]}") <release>..." >&2
exit 1
fi

gates_json="$(virtualization_feature_gates "$@" | jq -Rsc 'split("\n") | map(select(length > 0))')"
current_json="$(kubectl get mc virtualization -o jsonpath='{.spec.settings.featureGates}')"

echo "[INFO] Feature gates supported by $*: ${gates_json}"

if [ "${current_json}" = "${gates_json}" ]; then
echo "[INFO] Module config already lists exactly these gates, nothing to patch"
exit 0
fi

echo "[INFO] Patching feature gates: ${current_json:-none} -> ${gates_json}"
kubectl patch mc virtualization --type merge -p "{\"spec\":{\"settings\":{\"featureGates\":${gates_json}}}}"

patched_json="$(kubectl get mc virtualization -o jsonpath='{.spec.settings.featureGates}')"
if [ "${patched_json}" != "${gates_json}" ]; then
echo "[ERROR] Feature gates were not applied: expected ${gates_json}, got ${patched_json:-none}" >&2
exit 1
fi

echo "[INFO] Feature gates in effect: ${patched_json}"
119 changes: 119 additions & 0 deletions .github/scripts/bash/e2e/report-clusteralerts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env bash

# Copyright 2026 Flant JSC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -Eeuo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=.github/scripts/bash/e2e/common.sh
source "${SCRIPT_DIR}/common.sh"

require_env CLUSTERALERTS_DIR

alerts_dir="${CLUSTERALERTS_DIR:-}"
alert_prefix="${CLUSTERALERTS_PREFIX:-D8Virtualization}"
fail_on_alerts="${FAIL_ON_ALERTS:-true}"
watch_result="${WATCH_RESULT:-}"
summary_file="${GITHUB_STEP_SUMMARY:-/dev/stdout}"

# The watch runs as a single job and does not know which pipeline phase it is
# observing, so the phase is derived here from the upgrade timestamps. A zero
# means the corresponding job never reported one.
started="${RELEASE_UPGRADE_STARTED_AT:-0}"
finished="${RELEASE_UPGRADE_FINISHED_AT:-0}"
[[ "${started}" =~ ^[0-9]+$ ]] || started=0
[[ "${finished}" =~ ^[0-9]+$ ]] || finished=0

# shellcheck disable=SC2016 # $started and $finished are jq variables, passed in via --argjson
phase_program='
def phase_of(upgrade_started; upgrade_finished):
if upgrade_started == 0 or .observedAt < upgrade_started
then { phase: "pre-upgrade", order: 0 }
elif upgrade_finished == 0 or .observedAt < upgrade_finished
then { phase: "upgrade", order: 1 }
else { phase: "post-upgrade", order: 2 }
end;
map(. + phase_of($started; $finished))
| unique_by([.phase, .name, .id])
| sort_by([.order, .name])
'

shopt -s nullglob
logs=("${alerts_dir}"/*.jsonl)
shopt -u nullglob

if [ "${#logs[@]}" -eq 0 ]; then
echo "[WARN] No ClusterAlerts logs found in ${alerts_dir}"
alerts='[]'
else
echo "[INFO] Reading collected ClusterAlerts from: ${logs[*]}"
echo "[INFO] Upgrade window: started_at=${started}, finished_at=${finished}"
alerts="$(jq -s \
--argjson started "${started}" \
--argjson finished "${finished}" \
"${phase_program}" "${logs[@]}")"
fi

count="$(jq 'length' <<< "${alerts}")"

# Alert summaries are markdown ending with a newline; flatten them for the
# table and for annotations.
oneline='def oneline: gsub("\\s+"; " ") | sub("^ "; "") | sub(" $"; "");'

{
echo "## ClusterAlerts in the nested cluster"
echo
} >> "${summary_file}"

if [ "${count}" -eq 0 ]; then
# An empty report means "nothing was firing" only if the watch actually ran.
if [ -n "${watch_result}" ] && [ "${watch_result}" != "success" ]; then
echo "The watch job did not complete (result: \`${watch_result}\`), so alerts were **not** monitored." >> "${summary_file}"
echo "::warning title=ClusterAlerts were not monitored::The watch job result is '${watch_result}'"
exit 0
fi

echo "No \`${alert_prefix}*\` alerts were firing during the release rollover." >> "${summary_file}"
echo "[INFO] No ${alert_prefix}* alerts were firing during the release rollover"
exit 0
fi

{
echo "| Phase | Alert | Severity | First seen | Summary |"
echo "|---|---|---|---|---|"
jq -r "${oneline}"' .[] | "| \(.phase) | \(.name) | \(.severityLevel) | \(.firstSeen) | \(.summary | oneline | gsub("\\|"; "\\|")) |"' <<< "${alerts}"
echo
echo "<details><summary>Alert details</summary>"
echo
jq -r '.[] | "#### \(.name) — \(.phase)\n\n- severity level: \(.severityLevel)\n- labels: `\(.labels | tojson)`\n\n\(.description)\n"' <<< "${alerts}"
echo "</details>"
} >> "${summary_file}"

# Annotations put the alerts on top of the run page, not only in the summary.
jq -r "${oneline}"' .[] | "::warning title=ClusterAlert \(.name)::[\(.phase)] \(.summary | oneline)"' <<< "${alerts}"

echo "[INFO] Firing alerts:"
jq -r '.[] | " [\(.phase)] \(.name) (severity \(.severityLevel))"' <<< "${alerts}"

if [ "${fail_on_alerts}" != "true" ]; then
echo "[INFO] FAIL_ON_ALERTS is not 'true', not failing the job"
exit 0
fi

# Failing here is what paints this job red; the job itself is
# continue-on-error, so the workflow conclusion stays successful.
echo "[ERROR] ${count} ClusterAlert(s) were firing in the nested cluster, see the job summary" >&2
trap - ERR
exit 1
39 changes: 39 additions & 0 deletions .github/scripts/bash/e2e/signal-clusteralerts-watch-stop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash

# Copyright 2026 Flant JSC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Tells the ClusterAlerts watch that the pipeline is over. The marker is a
# ConfigMap in the nested cluster because the watch runs on its own runner and
# runners share no filesystem.

set -Eeuo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=.github/scripts/bash/e2e/common.sh
source "${SCRIPT_DIR}/common.sh"

stop_namespace="${WATCH_STOP_NAMESPACE:-default}"
stop_configmap="${WATCH_STOP_CONFIGMAP:-e2e-clusteralerts-watch-stop}"

echo "[INFO] Creating stop marker: configmap ${stop_configmap} in namespace ${stop_namespace}"

# Never fail the pipeline over the marker: if it cannot be created, the watch
# ends on its own timeout instead.
if kubectl -n "${stop_namespace}" create configmap "${stop_configmap}" \
--from-literal=run_id="${GITHUB_RUN_ID:-unknown}"; then
echo "[INFO] Stop marker created"
else
echo "[WARN] Failed to create the stop marker, the watch will end on its own timeout"
fi
3 changes: 1 addition & 2 deletions .github/scripts/bash/e2e/verify-image-digests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ required_env_value() {
new_release="$(required_env_value NEW_RELEASE)"
dev_module_source="$(required_env_value DEV_MODULE_SOURCE)"

MODULE_IMAGE="${dev_module_source}/virtualization:${new_release}"
echo "[INFO] Extracting images_digests.json from virtualization:${new_release}"
images_hash="$(crane export "${MODULE_IMAGE}" - | tar -Oxf - images_digests.json)"
images_hash="$(module_images_digests "${dev_module_source}" "${new_release}")"
echo "[INFO] Expected image digests:"
echo "::group::images_digests.json"
echo "${images_hash}" | jq .
Expand Down
37 changes: 37 additions & 0 deletions .github/scripts/bash/e2e/wait-vmops-migration-terminal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,43 @@ release_namespace="$(required_env_value RELEASE_NAMESPACE)"

sleep_interval="${SLEEP_INTERVAL:-10}"
timeout_seconds="${TIMEOUT_SECONDS:-1200}"
# Virtual machines are only moved when the workload images change: a new
# virt-handler drains the VMs off its node, a new virt-launcher makes the
# workload updater migrate the running ones. Releases that leave both untouched
# never trigger a migration, so there would be nothing to wait for.
migration_expected() {
local module_source="${DEV_MODULE_SOURCE:-}"
local current="${CURRENT_RELEASE:-}"
local new="${NEW_RELEASE:-}"
local current_digests new_digests image

if [ -z "${module_source}" ] || [ -z "${current}" ] || [ -z "${new}" ]; then
echo "[WARN] DEV_MODULE_SOURCE, CURRENT_RELEASE or NEW_RELEASE is not set, cannot tell whether the upgrade migrates VMs; waiting anyway"
return 0
fi

if ! current_digests="$(module_images_digests "${module_source}" "${current}")" ||
! new_digests="$(module_images_digests "${module_source}" "${new}")"; then
echo "[WARN] Failed to read the image digests of ${current} or ${new}, cannot tell whether the upgrade migrates VMs; waiting anyway"
return 0
fi

for image in virtHandler virtLauncher; do
if [ "$(jq -r --arg i "${image}" '.[$i] // ""' <<< "${current_digests}")" \
!= "$(jq -r --arg i "${image}" '.[$i] // ""' <<< "${new_digests}")" ]; then
echo "[INFO] The ${image} image differs between ${current} and ${new}, virtual machines will be migrated"
return 0
fi
done

return 1
}

if ! migration_expected; then
echo "[INFO] ${CURRENT_RELEASE} and ${NEW_RELEASE} ship the same virt-handler and virt-launcher: the upgrade does not migrate virtual machines, nothing to wait for"
exit 0
fi

deadline=$(( $(date +%s) + timeout_seconds ))

# The number of Running VMs at the start is the number of Evict VMOPs we
Expand Down
Loading