diff --git a/.github/workflows/audit.yaml b/.github/workflows/audit.yaml index fa92dc2..eac6402 100644 --- a/.github/workflows/audit.yaml +++ b/.github/workflows/audit.yaml @@ -7,7 +7,7 @@ on: jobs: audit: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/cmd/component/action/port_forward.go b/cmd/component/action/port_forward.go index d37f4d3..8f1871b 100644 --- a/cmd/component/action/port_forward.go +++ b/cmd/component/action/port_forward.go @@ -18,8 +18,9 @@ func init() { settings := config.GetSettings() var ( - resourcePath string - podName string + resourcePath string + podName string + overrideClusterServer string ) command := &cobra.Command{ @@ -54,6 +55,10 @@ func init() { portForwardManager.WithPortMappings(portMappings) + if overrideClusterServer != "" { + portForwardManager.WithOverrideClusterServer(overrideClusterServer) + } + environmentResource, err := environment.NewFromWizard(&settings.Profile.Context, resourcePath) if err != nil { return err @@ -61,6 +66,7 @@ func init() { _ = portForwardManager. WithEnvironmentResource(environmentResource). + WithWorkspace(). PrepareKubernetesClient() if podName != "" { @@ -88,6 +94,7 @@ func init() { flags.StringVarP(&resourcePath, "resource", "s", "", "The cluster resource to use (namespace/kind/name format).") flags.StringVar(&podName, "pod", "", "The resource pod to forward ports to.") + flags.StringVar(&overrideClusterServer, "override-kubeconfig-cluster-server", "", "Override kubeconfig cluster server with :port, host:port or scheme://host:port") mainCmd.AddCommand(command) } diff --git a/cmd/component/action/ssh.go b/cmd/component/action/ssh.go index 122f5f2..887f540 100644 --- a/cmd/component/action/ssh.go +++ b/cmd/component/action/ssh.go @@ -26,6 +26,8 @@ type SSHOptions struct { NoTTY bool NoBanner bool + + OverrideClusterServer string } func (o *SSHOptions) UpdateFlagSet(flags *pflag.FlagSet) { @@ -35,6 +37,8 @@ func (o *SSHOptions) UpdateFlagSet(flags *pflag.FlagSet) { flags.BoolVar(&o.NoTTY, "no-tty", o.NoTTY, "Do not allocate a TTY") flags.BoolVar(&o.NoBanner, "no-banner", o.NoBanner, "Do not show environment banner before ssh") + + flags.StringVar(&o.OverrideClusterServer, "override-kubeconfig-cluster-server", o.OverrideClusterServer, "Override kubeconfig cluster server with :port, host:port or scheme://host:port") } func (o *SSHOptions) MakeExecOptions(kubeConfig *environment.KubeConfigItem) *k8sExec.Options { @@ -63,7 +67,8 @@ func init() { settings := config.GetSettings() sshOptions := SSHOptions{ - Shell: "/bin/sh", + Shell: "/bin/sh", + OverrideClusterServer: "", } command := &cobra.Command{ @@ -78,7 +83,7 @@ func init() { return err } - kubeConfigOptions := environment.NewKubeConfigOptions(componentItem.GetEnvironment()) + kubeConfigOptions := environment.NewKubeConfigOptions(componentItem.GetEnvironment(), sshOptions.OverrideClusterServer) kubeConfig, err := environment.KubeConfig(kubeConfigOptions) if err != nil { return err diff --git a/cmd/component_debug/ssh.go b/cmd/component_debug/ssh.go index be2aa9d..2e03224 100644 --- a/cmd/component_debug/ssh.go +++ b/cmd/component_debug/ssh.go @@ -9,6 +9,8 @@ type SSHOptions struct { NoTTY bool NoBanner bool + + OverrideClusterServer string } func (o *SSHOptions) UpdateFlagSet(flags *pflag.FlagSet) { diff --git a/cmd/component_debug/up.go b/cmd/component_debug/up.go index 38286b9..063a6bf 100644 --- a/cmd/component_debug/up.go +++ b/cmd/component_debug/up.go @@ -1,13 +1,13 @@ package component_debug import ( - "fmt" + "fmt" "bunnyshell.com/cli/pkg/config" - "bunnyshell.com/cli/pkg/k8s/bridge" - "bunnyshell.com/cli/pkg/lib" "bunnyshell.com/cli/pkg/debug_component/action" upAction "bunnyshell.com/cli/pkg/debug_component/action/up" + "bunnyshell.com/cli/pkg/k8s/bridge" + "bunnyshell.com/cli/pkg/lib" "github.com/spf13/cobra" ) @@ -16,8 +16,9 @@ func init() { settings := config.GetSettings() sshOptions := SSHOptions{ - Shell: "/bin/sh", - } + Shell: "/bin/sh", + OverrideClusterServer: "", + } resourceLoader := bridge.NewResourceLoader() upOptions := upAction.NewOptions(resourceLoader) @@ -53,14 +54,15 @@ func init() { return err } - selectedContainerName, err := upAction.GetSelectedContainerName() - if err != nil { - return err - } + selectedContainerName, err := upAction.GetSelectedContainerName() + if err != nil { + return err + } - if err = startSSH(*resourceLoader.Component.Id, selectedContainerName, sshOptions, cmd, args); err != nil { - return fmt.Errorf("debug SSH exited with: %s", err) - } + sshOptions.OverrideClusterServer = upParameters.OverrideClusterServer + if err = startSSH(*resourceLoader.Component.Id, selectedContainerName, sshOptions, cmd, args); err != nil { + return fmt.Errorf("debug SSH exited with: %s", err) + } return upAction.Close() }, @@ -81,31 +83,35 @@ func init() { } func startSSH(componentId string, containerName string, sshOptions SSHOptions, cmd *cobra.Command, args []string) error { - proxyArgs := []string{ - "components", "ssh", - "--id", componentId, - } + proxyArgs := []string{ + "components", "ssh", + "--id", componentId, + } - proxyArgs = append(proxyArgs, "--container", containerName) + proxyArgs = append(proxyArgs, "--container", containerName) + + if sshOptions.Shell != "" { + proxyArgs = append(proxyArgs, "--shell", sshOptions.Shell) + } - if sshOptions.Shell != "" { - proxyArgs = append(proxyArgs, "--shell", sshOptions.Shell) - } + if sshOptions.NoBanner { + proxyArgs = append(proxyArgs, "--no-banner") + } - if sshOptions.NoBanner { - proxyArgs = append(proxyArgs, "--no-banner") - } + if sshOptions.NoTTY { + proxyArgs = append(proxyArgs, "--no-tty") + } - if sshOptions.NoTTY { - proxyArgs = append(proxyArgs, "--no-tty") - } + if sshOptions.OverrideClusterServer != "" { + proxyArgs = append(proxyArgs, "--override-kubeconfig-cluster-server", sshOptions.OverrideClusterServer) + } - root := cmd.Root() - root.SetArgs(append(proxyArgs, args...)) + root := cmd.Root() + root.SetArgs(append(proxyArgs, args...)) - if err := root.Execute(); err != nil { - return err - } + if err := root.Execute(); err != nil { + return err + } - return nil + return nil } diff --git a/cmd/template/validate.go b/cmd/template/validate.go index 5d63aa8..9d538bb 100644 --- a/cmd/template/validate.go +++ b/cmd/template/validate.go @@ -150,6 +150,10 @@ func getSource(validateSource ValidateSource, validateOptions template.ValidateO source.SetValidateComponents(true) } + if !validateOptions.AllowExtraFields { + source.SetValidateAllowExtraFields(false) + } + action := sdk.ValidateSourceGitAsTemplateValidateActionSource(source) return &action, nil @@ -172,6 +176,10 @@ func getSource(validateSource ValidateSource, validateOptions template.ValidateO source.SetValidateForOrganizationId(validateOptions.Organization) } + if !validateOptions.AllowExtraFields { + source.SetValidateAllowExtraFields(false) + } + action := sdk.ValidateSourceStringAsTemplateValidateActionSource(source) return &action, nil diff --git a/cmd/utils/port_forward.go b/cmd/utils/port_forward.go index 1815670..f8774ce 100644 --- a/cmd/utils/port_forward.go +++ b/cmd/utils/port_forward.go @@ -13,8 +13,9 @@ func init() { settings := config.GetSettings() var ( - resourcePath string - podName string + resourcePath string + podName string + overrideClusterServer string ) command := &cobra.Command{ @@ -33,6 +34,7 @@ func init() { "--id", settings.Profile.Context.ServiceComponent, "--resource", resourcePath, "--pod", podName, + "--override-kubeconfig-cluster-server", overrideClusterServer, }, portMappings...)) if err := root.Execute(); err != nil { @@ -49,6 +51,7 @@ func init() { flags.StringVarP(&resourcePath, "resource", "s", "", "The cluster resource to use (namespace/kind/name format).") flags.StringVar(&podName, "pod", "", "The resource pod to forward ports to.") + flags.StringVar(&overrideClusterServer, "override-kubeconfig-cluster-server", "", "Override kubeconfig cluster server with :port, host:port or scheme://host:port") mainCmd.AddCommand(command) } diff --git a/cmd/utils/ssh.go b/cmd/utils/ssh.go index d8f2e5d..547ca23 100644 --- a/cmd/utils/ssh.go +++ b/cmd/utils/ssh.go @@ -50,6 +50,10 @@ func init() { proxyArgs = append(proxyArgs, "--no-tty") } + if sshOptions.OverrideClusterServer != "" { + proxyArgs = append(proxyArgs, "--override-kubeconfig-cluster-server", sshOptions.OverrideClusterServer) + } + root := cmd.Root() root.SetArgs(append(proxyArgs, args...)) diff --git a/go.mod b/go.mod index 442c616..736bdc0 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ toolchain go1.23.2 replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16 require ( - bunnyshell.com/dev v0.7.1 - bunnyshell.com/sdk v0.20.1 + bunnyshell.com/dev v0.7.2 + bunnyshell.com/sdk v0.20.3 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/MakeNowJust/heredoc v1.0.0 github.com/avast/retry-go/v4 v4.6.0 diff --git a/go.sum b/go.sum index 854318e..8aefd0c 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -bunnyshell.com/dev v0.7.1 h1:mNLF0h2bbrTi9LNr2e2zFUC2fGg8TUh/9SM5rTXkzDI= -bunnyshell.com/dev v0.7.1/go.mod h1:+Xk46UXX9AW0nHrFMdO/IwpUPfALrck1/qI+LIXsDmE= -bunnyshell.com/sdk v0.20.1 h1:YxmYrW8UXGNNtLe+l6pXFKyndgwyEyQtdX0f5rHV1fE= -bunnyshell.com/sdk v0.20.1/go.mod h1:RfgfUzZ4WHZGCkToUfu2/hoQS6XsQc8IdPTVAlpS138= +bunnyshell.com/dev v0.7.2 h1:fa0ZvnIAXLVJINCJqo7uenjHmjPrlHmY18Zc8ypo/6E= +bunnyshell.com/dev v0.7.2/go.mod h1:+Xk46UXX9AW0nHrFMdO/IwpUPfALrck1/qI+LIXsDmE= +bunnyshell.com/sdk v0.20.3 h1:Kd2s/fhkMrn6Jqp9UmnXfNu12Oiwg+hgNSNBOIBYPH8= +bunnyshell.com/sdk v0.20.3/go.mod h1:RfgfUzZ4WHZGCkToUfu2/hoQS6XsQc8IdPTVAlpS138= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= diff --git a/pkg/api/environment/action_kubeconfig.go b/pkg/api/environment/action_kubeconfig.go index 146f54f..cbbf701 100644 --- a/pkg/api/environment/action_kubeconfig.go +++ b/pkg/api/environment/action_kubeconfig.go @@ -1,8 +1,13 @@ package environment import ( + "bytes" + "fmt" "io" + "net" "net/http" + "net/url" + "strings" "bunnyshell.com/cli/pkg/api" "bunnyshell.com/cli/pkg/api/common" @@ -20,11 +25,14 @@ type KubeConfigItem struct { type KubeConfigOptions struct { common.ItemOptions + + OverrideClusterServer string } -func NewKubeConfigOptions(id string) *KubeConfigOptions { +func NewKubeConfigOptions(id string, overrideClusterServer string) *KubeConfigOptions { return &KubeConfigOptions{ - ItemOptions: *common.NewItemOptions(id), + ItemOptions: *common.NewItemOptions(id), + OverrideClusterServer: overrideClusterServer, } } @@ -34,14 +42,43 @@ func KubeConfig(options *KubeConfigOptions) (*KubeConfigItem, error) { return nil, api.ParseError(resp, err) } - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err + var cfgBytes []byte + if options.OverrideClusterServer != "" { + // normally it would be: + // model.Clusters[0].Cluster.Server, err = overrideServer(model.Clusters[0].Cluster.Server, options.OverrideClusterServer) + // if err != nil { + // return nil, fmt.Errorf("error overriding cluster server: %w", err) + // } + // cfgBytes, err = yaml.Marshal(model) + // if err != nil { + // return nil, fmt.Errorf("error marshaling to YAML: %w", err) + // } + // but model is not unmarshalled correctly, because the struct doesn't have yaml tags, only json tags, + // and the response is in application/x+yaml format (it skips the multi words properties like ApiVersion, CurrentContext) + // se we use the string replace hack + + tmpBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + oldServer := model.Clusters[0].Cluster.Server + model.Clusters[0].Cluster.Server, err = overrideServer(model.Clusters[0].Cluster.Server, options.OverrideClusterServer) + if err != nil { + return nil, fmt.Errorf("error overriding cluster server: %w", err) + } + + cfgBytes = bytes.ReplaceAll(tmpBytes, []byte(oldServer), []byte(model.Clusters[0].Cluster.Server)) + } else { + cfgBytes, err = io.ReadAll(resp.Body) + if err != nil { + return nil, err + } } return &KubeConfigItem{ Data: model, - Bytes: bytes, + Bytes: cfgBytes, }, nil } @@ -55,3 +92,60 @@ func KubeConfigRaw(options *common.ItemOptions) (*sdk.EnvironmentKubeConfigKubeC return request.Execute() } + +// overrideServer takes a base Kubernetes server URL like +// +// "https://my.example.com:6443" +// +// and an override string which can be one of: +// +// "SCHEME://..." → full replacement (any Scheme, host, port, path, etc.) +// ":PORT" → change only the port +// "HOST:PORT" → change host and port +// "HOST" → change only the host +// +// It returns the new URL string or an error. +func overrideServer(base, override string) (string, error) { + // Full-URL override + if strings.Contains(override, "://") { + // we treat anything containing "://" as a complete URL replacement + v, err := url.Parse(override) + if err != nil { + return "", fmt.Errorf("parsing full override %q: %w", override, err) + } + return v.String(), nil + } + + // Parse the base + u, err := url.Parse(base) + if err != nil { + return "", fmt.Errorf("parsing base %q: %w", base, err) + } + + // Extract the existing host and port + host := u.Hostname() + port := u.Port() + + switch { + case strings.HasPrefix(override, ":"): + // override only the port + port = strings.TrimPrefix(override, ":") + + case strings.Contains(override, ":"): + // override both host and port + h, p, err := net.SplitHostPort(override) + if err != nil { + return "", fmt.Errorf("parsing host:port override %q: %w", override, err) + } + host, port = h, p + + default: + // (optional) override only the host, keep existing port + host = override + } + + // Re-join host and port (adds the brackets for IPv6 if needed) + u.Host = net.JoinHostPort(host, port) + + return u.String(), nil +} diff --git a/pkg/api/template/validate.go b/pkg/api/template/validate.go index 32f1293..e7c03ba 100644 --- a/pkg/api/template/validate.go +++ b/pkg/api/template/validate.go @@ -18,10 +18,13 @@ type ValidateOptions struct { Organization string WithComponents bool + + AllowExtraFields bool } func (vo *ValidateOptions) UpdateFlagSet(flags *pflag.FlagSet) { flags.BoolVar(&vo.WithComponents, "with-components", vo.WithComponents, "Validate components along with the template") + flags.BoolVar(&vo.AllowExtraFields, "allow-extra-fields", vo.AllowExtraFields, "Allow extra fields when validating components") } func NewValidateOptions() *ValidateOptions { @@ -30,7 +33,8 @@ func NewValidateOptions() *ValidateOptions { TemplateValidateAction: sdk.TemplateValidateAction{}, - WithComponents: false, + WithComponents: false, + AllowExtraFields: true, } } diff --git a/pkg/debug_component/action/action.go b/pkg/debug_component/action/action.go index 689c90d..b7ba036 100644 --- a/pkg/debug_component/action/action.go +++ b/pkg/debug_component/action/action.go @@ -23,8 +23,8 @@ func NewAction( } } -func (action *Action) GetDebugCmp(resource sdk.ComponentResourceItem) (*debug.DebugComponent, error) { - kubeConfigFile, err := action.workspace.DownloadKubeConfig() +func (action *Action) GetDebugCmp(resource sdk.ComponentResourceItem, overrideClusterServer string) (*debug.DebugComponent, error) { + kubeConfigFile, err := action.workspace.DownloadKubeConfig(overrideClusterServer) if err != nil { return nil, err } diff --git a/pkg/debug_component/action/down.go b/pkg/debug_component/action/down.go index 6ba1607..620fac0 100644 --- a/pkg/debug_component/action/down.go +++ b/pkg/debug_component/action/down.go @@ -6,6 +6,8 @@ import ( type DownParameters struct { Resource sdk.ComponentResourceItem + + OverrideClusterServer string } type Down struct { @@ -21,7 +23,7 @@ func NewDown( } func (down *Down) Run(parameters *DownParameters) error { - debugCmp, err := down.Action.GetDebugCmp(parameters.Resource) + debugCmp, err := down.Action.GetDebugCmp(parameters.Resource, parameters.OverrideClusterServer) if err != nil { return err } diff --git a/pkg/debug_component/action/down/down.cobra.go b/pkg/debug_component/action/down/down.cobra.go index ce06f78..9290c1f 100644 --- a/pkg/debug_component/action/down/down.cobra.go +++ b/pkg/debug_component/action/down/down.cobra.go @@ -10,4 +10,6 @@ func (down *Options) UpdateFlagSet( flags *pflag.FlagSet, ) { flags.StringVarP(&down.resourcePath, "resource", "s", down.resourcePath, "The cluster resource to use (namespace/kind/name format).") + flags.StringVar(&down.overrideClusterServer, "override-kubeconfig-cluster-server", down.overrideClusterServer, "Override kubeconfig cluster server with :port, host:port or scheme://host:port") + } diff --git a/pkg/debug_component/action/down/down.go b/pkg/debug_component/action/down/down.go index 8b1e33e..a8c22c2 100644 --- a/pkg/debug_component/action/down/down.go +++ b/pkg/debug_component/action/down/down.go @@ -1,8 +1,8 @@ package down import ( - "bunnyshell.com/cli/pkg/k8s/bridge" "bunnyshell.com/cli/pkg/debug_component/action" + "bunnyshell.com/cli/pkg/k8s/bridge" ) type Options struct { @@ -11,6 +11,8 @@ type Options struct { resourceLoader *bridge.ResourceLoader resourcePath string + + overrideClusterServer string } func NewOptions( @@ -18,6 +20,8 @@ func NewOptions( ) *Options { return &Options{ resourceLoader: resourceLoader, + + overrideClusterServer: "", } } @@ -29,7 +33,8 @@ func (down *Options) ToParameters() (*action.DownParameters, error) { } parameters := &action.DownParameters{ - Resource: *down.resourceLoader.GetResource(), + Resource: *down.resourceLoader.GetResource(), + OverrideClusterServer: down.overrideClusterServer, } return parameters, nil diff --git a/pkg/debug_component/action/up.go b/pkg/debug_component/action/up.go index 6c1a357..fa53c66 100644 --- a/pkg/debug_component/action/up.go +++ b/pkg/debug_component/action/up.go @@ -13,11 +13,11 @@ type UpOptions struct { Command []string - LimitCPU string - LimitMemory string + LimitCPU string + LimitMemory string - RequestCPU string - RequestMemory string + RequestCPU string + RequestMemory string WaitTimeout int64 } @@ -29,6 +29,8 @@ type UpParameters struct { ForceRecreateResource bool + OverrideClusterServer string + Options *UpOptions } @@ -55,7 +57,7 @@ func NewUp( } func (up *Up) Run(parameters *UpParameters) error { - debugCmp, err := up.Action.GetDebugCmp(parameters.Resource) + debugCmp, err := up.Action.GetDebugCmp(parameters.Resource, parameters.OverrideClusterServer) if err != nil { return err } @@ -93,9 +95,9 @@ func (up *Up) run( return err } - if err := debugCmp.CanUp(parameters.ForceRecreateResource); err != nil { - return err - } + if err := debugCmp.CanUp(parameters.ForceRecreateResource); err != nil { + return err + } up.debugCmp = debugCmp @@ -113,7 +115,7 @@ func (up *Up) loadDebugCmpOptions(debugCmp *debug.DebugComponent, options *UpOpt debugCmp.WithWaitTimeout(options.WaitTimeout) } - up.setContainerResources(&debugCmp.ContainerConfig, options) + up.setContainerResources(&debugCmp.ContainerConfig, options) if len(options.EnvironPairs) > 0 { for _, pair := range options.EnvironPairs { @@ -139,17 +141,17 @@ func (up *Up) setContainerResources(containerConfig *container.Config, options * } } - if options.LimitCPU != "" { - if err := containerConfig.Resources.SetLimitsCPU(options.LimitCPU); err != nil { - return err - } - } + if options.LimitCPU != "" { + if err := containerConfig.Resources.SetLimitsCPU(options.LimitCPU); err != nil { + return err + } + } - if options.LimitMemory != "" { - if err := containerConfig.Resources.SetLimitsMemory(options.LimitMemory); err != nil { - return err - } - } + if options.LimitMemory != "" { + if err := containerConfig.Resources.SetLimitsMemory(options.LimitMemory); err != nil { + return err + } + } return nil } @@ -160,4 +162,4 @@ func (up *Up) GetSelectedContainerName() (string, error) { } return up.debugCmp.GetSelectedContainerName() -} \ No newline at end of file +} diff --git a/pkg/debug_component/action/up/up.cobra.go b/pkg/debug_component/action/up/up.cobra.go index e82f408..2d97f51 100644 --- a/pkg/debug_component/action/up/up.cobra.go +++ b/pkg/debug_component/action/up/up.cobra.go @@ -19,16 +19,17 @@ func (up *Options) UpdateFlagSet( _ = flags.MarkHidden("no-auto-select-one") flags.BoolVar( - &up.ForceRecreateResource, - "force-recreate-resource", - up.ForceRecreateResource, - "Force recreate Pod even if another debug session is in progress. May break the debug down command", - ) + &up.ForceRecreateResource, + "force-recreate-resource", + up.ForceRecreateResource, + "Force recreate Pod even if another debug session is in progress. May break the debug stop command", + ) flags.DurationVarP(&up.waitTimeout, "wait-timeout", "w", up.waitTimeout, "Time to wait for the pod to be ready") + flags.StringVar(&up.overrideClusterServer, "override-kubeconfig-cluster-server", up.overrideClusterServer, "Override kubeconfig cluster server with :port, host:port or scheme://host:port") flags.StringVarP(&up.resourcePath, "resource", "s", up.resourcePath, "The cluster resource to use (namespace/kind/name format).") - flags.StringVar(&up.containerName, "container", up.containerName, "The container name to use for remote development") + flags.StringVar(&up.containerName, "container", up.containerName, "The container name to use for debug session") up.addContainerConfigFlags(flags) } diff --git a/pkg/debug_component/action/up/up.go b/pkg/debug_component/action/up/up.go index 8382a08..541c39d 100644 --- a/pkg/debug_component/action/up/up.go +++ b/pkg/debug_component/action/up/up.go @@ -3,8 +3,8 @@ package up import ( "time" - "bunnyshell.com/cli/pkg/k8s/bridge" "bunnyshell.com/cli/pkg/debug_component/action" + "bunnyshell.com/cli/pkg/k8s/bridge" ) type Options struct { @@ -14,7 +14,8 @@ type Options struct { resourceLoader *bridge.ResourceLoader - waitTimeout time.Duration + waitTimeout time.Duration + overrideClusterServer string resourcePath string containerName string @@ -37,6 +38,8 @@ func NewOptions( resourceLoader: resourceLoader, waitTimeout: defaultWaitTimeout, + + overrideClusterServer: "", } } @@ -53,7 +56,8 @@ func (up *Options) ToParameters() (*action.UpParameters, error) { parameters := &action.UpParameters{ ManualSelectSingleResource: up.ManualSelectSingleResource, - ForceRecreateResource: up.ForceRecreateResource, + ForceRecreateResource: up.ForceRecreateResource, + OverrideClusterServer: up.overrideClusterServer, Options: &action.UpOptions{ WaitTimeout: int64(up.waitTimeout.Seconds()), diff --git a/pkg/formatter/stylish.go b/pkg/formatter/stylish.go index 6e8b9c4..9bc650a 100644 --- a/pkg/formatter/stylish.go +++ b/pkg/formatter/stylish.go @@ -184,11 +184,11 @@ func tabulateBuildSettings(w *tabwriter.Writer, item *sdk.BuildSettingsItem) { } func tabulateEnvironmentCollection(w *tabwriter.Writer, data *sdk.PaginatedEnvironmentCollection) { - fmt.Fprintf(w, "%v\t %v\t %v\t %v\t %v\t %v\n", "EnvironmentID", "ProjectID", "Name", "Namespace", "Type", "OperationStatus") + fmt.Fprintf(w, "%v\t %v\t %v\t %v\t %v\t %v\t %v\n", "EnvironmentID", "ProjectID", "Name", "Namespace", "UrlHandle", "Type", "OperationStatus") if data.Embedded != nil { for _, item := range data.Embedded.Item { - fmt.Fprintf(w, "%v\t %v\t %v\t %v\t %v\t %v\n", item.GetId(), item.GetProject(), item.GetName(), item.GetNamespace(), item.GetType(), item.GetOperationStatus()) + fmt.Fprintf(w, "%v\t %v\t %v\t %v\t %v\t %v\t %v\n", item.GetId(), item.GetProject(), item.GetName(), item.GetNamespace(), item.GetUrlHandle(), item.GetType(), item.GetOperationStatus()) } } } @@ -198,6 +198,7 @@ func tabulateEnvironmentItem(w *tabwriter.Writer, item *sdk.EnvironmentItem) { fmt.Fprintf(w, "%v\t %v\n", "ProjectID", item.GetProject()) fmt.Fprintf(w, "%v\t %v\n", "Name", item.GetName()) fmt.Fprintf(w, "%v\t %v\n", "Namespace", item.GetNamespace()) + fmt.Fprintf(w, "%v\t %v\n", "UrlHandle", item.GetUrlHandle()) fmt.Fprintf(w, "%v\t %v\n", "Type", item.GetType()) fmt.Fprintf(w, "%v\t %v\n", "Components", item.GetTotalComponents()) fmt.Fprintf(w, "%v\t %v\n", "OperationStatus", item.GetOperationStatus()) diff --git a/pkg/port_forward/port_forward_manager.go b/pkg/port_forward/port_forward_manager.go index f3936b2..ade3dbd 100644 --- a/pkg/port_forward/port_forward_manager.go +++ b/pkg/port_forward/port_forward_manager.go @@ -4,22 +4,19 @@ import ( "fmt" "os" "os/signal" - "path/filepath" "strconv" "syscall" "bunnyshell.com/cli/pkg/environment" "bunnyshell.com/cli/pkg/interactive" "bunnyshell.com/cli/pkg/k8s" - "bunnyshell.com/cli/pkg/lib" - "bunnyshell.com/cli/pkg/util" + "bunnyshell.com/cli/pkg/port_forward/workspace" v1 "k8s.io/api/core/v1" "k8s.io/client-go/tools/portforward" ) const ( PortForwardDefaultInterface = "127.0.0.1" - KubeConfigFilename = "kube-config-pfwd.yaml" ) var ( @@ -35,10 +32,10 @@ type PortForwardManager struct { environmentResource *environment.EnvironmentResource pod *v1.Pod - environmentWorkspaceDir string + workspace *workspace.Workspace - kubernetesClient *k8s.KubernetesClient - kubeConfigPath string + kubernetesClient *k8s.KubernetesClient + overrideClusterServer string portForwards []*k8s.PortForward portForwarders []*portforward.PortForwarder @@ -46,7 +43,8 @@ type PortForwardManager struct { func NewPortForwardManager() *PortForwardManager { portForwardManager := &PortForwardManager{ - environmentResource: environment.NewEnvironmentResource(), + environmentResource: environment.NewEnvironmentResource(), + overrideClusterServer: "", } return portForwardManager @@ -58,14 +56,14 @@ func (m *PortForwardManager) WithEnvironmentResource(environmentResource *enviro return m } -func (m *PortForwardManager) WithEnvironmentWorkspaceDir(environmentWorkspaceDir string) *PortForwardManager { - m.environmentWorkspaceDir = environmentWorkspaceDir +func (m *PortForwardManager) WithWorkspace() *PortForwardManager { + m.workspace = workspace.NewWorkspace(m.environmentResource.Environment.GetId()) return m } -func (m *PortForwardManager) WithKubeConfigPath(kubeConfigPath string) *PortForwardManager { - m.kubeConfigPath = kubeConfigPath +func (m *PortForwardManager) WithOverrideClusterServer(overrideClusterServer string) *PortForwardManager { + m.overrideClusterServer = overrideClusterServer return m } @@ -130,28 +128,6 @@ func (m *PortForwardManager) WithPortMappings(portMappings []string) *PortForwar return m } -func (m *PortForwardManager) ensureEnvironmentWorkspaceDir() error { - workspace, err := util.GetWorkspaceDir() - if err != nil { - return err - } - - m.WithEnvironmentWorkspaceDir(filepath.Join(workspace, m.environmentResource.Environment.GetId())) - - return os.MkdirAll(m.environmentWorkspaceDir, 0755) -} - -func (m *PortForwardManager) ensureEnvironmentKubeConfig() error { - kubeConfigPath := filepath.Join(m.environmentWorkspaceDir, KubeConfigFilename) - if err := lib.DownloadEnvironmentKubeConfig(kubeConfigPath, m.environmentResource.Environment.GetId()); err != nil { - return err - } - - m.WithKubeConfigPath(kubeConfigPath) - - return nil -} - func (m *PortForwardManager) SelectPod() error { componentResource := m.environmentResource.ComponentResource @@ -191,15 +167,12 @@ func (m *PortForwardManager) SelectPod() error { } func (m *PortForwardManager) PrepareKubernetesClient() error { - if err := m.ensureEnvironmentWorkspaceDir(); err != nil { - return err - } - - if err := m.ensureEnvironmentKubeConfig(); err != nil { + kubeConfigFile, err := m.workspace.DownloadKubeConfig(m.overrideClusterServer) + if err != nil { return err } - kubernetesClient, err := k8s.NewKubernetesClient(m.kubeConfigPath) + kubernetesClient, err := k8s.NewKubernetesClient(kubeConfigFile) if err != nil { return err } diff --git a/pkg/port_forward/workspace/vars.go b/pkg/port_forward/workspace/vars.go new file mode 100644 index 0000000..8299804 --- /dev/null +++ b/pkg/port_forward/workspace/vars.go @@ -0,0 +1,9 @@ +package workspace + +const ( + workspacePerm = 0o700 + + kubeConfigPerm = 0o600 + + kubeConfigFilename = "kube-config.yaml" +) diff --git a/pkg/port_forward/workspace/workspace.go b/pkg/port_forward/workspace/workspace.go new file mode 100644 index 0000000..f5dea87 --- /dev/null +++ b/pkg/port_forward/workspace/workspace.go @@ -0,0 +1,84 @@ +package workspace + +import ( + "os" + "path/filepath" + + "bunnyshell.com/cli/pkg/api/environment" + "bunnyshell.com/cli/pkg/util" +) + +type Workspace struct { + environment string + + rootDir string + + kubeConfigFile string +} + +func NewWorkspace(environment string) *Workspace { + return &Workspace{ + environment: environment, + } +} + +func (workspace *Workspace) GetEnvironmentDir() (string, error) { + if workspace.rootDir == "" { + dir, err := workspace.makeWorkspace() + if err != nil { + return "", err + } + + workspace.rootDir = dir + } + + return workspace.rootDir, nil +} + +func (workspace *Workspace) GetKubeConfigFile() (string, error) { + if workspace.kubeConfigFile == "" { + envWorkspace, err := workspace.GetEnvironmentDir() + if err != nil { + return "", err + } + + workspace.kubeConfigFile = filepath.Join(envWorkspace, kubeConfigFilename) + } + + return workspace.kubeConfigFile, nil +} + +func (workspace *Workspace) DownloadKubeConfig(overrideClusterServer string) (string, error) { + kubeConfigFile, err := workspace.GetKubeConfigFile() + if err != nil { + return "", err + } + + kubeConfig, err := environment.KubeConfig( + environment.NewKubeConfigOptions(workspace.environment, overrideClusterServer), + ) + if err != nil { + return "", err + } + + if err = os.WriteFile(kubeConfigFile, kubeConfig.Bytes, kubeConfigPerm); err != nil { + return "", err + } + + return kubeConfigFile, nil +} + +func (workspace *Workspace) makeWorkspace() (string, error) { + workspaceRoot, err := util.GetWorkspaceDir() + if err != nil { + return "", err + } + + dir := filepath.Join(workspaceRoot, workspace.environment) + + if err = os.MkdirAll(dir, workspacePerm); err != nil { + return "", err + } + + return dir, nil +} diff --git a/pkg/remote_development/action/action.go b/pkg/remote_development/action/action.go index 918c98c..c63ebab 100644 --- a/pkg/remote_development/action/action.go +++ b/pkg/remote_development/action/action.go @@ -23,8 +23,8 @@ func NewAction( } } -func (action *Action) GetRemoteDev(resource sdk.ComponentResourceItem) (*remote.RemoteDevelopment, error) { - kubeConfigFile, err := action.workspace.DownloadKubeConfig() +func (action *Action) GetRemoteDev(resource sdk.ComponentResourceItem, overrideClusterServer string) (*remote.RemoteDevelopment, error) { + kubeConfigFile, err := action.workspace.DownloadKubeConfig(overrideClusterServer) if err != nil { return nil, err } diff --git a/pkg/remote_development/action/down.go b/pkg/remote_development/action/down.go index c874214..9d0a002 100644 --- a/pkg/remote_development/action/down.go +++ b/pkg/remote_development/action/down.go @@ -6,6 +6,8 @@ import ( type DownParameters struct { Resource sdk.ComponentResourceItem + + OverrideClusterServer string } type Down struct { @@ -21,7 +23,7 @@ func NewDown( } func (down *Down) Run(parameters *DownParameters) error { - remoteDev, err := down.Action.GetRemoteDev(parameters.Resource) + remoteDev, err := down.Action.GetRemoteDev(parameters.Resource, parameters.OverrideClusterServer) if err != nil { return err } diff --git a/pkg/remote_development/action/down/down.cobra.go b/pkg/remote_development/action/down/down.cobra.go index 98ad503..99c636a 100644 --- a/pkg/remote_development/action/down/down.cobra.go +++ b/pkg/remote_development/action/down/down.cobra.go @@ -10,6 +10,7 @@ func (down *Options) UpdateFlagSet( flags *pflag.FlagSet, ) { flags.StringVarP(&down.resourcePath, "resource", "s", down.resourcePath, "The cluster resource to use (namespace/kind/name format).") + flags.StringVar(&down.overrideClusterServer, "override-kubeconfig-cluster-server", down.overrideClusterServer, "Override kubeconfig cluster server with :port, host:port or scheme://host:port") down.manager.UpdateFlagSet(command, flags) } diff --git a/pkg/remote_development/action/down/down.go b/pkg/remote_development/action/down/down.go index f8ce58a..780dc56 100644 --- a/pkg/remote_development/action/down/down.go +++ b/pkg/remote_development/action/down/down.go @@ -14,6 +14,8 @@ type Options struct { resourceLoader *bridge.ResourceLoader resourcePath string + + overrideClusterServer string } func NewOptions( @@ -24,6 +26,8 @@ func NewOptions( manager: manager, resourceLoader: resourceLoader, + + overrideClusterServer: "", } } @@ -35,7 +39,8 @@ func (down *Options) ToParameters() (*action.DownParameters, error) { } parameters := &action.DownParameters{ - Resource: *down.resourceLoader.GetResource(), + Resource: *down.resourceLoader.GetResource(), + OverrideClusterServer: down.overrideClusterServer, } return parameters, nil diff --git a/pkg/remote_development/action/up.go b/pkg/remote_development/action/up.go index 2d268fb..81dc185 100644 --- a/pkg/remote_development/action/up.go +++ b/pkg/remote_development/action/up.go @@ -29,6 +29,10 @@ type UpParameters struct { PortMappings []string + ForceRecreateResource bool + + OverrideClusterServer string + Options *UpOptions } @@ -80,7 +84,7 @@ func NewUp( } func (up *Up) Run(parameters *UpParameters) error { - remoteDev, err := up.Action.GetRemoteDev(parameters.Resource) + remoteDev, err := up.Action.GetRemoteDev(parameters.Resource, parameters.OverrideClusterServer) if err != nil { return err } @@ -125,9 +129,9 @@ func (up *Up) run( return err } - if err := remoteDev.CanUp(); err != nil { - return err - } + if err := remoteDev.CanUp(parameters.ForceRecreateResource); err != nil { + return err + } if err := remoteDev.PrepareSSHTunnels(parameters.PortMappings); err != nil { return err diff --git a/pkg/remote_development/action/up/up.cobra.go b/pkg/remote_development/action/up/up.cobra.go index 2c1a62a..cd9b874 100644 --- a/pkg/remote_development/action/up/up.cobra.go +++ b/pkg/remote_development/action/up/up.cobra.go @@ -23,7 +23,15 @@ func (up *Options) UpdateFlagSet( _ = flags.MarkHidden("no-auto-select-one") + flags.BoolVar( + &up.ForceRecreateResource, + "force-recreate-resource", + up.ForceRecreateResource, + "Force recreate Pod even if another remote-development session is in progress. May break the remote-development down command", + ) + flags.DurationVarP(&up.waitTimeout, "wait-timeout", "w", up.waitTimeout, "Time to wait for the pod to be ready") + flags.StringVar(&up.overrideClusterServer, "override-kubeconfig-cluster-server", up.overrideClusterServer, "Override kubeconfig cluster server with :port, host:port or scheme://host:port") flags.StringVarP( &up.localSyncPath, diff --git a/pkg/remote_development/action/up/up.go b/pkg/remote_development/action/up/up.go index 36a80cf..ecf1533 100644 --- a/pkg/remote_development/action/up/up.go +++ b/pkg/remote_development/action/up/up.go @@ -13,11 +13,14 @@ import ( type Options struct { ManualSelectSingleResource bool + ForceRecreateResource bool + manager *config.Manager resourceLoader *bridge.ResourceLoader - waitTimeout time.Duration + waitTimeout time.Duration + overrideClusterServer string resourcePath string containerName string @@ -51,6 +54,8 @@ func NewOptions( waitTimeout: defaultWaitTimeout, + overrideClusterServer: "", + syncMode: TwoWayResolved, portMappings: []string{}, @@ -77,6 +82,8 @@ func (up *Options) ToParameters() (*action.UpParameters, error) { ManualSelectSingleResource: up.ManualSelectSingleResource, + ForceRecreateResource: up.ForceRecreateResource, + PortMappings: up.portMappings, Options: &action.UpOptions{ @@ -160,6 +167,10 @@ func (up *Options) makeAbsolutePaths(parameters *action.UpParameters) error { } func (up *Options) fillFromFlags(parameters *action.UpParameters) { + if up.overrideClusterServer != "" { + parameters.OverrideClusterServer = up.overrideClusterServer + } + if up.localSyncPath != "" { ensureProfileSyncPath(parameters).SetLocalPath(up.localSyncPath) } diff --git a/pkg/remote_development/workspace/workspace.go b/pkg/remote_development/workspace/workspace.go index 2c03132..f5dea87 100644 --- a/pkg/remote_development/workspace/workspace.go +++ b/pkg/remote_development/workspace/workspace.go @@ -48,14 +48,14 @@ func (workspace *Workspace) GetKubeConfigFile() (string, error) { return workspace.kubeConfigFile, nil } -func (workspace *Workspace) DownloadKubeConfig() (string, error) { +func (workspace *Workspace) DownloadKubeConfig(overrideClusterServer string) (string, error) { kubeConfigFile, err := workspace.GetKubeConfigFile() if err != nil { return "", err } kubeConfig, err := environment.KubeConfig( - environment.NewKubeConfigOptions(workspace.environment), + environment.NewKubeConfigOptions(workspace.environment, overrideClusterServer), ) if err != nil { return "", err