Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the binary dynamically determine whether it is a kubectl plugin #61

Merged
merged 3 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,68 @@

The documentation for `cmctl` can be found on the [cert-manager website](https://cert-manager.io/docs/usage/cmctl/).

## Installation

> [!Note]
> These instructions are a copy of the [official installation instructions](https://cert-manager.io/docs/usage/cmctl/#installation).

### Homebrew

On Mac or Linux if you have [Homebrew](https://brew.sh/) installed, you can install `cmctl` with:

```sh
brew install cmctl
```

This will also install shell completion.

### Go install

If you have Go installed, you can install `cmctl` with:

```sh
go install github.com/cert-manager/cmctl/v2@latest
```

### Manual Installation

You need the `cmctl` file for the platform you're using, these can be found on our [cmctl GitHub releases page](https://github.com/cert-manager/cmctl/releases).
In order to use `cmctl` you need its binary to be accessible under the name `cmctl` in your `$PATH`. Run the following commands to set up the CLI. Replace OS and ARCH with your systems equivalents:

```sh
OS=$(uname -s | tr A-Z a-z); ARCH=$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/'); curl -fsSL -o cmctl https://github.com/cert-manager/cmctl/releases/latest/download/cmctl_${OS}_${ARCH}
chmod +x cmctl
sudo mv cmctl /usr/local/bin
# or `sudo mv cmctl /usr/local/bin/kubectl-cert_manager` to use `kubectl cert-manager` instead.
```

### Shell Completion

`cmctl` supports shell completion for most popular shells. To get help on how to enable shell completion, run the following commands:

```sh
$ cmctl completion --help
# or `kubectl cert-manager completion --help`
...
Available Commands:
bash Generate cert-manager CLI scripts for a Bash shell
fish Generate cert-manager CLI scripts for a Fish shell
powershell Generate cert-manager CLI scripts for a PowerShell shell
zsh Generation cert-manager CLI scripts for a ZSH shell

$ cmctl completion bash --help
To load completions:
Bash:
$ source <(cmctl completion bash)
# To load completions for each session, execute once:
# Linux:
$ cmctl completion bash > /etc/bash_completion.d/cmctl

# macOS:
$ cmctl completion bash > /usr/local/etc/bash_completion.d/cmctl
...
```

## Versioning

Before v2, `cmctl` was located in the cert-manager repository and versioned together with cert-manager.
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
}

ThatsMrTalbot marked this conversation as resolved.
Show resolved Hide resolved
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
5 changes: 2 additions & 3 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,12 +50,12 @@ 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.
We use v1alpha2 API to ensure that the API server has also connected to the
cert-manager conversion webhook.`))
cert-manager conversion webhook.`)

// NewOptions returns initialized Options
func NewOptions(ioStreams genericclioptions.IOStreams) *Options {
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
Loading