diff --git a/Gopkg.lock b/Gopkg.lock index f796ec937..2a47ddd8e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -419,13 +419,13 @@ [[projects]] name = "k8s.io/kubernetes" - packages = ["pkg/printers"] + packages = ["pkg/printers","pkg/util/version"] revision = "bdaeafa71f6c7c04636251031f93464384d54963" version = "v1.8.2" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "c3cd1b703421685e5b2343ced6eaa6ec958b9c44d62277322f4c93de164c2d04" + inputs-digest = "cd582891fb7e89c2ea28ea41e52687e2902b6105c6c3bf989bd628ebb4b72208" solver-name = "gps-cdcl" solver-version = 1 diff --git a/pkg/cmd/cli/backup/delete.go b/pkg/cmd/cli/backup/delete.go index d2d9c6984..419dff40f 100644 --- a/pkg/cmd/cli/backup/delete.go +++ b/pkg/cmd/cli/backup/delete.go @@ -20,11 +20,14 @@ import ( "fmt" "os" + "github.com/pkg/errors" "github.com/spf13/cobra" api "github.com/heptio/ark/pkg/apis/ark/v1" "github.com/heptio/ark/pkg/client" "github.com/heptio/ark/pkg/cmd" + "github.com/heptio/ark/pkg/controller" + kubeutil "github.com/heptio/ark/pkg/util/kube" ) func NewDeleteCommand(f client.Factory, use string) *cobra.Command { @@ -37,6 +40,16 @@ func NewDeleteCommand(f client.Factory, use string) *cobra.Command { os.Exit(1) } + kubeClient, err := f.KubeClient() + cmd.CheckError(err) + + serverVersion, err := kubeutil.ServerVersion(kubeClient.Discovery()) + cmd.CheckError(err) + + if !serverVersion.AtLeast(controller.MinVersionForDelete) { + cmd.CheckError(errors.Errorf("this command requires the Kubernetes server version to be at least %s", controller.MinVersionForDelete)) + } + arkClient, err := f.Client() cmd.CheckError(err) diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index b7bc69a37..6efa26e9a 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -55,6 +55,7 @@ import ( "github.com/heptio/ark/pkg/plugin" "github.com/heptio/ark/pkg/restore" "github.com/heptio/ark/pkg/util/kube" + kubeutil "github.com/heptio/ark/pkg/util/kube" "github.com/heptio/ark/pkg/util/logging" ) @@ -470,22 +471,31 @@ func (s *server) runControllers(config *api.Config) error { wg.Done() }() - gcController := controller.NewGCController( - s.backupService, - s.snapshotService, - config.BackupStorageProvider.Bucket, - config.GCSyncPeriod.Duration, - s.sharedInformerFactory.Ark().V1().Backups(), - s.arkClient.ArkV1(), - s.sharedInformerFactory.Ark().V1().Restores(), - s.arkClient.ArkV1(), - s.logger, - ) - wg.Add(1) - go func() { - gcController.Run(ctx, 1) - wg.Done() - }() + serverVersion, err := kubeutil.ServerVersion(s.kubeClient.Discovery()) + if err != nil { + return err + } + + if !serverVersion.AtLeast(controller.MinVersionForDelete) { + s.logger.Errorf("Garbage-collection is disabled because it requires the Kubernetes server version to be at least %s", controller.MinVersionForDelete) + } else { + gcController := controller.NewGCController( + s.backupService, + s.snapshotService, + config.BackupStorageProvider.Bucket, + config.GCSyncPeriod.Duration, + s.sharedInformerFactory.Ark().V1().Backups(), + s.arkClient.ArkV1(), + s.sharedInformerFactory.Ark().V1().Restores(), + s.arkClient.ArkV1(), + s.logger, + ) + wg.Add(1) + go func() { + gcController.Run(ctx, 1) + wg.Done() + }() + } } restorer, err := newRestorer( diff --git a/pkg/controller/gc_controller.go b/pkg/controller/gc_controller.go index 8608fc843..16e2de2c6 100644 --- a/pkg/controller/gc_controller.go +++ b/pkg/controller/gc_controller.go @@ -31,6 +31,7 @@ import ( kerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/tools/cache" + "k8s.io/kubernetes/pkg/util/version" api "github.com/heptio/ark/pkg/apis/ark/v1" "github.com/heptio/ark/pkg/cloudprovider" @@ -42,6 +43,13 @@ import ( const gcFinalizer = "gc.ark.heptio.com" +// MinVersionForDelete is the minimum Kubernetes server version that Ark +// requires in order to be able to properly delete backups (including +// the associated snapshots and object storage files). This is because +// Ark uses finalizers on the backup CRD to implement garbage-collection +// and deletion. +var MinVersionForDelete = version.MustParseSemantic("1.7.5") + // gcController removes expired backup content from object storage. type gcController struct { backupService cloudprovider.BackupService diff --git a/pkg/util/kube/utils.go b/pkg/util/kube/utils.go index 57f42b0c0..97a333739 100644 --- a/pkg/util/kube/utils.go +++ b/pkg/util/kube/utils.go @@ -24,7 +24,9 @@ import ( "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/discovery" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/kubernetes/pkg/util/version" ) // NamespaceAndName returns a string in the format / @@ -48,3 +50,17 @@ func EnsureNamespaceExists(namespace *v1.Namespace, client corev1.NamespaceInter return false, errors.Wrapf(err, "error creating namespace %s", namespace.Name) } } + +func ServerVersion(client discovery.DiscoveryInterface) (*version.Version, error) { + versionInfo, err := client.ServerVersion() + if err != nil { + return nil, errors.Wrap(err, "error getting server version") + } + + semVer, err := version.ParseSemantic(versionInfo.String()) + if err != nil { + return nil, errors.Wrap(err, "error parsing server version") + } + + return semVer, err +} diff --git a/vendor/k8s.io/kubernetes/pkg/util/verify-util-pkg.sh b/vendor/k8s.io/kubernetes/pkg/util/verify-util-pkg.sh new file mode 100755 index 000000000..755924a09 --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/util/verify-util-pkg.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# 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. + +# verify-util-pkg.sh checks whether *.go except doc.go in pkg/util have been moved into +# sub-pkgs, see issue #15634. + +set -o errexit +set -o nounset +set -o pipefail + +BASH_DIR=$(dirname "${BASH_SOURCE}") + +find_go_files() { + find . -maxdepth 1 -not \( \ + \( \ + -wholename './doc.go' \ + \) -prune \ + \) -name '*.go' +} + +ret=0 + +pushd "${BASH_DIR}" > /dev/null + for path in `find_go_files`; do + file=$(basename $path) + echo "Found pkg/util/${file}, but should be moved into util sub-pkgs." 1>&2 + ret=1 + done +popd > /dev/null + +if [[ ${ret} > 0 ]]; then + exit ${ret} +fi + +echo "Util Package Verified." diff --git a/vendor/k8s.io/kubernetes/pkg/util/version/doc.go b/vendor/k8s.io/kubernetes/pkg/util/version/doc.go new file mode 100644 index 000000000..ebe43152e --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/util/version/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2016 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 version provides utilities for version number comparisons +package version // import "k8s.io/kubernetes/pkg/util/version" diff --git a/vendor/k8s.io/kubernetes/pkg/util/version/version.go b/vendor/k8s.io/kubernetes/pkg/util/version/version.go new file mode 100644 index 000000000..e8cd0cecf --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/util/version/version.go @@ -0,0 +1,264 @@ +/* +Copyright 2016 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 version + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + "strings" +) + +// Version is an opqaue representation of a version number +type Version struct { + components []uint + semver bool + preRelease string + buildMetadata string +} + +var ( + // versionMatchRE splits a version string into numeric and "extra" parts + versionMatchRE = regexp.MustCompile(`^\s*v?([0-9]+(?:\.[0-9]+)*)(.*)*$`) + // extraMatchRE splits the "extra" part of versionMatchRE into semver pre-release and build metadata; it does not validate the "no leading zeroes" constraint for pre-release + extraMatchRE = regexp.MustCompile(`^(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?\s*$`) +) + +func parse(str string, semver bool) (*Version, error) { + parts := versionMatchRE.FindStringSubmatch(str) + if parts == nil { + return nil, fmt.Errorf("could not parse %q as version", str) + } + numbers, extra := parts[1], parts[2] + + components := strings.Split(numbers, ".") + if (semver && len(components) != 3) || (!semver && len(components) < 2) { + return nil, fmt.Errorf("illegal version string %q", str) + } + + v := &Version{ + components: make([]uint, len(components)), + semver: semver, + } + for i, comp := range components { + if (i == 0 || semver) && strings.HasPrefix(comp, "0") && comp != "0" { + return nil, fmt.Errorf("illegal zero-prefixed version component %q in %q", comp, str) + } + num, err := strconv.ParseUint(comp, 10, 0) + if err != nil { + return nil, fmt.Errorf("illegal non-numeric version component %q in %q: %v", comp, str, err) + } + v.components[i] = uint(num) + } + + if semver && extra != "" { + extraParts := extraMatchRE.FindStringSubmatch(extra) + if extraParts == nil { + return nil, fmt.Errorf("could not parse pre-release/metadata (%s) in version %q", extra, str) + } + v.preRelease, v.buildMetadata = extraParts[1], extraParts[2] + + for _, comp := range strings.Split(v.preRelease, ".") { + if _, err := strconv.ParseUint(comp, 10, 0); err == nil { + if strings.HasPrefix(comp, "0") && comp != "0" { + return nil, fmt.Errorf("illegal zero-prefixed version component %q in %q", comp, str) + } + } + } + } + + return v, nil +} + +// ParseGeneric parses a "generic" version string. The version string must consist of two +// or more dot-separated numeric fields (the first of which can't have leading zeroes), +// followed by arbitrary uninterpreted data (which need not be separated from the final +// numeric field by punctuation). For convenience, leading and trailing whitespace is +// ignored, and the version can be preceded by the letter "v". See also ParseSemantic. +func ParseGeneric(str string) (*Version, error) { + return parse(str, false) +} + +// MustParseGeneric is like ParseGeneric except that it panics on error +func MustParseGeneric(str string) *Version { + v, err := ParseGeneric(str) + if err != nil { + panic(err) + } + return v +} + +// ParseSemantic parses a version string that exactly obeys the syntax and semantics of +// the "Semantic Versioning" specification (http://semver.org/) (although it ignores +// leading and trailing whitespace, and allows the version to be preceded by "v"). For +// version strings that are not guaranteed to obey the Semantic Versioning syntax, use +// ParseGeneric. +func ParseSemantic(str string) (*Version, error) { + return parse(str, true) +} + +// MustParseSemantic is like ParseSemantic except that it panics on error +func MustParseSemantic(str string) *Version { + v, err := ParseSemantic(str) + if err != nil { + panic(err) + } + return v +} + +// Major returns the major release number +func (v *Version) Major() uint { + return v.components[0] +} + +// Minor returns the minor release number +func (v *Version) Minor() uint { + return v.components[1] +} + +// Patch returns the patch release number if v is a Semantic Version, or 0 +func (v *Version) Patch() uint { + if len(v.components) < 3 { + return 0 + } + return v.components[2] +} + +// BuildMetadata returns the build metadata, if v is a Semantic Version, or "" +func (v *Version) BuildMetadata() string { + return v.buildMetadata +} + +// PreRelease returns the prerelease metadata, if v is a Semantic Version, or "" +func (v *Version) PreRelease() string { + return v.preRelease +} + +// Components returns the version number components +func (v *Version) Components() []uint { + return v.components +} + +// String converts a Version back to a string; note that for versions parsed with +// ParseGeneric, this will not include the trailing uninterpreted portion of the version +// number. +func (v *Version) String() string { + var buffer bytes.Buffer + + for i, comp := range v.components { + if i > 0 { + buffer.WriteString(".") + } + buffer.WriteString(fmt.Sprintf("%d", comp)) + } + if v.preRelease != "" { + buffer.WriteString("-") + buffer.WriteString(v.preRelease) + } + if v.buildMetadata != "" { + buffer.WriteString("+") + buffer.WriteString(v.buildMetadata) + } + + return buffer.String() +} + +// compareInternal returns -1 if v is less than other, 1 if it is greater than other, or 0 +// if they are equal +func (v *Version) compareInternal(other *Version) int { + for i := range v.components { + switch { + case i >= len(other.components): + if v.components[i] != 0 { + return 1 + } + case other.components[i] < v.components[i]: + return 1 + case other.components[i] > v.components[i]: + return -1 + } + } + + if !v.semver || !other.semver { + return 0 + } + + switch { + case v.preRelease == "" && other.preRelease != "": + return 1 + case v.preRelease != "" && other.preRelease == "": + return -1 + case v.preRelease == other.preRelease: // includes case where both are "" + return 0 + } + + vPR := strings.Split(v.preRelease, ".") + oPR := strings.Split(other.preRelease, ".") + for i := range vPR { + if i >= len(oPR) { + return 1 + } + vNum, err := strconv.ParseUint(vPR[i], 10, 0) + if err == nil { + oNum, err := strconv.ParseUint(oPR[i], 10, 0) + if err == nil { + switch { + case oNum < vNum: + return 1 + case oNum > vNum: + return -1 + default: + continue + } + } + } + if oPR[i] < vPR[i] { + return 1 + } else if oPR[i] > vPR[i] { + return -1 + } + } + + return 0 +} + +// AtLeast tests if a version is at least equal to a given minimum version. If both +// Versions are Semantic Versions, this will use the Semantic Version comparison +// algorithm. Otherwise, it will compare only the numeric components, with non-present +// components being considered "0" (ie, "1.4" is equal to "1.4.0"). +func (v *Version) AtLeast(min *Version) bool { + return v.compareInternal(min) != -1 +} + +// LessThan tests if a version is less than a given version. (It is exactly the opposite +// of AtLeast, for situations where asking "is v too old?" makes more sense than asking +// "is v new enough?".) +func (v *Version) LessThan(other *Version) bool { + return v.compareInternal(other) == -1 +} + +// Compare compares v against a version string (which will be parsed as either Semantic +// or non-Semantic depending on v). On success it returns -1 if v is less than other, 1 if +// it is greater than other, or 0 if they are equal. +func (v *Version) Compare(other string) (int, error) { + ov, err := parse(other, v.semver) + if err != nil { + return 0, err + } + return v.compareInternal(ov), nil +}