diff --git a/CHANGELOG.md b/CHANGELOG.md index 470e58a6be..4f676b01cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - v1.15.x - v1.14.x +### Improvements + +- Provide detailed error for removed apiVersions. (https://github.com/pulumi/pulumi-kubernetes/pull/809). + ## 1.1.0 (September 18, 2019) ### Supported Kubernetes versions diff --git a/pkg/await/await.go b/pkg/await/await.go index 0157744e63..b8065c3e2c 100644 --- a/pkg/await/await.go +++ b/pkg/await/await.go @@ -20,6 +20,7 @@ import ( "github.com/golang/glog" "github.com/pulumi/pulumi-kubernetes/pkg/clients" + "github.com/pulumi/pulumi-kubernetes/pkg/cluster" "github.com/pulumi/pulumi-kubernetes/pkg/logging" "github.com/pulumi/pulumi-kubernetes/pkg/metadata" "github.com/pulumi/pulumi-kubernetes/pkg/openapi" @@ -422,7 +423,7 @@ func Deletion(c DeleteConfig) error { return nilIfGVKDeleted(err) } - err = deleteResource(c.Name, client, ServerVersion(c.ClientSet.DiscoveryClientCached)) + err = deleteResource(c.Name, client, cluster.GetServerVersion(c.ClientSet.DiscoveryClientCached)) if err != nil { return nilIfGVKDeleted(err) } @@ -500,15 +501,15 @@ func Deletion(c DeleteConfig) error { return waitErr } -func deleteResource(name string, client dynamic.ResourceInterface, version serverVersion) error { +func deleteResource(name string, client dynamic.ResourceInterface, version cluster.ServerVersion) error { // Manually set delete propagation for Kubernetes versions < 1.6 to avoid bugs. deleteOpts := metav1.DeleteOptions{} - if version.Compare(1, 6) < 0 { + if version.Compare(cluster.ServerVersion{Major: 1, Minor: 6}) < 0 { // 1.5.x option. boolFalse := false // nolint deleteOpts.OrphanDependents = &boolFalse - } else if version.Compare(1, 7) < 0 { + } else if version.Compare(cluster.ServerVersion{Major: 1, Minor: 7}) < 0 { // 1.6.x option. Background delete propagation is broken in k8s v1.6. fg := metav1.DeletePropagationForeground deleteOpts.PropagationPolicy = &fg diff --git a/pkg/await/core_service.go b/pkg/await/core_service.go index 7e2d0cfb5e..d21db1993f 100644 --- a/pkg/await/core_service.go +++ b/pkg/await/core_service.go @@ -5,6 +5,7 @@ import ( "reflect" "time" + "github.com/pulumi/pulumi-kubernetes/pkg/cluster" "github.com/pulumi/pulumi/pkg/util/cmdutil" "github.com/golang/glog" @@ -140,7 +141,7 @@ func (sia *serviceInitAwaiter) Await() error { } defer endpointWatcher.Stop() - version := ServerVersion(sia.config.clientSet.DiscoveryClientCached) + version := cluster.GetServerVersion(sia.config.clientSet.DiscoveryClientCached) timeout := metadata.TimeoutDuration(sia.config.timeout, sia.config.currentInputs, DefaultServiceTimeoutMins*60) return sia.await(serviceWatcher, endpointWatcher, time.After(timeout), make(chan struct{}), version) @@ -175,14 +176,14 @@ func (sia *serviceInitAwaiter) Read() error { endpointList = &unstructured.UnstructuredList{Items: []unstructured.Unstructured{}} } - version := ServerVersion(sia.config.clientSet.DiscoveryClientCached) + version := cluster.GetServerVersion(sia.config.clientSet.DiscoveryClientCached) return sia.read(service, endpointList, version) } func (sia *serviceInitAwaiter) read( service *unstructured.Unstructured, endpoints *unstructured.UnstructuredList, - version serverVersion, + version cluster.ServerVersion, ) error { sia.processServiceEvent(watchAddedEvent(service)) @@ -216,7 +217,7 @@ func (sia *serviceInitAwaiter) await( serviceWatcher, endpointWatcher watch.Interface, timeout <-chan time.Time, settled chan struct{}, - version serverVersion, + version cluster.ServerVersion, ) error { sia.config.logStatus(diag.Info, "[1/3] Finding Pods to direct traffic to") @@ -394,14 +395,14 @@ func (sia *serviceInitAwaiter) emptyHeadlessOrExternalName() bool { // // [1]: https://github.com/kubernetes/dns/issues/174 // [2]: https://github.com/kubernetes/kubernetes/commit/1c0137252465574519baf99252df8d75048f1304 -func (sia *serviceInitAwaiter) hasHeadlessServicePortBug(version serverVersion) bool { +func (sia *serviceInitAwaiter) hasHeadlessServicePortBug(version cluster.ServerVersion) bool { // This bug only affects headless or external name Services. if !sia.isHeadlessService() && !sia.isExternalNameService() { return false } // k8s versions < 1.12 have the bug. - if version.Compare(1, 12) < 0 { + if version.Compare(cluster.ServerVersion{Major: 1, Minor: 12}) < 0 { portsI, _ := openapi.Pluck(sia.service.Object, "spec", "ports") ports, _ := portsI.([]map[string]interface{}) hasPorts := len(ports) > 0 @@ -416,7 +417,7 @@ func (sia *serviceInitAwaiter) hasHeadlessServicePortBug(version serverVersion) } // shouldWaitForPods determines whether to wait for Pods to be ready before marking the Service ready. -func (sia *serviceInitAwaiter) shouldWaitForPods(version serverVersion) bool { +func (sia *serviceInitAwaiter) shouldWaitForPods(version cluster.ServerVersion) bool { // For these special cases, skip the wait for Pod logic. if sia.emptyHeadlessOrExternalName() || sia.hasHeadlessServicePortBug(version) { sia.endpointsReady = true @@ -426,7 +427,7 @@ func (sia *serviceInitAwaiter) shouldWaitForPods(version serverVersion) bool { return true } -func (sia *serviceInitAwaiter) checkAndLogStatus(version serverVersion) bool { +func (sia *serviceInitAwaiter) checkAndLogStatus(version cluster.ServerVersion) bool { if !sia.shouldWaitForPods(version) { return sia.serviceReady } diff --git a/pkg/await/core_service_test.go b/pkg/await/core_service_test.go index f83c3da8b9..f845bce01d 100644 --- a/pkg/await/core_service_test.go +++ b/pkg/await/core_service_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/pulumi/pulumi-kubernetes/pkg/cluster" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/watch" @@ -15,7 +16,7 @@ func Test_Core_Service(t *testing.T) { description string serviceInput func(namespace, name string) *unstructured.Unstructured do func(services, endpoints chan watch.Event, settled chan struct{}, timeout chan time.Time) - version serverVersion + version cluster.ServerVersion expectedError error }{ { @@ -190,7 +191,7 @@ func Test_Core_Service(t *testing.T) { { description: "Should succeed if non-empty headless service doesn't target any Pods before k8s 1.12", serviceInput: headlessNonemptyServiceInput, - version: serverVersion{1, 11}, + version: cluster.ServerVersion{Major: 1, Minor: 11}, do: func(services, endpoints chan watch.Event, settled chan struct{}, timeout chan time.Time) { services <- watchAddedEvent(headlessNonemptyServiceOutput("default", "foo-4setj4y6")) @@ -201,7 +202,7 @@ func Test_Core_Service(t *testing.T) { { description: "Should fail if non-empty headless service doesn't target any Pods", serviceInput: headlessNonemptyServiceInput, - version: serverVersion{1, 12}, + version: cluster.ServerVersion{Major: 1, Minor: 12}, do: func(services, endpoints chan watch.Event, settled chan struct{}, timeout chan time.Time) { services <- watchAddedEvent(headlessNonemptyServiceOutput("default", "foo-4setj4y6")) @@ -239,7 +240,7 @@ func Test_Core_Service_Read(t *testing.T) { serviceInput func(namespace, name string) *unstructured.Unstructured service func(namespace, name string) *unstructured.Unstructured endpoint func(namespace, name string) *unstructured.Unstructured - version serverVersion + version cluster.ServerVersion expectedSubErrors []string }{ { @@ -280,13 +281,13 @@ func Test_Core_Service_Read(t *testing.T) { description: "Read succeed if headless non-empty Service doesn't target any Pods before k8s 1.12", serviceInput: headlessNonemptyServiceInput, service: headlessNonemptyServiceInput, - version: serverVersion{1, 11}, + version: cluster.ServerVersion{Major: 1, Minor: 11}, }, { description: "Read fail if headless non-empty Service doesn't target any Pods", serviceInput: headlessNonemptyServiceInput, service: headlessNonemptyServiceInput, - version: serverVersion{1, 12}, + version: cluster.ServerVersion{Major: 1, Minor: 12}, expectedSubErrors: []string{ "Service does not target any Pods. Selected Pods may not be ready, or " + "field '.spec.selector' may not match labels on any Pods"}, diff --git a/pkg/await/util.go b/pkg/await/util.go index d186aa7b42..9a05ac07ec 100644 --- a/pkg/await/util.go +++ b/pkg/await/util.go @@ -15,7 +15,6 @@ package await import ( - "fmt" "log" "sort" @@ -27,7 +26,6 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" ) @@ -66,17 +64,6 @@ func watchAddedEvent(obj runtime.Object) watch.Event { } } -// nolint -func stringifyEvents(events []v1.Event) string { - var output string - for _, e := range events { - output += fmt.Sprintf("\n * %s (%s): %s: %s", - e.InvolvedObject.Name, e.InvolvedObject.Kind, - e.Reason, e.Message) - } - return output -} - // nolint func getLastWarningsForObject( clientForEvents dynamic.ResourceInterface, namespace, name, kind string, limit int, @@ -152,29 +139,6 @@ func getLastWarningsForObject( // -------------------------------------------------------------------------- -// Version helpers. - -// -------------------------------------------------------------------------- - -// ServerVersion attempts to retrieve the server version from k8s. -// Returns the configured default version in case this fails. -func ServerVersion(cdi discovery.CachedDiscoveryInterface) serverVersion { - var version serverVersion - if sv, err := cdi.ServerVersion(); err == nil { - if v, err := parseVersion(sv); err == nil { - version = v - } else { - version = defaultVersion() - } - } else { - version = defaultVersion() - } - - return version -} - -// -------------------------------------------------------------------------- - // Response helpers. // -------------------------------------------------------------------------- diff --git a/pkg/await/version.go b/pkg/await/version.go index e306a23956..307523487f 100644 --- a/pkg/await/version.go +++ b/pkg/await/version.go @@ -14,131 +14,7 @@ package await -import ( - "fmt" - "regexp" - "strconv" - "strings" - - "github.com/pulumi/pulumi/pkg/diag" - "github.com/pulumi/pulumi/pkg/util/cmdutil" - "k8s.io/apimachinery/pkg/version" -) - -// Format v0.0.0(-master+$Format:%h$) -var gitVersionRe = regexp.MustCompile(`v([0-9]+).([0-9]+).([0-9]+).*`) - -// serverVersion captures k8s major.minor version in a parsed form -type serverVersion struct { - Major, Minor int -} - -// Compare returns -1/0/+1 iff v is less than / equal / greater than major.minor -func (v serverVersion) Compare(major, minor int) int { - a := v.Major - b := major - - if a == b { - a = v.Minor - b = minor - } - - var res int - if a > b { - res = 1 - } else if a == b { - res = 0 - } else { - res = -1 - } - return res -} - -func (v serverVersion) String() string { - return fmt.Sprintf("%d.%d", v.Major, v.Minor) -} - -// gitVersion captures k8s major.minor.patch version in a parsed form -type gitVersion struct { - Major, Minor, Patch int -} - -func (gv gitVersion) String() string { - return fmt.Sprintf("%d.%d.%d", gv.Major, gv.Minor, gv.Patch) -} - -// DefaultVersion takes a wild guess (v1.9) at the version of a Kubernetes cluster. -func defaultVersion() serverVersion { - cmdutil.Diag().Warningf( - diag.Message("", "Cluster failed to report its version number; falling back to 1.9"), false) - - // - // Fallback behavior to work around [1]. Some versions of minikube erroneously report a blank - // `version.Info`, which will cause us to break. It is necessary for us to check this version for - // `Delete`, because of bugs and quirks in various Kubernetes versions. Currently it is only - // important that we know the version is above or below 1.5, so here we (hopefully) temporarily - // choose to fall back to 1.9, which is what most people running minikube use out of the box. - // - // [1]: https://github.com/kubernetes/minikube/issues/2505 - // - return serverVersion{Major: 1, Minor: 9} -} - -func parseGitVersion(versionString string) (gitVersion, error) { - parsedVersion := gitVersionRe.FindStringSubmatch(versionString) - if len(parsedVersion) != 4 { - err := fmt.Errorf("unable to parse git version %q", versionString) - return gitVersion{}, err - } - - var gv gitVersion - var err error - gv.Major, err = strconv.Atoi(parsedVersion[1]) - if err != nil { - return gitVersion{}, err - } - gv.Minor, err = strconv.Atoi(parsedVersion[2]) - if err != nil { - return gitVersion{}, err - } - gv.Patch, err = strconv.Atoi(parsedVersion[3]) - if err != nil { - return gitVersion{}, err - } - - return gv, nil -} - -// parseVersion parses version.Info into a serverVersion struct -func parseVersion(v *version.Info) (serverVersion, error) { - fallbackToGitVersion := false - - major, err := strconv.Atoi(v.Major) - if err != nil { - fallbackToGitVersion = true - } - - // trim "+" in minor version (happened on GKE) - v.Minor = strings.TrimSuffix(v.Minor, "+") - - minor, err := strconv.Atoi(v.Minor) - if err != nil { - fallbackToGitVersion = true - } - - if fallbackToGitVersion { - gv, err := parseGitVersion(v.GitVersion) - if err != nil { - return serverVersion{}, err - } - - return serverVersion{Major: gv.Major, Minor: gv.Minor}, nil - } - - return serverVersion{Major: major, Minor: minor}, nil -} - -// canonicalizeDeploymentAPIVersion unifies the various pre-release apiVerion values for a +// canonicalizeDeploymentAPIVersion unifies the various pre-release apiVersion values for a // Deployment into "apps/v1". func canonicalizeDeploymentAPIVersion(ver string) string { switch ver { diff --git a/pkg/cluster/version.go b/pkg/cluster/version.go new file mode 100644 index 0000000000..aa59467e00 --- /dev/null +++ b/pkg/cluster/version.go @@ -0,0 +1,140 @@ +// Copyright 2016-2019, Pulumi Corporation. +// +// 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 cluster + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/discovery" +) + +// ServerVersion captures k8s major.minor version in a parsed form +type ServerVersion struct { + Major, Minor int +} + +func (v ServerVersion) String() string { + return fmt.Sprintf("%d.%d", v.Major, v.Minor) +} + +// Compare returns -1/0/+1 iff v is less than / equal / greater than input version. +func (v ServerVersion) Compare(version ServerVersion) int { + a := v.Major + b := version.Major + + if a == b { + a = v.Minor + b = version.Minor + } + + var res int + if a > b { + res = 1 + } else if a == b { + res = 0 + } else { + res = -1 + } + return res +} + +// GetServerVersion attempts to retrieve the server version from k8s. +// Returns the configured default version in case this fails. +func GetServerVersion(cdi discovery.CachedDiscoveryInterface) ServerVersion { + defaultSV := ServerVersion{ + Major: 1, + Minor: 14, + } + + if sv, err := cdi.ServerVersion(); err == nil { + if v, err := parseVersion(sv); err == nil { + return v + } else { + return defaultSV + } + } + + return defaultSV +} + +// gitVersion captures k8s major.minor.patch version in a parsed form +type gitVersion struct { + Major, Minor, Patch int +} + +func (gv gitVersion) String() string { + return fmt.Sprintf("%d.%d.%d", gv.Major, gv.Minor, gv.Patch) +} + +func parseGitVersion(versionString string) (gitVersion, error) { + // Format v0.0.0(-master+$Format:%h$) + gitVersionRe := regexp.MustCompile(`v([0-9]+).([0-9]+).([0-9]+).*`) + + parsedVersion := gitVersionRe.FindStringSubmatch(versionString) + if len(parsedVersion) != 4 { + err := fmt.Errorf("unable to parse git version %q", versionString) + return gitVersion{}, err + } + + var gv gitVersion + var err error + gv.Major, err = strconv.Atoi(parsedVersion[1]) + if err != nil { + return gitVersion{}, err + } + gv.Minor, err = strconv.Atoi(parsedVersion[2]) + if err != nil { + return gitVersion{}, err + } + gv.Patch, err = strconv.Atoi(parsedVersion[3]) + if err != nil { + return gitVersion{}, err + } + + return gv, nil +} + +// parseVersion parses version.Info into a serverVersion struct +func parseVersion(v *version.Info) (ServerVersion, error) { + fallbackToGitVersion := false + + major, err := strconv.Atoi(v.Major) + if err != nil { + fallbackToGitVersion = true + } + + // trim "+" in minor version (happened on GKE) + v.Minor = strings.TrimSuffix(v.Minor, "+") + + minor, err := strconv.Atoi(v.Minor) + if err != nil { + fallbackToGitVersion = true + } + + if fallbackToGitVersion { + gv, err := parseGitVersion(v.GitVersion) + if err != nil { + return ServerVersion{}, err + } + + return ServerVersion{Major: gv.Major, Minor: gv.Minor}, nil + } + + return ServerVersion{Major: major, Minor: minor}, nil +} diff --git a/pkg/await/version_test.go b/pkg/cluster/version_test.go similarity index 71% rename from pkg/await/version_test.go rename to pkg/cluster/version_test.go index c9afa02ba5..f593073ad3 100644 --- a/pkg/await/version_test.go +++ b/pkg/cluster/version_test.go @@ -4,7 +4,7 @@ // 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 +// 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, @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package await +package cluster import ( "reflect" @@ -21,19 +21,88 @@ import ( "k8s.io/apimachinery/pkg/version" ) +func TestServerVersion_Compare(t *testing.T) { + v := ServerVersion{Major: 2, Minor: 3} + + tests := []struct { + name string + input ServerVersion + version ServerVersion + want int + }{ + {"Older major", ServerVersion{1, 0}, v, 1}, + {"Older minor", ServerVersion{2, 0}, v, 1}, + {"Equal", ServerVersion{2, 3}, v, 0}, + {"Newer minor", ServerVersion{2, 4}, v, -1}, + {"Newer major", ServerVersion{3, 0}, v, -1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := v.Compare(tt.input); got != tt.want { + t.Errorf("Compare() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseGitVersion(t *testing.T) { + type args struct { + versionString string + } + tests := []struct { + name string + args args + want gitVersion + wantErr bool + }{ + { + name: "Valid", + args: args{"v1.13.1"}, + want: gitVersion{1, 13, 1}, + }, + { + name: "Valid + suffix", + args: args{"v1.8.8-test.0"}, + want: gitVersion{1, 8, 8}, + }, + { + name: "Missing v", + args: args{"1.13.0"}, + wantErr: true, + }, + { + name: "Missing patch", + args: args{"1.13"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseGitVersion(tt.args.versionString) + if (err != nil) != tt.wantErr { + t.Errorf("parseGitVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseGitVersion() = %v, want %v", got, tt.want) + } + }) + } +} + func TestParseVersion(t *testing.T) { tests := []struct { input version.Info - expected serverVersion + expected ServerVersion error bool }{ { input: version.Info{Major: "1", Minor: "6"}, - expected: serverVersion{Major: 1, Minor: 6}, + expected: ServerVersion{Major: 1, Minor: 6}, }, { input: version.Info{Major: "1", Minor: "70"}, - expected: serverVersion{Major: 1, Minor: 70}, + expected: ServerVersion{Major: 1, Minor: 70}, }, { input: version.Info{Major: "1", Minor: "6x"}, @@ -41,35 +110,35 @@ func TestParseVersion(t *testing.T) { }, { input: version.Info{Major: "1", Minor: "8+"}, - expected: serverVersion{Major: 1, Minor: 8}, + expected: ServerVersion{Major: 1, Minor: 8}, }, { input: version.Info{Major: "", Minor: "", GitVersion: "v1.8.0"}, - expected: serverVersion{Major: 1, Minor: 8}, + expected: ServerVersion{Major: 1, Minor: 8}, }, { input: version.Info{Major: "1", Minor: "", GitVersion: "v1.8.0"}, - expected: serverVersion{Major: 1, Minor: 8}, + expected: ServerVersion{Major: 1, Minor: 8}, }, { input: version.Info{Major: "", Minor: "8", GitVersion: "v1.8.0"}, - expected: serverVersion{Major: 1, Minor: 8}, + expected: ServerVersion{Major: 1, Minor: 8}, }, { input: version.Info{Major: "", Minor: "", GitVersion: "v1.8.8-test.0"}, - expected: serverVersion{Major: 1, Minor: 8}, + expected: ServerVersion{Major: 1, Minor: 8}, }, { input: version.Info{Major: "1", Minor: "8", GitVersion: "v1.9.0"}, - expected: serverVersion{Major: 1, Minor: 8}, + expected: ServerVersion{Major: 1, Minor: 8}, }, { input: version.Info{Major: "1", Minor: "9", GitVersion: "v1.9.1"}, - expected: serverVersion{Major: 1, Minor: 9}, + expected: ServerVersion{Major: 1, Minor: 9}, }, { input: version.Info{Major: "1", Minor: "13", GitVersion: "v1.13.0"}, - expected: serverVersion{Major: 1, Minor: 13}, + expected: ServerVersion{Major: 1, Minor: 13}, }, { input: version.Info{Major: "", Minor: "", GitVersion: "v1.a"}, @@ -94,69 +163,3 @@ func TestParseVersion(t *testing.T) { } } } - -func TestVersionCompare(t *testing.T) { - v := serverVersion{Major: 2, Minor: 3} - tests := []struct { - major, minor, result int - }{ - {major: 1, minor: 0, result: 1}, - {major: 2, minor: 0, result: 1}, - {major: 2, minor: 2, result: 1}, - {major: 2, minor: 2, result: 1}, - {major: 2, minor: 3, result: 0}, - {major: 2, minor: 4, result: -1}, - {major: 3, minor: 0, result: -1}, - } - for _, test := range tests { - res := v.Compare(test.major, test.minor) - if res != test.result { - t.Errorf("%d.%d => Expected %d, got %d", test.major, test.minor, test.result, res) - } - } -} - -func Test_parseGitVersion(t *testing.T) { - type args struct { - versionString string - } - tests := []struct { - name string - args args - want gitVersion - wantErr bool - }{ - { - name: "Valid", - args: args{"v1.13.1"}, - want: gitVersion{1, 13, 1}, - }, - { - name: "Valid + suffix", - args: args{"v1.8.8-test.0"}, - want: gitVersion{1, 8, 8}, - }, - { - name: "Missing v", - args: args{"1.13.0"}, - wantErr: true, - }, - { - name: "Missing patch", - args: args{"1.13"}, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseGitVersion(tt.args.versionString) - if (err != nil) != tt.wantErr { - t.Errorf("parseGitVersion() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("parseGitVersion() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkg/kinds/deprecated.go b/pkg/kinds/deprecated.go index f59b996ca8..ac39f77acf 100644 --- a/pkg/kinds/deprecated.go +++ b/pkg/kinds/deprecated.go @@ -15,6 +15,9 @@ package kinds import ( + "fmt" + + "github.com/pulumi/pulumi-kubernetes/pkg/cluster" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -27,6 +30,23 @@ func DeprecatedApiVersion(gvk schema.GroupVersionKind) bool { return SuggestedApiVersion(gvk) != gvkStr(gvk) } +// RemovedApiVersion returns true if the given GVK has been removed in the given k8s version, and the corresponding +// ServerVersion where the GVK was removed. +func RemovedApiVersion(gvk schema.GroupVersionKind, version cluster.ServerVersion) (bool, *cluster.ServerVersion) { + var removedIn cluster.ServerVersion + + switch gvk.GroupVersion() { + case schema.GroupVersion{Group: "extensions", Version: "v1beta1"}, + schema.GroupVersion{Group: "apps", Version: "v1beta1"}, + schema.GroupVersion{Group: "apps", Version: "v1beta2"}: + removedIn = cluster.ServerVersion{Major: 1, Minor: 16} + default: + return false, nil + } + + return version.Compare(removedIn) >= 0, &removedIn +} + // SuggestedApiVersion returns a string with the suggested apiVersion for a given GVK. // This is used to provide useful warning messages when a user creates a resource using a deprecated GVK. func SuggestedApiVersion(gvk schema.GroupVersionKind) string { @@ -48,5 +68,36 @@ func SuggestedApiVersion(gvk schema.GroupVersionKind) string { default: return gvkStr(gvk) } +} + +// upstreamDocsLink returns a link to information about apiVersion deprecations for the given k8s version. +func upstreamDocsLink(version cluster.ServerVersion) string { + switch version { + case cluster.ServerVersion{Major: 1, Minor: 16}: + return "https://git.k8s.io/kubernetes/CHANGELOG-1.16.md#deprecations-and-removals" + default: + return "" + } +} + +// RemovedApiError is returned if the provided GVK does not exist in the targeted k8s cluster because the apiVersion +// has been deprecated and removed. +type RemovedApiError struct { + GVK schema.GroupVersionKind + Version *cluster.ServerVersion +} + +func (e *RemovedApiError) Error() string { + if e.Version == nil { + return fmt.Sprintf("apiVersion %q was removed in a previous version of Kubernetes", gvkStr(e.GVK)) + } + link := upstreamDocsLink(*e.Version) + str := fmt.Sprintf("apiVersion %q was removed in Kubernetes %s. Use %q instead.", + gvkStr(e.GVK), e.Version, SuggestedApiVersion(e.GVK)) + + if len(link) > 0 { + str += fmt.Sprintf("\nSee %s for more information.", link) + } + return str } diff --git a/pkg/kinds/deprecated_test.go b/pkg/kinds/deprecated_test.go index 0773dbfc7a..6e3ab36404 100644 --- a/pkg/kinds/deprecated_test.go +++ b/pkg/kinds/deprecated_test.go @@ -15,8 +15,10 @@ package kinds import ( + "reflect" "testing" + "github.com/pulumi/pulumi-kubernetes/pkg/cluster" . "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -84,3 +86,35 @@ func TestSuggestedApiVersion(t *testing.T) { }) } } + +func TestRemovedApiVersion(t *testing.T) { + type args struct { + gvk GroupVersionKind + version cluster.ServerVersion + } + tests := []struct { + name string + args args + wantRemoved bool + wantVersion *cluster.ServerVersion + }{ + {"API exists", args{ + GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + cluster.ServerVersion{Major: 1, Minor: 16}}, false, nil}, + {"API removed", args{ + GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Deployment"}, + cluster.ServerVersion{Major: 1, Minor: 16}}, + true, &cluster.ServerVersion{Major: 1, Minor: 16}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := RemovedApiVersion(tt.args.gvk, tt.args.version) + if got != tt.wantRemoved { + t.Errorf("RemovedApiVersion() got = %v, want %v", got, tt.wantRemoved) + } + if !reflect.DeepEqual(got1, tt.wantVersion) { + t.Errorf("RemovedApiVersion() got1 = %v, want %v", got1, tt.wantVersion) + } + }) + } +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 7615b1d4ec..07207c40d1 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -30,6 +30,7 @@ import ( pkgerrors "github.com/pkg/errors" "github.com/pulumi/pulumi-kubernetes/pkg/await" "github.com/pulumi/pulumi-kubernetes/pkg/clients" + "github.com/pulumi/pulumi-kubernetes/pkg/cluster" "github.com/pulumi/pulumi-kubernetes/pkg/gen" "github.com/pulumi/pulumi-kubernetes/pkg/kinds" "github.com/pulumi/pulumi-kubernetes/pkg/logging" @@ -97,7 +98,8 @@ type kubeProvider struct { suppressDeprecationWarnings bool enableSecrets bool - clientSet *clients.DynamicClientSet + clientSet *clients.DynamicClientSet + k8sVersion cluster.ServerVersion } var _ pulumirpc.ResourceProviderServer = (*kubeProvider)(nil) @@ -307,6 +309,8 @@ func (k *kubeProvider) Configure(_ context.Context, req *pulumirpc.ConfigureRequ } k.clientSet = cs + k.k8sVersion = cluster.GetServerVersion(cs.DiscoveryClientCached) + return &pulumirpc.ConfigureResponse{ AcceptSecrets: true, }, nil @@ -443,6 +447,9 @@ func (k *kubeProvider) Check(ctx context.Context, req *pulumirpc.CheckRequest) ( return nil, err } + if removed, version := kinds.RemovedApiVersion(gvk, k.k8sVersion); removed { + return nil, &kinds.RemovedApiError{GVK: gvk, Version: version} + } if !k.suppressDeprecationWarnings && kinds.DeprecatedApiVersion(gvk) { _ = k.host.Log(ctx, diag.Warning, urn, gen.ApiVersionComment(gvk)) }