Skip to content

Commit

Permalink
make the binary dynamically determine whether it is a kubectl plugin
Browse files Browse the repository at this point in the history
Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com>
  • Loading branch information
inteon committed May 8, 2024
1 parent 51704f2 commit 59119cf
Show file tree
Hide file tree
Showing 25 changed files with 277 additions and 278 deletions.
1 change: 0 additions & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# to this builds array (environment variables, flags, ...)
builds:
- id: cmctl
- id: kubectl_cert-manager

# config the checksum filename
# https://goreleaser.com/customization/checksum
Expand Down
39 changes: 8 additions & 31 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package cmd

import (
"context"
"fmt"
"io"

logf "github.com/cert-manager/cert-manager/pkg/logs"
Expand All @@ -31,14 +30,18 @@ import (
)

func NewCertManagerCtlCommand(ctx context.Context, in io.Reader, out, err io.Writer) *cobra.Command {
ctx = logf.NewContext(ctx, logf.Log)

logOptions := logs.NewOptions()

cmds := &cobra.Command{
Use: build.Name(),
Use: build.Name(ctx),
Annotations: map[string]string{
// For commands that have a space (eg. kubectl cert-manager), the name
// is not correctly determined based on just the Use field.
cobra.CommandDisplayNameAnnotation: build.Name(ctx),
},

Short: "cert-manager CLI tool to manage and configure cert-manager resources",
Long: build.WithTemplate(`
Long: build.WithTemplate(ctx, `
{{.BuildName}} is a CLI tool manage and configure cert-manager resources for Kubernetes`),
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
Expand All @@ -49,7 +52,6 @@ func NewCertManagerCtlCommand(ctx context.Context, in io.Reader, out, err io.Wri
SilenceErrors: true, // Errors are already logged when calling cmd.Execute()
SilenceUsage: true, // Don't print usage when an error occurs
}
cmds.SetUsageTemplate(usageTemplate())

logf.AddFlagsNonDeprecated(logOptions, cmds.PersistentFlags())

Expand All @@ -60,28 +62,3 @@ func NewCertManagerCtlCommand(ctx context.Context, in io.Reader, out, err io.Wri

return cmds
}

func usageTemplate() string {
return fmt.Sprintf(`Usage:{{if .Runnable}} %s {{end}}{{if .HasAvailableSubCommands}} %s [command]{{end}}{{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "%s [command] --help" for more information about a command.{{end}}
`, build.Name(), build.Name(), build.Name())
}
6 changes: 5 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,20 @@ import (

ctlcmd "github.com/cert-manager/cmctl/v2/cmd"
"github.com/cert-manager/cmctl/v2/internal/util"
"github.com/cert-manager/cmctl/v2/pkg/build"
)

func main() {
ctx, exit := util.SetupExitHandler(context.Background(), util.AlwaysErrCode)
defer exit() // This function might call os.Exit, so defer last

ctlName, isKubectlPlugin := build.DetectCtlInfo(os.Args)

logf.InitLogs()
defer logf.FlushLogs()
ctrl.SetLogger(logf.Log)
ctx = logf.NewContext(ctx, logf.Log, "cmctl")
ctx = logf.NewContext(ctx, logf.Log, ctlName)
ctx = build.WithCtlInfo(ctx, ctlName, isKubectlPlugin)

// In cmctl, we are using cmdutil.CheckErr, a kubectl utility function that creates human readable
// error messages from errors. By default, this function will call os.Exit(1) if it receives an error.
Expand Down
12 changes: 1 addition & 11 deletions make/00_mod.mk
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,13 @@

repo_name := github.com/cert-manager/cmctl/v2

exe_build_names := cmctl kubectl_cert-manager
exe_build_names := cmctl
gorelease_file := .goreleaser.yml

go_cmctl_main_dir := .
go_cmctl_mod_dir := .
go_cmctl_ldflags := \
-X $(repo_name)/pkg/build.name=cmctl \
-X $(repo_name)/pkg/build/commands.registerCompletion=true \
-X github.com/cert-manager/cert-manager/pkg/util.AppVersion=$(VERSION) \
-X github.com/cert-manager/cert-manager/pkg/util.AppGitCommit=$(GITCOMMIT)

go_kubectl_cert-manager_main_dir := .
go_kubectl_cert-manager_mod_dir := .
go_kubectl_cert-manager_ldflags := \
-X $(repo_name)/pkg/build.name=kubectl \
-X $(repo_name)/pkg/build/commands.registerCompletion=false \
-X github.com/cert-manager/cert-manager/pkg/util/version.AppVersion=$(VERSION) \
-X github.com/cert-manager/cert-manager/pkg/util/version.AppGitCommit=$(GITCOMMIT)

golangci_lint_config := .golangci.yaml
33 changes: 14 additions & 19 deletions pkg/approve/approve.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,12 @@ import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"

"github.com/cert-manager/cmctl/v2/pkg/build"
"github.com/cert-manager/cmctl/v2/pkg/factory"
)

var (
example = templates.Examples(i18n.T(build.WithTemplate(`
# Approve a CertificateRequest with the name 'my-cr'
{{.BuildName}} approve my-cr
# Approve a CertificateRequest in namespace default
{{.BuildName}} approve my-cr --namespace default
# Approve a CertificateRequest giving a custom reason and message
{{.BuildName}} approve my-cr --reason "ManualApproval" --reason "Approved by PKI department"
`)))
)

// Options is a struct to support create certificaterequest command
type Options struct {
// Reason is the string that will be set on the Reason field of the Approved
Expand All @@ -71,10 +57,19 @@ func NewCmdApprove(setupCtx context.Context, ioStreams genericclioptions.IOStrea
o := newOptions(ioStreams)

cmd := &cobra.Command{
Use: "approve",
Short: "Approve a CertificateRequest",
Long: `Mark a CertificateRequest as Approved, so it may be signed by a configured Issuer.`,
Example: example,
Use: "approve",
Short: "Approve a CertificateRequest",
Long: `Mark a CertificateRequest as Approved, so it may be signed by a configured Issuer.`,
Example: templates.Examples(build.WithTemplate(setupCtx, `
# Approve a CertificateRequest with the name 'my-cr'
{{.BuildName}} approve my-cr
# Approve a CertificateRequest in namespace default
{{.BuildName}} approve my-cr --namespace default
# Approve a CertificateRequest giving a custom reason and message
{{.BuildName}} approve my-cr --reason "ManualApproval" --reason "Approved by PKI department"
`)),
ValidArgsFunction: factory.ValidArgsListCertificateRequests(&o.Factory),
PreRunE: func(cmd *cobra.Command, args []string) error {
return o.Validate(args)
Expand All @@ -87,7 +82,7 @@ func NewCmdApprove(setupCtx context.Context, ioStreams genericclioptions.IOStrea

cmd.Flags().StringVar(&o.Reason, "reason", "KubectlCertManager",
"The reason to give as to what approved this CertificateRequest.")
cmd.Flags().StringVar(&o.Message, "message", fmt.Sprintf("manually approved by %q", build.Name()),
cmd.Flags().StringVar(&o.Message, "message", fmt.Sprintf("manually approved by %q", build.Name(setupCtx)),
"The message to give as to why this CertificateRequest was approved.")

o.Factory = factory.New(cmd)
Expand Down
52 changes: 45 additions & 7 deletions pkg/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,62 @@ package build

import (
"bytes"
"context"
"os"
"path/filepath"
"strings"
"text/template"
)

// name is the build time configurable name of the build (name of the target
// binary name).
var name = "cmctl"
var defaultCtlName string = "cmctl"
var defaultIsKubectlPlugin bool = false

func DetectCtlInfo(args []string) (name string, isKubectlPlugin bool) {
commandName := filepath.Base(os.Args[0])
if strings.HasPrefix(commandName, "kubectl-") || strings.HasPrefix(commandName, "kubectl_") {
return "kubectl cert-manager", true
}

return commandName, false
}

// contextNameKey is how we find the ctl name in a context.Context.
type contextNameKey struct{}

// contextIsKubectlPluginKey is how we find if the ctl is a Kubectl plugin in a context.Context.
type contextIsKubectlPluginKey struct{}

func WithCtlInfo(ctx context.Context, name string, isKubectlPlugin bool) context.Context {
ctx = context.WithValue(ctx, contextNameKey{}, name)
ctx = context.WithValue(ctx, contextIsKubectlPluginKey{}, isKubectlPlugin)
return ctx
}

func Name(ctx context.Context) string {
name, ok := ctx.Value(contextNameKey{}).(string)
if !ok {
return defaultCtlName
}

// Name returns the build name.
func Name() string {
return name
}

func IsKubectlPlugin(ctx context.Context) bool {
isKubectlPlugin, ok := ctx.Value(contextIsKubectlPluginKey{}).(bool)
if !ok {
return defaultIsKubectlPlugin
}

return isKubectlPlugin
}

// WithTemplate returns a string that has the build name templated out with the
// configured build name. Build name templates on '{{ .BuildName }}' variable.
func WithTemplate(str string) string {
func WithTemplate(ctx context.Context, str string) string {
buildName := Name(ctx)
tmpl := template.Must(template.New("build-name").Parse(str))
var buf bytes.Buffer
if err := tmpl.Execute(&buf, struct{ BuildName string }{name}); err != nil {
if err := tmpl.Execute(&buf, struct{ BuildName string }{buildName}); err != nil {
// We panic here as it should never be possible that this template fails.
panic(err)
}
Expand Down
10 changes: 1 addition & 9 deletions pkg/build/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package commands

import (
"context"
"strings"

"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
Expand All @@ -37,11 +36,6 @@ import (
"github.com/cert-manager/cmctl/v2/pkg/version"
)

// registerCompletion gates whether the completion command is registered.
// Specifically useful when building the CLI as a kubectl plugin which does not
// support completion.
var registerCompletion = "false"

type RegisterCommandFunc func(context.Context, genericclioptions.IOStreams) *cobra.Command

// Commands returns the cobra Commands that should be registered for the CLI
Expand All @@ -61,10 +55,8 @@ func Commands() []RegisterCommandFunc {

// Experimental features
experimental.NewCmdExperimental,
}

if strings.ToLower(registerCompletion) == "true" {
cmds = append(cmds, completion.NewCmdCompletion)
completion.NewCmdCompletion,
}

return cmds
Expand Down
3 changes: 1 addition & 2 deletions pkg/check/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"

cmcmdutil "github.com/cert-manager/cmctl/v2/internal/util"
Expand All @@ -51,7 +50,7 @@ type Options struct {
*factory.Factory
}

var checkApiDesc = templates.LongDesc(i18n.T(`
var checkApiDesc = templates.LongDesc((`
This check attempts to perform a dry-run create of a cert-manager *v1alpha2*
Certificate resource in order to verify that CRDs are installed and all the
required webhooks are reachable by the K8S API server.
Expand Down
6 changes: 4 additions & 2 deletions pkg/completion/bash.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@ limitations under the License.
package completion

import (
"context"

"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"

"github.com/cert-manager/cmctl/v2/pkg/build"
)

func newCmdCompletionBash(ioStreams genericclioptions.IOStreams) *cobra.Command {
func newCmdCompletionBash(setupCtx context.Context, ioStreams genericclioptions.IOStreams) *cobra.Command {
return &cobra.Command{
Use: "bash",
Short: "Generate cert-manager CLI scripts for a Bash shell",
Long: build.WithTemplate(`To load completions:
Long: build.WithTemplate(setupCtx, `To load completions:
Bash:
$ source <({{.BuildName}} completion bash)
# To load completions for each session, execute once:
Expand Down
14 changes: 10 additions & 4 deletions pkg/completion/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (

"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"

"github.com/cert-manager/cmctl/v2/pkg/build"
)

func NewCmdCompletion(setupCtx context.Context, ioStreams genericclioptions.IOStreams) *cobra.Command {
Expand All @@ -30,10 +32,14 @@ func NewCmdCompletion(setupCtx context.Context, ioStreams genericclioptions.IOSt
Long: "Generate completion for the cert-manager CLI so arguments and flags can be suggested and auto-completed",
}

cmds.AddCommand(newCmdCompletionBash(ioStreams))
cmds.AddCommand(newCmdCompletionZSH(ioStreams))
cmds.AddCommand(newCmdCompletionFish(ioStreams))
cmds.AddCommand(newCmdCompletionPowerShell(ioStreams))
if build.IsKubectlPlugin(setupCtx) {
cmds.AddCommand(newCmdCompletionKubectl(setupCtx, ioStreams))
} else {
cmds.AddCommand(newCmdCompletionBash(setupCtx, ioStreams))
cmds.AddCommand(newCmdCompletionZSH(setupCtx, ioStreams))
cmds.AddCommand(newCmdCompletionFish(setupCtx, ioStreams))
cmds.AddCommand(newCmdCompletionPowerShell(setupCtx, ioStreams))
}

return cmds
}
6 changes: 4 additions & 2 deletions pkg/completion/fish.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@ limitations under the License.
package completion

import (
"context"

"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"

"github.com/cert-manager/cmctl/v2/pkg/build"
)

func newCmdCompletionFish(ioStreams genericclioptions.IOStreams) *cobra.Command {
func newCmdCompletionFish(setupCtx context.Context, ioStreams genericclioptions.IOStreams) *cobra.Command {
return &cobra.Command{
Use: "fish",
Short: "Generate cert-manager CLI scripts for a Fish shell",
Long: build.WithTemplate(`To load completions:
Long: build.WithTemplate(setupCtx, `To load completions:
$ {{.BuildName}} completion fish | source
# To load completions for each session, execute once:
Expand Down
Loading

0 comments on commit 59119cf

Please sign in to comment.