├── code-of-conduct.md ├── go.mod ├── OWNERS ├── RELEASE.md ├── SECURITY_CONTACTS ├── hack ├── utils.sh ├── verify-boilerplate.sh ├── update-gofmt.sh ├── update-all.sh ├── update-deps.sh ├── verify-gofmt.sh ├── verify-gotest.sh ├── verify-govet.sh ├── verify-deps.sh ├── verify-staticcheck.sh ├── verify-golint.sh ├── verify-whitespace.sh ├── verify-shellcheck.sh ├── verify-spelling.sh ├── verify-all.sh ├── verify-boilerplate_test.go └── verify-boilerplate.go ├── go.sum ├── validators ├── kernel_validator_helper.go ├── package_validator_other.go ├── cgroup_validator_other.go ├── os_validator_unix_test.go ├── os_validator_windows_test.go ├── os_validator_unix.go ├── os_validator_windows.go ├── report.go ├── types_windows.go ├── validators.go ├── docker_validator.go ├── types_unix.go ├── docker_validator_test.go ├── types.go ├── kernel_validator_test.go ├── package_validator_test.go ├── cgroup_validator_test.go ├── kernel_validator.go ├── cgroup_validator_linux.go └── package_validator_linux.go ├── SECURITY.md ├── README.md ├── CONTRIBUTING.md └── LICENSE /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module k8s.io/system-validators 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/blang/semver/v4 v4.0.0 7 | github.com/stretchr/testify v1.3.0 8 | golang.org/x/sys v0.27.0 9 | ) 10 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | approvers: 4 | - neolit123 5 | - SataQiu 6 | - pacoxu 7 | reviewers: 8 | - KentaTada 9 | - carlory 10 | emeritus_approvers: 11 | - luxas 12 | - timothysc 13 | - rosti 14 | - ereslibre 15 | - fabriziopandini 16 | labels: 17 | - sig/cluster-lifecycle 18 | - sig/node 19 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | The system-validators is released on an as-needed basis. The process is as follows: 4 | 5 | 1. An issue is proposing a new release with a changelog since the last release 6 | 1. All [OWNERS](OWNERS) must LGTM this release 7 | 1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION` 8 | 1. The release issue is closed 9 | 1. An announcement email is sent to `kubernetes-dev@googlegroups.com` with the subject `[ANNOUNCE] system-validators $VERSION is released` 10 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Committee to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | cjcullen 14 | joelsmith 15 | jonpulsifer 16 | liggitt 17 | philips 18 | tallclair 19 | -------------------------------------------------------------------------------- /hack/utils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # get_root_path returns the root path of the kinder source tree 17 | get_root_path() { 18 | echo "$(git rev-parse --show-toplevel)" 19 | } 20 | 21 | # cd_root_path cds to the root path of the kinder source tree 22 | cd_root_path() { 23 | cd "$(get_root_path)" || exit 24 | } 25 | -------------------------------------------------------------------------------- /hack/verify-boilerplate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -o errexit 17 | set -o nounset 18 | set -o pipefail 19 | 20 | # shellcheck source=/dev/null 21 | source "$(dirname "$0")/utils.sh" 22 | # cd to the root path 23 | cd_root_path 24 | 25 | git ls-files | grep -v "vendor\/" | xargs go run ./hack/verify-boilerplate.go 26 | -------------------------------------------------------------------------------- /hack/update-gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # script to run gofmt over our code (not vendor) 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | # shellcheck source=/dev/null 22 | source "$(dirname "$0")/utils.sh" 23 | # cd to the root path 24 | cd_root_path 25 | 26 | # update go fmt 27 | go fmt ./... 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 2 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 9 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 10 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 11 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 12 | -------------------------------------------------------------------------------- /validators/kernel_validator_helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package system 18 | 19 | // KernelValidatorHelper is an interface intended to help with os specific kernel validation 20 | type KernelValidatorHelper interface { 21 | // GetKernelReleaseVersion gets the current kernel release version of the system 22 | GetKernelReleaseVersion() (string, error) 23 | } 24 | -------------------------------------------------------------------------------- /hack/update-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2018 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # script to run all update scripts (except deps) 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | # shellcheck source=/dev/null 22 | source "$(dirname "$0")/utils.sh" 23 | REPO_PATH=$(get_root_path) 24 | 25 | "${REPO_PATH}"/hack/update-deps.sh 26 | "${REPO_PATH}"/hack/update-gofmt.sh 27 | -------------------------------------------------------------------------------- /hack/update-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2018 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # Runs go mod tidy 17 | # 18 | # Usage: 19 | # update-deps.sh 20 | 21 | set -o nounset 22 | set -o errexit 23 | set -o pipefail 24 | 25 | # shellcheck source=/dev/null 26 | source "$(dirname "$0")/utils.sh" 27 | # cd to the root path 28 | cd_root_path 29 | 30 | export GO111MODULE="on" 31 | go mod tidy 32 | -------------------------------------------------------------------------------- /hack/verify-gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -o errexit 17 | set -o nounset 18 | set -o pipefail 19 | 20 | # shellcheck source=/dev/null 21 | source "$(dirname "$0")/utils.sh" 22 | # cd to the root path 23 | cd_root_path 24 | 25 | # check for gofmt diffs 26 | diff=$(git ls-files | grep "\.go" | grep -v "vendor\/" | xargs gofmt -s -d 2>&1) 27 | if [[ -n "${diff}" ]]; then 28 | echo "${diff}" 29 | echo 30 | echo "Check failed. Please run hack/update-gofmt.sh" 31 | exit 1 32 | fi 33 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security Announcements 4 | 5 | Join the [kubernetes-security-announce] group for security and vulnerability announcements. 6 | 7 | You can also subscribe to an RSS feed of the above using [this link][kubernetes-security-announce-rss]. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Instructions for reporting a vulnerability can be found on the 12 | [Kubernetes Security and Disclosure Information] page. 13 | 14 | ## Supported Versions 15 | 16 | Information about supported Kubernetes versions can be found on the 17 | [Kubernetes version and version skew support policy] page on the Kubernetes website. 18 | 19 | [kubernetes-security-announce]: https://groups.google.com/forum/#!forum/kubernetes-security-announce 20 | [kubernetes-security-announce-rss]: https://groups.google.com/forum/feed/kubernetes-security-announce/msgs/rss_v2_0.xml?num=50 21 | [Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions 22 | [Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability 23 | -------------------------------------------------------------------------------- /hack/verify-gotest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -o errexit 17 | set -o nounset 18 | set -o pipefail 19 | 20 | # shellcheck source=/dev/null 21 | source "$(dirname "$0")/utils.sh" 22 | # cd to the root path 23 | cd_root_path 24 | 25 | # run go test 26 | export GO111MODULE=on 27 | ERROR=0 28 | DIRS=$(git ls-files | grep -v "vendor\/" | grep ".go" | xargs dirname | grep -v "\." | cut -d '/' -f 1 | uniq) 29 | while read -r dir; do 30 | go test ./"$dir"/... || ERROR=1 31 | done <<< "$DIRS" 32 | 33 | if [[ "${ERROR}" = 1 ]]; then 34 | echo "found errors in go test!" 35 | fi 36 | 37 | exit ${ERROR} 38 | -------------------------------------------------------------------------------- /hack/verify-govet.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # CI script to run go vet over our code 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | # shellcheck source=/dev/null 22 | source "$(dirname "$0")/utils.sh" 23 | # cd to the root path 24 | cd_root_path 25 | 26 | # run go vet 27 | export GO111MODULE=on 28 | ERROR=0 29 | DIRS=$(git ls-files | grep -v "vendor\/" | grep ".go" | xargs dirname | grep -v "\." | cut -d '/' -f 1 | uniq) 30 | while read -r dir; do 31 | for os in {darwin,linux,windows}; do 32 | GOOS=$os go vet ./"$dir"/... || ERROR=1 33 | done 34 | done <<< "$DIRS" 35 | 36 | if [[ "${ERROR}" = 1 ]]; then 37 | echo "found errors in go vet!" 38 | fi 39 | 40 | exit ${ERROR} 41 | -------------------------------------------------------------------------------- /validators/package_validator_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | /* 5 | Copyright 2020 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | // NOOP for non-Linux OSes. 23 | 24 | // packageValidator implements the Validator interface. It validates packages 25 | // and their versions. 26 | type packageValidator struct { 27 | reporter Reporter 28 | } 29 | 30 | // Name returns the name of the package validator. 31 | func (validator *packageValidator) Name() string { 32 | return "package" 33 | } 34 | 35 | // Validate checks packages and their versions against the packageSpecs using 36 | // the packageManager, and returns an error on any package/version mismatch. 37 | func (validator *packageValidator) Validate(spec SysSpec) ([]error, []error) { 38 | return nil, nil 39 | } 40 | -------------------------------------------------------------------------------- /validators/cgroup_validator_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | /* 5 | Copyright 2020 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | // NOOP for non-Linux OSes. 23 | 24 | var _ Validator = &CgroupsValidator{} 25 | 26 | const mountsFilePath = "" 27 | 28 | // CgroupsValidator validates cgroup configuration. 29 | type CgroupsValidator struct { 30 | Reporter Reporter 31 | KubeletVersion string 32 | } 33 | 34 | // Validate is part of the system.Validator interface. 35 | func (c *CgroupsValidator) Validate(spec SysSpec) (warns, errs []error) { 36 | return 37 | } 38 | 39 | // Name is part of the system.Validator interface. 40 | func (c *CgroupsValidator) Name() string { 41 | return "cgroups" 42 | } 43 | 44 | // getUnifiedMountpoint is a no-op for non-Linux OSes. 45 | func getUnifiedMountpoint(path string) (string, bool, error) { 46 | return "", false, nil 47 | } 48 | -------------------------------------------------------------------------------- /hack/verify-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -o nounset 17 | set -o pipefail 18 | 19 | # shellcheck source=/dev/null 20 | source "$(dirname "$0")/utils.sh" 21 | # cd to the root path 22 | cd_root_path 23 | 24 | # cleanup on exit 25 | cleanup() { 26 | echo "Cleaning up..." 27 | mv go.mod.old go.mod 28 | mv go.sum.old go.sum 29 | } 30 | trap cleanup EXIT 31 | 32 | echo "Verifying..." 33 | # temporary copy the go mod and sum files 34 | cp go.mod go.mod.old || exit 35 | cp go.sum go.sum.old || exit 36 | 37 | # run update-deps.sh 38 | export GO111MODULE="on" 39 | ./hack/update-deps.sh 40 | 41 | # compare the old and new files 42 | DIFF0=$(diff -u go.mod go.mod.old) 43 | DIFF1=$(diff -u go.sum go.sum.old) 44 | 45 | if [[ -n "${DIFF0}" ]] || [[ -n "${DIFF1}" ]]; then 46 | echo "${DIFF0}" 47 | echo "${DIFF1}" 48 | echo "Check failed. Please run ./hack/update-deps.sh" 49 | exit 1 50 | fi 51 | -------------------------------------------------------------------------------- /validators/os_validator_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | /* 5 | Copyright 2016 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | import ( 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestValidateOS(t *testing.T) { 29 | v := &OSValidator{ 30 | Reporter: DefaultReporter, 31 | } 32 | specOS := "Linux" 33 | for _, test := range []struct { 34 | os string 35 | err bool 36 | }{ 37 | { 38 | os: "Linux", 39 | err: false, 40 | }, 41 | { 42 | os: "Windows", 43 | err: true, 44 | }, 45 | { 46 | os: "Darwin", 47 | err: true, 48 | }, 49 | } { 50 | t.Run(test.os, func(t *testing.T) { 51 | err := v.validateOS(test.os, specOS) 52 | if !test.err { 53 | assert.Nil(t, err, "Expect error not to occur with os %q", test.os) 54 | } else { 55 | assert.NotNil(t, err, "Expect error to occur with os %q", test.os) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /hack/verify-staticcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # CI script to run staticcheck 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | # shellcheck source=/dev/null 22 | source "$(dirname "$0")/utils.sh" 23 | 24 | # cd to the root path 25 | REPO_PATH=$(get_root_path) 26 | 27 | # create a temporary directory 28 | TMP_DIR=$(mktemp -d) 29 | 30 | # cleanup 31 | exitHandler() ( 32 | echo "Cleaning up..." 33 | rm -rf "${TMP_DIR}" 34 | ) 35 | trap exitHandler EXIT 36 | 37 | # pull the source code and build the binary 38 | cd "${TMP_DIR}" 39 | URL="http://github.com/dominikh/go-tools" 40 | echo "Cloning ${URL} in ${TMP_DIR}..." 41 | git clone --quiet --depth=1 "${URL}" . 42 | echo "Building staticcheck..." 43 | export GO111MODULE=on 44 | go build -o ./bin/staticheck ./cmd/staticcheck 45 | 46 | # run the binary 47 | cd "${REPO_PATH}" 48 | echo "Running staticcheck..." 49 | "${TMP_DIR}/bin/staticheck" ./... 50 | -------------------------------------------------------------------------------- /validators/os_validator_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | /* 5 | Copyright 2024 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | import ( 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestValidateOS(t *testing.T) { 29 | v := &OSValidator{ 30 | Reporter: DefaultReporter, 31 | } 32 | specOS := "Windows Server" 33 | for _, test := range []struct { 34 | os string 35 | err bool 36 | }{ 37 | { 38 | os: "Windows Server", 39 | err: false, 40 | }, 41 | { 42 | os: "Windows 10 Enterprise", 43 | err: true, 44 | }, 45 | { 46 | os: "Windows", 47 | err: true, 48 | }, 49 | } { 50 | t.Run(test.os, func(t *testing.T) { 51 | err := v.validateOS(test.os, specOS) 52 | if !test.err { 53 | assert.Nil(t, err, "Expect error not to occur with os %q", test.os) 54 | } else { 55 | assert.NotNil(t, err, "Expect error to occur with os %q", test.os) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /hack/verify-golint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # CI script to run go lint over our code 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | # shellcheck source=/dev/null 22 | source "$(dirname "$0")/utils.sh" 23 | 24 | # cd to the root path 25 | REPO_PATH=$(get_root_path) 26 | 27 | # create a temporary directory 28 | TMP_DIR=$(mktemp -d) 29 | 30 | # cleanup 31 | exitHandler() ( 32 | echo "Cleaning up..." 33 | rm -rf "${TMP_DIR}" 34 | ) 35 | trap exitHandler EXIT 36 | 37 | # pull the source code and build the binary 38 | cd "${TMP_DIR}" 39 | URL="http://github.com/golang/lint" 40 | echo "Cloning ${URL} in ${TMP_DIR}..." 41 | git clone --quiet --depth=1 "${URL}" . 42 | echo "Building golint..." 43 | export GO111MODULE=on 44 | go build -o ./golint/golint ./golint 45 | 46 | # run the binary 47 | cd "${REPO_PATH}" 48 | echo "Running golint..." 49 | git ls-files | grep "\.go" | \ 50 | grep -v "vendor\/" | \ 51 | xargs -L1 "${TMP_DIR}/golint/golint" -set_exit_status 52 | -------------------------------------------------------------------------------- /hack/verify-whitespace.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -o nounset 17 | set -o pipefail 18 | 19 | # shellcheck source=/dev/null 20 | source "$(dirname "$0")/utils.sh" 21 | # cd to the root path 22 | cd_root_path 23 | 24 | echo "Verifying trailing whitespace..." 25 | TRAILING="$(grep -rnI '[[:blank:]]$' . | grep -v -e "\.git" -v -e "vendor\/" -v -e "\.svg")" 26 | 27 | ERR="0" 28 | if [[ ! -z "$TRAILING" ]]; then 29 | echo "Found trailing whitespace in the follow files:" 30 | echo "${TRAILING}" 31 | ERR="1" 32 | fi 33 | 34 | echo -e "Verifying new lines at end of files..." 35 | FILES="$(git ls-files | grep -I -v -e "vendor\/" -v -e "\.svg")" 36 | while read -r LINE; do 37 | grep -qI . "${LINE}" || continue # skip binary files 38 | c="$(tail -c 1 "${LINE}")" 39 | if [[ "$c" != "" ]]; then 40 | echo "${LINE}: no newline at the end of file" 41 | ERR=1 42 | fi 43 | done <<< "${FILES}" 44 | 45 | if [[ "$ERR" == "1" ]]; then 46 | echo "Found whitespace errors!" 47 | exit 1 48 | fi 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # System Validators 2 | 3 | A set of system-oriented validators for kubeadm preflight checks. 4 | 5 | ## Creating releases 6 | 7 | To prepare a release of this library please follow this guide: 8 | - The main branch should always contain WIP commits planned for the upcoming release. 9 | - Always create a new branch for MAJOR and MINOR releases. This allows backporting changes. 10 | - Release branch names should be in the format `release-MAJOR.MINOR` (without a `v` prefix). 11 | - Only non-breaking bug fixes can be done in a PATCH release. 12 | - New features must not be added in PATCH releases. 13 | - Breaking changes must be added in a MAJOR release. 14 | - Pushing releases requires write access. To obtain that you must be part of 15 | the [`system-validator-maintainers` team](http://git.k8s.io/org/config/kubernetes/sig-cluster-lifecycle/teams.yaml). 16 | 17 | For vendoring the new release in kubernetes/kubernetes you can use its `pin-dependency.sh` script. 18 | 19 | Example: 20 | ```bash 21 | ./hack/pin-dependency.sh k8s.io/system-validators 22 | ``` 23 | 24 | And then PR the changes. 25 | 26 | ## Community, discussion, contribution, and support 27 | 28 | Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). 29 | 30 | You can reach the maintainers of this project at: 31 | 32 | - [Slack](https://kubernetes.slack.com/messages/sig-cluster-lifecycle) 33 | - [Mailing List](https://groups.google.com/a/kubernetes.io/g/sig-cluster-lifecycle) 34 | 35 | ### Code of conduct 36 | 37 | Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). 38 | -------------------------------------------------------------------------------- /hack/verify-shellcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # set -o errexit 17 | set -o nounset 18 | set -o pipefail 19 | 20 | # shellcheck source=/dev/null 21 | source "$(dirname "$0")/utils.sh" 22 | ROOT_PATH=$(get_root_path) 23 | 24 | # create a temporary directory 25 | TMP_DIR=$(mktemp -d) 26 | 27 | # cleanup on exit 28 | cleanup() { 29 | echo "Cleaning up..." 30 | rm -rf "${TMP_DIR}" 31 | } 32 | trap cleanup EXIT 33 | 34 | # install shellcheck (Linux-x64 only!) 35 | cd "${TMP_DIR}" || exit 36 | VERSION="shellcheck-v0.6.0" 37 | DOWNLOAD_FILE="${VERSION}.linux.x86_64.tar.xz" 38 | wget https://storage.googleapis.com/shellcheck/"${DOWNLOAD_FILE}" 39 | tar xf "${DOWNLOAD_FILE}" 40 | cd "${VERSION}" || exit 41 | 42 | echo "Running shellcheck..." 43 | cd "${ROOT_PATH}" || exit 44 | OUT="${TMP_DIR}/out.log" 45 | FILES=$(git ls-files | grep -v "vendor\/" | grep ".sh") 46 | while read -r file; do 47 | "${TMP_DIR}/${VERSION}/shellcheck" "$file" >> "${OUT}" 2>&1 48 | done <<< "$FILES" 49 | 50 | if [[ -s "${OUT}" ]]; then 51 | echo "Found errors:" 52 | cat "${OUT}" 53 | exit 1 54 | fi 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://git.k8s.io/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt: 4 | 5 | _As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._ 6 | 7 | ## Getting Started 8 | 9 | We have full documentation on how to get started contributing here: 10 | 11 | 14 | 15 | - [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests 16 | - [Kubernetes Contributor Guide](https://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](https://git.k8s.io/community/contributors/guide#contributing) 17 | - [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet/README.md) - Common resources for existing developers 18 | 19 | ## Mentorship 20 | 21 | - [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! 22 | 23 | ## Contact Information 24 | 25 | - [Slack](https://kubernetes.slack.com/messages/sig-cluster-lifecycle) 26 | - [Mailing List](https://groups.google.com/forum/#!forum/kubernetes-sig-cluster-lifecycle) 27 | -------------------------------------------------------------------------------- /validators/os_validator_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | /* 5 | Copyright 2016 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | import ( 23 | "fmt" 24 | "strings" 25 | 26 | "golang.org/x/sys/unix" 27 | ) 28 | 29 | var _ Validator = &OSValidator{} 30 | 31 | // OSValidator validates OS. 32 | type OSValidator struct { 33 | Reporter Reporter 34 | } 35 | 36 | // Name is part of the system.Validator interface. 37 | func (o *OSValidator) Name() string { 38 | return "os" 39 | } 40 | 41 | // Validate is part of the system.Validator interface. 42 | func (o *OSValidator) Validate(spec SysSpec) ([]error, []error) { 43 | var utsname unix.Utsname 44 | err := unix.Uname(&utsname) 45 | if err != nil { 46 | return nil, []error{fmt.Errorf("failed to get OS name: %w", err)} 47 | } 48 | os := strings.TrimSpace(unix.ByteSliceToString(utsname.Sysname[:])) 49 | if err = o.validateOS(os, spec.OS); err != nil { 50 | return nil, []error{err} 51 | } 52 | return nil, nil 53 | } 54 | 55 | func (o *OSValidator) validateOS(os, specOS string) error { 56 | if os != specOS { 57 | o.Reporter.Report("OS", os, bad) 58 | return fmt.Errorf("unsupported operating system: %s", os) 59 | } 60 | o.Reporter.Report("OS", os, good) 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /hack/verify-spelling.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -o errexit 17 | set -o nounset 18 | set -o pipefail 19 | 20 | # shellcheck source=/dev/null 21 | source "$(dirname "$0")/utils.sh" 22 | # cd to the root path 23 | cd_root_path 24 | 25 | # create a temporary directory 26 | TMP_DIR=$(mktemp -d) 27 | 28 | # cleanup 29 | exitHandler() ( 30 | echo "Cleaning up..." 31 | rm -rf "${TMP_DIR}" 32 | ) 33 | trap exitHandler EXIT 34 | 35 | # pull misspell 36 | export GO111MODULE=on 37 | URL="https://github.com/client9/misspell" 38 | echo "Cloning ${URL} in ${TMP_DIR}..." 39 | git clone --quiet --depth=1 "${URL}" "${TMP_DIR}" 40 | pushd "${TMP_DIR}" > /dev/null 41 | go mod init misspell 42 | go mod tidy 43 | popd > /dev/null 44 | 45 | # build misspell 46 | BIN_PATH="${TMP_DIR}/cmd/misspell" 47 | pushd "${BIN_PATH}" > /dev/null 48 | echo "Building misspell..." 49 | go build > /dev/null 50 | popd > /dev/null 51 | 52 | # check spelling 53 | RES=0 54 | ERROR_LOG="${TMP_DIR}/errors.log" 55 | echo "Checking spelling..." 56 | git ls-files | grep -v -e vendor | xargs "${BIN_PATH}/misspell" > "${ERROR_LOG}" 57 | if [[ -s "${ERROR_LOG}" ]]; then 58 | sed 's/^/error: /' "${ERROR_LOG}" # add 'error' to each line to highlight in e2e status 59 | echo "Found spelling errors!" 60 | RES=1 61 | fi 62 | exit "${RES}" 63 | -------------------------------------------------------------------------------- /validators/os_validator_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | /* 5 | Copyright 2024 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | import ( 23 | "fmt" 24 | "os/exec" 25 | "strings" 26 | ) 27 | 28 | var _ Validator = &OSValidator{} 29 | 30 | // OSValidator validates OS. 31 | type OSValidator struct { 32 | Reporter Reporter 33 | } 34 | 35 | // Name is part of the system.Validator interface. 36 | func (o *OSValidator) Name() string { 37 | return "os" 38 | } 39 | 40 | // Validate is part of the system.Validator interface. 41 | func (o *OSValidator) Validate(spec SysSpec) ([]error, []error) { 42 | args := []string{`(Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').ProductName`} 43 | os, err := exec.Command("powershell", args...).Output() 44 | if err != nil { 45 | return nil, []error{fmt.Errorf("failed to get OS name: %w", err)} 46 | } 47 | if err = o.validateOS(strings.TrimSpace(string(os)), spec.OS); err != nil { 48 | return nil, []error{err} 49 | } 50 | return nil, nil 51 | } 52 | 53 | // validateOS would check if the reported string such as 'Windows Server 2019' contains 54 | // the required OS prefix from the spec 'Windows Server'. 55 | func (o *OSValidator) validateOS(os, specOS string) error { 56 | if !strings.HasPrefix(os, specOS) { 57 | o.Reporter.Report("OS", os, bad) 58 | return fmt.Errorf("unsupported operating system: %s", os) 59 | } 60 | o.Reporter.Report("OS", os, good) 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /validators/report.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package system 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "io" 23 | "os" 24 | ) 25 | 26 | // ValidationResultType is type of the validation result. Different validation results 27 | // corresponds to different colors. 28 | type ValidationResultType int32 29 | 30 | const ( 31 | good ValidationResultType = iota 32 | bad 33 | warn 34 | ) 35 | 36 | // color is the color of the message. 37 | type color int32 38 | 39 | const ( 40 | red color = 31 41 | green color = 32 42 | yellow color = 33 43 | white color = 37 44 | ) 45 | 46 | func colorize(s string, c color) string { 47 | return fmt.Sprintf("\033[0;%dm%s\033[0m", c, s) 48 | } 49 | 50 | // StreamReporter is the default reporter for the system verification test. 51 | type StreamReporter struct { 52 | // The stream that this reporter is writing to 53 | WriteStream io.Writer 54 | } 55 | 56 | // Report reports validation result in different color depending on the result type. 57 | func (dr *StreamReporter) Report(key, value string, resultType ValidationResultType) error { 58 | var c color 59 | switch resultType { 60 | case good: 61 | c = green 62 | case bad: 63 | c = red 64 | case warn: 65 | c = yellow 66 | default: 67 | c = white 68 | } 69 | if dr.WriteStream == nil { 70 | return errors.New("WriteStream has to be defined for this reporter") 71 | } 72 | 73 | fmt.Fprintf(dr.WriteStream, "%s: %s\n", colorize(key, white), colorize(value, c)) 74 | return nil 75 | } 76 | 77 | // DefaultReporter is the default Reporter 78 | var DefaultReporter = &StreamReporter{ 79 | WriteStream: os.Stdout, 80 | } 81 | -------------------------------------------------------------------------------- /validators/types_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | /* 5 | Copyright 2017 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | import ( 23 | "os/exec" 24 | "strings" 25 | ) 26 | 27 | // DefaultSysSpec is the default SysSpec for Windows. 28 | var DefaultSysSpec = SysSpec{ 29 | OS: "Windows Server", 30 | KernelSpec: KernelSpec{ 31 | Versions: []string{`10\.0\.1439[3-9]`, `10\.0\.14[4-9][0-9]{2}`, `10\.0\.1[5-9][0-9]{3}`, `10\.0\.[2-9][0-9]{4}`, `10\.[1-9]+\.[0-9]+`}, //requires >= '10.0.14393' 32 | VersionsNote: "The kernel version should be >= '10.0.14393'", 33 | Required: []KernelConfig{}, 34 | Optional: []KernelConfig{}, 35 | Forbidden: []KernelConfig{}, 36 | }, 37 | RuntimeSpec: RuntimeSpec{ 38 | DockerSpec: &DockerSpec{ 39 | Version: []string{`18\.0[6,9]\..*`, `19\.03\..*`}, 40 | GraphDriver: []string{"windowsfilter"}, 41 | }, 42 | }, 43 | } 44 | 45 | // KernelValidatorHelperImpl is the 'windows' implementation of KernelValidatorHelper 46 | type KernelValidatorHelperImpl struct{} 47 | 48 | var _ KernelValidatorHelper = &KernelValidatorHelperImpl{} 49 | 50 | // GetKernelReleaseVersion returns the Windows release version (e.g. 10.0.14393) as a string. 51 | // It does not include the UBR (revision) 52 | func (o *KernelValidatorHelperImpl) GetKernelReleaseVersion() (string, error) { 53 | args := []string{`$props = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'; ` + 54 | `"$($props.CurrentMajorVersionNumber).$($props.CurrentMinorVersionNumber).$($props.CurrentBuildNumber)"`} 55 | releaseVersion, err := exec.Command("powershell", args...).Output() 56 | if err != nil { 57 | return "", err 58 | } 59 | return strings.TrimSpace(string(releaseVersion)), nil 60 | } 61 | -------------------------------------------------------------------------------- /hack/verify-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -o errexit 17 | set -o nounset 18 | set -o pipefail 19 | 20 | # shellcheck source=/dev/null 21 | source "$(dirname "$0")/utils.sh" 22 | 23 | # set REPO_PATH 24 | REPO_PATH=$(get_root_path) 25 | cd "${REPO_PATH}" 26 | 27 | # exit code, if a script fails we'll set this to 1 28 | res=0 29 | 30 | # run all verify scripts, optionally skipping any of them 31 | 32 | if [[ "${VERIFY_WHITESPACE:-true}" == "true" ]]; then 33 | echo "[*] Verifying whitespace..." 34 | hack/verify-whitespace.sh || res=1 35 | cd "${REPO_PATH}" 36 | fi 37 | 38 | if [[ "${VERIFY_SPELLING:-true}" == "true" ]]; then 39 | echo "[*] Verifying spelling..." 40 | hack/verify-spelling.sh || res=1 41 | cd "${REPO_PATH}" 42 | fi 43 | 44 | if [[ "${VERIFY_BOILERPLATE:-true}" == "true" ]]; then 45 | echo "[*] Verifying boilerplate..." 46 | hack/verify-boilerplate.sh || res=1 47 | cd "${REPO_PATH}" 48 | fi 49 | 50 | if [[ "${VERIFY_GOFMT:-true}" == "true" ]]; then 51 | echo "[*] Verifying gofmt..." 52 | hack/verify-gofmt.sh || res=1 53 | cd "${REPO_PATH}" 54 | fi 55 | 56 | if [[ "${VERIFY_GOLINT:-true}" == "true" ]]; then 57 | echo "[*] Verifying golint..." 58 | hack/verify-golint.sh || res=1 59 | cd "${REPO_PATH}" 60 | fi 61 | 62 | if [[ "${VERIFY_GOVET:-true}" == "true" ]]; then 63 | echo "[*] Verifying govet..." 64 | hack/verify-govet.sh || res=1 65 | cd "${REPO_PATH}" 66 | fi 67 | 68 | if [[ "${VERIFY_DEPS:-true}" == "true" ]]; then 69 | echo "[*] Verifying deps..." 70 | hack/verify-deps.sh || res=1 71 | cd "${REPO_PATH}" 72 | fi 73 | 74 | if [[ "${VERIFY_GOTEST:-true}" == "true" ]]; then 75 | echo "[*] Verifying gotest..." 76 | hack/verify-gotest.sh || res=1 77 | cd "${REPO_PATH}" 78 | fi 79 | 80 | # exit based on verify scripts 81 | if [[ "${res}" = 0 ]]; then 82 | echo "" 83 | echo "All verify checks passed, congrats!" 84 | else 85 | echo "" 86 | echo "One or more verify checks failed! See output above..." 87 | fi 88 | exit "${res}" 89 | -------------------------------------------------------------------------------- /validators/validators.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package system 18 | 19 | import ( 20 | "fmt" 21 | "runtime" 22 | ) 23 | 24 | // Validator is the interface for all validators. 25 | type Validator interface { 26 | // Name is the name of the validator. 27 | Name() string 28 | // Validate is the validate function. 29 | Validate(SysSpec) ([]error, []error) 30 | } 31 | 32 | // Reporter is the interface for the reporters for the validators. 33 | type Reporter interface { 34 | // Report reports the results of the system verification 35 | Report(string, string, ValidationResultType) error 36 | } 37 | 38 | // Validate uses validators to validate the system and returns a warning or error. 39 | func Validate(spec SysSpec, validators []Validator) ([]error, []error) { 40 | var errs []error 41 | var warns []error 42 | 43 | for _, v := range validators { 44 | fmt.Printf("Validating %s...\n", v.Name()) 45 | warn, err := v.Validate(spec) 46 | if len(err) != 0 { 47 | errs = append(errs, err...) 48 | } 49 | if len(warn) != 0 { 50 | warns = append(warns, warn...) 51 | } 52 | } 53 | return warns, errs 54 | } 55 | 56 | // ValidateSpec uses all default validators to validate the system and writes to stdout. 57 | func ValidateSpec(spec SysSpec, containerRuntime string) ([]error, []error) { 58 | // OS-level validators. 59 | var osValidators = []Validator{ 60 | &OSValidator{Reporter: DefaultReporter}, 61 | &KernelValidator{Reporter: DefaultReporter}, 62 | } 63 | 64 | // Docker-specific validators. 65 | var dockerValidators = []Validator{ 66 | &DockerValidator{Reporter: DefaultReporter}, 67 | } 68 | 69 | validators := osValidators 70 | switch containerRuntime { 71 | case "docker": 72 | validators = append(validators, dockerValidators...) 73 | } 74 | 75 | // Linux-specific validators. 76 | if runtime.GOOS == "linux" { 77 | validators = append(validators, 78 | &CgroupsValidator{Reporter: DefaultReporter}, 79 | &packageValidator{reporter: DefaultReporter}, 80 | ) 81 | } 82 | 83 | return Validate(spec, validators) 84 | } 85 | -------------------------------------------------------------------------------- /hack/verify-boilerplate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestVerifyBoilerPlate(t *testing.T) { 24 | testcases := []struct { 25 | name string 26 | bp string 27 | expectedError bool 28 | }{ 29 | { 30 | name: "valid: boilerplate is valid", 31 | bp: `\/* 32 | Copyright 2019 The Kubernetes Authors. 33 | 34 | Licensed under the Apache License, Version 2.0 (the "License"); 35 | you may not use this file except in compliance with the License. 36 | You may obtain a copy of the License at 37 | 38 | http://www.apache.org/licenses/LICENSE-2.0 39 | 40 | Unless required by applicable law or agreed to in writing, software 41 | distributed under the License is distributed on an "AS IS" BASIS, 42 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 43 | See the License for the specific language governing permissions and 44 | limitations under the License. 45 | */`, 46 | expectedError: false, 47 | }, 48 | { 49 | name: "invalid: missing lines", 50 | bp: ` 51 | Copyright 2019 The Kubernetes Authors. 52 | 53 | Licensed under the Apache License, Version 2.0 (the "License"); 54 | `, 55 | expectedError: true, 56 | }, 57 | { 58 | name: "invalid: bad year", 59 | bp: "Copyright 1019 The Kubernetes Authors.", 60 | expectedError: true, 61 | }, 62 | } 63 | 64 | for _, tc := range testcases { 65 | t.Run(tc.name, func(t *testing.T) { 66 | if err := verifyBoilerplate(tc.bp); err != nil != tc.expectedError { 67 | t.Errorf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestTrimLeadingComment(t *testing.T) { 74 | testcases := []struct { 75 | name string 76 | comment string 77 | line string 78 | expectedResult string 79 | }{ 80 | { 81 | name: "trim leading comment", 82 | comment: "#", 83 | line: "# test", 84 | expectedResult: "test", 85 | }, 86 | { 87 | name: "empty line", 88 | comment: "#", 89 | line: "#", 90 | expectedResult: "", 91 | }, 92 | { 93 | name: "trim leading comment and space", 94 | comment: "//", 95 | line: "// test", 96 | expectedResult: "test", 97 | }, 98 | { 99 | name: "no comment", 100 | comment: "//", 101 | line: "test", 102 | expectedResult: "test", 103 | }, 104 | } 105 | 106 | for _, tc := range testcases { 107 | t.Run(tc.name, func(t *testing.T) { 108 | if res := trimLeadingComment(tc.line, tc.comment); res != tc.expectedResult { 109 | t.Errorf("expected: %q, got: %q", tc.expectedResult, res) 110 | } 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /validators/docker_validator.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package system 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | "os/exec" 24 | "regexp" 25 | "strings" 26 | ) 27 | 28 | var _ Validator = &DockerValidator{} 29 | 30 | // DockerValidator validates docker configuration. 31 | type DockerValidator struct { 32 | Reporter Reporter 33 | } 34 | 35 | // dockerInfo holds a local subset of the Info struct from 36 | // https://github.com/docker/cli/blob/master/cli/command/system/info.go 37 | // and https://github.com/moby/moby/blob/master/api/types/types.go 38 | // The JSON output from 'docker info' should map to this struct. 39 | type dockerInfo struct { 40 | Driver string `json:"Driver"` 41 | ServerVersion string `json:"ServerVersion"` 42 | ServerErrors []string `json:",omitempty"` 43 | } 44 | 45 | // Name is part of the system.Validator interface. 46 | func (d *DockerValidator) Name() string { 47 | return "docker" 48 | } 49 | 50 | const ( 51 | dockerConfigPrefix = "DOCKER_" 52 | latestValidatedDockerVersion = "20.10" 53 | ) 54 | 55 | // Validate is part of the system.Validator interface. 56 | // TODO(random-liu): Add more validating items. 57 | func (d *DockerValidator) Validate(spec SysSpec) ([]error, []error) { 58 | if spec.RuntimeSpec.DockerSpec == nil { 59 | // If DockerSpec is not specified, assume current runtime is not 60 | // docker, skip the docker configuration validation. 61 | return nil, nil 62 | } 63 | 64 | // Run 'docker info' with a JSON output and unmarshal it into a dockerInfo object 65 | info := dockerInfo{} 66 | cmd := exec.Command("docker", "info", "--format", "{{json .}}") 67 | 68 | // Stderr can contain warnings despite docker info success. 69 | var outb, errb bytes.Buffer 70 | cmd.Stdout = &outb 71 | cmd.Stderr = &errb 72 | err := cmd.Run() 73 | if err != nil { 74 | return nil, []error{fmt.Errorf(`failed executing "docker info --format '{{json .}}'"\noutput: %s\nstderr: %s\nerror: %v`, outb.String(), errb.String(), err)} 75 | } 76 | if err := d.unmarshalDockerInfo(outb.Bytes(), &info); err != nil { 77 | return nil, []error{err} 78 | } 79 | 80 | // validate the resulted docker info object against the spec 81 | warnings, errs := d.validateDockerInfo(spec.RuntimeSpec.DockerSpec, info) 82 | 83 | if len(errb.String()) > 0 { 84 | warnings = append(warnings, fmt.Errorf(`the command "docker info --format '{{json.}}'" succeeded with potential warnings\noutput: %s`, errb.String())) 85 | } 86 | return warnings, errs 87 | } 88 | 89 | func (d *DockerValidator) unmarshalDockerInfo(b []byte, info *dockerInfo) error { 90 | if err := json.Unmarshal(b, &info); err != nil { 91 | return fmt.Errorf("could not unmarshal the JSON output of 'docker info':\n%s\n err: %w", b, err) 92 | } 93 | return nil 94 | } 95 | 96 | func (d *DockerValidator) validateDockerInfo(spec *DockerSpec, info dockerInfo) ([]error, []error) { 97 | // Validate docker version. 98 | if info.ServerErrors != nil { 99 | return nil, []error{fmt.Errorf("error verifying Docker info: %q", strings.Join(info.ServerErrors, `", "`))} 100 | } 101 | 102 | matched := false 103 | for _, v := range spec.Version { 104 | r := regexp.MustCompile(v) 105 | if r.MatchString(info.ServerVersion) { 106 | d.Reporter.Report(dockerConfigPrefix+"VERSION", info.ServerVersion, good) 107 | matched = true 108 | } 109 | } 110 | if !matched { 111 | // If it's of the new Docker version scheme but didn't match above, it 112 | // must be a newer version than the most recently validated one. 113 | ver := `\d{2}\.\d+\.\d+(?:-[a-z]{2})?` 114 | r := regexp.MustCompile(ver) 115 | if r.MatchString(info.ServerVersion) { 116 | d.Reporter.Report(dockerConfigPrefix+"VERSION", info.ServerVersion, good) 117 | w := fmt.Errorf( 118 | "this Docker version is not on the list of validated versions: %s. Latest validated version: %s", 119 | info.ServerVersion, 120 | latestValidatedDockerVersion, 121 | ) 122 | return []error{w}, nil 123 | } 124 | d.Reporter.Report(dockerConfigPrefix+"VERSION", info.ServerVersion, bad) 125 | return nil, []error{fmt.Errorf("unsupported docker version: %s", info.ServerVersion)} 126 | } 127 | // Validate graph driver. 128 | item := dockerConfigPrefix + "GRAPH_DRIVER" 129 | for _, gd := range spec.GraphDriver { 130 | if info.Driver == gd { 131 | d.Reporter.Report(item, info.Driver, good) 132 | return nil, nil 133 | } 134 | } 135 | d.Reporter.Report(item, info.Driver, bad) 136 | return nil, []error{fmt.Errorf("unsupported graph driver: %s", info.Driver)} 137 | } 138 | -------------------------------------------------------------------------------- /hack/verify-boilerplate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "os" 23 | "regexp" 24 | "strings" 25 | ) 26 | 27 | const ( 28 | yearPlaceholder = "YEAR" 29 | boilerPlateStart = "Copyright " 30 | boilerPlateEnd = "limitations under the License." 31 | ) 32 | 33 | var ( 34 | supportedExt = []string{".go", ".py", ".sh"} 35 | yearRegexp = regexp.MustCompile("(20)[0-9][0-9]") 36 | boilerPlate = []string{ 37 | boilerPlateStart + yearPlaceholder + " The Kubernetes Authors.", 38 | "", 39 | `Licensed under the Apache License, Version 2.0 (the "License");`, 40 | "you may not use this file except in compliance with the License.", 41 | "You may obtain a copy of the License at", 42 | "", 43 | " http://www.apache.org/licenses/LICENSE-2.0", 44 | "", 45 | "Unless required by applicable law or agreed to in writing, software", 46 | `distributed under the License is distributed on an "AS IS" BASIS,`, 47 | "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.", 48 | "See the License for the specific language governing permissions and", 49 | boilerPlateEnd, 50 | } 51 | ) 52 | 53 | // trimLeadingComment strips a single line comment characters such as # or // 54 | // at the exact beginning of a line, but also the first possible space character after it. 55 | func trimLeadingComment(line, c string) string { 56 | if strings.Index(line, c) == 0 { 57 | x := len(c) 58 | if len(line) == x { 59 | return "" 60 | } 61 | if line[x] == byte(' ') { 62 | return line[x+1:] 63 | } 64 | return line[x:] 65 | } 66 | return line 67 | } 68 | 69 | // verifyFileExtension verifies if the file extensions is supported 70 | func isSupportedFileExtension(filePath string) bool { 71 | // check if the file has an extension 72 | idx := strings.LastIndex(filePath, ".") 73 | if idx == -1 { 74 | return false 75 | } 76 | 77 | // check if the file has a supported extension 78 | ext := filePath[idx : idx+len(filePath)-idx] 79 | for _, e := range supportedExt { 80 | if e == ext { 81 | return true 82 | } 83 | } 84 | return false 85 | } 86 | 87 | // verifyBoilerplate verifies if a string contains the boilerplate 88 | func verifyBoilerplate(contents string) error { 89 | idx := 0 90 | foundBoilerplateStart := false 91 | lines := strings.Split(contents, "\n") 92 | for _, line := range lines { 93 | // handle leading comments 94 | line = trimLeadingComment(line, "//") 95 | line = trimLeadingComment(line, "#") 96 | 97 | // find the start of the boilerplate 98 | bpLine := boilerPlate[idx] 99 | if strings.Contains(line, boilerPlateStart) { 100 | foundBoilerplateStart = true 101 | 102 | // validate the year of the copyright 103 | yearWords := strings.Split(line, " ") 104 | expectedLen := len(strings.Split(boilerPlate[0], " ")) 105 | if len(yearWords) != expectedLen { 106 | return fmt.Errorf("copyright line should contain exactly %d words", expectedLen) 107 | } 108 | if !yearRegexp.MatchString(yearWords[1]) { 109 | return errors.New("cannot parse the year in the copyright line") 110 | } 111 | bpLine = strings.ReplaceAll(bpLine, yearPlaceholder, yearWords[1]) 112 | } 113 | 114 | // match line by line 115 | if foundBoilerplateStart { 116 | if line != bpLine { 117 | return fmt.Errorf("boilerplate line %d does not match\nexpected: %q\ngot: %q", idx+1, bpLine, line) 118 | } 119 | idx++ 120 | // exit after the last line is found 121 | if strings.Index(line, boilerPlateEnd) == 0 { 122 | break 123 | } 124 | } 125 | } 126 | 127 | if !foundBoilerplateStart { 128 | return errors.New("the file is missing a boilerplate") 129 | } 130 | if idx < len(boilerPlate) { 131 | return errors.New("boilerplate has missing lines") 132 | } 133 | return nil 134 | } 135 | 136 | // verifyFile verifies if a file contains the boilerplate 137 | func verifyFile(filePath string) error { 138 | if len(filePath) == 0 { 139 | return errors.New("empty file name") 140 | } 141 | 142 | if !isSupportedFileExtension(filePath) { 143 | fmt.Printf("skipping %q: unsupported file type\n", filePath) 144 | return nil 145 | } 146 | 147 | // read the file 148 | b, err := os.ReadFile(filePath) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | return verifyBoilerplate(string(b)) 154 | } 155 | 156 | func main() { 157 | if len(os.Args) < 2 { 158 | fmt.Println("usage: " + 159 | "go run verify-boilerplate.go ...") 160 | os.Exit(1) 161 | } 162 | 163 | hasErr := false 164 | for _, filePath := range os.Args[1:] { 165 | if err := verifyFile(filePath); err != nil { 166 | fmt.Printf("error validating %q: %v\n", filePath, err) 167 | hasErr = true 168 | } 169 | } 170 | if hasErr { 171 | os.Exit(1) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /validators/types_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | /* 5 | Copyright 2017 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | import ( 23 | "fmt" 24 | "strings" 25 | 26 | "golang.org/x/sys/unix" 27 | ) 28 | 29 | // DefaultSysSpec is the default SysSpec for Linux 30 | var DefaultSysSpec = SysSpec{ 31 | OS: "Linux", 32 | KernelSpec: KernelSpec{ 33 | // 5.4, 5.10, 5.15 is an active kernel Long Term Support (LTS) release, tracked in https://www.kernel.org/category/releases.html. 34 | Versions: []string{`^5\.4.*$`, `^5\.10.*$`, `^5\.15.*$`, `^([6-9]|[1-9][0-9]+)\.([0-9]+)\.([0-9]+).*$`}, 35 | VersionsNote: "Supported LTS versions from the 5.x series are 5.4, 5.10 and 5.15. Any 6.x version is also supported. For cgroups v2 support, the recommended version is 5.10 or newer", 36 | // TODO(random-liu): Add more config 37 | // TODO(random-liu): Add description for each kernel configuration: 38 | Required: []KernelConfig{ 39 | {Name: "NAMESPACES"}, 40 | {Name: "NET_NS"}, 41 | {Name: "PID_NS"}, 42 | {Name: "IPC_NS"}, 43 | {Name: "UTS_NS"}, 44 | {Name: "CPUSETS"}, 45 | {Name: "MEMCG"}, 46 | {Name: "INET"}, 47 | {Name: "EXT4_FS"}, 48 | {Name: "PROC_FS"}, 49 | {Name: "NETFILTER_XT_TARGET_REDIRECT", Aliases: []string{"IP_NF_TARGET_REDIRECT"}}, 50 | {Name: "NETFILTER_XT_MATCH_COMMENT"}, 51 | {Name: "FAIR_GROUP_SCHED"}, 52 | }, 53 | RequiredCgroupsV1: []KernelConfig{ 54 | {Name: "CGROUPS", Description: "Required for cgroups."}, 55 | {Name: "CGROUP_CPUACCT", Description: "Required for cpuacct controller, used in simple CPU accounting controller."}, 56 | {Name: "CGROUP_DEVICE", Description: "Required for device controller."}, 57 | {Name: "CGROUP_FREEZER", Description: "Required for freezer controller."}, 58 | {Name: "CGROUP_PIDS", Description: "Required for PIDs controller."}, 59 | {Name: "CGROUP_SCHED", Description: "Required for CPU controller."}, 60 | }, 61 | RequiredCgroupsV2: []KernelConfig{ 62 | {Name: "CGROUPS", Description: "Required for cgroups."}, 63 | {Name: "CGROUP_BPF", Description: "Required for eBPF programs attached to cgroups, used in device controller."}, 64 | {Name: "CGROUP_PIDS", Description: "Required for PIDs controller."}, 65 | {Name: "CGROUP_SCHED", Description: "Required for CPU controller."}, 66 | }, 67 | Optional: []KernelConfig{ 68 | {Name: "OVERLAY_FS", Aliases: []string{"OVERLAYFS_FS"}, Description: "Required for overlayfs."}, 69 | {Name: "AUFS_FS", Description: "Required for aufs."}, 70 | {Name: "BLK_DEV_DM", Description: "Required for devicemapper."}, 71 | {Name: "CFS_BANDWIDTH", Description: "Required for CPU quota."}, 72 | {Name: "SECCOMP", Description: "Required for seccomp."}, 73 | {Name: "SECCOMP_FILTER", Description: "Required for seccomp mode 2."}, 74 | }, 75 | OptionalCgroupsV1: []KernelConfig{ 76 | {Name: "CGROUP_HUGETLB", Description: "Required for hugetlb cgroup."}, 77 | }, 78 | OptionalCgroupsV2: []KernelConfig{ 79 | {Name: "CGROUP_HUGETLB", Description: "Required for hugetlb cgroup."}, 80 | }, 81 | Forbidden: []KernelConfig{}, 82 | }, 83 | Cgroups: []string{"cpu", "cpuacct", "cpuset", "devices", "freezer", "memory", "pids"}, 84 | CgroupsOptional: []string{ 85 | // The hugetlb cgroup is optional since some kernels are compiled without support for huge pages 86 | // and therefore lacks corresponding hugetlb cgroup 87 | "hugetlb", 88 | // The blkio cgroup is optional since some kernels are compiled without support for block I/O throttling. 89 | // Containerd and cri-o will use blkio to track disk I/O and throttling in both cgroups v1 and v2. 90 | "blkio", 91 | }, 92 | CgroupsV2: []string{"cpu", "cpuset", "devices", "freezer", "memory", "pids"}, 93 | CgroupsV2Optional: []string{ 94 | "hugetlb", 95 | // The cgroups v2 io controller is the successor of the v1 blkio controller. 96 | "io", 97 | }, 98 | RuntimeSpec: RuntimeSpec{ 99 | DockerSpec: &DockerSpec{ 100 | Version: []string{`1\.1[1-3]\..*`, `17\.0[3,6,9]\..*`, `18\.0[6,9]\..*`, `19\.03\..*`, `20\.10\..*`}, 101 | GraphDriver: []string{"aufs", "btrfs", "overlay", "overlay2", "devicemapper", "zfs"}, 102 | }, 103 | }, 104 | } 105 | 106 | // KernelValidatorHelperImpl is the 'linux' implementation of KernelValidatorHelper 107 | type KernelValidatorHelperImpl struct{} 108 | 109 | var _ KernelValidatorHelper = &KernelValidatorHelperImpl{} 110 | 111 | // GetKernelReleaseVersion returns the kernel release version (ex. 4.4.0-96-generic) as a string 112 | func (o *KernelValidatorHelperImpl) GetKernelReleaseVersion() (string, error) { 113 | return getKernelRelease() 114 | } 115 | 116 | // getKernelRelease returns the kernel release of the local machine. 117 | func getKernelRelease() (string, error) { 118 | var utsname unix.Utsname 119 | err := unix.Uname(&utsname) 120 | if err != nil { 121 | return "", fmt.Errorf("failed to get kernel release: %w", err) 122 | } 123 | return strings.TrimSpace(unix.ByteSliceToString(utsname.Release[:])), nil 124 | } 125 | -------------------------------------------------------------------------------- /validators/docker_validator_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package system 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestValidateDockerInfo(t *testing.T) { 27 | v := &DockerValidator{ 28 | Reporter: DefaultReporter, 29 | } 30 | spec := &DockerSpec{ 31 | Version: []string{`1\.13\..*`, `17\.0[3,6,9]\..*`, `18\.0[6,9]\..*`, `19\.03\..*`, `20\.10\..*`}, 32 | GraphDriver: []string{"driver_1", "driver_2"}, 33 | } 34 | for _, test := range []struct { 35 | name string 36 | info dockerInfo 37 | err bool 38 | warn bool 39 | }{ 40 | { 41 | name: "unsupported Docker version 1.12.1", 42 | info: dockerInfo{Driver: "driver_2", ServerVersion: "1.12.1"}, 43 | err: true, 44 | warn: false, 45 | }, 46 | { 47 | name: "unsupported driver", 48 | info: dockerInfo{Driver: "bad_driver", ServerVersion: "1.13.1"}, 49 | err: true, 50 | warn: false, 51 | }, 52 | { 53 | name: "valid Docker version 1.13.1", 54 | info: dockerInfo{Driver: "driver_1", ServerVersion: "1.13.1"}, 55 | err: false, 56 | warn: false, 57 | }, 58 | { 59 | name: "valid Docker version 17.03.0-ce", 60 | info: dockerInfo{Driver: "driver_2", ServerVersion: "17.03.0-ce"}, 61 | err: false, 62 | warn: false, 63 | }, 64 | { 65 | name: "valid Docker version 17.06.0-ce", 66 | info: dockerInfo{Driver: "driver_2", ServerVersion: "17.06.0-ce"}, 67 | err: false, 68 | warn: false, 69 | }, 70 | { 71 | name: "valid Docker version 17.09.0-ce", 72 | info: dockerInfo{Driver: "driver_2", ServerVersion: "17.09.0-ce"}, 73 | err: false, 74 | warn: false, 75 | }, 76 | { 77 | name: "valid Docker version 18.06.0-ce", 78 | info: dockerInfo{Driver: "driver_2", ServerVersion: "18.06.0-ce"}, 79 | err: false, 80 | warn: false, 81 | }, 82 | { 83 | name: "valid Docker version 18.09.1-ce", 84 | info: dockerInfo{Driver: "driver_2", ServerVersion: "18.09.1-ce"}, 85 | err: false, 86 | warn: false, 87 | }, 88 | { 89 | name: "valid Docker version 19.03.1-ce", 90 | info: dockerInfo{Driver: "driver_2", ServerVersion: "19.03.1-ce"}, 91 | err: false, 92 | warn: false, 93 | }, 94 | { 95 | name: "Docker version 19.06.0 is not in the list of validated versions", 96 | info: dockerInfo{Driver: "driver_2", ServerVersion: "19.06.0"}, 97 | err: false, 98 | warn: true, 99 | }, 100 | { 101 | name: "valid Docker version 20.10.3", 102 | info: dockerInfo{Driver: "driver_2", ServerVersion: "20.10.3"}, 103 | err: false, 104 | warn: false, 105 | }, 106 | { 107 | name: "Docker daemon not available", 108 | info: dockerInfo{ServerErrors: []string{"Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running"}}, 109 | err: true, 110 | warn: false, 111 | }, 112 | } { 113 | t.Run(test.name, func(t *testing.T) { 114 | warn, err := v.validateDockerInfo(spec, test.info) 115 | if !test.err { 116 | assert.Nil(t, err, "Expect error not to occur with docker info %+v", test.info) 117 | } else { 118 | assert.NotNil(t, err, "Expect error to occur with docker info %+v", test.info) 119 | } 120 | if !test.warn { 121 | assert.Nil(t, warn, "Expect error not to occur with docker info %+v", test.info) 122 | } else { 123 | assert.NotNil(t, warn, "Expect error to occur with docker info %+v", test.info) 124 | } 125 | }) 126 | } 127 | } 128 | 129 | func TestUnmarshalDockerInfo(t *testing.T) { 130 | v := &DockerValidator{} 131 | 132 | testCases := []struct { 133 | name string 134 | input string 135 | expectedInfo dockerInfo 136 | expectedError bool 137 | }{ 138 | { 139 | name: "valid: expected dockerInfo is valid", 140 | input: `{ "Driver":"foo", "ServerVersion":"bar" }`, 141 | expectedInfo: dockerInfo{Driver: "foo", ServerVersion: "bar"}, 142 | }, 143 | { 144 | name: "valid: expected dockerInfo is valid although daemon is not running", 145 | input: `{ "ServerErrors":["Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?"]}`, 146 | expectedInfo: dockerInfo{ServerErrors: []string{"Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?"}}, 147 | }, 148 | { 149 | name: "invalid: the JSON input is not valid", 150 | input: `{ "Driver":"foo"`, 151 | expectedError: true, 152 | }, 153 | } 154 | 155 | for _, tc := range testCases { 156 | t.Run(tc.name, func(t *testing.T) { 157 | var err error 158 | info := dockerInfo{} 159 | if err = v.unmarshalDockerInfo([]byte(tc.input), &info); (err != nil) != tc.expectedError { 160 | t.Fatalf("failed unmarshaling; expected error: %v, got: %v, error: %v", tc.expectedError, (err != nil), err) 161 | } 162 | if err != nil { 163 | return 164 | } 165 | if !reflect.DeepEqual(tc.expectedInfo, info) { 166 | t.Fatalf("dockerInfo do not match, expected: %#v, got: %#v", tc.expectedInfo, info) 167 | } 168 | }) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /validators/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package system 18 | 19 | // KernelConfig defines one kernel configuration item. 20 | type KernelConfig struct { 21 | // Name is the general name of the kernel configuration. It is used to 22 | // match kernel configuration. 23 | Name string `json:"name,omitempty"` 24 | // TODO(yguo0905): Support the "or" operation, which will be the same 25 | // as the "aliases". 26 | // 27 | // Aliases are aliases of the kernel configuration. Some configuration 28 | // has different names in different kernel version. Names of different 29 | // versions will be treated as aliases. 30 | Aliases []string `json:"aliases,omitempty"` 31 | // Description is the description of the kernel configuration, for example: 32 | // * What is it used for? 33 | // * Why is it needed? 34 | // * Who needs it? 35 | Description string `json:"description,omitempty"` 36 | } 37 | 38 | // KernelSpec defines the specification for the kernel. Currently, it contains 39 | // specification for: 40 | // - Kernel Version 41 | // - Kernel Configuration 42 | type KernelSpec struct { 43 | // Versions define supported kernel version. It is a group of regexps. 44 | Versions []string `json:"versions,omitempty"` 45 | // VersionsNote provides additional information if Versions do not match. 46 | VersionsNote string `json:"versionsNote,omitempty"` 47 | // Required contains all kernel configurations required to be enabled 48 | // (built in or as module). 49 | // RequiredCgroupsV1 and RequiredCgroupsV2 are mutually exclusive. 50 | Required []KernelConfig `json:"required,omitempty"` 51 | // RequiredCgroupsV1 contains all kernel configurations required to be enabled for cgroups v1. 52 | RequiredCgroupsV1 []KernelConfig `json:"requiredCgroupsV1,omitempty"` 53 | // RequiredCgroupsV2 contains all kernel configurations required to be enabled for cgroups v2. 54 | RequiredCgroupsV2 []KernelConfig `json:"requiredCgroupsV2,omitempty"` 55 | // Optional contains optional kernel configurations. 56 | Optional []KernelConfig `json:"optional,omitempty"` 57 | // OptionalCgroupsV1 contains optional kernel configurations related to cgroups v1. 58 | OptionalCgroupsV1 []KernelConfig `json:"optionalCgroupsV1,omitempty"` 59 | // OptionalCgroupsV2 contains optional kernel configurations related to cgroups v2. 60 | OptionalCgroupsV2 []KernelConfig `json:"optionalCgroupsV2,omitempty"` 61 | // Forbidden contains all kernel configurations which areforbidden (disabled 62 | // or not set) 63 | Forbidden []KernelConfig `json:"forbidden,omitempty"` 64 | } 65 | 66 | // DockerSpec defines the requirement configuration for docker. Currently, it only 67 | // contains spec for graph driver. 68 | type DockerSpec struct { 69 | // Version is a group of regex matching supported docker versions. 70 | Version []string `json:"version,omitempty"` 71 | // GraphDriver is the graph drivers supported by kubelet. 72 | GraphDriver []string `json:"graphDriver,omitempty"` 73 | } 74 | 75 | // RuntimeSpec is the abstract layer for different runtimes. Different runtimes 76 | // should put their spec inside the RuntimeSpec. 77 | type RuntimeSpec struct { 78 | *DockerSpec `json:",inline"` 79 | } 80 | 81 | // PackageSpec defines the required packages and their versions. 82 | // PackageSpec is only supported on OS distro with Debian package manager. 83 | // 84 | // TODO(yguo0905): Support operator OR of multiple packages for the case where 85 | // either "foo (>=1.0)" or "bar (>=2.0)" is required. 86 | type PackageSpec struct { 87 | // Name is the name of the package to be checked. 88 | Name string `json:"name,omitempty"` 89 | // VersionRange represents a range of versions that the package must 90 | // satisfy. Note that the version requirement will not be enforced if 91 | // the version range is empty. For example, 92 | // - "" would match any versions but the package must be installed. 93 | // - ">=1" would match "1.0.0", "1.0.1", "1.1.0", and "2.0". 94 | // - ">1.0 <2.0" would match between both ranges, so "1.1.1" and "1.8.7" 95 | // but not "1.0.0" or "2.0.0". 96 | // - "<2.0.0 || >=3.0.0" would match "1.0.0" and "3.0.0" but not "2.0.0". 97 | VersionRange string `json:"versionRange,omitempty"` 98 | // Description explains the reason behind this package requirements. 99 | // 100 | // TODO(yguo0905): Print the description where necessary. 101 | Description string `json:"description,omitempty"` 102 | } 103 | 104 | // PackageSpecOverride defines the overrides on the PackageSpec for an OS 105 | // distro. 106 | type PackageSpecOverride struct { 107 | // OSDistro identifies to which OS distro this override applies. 108 | // Must be "ubuntu", "cos" or "coreos". 109 | OSDistro string `json:"osDistro,omitempty"` 110 | // Subtractions is a list of package names that are excluded from the 111 | // package spec. 112 | Subtractions []PackageSpec `json:"subtractions,omitempty"` 113 | // Additions is a list of additional package requirements included the 114 | // package spec. 115 | Additions []PackageSpec `json:"additions,omitempty"` 116 | } 117 | 118 | // SysSpec defines the requirement of supported system. Currently, it only contains 119 | // spec for OS, Kernel and Cgroups. 120 | type SysSpec struct { 121 | // OS is the operating system of the SysSpec. 122 | OS string `json:"os,omitempty"` 123 | // KernelConfig defines the spec for kernel. 124 | KernelSpec KernelSpec `json:"kernelSpec,omitempty"` 125 | 126 | // Cgroups is the required cgroups. 127 | Cgroups []string `json:"cgroups,omitempty"` 128 | // CgroupsOptional is the optional cgroups. 129 | CgroupsOptional []string `json:"cgroupsOptional,omitempty"` 130 | // CgroupsV2 is the required cgroups v2. 131 | CgroupsV2 []string `json:"cgroupsV2,omitempty"` 132 | // CgroupsV2Optional is the optional cgroups v2. 133 | CgroupsV2Optional []string `json:"cgroupsV2Optional,omitempty"` 134 | 135 | // RuntimeSpec defines the spec for runtime. 136 | RuntimeSpec RuntimeSpec `json:"runtimeSpec,omitempty"` 137 | // PackageSpec defines the required packages and their versions. 138 | PackageSpecs []PackageSpec `json:"packageSpecs,omitempty"` 139 | // PackageSpec defines the overrides of the required packages and their 140 | // versions for an OS distro. 141 | PackageSpecOverrides []PackageSpecOverride `json:"packageSpecOverrides,omitempty"` 142 | } 143 | -------------------------------------------------------------------------------- /validators/kernel_validator_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package system 18 | 19 | import ( 20 | "bytes" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestValidateKernelVersion(t *testing.T) { 27 | v := &KernelValidator{ 28 | Reporter: DefaultReporter, 29 | } 30 | // Currently, testRegex is align with DefaultSysSpec.KernelVersion, but in the future 31 | // they may be different. 32 | // This is fine, because the test mainly tests the kernel version validation logic, 33 | // not the DefaultSysSpec. The DefaultSysSpec should be tested with node e2e. 34 | testRegex := []string{`^5\.4.*$`, `^5\.10.*$`, `^5\.15.*$`, `^([6-9]|[1-9][0-9]+)\.([0-9]+)\.([0-9]+).*$`} 35 | for _, test := range []struct { 36 | name string 37 | version string 38 | err bool 39 | }{ 40 | { 41 | name: "2.0.0 no version regex matches", 42 | version: "2.0.0", 43 | err: true, 44 | }, 45 | { 46 | name: "3.9.0 no version regex matches", 47 | version: "3.9.0", 48 | err: true, 49 | }, 50 | { 51 | name: "3.19.9-99-test no version regex matches", 52 | version: "3.19.9-99-test", 53 | err: true, 54 | }, 55 | { 56 | name: "4.4.14+ no version regex matches", 57 | version: "4.4.14+", 58 | err: true, 59 | }, 60 | { 61 | name: "4.17.3 no version regex matches", 62 | version: "4.17.3", 63 | err: true, 64 | }, 65 | { 66 | name: "4.19.3-99-test no version regex matches", 67 | version: "4.19.3-99-test", 68 | err: true, 69 | }, 70 | { 71 | name: "4.20.3+ no version regex matches", 72 | version: "4.20.3+", 73 | err: true, 74 | }, 75 | { 76 | name: "5.0.0 no version regexes matches", 77 | version: "5.0.0", 78 | err: true, 79 | }, 80 | { 81 | name: "5.4.288 matches", 82 | version: "5.4.288", 83 | err: false, 84 | }, 85 | { 86 | name: "5.10.232 matches", 87 | version: "5.10.232", 88 | err: false, 89 | }, 90 | { 91 | name: "5.12.3 no version regex matches(no lts version)", 92 | version: "5.12.3", 93 | err: true, 94 | }, 95 | { 96 | name: "5.15.175 matches", 97 | version: "5.15.175", 98 | err: false, 99 | }, 100 | { 101 | name: "5.16.3 no version regex matches(no such version)", 102 | version: "5.16.3", 103 | err: true, 104 | }, 105 | { 106 | name: "6.12.8 matches", 107 | version: "6.12.8", 108 | err: false, 109 | }, 110 | { 111 | name: "10.21.1 one of version regexes matches", 112 | version: "10.21.1", 113 | err: false, 114 | }, 115 | { 116 | name: "99.12.12 one of version regexes matches", 117 | version: "99.12.12", 118 | err: false, 119 | }, 120 | } { 121 | t.Run(test.name, func(t *testing.T) { 122 | v.kernelRelease = test.version 123 | err := v.validateKernelVersion(KernelSpec{Versions: testRegex}) 124 | if !test.err { 125 | assert.Nil(t, err, "Expect error not to occur with kernel version %q", test.version) 126 | } else { 127 | assert.NotNil(t, err, "Expect error to occur with kernel version %q", test.version) 128 | } 129 | }) 130 | } 131 | } 132 | 133 | func TestValidateCachedKernelConfig(t *testing.T) { 134 | v := &KernelValidator{ 135 | Reporter: DefaultReporter, 136 | } 137 | testKernelSpec := KernelSpec{ 138 | Required: []KernelConfig{{Name: "REQUIRED_1"}, {Name: "REQUIRED_2", Aliases: []string{"ALIASE_REQUIRED_2"}}}, 139 | Optional: []KernelConfig{{Name: "OPTIONAL_1"}, {Name: "OPTIONAL_2"}}, 140 | Forbidden: []KernelConfig{ 141 | {Name: "FORBIDDEN_1", Description: "TEST FORBIDDEN"}, 142 | {Name: "FORBIDDEN_2", Aliases: []string{"ALIASE_FORBIDDEN_2"}}, 143 | }, 144 | } 145 | for _, test := range []struct { 146 | desc string 147 | config map[string]kConfigOption 148 | err bool 149 | }{ 150 | { 151 | desc: "meet all required configurations should not report error", 152 | config: map[string]kConfigOption{ 153 | "REQUIRED_1": builtIn, 154 | "REQUIRED_2": asModule, 155 | }, 156 | err: false, 157 | }, 158 | { 159 | desc: "one required configuration disabled should report error", 160 | config: map[string]kConfigOption{ 161 | "REQUIRED_1": leftOut, 162 | "REQUIRED_2": builtIn, 163 | }, 164 | err: true, 165 | }, 166 | { 167 | desc: "one required configuration missing should report error", 168 | config: map[string]kConfigOption{ 169 | "REQUIRED_1": builtIn, 170 | }, 171 | err: true, 172 | }, 173 | { 174 | desc: "alias of required configuration should not report error", 175 | config: map[string]kConfigOption{ 176 | "REQUIRED_1": builtIn, 177 | "ALIASE_REQUIRED_2": asModule, 178 | }, 179 | err: false, 180 | }, 181 | { 182 | desc: "optional configuration set or not should not report error", 183 | config: map[string]kConfigOption{ 184 | "REQUIRED_1": builtIn, 185 | "REQUIRED_2": asModule, 186 | "OPTIONAL_1": builtIn, 187 | }, 188 | err: false, 189 | }, 190 | { 191 | desc: "forbidden configuration disabled should not report error", 192 | config: map[string]kConfigOption{ 193 | "REQUIRED_1": builtIn, 194 | "REQUIRED_2": asModule, 195 | "FORBIDDEN_1": leftOut, 196 | }, 197 | err: false, 198 | }, 199 | { 200 | desc: "forbidden configuration built-in should report error", 201 | config: map[string]kConfigOption{ 202 | "REQUIRED_1": builtIn, 203 | "REQUIRED_2": asModule, 204 | "FORBIDDEN_1": builtIn, 205 | }, 206 | err: true, 207 | }, 208 | { 209 | desc: "forbidden configuration built as module should report error", 210 | config: map[string]kConfigOption{ 211 | "REQUIRED_1": builtIn, 212 | "REQUIRED_2": asModule, 213 | "FORBIDDEN_1": asModule, 214 | }, 215 | err: true, 216 | }, 217 | { 218 | desc: "alias of forbidden configuration should report error", 219 | config: map[string]kConfigOption{ 220 | "REQUIRED_1": builtIn, 221 | "REQUIRED_2": asModule, 222 | "ALIASE_FORBIDDEN_2": asModule, 223 | }, 224 | err: true, 225 | }, 226 | } { 227 | t.Run(test.desc, func(t *testing.T) { 228 | // Add kernel config prefix. 229 | for k, v := range test.config { 230 | delete(test.config, k) 231 | test.config[kernelConfigPrefix+k] = v 232 | } 233 | err := v.validateCachedKernelConfig(test.config, testKernelSpec) 234 | if !test.err { 235 | assert.Nil(t, err, "Expect error not to occur with kernel config %q", test.config) 236 | } else { 237 | assert.NotNil(t, err, "Expect error to occur with kernel config %q", test.config) 238 | } 239 | }) 240 | } 241 | } 242 | 243 | func TestValidateParseKernelConfig(t *testing.T) { 244 | config := `CONFIG_1=y 245 | CONFIG_2=m 246 | CONFIG_3=n` 247 | expected := map[string]kConfigOption{ 248 | "CONFIG_1": builtIn, 249 | "CONFIG_2": asModule, 250 | "CONFIG_3": leftOut, 251 | } 252 | v := &KernelValidator{ 253 | Reporter: DefaultReporter, 254 | } 255 | got, err := v.parseKernelConfig(bytes.NewReader([]byte(config))) 256 | assert.Nil(t, err, "Expect error not to occur when parse kernel configuration %q", config) 257 | assert.Equal(t, expected, got) 258 | } 259 | -------------------------------------------------------------------------------- /validators/package_validator_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | /* 5 | Copyright 2017 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | import ( 23 | "errors" 24 | "fmt" 25 | "reflect" 26 | "testing" 27 | ) 28 | 29 | func TestExtractUpstreamVersion(t *testing.T) { 30 | for _, test := range []struct { 31 | input string 32 | expected string 33 | }{ 34 | { 35 | input: "", 36 | expected: "", 37 | }, 38 | { 39 | input: "1.0.6", 40 | expected: "1.0.6", 41 | }, 42 | { 43 | input: "1:1.0.6", 44 | expected: "1.0.6", 45 | }, 46 | { 47 | input: "1.0.6-2ubuntu2.1", 48 | expected: "1.0.6", 49 | }, 50 | { 51 | input: "1:1.0.6-2ubuntu2.1", 52 | expected: "1.0.6", 53 | }, 54 | } { 55 | t.Run(fmt.Sprintf("input:%s,expected:%s", test.input, test.expected), func(t *testing.T) { 56 | got := extractUpstreamVersion(test.input) 57 | if test.expected != got { 58 | t.Errorf("extractUpstreamVersion(%q) = %q, want %q", test.input, got, test.expected) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestToSemVer(t *testing.T) { 65 | for _, test := range []struct { 66 | input string 67 | expected string 68 | }{ 69 | { 70 | input: "", 71 | expected: "", 72 | }, 73 | { 74 | input: "1.2.3", 75 | expected: "1.2.3", 76 | }, 77 | { 78 | input: "1.8.19p1", 79 | expected: "1.8.19", 80 | }, 81 | { 82 | input: "1.8.19.p1", 83 | expected: "1.8.19", 84 | }, 85 | { 86 | input: "p1", 87 | expected: "", 88 | }, 89 | { 90 | input: "1.18", 91 | expected: "1.18.0", 92 | }, 93 | { 94 | input: "481", 95 | expected: "481.0.0", 96 | }, 97 | { 98 | input: "2.0.10.4", 99 | expected: "2.0.10", 100 | }, 101 | { 102 | input: "03", 103 | expected: "3.0.0", 104 | }, 105 | { 106 | input: "2.02", 107 | expected: "2.2.0", 108 | }, 109 | { 110 | input: "8.0.0095", 111 | expected: "8.0.95", 112 | }, 113 | } { 114 | t.Run(fmt.Sprintf("input:%s,expected:%s", test.input, test.expected), func(t *testing.T) { 115 | got := toSemVer(test.input) 116 | if test.expected != got { 117 | t.Errorf("toSemVer(%q) = %q, want %q", test.input, got, test.expected) 118 | } 119 | }) 120 | } 121 | } 122 | 123 | func TestToSemVerRange(t *testing.T) { 124 | for _, test := range []struct { 125 | input string 126 | expected string 127 | }{ 128 | { 129 | input: "", 130 | expected: "", 131 | }, 132 | { 133 | input: ">=1.0.0", 134 | expected: ">=1.0.0", 135 | }, 136 | { 137 | input: ">=1.0", 138 | expected: ">=1.0.x", 139 | }, 140 | { 141 | input: ">=1", 142 | expected: ">=1.x", 143 | }, 144 | { 145 | input: ">=1 || !2.3", 146 | expected: ">=1.x || !2.3.x", 147 | }, 148 | { 149 | input: ">1 || >3.1.0 !4.2", 150 | expected: ">1.x || >3.1.0 !4.2.x", 151 | }, 152 | } { 153 | t.Run(fmt.Sprintf("input:%s,expected:%s", test.input, test.expected), func(t *testing.T) { 154 | got := toSemVerRange(test.input) 155 | if test.expected != got { 156 | t.Errorf("toSemVerRange(%q) = %q, want %q", test.input, got, test.expected) 157 | } 158 | }) 159 | } 160 | } 161 | 162 | // testPackageManager implements the packageManager interface. 163 | type testPackageManager struct { 164 | packageVersions map[string]string 165 | } 166 | 167 | func (m testPackageManager) getPackageVersion(packageName string) (string, error) { 168 | if v, ok := m.packageVersions[packageName]; ok { 169 | return v, nil 170 | } 171 | return "", fmt.Errorf("package %q does not exist", packageName) 172 | } 173 | 174 | func TestValidatePackageVersion(t *testing.T) { 175 | testKernelRelease := "test-kernel-release" 176 | manager := testPackageManager{ 177 | packageVersions: map[string]string{ 178 | "foo": "1.0.0", 179 | "bar": "2.1.0", 180 | "bar-" + testKernelRelease: "3.0.0", 181 | }, 182 | } 183 | v := &packageValidator{ 184 | reporter: DefaultReporter, 185 | kernelRelease: testKernelRelease, 186 | } 187 | for _, test := range []struct { 188 | desc string 189 | specs []PackageSpec 190 | errs []error 191 | }{ 192 | { 193 | desc: "all packages meet the spec", 194 | specs: []PackageSpec{ 195 | {Name: "foo", VersionRange: ">=1.0"}, 196 | {Name: "bar", VersionRange: ">=2.0 <= 3.0"}, 197 | }, 198 | }, 199 | { 200 | desc: "package version does not meet the spec", 201 | specs: []PackageSpec{ 202 | {Name: "foo", VersionRange: ">=1.0"}, 203 | {Name: "bar", VersionRange: ">=3.0"}, 204 | }, 205 | errs: []error{ 206 | errors.New("package \"bar 2.1.0\" does not meet the spec \"bar (>=3.0)\""), 207 | }, 208 | }, 209 | { 210 | desc: "package not installed", 211 | specs: []PackageSpec{ 212 | {Name: "baz"}, 213 | }, 214 | errs: []error{ 215 | errors.New("package \"baz\" does not exist"), 216 | }, 217 | }, 218 | { 219 | desc: "use variable in package name", 220 | specs: []PackageSpec{ 221 | {Name: "bar-${KERNEL_RELEASE}", VersionRange: ">=3.0"}, 222 | }, 223 | }, 224 | } { 225 | t.Run(test.desc, func(t *testing.T) { 226 | _, errs := v.validate(test.specs, manager) 227 | if len(test.errs) == 0 && len(errs) > 0 { 228 | t.Errorf("%s: v.validate(): err = %s", test.desc, errs) 229 | } 230 | if len(test.errs) > 0 { 231 | if len(errs) == 0 { 232 | t.Errorf("%s: v.validate() is expected to fail.", test.desc) 233 | } else if len(errs) != len(test.errs) { 234 | t.Errorf("%s: v.validate(): errs = %q, want = %q", test.desc, errs, test.errs) 235 | } else { 236 | for i, v := range test.errs { 237 | if v.Error() != errs[i].Error() { 238 | t.Errorf("%s: v.validate(): errs = %q, want = %q", test.desc, errs, test.errs) 239 | } 240 | } 241 | } 242 | } 243 | }) 244 | } 245 | } 246 | 247 | func TestApplyPackageOverride(t *testing.T) { 248 | for _, test := range []struct { 249 | name string 250 | overrides []PackageSpecOverride 251 | osDistro string 252 | specs []PackageSpec 253 | expected []PackageSpec 254 | }{ 255 | { 256 | name: "foo>=1.0", 257 | specs: []PackageSpec{{Name: "foo", VersionRange: ">=1.0"}}, 258 | expected: []PackageSpec{{Name: "foo", VersionRange: ">=1.0"}}, 259 | }, 260 | { 261 | name: "ubuntu:foo>=1.0/bar>=2.0", 262 | osDistro: "ubuntu", 263 | overrides: []PackageSpecOverride{ 264 | { 265 | OSDistro: "ubuntu", 266 | Subtractions: []PackageSpec{{Name: "foo"}}, 267 | Additions: []PackageSpec{{Name: "bar", VersionRange: ">=2.0"}}, 268 | }, 269 | }, 270 | specs: []PackageSpec{{Name: "foo", VersionRange: ">=1.0"}}, 271 | expected: []PackageSpec{{Name: "bar", VersionRange: ">=2.0"}}, 272 | }, 273 | { 274 | name: "ubuntu:foo>=1.0/debian:foo", 275 | osDistro: "ubuntu", 276 | overrides: []PackageSpecOverride{ 277 | { 278 | OSDistro: "debian", 279 | Subtractions: []PackageSpec{{Name: "foo"}}, 280 | }, 281 | }, 282 | specs: []PackageSpec{{Name: "foo", VersionRange: ">=1.0"}}, 283 | expected: []PackageSpec{{Name: "foo", VersionRange: ">=1.0"}}, 284 | }, 285 | } { 286 | t.Run(test.name, func(t *testing.T) { 287 | got := applyPackageSpecOverride(test.specs, test.overrides, test.osDistro) 288 | if !reflect.DeepEqual(test.expected, got) { 289 | t.Errorf("applyPackageSpecOverride(%+v, %+v, %s) = %+v, want = %+v", test.specs, test.overrides, test.osDistro, got, test.expected) 290 | } 291 | }) 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /validators/cgroup_validator_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | /* 5 | Copyright 2016 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | import ( 23 | "os" 24 | "path/filepath" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestValidateCgroupSubsystem(t *testing.T) { 31 | // hardcoded cgroups v2 subsystems 32 | pseudoSubsystems := []string{"devices", "freezer"} 33 | 34 | v := &CgroupsValidator{ 35 | Reporter: DefaultReporter, 36 | } 37 | for desc, test := range map[string]struct { 38 | subsystems []string 39 | cgroupSpec []string 40 | required bool 41 | missing []string 42 | }{ 43 | "missing required cgroup subsystem should report missing": { 44 | cgroupSpec: []string{"system1", "system2"}, 45 | subsystems: []string{"system1"}, 46 | required: true, 47 | missing: []string{"system2"}, 48 | }, 49 | "missing optional cgroup subsystem should report missing": { 50 | cgroupSpec: []string{"system1", "system2"}, 51 | subsystems: []string{"system1"}, 52 | required: false, 53 | missing: []string{"system2"}, 54 | }, 55 | "extra cgroup subsystems should not report missing": { 56 | cgroupSpec: []string{"system1", "system2"}, 57 | subsystems: []string{"system1", "system2", "system3"}, 58 | required: true, 59 | missing: nil, 60 | }, 61 | "subsystems the same with spec should not report missing": { 62 | cgroupSpec: []string{"system1"}, 63 | subsystems: []string{"system1", "system2"}, 64 | required: false, 65 | missing: nil, 66 | }, 67 | "missing required cgroup subsystem when pseudo hardcoded subsystems are set": { 68 | cgroupSpec: []string{"system1", "devices", "freezer"}, 69 | subsystems: pseudoSubsystems, 70 | required: true, 71 | missing: []string{"system1"}, 72 | }, 73 | "missing optional cgroup subsystem when pseudo hardcoded subsystems are set": { 74 | cgroupSpec: []string{"system1", "devices", "freezer"}, 75 | subsystems: pseudoSubsystems, 76 | required: false, 77 | missing: []string{"system1"}, 78 | }, 79 | "extra cgroup subsystems when pseudo hardcoded subsystems are set": { 80 | cgroupSpec: []string{"system1", "devices", "freezer"}, 81 | subsystems: append(pseudoSubsystems, "system1", "system2"), 82 | required: true, 83 | missing: nil, 84 | }, 85 | "matching list of cgroup subsystems including pseudo hardcoded subsystems": { 86 | cgroupSpec: []string{"system1", "devices", "freezer"}, 87 | subsystems: append(pseudoSubsystems, "system1"), 88 | required: false, 89 | missing: nil, 90 | }, 91 | } { 92 | t.Run(desc, func(t *testing.T) { 93 | missing := v.validateCgroupSubsystems(test.cgroupSpec, test.subsystems, test.required) 94 | assert.Equal(t, test.missing, missing, "%q: Expect error not to occur with cgroup", desc) 95 | }) 96 | } 97 | } 98 | 99 | func TestGetUnifiedMountpoint(t *testing.T) { 100 | c, err := os.Open(filepath.Join(defaultUnifiedMountPoint, "cgroup.controllers")) 101 | if err == nil { 102 | defer c.Close() 103 | } 104 | tests := map[string]struct { 105 | mountsFileContent string 106 | expectedErr bool 107 | expectedPath string 108 | expectedIsCgroupsV2 bool 109 | // when /sys/fs/cgroup is mounted as tmpfs, 110 | // the cgroup version check depends on checking local dir: `/sys/fs/cgroup/memory` 111 | skipIsCgroupsV2Check bool 112 | }{ 113 | "cgroups v2": { 114 | mountsFileContent: "cgroup2 /sys/fs/cgroup cgroup2 rw,seclabel,nosuid,nodev,noexec,relatime 0 0", 115 | expectedErr: false, 116 | expectedPath: "/sys/fs/cgroup", 117 | expectedIsCgroupsV2: true, 118 | }, 119 | "cgroups v1": { 120 | mountsFileContent: "cgroup /sys/fs/cgroup cgroup rw,seclabel,nosuid,nodev,noexec,relatime 0 0", 121 | expectedErr: false, 122 | expectedPath: "/sys/fs/cgroup", 123 | }, 124 | "empty file": { 125 | mountsFileContent: "", 126 | expectedErr: true, 127 | expectedPath: "", 128 | }, 129 | "no cgroup mounts": { 130 | mountsFileContent: `proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0 131 | sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0`, 132 | expectedErr: true, 133 | expectedPath: "", 134 | }, 135 | "multiple cgroups v1 and v2": { 136 | mountsFileContent: `cgroup /sys/fs/cgroup/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset 137 | cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 138 | cgroup2 /sys/fs/cgroup/unified cgroup2 rw,seclabel,nosuid,nodev,noexec,relatime`, 139 | expectedErr: false, 140 | expectedPath: "/sys/fs/cgroup/unified", 141 | expectedIsCgroupsV2: true, 142 | }, 143 | "cgroups v1 only with multiple subsystems": { 144 | mountsFileContent: `cgroup /sys/fs/cgroup/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset 145 | cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory`, 146 | expectedErr: false, 147 | expectedPath: "/sys/fs/cgroup/cpuset", // First valid cgroups v1 path 148 | }, 149 | "no valid cgroup": { 150 | mountsFileContent: "proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0\nsysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0", 151 | expectedErr: true, 152 | expectedPath: "", 153 | }, 154 | "cgroups using tmpfs, v1 and v2": { 155 | mountsFileContent: `tmpfs /run tmpfs rw,nosuid,nodev,size=803108k,nr_inodes=819200,mode=755 0 0 156 | tmpfs /sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec,size=4096k,nr_inodes=1024,mode=755 0 0 157 | cgroup2 /sys/fs/cgroup/unified cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate 0 0 158 | cgroup /sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,xattr,name=systemd 0 0`, 159 | expectedErr: false, 160 | expectedPath: "/sys/fs/cgroup", 161 | skipIsCgroupsV2Check: true, 162 | }, 163 | "cgroups using tmpfs, v1": { 164 | mountsFileContent: `tmpfs /sys/fs/cgroup tmpfs ro,seclabel,nosuid,nodev,noexec,mode=755 0 0 165 | cgroup /sys/fs/cgroup/systemd cgroup rw,seclabel,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd 0 0 166 | cgroup /sys/fs/cgroup/net_cls,net_prio cgroup rw,seclabel,nosuid,nodev,noexec,relatime,net_cls,net_prio 0 0 167 | cgroup /sys/fs/cgroup/blkio cgroup rw,seclabel,nosuid,nodev,noexec,relatime,blkio 0 0 168 | cgroup /sys/fs/cgroup/memory cgroup rw,seclabel,nosuid,nodev,noexec,relatime,memory 0 0`, 169 | expectedErr: false, 170 | expectedPath: "/sys/fs/cgroup", 171 | skipIsCgroupsV2Check: true, 172 | }, 173 | } 174 | 175 | for desc, test := range tests { 176 | t.Run(desc, func(t *testing.T) { 177 | tmpFile, err := os.CreateTemp("", "mounts") 178 | assert.NoError(t, err, "Unexpected error creating temp file") 179 | defer os.Remove(tmpFile.Name()) 180 | 181 | _, err = tmpFile.Write([]byte(test.mountsFileContent)) 182 | assert.NoError(t, err, "Unexpected error writing to temp file") 183 | tmpFile.Close() 184 | 185 | path, isCgroupsV2, err := getUnifiedMountpoint(tmpFile.Name()) 186 | 187 | if test.expectedErr { 188 | assert.Error(t, err, "Expected error but got none") 189 | } else { 190 | assert.NoError(t, err, "Did not expect error but got one: %s", err) 191 | } 192 | 193 | assert.Equal(t, test.expectedPath, path, "Expected cgroup path mismatch") 194 | if !test.skipIsCgroupsV2Check { 195 | assert.Equal(t, test.expectedIsCgroupsV2, isCgroupsV2, "Expected cgroup version mismatch") 196 | } 197 | }) 198 | } 199 | } 200 | 201 | func TestIsCgroupsV1DisabledInKubelet(t *testing.T) { 202 | tests := []struct { 203 | name string 204 | version string 205 | expectedResult bool 206 | expectedError bool 207 | }{ 208 | { 209 | name: "invalid version", 210 | version: "foo", 211 | expectedError: true, 212 | }, 213 | { 214 | name: "empty version", 215 | version: "", 216 | expectedResult: false, 217 | }, 218 | { 219 | name: "version older than 1.35.0-0 causes a warning", 220 | version: "1.34.7", 221 | expectedResult: false, 222 | }, 223 | { 224 | name: "1.35.0 pre-release causes an error", 225 | version: "1.35.0-alpha.1", 226 | expectedResult: true, 227 | }, 228 | { 229 | name: "newer versions than 1.35 cause an error", 230 | version: "1.35.1", 231 | expectedResult: true, 232 | }, 233 | } 234 | 235 | v := CgroupsValidator{} 236 | 237 | for _, tc := range tests { 238 | t.Run(tc.name, func(t *testing.T) { 239 | v.KubeletVersion = tc.version 240 | result, err := v.isCgroupsV1DisabledInKubelet() 241 | 242 | if (err != nil) != tc.expectedError { 243 | t.Fatalf("expected error: %v, got: %v", tc.expectedError, err != nil) 244 | } 245 | if result != tc.expectedResult { 246 | t.Fatalf("expected result: %v, got: %v", tc.expectedResult, result) 247 | } 248 | }) 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /validators/kernel_validator.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package system 18 | 19 | import ( 20 | "bufio" 21 | "bytes" 22 | "compress/gzip" 23 | "fmt" 24 | "io" 25 | "os" 26 | "os/exec" 27 | "path/filepath" 28 | "regexp" 29 | "strings" 30 | ) 31 | 32 | var _ Validator = &KernelValidator{} 33 | 34 | // KernelValidator validates kernel. Currently only validate kernel version 35 | // and kernel configuration. 36 | type KernelValidator struct { 37 | kernelRelease string 38 | Reporter Reporter 39 | } 40 | 41 | // Name is part of the system.Validator interface. 42 | func (k *KernelValidator) Name() string { 43 | return "kernel" 44 | } 45 | 46 | // kConfigOption is the possible kernel config option. 47 | type kConfigOption string 48 | 49 | const ( 50 | builtIn kConfigOption = "y" 51 | asModule kConfigOption = "m" 52 | leftOut kConfigOption = "n" 53 | 54 | // validKConfigRegex is the regex matching kernel configuration line. 55 | validKConfigRegex = "^CONFIG_[A-Z0-9_]+=[myn]" 56 | // kernelConfigPrefix is the prefix of kernel configuration. 57 | kernelConfigPrefix = "CONFIG_" 58 | ) 59 | 60 | // Validate is part of the system.Validator interface. 61 | func (k *KernelValidator) Validate(spec SysSpec) ([]error, []error) { 62 | helper := KernelValidatorHelperImpl{} 63 | release, err := helper.GetKernelReleaseVersion() 64 | if err != nil { 65 | return nil, []error{fmt.Errorf("failed to get kernel release: %w", err)} 66 | } 67 | k.kernelRelease = release 68 | var errs, warns []error 69 | if warn := k.validateKernelVersion(spec.KernelSpec); warn != nil { 70 | warns = append(warns, warn) 71 | } 72 | // only validate kernel config when necessary (currently no kernel config for windows) 73 | if len(spec.KernelSpec.Required) > 0 || len(spec.KernelSpec.Forbidden) > 0 || len(spec.KernelSpec.Optional) > 0 || 74 | len(spec.KernelSpec.RequiredCgroupsV1) > 0 || len(spec.KernelSpec.RequiredCgroupsV2) > 0 { 75 | if err = k.validateKernelConfig(spec.KernelSpec); err != nil { 76 | errs = append(errs, err) 77 | } 78 | } 79 | return warns, errs 80 | } 81 | 82 | // validateKernelVersion validates the kernel version. 83 | func (k *KernelValidator) validateKernelVersion(kSpec KernelSpec) error { 84 | versionRegexps := kSpec.Versions 85 | for _, versionRegexp := range versionRegexps { 86 | r := regexp.MustCompile(versionRegexp) 87 | if r.MatchString(k.kernelRelease) { 88 | k.Reporter.Report("KERNEL_VERSION", k.kernelRelease, good) 89 | return nil 90 | } 91 | } 92 | k.Reporter.Report("KERNEL_VERSION", k.kernelRelease, bad) 93 | return fmt.Errorf("kernel release %s is unsupported. %s", k.kernelRelease, kSpec.VersionsNote) 94 | } 95 | 96 | // validateKernelConfig validates the kernel configurations. 97 | func (k *KernelValidator) validateKernelConfig(kSpec KernelSpec) error { 98 | allConfig, err := k.getKernelConfig() 99 | if err != nil { 100 | return fmt.Errorf("failed to parse kernel config: %w", err) 101 | } 102 | return k.validateCachedKernelConfig(allConfig, kSpec) 103 | } 104 | 105 | // validateCachedKernelConfig validates the kernel confgiurations cached in internal data type. 106 | func (k *KernelValidator) validateCachedKernelConfig(allConfig map[string]kConfigOption, kSpec KernelSpec) error { 107 | badConfigs := []string{} 108 | // reportAndRecord is a helper function to record bad config when 109 | // report. 110 | reportAndRecord := func(name, msg, desc string, result ValidationResultType) { 111 | if result == bad { 112 | badConfigs = append(badConfigs, name) 113 | } 114 | // report description when the config is bad or warn. 115 | if result != good && desc != "" { 116 | msg = msg + " - " + desc 117 | } 118 | k.Reporter.Report(name, msg, result) 119 | } 120 | const ( 121 | required = iota 122 | optional 123 | forbidden 124 | ) 125 | validateOpt := func(config KernelConfig, expect int) { 126 | var found, missing ValidationResultType 127 | switch expect { 128 | case required: 129 | found, missing = good, bad 130 | case optional: 131 | found, missing = good, warn 132 | case forbidden: 133 | found, missing = bad, good 134 | } 135 | var name string 136 | var opt kConfigOption 137 | var ok bool 138 | for _, name = range append([]string{config.Name}, config.Aliases...) { 139 | name = kernelConfigPrefix + name 140 | if opt, ok = allConfig[name]; ok { 141 | break 142 | } 143 | } 144 | if !ok { 145 | reportAndRecord(name, "not set", config.Description, missing) 146 | return 147 | } 148 | switch opt { 149 | case builtIn: 150 | reportAndRecord(name, "enabled", config.Description, found) 151 | case asModule: 152 | reportAndRecord(name, "enabled (as module)", config.Description, found) 153 | case leftOut: 154 | reportAndRecord(name, "disabled", config.Description, missing) 155 | default: 156 | reportAndRecord(name, fmt.Sprintf("unknown option: %s", opt), config.Description, missing) 157 | } 158 | } 159 | for _, config := range kSpec.Required { 160 | validateOpt(config, required) 161 | } 162 | _, isCgroupsV2, err := getUnifiedMountpoint(mountsFilePath) 163 | if err != nil { 164 | return fmt.Errorf("failed to get unified mountpoint: %w", err) 165 | } 166 | if isCgroupsV2 { 167 | for _, config := range kSpec.RequiredCgroupsV2 { 168 | validateOpt(config, required) 169 | } 170 | } else { 171 | for _, config := range kSpec.RequiredCgroupsV1 { 172 | validateOpt(config, required) 173 | } 174 | } 175 | 176 | for _, config := range kSpec.Optional { 177 | validateOpt(config, optional) 178 | } 179 | for _, config := range kSpec.Forbidden { 180 | validateOpt(config, forbidden) 181 | } 182 | if len(badConfigs) > 0 { 183 | return fmt.Errorf("unexpected kernel config: %s", strings.Join(badConfigs, " ")) 184 | } 185 | return nil 186 | } 187 | 188 | // getKernelConfigReader search kernel config file in a predefined list. Once the kernel config 189 | // file is found it will read the configurations into a byte buffer and return. If the kernel 190 | // config file is not found, it will try to load kernel config module and retry again. 191 | func (k *KernelValidator) getKernelConfigReader() (io.Reader, error) { 192 | possibePaths := []string{ 193 | "/proc/config.gz", 194 | "/boot/config-" + k.kernelRelease, 195 | "/usr/src/linux-" + k.kernelRelease + "/.config", 196 | "/usr/src/linux/.config", 197 | "/usr/lib/modules/" + k.kernelRelease + "/config", 198 | "/usr/lib/ostree-boot/config-" + k.kernelRelease, 199 | "/usr/lib/kernel/config-" + k.kernelRelease, 200 | "/usr/src/linux-headers-" + k.kernelRelease + "/.config", 201 | "/lib/modules/" + k.kernelRelease + "/build/.config", 202 | } 203 | configsModule := "configs" 204 | modprobeCmd := "modprobe" 205 | // loadModule indicates whether we've tried to load kernel config module ourselves. 206 | loadModule := false 207 | for { 208 | for _, path := range possibePaths { 209 | _, err := os.Stat(path) 210 | if err != nil { 211 | continue 212 | } 213 | // Buffer the whole file, so that we can close the file and unload 214 | // kernel config module in this function. 215 | b, err := os.ReadFile(path) 216 | if err != nil { 217 | return nil, err 218 | } 219 | var r io.Reader 220 | r = bytes.NewReader(b) 221 | // This is a gzip file (config.gz), unzip it. 222 | if filepath.Ext(path) == ".gz" { 223 | r, err = gzip.NewReader(r) 224 | if err != nil { 225 | return nil, err 226 | } 227 | } 228 | return r, nil 229 | } 230 | // If we've tried to load kernel config module, break and return error. 231 | if loadModule { 232 | break 233 | } 234 | // If the kernel config file is not found, try to load the kernel 235 | // config module and check again. 236 | output, err := exec.Command(modprobeCmd, configsModule).CombinedOutput() 237 | if err != nil { 238 | return nil, fmt.Errorf("unable to load kernel module: %q, output: %q, err: %w", 239 | configsModule, output, err) 240 | } 241 | // Unload the kernel config module to make sure the validation have no side effect. 242 | defer exec.Command(modprobeCmd, "-r", configsModule).Run() 243 | loadModule = true 244 | } 245 | return nil, fmt.Errorf("no config path in %v is available", possibePaths) 246 | } 247 | 248 | // getKernelConfig gets kernel config from kernel config file and convert kernel config to internal type. 249 | func (k *KernelValidator) getKernelConfig() (map[string]kConfigOption, error) { 250 | r, err := k.getKernelConfigReader() 251 | if err != nil { 252 | return nil, err 253 | } 254 | return k.parseKernelConfig(r) 255 | } 256 | 257 | // parseKernelConfig converts kernel config to internal type. 258 | func (k *KernelValidator) parseKernelConfig(r io.Reader) (map[string]kConfigOption, error) { 259 | config := map[string]kConfigOption{} 260 | regex := regexp.MustCompile(validKConfigRegex) 261 | s := bufio.NewScanner(r) 262 | for s.Scan() { 263 | if err := s.Err(); err != nil { 264 | return nil, err 265 | } 266 | line := strings.TrimSpace(s.Text()) 267 | if !regex.MatchString(line) { 268 | continue 269 | } 270 | fields := strings.Split(line, "=") 271 | config[fields[0]] = kConfigOption(fields[1][0]) 272 | } 273 | return config, nil 274 | } 275 | -------------------------------------------------------------------------------- /validators/cgroup_validator_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | /* 5 | Copyright 2016 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | import ( 23 | "bufio" 24 | "errors" 25 | "fmt" 26 | "os" 27 | "path/filepath" 28 | "strings" 29 | 30 | "github.com/blang/semver/v4" 31 | ) 32 | 33 | var _ Validator = &CgroupsValidator{} 34 | 35 | // CgroupsValidator validates cgroup configuration. 36 | type CgroupsValidator struct { 37 | Reporter Reporter 38 | KubeletVersion string 39 | } 40 | 41 | // Name is part of the system.Validator interface. 42 | func (c *CgroupsValidator) Name() string { 43 | return "cgroups" 44 | } 45 | 46 | const ( 47 | cgroupsConfigPrefix = "CGROUPS_" 48 | mountsFilePath = "/proc/mounts" 49 | defaultUnifiedMountPoint = "/sys/fs/cgroup" 50 | ) 51 | 52 | // getUnifiedMountpoint checks if the default mount point is available. 53 | // If not, it parses the mounts file to find a valid cgroup mount point. 54 | func getUnifiedMountpoint(path string) (string, bool, error) { 55 | f, err := os.Open(path) 56 | if err != nil { 57 | return "", false, err 58 | } 59 | defer f.Close() 60 | scanner := bufio.NewScanner(f) 61 | var cgroupV1MountPoint string 62 | for scanner.Scan() { 63 | line := scanner.Text() 64 | if !strings.Contains(line, "cgroup") { 65 | continue 66 | } 67 | // Example fields: `cgroup2 /sys/fs/cgroup cgroup2 rw,seclabel,nosuid,nodev,noexec,relatime 0 0`. 68 | fields := strings.Fields(line) 69 | if len(fields) >= 3 { 70 | // If default unified mount point is available, return it directly. 71 | if fields[1] == defaultUnifiedMountPoint { 72 | if fields[2] == "tmpfs" { 73 | // if `/sys/fs/cgroup/memory` is a dir, this means it uses cgroups v1 74 | info, err := os.Stat(filepath.Join(defaultUnifiedMountPoint, "memory")) 75 | return defaultUnifiedMountPoint, os.IsNotExist(err) || !info.IsDir(), nil 76 | } 77 | return defaultUnifiedMountPoint, fields[2] == "cgroup2", nil 78 | } 79 | switch fields[2] { 80 | case "cgroup2": 81 | // Return the first cgroups v2 mount point directly. 82 | return fields[1], true, nil 83 | case "cgroup": 84 | // Set the first cgroups v1 mount point only, 85 | // and continue the loop to find if there is a cgroups v2 mount point. 86 | if len(cgroupV1MountPoint) == 0 { 87 | cgroupV1MountPoint = fields[1] 88 | } 89 | } 90 | } 91 | } 92 | // Return cgroups v1 mount point if no cgroups v2 mount point is found. 93 | if len(cgroupV1MountPoint) != 0 { 94 | return cgroupV1MountPoint, false, nil 95 | } 96 | return "", false, fmt.Errorf("cannot get a cgroupfs mount point from %q", path) 97 | } 98 | 99 | // Validate is part of the system.Validator interface. 100 | func (c *CgroupsValidator) Validate(spec SysSpec) (warns, errs []error) { 101 | unifiedMountpoint, isCgroupsV2, err := getUnifiedMountpoint(mountsFilePath) 102 | if err != nil { 103 | return nil, []error{fmt.Errorf("cannot get a cgroup mount point: %w", err)} 104 | } 105 | var requiredCgroupSpec []string 106 | var optionalCgroupSpec []string 107 | var subsystems []string 108 | var warn error 109 | if isCgroupsV2 { 110 | subsystems, err, warn = c.getCgroupV2Subsystems(unifiedMountpoint) 111 | if err != nil { 112 | return nil, []error{fmt.Errorf("failed to get cgroups v2 subsystems: %w", err)} 113 | } 114 | if warn != nil { 115 | warns = append(warns, warn) 116 | } 117 | requiredCgroupSpec = spec.CgroupsV2 118 | optionalCgroupSpec = spec.CgroupsV2Optional 119 | } else { 120 | v1DisabledInKubelet, err := c.isCgroupsV1DisabledInKubelet() 121 | if err != nil { 122 | return nil, []error{err} 123 | } 124 | 125 | v1Error := errors.New("cgroups v1 support is deprecated and will be removed in a future release. " + 126 | "Please migrate to cgroups v2. To explicitly enable cgroups v1 support for kubelet v1.35 or newer, " + 127 | "you must set the kubelet configuration option 'FailCgroupV1' to 'false'. You must also explicitly " + 128 | "skip this validation. For more information, see https://git.k8s.io/enhancements/keps/sig-node/5573-remove-cgroup-v1") 129 | 130 | if v1DisabledInKubelet { 131 | errs = append(errs, v1Error) 132 | } else { 133 | warns = append(warns, v1Error) 134 | } 135 | 136 | subsystems, err = c.getCgroupV1Subsystems() 137 | if err != nil { 138 | return nil, []error{fmt.Errorf("failed to get cgroups v1 subsystems: %w", err)} 139 | } 140 | requiredCgroupSpec = spec.Cgroups 141 | optionalCgroupSpec = spec.CgroupsOptional 142 | } 143 | 144 | if missingRequired := c.validateCgroupSubsystems(requiredCgroupSpec, subsystems, true); len(missingRequired) != 0 { 145 | errs = []error{fmt.Errorf("missing required cgroups: %s", strings.Join(missingRequired, " "))} 146 | } 147 | if missingOptional := c.validateCgroupSubsystems(optionalCgroupSpec, subsystems, false); len(missingOptional) != 0 { 148 | warns = append(warns, fmt.Errorf("missing optional cgroups: %s", strings.Join(missingOptional, " "))) 149 | } 150 | return 151 | } 152 | 153 | // validateCgroupSubsystems returns a list with the missing cgroups in the cgroup 154 | func (c *CgroupsValidator) validateCgroupSubsystems(cgroups, subsystems []string, required bool) []string { 155 | var missing []string 156 | for _, cgroup := range cgroups { 157 | found := false 158 | for _, subsystem := range subsystems { 159 | if cgroup == subsystem { 160 | found = true 161 | break 162 | } 163 | } 164 | item := cgroupsConfigPrefix + strings.ToUpper(cgroup) 165 | if found { 166 | c.Reporter.Report(item, "enabled", good) 167 | continue 168 | } else if required { 169 | c.Reporter.Report(item, "missing", bad) 170 | } else { 171 | c.Reporter.Report(item, "missing", warn) 172 | } 173 | missing = append(missing, cgroup) 174 | } 175 | return missing 176 | } 177 | 178 | func (c *CgroupsValidator) getCgroupV1Subsystems() ([]string, error) { 179 | // Get the subsystems from /proc/cgroups when cgroups v1 is used. 180 | f, err := os.Open("/proc/cgroups") 181 | if err != nil { 182 | return nil, err 183 | } 184 | defer f.Close() 185 | 186 | subsystems := []string{} 187 | s := bufio.NewScanner(f) 188 | for s.Scan() { 189 | if err := s.Err(); err != nil { 190 | return nil, err 191 | } 192 | text := s.Text() 193 | if text[0] != '#' { 194 | parts := strings.Fields(text) 195 | if len(parts) >= 4 && parts[3] != "0" { 196 | subsystems = append(subsystems, parts[0]) 197 | } 198 | } 199 | } 200 | return subsystems, nil 201 | } 202 | 203 | func (c *CgroupsValidator) getCgroupV2Subsystems(unifiedMountpoint string) ([]string, error, error) { 204 | // Some controllers are implicitly enabled by the kernel. 205 | // Those controllers do not appear in /sys/fs/cgroup/cgroup.controllers. 206 | // https://github.com/torvalds/linux/blob/v5.3/kernel/cgroup/cgroup.c#L433-L434 207 | // For freezer, we use checkCgroupV2Freeze() to check. 208 | // For others, we assume these are always available, as it is hard to detect availability. 209 | // We hardcode the following as initial controllers. 210 | // - devices: implemented in kernel 4.15. 211 | subsystems := []string{"devices"} 212 | freezeSupported, warn := checkCgroupV2Freeze(unifiedMountpoint) 213 | if freezeSupported { 214 | subsystems = append(subsystems, "freezer") 215 | } 216 | data, err := os.ReadFile(filepath.Join(unifiedMountpoint, "cgroup.controllers")) 217 | if err != nil { 218 | return nil, err, warn 219 | } 220 | subsystems = append(subsystems, strings.Fields(string(data))...) 221 | return subsystems, err, warn 222 | } 223 | 224 | // checkCgroupV2Freeze checks if the freezer controller is enabled in Linux kernels 5.2. 225 | // It determines that by creating a cgroup.freeze file under the unified mountpoint location. 226 | func checkCgroupV2Freeze(unifiedMountpoint string) (isCgroupfs bool, warn error) { 227 | const freezeFile = "cgroup.freeze" 228 | tmpDir, warn := os.MkdirTemp(unifiedMountpoint, "freezer-test") 229 | if warn != nil { 230 | return 231 | } 232 | defer func() { 233 | err := os.RemoveAll(tmpDir) 234 | if err != nil { 235 | warn = fmt.Errorf("error removing directory %q: %v", tmpDir, err) 236 | } 237 | }() 238 | _, warn = os.Stat(filepath.Join(tmpDir, freezeFile)) 239 | if os.IsNotExist(warn) { 240 | return 241 | } else if warn != nil { 242 | // If the err is not NotExist error, it means that `cgroup.freeze` exists. 243 | isCgroupfs = true 244 | warn = fmt.Errorf("could not stat %q file in %q: %v", freezeFile, tmpDir, warn) 245 | return 246 | } 247 | isCgroupfs = true 248 | return 249 | } 250 | 251 | // isCgroupsV1DisabledInKubelet checks the KubeletVersion and determines if that version 252 | // disabled cgroups v1 support by default: 253 | // - If the version is newer than 1.35 pre-release, return true. 254 | // - If the version is not defined or older than pre-release 1.35, return false. 255 | func (c *CgroupsValidator) isCgroupsV1DisabledInKubelet() (bool, error) { 256 | if c.KubeletVersion == "" { 257 | return false, nil 258 | } 259 | 260 | kv, err := semver.Parse(c.KubeletVersion) 261 | if err != nil { 262 | return false, fmt.Errorf("malformed KubeletVersion in CgroupsValidator: %w", err) 263 | } 264 | 265 | if kv.Compare(semver.MustParse("1.35.0-0")) > -1 { 266 | return true, nil 267 | } 268 | 269 | return false, nil 270 | } 271 | -------------------------------------------------------------------------------- /validators/package_validator_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | /* 5 | Copyright 2017 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package system 21 | 22 | import ( 23 | "errors" 24 | "fmt" 25 | "os" 26 | "os/exec" 27 | "strings" 28 | 29 | "github.com/blang/semver/v4" 30 | ) 31 | 32 | // semVerDotsCount is the number of dots in a valid semantic version. 33 | const semVerDotsCount int = 2 34 | 35 | // packageManager is an interface that abstracts the basic operations of a 36 | // package manager. 37 | type packageManager interface { 38 | // getPackageVersion returns the version of the package given the 39 | // packageName, or an error if no such package exists. 40 | getPackageVersion(packageName string) (string, error) 41 | } 42 | 43 | // newPackageManager returns the package manager on the running machine, and an 44 | // error if no package managers is available. 45 | func newPackageManager() (packageManager, error) { 46 | if m, ok := newDPKG(); ok { 47 | return m, nil 48 | } 49 | return nil, errors.New("failed to find package manager") 50 | } 51 | 52 | // dpkg implements packageManager. It uses "dpkg-query" to retrieve package 53 | // information. 54 | type dpkg struct{} 55 | 56 | // newDPKG returns a Debian package manager. It returns (nil, false) if no such 57 | // package manager exists on the running machine. 58 | func newDPKG() (packageManager, bool) { 59 | _, err := exec.LookPath("dpkg-query") 60 | if err != nil { 61 | return nil, false 62 | } 63 | return dpkg{}, true 64 | } 65 | 66 | // getPackageVersion returns the upstream package version for the package given 67 | // the packageName, and an error if no such package exists. 68 | func (dpkg) getPackageVersion(packageName string) (string, error) { 69 | output, err := exec.Command("dpkg-query", "--show", "--showformat='${Version}'", packageName).Output() 70 | if err != nil { 71 | return "", fmt.Errorf("dpkg-query failed: %w", err) 72 | } 73 | version := extractUpstreamVersion(string(output)) 74 | if version == "" { 75 | return "", errors.New("no version information") 76 | } 77 | return version, nil 78 | } 79 | 80 | // packageValidator implements the Validator interface. It validates packages 81 | // and their versions. 82 | type packageValidator struct { 83 | reporter Reporter 84 | kernelRelease string 85 | osDistro string 86 | } 87 | 88 | // Name returns the name of the package validator. 89 | func (validator *packageValidator) Name() string { 90 | return "package" 91 | } 92 | 93 | // Validate checks packages and their versions against the spec using the 94 | // package manager on the running machine, and returns an error on any 95 | // package/version mismatch. 96 | func (validator *packageValidator) Validate(spec SysSpec) ([]error, []error) { 97 | if len(spec.PackageSpecs) == 0 { 98 | return nil, nil 99 | } 100 | 101 | var err error 102 | if validator.kernelRelease, err = getKernelRelease(); err != nil { 103 | return nil, []error{err} 104 | } 105 | if validator.osDistro, err = getOSDistro(); err != nil { 106 | return nil, []error{err} 107 | } 108 | 109 | manager, err := newPackageManager() 110 | if err != nil { 111 | return nil, []error{err} 112 | } 113 | specs := applyPackageSpecOverride(spec.PackageSpecs, spec.PackageSpecOverrides, validator.osDistro) 114 | return validator.validate(specs, manager) 115 | } 116 | 117 | // Validate checks packages and their versions against the packageSpecs using 118 | // the packageManager, and returns an error on any package/version mismatch. 119 | func (validator *packageValidator) validate(packageSpecs []PackageSpec, manager packageManager) ([]error, []error) { 120 | var errs []error 121 | for _, spec := range packageSpecs { 122 | // Substitute variables in package name. 123 | packageName := resolvePackageName(spec.Name, validator.kernelRelease) 124 | 125 | nameWithVerRange := fmt.Sprintf("%s (%s)", packageName, spec.VersionRange) 126 | 127 | // Get the version of the package on the running machine. 128 | version, err := manager.getPackageVersion(packageName) 129 | if err != nil { 130 | errs = append(errs, err) 131 | validator.reporter.Report(nameWithVerRange, "not installed", bad) 132 | continue 133 | } 134 | 135 | // Version requirement will not be enforced if version range is 136 | // not specified in the spec. 137 | if spec.VersionRange == "" { 138 | validator.reporter.Report(packageName, version, good) 139 | continue 140 | } 141 | 142 | // Convert both the version range in the spec and the version returned 143 | // from package manager to semantic version format, and then check if 144 | // the version is in the range. 145 | sv, err := semver.Make(toSemVer(version)) 146 | if err != nil { 147 | errs = append(errs, err) 148 | validator.reporter.Report(nameWithVerRange, "internal error", bad) 149 | continue 150 | } 151 | versionRange := semver.MustParseRange(toSemVerRange(spec.VersionRange)) 152 | if versionRange(sv) { 153 | validator.reporter.Report(nameWithVerRange, version, good) 154 | } else { 155 | errs = append(errs, fmt.Errorf("package \"%s %s\" does not meet the spec \"%s (%s)\"", packageName, sv, packageName, spec.VersionRange)) 156 | validator.reporter.Report(nameWithVerRange, version, bad) 157 | } 158 | } 159 | return nil, errs 160 | } 161 | 162 | // getOSDistro returns the OS distro of the local machine. 163 | func getOSDistro() (string, error) { 164 | f := "/etc/lsb-release" 165 | b, err := os.ReadFile(f) 166 | if err != nil { 167 | return "", fmt.Errorf("failed to read %q: %w", f, err) 168 | } 169 | content := string(b) 170 | switch { 171 | case strings.Contains(content, "Ubuntu"): 172 | return "ubuntu", nil 173 | case strings.Contains(content, "Chrome OS"): 174 | return "cos", nil 175 | case strings.Contains(content, "CoreOS"): 176 | return "coreos", nil 177 | default: 178 | return "", fmt.Errorf("failed to get OS distro: %s", content) 179 | } 180 | } 181 | 182 | // resolvePackageName substitutes the variables in the packageName with the 183 | // local information. 184 | // E.g., "linux-headers-${KERNEL_RELEASE}" -> "linux-headers-4.4.0-75-generic". 185 | func resolvePackageName(packageName string, kernelRelease string) string { 186 | packageName = strings.Replace(packageName, "${KERNEL_RELEASE}", kernelRelease, -1) 187 | return packageName 188 | } 189 | 190 | // applyPackageSpecOverride applies the package spec overrides for the given 191 | // osDistro to the packageSpecs and returns the applied result. 192 | func applyPackageSpecOverride(packageSpecs []PackageSpec, overrides []PackageSpecOverride, osDistro string) []PackageSpec { 193 | var override *PackageSpecOverride 194 | for _, o := range overrides { 195 | if o.OSDistro == osDistro { 196 | override = &o 197 | break 198 | } 199 | } 200 | if override == nil { 201 | return packageSpecs 202 | } 203 | 204 | // Remove packages in the spec that matches the overrides in 205 | // Subtractions. 206 | var out []PackageSpec 207 | subtractions := make(map[string]bool) 208 | for _, spec := range override.Subtractions { 209 | subtractions[spec.Name] = true 210 | } 211 | for _, spec := range packageSpecs { 212 | if _, ok := subtractions[spec.Name]; !ok { 213 | out = append(out, spec) 214 | } 215 | } 216 | 217 | // Add packages in the spec that matches the overrides in Additions. 218 | return append(out, override.Additions...) 219 | } 220 | 221 | // extractUpstreamVersion returns the upstream version of the given full 222 | // version in dpkg format. E.g., "1:1.0.6-2ubuntu2.1" -> "1.0.6". 223 | func extractUpstreamVersion(version string) string { 224 | // The full version is in the format of 225 | // "[epoch:]upstream_version[-debian_revision]". See 226 | // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version. 227 | version = strings.Trim(version, " '") 228 | if i := strings.Index(version, ":"); i != -1 { 229 | version = version[i+1:] 230 | } 231 | if i := strings.Index(version, "-"); i != -1 { 232 | version = version[:i] 233 | } 234 | return version 235 | } 236 | 237 | // toSemVerRange converts the input to a semantic version range. 238 | // E.g., 239 | // - ">=1.0" -> ">=1.0.x" 240 | // - ">=1" -> ">=1.x" 241 | // - ">=1 <=2.3" -> ">=1.x <=2.3.x" 242 | // - ">1 || >3.1.0 !4.2" -> ">1.x || >3.1.0 !4.2.x" 243 | func toSemVerRange(input string) string { 244 | var output []string 245 | fields := strings.Fields(input) 246 | for _, f := range fields { 247 | numDots, hasDigits := 0, false 248 | for _, c := range f { 249 | switch { 250 | case c == '.': 251 | numDots++ 252 | case c >= '0' && c <= '9': 253 | hasDigits = true 254 | } 255 | } 256 | if hasDigits && numDots < semVerDotsCount { 257 | f = strings.TrimRight(f, " ") 258 | f += ".x" 259 | } 260 | output = append(output, f) 261 | } 262 | return strings.Join(output, " ") 263 | } 264 | 265 | // toSemVer converts the input to a semantic version, and an empty string on 266 | // error. 267 | func toSemVer(version string) string { 268 | // Remove the first non-digit and non-dot character as well as the ones 269 | // following it. 270 | // E.g., "1.8.19p1" -> "1.8.19". 271 | if i := strings.IndexFunc(version, func(c rune) bool { 272 | if (c < '0' || c > '9') && c != '.' { 273 | return true 274 | } 275 | return false 276 | }); i != -1 { 277 | version = version[:i] 278 | } 279 | 280 | // Remove the trailing dots if there's any, and then returns an empty 281 | // string if nothing left. 282 | version = strings.TrimRight(version, ".") 283 | if version == "" { 284 | return "" 285 | } 286 | 287 | numDots := strings.Count(version, ".") 288 | switch { 289 | case numDots < semVerDotsCount: 290 | // Add minor version and patch version. 291 | // E.g. "1.18" -> "1.18.0" and "481" -> "481.0.0". 292 | version += strings.Repeat(".0", semVerDotsCount-numDots) 293 | case numDots > semVerDotsCount: 294 | // Remove anything beyond the patch version 295 | // E.g. "2.0.10.4" -> "2.0.10". 296 | for numDots != semVerDotsCount { 297 | if i := strings.LastIndex(version, "."); i != -1 { 298 | version = version[:i] 299 | numDots-- 300 | } 301 | } 302 | } 303 | 304 | // Remove leading zeros in major/minor/patch version. 305 | // E.g., "2.02" -> "2.2" 306 | // "8.0.0095" -> "8.0.95" 307 | var subs []string 308 | for _, s := range strings.Split(version, ".") { 309 | s := strings.TrimLeft(s, "0") 310 | if s == "" { 311 | s = "0" 312 | } 313 | subs = append(subs, s) 314 | } 315 | return strings.Join(subs, ".") 316 | } 317 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------