Skip to content
Open
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
13 changes: 12 additions & 1 deletion Makefile.core.mk
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ OPM ?= $(LOCALBIN)/opm
ISTIOCTL ?= $(LOCALBIN)/istioctl
RUNME ?= $(LOCALBIN)/runme
MISSPELL ?= $(LOCALBIN)/misspell
CRD_SCHEMA_CHECKER ?= $(LOCALBIN)/crd-schema-checker

## Tool Versions
OPERATOR_SDK_VERSION ?= v1.41.1
Expand All @@ -562,6 +563,7 @@ GITLEAKS_VERSION ?= v8.28.0
ISTIOCTL_VERSION ?= 1.26.2
RUNME_VERSION ?= 3.15.4
MISSPELL_VERSION ?= v0.3.4
CRD_SCHEMA_CHECKER_VERSION ?= release-4.22

.PHONY: helm $(HELM)
helm: $(HELM) ## Download helm to bin directory. If wrong version is installed, it will be overwritten.
Expand Down Expand Up @@ -776,8 +778,17 @@ lint-spell: misspell
misspell: $(LOCALBIN) ## Download misspell to bin directory.
@test -s $(LOCALBIN)/misspell || GOBIN=$(LOCALBIN) go install github.com/client9/misspell/cmd/misspell@$(MISSPELL_VERSION)

.PHONY: crd-schema-checker
crd-schema-checker: $(CRD_SCHEMA_CHECKER) ## Download crd-schema-checker to bin directory.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why even have the phony target?

It seems to me lint-crds can just depend on $(CRD_SCHEMA_CHECKER) directly

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically yes, but that's how we're doing it for all other tools as well

$(CRD_SCHEMA_CHECKER): $(LOCALBIN)
@test -x $(LOCALBIN)/crd-schema-checker || GOBIN=$(LOCALBIN) GO111MODULE=on go install github.com/openshift/crd-schema-checker/cmd/crd-schema-checker@$(CRD_SCHEMA_CHECKER_VERSION) > /dev/stderr

.PHONY: lint-crds
lint-crds: crd-schema-checker ## Lint CRDs for backwards compatibility on release branches.
@PREVIOUS_VERSION=$(PREVIOUS_VERSION) ./tools/crd-schema-checker.sh

.PHONY: lint
lint: lint-scripts lint-licenses lint-copyright-banner lint-go lint-yaml lint-helm lint-bundle lint-watches lint-secrets lint-spell ## Run all linters.
lint: lint-scripts lint-licenses lint-copyright-banner lint-go lint-yaml lint-helm lint-bundle lint-watches lint-secrets lint-spell lint-crds ## Run all linters.

.PHONY: format
format: format-go tidy-go ## Auto-format all code. This should be run before sending a PR.
Expand Down
197 changes: 197 additions & 0 deletions tools/crd-schema-checker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/bin/bash

# Copyright Istio Authors
#
# 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.

# CRD compatibility checker for release branches.
# Compares CRDs between the current release branch and the previous one to catch breaking changes.
# Uses OpenShift's crd-schema-checker to detect issues like removed fields or stricter validation.
# Run 'make crd-schema-checker' first to install the dependency, then 'make lint-crds' to check.

set -euo pipefail

CHECKED_CRDS=0 ERRORS=0 STABLE_ERRORS=0 WARNINGS=0 INFOS=0

# TODO: fix the SSA tags on lists and enable the validator
DISABLED_VALIDATORS="NoBools,NoMaps,ListsMustHaveSSATags"

# Get the latest version from a CRD
getLatestCRDVersion() {
command -v yq &>/dev/null || { echo "unknown"; return; }
yq eval '.spec.versions[-1].name' "$1" 2>/dev/null || echo "unknown"
}

# Check if version is stable (not alpha/beta)
isStableVersion() {
[[ "$1" =~ ^v[0-9]+(\.[0-9]+)*$ ]]
}

# Output result with version info
output_result() {
local crd_name="$1" version="$2" output="$3"
local errors=0 warnings=0 infos=0
echo "$crd_name ($version)"
if [ -n "${output}" ]; then
while read -r line; do
if echo "${line}" | grep -iq "ERROR:"; then
errors=$((errors + 1))
elif echo "${line}" | grep -iq "Warning:"; then
warnings=$((warnings + 1))
elif echo "${line}" | grep -iq "info:"; then
infos=$((infos + 1))
fi
echo " - $line"
done <<< "$output"
fi
echo "--> ${errors} errors, ${warnings} warnings, ${infos} infos"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it pertinent to count the errors/warn/info here?

Wouldn't it make more sense to push a PR to print that in the original command anyway?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah we need to count, if only to separate them by stable vs. non-stable APIs- we can allow breaking changes in v1alpha1 CRDs, but never in v1 CRDs, for example

if isStableVersion "${version}"; then
STABLE_ERRORS=$((STABLE_ERRORS + errors))
fi
ERRORS=$((ERRORS + errors))
WARNINGS=$((WARNINGS + warnings))
INFOS=$((INFOS + infos))
}

if [[ -x "$(pwd)/bin/crd-schema-checker" ]]; then
CRD_SCHEMA_CHECKER="$(pwd)/bin/crd-schema-checker"
elif command -v crd-schema-checker &>/dev/null; then
CRD_SCHEMA_CHECKER="crd-schema-checker"
else
echo "ERROR: crd-schema-checker not found. Run 'make crd-schema-checker'"
exit 1
fi

repo_url="https://github.com/istio-ecosystem/sail-operator.git"
[[ -n "${PROW_JOB_ID:-}" && -n "${REPO_OWNER:-}" && -n "${REPO_NAME:-}" ]] &&
repo_url="https://github.com/${REPO_OWNER}/${REPO_NAME}.git"

temp_dir=$(mktemp -d)
trap 'rm -rf "$temp_dir"' EXIT
local_repo_path=$(pwd)
git clone "$repo_url" "$temp_dir/repo"

# Determine branches to compare
current_branch=$(git rev-parse --abbrev-ref HEAD)

pushd "$temp_dir/repo" >/dev/null

git fetch origin '+refs/heads/release-*:refs/remotes/origin/release-*' || true

if [[ "$current_branch" =~ ^release-[0-9]+\.[0-9]+$ ]]; then
# we're on a release branch. find the previous release branch
previous_branch=$(git branch -r | grep -E 'origin/release-[0-9]+\.[0-9]+$' |
sed 's|.*origin/||' | sort -V |
awk -v target="$current_branch" '$0 == target { print prev; exit } { prev = $0 }')
elif [[ -n "${PREVIOUS_VERSION:-}" ]]; then
previous_branch="release-$(echo "${PREVIOUS_VERSION}" | cut -f1,2 -d'.')"
else
echo "Not on a release branch and PREVIOUS_VERSION not set. Skipping."
exit 0
fi

if [[ -z "$previous_branch" ]]; then
echo "ERROR: No previous release branch found for $current_branch"
exit 1
fi

git checkout "$previous_branch"
popd >/dev/null

echo "Checking CRD compatibility: $previous_branch -> $current_branch"

# Extract CRDs from repo_dir $1's, copy to output_dir $2
extract_crds() {
local repo_dir="$1" output_dir="$2"
mkdir -p "$output_dir"
pushd "$repo_dir" > /dev/null
local files
mapfile -t files < <(ls bundle/manifests/sailoperator.io*.yaml)

for filepath in "${files[@]}"; do
[[ -z "$filepath" ]] && continue
content=$(cat "$filepath")
if [[ "$content" == *"kind: CustomResourceDefinition"* ]]; then
file=$(basename "$filepath")
echo "$content" > "$output_dir/$file"
echo "$file"
fi
done
popd > /dev/null
}
mapfile -t previous_crds < <(extract_crds "$temp_dir/repo" "$temp_dir/prev")
echo Found "${#previous_crds[@]}" CRDs in "$previous_branch": "${previous_crds[@]}"
mapfile -t current_crds < <(extract_crds "$local_repo_path" "$temp_dir/curr")
echo Found "${#current_crds[@]}" CRDs in "$current_branch": "${current_crds[@]}"

# Create lookup maps
declare -A current_crd_map previous_crd_map
for crd in "${current_crds[@]}"; do
current_crd_map["$crd"]="$temp_dir/curr/${crd}"
done
for crd in "${previous_crds[@]}"; do
previous_crd_map["$crd"]="$temp_dir/prev/${crd}"
done

echo "Comparing CRDs..."

# Check existing CRDs for breaking changes
for crd in "${previous_crds[@]}"; do
if [[ -n "${current_crd_map[$crd]:-}" ]]; then
set +e
output=$($CRD_SCHEMA_CHECKER check-manifests \
--disabled-validators=${DISABLED_VALIDATORS} \
--existing-crd-filename="${previous_crd_map[$crd]}" \
--new-crd-filename="${current_crd_map[$crd]}" 2>&1)
set -e

version=$(getLatestCRDVersion "${current_crd_map[$crd]}")
output_result "${crd}" "${version}" "${output}"
CHECKED_CRDS=$((CHECKED_CRDS + 1))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
CHECKED_CRDS=$((CHECKED_CRDS + 1))
((CHECKED_CRDS++))

Copy link
Collaborator Author

@dgn dgn Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand there is no practical difference between the two. I noticed your comment fixed - did you forget to update the PR?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, that was a mistake. I tried this and it didn't work I think

else
# CRD was removed
version=$(getLatestCRDVersion "${previous_crd_map[$crd]}")
if ! isStableVersion "$version"; then
echo "WARNING: CRD $crd was removed ($version)"
WARNINGS=$((WARNINGS + 1))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: In case you want to make the same change here as well as in Line 166 below.

else
echo "ERROR: CRD $crd was removed (${version})"
ERRORS=$((ERRORS + 1))
fi
fi
done

# Check for new CRDs
for crd in "${current_crds[@]}"; do
[[ -n "${previous_crd_map[$crd]:-}" ]] && continue
echo "INFO: New CRD added: $crd"
set +e
output=$($CRD_SCHEMA_CHECKER check-manifests \
--disabled-validators=${DISABLED_VALIDATORS} \
--new-crd-filename="${current_crd_map[$crd]}" 2>&1)
set -e
version=$(getLatestCRDVersion "${current_crd_map[$crd]}")
output_result "${crd}" "${version}" "${output}"
((CHECKED_CRDS++))
done

echo
echo "=== Results ==="
echo "Checked $CHECKED_CRDS CRDs: $ERRORS errors ($STABLE_ERRORS errors in stable APIs), $WARNINGS warnings, $INFOS infos"

if [[ $STABLE_ERRORS -gt 0 ]]; then
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally to me parsing the output seems like the wrong approach.
We have the exit code of the checks, we should rely on those instead of trying to read and interpret the output.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, we need to parse to get detailed results - warnings we can always ignore, for pre-release CRDs we can even ignore errors. yes, technically we could do all that based on just exit codes but we wouldn't be able to produce detailed counts

echo "FAILED: Breaking changes detected"
exit 1
else
echo "PASSED: No breaking changes"
fi