kata-containers/tests/static-checks.sh
Ruoqing He d23284a0dc header_check: Check header for changed text files
We are running `header_check` for non-text files like binary files,
symbolic link files, image files (pictures) and etc., which does not
make sense.

Filter out non-text files and run `header_check` only for text files
changed.

Signed-off-by: Ruoqing He <heruoqing@iscas.ac.cn>
2025-02-18 22:39:53 +08:00

1566 lines
39 KiB
Bash
Executable File

#!/usr/bin/env bash
# Copyright (c) 2017-2023 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
# Description: Central script to run all static checks.
# This script should be called by all other repositories to ensure
# there is only a single source of all static checks.
set -e
[ -n "$DEBUG" ] && set -x
cidir=$(realpath $(dirname "$0"))
source "${cidir}/common.bash"
# By default in Golang >= 1.16 GO111MODULE is set to "on",
# some subprojects in this repo may not support "go modules",
# set GO111MODULE to "auto" to enable module-aware mode only when
# a go.mod file is present in the current directory.
export GO111MODULE="auto"
export test_path="${test_path:-github.com/kata-containers/kata-containers/tests}"
export test_dir="${GOPATH}/src/${test_path}"
# List of files to delete on exit
files_to_remove=()
script_name=${0##*/}
# Static check functions must follow the following naming conventions:
#
# All static check function names must match this pattern.
typeset -r check_func_regex="^static_check_"
# All architecture-specific static check functions must match this pattern.
typeset -r arch_func_regex="_arch_specific$"
repo=""
repo_path=""
specific_branch="false"
force="false"
branch=${branch:-main}
# Which static check functions to consider.
handle_funcs="all"
single_func_only="false"
list_only="false"
# number of seconds to wait for curl to check a URL
typeset url_check_timeout_secs="${url_check_timeout_secs:-60}"
# number of attempts that will be made to check an individual URL.
typeset url_check_max_tries="${url_check_max_tries:-3}"
typeset -A long_options
# Generated code
ignore_clh_generated_code="virtcontainers/pkg/cloud-hypervisor/client"
paths_to_skip=(
"${ignore_clh_generated_code}"
"vendor"
)
# Skip paths that are not statically checked
# $1 : List of paths to check, space separated list
# If you have a list in a bash array call in this way:
# list=$(skip_paths "${list[@]}")
# If you still want to use it as an array do:
# list=(${list})
skip_paths(){
local list_param="${1}"
[ -z "$list_param" ] && return
local list=(${list_param})
for p in "${paths_to_skip[@]}"; do
new_list=()
for l in "${list[@]}"; do
if echo "${l}" | grep -qv "${p}"; then
new_list=("${new_list[@]}" "${l}")
fi
done
list=("${new_list[@]}")
done
echo "${list[@]}"
}
long_options=(
[all]="Force checking of all changes, including files in the base branch"
[branch]="Specify upstream branch to compare against (default '$branch')"
[docs]="Check document files"
[dockerfiles]="Check dockerfiles"
[files]="Check files"
[force]="Force a skipped test to run"
[golang]="Check '.go' files"
[help]="Display usage statement"
[json]="Check JSON files"
[labels]="Check labels databases"
[licenses]="Check licenses"
[list]="List tests that would run"
[no-arch]="Run/list all tests except architecture-specific ones"
[only-arch]="Only run/list architecture-specific tests"
[repo:]="Specify GitHub URL of repo to use (github.com/user/repo)"
[scripts]="Check script files"
[vendor]="Check vendor files"
[versions]="Check versions files"
[xml]="Check XML files"
)
yamllint_cmd="yamllint"
have_yamllint_cmd=$(command -v "$yamllint_cmd" || true)
chronic=chronic
# Disable chronic on OSX to avoid having to update the Travis config files
# for additional packages on that platform.
[ "$(uname -s)" == "Darwin" ] && chronic=
usage()
{
cat <<EOF
Usage: $script_name help
$script_name [options] repo-name [true]
Options:
EOF
local option
local description
local long_option_names="${!long_options[@]}"
# Sort space-separated list by converting to newline separated list
# and back again.
long_option_names=$(echo "$long_option_names"|tr ' ' '\n'|sort|tr '\n' ' ')
# Display long options
for option in ${long_option_names}
do
description=${long_options[$option]}
# Remove any trailing colon which is for getopt(1) alone.
option=$(echo "$option"|sed 's/:$//g')
printf " --%-10.10s # %s\n" "$option" "$description"
done
cat <<EOF
Parameters:
help : Show usage.
repo-name : GitHub URL of repo to check in form "github.com/user/repo"
(equivalent to "--repo \$URL").
true : Specify as "true" if testing a specific branch, else assume a
PR branch (equivalent to "--all").
Notes:
- If no options are specified, all non-skipped tests will be run.
- Some required tools may be installed in \$GOPATH/bin, so you should ensure
that it is in your \$PATH.
Examples:
- Run all tests on a specific branch (stable or main) of kata-containers repo:
$ $script_name github.com/kata-containers/kata-containers true
- Auto-detect repository and run golang tests for current repository:
$ KATA_DEV_MODE=true $script_name --golang
- Run all tests on the kata-containers repository, forcing the tests to
consider all files, not just those changed by a PR branch:
$ $script_name github.com/kata-containers/kata-containers --all
EOF
}
# Calls die() if the specified function is not valid.
func_is_valid() {
local name="$1"
type -t "$name" &>/dev/null || die "function '$name' does not exist"
}
# Calls die() if the specified function is not valid or not a check function.
ensure_func_is_check_func() {
local name="$1"
func_is_valid "$name"
{ echo "$name" | grep -q "${check_func_regex}"; ret=$?; }
[ "$ret" = 0 ] || die "function '$name' is not a check function"
}
# Returns "yes" if the specified function needs to run on all architectures,
# else "no".
func_is_arch_specific() {
local name="$1"
ensure_func_is_check_func "$name"
{ echo "$name" | grep -q "${arch_func_regex}"; ret=$?; }
if [ "$ret" = 0 ]; then
echo "yes"
else
echo "no"
fi
}
function remove_tmp_files() {
rm -rf "${files_to_remove[@]}"
}
# Convert a golang package to a full path
pkg_to_path()
{
local pkg="$1"
go list -f '{{.Dir}}' "$pkg"
}
# Check that chronic is installed, otherwise die.
need_chronic() {
local first_word
[ -z "$chronic" ] && return
first_word="${chronic%% *}"
command -v chronic &>/dev/null || \
die "chronic command not found. You must have it installed to run this check." \
"Usually it is distributed with the 'moreutils' package of your Linux distribution."
}
static_check_go_arch_specific()
{
local go_packages
local submodule_packages
local all_packages
pushd $repo_path
# List of all golang packages found in all submodules
#
# These will be ignored: since they are references to other
# repositories, we assume they are tested independently in their
# repository so do not need to be re-tested here.
submodule_packages=$(mktemp)
git submodule -q foreach "go list ./..." | sort > "$submodule_packages" || true
# all packages
all_packages=$(mktemp)
go list ./... | sort > "$all_packages" || true
files_to_remove+=("$submodule_packages" "$all_packages")
# List of packages to consider which is defined as:
#
# "all packages" - "submodule packages"
#
# Note: the vendor filtering is required for versions of go older than 1.9
go_packages=$(comm -3 "$all_packages" "$submodule_packages" || true)
go_packages=$(skip_paths "${go_packages[@]}")
# No packages to test
[ -z "$go_packages" ] && popd && return
local linter="golangci-lint"
# Run golang checks
if [ ! "$(command -v $linter)" ]
then
info "Installing ${linter}"
local linter_url=$(get_test_version "languages.golangci-lint.url")
local linter_version=$(get_test_version "languages.golangci-lint.version")
info "Forcing ${linter} version ${linter_version}"
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin "v${linter_version}"
command -v $linter &>/dev/null || \
die "$linter command not found. Ensure that \"\$GOPATH/bin\" is in your \$PATH."
fi
local linter_args="run -c ${cidir}/.golangci.yml"
# Non-option arguments other than "./..." are
# considered to be directories by $linter, not package names.
# Hence, we need to obtain a list of package directories to check,
# excluding any that relate to submodules.
local dirs
for pkg in $go_packages
do
path=$(pkg_to_path "$pkg")
makefile="${path}/Makefile"
# perform a basic build since some repos generate code which
# is required for the package to be buildable (and thus
# checkable).
[ -f "$makefile" ] && (cd "$path" && make)
dirs+=" $path"
done
info "Running $linter checks on the following packages:\n"
echo "$go_packages"
echo
info "Package paths:\n"
echo "$dirs" | sed 's/^ *//g' | tr ' ' '\n'
for d in ${dirs};do
info "Running $linter on $d"
(cd $d && GO111MODULE=auto eval "$linter" "${linter_args}" ".")
done
popd
}
# Install yamllint in the different Linux distributions
install_yamllint()
{
package="yamllint"
case "$ID" in
centos|rhel) sudo yum -y install $package ;;
ubuntu) sudo apt-get -y install $package ;;
fedora) sudo dnf -y install $package ;;
*) die "Please install yamllint on $ID" ;;
esac
have_yamllint_cmd=$(command -v "$yamllint_cmd" || true)
if [ -z "$have_yamllint_cmd" ]; then
info "Cannot install $package" && return
fi
}
# Check the "versions database".
#
# Some repositories use a versions database to maintain version information
# about non-golang dependencies. If found, check it for validity.
static_check_versions()
{
local db="versions.yaml"
if [ -z "$have_yamllint_cmd" ]; then
info "Installing yamllint"
install_yamllint
fi
pushd $repo_path
[ ! -e "$db" ] && popd && return
if [ -n "$have_yamllint_cmd" ]; then
eval "$yamllint_cmd" "$db"
else
info "Cannot check versions as $yamllint_cmd not available"
fi
popd
}
static_check_labels()
{
[ $(uname -s) != Linux ] && info "Can only check labels under Linux" && return
# Handle SLES which doesn't provide the required command.
[ -z "$have_yamllint_cmd" ] && info "Cannot check labels as $yamllint_cmd not available" && return
# Since this script is called from another repositories directory,
# ensure the utility is built before the script below (which uses it) is run.
(cd "${test_dir}/cmd/github-labels" && make)
tmp=$(mktemp)
files_to_remove+=("${tmp}")
info "Checking labels for repo ${repo} using temporary combined database ${tmp}"
bash -f "${test_dir}/cmd/github-labels/github-labels.sh" "generate" "${repo}" "${tmp}"
}
# Ensure all files (where possible) contain an SPDX license header
static_check_license_headers()
{
# The branch is the baseline - ignore it.
[ "$specific_branch" = "true" ] && return
# See: https://spdx.org/licenses/Apache-2.0.html
local -r spdx_tag="SPDX-License-Identifier"
local -r spdx_license="Apache-2.0"
local -r license_pattern="${spdx_tag}: ${spdx_license}"
local -r copyright_pattern="Copyright"
local header_checks=()
header_checks+=("SPDX license header::${license_pattern}")
header_checks+=("Copyright header:-i:${copyright_pattern}")
pushd $repo_path
files=$(get_pr_changed_file_details || true)
# Strip off status and convert to array
files=($(echo "$files"|awk '{print $NF}'))
text_files=()
# Filter out non-text files
for file in "${files[@]}"; do
if [[ -f "$file" ]] && file --mime-type "$file" | grep -q "text/"; then
text_files+=("$file")
else
info "Ignoring non-text file: $file"
fi
done
files="${text_files[*]}"
# no text files were changed
[ -z "$files" ] && info "No files found" && popd && return
local header_check
for header_check in "${header_checks[@]}"
do
local desc=$(echo "$header_check"|cut -d: -f1)
local extra_args=$(echo "$header_check"|cut -d: -f2)
local pattern=$(echo "$header_check"|cut -d: -f3-)
info "Checking $desc"
local missing=$(grep \
--exclude=".git/*" \
--exclude=".gitignore" \
--exclude=".dockerignore" \
--exclude="Gopkg.lock" \
--exclude="*.gpl.c" \
--exclude="*.ipynb" \
--exclude="*.jpg" \
--exclude="*.json" \
--exclude="LICENSE*" \
--exclude="THIRD-PARTY" \
--exclude="*.md" \
--exclude="*.pb.go" \
--exclude="*pb_test.go" \
--exclude="*.bin" \
--exclude="*.png" \
--exclude="*.pub" \
--exclude="*.service" \
--exclude="*.svg" \
--exclude="*.drawio" \
--exclude="*.toml" \
--exclude="*.txt" \
--exclude="*.dtd" \
--exclude="vendor/*" \
--exclude="VERSION" \
--exclude="kata_config_version" \
--exclude="tools/packaging/kernel/configs/*" \
--exclude="virtcontainers/pkg/firecracker/*" \
--exclude="${ignore_clh_generated_code}*" \
--exclude="*.xml" \
--exclude="*.yaml" \
--exclude="*.yml" \
--exclude="go.mod" \
--exclude="go.sum" \
--exclude="*.lock" \
--exclude="grpc-rs/*" \
--exclude="target/*" \
--exclude="*.patch" \
--exclude="*.diff" \
--exclude="tools/packaging/static-build/qemu.blacklist" \
--exclude="tools/packaging/qemu/default-configs/*" \
--exclude="src/libs/protocols/protos/gogo/*.proto" \
--exclude="src/libs/protocols/protos/google/*.proto" \
--exclude="src/libs/protocols/protos/cri-api/api.proto" \
--exclude="src/mem-agent/example/protocols/protos/google/protobuf/*.proto" \
--exclude="src/libs/*/test/texture/*" \
--exclude="*.dic" \
-EL $extra_args -E "\<${pattern}\>" \
$files || true)
if [ -n "$missing" ]; then
cat >&2 <<-EOF
ERROR: Required $desc check ('$pattern') failed for the following files:
$missing
EOF
exit 1
fi
done
popd
}
run_url_check_cmd()
{
local url="${1:-}"
[ -n "$url" ] || die "need URL"
local out_file="${2:-}"
[ -n "$out_file" ] || die "need output file"
# Can be blank
local extra_args="${3:-}"
local curl_extra_args=()
curl_extra_args+=("$extra_args")
# Authenticate for github to increase threshold for rate limiting
if [[ "$url" =~ github\.com && -n "$GITHUB_USER" && -n "$GITHUB_TOKEN" ]]; then
curl_extra_args+=("-u ${GITHUB_USER}:${GITHUB_TOKEN}")
fi
# Some endpoints return 403 to HEAD but 200 for GET,
# so perform a GET but only read headers.
curl \
${curl_extra_args[*]} \
-sIL \
-X GET \
-c - \
-H "Accept-Encoding: zstd, none, gzip, deflate" \
--max-time "$url_check_timeout_secs" \
--retry "$url_check_max_tries" \
"$url" \
&>"$out_file"
}
check_url()
{
local url="${1:-}"
[ -n "$url" ] || die "need URL to check"
local invalid_urls_dir="${2:-}"
[ -n "$invalid_urls_dir" ] || die "need invalid URLs directory"
local curl_out
curl_out=$(mktemp)
files_to_remove+=("${curl_out}")
# Process specific file to avoid out-of-order writes
local invalid_file
invalid_file=$(printf "%s/%d" "$invalid_urls_dir" "$$")
local ret
local -a errors=()
local -a user_agents=()
# Test an unspecified UA (curl default)
user_agents+=('')
# Test an explictly blank UA
user_agents+=('""')
# Single space
user_agents+=(' ')
# CLI HTTP tools
user_agents+=('Wget')
user_agents+=('curl')
# console based browsers
# Hopefully, these will always be supported for a11y.
user_agents+=('Lynx')
user_agents+=('Elinks')
# Emacs' w3m browser
user_agents+=('Emacs')
# The full craziness
user_agents+=('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36')
local user_agent
# Cycle through the user agents until we find one that works.
#
# Note that we also test an unspecified user agent
# (no '-A <value>').
for user_agent in "${user_agents[@]}"
do
info "Checking URL $url with User Agent '$user_agent'"
local curl_ua_args
[ -n "$user_agent" ] && curl_ua_args="-A '$user_agent'"
{ run_url_check_cmd "$url" "$curl_out" "$curl_ua_args"; ret=$?; } || true
# A transitory error, or the URL is incorrect,
# but capture either way.
if [ "$ret" -ne 0 ]; then
errors+=("Failed to check URL '$url' (user agent: '$user_agent', return code $ret)")
# Try again with another UA since it appears that some return codes
# indicate the server was unhappy with the details
# presented by the client.
continue
fi
local http_statuses
http_statuses=$(grep -E "^HTTP" "$curl_out" |\
awk '{print $2}' || true)
if [ -z "$http_statuses" ]; then
errors+=("no HTTP status codes for URL '$url' (user agent: '$user_agent')")
continue
fi
local status
local -i fail_count=0
# Check all HTTP status codes
for status in $http_statuses
do
# Ignore the following ranges of status codes:
#
# - 1xx: Informational codes.
# - 2xx: Success codes.
# - 3xx: Redirection codes.
# - 405: Specifically to handle some sites
# which get upset by "curl -L" when the
# redirection is not required.
#
# Anything else is considered an error.
#
# See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
{ grep -qE "^(1[0-9][0-9]|2[0-9][0-9]|3[0-9][0-9]|405)" <<< "$status"; ret=$?; } || true
[ "$ret" -eq 0 ] && continue
fail_count+=1
done
# If we didn't receive any unexpected HTTP status codes for
# this UA, the URL is valid so we don't need to check with any
# further UAs, so clear any (transitory) errors we've
# recorded.
[ "$fail_count" -eq 0 ] && errors=() && break
echo "$url" >> "$invalid_file"
errors+=("found HTTP error status codes for URL $url (status: '$status', user agent: '$user_agent')")
done
[ "${#errors}" = 0 ] && return 0
die "failed to check URL '$url': errors: '${errors[*]}'"
}
# Perform basic checks on documentation files
static_check_docs()
{
local cmd="xurls"
pushd $repo_path
if [ ! "$(command -v $cmd)" ]
then
info "Installing $cmd utility"
local version
local url
version=$(get_test_version "externals.xurls.version")
url=$(get_test_version "externals.xurls.url")
# xurls is very fussy about how it's built.
go install "${url}@${version}"
command -v xurls &>/dev/null ||
die 'xurls not found. Ensure that "$GOPATH/bin" is in your $PATH'
fi
info "Checking documentation"
local doc
local all_docs
local docs
local docs_status
local new_docs
local new_urls
local url
pushd $repo_path
all_docs=$(git ls-files "*.md" | grep -Ev "(grpc-rs|target)/" | sort || true)
all_docs=$(skip_paths "${all_docs[@]}")
if [ "$specific_branch" = "true" ]
then
info "Checking all documents in $branch branch"
docs="$all_docs"
else
info "Checking local branch for changed documents only"
docs_status=$(get_pr_changed_file_details || true)
docs_status=$(echo "$docs_status" | grep "\.md$" || true)
docs=$(echo "$docs_status" | awk '{print $NF}' | sort)
docs=$(skip_paths "${docs[@]}")
# Newly-added docs
new_docs=$(echo "$docs_status" | awk '/^A/ {print $NF}' | sort)
new_docs=$(skip_paths "${new_docs[@]}")
for doc in $new_docs
do
# A new document file has been added. If that new doc
# file is referenced by any files on this PR, checking
# its URL will fail since the PR hasn't been merged
# yet. We could construct the URL based on the users
# original PR branch and validate that. But it's
# simpler to just construct the URL that the "pending
# document" *will* result in when the PR has landed
# and then check docs for that new URL and exclude
# them from the real URL check.
url="https://${repo}/blob/${branch}/${doc}"
new_urls+=" ${url}"
done
fi
[ -z "$docs" ] && info "No documentation to check" && return
local urls
local url_map=$(mktemp)
local invalid_urls=$(mktemp)
local md_links=$(mktemp)
files_to_remove+=("${url_map}" "${invalid_urls}" "${md_links}")
info "Checking document markdown references"
local md_docs_to_check
# All markdown docs are checked (not just those changed by a PR). This
# is necessary to guarantee that all docs are referenced.
md_docs_to_check="$all_docs"
command -v kata-check-markdown &>/dev/null ||\
(cd "${test_dir}/cmd/check-markdown" && make)
command -v kata-check-markdown &>/dev/null || \
die 'kata-check-markdown command not found. Ensure that "$GOPATH/bin" is in your $PATH.'
for doc in $md_docs_to_check
do
kata-check-markdown check "$doc"
# Get a link of all other markdown files this doc references
kata-check-markdown list links --format tsv --no-header "$doc" |\
grep "external-link" |\
awk '{print $3}' |\
sort -u >> "$md_links"
done
# clean the list of links
local tmp
tmp=$(mktemp)
sort -u "$md_links" > "$tmp"
mv "$tmp" "$md_links"
# A list of markdown files that do not have to be referenced by any
# other markdown file.
exclude_doc_regexs+=()
exclude_doc_regexs+=(^CODE_OF_CONDUCT\.md$)
exclude_doc_regexs+=(^CONTRIBUTING\.md$)
# Magic github template files
exclude_doc_regexs+=(^\.github/.*\.md$)
# The top level README doesn't need to be referenced by any other
# since it displayed by default when visiting the repo.
exclude_doc_regexs+=(^README\.md$)
# Exclude READMEs for test integration
exclude_doc_regexs+=(^\tests/cmd/.*/README\.md$)
local exclude_pattern
# Convert the list of files into an grep(1) alternation pattern.
exclude_pattern=$(echo "${exclude_doc_regexs[@]}"|sed 's, ,|,g')
# Every document in the repo (except a small handful of exceptions)
# should be referenced by another document.
for doc in $md_docs_to_check
do
# Check the ignore list for markdown files that do not need to
# be referenced by others.
echo "$doc"|grep -q -E "(${exclude_pattern})" && continue
grep -q "$doc" "$md_links" || die "Document $doc is not referenced"
done
info "Checking document code blocks"
local doc_to_script_cmd="${cidir}/kata-doc-to-script.sh"
for doc in $docs
do
bash "${doc_to_script_cmd}" -csv "$doc"
# Look for URLs in the document
urls=$("${doc_to_script_cmd}" -i "$doc" - | "$cmd")
# Gather URLs
for url in $urls
do
printf "%s\t%s\n" "${url}" "${doc}" >> "$url_map"
done
done
# Get unique list of URLs
urls=$(awk '{print $1}' "$url_map" | sort -u)
info "Checking all document URLs"
local invalid_urls_dir=$(mktemp -d)
files_to_remove+=("${invalid_urls_dir}")
for url in $urls
do
if [ "$specific_branch" != "true" ]
then
# If the URL is new on this PR, it cannot be checked.
echo "$new_urls" | grep -q -E "\<${url}\>" && \
info "ignoring new (but correct) URL: $url" && continue
fi
# Ignore local URLs. The only time these are used is in
# examples (meaning these URLs won't exist).
echo "$url" | grep -q "^file://" && continue
echo "$url" | grep -q "^http://localhost" && continue
# Ignore the install guide URLs that contain a shell variable
echo "$url" | grep -q "\\$" && continue
# This prefix requires the client to be logged in to github, so ignore
echo "$url" | grep -q 'https://github.com/pulls' && continue
# Sigh.
echo "$url"|grep -q 'https://example.com' && continue
# Google APIs typically require an auth token.
echo "$url"|grep -q 'https://www.googleapis.com' && continue
# Git repo URL check
if echo "$url"|grep -q '^https.*git'
then
timeout "${KATA_NET_TIMEOUT}" git ls-remote "$url" > /dev/null 2>&1 && continue
fi
# Check the URL, saving it if invalid
#
# Each URL is checked in a separate process as each unique URL
# requires us to hit the network.
check_url "$url" "$invalid_urls_dir" &
done
# Synchronisation point
wait
# Combine all the separate invalid URL files into one
local invalid_files=$(ls "$invalid_urls_dir")
if [ -n "$invalid_files" ]; then
pushd "$invalid_urls_dir" &>/dev/null
cat $(echo "$invalid_files"|tr '\n' ' ') > "$invalid_urls"
popd &>/dev/null
fi
if [ -s "$invalid_urls" ]
then
local files
cat "$invalid_urls" | while read url
do
files=$(grep "^${url}" "$url_map" | awk '{print $2}' | sort -u)
echo >&2 -e "ERROR: Invalid URL '$url' found in the following files:\n"
for file in $files
do
echo >&2 "$file"
done
done
exit 1
fi
# Now, spell check the docs
cmd="${test_dir}/cmd/check-spelling/kata-spell-check.sh"
local docs_failed=0
for doc in $docs
do
"$cmd" check "$doc" || { info "spell check failed for document $doc" && docs_failed=1; }
static_check_eof "$doc"
done
popd
[ $docs_failed -eq 0 ] || {
url='https://github.com/kata-containers/kata-containers/blob/main/docs/Documentation-Requirements.md#spelling'
die "spell check failed, See $url for more information."
}
}
static_check_eof()
{
local file="$1"
local anchor="EOF"
[ -z "$file" ] && info "No files to check" && return
# Skip the itself
[ "$file" == "$script_name" ] && return
# Skip the Vagrantfile
[ "$file" == "Vagrantfile" ] && return
local invalid=$(cat "$file" |\
grep -o -E '<<-* *\w*' |\
sed -e 's/^<<-*//g' |\
tr -d ' ' |\
sort -u |\
grep -v -E '^$' |\
grep -v -E "$anchor" || true)
[ -z "$invalid" ] || die "Expected '$anchor' here anchor, in $file found: $invalid"
}
# Tests to apply to all files.
#
# Currently just looks for TODO/FIXME comments that should be converted to
# (or annotated with) an Issue URL.
static_check_files()
{
local file
local files
if [ "$force" = "false" ]
then
info "Skipping check_files: see https://github.com/kata-containers/tests/issues/469"
return
else
info "Force override of check_files skip"
fi
info "Checking files"
if [ "$specific_branch" = "true" ]
then
info "Checking all files in $branch branch"
files=$(git ls-files | grep -v -E "/(.git|vendor|grpc-rs|target)/" || true)
else
info "Checking local branch for changed files only"
files=$(get_pr_changed_file_details || true)
# Strip off status
files=$(echo "$files"|awk '{print $NF}')
fi
[ -z "$files" ] && info "No files changed" && return
local matches=""
pushd $repo_path
for file in $files
do
local match
# Look for files containing the specified comment tags but
# which do not include a github URL.
match=$(grep -H -E "\<FIXME\>|\<TODO\>" "$file" |\
grep -v "https://github.com/.*/issues/[0-9]" |\
cut -d: -f1 |\
sort -u || true)
[ -z "$match" ] && continue
# Don't fail if this script contains the patterns
# (as it is guaranteed to ;)
echo "$file" | grep -q "${script_name}$" && info "Ignoring special file $file" && continue
# We really only care about comments in code. But to avoid
# having to hard-code the list of file extensions to search,
# invert the problem by simply ignoring document files and
# considering all other file types.
echo "$file" | grep -q ".md$" && info "Ignoring comment tag in document $file" && continue
matches+=" $match"
done
popd
[ -z "$matches" ] && return
echo >&2 -n \
"ERROR: The following files contain TODO/FIXME's that need "
echo >&2 -e "converting to issues:\n"
for file in $matches
do
echo >&2 "$file"
done
# spacer
echo >&2
exit 1
}
# Perform vendor checks:
#
# - Ensure that changes to vendored code are accompanied by an update to the
# vendor tooling config file. If not, the user simply hacked the vendor files
# rather than following the correct process:
#
# https://github.com/kata-containers/community/blob/main/VENDORING.md
#
# - Ensure vendor metadata is valid.
static_check_vendor()
{
pushd $repo_path
local files
local files_arr=()
files=$(find . -type f -name "go.mod")
while IFS= read -r line; do
files_arr+=("$line")
done <<< "$files"
for file in "${files_arr[@]}"; do
local dir=$(echo $file | sed 's/go\.mod//')
pushd $dir
# Check if directory has been changed to use go modules
if [ -f "go.mod" ]; then
info "go.mod file found in $dir, running go mod verify instead"
# This verifies the integrity of modules in the local cache.
# This does not really verify the integrity of vendored code:
# https://github.com/golang/go/issues/27348
# Once that is added we need to add an extra step to verify vendored code.
go mod verify
fi
popd
done
popd
}
static_check_xml()
{
local all_xml
local files
pushd $repo_path
need_chronic
all_xml=$(git ls-files "*.xml" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true)
if [ "$specific_branch" = "true" ]
then
info "Checking all XML files in $branch branch"
files="$all_xml"
else
info "Checking local branch for changed XML files only"
local xml_status
xml_status=$(get_pr_changed_file_details || true)
xml_status=$(echo "$xml_status" | grep "\.xml$" || true)
files=$(echo "$xml_status" | awk '{print $NF}')
fi
[ -z "$files" ] && info "No XML files to check" && popd && return
local file
for file in $files
do
info "Checking XML file '$file'"
local contents
# Most XML documents are specified as XML 1.0 since, with the
# advent of XML 1.0 (Fifth Edition), XML 1.1 is "almost
# redundant" due to XML 1.0 providing the majority of XML 1.1
# features. xmllint doesn't support XML 1.1 seemingly for this
# reason, so the only check we can do is to (crudely) force
# the document to be an XML 1.0 one since XML 1.1 documents
# can mostly be represented as XML 1.0.
#
# This is only really required since Jenkins creates XML 1.1
# documents.
contents=$(sed "s/xml version='1.1'/xml version='1.0'/g" "$file")
local ret
{ $chronic xmllint -format - <<< "$contents"; ret=$?; } || true
[ "$ret" -eq 0 ] || die "failed to check XML file '$file'"
done
popd
}
static_check_shell()
{
local all_scripts
local scripts
pushd $repo_path
need_chronic
all_scripts=$(git ls-files "*.sh" "*.bash" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true)
if [ "$specific_branch" = "true" ]
then
info "Checking all scripts in $branch branch"
scripts="$all_scripts"
else
info "Checking local branch for changed scripts only"
local scripts_status
scripts_status=$(get_pr_changed_file_details || true)
scripts_status=$(echo "$scripts_status" | grep -E "\.(sh|bash)$" || true)
scripts=$(echo "$scripts_status" | awk '{print $NF}')
fi
[ -z "$scripts" ] && info "No scripts to check" && popd && return 0
local script
for script in $scripts
do
info "Checking script file '$script'"
local ret
{ $chronic bash -n "$script"; ret=$?; } || true
[ "$ret" -eq 0 ] || die "check for script '$script' failed"
static_check_eof "$script"
done
popd
}
static_check_json()
{
local all_json
local json_files
pushd $repo_path
need_chronic
all_json=$(git ls-files "*.json" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true)
if [ "$specific_branch" = "true" ]
then
info "Checking all JSON in $branch branch"
json_files="$all_json"
else
info "Checking local branch for changed JSON only"
local json_status
json_status=$(get_pr_changed_file_details || true)
json_status=$(echo "$json_status" | grep "\.json$" || true)
json_files=$(echo "$json_status" | awk '{print $NF}')
fi
[ -z "$json_files" ] && info "No JSON files to check" && popd && return 0
local json
for json in $json_files
do
info "Checking JSON file '$json'"
local ret
{ $chronic jq -S . "$json"; ret=$?; } || true
[ "$ret" -eq 0 ] || die "failed to check JSON file '$json'"
done
popd
}
# The dockerfile checker relies on the hadolint tool. This function handle its
# installation if it is not found on PATH.
# Note that we need a specific version of the tool as it seems to not have
# backward/forward compatibility between versions.
has_hadolint_or_install()
{
# Global variable set by the caller. It might be overwritten here.
linter_cmd=${linter_cmd:-"hadolint"}
local linter_version=$(get_test_version "externals.hadolint.version")
local linter_url=$(get_test_version "externals.hadolint.url")
local linter_dest="${GOPATH}/bin/hadolint"
local has_linter=$(command -v "$linter_cmd")
if [[ -z "$has_linter" && "$KATA_DEV_MODE" == "yes" ]]; then
# Do not install if it is in development mode.
die "$linter_cmd command not found. You must have the version $linter_version installed to run this check."
elif [ -n "$has_linter" ]; then
# Check if the expected linter version
if $linter_cmd --version | grep -v "$linter_version" &>/dev/null; then
warn "$linter_cmd command found but not the required version $linter_version"
has_linter=""
fi
fi
if [ -z "$has_linter" ]; then
local download_url="${linter_url}/releases/download/v${linter_version}/hadolint-Linux-x86_64"
info "Installing $linter_cmd $linter_version at $linter_dest"
curl -sfL "$download_url" -o "$linter_dest" || \
die "Failed to download $download_url"
chmod +x "$linter_dest"
# Overwrite in case it cannot be found in PATH.
linter_cmd="$linter_dest"
fi
}
static_check_dockerfiles()
{
local all_files
local files
local ignore_files
# Put here a list of files which should be ignored.
local ignore_files=(
)
pushd $repo_path
local linter_cmd="hadolint"
all_files=$(git ls-files "*/Dockerfile*" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true)
if [ "$specific_branch" = "true" ]; then
info "Checking all Dockerfiles in $branch branch"
files="$all_files"
else
info "Checking local branch for changed Dockerfiles only"
local files_status
files_status=$(get_pr_changed_file_details || true)
files_status=$(echo "$files_status" | grep -E "Dockerfile.*$" || true)
files=$(echo "$files_status" | awk '{print $NF}')
fi
[ -z "$files" ] && info "No Dockerfiles to check" && popd && return 0
# As of this writing hadolint is only distributed for x86_64
if [ "$(uname -m)" != "x86_64" ]; then
info "Skip checking as $linter_cmd is not available for $(uname -m)"
popd
return 0
fi
has_hadolint_or_install
linter_cmd+=" --no-color"
# Let's not fail with INFO rules.
linter_cmd+=" --failure-threshold warning"
# Some rules we don't want checked, below we ignore them.
#
# "DL3008 warning: Pin versions in apt get install"
linter_cmd+=" --ignore DL3008"
# "DL3041 warning: Specify version with `dnf install -y <package>-<version>`"
linter_cmd+=" --ignore DL3041"
# "DL3033 warning: Specify version with `yum install -y <package>-<version>`"
linter_cmd+=" --ignore DL3033"
# "DL3018 warning: Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"
linter_cmd+=" --ignore DL3018"
# "DL3003 warning: Use WORKDIR to switch to a directory"
# See https://github.com/hadolint/hadolint/issues/70
linter_cmd+=" --ignore DL3003"
# "DL3048 style: Invalid label key"
linter_cmd+=" --ignore DL3048"
# DL3037 warning: Specify version with `zypper install -y <package>=<version>`.
linter_cmd+=" --ignore DL3037"
# Temporary add to prevent failure for test migration
# DL3040 warning: `dnf clean all` missing after dnf command.
linter_cmd+=" --ignore DL3040"
local file
for file in $files; do
if echo "${ignore_files[@]}" | grep -q $file ; then
info "Ignoring Dockerfile '$file'"
continue
fi
info "Checking Dockerfile '$file'"
local ret
# The linter generates an Abstract Syntax Tree (AST) from the
# dockerfile. Some of our dockerfiles are actually templates
# with special syntax, thus the linter might fail to build
# the AST. Here we handle Dockerfile templates.
if [[ "$file" =~ Dockerfile.*\.(in|template)$ ]]; then
# In our templates, text with marker as @SOME_NAME@ is
# replaceable. Usually it is used to replace in a
# FROM command (e.g. `FROM @UBUNTU_REGISTRY@/ubuntu`)
# but also to add an entire block of commands. Example
# of later:
# ```
# RUN apt-get install -y package1
# @INSTALL_MUSL@
# @INSTALL_RUST@
# ```
# It's known that the linter will fail to parse lines
# started with `@`. Also it might give false-positives
# on some cases. Here we remove all markers as a best
# effort approach. If the template file is still
# unparseable then it should be added in the
# `$ignore_files` list.
{ sed -e 's/^@[A-Z_]*@//' -e 's/@\([a-zA-Z_]*\)@/\1/g' "$file" | $linter_cmd -; ret=$?; }\
|| true
else
# Non-template Dockerfile.
{ $linter_cmd "$file"; ret=$?; } || true
fi
[ "$ret" -eq 0 ] || die "failed to check Dockerfile '$file'"
done
popd
}
# Run the specified function (after first checking it is compatible with the
# users architectural preferences), or simply list the function name if list
# mode is active.
run_or_list_check_function()
{
local name="$1"
func_is_valid "$name"
local arch_func
local handler
arch_func=$(func_is_arch_specific "$name")
handler="info"
# If the user requested only a single function to run, we should die
# if the function cannot be run due to the other options specified.
#
# Whereas if this script is running all functions, just display an
# info message if a function cannot be run.
[ "$single_func_only" = "true" ] && handler="die"
if [ "$handle_funcs" = "arch-agnostic" ] && [ "$arch_func" = "yes" ]; then
if [ "$list_only" != "true" ]; then
"$handler" "Not running '$func' as requested no architecture-specific functions"
fi
return 0
fi
if [ "$handle_funcs" = "arch-specific" ] && [ "$arch_func" = "no" ]; then
if [ "$list_only" != "true" ]; then
"$handler" "Not running architecture-agnostic function '$func' as requested only architecture specific functions"
fi
return 0
fi
if [ "$list_only" = "true" ]; then
echo "$func"
return 0
fi
info "Running '$func' function"
eval "$func"
}
setup()
{
source /etc/os-release || source /usr/lib/os-release
trap remove_tmp_files EXIT
}
# Display a message showing some system details.
announce()
{
local arch
arch=$(uname -m)
local file='/proc/cpuinfo'
local detail
detail=$(grep -m 1 -E '\<vendor_id\>|\<cpu\> * *:' "$file" \
2>/dev/null |\
cut -d: -f2- |\
tr -d ' ' || true)
local arch="$arch"
[ -n "$detail" ] && arch+=" ('$detail')"
local kernel
kernel=$(uname -r)
local distro_name
local distro_version
distro_name="${NAME:-}"
distro_version="${VERSION:-}"
local -a lines
local IFS=$'\n'
lines=( $(cat <<-EOF
Running static checks:
script: $script_name
architecture: $arch
kernel: $kernel
distro:
name: $distro_name
version: $distro_version
EOF
))
local line
for line in "${lines[@]}"
do
info "$line"
done
}
main()
{
setup
local long_option_names="${!long_options[@]}"
local args
args=$(getopt \
-n "$script_name" \
-a \
--options="h" \
--longoptions="$long_option_names" \
-- "$@")
[ $? -eq 0 ] || { usage >&2; exit 1; }
eval set -- "$args"
local func=
while [ $# -gt 1 ]
do
case "$1" in
--all) specific_branch="true" ;;
--branch) branch="$2"; shift ;;
--commits) func=static_check_commits ;;
--docs) func=static_check_docs ;;
--dockerfiles) func=static_check_dockerfiles ;;
--files) func=static_check_files ;;
--force) force="true" ;;
--golang) func=static_check_go_arch_specific ;;
-h|--help) usage; exit 0 ;;
--json) func=static_check_json ;;
--labels) func=static_check_labels;;
--licenses) func=static_check_license_headers ;;
--list) list_only="true" ;;
--no-arch) handle_funcs="arch-agnostic" ;;
--only-arch) handle_funcs="arch-specific" ;;
--repo) repo="$2"; shift ;;
--scripts) func=static_check_shell ;;
--vendor) func=static_check_vendor;;
--versions) func=static_check_versions ;;
--xml) func=static_check_xml ;;
--) shift; break ;;
esac
shift
done
# Consume getopt cruft
[ "$1" = "--" ] && shift
[ "$1" = "help" ] && usage && exit 0
# Set if not already set by options
[ -z "$repo" ] && repo="$1"
[ "$specific_branch" = "false" ] && specific_branch="$2"
if [ -z "$repo" ]
then
if [ -n "$KATA_DEV_MODE" ]
then
# No repo param provided so assume it's the current
# one to avoid developers having to specify one now
# (backwards compatability).
repo=$(git config --get remote.origin.url |\
sed 's!https://!!g' || true)
info "Auto-detected repo as $repo"
else
if [ "$list_only" != "true" ]; then
echo >&2 "ERROR: need repo" && usage && exit 1
fi
fi
fi
repo_path=$GOPATH/src/$repo
announce
local all_check_funcs=$(typeset -F|awk '{print $3}'|grep "${check_func_regex}"|sort)
# Run user-specified check and quit
if [ -n "$func" ]; then
single_func_only="true"
run_or_list_check_function "$func"
exit 0
fi
for func in $all_check_funcs
do
run_or_list_check_function "$func"
done
}
main "$@"