123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- /*
- Copyright 2017 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package system
- import (
- "fmt"
- "io/ioutil"
- "os/exec"
- "strings"
- "github.com/blang/semver"
- "github.com/pkg/errors"
- errorsutil "k8s.io/apimachinery/pkg/util/errors"
- "k8s.io/klog"
- )
- // semVerDotsCount is the number of dots in a valid semantic version.
- const semVerDotsCount int = 2
- // packageManager is an interface that abstracts the basic operations of a
- // package manager.
- type packageManager interface {
- // getPackageVersion returns the version of the package given the
- // packageName, or an error if no such package exists.
- getPackageVersion(packageName string) (string, error)
- }
- // newPackageManager returns the package manager on the running machine, and an
- // error if no package managers is available.
- func newPackageManager() (packageManager, error) {
- if m, ok := newDPKG(); ok {
- return m, nil
- }
- return nil, errors.New("failed to find package manager")
- }
- // dpkg implements packageManager. It uses "dpkg-query" to retrieve package
- // information.
- type dpkg struct{}
- // newDPKG returns a Debian package manager. It returns (nil, false) if no such
- // package manager exists on the running machine.
- func newDPKG() (packageManager, bool) {
- _, err := exec.LookPath("dpkg-query")
- if err != nil {
- return nil, false
- }
- return dpkg{}, true
- }
- // getPackageVersion returns the upstream package version for the package given
- // the packageName, and an error if no such package exists.
- func (dpkg) getPackageVersion(packageName string) (string, error) {
- output, err := exec.Command("dpkg-query", "--show", "--showformat='${Version}'", packageName).Output()
- if err != nil {
- return "", errors.Wrap(err, "dpkg-query failed")
- }
- version := extractUpstreamVersion(string(output))
- if version == "" {
- return "", errors.New("no version information")
- }
- return version, nil
- }
- // packageValidator implements the Validator interface. It validates packages
- // and their versions.
- type packageValidator struct {
- reporter Reporter
- kernelRelease string
- osDistro string
- }
- // Name returns the name of the package validator.
- func (validator *packageValidator) Name() string {
- return "package"
- }
- // Validate checks packages and their versions against the spec using the
- // package manager on the running machine, and returns an error on any
- // package/version mismatch.
- func (validator *packageValidator) Validate(spec SysSpec) (error, error) {
- if len(spec.PackageSpecs) == 0 {
- return nil, nil
- }
- var err error
- if validator.kernelRelease, err = getKernelRelease(); err != nil {
- return nil, err
- }
- if validator.osDistro, err = getOSDistro(); err != nil {
- return nil, err
- }
- manager, err := newPackageManager()
- if err != nil {
- return nil, err
- }
- specs := applyPackageSpecOverride(spec.PackageSpecs, spec.PackageSpecOverrides, validator.osDistro)
- return validator.validate(specs, manager)
- }
- // Validate checks packages and their versions against the packageSpecs using
- // the packageManager, and returns an error on any package/version mismatch.
- func (validator *packageValidator) validate(packageSpecs []PackageSpec, manager packageManager) (error, error) {
- var errs []error
- for _, spec := range packageSpecs {
- // Substitute variables in package name.
- packageName := resolvePackageName(spec.Name, validator.kernelRelease)
- nameWithVerRange := fmt.Sprintf("%s (%s)", packageName, spec.VersionRange)
- // Get the version of the package on the running machine.
- version, err := manager.getPackageVersion(packageName)
- if err != nil {
- klog.V(1).Infof("Failed to get the version for the package %q: %s\n", packageName, err)
- errs = append(errs, err)
- validator.reporter.Report(nameWithVerRange, "not installed", bad)
- continue
- }
- // Version requirement will not be enforced if version range is
- // not specified in the spec.
- if spec.VersionRange == "" {
- validator.reporter.Report(packageName, version, good)
- continue
- }
- // Convert both the version range in the spec and the version returned
- // from package manager to semantic version format, and then check if
- // the version is in the range.
- sv, err := semver.Make(toSemVer(version))
- if err != nil {
- klog.Errorf("Failed to convert %q to semantic version: %s\n", version, err)
- errs = append(errs, err)
- validator.reporter.Report(nameWithVerRange, "internal error", bad)
- continue
- }
- versionRange := semver.MustParseRange(toSemVerRange(spec.VersionRange))
- if versionRange(sv) {
- validator.reporter.Report(nameWithVerRange, version, good)
- } else {
- errs = append(errs, errors.Errorf("package \"%s %s\" does not meet the spec \"%s (%s)\"", packageName, sv, packageName, spec.VersionRange))
- validator.reporter.Report(nameWithVerRange, version, bad)
- }
- }
- return nil, errorsutil.NewAggregate(errs)
- }
- // getKernelRelease returns the kernel release of the local machine.
- func getKernelRelease() (string, error) {
- output, err := exec.Command("uname", "-r").Output()
- if err != nil {
- return "", errors.Wrap(err, "failed to get kernel release")
- }
- return strings.TrimSpace(string(output)), nil
- }
- // getOSDistro returns the OS distro of the local machine.
- func getOSDistro() (string, error) {
- f := "/etc/lsb-release"
- b, err := ioutil.ReadFile(f)
- if err != nil {
- return "", errors.Wrapf(err, "failed to read %q", f)
- }
- content := string(b)
- switch {
- case strings.Contains(content, "Ubuntu"):
- return "ubuntu", nil
- case strings.Contains(content, "Chrome OS"):
- return "cos", nil
- case strings.Contains(content, "CoreOS"):
- return "coreos", nil
- default:
- return "", errors.Errorf("failed to get OS distro: %s", content)
- }
- }
- // resolvePackageName substitutes the variables in the packageName with the
- // local information.
- // E.g., "linux-headers-${KERNEL_RELEASE}" -> "linux-headers-4.4.0-75-generic".
- func resolvePackageName(packageName string, kernelRelease string) string {
- packageName = strings.Replace(packageName, "${KERNEL_RELEASE}", kernelRelease, -1)
- return packageName
- }
- // applyPackageSpecOverride applies the package spec overrides for the given
- // osDistro to the packageSpecs and returns the applied result.
- func applyPackageSpecOverride(packageSpecs []PackageSpec, overrides []PackageSpecOverride, osDistro string) []PackageSpec {
- var override *PackageSpecOverride
- for _, o := range overrides {
- if o.OSDistro == osDistro {
- override = &o
- break
- }
- }
- if override == nil {
- return packageSpecs
- }
- // Remove packages in the spec that matches the overrides in
- // Subtractions.
- var out []PackageSpec
- subtractions := make(map[string]bool)
- for _, spec := range override.Subtractions {
- subtractions[spec.Name] = true
- }
- for _, spec := range packageSpecs {
- if _, ok := subtractions[spec.Name]; !ok {
- out = append(out, spec)
- }
- }
- // Add packages in the spec that matches the overrides in Additions.
- return append(out, override.Additions...)
- }
- // extractUpstreamVersion returns the upstream version of the given full
- // version in dpkg format. E.g., "1:1.0.6-2ubuntu2.1" -> "1.0.6".
- func extractUpstreamVersion(version string) string {
- // The full version is in the format of
- // "[epoch:]upstream_version[-debian_revision]". See
- // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version.
- version = strings.Trim(version, " '")
- if i := strings.Index(version, ":"); i != -1 {
- version = version[i+1:]
- }
- if i := strings.Index(version, "-"); i != -1 {
- version = version[:i]
- }
- return version
- }
- // toSemVerRange converts the input to a semantic version range.
- // E.g., ">=1.0" -> ">=1.0.x"
- // ">=1" -> ">=1.x"
- // ">=1 <=2.3" -> ">=1.x <=2.3.x"
- // ">1 || >3.1.0 !4.2" -> ">1.x || >3.1.0 !4.2.x"
- func toSemVerRange(input string) string {
- var output []string
- fields := strings.Fields(input)
- for _, f := range fields {
- numDots, hasDigits := 0, false
- for _, c := range f {
- switch {
- case c == '.':
- numDots++
- case c >= '0' && c <= '9':
- hasDigits = true
- }
- }
- if hasDigits && numDots < semVerDotsCount {
- f = strings.TrimRight(f, " ")
- f += ".x"
- }
- output = append(output, f)
- }
- return strings.Join(output, " ")
- }
- // toSemVer converts the input to a semantic version, and an empty string on
- // error.
- func toSemVer(version string) string {
- // Remove the first non-digit and non-dot character as well as the ones
- // following it.
- // E.g., "1.8.19p1" -> "1.8.19".
- if i := strings.IndexFunc(version, func(c rune) bool {
- if (c < '0' || c > '9') && c != '.' {
- return true
- }
- return false
- }); i != -1 {
- version = version[:i]
- }
- // Remove the trailing dots if there's any, and then returns an empty
- // string if nothing left.
- version = strings.TrimRight(version, ".")
- if version == "" {
- return ""
- }
- numDots := strings.Count(version, ".")
- switch {
- case numDots < semVerDotsCount:
- // Add minor version and patch version.
- // E.g. "1.18" -> "1.18.0" and "481" -> "481.0.0".
- version += strings.Repeat(".0", semVerDotsCount-numDots)
- case numDots > semVerDotsCount:
- // Remove anything beyond the patch version
- // E.g. "2.0.10.4" -> "2.0.10".
- for numDots != semVerDotsCount {
- if i := strings.LastIndex(version, "."); i != -1 {
- version = version[:i]
- numDots--
- }
- }
- }
- // Remove leading zeros in major/minor/patch version.
- // E.g., "2.02" -> "2.2"
- // "8.0.0095" -> "8.0.95"
- var subs []string
- for _, s := range strings.Split(version, ".") {
- s := strings.TrimLeft(s, "0")
- if s == "" {
- s = "0"
- }
- subs = append(subs, s)
- }
- return strings.Join(subs, ".")
- }
|