Merge pull request #4226 from inteon/simple_kubectl_check_version

add 'kubectl cert-manager version'
This commit is contained in:
jetstack-bot 2021-08-03 12:36:19 +01:00 committed by GitHub
commit d0f4c82baf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 904 additions and 17 deletions

View File

@ -67,7 +67,7 @@ kubectl cert-manager is a CLI tool manage and configure cert-manager resources f
}
ioStreams := genericclioptions.IOStreams{In: in, Out: out, ErrOut: err}
cmds.AddCommand(version.NewCmdVersion(ctx, ioStreams))
cmds.AddCommand(version.NewCmdVersion(ctx, ioStreams, factory))
cmds.AddCommand(convert.NewCmdConvert(ctx, ioStreams))
cmds.AddCommand(create.NewCmdCreate(ctx, ioStreams, factory))
cmds.AddCommand(renew.NewCmdRenew(ctx, ioStreams, factory))

View File

@ -56,8 +56,7 @@ const (
defaultCertManagerNamespace = "cert-manager"
)
const installDesc = `
This command installs cert-manager. It uses the Helm libraries to do so.
const installDesc = `This command installs cert-manager. It uses the Helm libraries to do so.
The latest published cert-manager chart in the "https://charts.jetstack.io" repo is used.
Most of the features supported by 'helm install' are also supported by this command.

View File

@ -7,9 +7,11 @@ go_library(
visibility = ["//visibility:public"],
deps = [
"//pkg/util:go_default_library",
"//pkg/util/versionchecker:go_default_library",
"@com_github_spf13_cobra//:go_default_library",
"@io_k8s_cli_runtime//pkg/genericclioptions:go_default_library",
"@io_k8s_kubectl//pkg/cmd/util:go_default_library",
"@io_k8s_kubectl//pkg/scheme:go_default_library",
"@io_k8s_sigs_yaml//:go_default_library",
],
)

View File

@ -25,19 +25,32 @@ import (
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme"
"sigs.k8s.io/yaml"
"github.com/jetstack/cert-manager/pkg/util"
"github.com/jetstack/cert-manager/pkg/util/versionchecker"
)
// Version is a struct for version information
type Version struct {
ClientVersion *util.Version `json:"clientVersion,omitempty"`
ServerVersion *versionchecker.Version `json:"serverVersion,omitempty"`
}
// Options is a struct to support version command
type Options struct {
// If true, don't try to retrieve the installed version
ClientOnly bool
// If true, only prints the version number.
Short bool
// Output is the target output format for the version string. This may be of
// value "", "json" or "yaml".
Output string
// If true, prints the version number.
Short bool
VersionChecker versionchecker.Interface
genericclioptions.IOStreams
}
@ -49,23 +62,48 @@ func NewOptions(ioStreams genericclioptions.IOStreams) *Options {
}
}
const versionLong = `Print the cert-manager kubectl plugin version and the deployed cert-manager version.
The kubectl plugin version is embedded in the binary and directly displayed. Determining
the the deployed cert-manager version is done by querying the cert-manger resources.
First, the tool looks at the labels of the cert-manager CRD resources. Then, it searches
for the labels of the resources related the the cert-manager webhook linked in the CRDs.
It also tries to derive the version from the docker image tag of that webhook service.
After gathering all this version information, the tool checks if all versions are the same
and returns that version. If no version information is found or the found versions differ,
an error will be displayed.
The '--client' flag can be used to disable the logic that tries to determine the installed
cert-manager version.
Some example uses:
$ kubectl cert-manager version
or
$ kubectl cert-manager version --client
or
$ kubectl cert-manager version --short
or
$ kubectl cert-manager version -o yaml
`
// NewCmdVersion returns a cobra command for fetching versions
func NewCmdVersion(ctx context.Context, ioStreams genericclioptions.IOStreams) *cobra.Command {
func NewCmdVersion(ctx context.Context, ioStreams genericclioptions.IOStreams, factory cmdutil.Factory) *cobra.Command {
o := NewOptions(ioStreams)
cmd := &cobra.Command{
Use: "version",
Short: "Print the kubectl cert-manager version",
Long: "Print the kubectl cert-manager version",
Short: "Print the cert-manager kubectl plugin version and the deployed cert-manager version",
Long: versionLong,
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(o.Validate())
cmdutil.CheckErr(o.Run())
cmdutil.CheckErr(o.Complete(factory))
cmdutil.CheckErr(o.Run(ctx))
},
}
cmd.Flags().StringVarP(&o.Output, "output", "o", "", "One of '', 'yaml' or 'json'.")
cmd.Flags().BoolVar(&o.Short, "short", false, "If true, print just the version number.")
cmd.Flags().BoolVar(&o.ClientOnly, "client", o.ClientOnly, "If true, shows client version only (no server required).")
cmd.Flags().BoolVar(&o.Short, "short", o.Short, "If true, print just the version number.")
cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, "One of 'yaml' or 'json'.")
return cmd
}
@ -79,16 +117,52 @@ func (o *Options) Validate() error {
}
}
// Complete takes the command arguments and factory and infers any remaining options.
func (o *Options) Complete(factory cmdutil.Factory) error {
if o.ClientOnly {
return nil
}
restConfig, err := factory.ToRESTConfig()
if err != nil {
return fmt.Errorf("cannot create the REST config: %v", err)
}
o.VersionChecker, err = versionchecker.New(restConfig, scheme.Scheme)
if err != nil {
return err
}
return nil
}
// Run executes version command
func (o *Options) Run() error {
versionInfo := util.VersionInfo()
func (o *Options) Run(ctx context.Context) error {
var (
serverVersion *versionchecker.Version
serverErr error
versionInfo Version
)
clientVersion := util.VersionInfo()
versionInfo.ClientVersion = &clientVersion
if !o.ClientOnly {
serverVersion, serverErr = o.VersionChecker.Version(ctx)
versionInfo.ServerVersion = serverVersion
}
switch o.Output {
case "":
if o.Short {
fmt.Fprintf(o.Out, "%s\n", versionInfo.GitVersion)
fmt.Fprintf(o.Out, "Client Version: %s\n", clientVersion.GitVersion)
if serverVersion != nil {
fmt.Fprintf(o.Out, "Server Version: %s\n", serverVersion.Detected)
}
} else {
fmt.Fprintf(o.Out, "%#v\n", versionInfo)
fmt.Fprintf(o.Out, "Client Version: %s\n", fmt.Sprintf("%#v", clientVersion))
if serverVersion != nil {
fmt.Fprintf(o.Out, "Server Version: %s\n", fmt.Sprintf("%#v", serverVersion))
}
}
case "yaml":
marshalled, err := yaml.Marshal(&versionInfo)
@ -108,5 +182,5 @@ func (o *Options) Run() error {
return fmt.Errorf("VersionOptions were not validated: --output=%q should have been rejected", o.Output)
}
return nil
return serverErr
}

View File

@ -40,6 +40,7 @@ filegroup(
"//pkg/util/pki:all-srcs",
"//pkg/util/predicate:all-srcs",
"//pkg/util/profiling:all-srcs",
"//pkg/util/versionchecker:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],

View File

@ -0,0 +1,67 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"fromcrd.go",
"fromlabels.go",
"fromservice.go",
"versionchecker.go",
],
importpath = "github.com/jetstack/cert-manager/pkg/util/versionchecker",
visibility = ["//visibility:public"],
deps = [
"@com_github_pkg_errors//:go_default_library",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/v1:go_default_library",
"@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/v1beta1:go_default_library",
"@io_k8s_apimachinery//pkg/api/errors:go_default_library",
"@io_k8s_apimachinery//pkg/labels:go_default_library",
"@io_k8s_apimachinery//pkg/runtime:go_default_library",
"@io_k8s_client_go//rest:go_default_library",
"@io_k8s_sigs_controller_runtime//pkg/client:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"getpodfromtemplate_test.go",
"versionchecker_test.go",
],
embed = [":go_default_library"],
embedsrcs = ["//pkg/util/versionchecker/testdata:test_manifests.tar"], # keep
deps = [
"//pkg/util:go_default_library",
"@io_k8s_api//apps/v1:go_default_library",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/v1:go_default_library",
"@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/v1beta1:go_default_library",
"@io_k8s_apimachinery//pkg/api/meta:go_default_library",
"@io_k8s_apimachinery//pkg/api/validation:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured:go_default_library",
"@io_k8s_apimachinery//pkg/labels:go_default_library",
"@io_k8s_apimachinery//pkg/runtime:go_default_library",
"@io_k8s_cli_runtime//pkg/resource:go_default_library",
"@io_k8s_client_go//kubernetes/scheme:go_default_library",
"@io_k8s_sigs_controller_runtime//pkg/client/fake:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/util/versionchecker/testdata:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,91 @@
/*
Copyright 2021 The cert-manager 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 versionchecker
import (
"context"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func (o *versionChecker) extractVersionFromCrd(ctx context.Context, crdName string) error {
crdKey := client.ObjectKey{Name: crdName}
objv1 := &apiextensionsv1.CustomResourceDefinition{}
err := o.client.Get(ctx, crdKey, objv1)
if err == nil {
if label := extractVersionFromLabels(objv1.Labels); label != "" {
o.versionSources["crdLabelVersion"] = label
}
return o.extractVersionFromCrdv1(ctx, objv1)
}
// If error differs from not found, don't continue and return error
if !apierrors.IsNotFound(err) {
return err
}
objv1beta1 := &apiextensionsv1beta1.CustomResourceDefinition{}
err = o.client.Get(ctx, crdKey, objv1beta1)
if err == nil {
if label := extractVersionFromLabels(objv1beta1.Labels); label != "" {
o.versionSources["crdLabelVersion"] = label
}
return o.extractVersionFromCrdv1beta1(ctx, objv1beta1)
}
// If error differs from not found, don't continue and return error
if !apierrors.IsNotFound(err) {
return err
}
return ErrCertManagerCRDsNotFound
}
func (o *versionChecker) extractVersionFromCrdv1(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition) error {
if (crd.Spec.Conversion == nil) ||
(crd.Spec.Conversion.Webhook == nil) ||
(crd.Spec.Conversion.Webhook.ClientConfig == nil) ||
(crd.Spec.Conversion.Webhook.ClientConfig.Service == nil) {
return nil
}
return o.extractVersionFromService(
ctx,
crd.Spec.Conversion.Webhook.ClientConfig.Service.Namespace,
crd.Spec.Conversion.Webhook.ClientConfig.Service.Name,
)
}
func (o *versionChecker) extractVersionFromCrdv1beta1(ctx context.Context, crd *apiextensionsv1beta1.CustomResourceDefinition) error {
if (crd.Spec.Conversion == nil) ||
(crd.Spec.Conversion.WebhookClientConfig == nil) ||
(crd.Spec.Conversion.WebhookClientConfig.Service == nil) {
return nil
}
return o.extractVersionFromService(
ctx,
crd.Spec.Conversion.WebhookClientConfig.Service.Namespace,
crd.Spec.Conversion.WebhookClientConfig.Service.Name,
)
}

View File

@ -0,0 +1,45 @@
/*
Copyright 2021 The cert-manager 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 versionchecker
import (
"regexp"
)
var helmChartVersion = regexp.MustCompile(`-(v(?:\d+)\.(?:\d+)\.(?:\d+)(?:.*))$`)
func extractVersionFromLabels(crdLabels map[string]string) string {
if version, ok := crdLabels["app.kubernetes.io/version"]; ok {
return version
}
if chartName, ok := crdLabels["helm.sh/chart"]; ok {
version := helmChartVersion.FindStringSubmatch(chartName)
if len(version) == 2 {
return version[1]
}
}
if chartName, ok := crdLabels["chart"]; ok {
version := helmChartVersion.FindStringSubmatch(chartName)
if len(version) == 2 {
return version[1]
}
}
return ""
}

View File

@ -0,0 +1,70 @@
/*
Copyright 2021 The cert-manager 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 versionchecker
import (
"context"
"regexp"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var imageVersion = regexp.MustCompile(`^quay.io/jetstack/cert-manager-webhook:(v(?:\d+)\.(?:\d+)\.(?:\d+)(?:.*))$`)
func (o *versionChecker) extractVersionFromService(
ctx context.Context,
namespace string,
serviceName string,
) error {
service := &corev1.Service{}
serviceKey := client.ObjectKey{Namespace: namespace, Name: serviceName}
err := o.client.Get(ctx, serviceKey, service)
if err != nil {
return err
}
if label := extractVersionFromLabels(service.Labels); label != "" {
o.versionSources["webhookServiceLabelVersion"] = label
}
listOptions := client.MatchingLabelsSelector{
Selector: labels.Set(service.Spec.Selector).AsSelector(),
}
pods := &corev1.PodList{}
err = o.client.List(ctx, pods, listOptions)
if err != nil {
return err
}
for _, pod := range pods.Items {
if label := extractVersionFromLabels(pod.Labels); label != "" {
o.versionSources["webhookPodLabelVersion"] = label
}
for _, container := range pod.Spec.Containers {
version := imageVersion.FindStringSubmatch(container.Image)
if len(version) == 2 {
o.versionSources["webhookPodImageVersion"] = version[1]
return nil
}
}
}
return nil
}

View File

@ -0,0 +1,91 @@
/*
Copyright 2021 The cert-manager 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 versionchecker
import (
"fmt"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
cmutil "github.com/jetstack/cert-manager/pkg/util"
)
// Based on https://github.com/kubernetes/kubernetes/blob/ca643a4d1f7bfe34773c74f79527be4afd95bf39/pkg/controller/controller_utils.go#L542
var validatePodName = validation.NameIsDNSSubdomain
func getPodFromTemplate(template *v1.PodTemplateSpec, parentObject runtime.Object, controllerRef *metav1.OwnerReference) (*v1.Pod, error) {
desiredLabels := getPodsLabelSet(template)
desiredFinalizers := getPodsFinalizers(template)
desiredAnnotations := getPodsAnnotationSet(template)
accessor, err := meta.Accessor(parentObject)
if err != nil {
return nil, fmt.Errorf("parentObject does not have ObjectMeta, %v", err)
}
prefix := getPodsPrefix(accessor.GetName())
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Labels: desiredLabels,
Annotations: desiredAnnotations,
GenerateName: prefix,
Name: prefix + cmutil.RandStringRunes(5),
Finalizers: desiredFinalizers,
},
}
if controllerRef != nil {
pod.OwnerReferences = append(pod.OwnerReferences, *controllerRef)
}
pod.Spec = *template.Spec.DeepCopy()
return pod, nil
}
func getPodsLabelSet(template *v1.PodTemplateSpec) labels.Set {
desiredLabels := make(labels.Set)
for k, v := range template.Labels {
desiredLabels[k] = v
}
return desiredLabels
}
func getPodsFinalizers(template *v1.PodTemplateSpec) []string {
desiredFinalizers := make([]string, len(template.Finalizers))
copy(desiredFinalizers, template.Finalizers)
return desiredFinalizers
}
func getPodsAnnotationSet(template *v1.PodTemplateSpec) labels.Set {
desiredAnnotations := make(labels.Set)
for k, v := range template.Annotations {
desiredAnnotations[k] = v
}
return desiredAnnotations
}
func getPodsPrefix(controllerName string) string {
// use the dash (if the name isn't too long) to make the pod name a bit prettier
prefix := fmt.Sprintf("%s-", controllerName)
if len(validatePodName(prefix, true)) != 0 {
prefix = controllerName
}
return prefix
}

View File

@ -0,0 +1 @@
*.tar

View File

@ -0,0 +1,41 @@
# Clone empty version of cert-manager repo and list all tags
genrule(
name = "git_tags",
outs = [":git_tags.txt"],
cmd = "git ls-remote -t --refs https://github.com/jetstack/cert-manager.git | awk '{print $$2;}' | sed 's/refs\\/tags\\///' | sed -n '/v1.0.0/,$$p' > $@",
)
genrule(
name = "test_manifests",
srcs = [
"//:version",
"//deploy/manifests:cert-manager.yaml",
":git_tags.txt",
],
outs = ["test_manifests.tar"],
cmd = """
$(location fetch.sh) \
$(location //:version) \
$(location //deploy/manifests:cert-manager.yaml) \
$(location :git_tags.txt) \
$(location test_manifests.tar)
""",
tools = [
"fetch.sh",
],
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

72
pkg/util/versionchecker/testdata/fetch.sh vendored Executable file
View File

@ -0,0 +1,72 @@
#!/bin/bash
# Copyright 2020 The cert-manager 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.
set -o errexit
set -o nounset
set -o pipefail
SCRIPT_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )"
if [[ $# -eq 4 ]]; then # Running inside bazel
echo "Updating generated clients..." >&2
elif ! command -v bazel &>/dev/null; then
echo "Install bazel at https://bazel.build" >&2
exit 1
else
(
set -o xtrace
bazel build //pkg/util/versionchecker/testdata:test_manifests.tar
cp -f "$(bazel info bazel-bin)/pkg/util/versionchecker/testdata/test_manifests.tar" "$SCRIPT_ROOT"
)
exit 0
fi
CURRENT_VERSION=$(cat "$1") # $(location //:version)
current_version_yaml=$(realpath "$2") # $(location //deploy/manifests:cert-manager.yaml)
tags=$(cat "$3") # $(location :git_tags.txt)
test_manifests_tar=$(realpath "$4") # $(location test_manifests.tar)
shift 4
# copy current version's manifest to current folder (will get included in tar)
cp "$current_version_yaml" "$CURRENT_VERSION.yaml"
manifest_urls=""
for tag in $tags
do
# The "v1.2.0-alpha.1" manifest contains duplicate CRD resources
# (2 CRD resources with the same name); don't download this manifest
# as it will cause the test to fail when adding the CRD resources
# to the fake client
if [[ $tag == "v1.2.0-alpha.1" ]]; then
continue
fi
manifest_urls+=",$tag"
done
# remove leading "," in string
manifest_urls=${manifest_urls#","}
# download all manifests
# --compressed: try gzip compressed download
# -s: don't show progress bar
# -f: fail if non success code
# -L: follow redirects
# -o: output to "#1.yaml"
curl --compressed -sfLo "#1.yaml" "https://github.com/jetstack/cert-manager/releases/download/{$manifest_urls}/cert-manager.yaml"
tar -cvf "$test_manifests_tar" *.yaml

View File

@ -0,0 +1,156 @@
/*
Copyright 2021 The cert-manager 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 versionchecker
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
errors "github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const certificatesCertManagerCrdName = "certificates.cert-manager.io"
const certificatesCertManagerOldCrdName = "certificates.certmanager.k8s.io"
var certManagerLabelSelector = map[string]string{
"app.kubernetes.io/instance": "cert-manager",
}
var certManagerOldLabelSelector = map[string]string{
"release": "cert-manager",
}
var (
ErrCertManagerCRDsNotFound = errors.New("the cert-manager CRDs are not yet installed on the Kubernetes API server")
ErrVersionNotDetected = errors.New("could not detect the cert-manager version")
ErrMultipleVersionsDetected = errors.New("detect multiple different cert-manager versions")
)
type Version struct {
// If all found versions are the same,
// this field will contain that version
Detected string `json:"detected,omitempty"`
Sources map[string]string `json:"sources"`
}
func shouldReturn(err error) bool {
return (err == nil) || (!errors.Is(err, ErrVersionNotDetected))
}
// Interface is used to check what cert-manager version is installed
type Interface interface {
Version(context.Context) (*Version, error)
}
type versionChecker struct {
client client.Client
versionSources map[string]string
}
// New returns a cert-manager version checker
func New(restcfg *rest.Config, scheme *runtime.Scheme) (Interface, error) {
if err := corev1.AddToScheme(scheme); err != nil {
return nil, err
}
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
return nil, err
}
if err := apiextensionsv1beta1.AddToScheme(scheme); err != nil {
return nil, err
}
cl, err := client.New(restcfg, client.Options{
Scheme: scheme,
})
if err != nil {
return nil, err
}
return &versionChecker{
client: cl,
versionSources: map[string]string{},
}, nil
}
// Determine the installed cert-manager version. First, we start by looking for
// the "certificates.cert-manager.io" CRD and try to extract the version from that
// resource's labels. Then, if it uses a webhook, that webhook service resource's
// labels are checked for a label. Lastly the pods linked to the webhook its labels
// are checked and the image tag is used to determine the version.
// If no "certificates.cert-manager.io" CRD is found, the older
// "certificates.certmanager.k8s.io" CRD is tried too.
func (o *versionChecker) Version(ctx context.Context) (*Version, error) {
// Use the "certificates.cert-manager.io" CRD as a starting point
err := o.extractVersionFromCrd(ctx, certificatesCertManagerCrdName)
if err != nil && errors.Is(err, ErrCertManagerCRDsNotFound) {
// Retry using the old CRD name "certificates.certmanager.k8s.io" as
// a starting point and overwrite ErrCertManagerCRDsNotFound error
err = o.extractVersionFromCrd(ctx, certificatesCertManagerOldCrdName)
}
// From the found versions, now determine if we have found any/
// if they are all the same version
version, detectionError := o.determineVersion()
if err != nil && detectionError != nil {
// There was an error while determining the version (which is probably
// caused by a bad setup/ permission or networking issue) and there also
// was an error while trying to reduce the found versions to 1 version
// Display both.
err = fmt.Errorf("%v: %v", detectionError, err)
} else if detectionError != nil {
// An error occured while trying to reduce the found versions to 1 version
err = detectionError
}
return version, err
}
// Try to determine the version of the cert-manager install based on all found
// versions. The function tries to reduce the found versions to 1 correct version.
// An error is returned if no sources were found or if multiple different versions
// were found.
func (o *versionChecker) determineVersion() (*Version, error) {
if len(o.versionSources) == 0 {
return nil, ErrVersionNotDetected
}
var detectedVersion string
for _, version := range o.versionSources {
if detectedVersion != "" && version != detectedVersion {
// We have found a conflicting version
return &Version{
Sources: o.versionSources,
}, ErrMultipleVersionsDetected
}
detectedVersion = version
}
return &Version{
Detected: detectedVersion,
Sources: o.versionSources,
}, nil
}

View File

@ -0,0 +1,177 @@
/*
Copyright 2021 The cert-manager 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 versionchecker
import (
"archive/tar"
"context"
"embed"
"errors"
"io"
"strings"
"testing"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"
kubernetesscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
//go:embed testdata/test_manifests.tar
var testFiles embed.FS
func loadManifests() (io.Reader, error, func() (string, error), func()) {
data, err := testFiles.Open("testdata/test_manifests.tar")
if err != nil {
return nil, err, nil, nil
}
fileReader := tar.NewReader(data)
return fileReader, nil, func() (string, error) {
header, err := fileReader.Next()
if err != nil {
return "", err
}
return strings.TrimSuffix(header.Name, ".yaml"), nil
}, func() {
if err := data.Close(); err != nil {
panic(err)
}
}
}
func manifestToObject(manifest io.Reader) ([]runtime.Object, error) {
obj, err := resource.
NewLocalBuilder().
Flatten().
Unstructured().
Stream(manifest, "").
Do().
Object()
if err != nil {
return nil, err
}
list, ok := obj.(*corev1.List)
if !ok {
return nil, errors.New("Could not get list")
}
return transformObjects(list.Items)
}
func transformObjects(objects []runtime.RawExtension) ([]runtime.Object, error) {
transformedObjects := []runtime.Object{}
for _, resource := range objects {
var err error
gvk := resource.Object.GetObjectKind().GroupVersionKind()
// Create a pod for a Deployment resource
if gvk.Group == "apps" && gvk.Version == "v1" && gvk.Kind == "Deployment" {
unstr := resource.Object.(*unstructured.Unstructured)
var deployment appsv1.Deployment
err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstr.Object, &deployment)
if err != nil {
return nil, err
}
pod, err := getPodFromTemplate(&deployment.Spec.Template, resource.Object, nil)
if err != nil {
return nil, err
}
transformedObjects = append(transformedObjects, pod)
}
transformedObjects = append(transformedObjects, resource.Object)
}
return transformedObjects, nil
}
func setupFakeVersionChecker(manifest io.Reader) (*versionChecker, error) {
scheme := runtime.NewScheme()
if err := kubernetesscheme.AddToScheme(scheme); err != nil {
return nil, err
}
if err := appsv1.AddToScheme(scheme); err != nil {
return nil, err
}
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
return nil, err
}
if err := apiextensionsv1beta1.AddToScheme(scheme); err != nil {
return nil, err
}
objs, err := manifestToObject(manifest)
if err != nil {
return nil, err
}
cl := fake.
NewClientBuilder().
WithScheme(scheme).
WithRuntimeObjects(objs...).
Build()
return &versionChecker{
client: cl,
versionSources: map[string]string{},
}, nil
}
func TestVersionChecker(t *testing.T) {
f, err, next, close := loadManifests()
if err != nil {
t.Fatal(err)
}
defer close()
for {
version, err := next()
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
t.Run(version, func(t *testing.T) {
checker, err := setupFakeVersionChecker(f)
if err != nil {
t.Error(err)
}
versionGuess, err := checker.Version(context.TODO())
if err != nil {
t.Error(err)
}
if version != versionGuess.Detected {
t.Fatalf("wrong -> expected: %s vs detected: %s", version, versionGuess)
}
})
}
}