package_validator.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. /*
  2. Copyright 2017 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package system
  14. import (
  15. "fmt"
  16. "io/ioutil"
  17. "os/exec"
  18. "strings"
  19. "github.com/blang/semver"
  20. "github.com/pkg/errors"
  21. errorsutil "k8s.io/apimachinery/pkg/util/errors"
  22. "k8s.io/klog"
  23. )
  24. // semVerDotsCount is the number of dots in a valid semantic version.
  25. const semVerDotsCount int = 2
  26. // packageManager is an interface that abstracts the basic operations of a
  27. // package manager.
  28. type packageManager interface {
  29. // getPackageVersion returns the version of the package given the
  30. // packageName, or an error if no such package exists.
  31. getPackageVersion(packageName string) (string, error)
  32. }
  33. // newPackageManager returns the package manager on the running machine, and an
  34. // error if no package managers is available.
  35. func newPackageManager() (packageManager, error) {
  36. if m, ok := newDPKG(); ok {
  37. return m, nil
  38. }
  39. return nil, errors.New("failed to find package manager")
  40. }
  41. // dpkg implements packageManager. It uses "dpkg-query" to retrieve package
  42. // information.
  43. type dpkg struct{}
  44. // newDPKG returns a Debian package manager. It returns (nil, false) if no such
  45. // package manager exists on the running machine.
  46. func newDPKG() (packageManager, bool) {
  47. _, err := exec.LookPath("dpkg-query")
  48. if err != nil {
  49. return nil, false
  50. }
  51. return dpkg{}, true
  52. }
  53. // getPackageVersion returns the upstream package version for the package given
  54. // the packageName, and an error if no such package exists.
  55. func (dpkg) getPackageVersion(packageName string) (string, error) {
  56. output, err := exec.Command("dpkg-query", "--show", "--showformat='${Version}'", packageName).Output()
  57. if err != nil {
  58. return "", errors.Wrap(err, "dpkg-query failed")
  59. }
  60. version := extractUpstreamVersion(string(output))
  61. if version == "" {
  62. return "", errors.New("no version information")
  63. }
  64. return version, nil
  65. }
  66. // packageValidator implements the Validator interface. It validates packages
  67. // and their versions.
  68. type packageValidator struct {
  69. reporter Reporter
  70. kernelRelease string
  71. osDistro string
  72. }
  73. // Name returns the name of the package validator.
  74. func (validator *packageValidator) Name() string {
  75. return "package"
  76. }
  77. // Validate checks packages and their versions against the spec using the
  78. // package manager on the running machine, and returns an error on any
  79. // package/version mismatch.
  80. func (validator *packageValidator) Validate(spec SysSpec) (error, error) {
  81. if len(spec.PackageSpecs) == 0 {
  82. return nil, nil
  83. }
  84. var err error
  85. if validator.kernelRelease, err = getKernelRelease(); err != nil {
  86. return nil, err
  87. }
  88. if validator.osDistro, err = getOSDistro(); err != nil {
  89. return nil, err
  90. }
  91. manager, err := newPackageManager()
  92. if err != nil {
  93. return nil, err
  94. }
  95. specs := applyPackageSpecOverride(spec.PackageSpecs, spec.PackageSpecOverrides, validator.osDistro)
  96. return validator.validate(specs, manager)
  97. }
  98. // Validate checks packages and their versions against the packageSpecs using
  99. // the packageManager, and returns an error on any package/version mismatch.
  100. func (validator *packageValidator) validate(packageSpecs []PackageSpec, manager packageManager) (error, error) {
  101. var errs []error
  102. for _, spec := range packageSpecs {
  103. // Substitute variables in package name.
  104. packageName := resolvePackageName(spec.Name, validator.kernelRelease)
  105. nameWithVerRange := fmt.Sprintf("%s (%s)", packageName, spec.VersionRange)
  106. // Get the version of the package on the running machine.
  107. version, err := manager.getPackageVersion(packageName)
  108. if err != nil {
  109. klog.V(1).Infof("Failed to get the version for the package %q: %s\n", packageName, err)
  110. errs = append(errs, err)
  111. validator.reporter.Report(nameWithVerRange, "not installed", bad)
  112. continue
  113. }
  114. // Version requirement will not be enforced if version range is
  115. // not specified in the spec.
  116. if spec.VersionRange == "" {
  117. validator.reporter.Report(packageName, version, good)
  118. continue
  119. }
  120. // Convert both the version range in the spec and the version returned
  121. // from package manager to semantic version format, and then check if
  122. // the version is in the range.
  123. sv, err := semver.Make(toSemVer(version))
  124. if err != nil {
  125. klog.Errorf("Failed to convert %q to semantic version: %s\n", version, err)
  126. errs = append(errs, err)
  127. validator.reporter.Report(nameWithVerRange, "internal error", bad)
  128. continue
  129. }
  130. versionRange := semver.MustParseRange(toSemVerRange(spec.VersionRange))
  131. if versionRange(sv) {
  132. validator.reporter.Report(nameWithVerRange, version, good)
  133. } else {
  134. errs = append(errs, errors.Errorf("package \"%s %s\" does not meet the spec \"%s (%s)\"", packageName, sv, packageName, spec.VersionRange))
  135. validator.reporter.Report(nameWithVerRange, version, bad)
  136. }
  137. }
  138. return nil, errorsutil.NewAggregate(errs)
  139. }
  140. // getKernelRelease returns the kernel release of the local machine.
  141. func getKernelRelease() (string, error) {
  142. output, err := exec.Command("uname", "-r").Output()
  143. if err != nil {
  144. return "", errors.Wrap(err, "failed to get kernel release")
  145. }
  146. return strings.TrimSpace(string(output)), nil
  147. }
  148. // getOSDistro returns the OS distro of the local machine.
  149. func getOSDistro() (string, error) {
  150. f := "/etc/lsb-release"
  151. b, err := ioutil.ReadFile(f)
  152. if err != nil {
  153. return "", errors.Wrapf(err, "failed to read %q", f)
  154. }
  155. content := string(b)
  156. switch {
  157. case strings.Contains(content, "Ubuntu"):
  158. return "ubuntu", nil
  159. case strings.Contains(content, "Chrome OS"):
  160. return "cos", nil
  161. case strings.Contains(content, "CoreOS"):
  162. return "coreos", nil
  163. default:
  164. return "", errors.Errorf("failed to get OS distro: %s", content)
  165. }
  166. }
  167. // resolvePackageName substitutes the variables in the packageName with the
  168. // local information.
  169. // E.g., "linux-headers-${KERNEL_RELEASE}" -> "linux-headers-4.4.0-75-generic".
  170. func resolvePackageName(packageName string, kernelRelease string) string {
  171. packageName = strings.Replace(packageName, "${KERNEL_RELEASE}", kernelRelease, -1)
  172. return packageName
  173. }
  174. // applyPackageSpecOverride applies the package spec overrides for the given
  175. // osDistro to the packageSpecs and returns the applied result.
  176. func applyPackageSpecOverride(packageSpecs []PackageSpec, overrides []PackageSpecOverride, osDistro string) []PackageSpec {
  177. var override *PackageSpecOverride
  178. for _, o := range overrides {
  179. if o.OSDistro == osDistro {
  180. override = &o
  181. break
  182. }
  183. }
  184. if override == nil {
  185. return packageSpecs
  186. }
  187. // Remove packages in the spec that matches the overrides in
  188. // Subtractions.
  189. var out []PackageSpec
  190. subtractions := make(map[string]bool)
  191. for _, spec := range override.Subtractions {
  192. subtractions[spec.Name] = true
  193. }
  194. for _, spec := range packageSpecs {
  195. if _, ok := subtractions[spec.Name]; !ok {
  196. out = append(out, spec)
  197. }
  198. }
  199. // Add packages in the spec that matches the overrides in Additions.
  200. return append(out, override.Additions...)
  201. }
  202. // extractUpstreamVersion returns the upstream version of the given full
  203. // version in dpkg format. E.g., "1:1.0.6-2ubuntu2.1" -> "1.0.6".
  204. func extractUpstreamVersion(version string) string {
  205. // The full version is in the format of
  206. // "[epoch:]upstream_version[-debian_revision]". See
  207. // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version.
  208. version = strings.Trim(version, " '")
  209. if i := strings.Index(version, ":"); i != -1 {
  210. version = version[i+1:]
  211. }
  212. if i := strings.Index(version, "-"); i != -1 {
  213. version = version[:i]
  214. }
  215. return version
  216. }
  217. // toSemVerRange converts the input to a semantic version range.
  218. // E.g., ">=1.0" -> ">=1.0.x"
  219. // ">=1" -> ">=1.x"
  220. // ">=1 <=2.3" -> ">=1.x <=2.3.x"
  221. // ">1 || >3.1.0 !4.2" -> ">1.x || >3.1.0 !4.2.x"
  222. func toSemVerRange(input string) string {
  223. var output []string
  224. fields := strings.Fields(input)
  225. for _, f := range fields {
  226. numDots, hasDigits := 0, false
  227. for _, c := range f {
  228. switch {
  229. case c == '.':
  230. numDots++
  231. case c >= '0' && c <= '9':
  232. hasDigits = true
  233. }
  234. }
  235. if hasDigits && numDots < semVerDotsCount {
  236. f = strings.TrimRight(f, " ")
  237. f += ".x"
  238. }
  239. output = append(output, f)
  240. }
  241. return strings.Join(output, " ")
  242. }
  243. // toSemVer converts the input to a semantic version, and an empty string on
  244. // error.
  245. func toSemVer(version string) string {
  246. // Remove the first non-digit and non-dot character as well as the ones
  247. // following it.
  248. // E.g., "1.8.19p1" -> "1.8.19".
  249. if i := strings.IndexFunc(version, func(c rune) bool {
  250. if (c < '0' || c > '9') && c != '.' {
  251. return true
  252. }
  253. return false
  254. }); i != -1 {
  255. version = version[:i]
  256. }
  257. // Remove the trailing dots if there's any, and then returns an empty
  258. // string if nothing left.
  259. version = strings.TrimRight(version, ".")
  260. if version == "" {
  261. return ""
  262. }
  263. numDots := strings.Count(version, ".")
  264. switch {
  265. case numDots < semVerDotsCount:
  266. // Add minor version and patch version.
  267. // E.g. "1.18" -> "1.18.0" and "481" -> "481.0.0".
  268. version += strings.Repeat(".0", semVerDotsCount-numDots)
  269. case numDots > semVerDotsCount:
  270. // Remove anything beyond the patch version
  271. // E.g. "2.0.10.4" -> "2.0.10".
  272. for numDots != semVerDotsCount {
  273. if i := strings.LastIndex(version, "."); i != -1 {
  274. version = version[:i]
  275. numDots--
  276. }
  277. }
  278. }
  279. // Remove leading zeros in major/minor/patch version.
  280. // E.g., "2.02" -> "2.2"
  281. // "8.0.0095" -> "8.0.95"
  282. var subs []string
  283. for _, s := range strings.Split(version, ".") {
  284. s := strings.TrimLeft(s, "0")
  285. if s == "" {
  286. s = "0"
  287. }
  288. subs = append(subs, s)
  289. }
  290. return strings.Join(subs, ".")
  291. }