From 6545064fcf2ee47d586cc4b09c540ebe14acafc0 Mon Sep 17 00:00:00 2001 From: Inteon <42113979+inteon@users.noreply.github.com> Date: Tue, 27 Jul 2021 18:02:21 +0200 Subject: [PATCH 1/4] align flags and behaviour to 'kubectl version' Signed-off-by: Inteon <42113979+inteon@users.noreply.github.com> --- cmd/ctl/cmd/cmd.go | 2 +- cmd/ctl/pkg/version/BUILD.bazel | 2 + cmd/ctl/pkg/version/version.go | 84 +++++-- pkg/util/BUILD.bazel | 1 + pkg/util/version.go | 4 + pkg/util/versionchecker/BUILD.bazel | 95 ++++++++ pkg/util/versionchecker/fromcrd.go | 91 ++++++++ pkg/util/versionchecker/fromlabels.go | 45 ++++ pkg/util/versionchecker/fromservice.go | 69 ++++++ pkg/util/versionchecker/getpodfromtemplate.go | 91 ++++++++ pkg/util/versionchecker/versionchecker.go | 104 +++++++++ .../versionchecker/versionchecker_test.go | 212 ++++++++++++++++++ 12 files changed, 785 insertions(+), 15 deletions(-) create mode 100644 pkg/util/versionchecker/BUILD.bazel create mode 100644 pkg/util/versionchecker/fromcrd.go create mode 100644 pkg/util/versionchecker/fromlabels.go create mode 100644 pkg/util/versionchecker/fromservice.go create mode 100644 pkg/util/versionchecker/getpodfromtemplate.go create mode 100644 pkg/util/versionchecker/versionchecker.go create mode 100644 pkg/util/versionchecker/versionchecker_test.go diff --git a/cmd/ctl/cmd/cmd.go b/cmd/ctl/cmd/cmd.go index 9a45adc3e..67a753231 100644 --- a/cmd/ctl/cmd/cmd.go +++ b/cmd/ctl/cmd/cmd.go @@ -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)) diff --git a/cmd/ctl/pkg/version/BUILD.bazel b/cmd/ctl/pkg/version/BUILD.bazel index 904d6ea04..c393f172c 100644 --- a/cmd/ctl/pkg/version/BUILD.bazel +++ b/cmd/ctl/pkg/version/BUILD.bazel @@ -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", ], ) diff --git a/cmd/ctl/pkg/version/version.go b/cmd/ctl/pkg/version/version.go index ae5d64ae3..7c158d830 100644 --- a/cmd/ctl/pkg/version/version.go +++ b/cmd/ctl/pkg/version/version.go @@ -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 *util.ServerVersion `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 } @@ -50,22 +63,23 @@ func NewOptions(ioStreams genericclioptions.IOStreams) *Options { } // 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: "Print the cert-manager kubectl plugin version and the deployed cert-manager version", 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 +93,58 @@ 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("Error: cannot create the REST config: %v", err) + } + + o.VersionChecker, err = versionchecker.New(restConfig, scheme.Scheme) + if err != nil { + return fmt.Errorf("Error: %v", 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 *util.ServerVersion + serverErr error + versionInfo Version + ) + + clientVersion := util.VersionInfo() + versionInfo.ClientVersion = &clientVersion + + if !o.ClientOnly { + var version string + version, serverErr = o.VersionChecker.Version(ctx) + if serverErr == nil { + serverVersion = &util.ServerVersion{ + GitVersion: version, + } + 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.GitVersion) + } } 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 +164,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 } diff --git a/pkg/util/BUILD.bazel b/pkg/util/BUILD.bazel index c4b848a82..cfe4110b7 100644 --- a/pkg/util/BUILD.bazel +++ b/pkg/util/BUILD.bazel @@ -42,6 +42,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"], diff --git a/pkg/util/version.go b/pkg/util/version.go index c0bd1f8d9..f21246ca3 100644 --- a/pkg/util/version.go +++ b/pkg/util/version.go @@ -30,6 +30,10 @@ type Version struct { Platform string `json:"platform"` } +type ServerVersion struct { + GitVersion string `json:"gitVersion"` +} + // This variable block holds information used to build up the version string var ( AppGitState = "" diff --git a/pkg/util/versionchecker/BUILD.bazel b/pkg/util/versionchecker/BUILD.bazel new file mode 100644 index 000000000..be3257830 --- /dev/null +++ b/pkg/util/versionchecker/BUILD.bazel @@ -0,0 +1,95 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +# 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-alpha.0/,$$p' > $@", +) + +genrule( + name = "test_manifests", + srcs = [":git_tags.txt"], + outs = ["test_manifests.tar"], + cmd = """ +for tag in $$(cat $(location :git_tags.txt)) +do + # The "v1.2.0-alpha.1" manifest contains duplicate crds, skip for tests + if [[ $$tag == "v1.2.0-alpha.1" ]]; then + continue + fi + + { + HTTP_CODE=$$(curl --compressed -sLo "$$tag.yaml" --write-out "%{http_code}" https://github.com/jetstack/cert-manager/releases/download/$$tag/cert-manager.yaml) + if [[ $${HTTP_CODE} -lt 200 || $${HTTP_CODE} -gt 299 ]]; then + (mv "$$tag.yaml" "$$tag.notfound") + fi + } & +done +wait +tar -cvf $@ *.yaml + """, + visibility = ["//visibility:public"], +) + +go_library( + name = "go_default_library", + srcs = [ + "fromcrd.go", + "fromlabels.go", + "fromservice.go", + "getpodfromtemplate.go", + "versionchecker.go", + ], + importpath = "github.com/jetstack/cert-manager/pkg/util/versionchecker", + visibility = ["//visibility:public"], + deps = [ + "//pkg/util:go_default_library", + "@com_github_pkg_errors//:go_default_library", + "@io_k8s_api//core/v1:go_default_library", + "@io_k8s_api//rbac/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/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/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 = ["versionchecker_test.go"], + embed = [":go_default_library"], + embedsrcs = ["test_manifests.tar"], + deps = [ + "@io_k8s_api//apps/v1:go_default_library", + "@io_k8s_api//core/v1:go_default_library", + "@io_k8s_api//rbac/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/apis/meta/v1/unstructured: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"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/util/versionchecker/fromcrd.go b/pkg/util/versionchecker/fromcrd.go new file mode 100644 index 000000000..549819b3b --- /dev/null +++ b/pkg/util/versionchecker/fromcrd.go @@ -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) (string, error) { + crdKey := client.ObjectKey{Name: crdName} + + objv1 := &apiextensionsv1.CustomResourceDefinition{} + err := o.client.Get(ctx, crdKey, objv1) + if err == nil { + if version, err := extractVersionFromLabels(objv1.Labels); shouldReturn(err) { + return version, err + } + + 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 version, err := extractVersionFromLabels(objv1beta1.Labels); shouldReturn(err) { + return version, err + } + + 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) (string, 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 "", ErrVersionNotDetected + } + + 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) (string, error) { + if (crd.Spec.Conversion == nil) || + (crd.Spec.Conversion.WebhookClientConfig == nil) || + (crd.Spec.Conversion.WebhookClientConfig.Service == nil) { + return "", ErrVersionNotDetected + } + + return o.extractVersionFromService( + ctx, + crd.Spec.Conversion.WebhookClientConfig.Service.Namespace, + crd.Spec.Conversion.WebhookClientConfig.Service.Name, + ) +} diff --git a/pkg/util/versionchecker/fromlabels.go b/pkg/util/versionchecker/fromlabels.go new file mode 100644 index 000000000..dafad1fcc --- /dev/null +++ b/pkg/util/versionchecker/fromlabels.go @@ -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, error) { + if version, ok := crdLabels["app.kubernetes.io/version"]; ok { + return version, nil + } + + if chartName, ok := crdLabels["helm.sh/chart"]; ok { + version := helmChartVersion.FindStringSubmatch(chartName) + if len(version) == 2 { + return version[1], nil + } + } + + if chartName, ok := crdLabels["chart"]; ok { + version := helmChartVersion.FindStringSubmatch(chartName) + if len(version) == 2 { + return version[1], nil + } + } + + return "", ErrVersionNotDetected +} diff --git a/pkg/util/versionchecker/fromservice.go b/pkg/util/versionchecker/fromservice.go new file mode 100644 index 000000000..03a8003fc --- /dev/null +++ b/pkg/util/versionchecker/fromservice.go @@ -0,0 +1,69 @@ +/* +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, +) (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 version, err := extractVersionFromLabels(service.Labels); shouldReturn(err) { + return version, err + } + + 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 version, err := extractVersionFromLabels(pod.Labels); shouldReturn(err) { + return version, err + } + + for _, container := range pod.Spec.Containers { + version := imageVersion.FindStringSubmatch(container.Image) + if len(version) == 2 { + return version[1], nil + } + } + } + + return "", ErrVersionNotDetected +} diff --git a/pkg/util/versionchecker/getpodfromtemplate.go b/pkg/util/versionchecker/getpodfromtemplate.go new file mode 100644 index 000000000..db9caa61e --- /dev/null +++ b/pkg/util/versionchecker/getpodfromtemplate.go @@ -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 +} diff --git a/pkg/util/versionchecker/versionchecker.go b/pkg/util/versionchecker/versionchecker.go new file mode 100644 index 000000000..1f03eacf1 --- /dev/null +++ b/pkg/util/versionchecker/versionchecker.go @@ -0,0 +1,104 @@ +/* +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" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + rbacv1beta1 "k8s.io/api/rbac/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") +) + +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) (string, error) +} + +type versionChecker struct { + client client.Client +} + +// 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 + } + if err := rbacv1.AddToScheme(scheme); err != nil { + return nil, err + } + if err := rbacv1beta1.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, + }, nil +} + +func (o *versionChecker) Version(ctx context.Context) (string, error) { + version, err := o.extractVersionFromCrd(ctx, certificatesCertManagerCrdName) + if (err == nil) || (!errors.Is(err, ErrVersionNotDetected) && !errors.Is(err, ErrCertManagerCRDsNotFound)) { + return version, err + } + + if errors.Is(err, ErrCertManagerCRDsNotFound) { + if version, err = o.extractVersionFromCrd(ctx, certificatesCertManagerOldCrdName); shouldReturn(err) { + return version, err + } + } + + return "", err +} diff --git a/pkg/util/versionchecker/versionchecker_test.go b/pkg/util/versionchecker/versionchecker_test.go new file mode 100644 index 000000000..0c26f8578 --- /dev/null +++ b/pkg/util/versionchecker/versionchecker_test.go @@ -0,0 +1,212 @@ +/* +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" + rbacv1 "k8s.io/api/rbac/v1" + rbacv1beta1 "k8s.io/api/rbac/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 test_manifests.tar +var testFiles embed.FS + +func loadManifests() (io.Reader, error, func() (string, error), func()) { + data, err := testFiles.Open("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() + + // Cast ClusterRole from unstructured to rbacv1 ClusterRole + if gvk.Group == "rbac.authorization.k8s.io" && gvk.Version == "v1" && gvk.Kind == "ClusterRole" { + unstr := resource.Object.(*unstructured.Unstructured) + + var clusterRole rbacv1.ClusterRole + err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstr.Object, &clusterRole) + if err != nil { + return nil, err + } + + transformedObjects = append(transformedObjects, &clusterRole) + continue + } + + // Cast ClusterRole from unstructured to rbacv1beta1 ClusterRole + if gvk.Group == "rbac.authorization.k8s.io" && gvk.Version == "v1beta1" && gvk.Kind == "ClusterRole" { + unstr := resource.Object.(*unstructured.Unstructured) + + var clusterRole rbacv1beta1.ClusterRole + err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstr.Object, &clusterRole) + if err != nil { + return nil, err + } + + transformedObjects = append(transformedObjects, &clusterRole) + continue + } + + // 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 + } + if err := rbacv1.AddToScheme(scheme); err != nil { + return nil, err + } + if err := rbacv1beta1.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, + }, 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 { + t.Fatalf("wrong -> expected: %s vs detected: %s", version, versionGuess) + } + }) + } +} From fa36a5bc87027ec7b6d9104d201ca6fe87c953d1 Mon Sep 17 00:00:00 2001 From: Inteon <42113979+inteon@users.noreply.github.com> Date: Tue, 27 Jul 2021 18:11:24 +0200 Subject: [PATCH 2/4] add version check for current version Signed-off-by: Inteon <42113979+inteon@users.noreply.github.com> --- pkg/util/versionchecker/BUILD.bazel | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/util/versionchecker/BUILD.bazel b/pkg/util/versionchecker/BUILD.bazel index be3257830..03713bfc3 100644 --- a/pkg/util/versionchecker/BUILD.bazel +++ b/pkg/util/versionchecker/BUILD.bazel @@ -9,9 +9,15 @@ genrule( genrule( name = "test_manifests", - srcs = [":git_tags.txt"], + srcs = [ + ":git_tags.txt", + "//deploy/manifests:cert-manager.yaml", + "//:version", + ], outs = ["test_manifests.tar"], cmd = """ +CURRENT_VERSION=$$(cat $(location //:version)) +cp $(location //deploy/manifests:cert-manager.yaml) "$$CURRENT_VERSION.yaml" for tag in $$(cat $(location :git_tags.txt)) do # The "v1.2.0-alpha.1" manifest contains duplicate crds, skip for tests From 644db10b92f58418f40d2e934408e47728911090 Mon Sep 17 00:00:00 2001 From: Inteon <42113979+inteon@users.noreply.github.com> Date: Thu, 29 Jul 2021 15:06:31 +0200 Subject: [PATCH 3/4] don't early-stop, instead return all versions Signed-off-by: Inteon <42113979+inteon@users.noreply.github.com> --- cmd/ctl/pkg/version/version.go | 20 +++--- pkg/util/version.go | 4 -- pkg/util/versionchecker/BUILD.bazel | 15 +++-- pkg/util/versionchecker/fromcrd.go | 24 +++---- pkg/util/versionchecker/fromlabels.go | 10 +-- pkg/util/versionchecker/fromservice.go | 19 +++--- ...template.go => getpodfromtemplate_test.go} | 6 +- pkg/util/versionchecker/versionchecker.go | 63 ++++++++++++++----- .../versionchecker/versionchecker_test.go | 7 ++- 9 files changed, 98 insertions(+), 70 deletions(-) rename pkg/util/versionchecker/{getpodfromtemplate.go => getpodfromtemplate_test.go} (94%) diff --git a/cmd/ctl/pkg/version/version.go b/cmd/ctl/pkg/version/version.go index 7c158d830..6be74b890 100644 --- a/cmd/ctl/pkg/version/version.go +++ b/cmd/ctl/pkg/version/version.go @@ -34,8 +34,8 @@ import ( // Version is a struct for version information type Version struct { - ClientVersion *util.Version `json:"clientVersion,omitempty"` - ServerVersion *util.ServerVersion `json:"serverVersion,omitempty"` + ClientVersion *util.Version `json:"clientVersion,omitempty"` + ServerVersion *versionchecker.Version `json:"serverVersion,omitempty"` } // Options is a struct to support version command @@ -114,7 +114,7 @@ func (o *Options) Complete(factory cmdutil.Factory) error { // Run executes version command func (o *Options) Run(ctx context.Context) error { var ( - serverVersion *util.ServerVersion + serverVersion *versionchecker.Version serverErr error versionInfo Version ) @@ -123,14 +123,8 @@ func (o *Options) Run(ctx context.Context) error { versionInfo.ClientVersion = &clientVersion if !o.ClientOnly { - var version string - version, serverErr = o.VersionChecker.Version(ctx) - if serverErr == nil { - serverVersion = &util.ServerVersion{ - GitVersion: version, - } - versionInfo.ServerVersion = serverVersion - } + serverVersion, serverErr = o.VersionChecker.Version(ctx) + versionInfo.ServerVersion = serverVersion } switch o.Output { @@ -138,12 +132,12 @@ func (o *Options) Run(ctx context.Context) error { if o.Short { fmt.Fprintf(o.Out, "Client Version: %s\n", clientVersion.GitVersion) if serverVersion != nil { - fmt.Fprintf(o.Out, "Server Version: %s\n", serverVersion.GitVersion) + fmt.Fprintf(o.Out, "Server Version: %s\n", serverVersion.Detected) } } else { 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)) + fmt.Fprintf(o.Out, "Server Version: %s\n", fmt.Sprintf("%#v", serverVersion)) } } case "yaml": diff --git a/pkg/util/version.go b/pkg/util/version.go index f21246ca3..c0bd1f8d9 100644 --- a/pkg/util/version.go +++ b/pkg/util/version.go @@ -30,10 +30,6 @@ type Version struct { Platform string `json:"platform"` } -type ServerVersion struct { - GitVersion string `json:"gitVersion"` -} - // This variable block holds information used to build up the version string var ( AppGitState = "" diff --git a/pkg/util/versionchecker/BUILD.bazel b/pkg/util/versionchecker/BUILD.bazel index 03713bfc3..c0f7bbafa 100644 --- a/pkg/util/versionchecker/BUILD.bazel +++ b/pkg/util/versionchecker/BUILD.bazel @@ -44,22 +44,17 @@ go_library( "fromcrd.go", "fromlabels.go", "fromservice.go", - "getpodfromtemplate.go", "versionchecker.go", ], importpath = "github.com/jetstack/cert-manager/pkg/util/versionchecker", visibility = ["//visibility:public"], deps = [ - "//pkg/util:go_default_library", "@com_github_pkg_errors//:go_default_library", "@io_k8s_api//core/v1:go_default_library", "@io_k8s_api//rbac/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/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/labels:go_default_library", "@io_k8s_apimachinery//pkg/runtime:go_default_library", "@io_k8s_client_go//rest:go_default_library", @@ -69,16 +64,24 @@ go_library( go_test( name = "go_default_test", - srcs = ["versionchecker_test.go"], + srcs = [ + "getpodfromtemplate_test.go", + "versionchecker_test.go", + ], embed = [":go_default_library"], embedsrcs = ["test_manifests.tar"], deps = [ + "//pkg/util:go_default_library", "@io_k8s_api//apps/v1:go_default_library", "@io_k8s_api//core/v1:go_default_library", "@io_k8s_api//rbac/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", diff --git a/pkg/util/versionchecker/fromcrd.go b/pkg/util/versionchecker/fromcrd.go index 549819b3b..d45069e8e 100644 --- a/pkg/util/versionchecker/fromcrd.go +++ b/pkg/util/versionchecker/fromcrd.go @@ -25,14 +25,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func (o *versionChecker) extractVersionFromCrd(ctx context.Context, crdName string) (string, error) { +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 version, err := extractVersionFromLabels(objv1.Labels); shouldReturn(err) { - return version, err + if label := extractVersionFromLabels(objv1.Labels); label != "" { + o.versionSources["crdLabelVersion"] = label } return o.extractVersionFromCrdv1(ctx, objv1) @@ -40,14 +40,14 @@ func (o *versionChecker) extractVersionFromCrd(ctx context.Context, crdName stri // If error differs from not found, don't continue and return error if !apierrors.IsNotFound(err) { - return "", err + return err } objv1beta1 := &apiextensionsv1beta1.CustomResourceDefinition{} err = o.client.Get(ctx, crdKey, objv1beta1) if err == nil { - if version, err := extractVersionFromLabels(objv1beta1.Labels); shouldReturn(err) { - return version, err + if label := extractVersionFromLabels(objv1beta1.Labels); label != "" { + o.versionSources["crdLabelVersion"] = label } return o.extractVersionFromCrdv1beta1(ctx, objv1beta1) @@ -55,18 +55,18 @@ func (o *versionChecker) extractVersionFromCrd(ctx context.Context, crdName stri // If error differs from not found, don't continue and return error if !apierrors.IsNotFound(err) { - return "", err + return err } - return "", ErrCertManagerCRDsNotFound + return ErrCertManagerCRDsNotFound } -func (o *versionChecker) extractVersionFromCrdv1(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition) (string, error) { +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 "", ErrVersionNotDetected + return nil } return o.extractVersionFromService( @@ -76,11 +76,11 @@ func (o *versionChecker) extractVersionFromCrdv1(ctx context.Context, crd *apiex ) } -func (o *versionChecker) extractVersionFromCrdv1beta1(ctx context.Context, crd *apiextensionsv1beta1.CustomResourceDefinition) (string, error) { +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 "", ErrVersionNotDetected + return nil } return o.extractVersionFromService( diff --git a/pkg/util/versionchecker/fromlabels.go b/pkg/util/versionchecker/fromlabels.go index dafad1fcc..24337e4ec 100644 --- a/pkg/util/versionchecker/fromlabels.go +++ b/pkg/util/versionchecker/fromlabels.go @@ -22,24 +22,24 @@ import ( var helmChartVersion = regexp.MustCompile(`-(v(?:\d+)\.(?:\d+)\.(?:\d+)(?:.*))$`) -func extractVersionFromLabels(crdLabels map[string]string) (string, error) { +func extractVersionFromLabels(crdLabels map[string]string) string { if version, ok := crdLabels["app.kubernetes.io/version"]; ok { - return version, nil + return version } if chartName, ok := crdLabels["helm.sh/chart"]; ok { version := helmChartVersion.FindStringSubmatch(chartName) if len(version) == 2 { - return version[1], nil + return version[1] } } if chartName, ok := crdLabels["chart"]; ok { version := helmChartVersion.FindStringSubmatch(chartName) if len(version) == 2 { - return version[1], nil + return version[1] } } - return "", ErrVersionNotDetected + return "" } diff --git a/pkg/util/versionchecker/fromservice.go b/pkg/util/versionchecker/fromservice.go index 03a8003fc..4be4d9109 100644 --- a/pkg/util/versionchecker/fromservice.go +++ b/pkg/util/versionchecker/fromservice.go @@ -31,16 +31,16 @@ func (o *versionChecker) extractVersionFromService( ctx context.Context, namespace string, serviceName string, -) (string, error) { +) error { service := &corev1.Service{} serviceKey := client.ObjectKey{Namespace: namespace, Name: serviceName} err := o.client.Get(ctx, serviceKey, service) if err != nil { - return "", err + return err } - if version, err := extractVersionFromLabels(service.Labels); shouldReturn(err) { - return version, err + if label := extractVersionFromLabels(service.Labels); label != "" { + o.versionSources["webhookServiceLabelVersion"] = label } listOptions := client.MatchingLabelsSelector{ @@ -49,21 +49,22 @@ func (o *versionChecker) extractVersionFromService( pods := &corev1.PodList{} err = o.client.List(ctx, pods, listOptions) if err != nil { - return "", err + return err } for _, pod := range pods.Items { - if version, err := extractVersionFromLabels(pod.Labels); shouldReturn(err) { - return version, err + 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 { - return version[1], nil + o.versionSources["webhookPodImageVersion"] = version[1] + return nil } } } - return "", ErrVersionNotDetected + return nil } diff --git a/pkg/util/versionchecker/getpodfromtemplate.go b/pkg/util/versionchecker/getpodfromtemplate_test.go similarity index 94% rename from pkg/util/versionchecker/getpodfromtemplate.go rename to pkg/util/versionchecker/getpodfromtemplate_test.go index db9caa61e..840bd78f6 100644 --- a/pkg/util/versionchecker/getpodfromtemplate.go +++ b/pkg/util/versionchecker/getpodfromtemplate_test.go @@ -31,9 +31,9 @@ import ( // Based on https://github.com/kubernetes/kubernetes/blob/ca643a4d1f7bfe34773c74f79527be4afd95bf39/pkg/controller/controller_utils.go#L542 -var ValidatePodName = validation.NameIsDNSSubdomain +var validatePodName = validation.NameIsDNSSubdomain -func GetPodFromTemplate(template *v1.PodTemplateSpec, parentObject runtime.Object, controllerRef *metav1.OwnerReference) (*v1.Pod, error) { +func getPodFromTemplate(template *v1.PodTemplateSpec, parentObject runtime.Object, controllerRef *metav1.OwnerReference) (*v1.Pod, error) { desiredLabels := getPodsLabelSet(template) desiredFinalizers := getPodsFinalizers(template) desiredAnnotations := getPodsAnnotationSet(template) @@ -84,7 +84,7 @@ func getPodsAnnotationSet(template *v1.PodTemplateSpec) labels.Set { 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 { + if len(validatePodName(prefix, true)) != 0 { prefix = controllerName } return prefix diff --git a/pkg/util/versionchecker/versionchecker.go b/pkg/util/versionchecker/versionchecker.go index 1f03eacf1..f0bf6980c 100644 --- a/pkg/util/versionchecker/versionchecker.go +++ b/pkg/util/versionchecker/versionchecker.go @@ -42,21 +42,32 @@ var certManagerOldLabelSelector = map[string]string{ } 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") + 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) (string, error) + Version(context.Context) (*Version, error) } type versionChecker struct { client client.Client + + versionSources map[string]string } // New returns a cert-manager version checker @@ -84,21 +95,43 @@ func New(restcfg *rest.Config, scheme *runtime.Scheme) (Interface, error) { return nil, err } return &versionChecker{ - client: cl, + client: cl, + versionSources: map[string]string{}, }, nil } -func (o *versionChecker) Version(ctx context.Context) (string, error) { - version, err := o.extractVersionFromCrd(ctx, certificatesCertManagerCrdName) - if (err == nil) || (!errors.Is(err, ErrVersionNotDetected) && !errors.Is(err, ErrCertManagerCRDsNotFound)) { - return version, err +func (o *versionChecker) Version(ctx context.Context) (*Version, error) { + err := o.extractVersionFromCrd(ctx, certificatesCertManagerCrdName) + if err != nil && errors.Is(err, ErrCertManagerCRDsNotFound) { + // Retry using the oldCrdName and overwrite ErrCertManagerCRDsNotFound error + err = o.extractVersionFromCrd(ctx, certificatesCertManagerOldCrdName) + } + if err != nil { + return nil, err } - if errors.Is(err, ErrCertManagerCRDsNotFound) { - if version, err = o.extractVersionFromCrd(ctx, certificatesCertManagerOldCrdName); shouldReturn(err) { - return version, err - } - } - - return "", err + return o.determineVersion() +} + +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 } diff --git a/pkg/util/versionchecker/versionchecker_test.go b/pkg/util/versionchecker/versionchecker_test.go index 0c26f8578..33b132081 100644 --- a/pkg/util/versionchecker/versionchecker_test.go +++ b/pkg/util/versionchecker/versionchecker_test.go @@ -125,7 +125,7 @@ func transformObjects(objects []runtime.RawExtension) ([]runtime.Object, error) return nil, err } - pod, err := GetPodFromTemplate(&deployment.Spec.Template, resource.Object, nil) + pod, err := getPodFromTemplate(&deployment.Spec.Template, resource.Object, nil) if err != nil { return nil, err } @@ -173,7 +173,8 @@ func setupFakeVersionChecker(manifest io.Reader) (*versionChecker, error) { Build() return &versionChecker{ - client: cl, + client: cl, + versionSources: map[string]string{}, }, nil } @@ -204,7 +205,7 @@ func TestVersionChecker(t *testing.T) { t.Error(err) } - if version != versionGuess { + if version != versionGuess.Detected { t.Fatalf("wrong -> expected: %s vs detected: %s", version, versionGuess) } }) From 85710579dd7983c3cd8a567e15f60dd957d94d8c Mon Sep 17 00:00:00 2001 From: Inteon <42113979+inteon@users.noreply.github.com> Date: Fri, 30 Jul 2021 17:00:27 +0200 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Richard Wall Signed-off-by: Inteon <42113979+inteon@users.noreply.github.com> --- cmd/ctl/pkg/install/install.go | 3 +- cmd/ctl/pkg/version/version.go | 30 +++++++- pkg/util/versionchecker/BUILD.bazel | 47 ++---------- pkg/util/versionchecker/testdata/.gitignore | 1 + pkg/util/versionchecker/testdata/BUILD.bazel | 41 +++++++++++ pkg/util/versionchecker/testdata/fetch.sh | 72 +++++++++++++++++++ pkg/util/versionchecker/versionchecker.go | 43 +++++++---- .../versionchecker/versionchecker_test.go | 40 +---------- 8 files changed, 180 insertions(+), 97 deletions(-) create mode 100644 pkg/util/versionchecker/testdata/.gitignore create mode 100644 pkg/util/versionchecker/testdata/BUILD.bazel create mode 100755 pkg/util/versionchecker/testdata/fetch.sh diff --git a/cmd/ctl/pkg/install/install.go b/cmd/ctl/pkg/install/install.go index ee661c0e7..a336269a5 100644 --- a/cmd/ctl/pkg/install/install.go +++ b/cmd/ctl/pkg/install/install.go @@ -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. diff --git a/cmd/ctl/pkg/version/version.go b/cmd/ctl/pkg/version/version.go index 6be74b890..17c833734 100644 --- a/cmd/ctl/pkg/version/version.go +++ b/cmd/ctl/pkg/version/version.go @@ -62,6 +62,30 @@ 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, factory cmdutil.Factory) *cobra.Command { o := NewOptions(ioStreams) @@ -69,7 +93,7 @@ func NewCmdVersion(ctx context.Context, ioStreams genericclioptions.IOStreams, f cmd := &cobra.Command{ Use: "version", Short: "Print the cert-manager kubectl plugin version and the deployed cert-manager version", - Long: "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.Complete(factory)) @@ -101,12 +125,12 @@ func (o *Options) Complete(factory cmdutil.Factory) error { restConfig, err := factory.ToRESTConfig() if err != nil { - return fmt.Errorf("Error: cannot create the REST config: %v", err) + return fmt.Errorf("cannot create the REST config: %v", err) } o.VersionChecker, err = versionchecker.New(restConfig, scheme.Scheme) if err != nil { - return fmt.Errorf("Error: %v", err) + return err } return nil } diff --git a/pkg/util/versionchecker/BUILD.bazel b/pkg/util/versionchecker/BUILD.bazel index c0f7bbafa..7a641f92b 100644 --- a/pkg/util/versionchecker/BUILD.bazel +++ b/pkg/util/versionchecker/BUILD.bazel @@ -1,43 +1,5 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") -# 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-alpha.0/,$$p' > $@", -) - -genrule( - name = "test_manifests", - srcs = [ - ":git_tags.txt", - "//deploy/manifests:cert-manager.yaml", - "//:version", - ], - outs = ["test_manifests.tar"], - cmd = """ -CURRENT_VERSION=$$(cat $(location //:version)) -cp $(location //deploy/manifests:cert-manager.yaml) "$$CURRENT_VERSION.yaml" -for tag in $$(cat $(location :git_tags.txt)) -do - # The "v1.2.0-alpha.1" manifest contains duplicate crds, skip for tests - if [[ $$tag == "v1.2.0-alpha.1" ]]; then - continue - fi - - { - HTTP_CODE=$$(curl --compressed -sLo "$$tag.yaml" --write-out "%{http_code}" https://github.com/jetstack/cert-manager/releases/download/$$tag/cert-manager.yaml) - if [[ $${HTTP_CODE} -lt 200 || $${HTTP_CODE} -gt 299 ]]; then - (mv "$$tag.yaml" "$$tag.notfound") - fi - } & -done -wait -tar -cvf $@ *.yaml - """, - visibility = ["//visibility:public"], -) - go_library( name = "go_default_library", srcs = [ @@ -51,7 +13,6 @@ go_library( deps = [ "@com_github_pkg_errors//:go_default_library", "@io_k8s_api//core/v1:go_default_library", - "@io_k8s_api//rbac/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", @@ -69,12 +30,11 @@ go_test( "versionchecker_test.go", ], embed = [":go_default_library"], - embedsrcs = ["test_manifests.tar"], + 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_api//rbac/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", @@ -98,7 +58,10 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//pkg/util/versionchecker/testdata:all-srcs", + ], tags = ["automanaged"], visibility = ["//visibility:public"], ) diff --git a/pkg/util/versionchecker/testdata/.gitignore b/pkg/util/versionchecker/testdata/.gitignore new file mode 100644 index 000000000..bd6411424 --- /dev/null +++ b/pkg/util/versionchecker/testdata/.gitignore @@ -0,0 +1 @@ +*.tar \ No newline at end of file diff --git a/pkg/util/versionchecker/testdata/BUILD.bazel b/pkg/util/versionchecker/testdata/BUILD.bazel new file mode 100644 index 000000000..ef803540b --- /dev/null +++ b/pkg/util/versionchecker/testdata/BUILD.bazel @@ -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"], +) diff --git a/pkg/util/versionchecker/testdata/fetch.sh b/pkg/util/versionchecker/testdata/fetch.sh new file mode 100755 index 000000000..839c09e21 --- /dev/null +++ b/pkg/util/versionchecker/testdata/fetch.sh @@ -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 diff --git a/pkg/util/versionchecker/versionchecker.go b/pkg/util/versionchecker/versionchecker.go index f0bf6980c..598e34d80 100644 --- a/pkg/util/versionchecker/versionchecker.go +++ b/pkg/util/versionchecker/versionchecker.go @@ -18,10 +18,9 @@ package versionchecker import ( "context" + "fmt" corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - rbacv1beta1 "k8s.io/api/rbac/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" @@ -81,12 +80,6 @@ func New(restcfg *rest.Config, scheme *runtime.Scheme) (Interface, error) { if err := apiextensionsv1beta1.AddToScheme(scheme); err != nil { return nil, err } - if err := rbacv1.AddToScheme(scheme); err != nil { - return nil, err - } - if err := rbacv1beta1.AddToScheme(scheme); err != nil { - return nil, err - } cl, err := client.New(restcfg, client.Options{ Scheme: scheme, @@ -100,19 +93,45 @@ func New(restcfg *rest.Config, scheme *runtime.Scheme) (Interface, error) { }, 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 oldCrdName and overwrite ErrCertManagerCRDsNotFound error + // Retry using the old CRD name "certificates.certmanager.k8s.io" as + // a starting point and overwrite ErrCertManagerCRDsNotFound error err = o.extractVersionFromCrd(ctx, certificatesCertManagerOldCrdName) } - if err != nil { - return nil, err + + // 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 o.determineVersion() + 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 diff --git a/pkg/util/versionchecker/versionchecker_test.go b/pkg/util/versionchecker/versionchecker_test.go index 33b132081..c0eb18c66 100644 --- a/pkg/util/versionchecker/versionchecker_test.go +++ b/pkg/util/versionchecker/versionchecker_test.go @@ -27,8 +27,6 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - rbacv1beta1 "k8s.io/api/rbac/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" @@ -38,11 +36,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -//go:embed test_manifests.tar +//go:embed testdata/test_manifests.tar var testFiles embed.FS func loadManifests() (io.Reader, error, func() (string, error), func()) { - data, err := testFiles.Open("test_manifests.tar") + data, err := testFiles.Open("testdata/test_manifests.tar") if err != nil { return nil, err, nil, nil } @@ -87,34 +85,6 @@ func transformObjects(objects []runtime.RawExtension) ([]runtime.Object, error) var err error gvk := resource.Object.GetObjectKind().GroupVersionKind() - // Cast ClusterRole from unstructured to rbacv1 ClusterRole - if gvk.Group == "rbac.authorization.k8s.io" && gvk.Version == "v1" && gvk.Kind == "ClusterRole" { - unstr := resource.Object.(*unstructured.Unstructured) - - var clusterRole rbacv1.ClusterRole - err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstr.Object, &clusterRole) - if err != nil { - return nil, err - } - - transformedObjects = append(transformedObjects, &clusterRole) - continue - } - - // Cast ClusterRole from unstructured to rbacv1beta1 ClusterRole - if gvk.Group == "rbac.authorization.k8s.io" && gvk.Version == "v1beta1" && gvk.Kind == "ClusterRole" { - unstr := resource.Object.(*unstructured.Unstructured) - - var clusterRole rbacv1beta1.ClusterRole - err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstr.Object, &clusterRole) - if err != nil { - return nil, err - } - - transformedObjects = append(transformedObjects, &clusterRole) - continue - } - // Create a pod for a Deployment resource if gvk.Group == "apps" && gvk.Version == "v1" && gvk.Kind == "Deployment" { unstr := resource.Object.(*unstructured.Unstructured) @@ -154,12 +124,6 @@ func setupFakeVersionChecker(manifest io.Reader) (*versionChecker, error) { if err := apiextensionsv1beta1.AddToScheme(scheme); err != nil { return nil, err } - if err := rbacv1.AddToScheme(scheme); err != nil { - return nil, err - } - if err := rbacv1beta1.AddToScheme(scheme); err != nil { - return nil, err - } objs, err := manifestToObject(manifest) if err != nil {