Skip to content

Commit

Permalink
Improve apiVersion deprecation comments
Browse files Browse the repository at this point in the history
Replace the deprecation comments in the SDKs with more
relevant information, and emit warnings when resources
are created using deprecated apiVersions.
  • Loading branch information
lblackstone committed Aug 30, 2019
1 parent 8912a60 commit 0d642a9
Show file tree
Hide file tree
Showing 33 changed files with 440 additions and 198 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

### Improvements

- Warn for deprecated apiVersions.
(https://github.com/pulumi/pulumi-kubernetes/pull/779).

### Bug fixes

- Fix name collisions in the Charts/YAML Python packages
Expand Down
15 changes: 12 additions & 3 deletions pkg/gen/awaitComments.go → pkg/gen/additionalComments.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/pulumi/pulumi-kubernetes/pkg/await"
"github.com/pulumi/pulumi-kubernetes/pkg/kinds"
"k8s.io/apimachinery/pkg/runtime/schema"
)

func timeoutComment(kind kinds.Kind) string {
Expand Down Expand Up @@ -51,7 +52,7 @@ time out and mark the resource update as Failed. You can override the default ti
by %s`, kind, timeoutStr, timeoutOverride)
}

func comments(kind kinds.Kind) string {
func awaitComments(kind kinds.Kind) string {
const preamble = `This resource waits until it is ready before registering success for
create/update and populating output properties from the current state of the resource.
The following conditions are used to determine whether the resource creation has
Expand Down Expand Up @@ -122,7 +123,7 @@ out. To work around this limitation, set 'pulumi.com/skipAwait: "true"' on
2. The value of '.status.updateRevision' matches '.status.currentRevision'.
`
default:
panic("comments: unhandled kind")
panic("awaitComments: unhandled kind")
}

comment += timeoutComment(kind)
Expand All @@ -135,8 +136,16 @@ func AwaitComment(kind string) string {
k := kinds.Kind(kind)
switch k {
case kinds.Deployment, kinds.Ingress, kinds.Job, kinds.Pod, kinds.Service, kinds.StatefulSet:
return prefix + comments(k)
return prefix + awaitComments(k)
default:
return ""
}
}

func ApiVersionComment(gvk schema.GroupVersionKind) string {
const template = `%s is not supported by Kubernetes 1.16+ clusters. Use %s instead.
`
gvkStr := gvk.GroupVersion().String() + "/" + gvk.Kind
return fmt.Sprintf(template, gvkStr, kinds.SuggestedApiVersion(gvk))
}
39 changes: 34 additions & 5 deletions pkg/gen/typegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,34 @@ func stripPrefix(name string) string {
return strings.TrimPrefix(name, prefix)
}

func fmtComment(comment interface{}, prefix string, bareRender bool, opts groupOpts) string {
func replaceDeprecationComment(comment string, gvk schema.GroupVersionKind, language language) string {
// The deprecation warning doesn't always appear in the same place in the OpenAPI comments.
// Standardize the message and where it appears in our docs.
re1 := regexp.MustCompile(`^DEPRECATED - .* is deprecated by .* for more information\.\s*`)
re2 := regexp.MustCompile(`DEPRECATED - .* is deprecated by .* for more information\.\s*`)

var replacement string
switch language {
case typescript:
replacement = "@deprecated " + ApiVersionComment(gvk)
case python:
replacement = "DEPRECATED - " + ApiVersionComment(gvk)
default:
panic(fmt.Sprintf("Unsupported language '%s'", language))
}

if re1.MatchString(comment) {
return re1.ReplaceAllString(comment, replacement)
} else if re2.MatchString(comment) {
return ApiVersionComment(gvk) + re2.ReplaceAllString(comment, "")
} else {
return comment
}
}

func fmtComment(
comment interface{}, prefix string, bareRender bool, opts groupOpts, gvk schema.GroupVersionKind,
) string {
if comment == nil {
return ""
}
Expand Down Expand Up @@ -294,6 +321,8 @@ func fmtComment(comment interface{}, prefix string, bareRender bool, opts groupO
`https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md`,
-1)

commentstr = replaceDeprecationComment(commentstr, gvk, opts.language)

split := strings.Split(commentstr, "\n")
var lines []string
for _, paragraph := range split {
Expand Down Expand Up @@ -721,8 +750,8 @@ func createGroups(definitionsJSON map[string]interface{}, opts groupOpts) []*Gro
}

return &Property{
comment: fmtComment(prop["description"], prefix, false, opts),
pythonConstructorComment: fmtComment(prop["description"], prefix+prefix+" ", true, opts),
comment: fmtComment(prop["description"], prefix, false, opts, d.gvk),
pythonConstructorComment: fmtComment(prop["description"], prefix+prefix+" ", true, opts, d.gvk),
propType: t,
pythonConstructorPropType: pyConstructorT,
name: propName,
Expand Down Expand Up @@ -781,8 +810,8 @@ func createGroups(definitionsJSON map[string]interface{}, opts groupOpts) []*Gro
kind: d.gvk.Kind,
// NOTE: This transformation assumes git users on Windows to set
// the "check in with UNIX line endings" setting.
comment: fmtComment(d.data["description"], " ", true, opts),
awaitComment: fmtComment(AwaitComment(d.gvk.Kind), prefix, true, opts),
comment: fmtComment(d.data["description"], " ", true, opts, d.gvk),
awaitComment: fmtComment(AwaitComment(d.gvk.Kind), prefix, true, opts, d.gvk),
properties: properties,
requiredInputProperties: requiredInputProperties,
optionalInputProperties: optionalInputProperties,
Expand Down
52 changes: 52 additions & 0 deletions pkg/kinds/deprecated.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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 kinds

import (
"k8s.io/apimachinery/pkg/runtime/schema"
)

func gvkStr(gvk schema.GroupVersionKind) string {
return gvk.GroupVersion().String() + "/" + gvk.Kind
}

// DeprecatedApiVersion returns true if the given GVK is deprecated in the most recent k8s release.
func DeprecatedApiVersion(gvk schema.GroupVersionKind) bool {
return SuggestedApiVersion(gvk) != gvkStr(gvk)
}

// 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 {
switch gvk.GroupVersion() {
case schema.GroupVersion{Group: "apps", Version: "v1beta1"},
schema.GroupVersion{Group: "apps", Version: "v1beta2"}:
return "apps/v1/" + gvk.Kind
case schema.GroupVersion{Group: "extensions", Version: "v1beta1"}:
switch Kind(gvk.Kind) {
case DaemonSet, Deployment, NetworkPolicy, ReplicaSet:
return "apps/v1/" + gvk.Kind
case Ingress:
return "networking/v1beta1/" + gvk.Kind
case PodSecurityPolicy:
return "policy/v1beta1/" + gvk.Kind
default:
return gvkStr(gvk)
}
default:
return gvkStr(gvk)
}

}
86 changes: 86 additions & 0 deletions pkg/kinds/deprecated_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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 kinds

import (
"testing"

. "k8s.io/apimachinery/pkg/runtime/schema"
)

func TestDeprecatedApiVersion(t *testing.T) {
tests := []struct {
gvk GroupVersionKind
want bool
}{
{GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, false},
{GroupVersionKind{Group: "apps", Version: "v1beta1", Kind: "Deployment"}, true},
{GroupVersionKind{Group: "apps", Version: "v1beta2", Kind: "Deployment"}, true},
{GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "DaemonSet"}, true},
{GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Deployment"}, true},
{GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Ingress"}, true},
{GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicy"}, true},
{GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicy"}, true},
{GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "ReplicaSet"}, true},
}
for _, tt := range tests {
t.Run(tt.gvk.String(), func(t *testing.T) {
if got := DeprecatedApiVersion(tt.gvk); got != tt.want {
t.Errorf("DeprecatedApiVersion() = %v, want %v", got, tt.want)
}
})
}
}

func TestSuggestedApiVersion(t *testing.T) {
tests := []struct {
gvk GroupVersionKind
want string
}{
// Deprecated ApiVersions return the suggested version string.
{
GroupVersionKind{Group: "apps", Version: "v1beta1", Kind: "Deployment"},
"apps/v1/Deployment",
},
{
GroupVersionKind{Group: "apps", Version: "v1beta2", Kind: "Deployment"},
"apps/v1/Deployment",
},
{
GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Deployment"},
"apps/v1/Deployment",
},
{
GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Ingress"},
"networking/v1beta1/Ingress",
},
{
GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicy"},
"policy/v1beta1/PodSecurityPolicy",
},
// Current ApiVersions return the same version string.
{
GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
"apps/v1/Deployment",
},
}
for _, tt := range tests {
t.Run(tt.gvk.String(), func(t *testing.T) {
if got := SuggestedApiVersion(tt.gvk); got != tt.want {
t.Errorf("SuggestedApiVersion() = %v, want %v", got, tt.want)
}
})
}
}
44 changes: 22 additions & 22 deletions pkg/provider/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,51 +30,51 @@ func forceNewProperties(gvk schema.GroupVersionKind) []string {
return props
}

type groups map[string]versions
type versions map[string]kinds
type kinds map[string]properties
type _groups map[string]_versions
type _versions map[string]_kinds
type _kinds map[string]properties
type properties []string

var forceNew = groups{
"apps": versions{
var forceNew = _groups{
"apps": _versions{
// NOTE: .spec.selector triggers a replacement in Deployment only AFTER v1beta1.
"v1beta1": kinds{"StatefulSet": statefulSet},
"v1beta2": kinds{
"v1beta1": _kinds{"StatefulSet": statefulSet},
"v1beta2": _kinds{
"Deployment": deployment,
"StatefulSet": statefulSet},
"v1": kinds{
"v1": _kinds{
"Deployment": deployment,
"StatefulSet": statefulSet},
},
// List `core` under its canonical name and under it's legacy name (i.e., "", the empty string)
// for compatibility purposes.
"core": core,
"": core,
"policy": versions{
"v1beta1": kinds{"PodDisruptionBudget": podDisruptionBudget},
"policy": _versions{
"v1beta1": _kinds{"PodDisruptionBudget": podDisruptionBudget},
},
"rbac.authorization.k8s.io": versions{
"v1alpha1": kinds{"ClusterRoleBinding": roleBinding, "RoleBinding": roleBinding},
"v1beta1": kinds{"ClusterRoleBinding": roleBinding, "RoleBinding": roleBinding},
"v1": kinds{"ClusterRoleBinding": roleBinding, "RoleBinding": roleBinding},
"rbac.authorization.k8s.io": _versions{
"v1alpha1": _kinds{"ClusterRoleBinding": roleBinding, "RoleBinding": roleBinding},
"v1beta1": _kinds{"ClusterRoleBinding": roleBinding, "RoleBinding": roleBinding},
"v1": _kinds{"ClusterRoleBinding": roleBinding, "RoleBinding": roleBinding},
},
"storage.k8s.io": versions{
"v1": kinds{
"storage.k8s.io": _versions{
"v1": _kinds{
"StorageClass": properties{
".parameters",
".provisioner",
},
},
},
"batch": versions{
"v1beta1": kinds{"Job": job},
"v1": kinds{"Job": job},
"v2alpha1": kinds{"Job": job},
"batch": _versions{
"v1beta1": _kinds{"Job": job},
"v1": _kinds{"Job": job},
"v2alpha1": _kinds{"Job": job},
},
}

var core = versions{
"v1": kinds{
var core = _versions{
"v1": _kinds{
"ConfigMap": properties{".binaryData", ".data"},
"PersistentVolumeClaim": append(
properties{
Expand Down
7 changes: 7 additions & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ 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/gen"
"github.com/pulumi/pulumi-kubernetes/pkg/kinds"
"github.com/pulumi/pulumi-kubernetes/pkg/logging"
"github.com/pulumi/pulumi-kubernetes/pkg/metadata"
"github.com/pulumi/pulumi-kubernetes/pkg/openapi"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/pulumi/pulumi/pkg/resource/provider"
Expand Down Expand Up @@ -420,6 +423,10 @@ func (k *kubeProvider) Check(ctx context.Context, req *pulumirpc.CheckRequest) (
return nil, err
}

if kinds.DeprecatedApiVersion(gvk) {
_ = k.host.Log(ctx, diag.Warning, urn, gen.ApiVersionComment(gvk))
}

// If a default namespace is set on the provider for this resource, check if the resource has Namespaced
// or Global scope. For namespaced resources, set the namespace to the default value if unset.
if k.defaultNamespace != "" && len(newInputs.GetNamespace()) == 0 {
Expand Down
5 changes: 3 additions & 2 deletions sdk/nodejs/apps/v1beta1/ControllerRevision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import * as outputs from "../../types/output";
import { getVersion } from "../../version";

/**
* DEPRECATED - This group version of ControllerRevision is deprecated by
* apps/v1beta2/ControllerRevision. See the release notes for more information.
* @deprecated apps/v1beta1/ControllerRevision is not supported by Kubernetes 1.16+ clusters.
* Use apps/v1/ControllerRevision instead.
*
* ControllerRevision implements an immutable snapshot of state data. Clients are responsible
* for serializing and deserializing the objects that contain their internal state. Once a
* ControllerRevision has been successfully created, it can not be updated. The API Server will
Expand Down
7 changes: 4 additions & 3 deletions sdk/nodejs/apps/v1beta1/Deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import * as outputs from "../../types/output";
import { getVersion } from "../../version";

/**
* DEPRECATED - This group version of Deployment is deprecated by apps/v1beta2/Deployment. See
* the release notes for more information. Deployment enables declarative updates for Pods and
* ReplicaSets.
* @deprecated apps/v1beta1/Deployment is not supported by Kubernetes 1.16+ clusters. Use
* apps/v1/Deployment instead.
*
* Deployment enables declarative updates for Pods and ReplicaSets.
*
* This resource waits until it is ready before registering success for
* create/update and populating output properties from the current state of the resource.
Expand Down
7 changes: 4 additions & 3 deletions sdk/nodejs/apps/v1beta1/StatefulSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import * as outputs from "../../types/output";
import { getVersion } from "../../version";

/**
* DEPRECATED - This group version of StatefulSet is deprecated by apps/v1beta2/StatefulSet. See
* the release notes for more information. StatefulSet represents a set of pods with consistent
* identities. Identities are defined as:
* @deprecated apps/v1beta1/StatefulSet is not supported by Kubernetes 1.16+ clusters. Use
* apps/v1/StatefulSet instead.
*
* StatefulSet represents a set of pods with consistent identities. Identities are defined as:
* - Network: A single stable DNS and hostname.
* - Storage: As many VolumeClaims as requested.
* The StatefulSet guarantees that a given network identity will always map to the same storage
Expand Down
Loading

0 comments on commit 0d642a9

Please sign in to comment.