Skip to content
Open
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
52 changes: 28 additions & 24 deletions cmd/crossplane/completion/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func Predictors() map[string]complete.Predictor {
// last completed argument.
func kubernetesResourcePredictor() complete.PredictFunc {
return func(a complete.Args) []string {
_, kubeconfig, _, err := kubernetesClient(parseConfigOverride(a), parseImpersonation(a))
_, kubeconfig, _, err := kubernetesClient(parseConfigFlags(a), parseConfigOverride(a), parseImpersonation(a))
if err != nil {
return nil
}
Expand Down Expand Up @@ -104,7 +104,7 @@ func kubernetesResourcePredictor() complete.PredictFunc {
// last completed argument.
func kubernetesResourceNamePredictor() complete.PredictFunc {
return func(a complete.Args) []string {
client, kubeconfig, clientconfig, err := kubernetesClient(parseConfigOverride(a), parseImpersonation(a))
client, kubeconfig, clientconfig, err := kubernetesClient(parseConfigFlags(a), parseConfigOverride(a), parseImpersonation(a))
if err != nil {
return nil
}
Expand Down Expand Up @@ -163,12 +163,7 @@ func kubernetesResourceNamePredictor() complete.PredictFunc {
// contextPredictor returns a predictor that suggests Kubernetes contexts from the KUBECONFIG.
func contextPredictor() complete.PredictFunc {
return func(a complete.Args) []string {
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&clientcmd.ConfigOverrides{},
)

kubeConfig, err := clientConfig.RawConfig()
kubeConfig, err := parseConfigFlags(a).ClientConfig(&clientcmd.ConfigOverrides{}).RawConfig()
if err != nil {
return nil
}
Expand All @@ -190,7 +185,7 @@ func contextPredictor() complete.PredictFunc {
// last completed argument.
func namespacePredictor() complete.PredictFunc {
return func(a complete.Args) []string {
client, err := kubernetesClientset(parseImpersonation(a))
client, err := kubernetesClientset(parseConfigFlags(a), parseImpersonation(a))
if err != nil {
return nil
}
Expand All @@ -212,15 +207,10 @@ func namespacePredictor() complete.PredictFunc {
}
}

// kubernetesClientset returns a Kubernetes clientset using the default
// kubeconfig and the given impersonation flags.
func kubernetesClientset(imp kube.ImpersonationFlags) (*kubernetes.Clientset, error) {
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&clientcmd.ConfigOverrides{},
)

kubeConfig, err := clientConfig.ClientConfig()
// kubernetesClientset returns a Kubernetes clientset using the given config
// and impersonation flags.
func kubernetesClientset(kc kube.ConfigFlags, imp kube.ImpersonationFlags) (*kubernetes.Clientset, error) {
kubeConfig, err := kc.ClientConfig(&clientcmd.ConfigOverrides{}).ClientConfig()
if err != nil {
return nil, err
}
Expand All @@ -231,12 +221,9 @@ func kubernetesClientset(imp kube.ImpersonationFlags) (*kubernetes.Clientset, er
}

// kubernetesClient returns a Kubernetes client and a rest.Config using the
// provided config overrides and impersonation flags.
func kubernetesClient(configOverrides *clientcmd.ConfigOverrides, imp kube.ImpersonationFlags) (controllerClient.Client, *rest.Config, clientcmd.ClientConfig, error) {
clientconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
configOverrides,
)
// provided config flags, config overrides and impersonation flags.
func kubernetesClient(kc kube.ConfigFlags, configOverrides *clientcmd.ConfigOverrides, imp kube.ImpersonationFlags) (controllerClient.Client, *rest.Config, clientcmd.ClientConfig, error) {
clientconfig := kc.ClientConfig(configOverrides)

kubeconfig, err := clientconfig.ClientConfig()
if err != nil {
Expand Down Expand Up @@ -269,6 +256,23 @@ func parseConfigOverride(a complete.Args) *clientcmd.ConfigOverrides {
}
}

// parseConfigFlags parses the --kubeconfig flag from the completed command
// line arguments. Supports both "--flag value" and "--flag=value" forms.
func parseConfigFlags(a complete.Args) kube.ConfigFlags {
var kc kube.ConfigFlags

for i, arg := range a.All {
switch {
case arg == "--kubeconfig" && i+1 < len(a.All):
kc.Kubeconfig = a.All[i+1]
case strings.HasPrefix(arg, "--kubeconfig="):
kc.Kubeconfig = strings.TrimPrefix(arg, "--kubeconfig=")
}
}

return kc
}

// parseImpersonation parses the kubectl-compatible impersonation flags (--as,
// --as-group, --as-uid) from the completed command line arguments. Supports
// both "--flag value" and "--flag=value" forms; --as-group may be repeated.
Expand Down
33 changes: 33 additions & 0 deletions cmd/crossplane/completion/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,39 @@ import (
"github.com/crossplane/cli/v2/internal/kube"
)

func TestParseConfigFlags(t *testing.T) {
cases := map[string]struct {
reason string
args []string
want kube.ConfigFlags
}{
"Equals": {
reason: "The --flag=value form is parsed.",
args: []string{"trace", "x", "--kubeconfig=/tmp/kubeconfig"},
want: kube.ConfigFlags{Kubeconfig: "/tmp/kubeconfig"},
},
"Space": {
reason: "The --flag value form is parsed.",
args: []string{"--kubeconfig", "/tmp/kubeconfig"},
want: kube.ConfigFlags{Kubeconfig: "/tmp/kubeconfig"},
},
"None": {
reason: "No --kubeconfig flag yields the zero value.",
args: []string{"trace", "x"},
want: kube.ConfigFlags{},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := parseConfigFlags(complete.Args{All: tc.args})
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("%s\nparseConfigFlags(): -want, +got:\n%s", tc.reason, diff)
}
})
}
}

func TestParseImpersonation(t *testing.T) {
cases := map[string]struct {
reason string
Expand Down
4 changes: 2 additions & 2 deletions cmd/crossplane/top/top.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/client-go/kubernetes"
"k8s.io/metrics/pkg/client/clientset/versioned"
ctrl "sigs.k8s.io/controller-runtime"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
"github.com/crossplane/crossplane-runtime/v2/pkg/logging"
Expand Down Expand Up @@ -61,6 +60,7 @@ type Cmd struct {
Summary bool `help:"Adds summary header for all Crossplane pods." name:"summary" short:"s"`
Namespace string `default:"crossplane-system" help:"Show pods from a specific namespace, defaults to crossplane-system." name:"namespace" predictor:"namespace" short:"n"`

Config kube.ConfigFlags `embed:""`
Impersonation kube.ImpersonationFlags `embed:""`
}

Expand Down Expand Up @@ -102,7 +102,7 @@ func (c *Cmd) Run(k *kong.Context, logger logging.Logger) error {
logger.Debug("Tabwriter header created")

// Build the config from the kubeconfig path
config, err := ctrl.GetConfig()
config, err := c.Config.RESTConfig()
if err != nil {
return errors.Wrap(err, errKubeConfig)
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/crossplane/trace/help/trace.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ You can further specify the kind as `TYPE[.VERSION][.GROUP]` if needed; for
example, `mykind.example.org` or `mykind.v1alpha1.example.org`.

By default, `crossplane resource trace` uses the Kubernetes configuration at
`~/.kube/config`. Override with the `KUBECONFIG` environment variable.
`~/.kube/config`. Override with the `--kubeconfig` flag or the `KUBECONFIG`
environment variable.

## Output options

Expand Down
6 changes: 2 additions & 4 deletions cmd/crossplane/trace/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type Cmd struct {
Concurrency int `default:"5" help:"load concurrency" name:"concurrency"`
Watch bool `default:"false" help:"Watch for changes until resource deletion." name:"watch" short:"w"`

Config kube.ConfigFlags `embed:""`
Impersonation kube.ImpersonationFlags `embed:""`
}

Expand All @@ -90,10 +91,7 @@ func (c *Cmd) Help() string {
}

func (c *Cmd) setupKubeClient(logger logging.Logger) (clientcmd.ClientConfig, client.WithWatch, meta.RESTMapper, error) {
clientconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&clientcmd.ConfigOverrides{CurrentContext: c.Context},
)
clientconfig := c.Config.ClientConfig(&clientcmd.ConfigOverrides{CurrentContext: c.Context})

kubeconfig, err := clientconfig.ClientConfig()
if err != nil {
Expand Down
5 changes: 2 additions & 3 deletions cmd/crossplane/version/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"github.com/google/go-containerregistry/pkg/name"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
ctrl "sigs.k8s.io/controller-runtime"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"

Expand All @@ -39,10 +38,10 @@ const (
// FetchCrossplaneVersion initializes a Kubernetes client and fetches
// and returns the version of the Crossplane deployment. If the version
// does not have a leading 'v', it prepends it.
func FetchCrossplaneVersion(ctx context.Context, imp kube.ImpersonationFlags) (string, error) {
func FetchCrossplaneVersion(ctx context.Context, kc kube.ConfigFlags, imp kube.ImpersonationFlags) (string, error) {
var version string

config, err := ctrl.GetConfig()
config, err := kc.RESTConfig()
if err != nil {
return "", errors.Wrap(err, errKubeConfig)
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/crossplane/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const (
type Cmd struct {
Client bool `env:"" help:"If true, shows client version only (no server required)."`

Config kube.ConfigFlags `embed:""`
Impersonation kube.ImpersonationFlags `embed:""`
}

Expand All @@ -52,7 +53,7 @@ func (c *Cmd) Run(k *kong.Context) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

vxp, err := FetchCrossplaneVersion(ctx, c.Impersonation)
vxp, err := FetchCrossplaneVersion(ctx, c.Config, c.Impersonation)
if err != nil {
return errors.Wrap(err, errGetCrossplaneVersion)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/crossplane/xpkg/help/install.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The `xpkg install` command installs a package in a Crossplane control plane. It
uses `~/.kube/config` to connect to the control plane; override the path with
the `KUBECONFIG` environment variable.
the `--kubeconfig` flag or the `KUBECONFIG` environment variable.

Specify the package kind, fully qualified package OCI reference, and optionally
a name for the package inside Crossplane:
Expand Down
2 changes: 1 addition & 1 deletion cmd/crossplane/xpkg/help/update.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The `xpkg update` command updates a package in a Crossplane control plane. It
uses `~/.kube/config` to connect to the control plane; override the path with
the `KUBECONFIG` environment variable.
the `--kubeconfig` flag or the `KUBECONFIG` environment variable.

Specify the package kind, a new fully qualified package OCI reference, and
optionally the name of the package already installed in Crossplane:
Expand Down
4 changes: 2 additions & 2 deletions cmd/crossplane/xpkg/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
Expand Down Expand Up @@ -70,6 +69,7 @@ type installCmd struct {
RevisionHistoryLimit int64 `help:"Number of package revisions that can exist before garbage collection." placeholder:"LIMIT" short:"r"`
Wait time.Duration `default:"0s" help:"How long to wait for the package to install before returning. The command doesn't wait by default." short:"w"`

Config kube.ConfigFlags `embed:""`
Impersonation kube.ImpersonationFlags `embed:""`
}

Expand Down Expand Up @@ -147,7 +147,7 @@ func (c *installCmd) Run(k *kong.Context, logger logging.Logger) error {
rpkg.SetRuntimeConfigRef(&v1.RuntimeConfigReference{Name: c.RuntimeConfig})
}

cfg, err := ctrl.GetConfig()
cfg, err := c.Config.RESTConfig()
if err != nil {
return errors.Wrap(err, errKubeConfig)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/crossplane/xpkg/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
Expand All @@ -52,6 +51,7 @@ type updateCmd struct {
Package string `arg:"" help:"The package to update to. Must be fully qualified, including the registry, repository, and tag." placeholder:"REGISTRY/REPOSITORY:TAG"`
Name string `arg:"" help:"The name of the package to update in the Crossplane API. Derived from the package repository and tag by default." optional:""`

Config kube.ConfigFlags `embed:""`
Impersonation kube.ImpersonationFlags `embed:""`
}

Expand Down Expand Up @@ -92,7 +92,7 @@ func (c *updateCmd) Run(k *kong.Context, logger logging.Logger) error {
return errors.Errorf("unsupported package kind %q", c.Kind)
}

cfg, err := ctrl.GetConfig()
cfg, err := c.Config.RESTConfig()
if err != nil {
return errors.Wrap(err, errKubeConfig)
}
Expand Down
70 changes: 70 additions & 0 deletions internal/kube/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
Copyright 2026 The Crossplane 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 kube

import (
"os"

"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
)

// ConfigFlags are the kubectl-compatible flags that select the kubeconfig
// used to reach the cluster. Embed it into a command's Kong flag struct with
// the `embed:""` tag.
type ConfigFlags struct {
Kubeconfig string `help:"Path to the kubeconfig file to use for CLI requests." name:"kubeconfig" placeholder:"PATH" predictor:"file"`
}

// ClientConfig returns a client config for the selected kubeconfig. An
// explicit --kubeconfig wins over the KUBECONFIG environment variable and the
// default ~/.kube/config location.
func (f ConfigFlags) ClientConfig(overrides *clientcmd.ConfigOverrides) clientcmd.ClientConfig {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.ExplicitPath = f.Kubeconfig

return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// RESTConfig returns a rest.Config with the precedence of controller-runtime's
// config.GetConfig: the --kubeconfig flag, then the KUBECONFIG environment
// variable, then the in-cluster config, then ~/.kube/config. Client-side rate
// limiting is disabled unless the kubeconfig sets it.
func (f ConfigFlags) RESTConfig() (*rest.Config, error) {
if f.Kubeconfig == "" && os.Getenv(clientcmd.RecommendedConfigPathEnvVar) == "" {
if cfg, err := rest.InClusterConfig(); err == nil {
return withDefaultQPS(cfg), nil
}
}

cfg, err := f.ClientConfig(&clientcmd.ConfigOverrides{}).ClientConfig()
if err != nil {
return nil, errors.Wrap(err, "cannot load a kubeconfig, use --kubeconfig to select one")
}

return withDefaultQPS(cfg), nil
}

func withDefaultQPS(cfg *rest.Config) *rest.Config {
if cfg.QPS == 0 {
cfg.QPS = -1
}

return cfg
}
Loading