Skip to content

Commit

Permalink
feat(cli): Fix --kubeconfig and support --cluster and --context flags (
Browse files Browse the repository at this point in the history
  • Loading branch information
irvinlim authored Feb 11, 2023
1 parent 1e86f73 commit 32a3ef2
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 86 deletions.
22 changes: 12 additions & 10 deletions pkg/cli/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,9 @@ import (
)

type RootCommand struct {
kubeconfig string
namespace string
verbosity int
dynConfigName string
dynConfigNamespace string

streams *streams.Streams

verbosity int
}

// NewRootCommand returns a new root command for the command-line utility.
Expand All @@ -56,18 +52,24 @@ func NewRootCommand(streams *streams.Streams) *cobra.Command {
streams.SetCmdOutput(cmd)

flags := cmd.PersistentFlags()
flags.StringVar(&c.kubeconfig, "kubeconfig", "",
flags.String("kubeconfig", "",
"Path to the kubeconfig file to use for CLI requests.")
flags.StringVarP(&c.namespace, "namespace", "n", "",
flags.String("cluster", "",
"The name of the kubeconfig cluster to use.")
flags.String("context", "",
"The name of the kubeconfig context to use.")
flags.StringP("namespace", "n", "",
"If present, the namespace scope for this CLI request.")
flags.StringVar(&c.dynConfigName, "dynamic-config-name", "execution-dynamic-config",
flags.String("dynamic-config-name", "execution-dynamic-config",
"Overrides the name of the dynamic cluster config.")
flags.StringVar(&c.dynConfigNamespace, "dynamic-config-namespace", "furiko-system",
flags.String("dynamic-config-namespace", "furiko-system",
"Overrides the namespace of the dynamic cluster config.")
flags.IntVarP(&c.verbosity, "v", "v", 0, "Sets the log level verbosity.")

if err := completion.RegisterFlagCompletions(cmd, []completion.FlagCompletion{
{FlagName: "namespace", Completer: &completion.ListNamespacesCompleter{}},
{FlagName: "cluster", CmdCompletionFunc: completion.ListKubeconfigClusterCompletionFunc},
{FlagName: "context", CmdCompletionFunc: completion.ListKubeconfigContextCompletionFunc},
}); err != nil {
common.Fatal(err, common.DefaultErrorExitCode)
}
Expand Down
75 changes: 2 additions & 73 deletions pkg/cli/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package common

import (
"context"
"fmt"
"os"
"strings"
Expand All @@ -27,12 +26,10 @@ import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"

configv1alpha1 "github.com/furiko-io/furiko/apis/config/v1alpha1"
"github.com/furiko-io/furiko/pkg/cli/printer"
"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
"github.com/furiko-io/furiko/pkg/utils/jsonyaml"
)

const (
Expand All @@ -58,19 +55,14 @@ func RunAllE(funcs ...RunEFunc) RunEFunc {
}

// NewContext returns a common context from the cobra command.
func NewContext(_ *cobra.Command) (controllercontext.Context, error) {
kubeconfig, err := ctrl.GetConfig()
func NewContext(cmd *cobra.Command) (controllercontext.Context, error) {
kubeconfig, err := GetKubeConfig(cmd)
if err != nil {
return nil, errors.Wrapf(err, "cannot get kubeconfig")
}
return controllercontext.NewForConfig(kubeconfig, &configv1alpha1.BootstrapConfigSpec{})
}

// PrerunWithKubeconfig is a pre-run function that will set up the common context when kubeconfig is needed.
func PrerunWithKubeconfig(cmd *cobra.Command, _ []string) error {
return SetupCtrlContext(cmd)
}

// SetupCtrlContext sets up the common context.
// TODO(irvinlim): We currently reuse controllercontext, but most of it is unusable for CLI interfaces.
// We should create a new common context as needed.
Expand Down Expand Up @@ -119,33 +111,6 @@ func GetNamespace(cmd *cobra.Command) (string, error) {
return namespace, nil
}

// GetDynamicConfig loads the dynamic config by name and unmarshals to out.
// TODO(irvinlim): If the current user does not have permissions to read the
// ConfigMap, or the ConfigMap uses a different name/namespace, we should
// gracefully handle this case.
func GetDynamicConfig(ctx context.Context, cmd *cobra.Command, name configv1alpha1.ConfigName, out interface{}) error {
cfgNamespace, err := cmd.Flags().GetString("dynamic-config-namespace")
if err != nil {
return err
}
cfgName, err := cmd.Flags().GetString("dynamic-config-name")
if err != nil {
return err
}

klog.V(2).InfoS("fetching dynamic config", "namespace", cfgNamespace, "name", cfgName)
cm, err := ctrlContext.Clientsets().Kubernetes().CoreV1().ConfigMaps(cfgNamespace).
Get(ctx, cfgName, metav1.GetOptions{})
if err != nil {
return errors.Wrapf(err, "cannot load dynamic config")
}

data := cm.Data[string(name)]
klog.V(2).InfoS("fetched dynamic config", "data", data)

return jsonyaml.UnmarshalString(data, out)
}

// GetOutputFormat returns the output format as parsed by the flag.
func GetOutputFormat(cmd *cobra.Command) printer.OutputFormat {
v := GetFlagString(cmd, "output")
Expand All @@ -154,42 +119,6 @@ func GetOutputFormat(cmd *cobra.Command) printer.OutputFormat {
return output
}

// GetFlagBool gets the boolean value of a flag.
func GetFlagBool(cmd *cobra.Command, flag string) bool {
b, err := cmd.Flags().GetBool(flag)
if err != nil {
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
}
return b
}

// GetFlagBoolIfExists gets the boolean value of a flag if it exists.
func GetFlagBoolIfExists(cmd *cobra.Command, flag string) (val bool, ok bool) {
if cmd.Flags().Lookup(flag) != nil {
val := GetFlagBool(cmd, flag)
return val, true
}
return false, false
}

// GetFlagString gets the string value of a flag.
func GetFlagString(cmd *cobra.Command, flag string) string {
v, err := cmd.Flags().GetString(flag)
if err != nil {
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
}
return v
}

// GetFlagInt64 gets the int64 value of a flag.
func GetFlagInt64(cmd *cobra.Command, flag string) int64 {
v, err := cmd.Flags().GetInt64(flag)
if err != nil {
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
}
return v
}

// PrepareExample replaces the root command name and indents all lines.
func PrepareExample(example string) string {
example = strings.TrimPrefix(example, "\n")
Expand Down
112 changes: 112 additions & 0 deletions pkg/cli/common/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,125 @@
package common

import (
"context"
"fmt"
"os"
"os/user"
"path/filepath"
"strings"

"github.com/pkg/errors"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
"k8s.io/klog/v2"

configv1alpha1 "github.com/furiko-io/furiko/apis/config/v1alpha1"
"github.com/furiko-io/furiko/pkg/config"
"github.com/furiko-io/furiko/pkg/utils/jsonyaml"
)

// GetKubeConfig returns the desired kubeconfig.
func GetKubeConfig(cmd *cobra.Command) (*rest.Config, error) {
// Attempt to use in-cluster config if both --kubeconfig and $KUBECONFIG is not set.
// If it cannot be loaded, fall through to the default loader behaviour.
if len(GetFlagString(cmd, "kubeconfig")) == 0 && len(os.Getenv(clientcmd.RecommendedConfigPathEnvVar)) == 0 {
if c, err := rest.InClusterConfig(); err == nil {
klog.V(1).InfoS("successfully loaded in-cluster kubeconfig")
return c, nil
}
}

// Load the kubeconfig in the following order of precedence:
// * --kubeconfig
// * $KUBECONFIG
// * $HOME/.kube/config
clientConfig, err := GetClientConfig(cmd)
if err != nil {
return nil, errors.Wrapf(err, "cannot get client config")
}
return clientConfig.ClientConfig()
}

// GetClientConfig loads the ClientConfig after parsing relevant flags.
func GetClientConfig(cmd *cobra.Command) (clientcmd.ClientConfig, error) {
// Get the --context and --cluster flags.
kubeconfigContext := GetFlagString(cmd, "context")
kubeconfigCluster := GetFlagString(cmd, "cluster")

// Read the --kubeconfig flag.
if kubeconfig := GetFlagString(cmd, "kubeconfig"); kubeconfig != "" {
klog.V(1).InfoS("loading kubeconfig from --kubeconfig",
"path", kubeconfig,
)

loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}
return makeClientConfig(loadingRules, kubeconfigContext, kubeconfigCluster), nil
}

// Load from $KUBECONFIG or other default locations.
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
if _, ok := os.LookupEnv("HOME"); !ok {
u, err := user.Current()
if err != nil {
return nil, fmt.Errorf("could not get current user: %v", err)
}
loadingRules.Precedence = append(loadingRules.Precedence, filepath.Join(u.HomeDir, clientcmd.RecommendedHomeDir, clientcmd.RecommendedFileName))
}

klog.V(1).InfoS("loading default kubeconfig from recommended locations",
"pathPrecedence", strings.Join(loadingRules.Precedence, ":"),
)

return makeClientConfig(loadingRules, kubeconfigContext, kubeconfigCluster), nil
}

func makeClientConfig(loadingRules *clientcmd.ClientConfigLoadingRules, context, cluster string) clientcmd.ClientConfig {
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
loadingRules,
&clientcmd.ConfigOverrides{
CurrentContext: context,
Context: api.Context{
Cluster: cluster,
},
},
)
}

// PrerunWithKubeconfig is a pre-run function that will set up the common context when kubeconfig is needed.
func PrerunWithKubeconfig(cmd *cobra.Command, _ []string) error {
return SetupCtrlContext(cmd)
}

// GetDynamicConfig loads the dynamic config by name and unmarshals to out.
// TODO(irvinlim): If the current user does not have permissions to read the
// ConfigMap, or the ConfigMap uses a different name/namespace, we should
// gracefully handle this case.
func GetDynamicConfig(ctx context.Context, cmd *cobra.Command, name configv1alpha1.ConfigName, out interface{}) error {
cfgNamespace, err := cmd.Flags().GetString("dynamic-config-namespace")
if err != nil {
return err
}
cfgName, err := cmd.Flags().GetString("dynamic-config-name")
if err != nil {
return err
}

klog.V(2).InfoS("fetching dynamic config", "namespace", cfgNamespace, "name", cfgName)
cm, err := ctrlContext.Clientsets().Kubernetes().CoreV1().ConfigMaps(cfgNamespace).
Get(ctx, cfgName, metav1.GetOptions{})
if err != nil {
return errors.Wrapf(err, "cannot load dynamic config")
}

data := cm.Data[string(name)]
klog.V(2).InfoS("fetched dynamic config", "data", data)

return jsonyaml.UnmarshalString(data, out)
}

// GetCronDynamicConfig returns the cron dynamic config.
func GetCronDynamicConfig(cmd *cobra.Command) *configv1alpha1.CronExecutionConfig {
ctx := cmd.Context()
Expand Down
58 changes: 58 additions & 0 deletions pkg/cli/common/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2022 The Furiko 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 common

import (
"github.com/spf13/cobra"
"k8s.io/klog/v2"
)

// GetFlagBool gets the boolean value of a flag.
func GetFlagBool(cmd *cobra.Command, flag string) bool {
b, err := cmd.Flags().GetBool(flag)
if err != nil {
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
}
return b
}

// GetFlagBoolIfExists gets the boolean value of a flag if it exists.
func GetFlagBoolIfExists(cmd *cobra.Command, flag string) (val bool, ok bool) {
if cmd.Flags().Lookup(flag) != nil {
val := GetFlagBool(cmd, flag)
return val, true
}
return false, false
}

// GetFlagString gets the string value of a flag.
func GetFlagString(cmd *cobra.Command, flag string) string {
v, err := cmd.Flags().GetString(flag)
if err != nil {
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
}
return v
}

// GetFlagInt64 gets the int64 value of a flag.
func GetFlagInt64(cmd *cobra.Command, flag string) int64 {
v, err := cmd.Flags().GetInt64(flag)
if err != nil {
klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err)
}
return v
}
Loading

0 comments on commit 32a3ef2

Please sign in to comment.