From 35a391dfbe7944c845333ed2f8d0142ecb1fb19e Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Fri, 26 Sep 2025 15:13:38 -0400 Subject: [PATCH 1/6] Add proxies feature --- cmd/beta.go | 23 +++++ cmd/browsers.go | 9 ++ cmd/invoke.go | 2 +- cmd/proxies/create.go | 210 +++++++++++++++++++++++++++++++++++++++++ cmd/proxies/delete.go | 64 +++++++++++++ cmd/proxies/get.go | 120 +++++++++++++++++++++++ cmd/proxies/helpers.go | 25 +++++ cmd/proxies/list.go | 110 +++++++++++++++++++++ cmd/proxies/proxies.go | 95 +++++++++++++++++++ cmd/proxies/types.go | 53 +++++++++++ cmd/root.go | 1 + go.mod | 2 +- go.sum | 6 +- 13 files changed, 714 insertions(+), 6 deletions(-) create mode 100644 cmd/beta.go create mode 100644 cmd/proxies/create.go create mode 100644 cmd/proxies/delete.go create mode 100644 cmd/proxies/get.go create mode 100644 cmd/proxies/helpers.go create mode 100644 cmd/proxies/list.go create mode 100644 cmd/proxies/proxies.go create mode 100644 cmd/proxies/types.go diff --git a/cmd/beta.go b/cmd/beta.go new file mode 100644 index 0000000..edc0439 --- /dev/null +++ b/cmd/beta.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/onkernel/cli/cmd/proxies" + "github.com/spf13/cobra" +) + +// betaCmd is the parent command for experimental features +var betaCmd = &cobra.Command{ + Use: "beta", + Short: "Experimental features (subject to change)", + Long: `The beta command provides access to experimental features that are still under development. +These features may change, break, or be removed in future versions without notice.`, + Run: func(cmd *cobra.Command, args []string) { + // If called without subcommands, show help + _ = cmd.Help() + }, +} + +func init() { + // Add proxy commands under beta + betaCmd.AddCommand(proxies.ProxiesCmd) +} diff --git a/cmd/browsers.go b/cmd/browsers.go index 49605d7..fd0152f 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -82,6 +82,7 @@ type BrowsersCreateInput struct { ProfileID string ProfileName string ProfileSaveChanges BoolFlag + ProxyID string } type BrowsersDeleteInput struct { @@ -178,6 +179,11 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { } } + // Add proxy if specified + if in.ProxyID != "" { + params.ProxyID = kernel.Opt(in.ProxyID) + } + browser, err := b.browsers.New(ctx, params) if err != nil { return util.CleanedUpSdkError{Err: err} @@ -1321,6 +1327,7 @@ func init() { browsersCreateCmd.Flags().String("profile-id", "", "Profile ID to load into the browser session (mutually exclusive with --profile-name)") browsersCreateCmd.Flags().String("profile-name", "", "Profile name to load into the browser session (mutually exclusive with --profile-id)") browsersCreateCmd.Flags().Bool("save-changes", false, "If set, save changes back to the profile when the session ends") + browsersCreateCmd.Flags().String("proxy-id", "", "Proxy ID to use for the browser session") // Add flags for delete command browsersDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") @@ -1346,6 +1353,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { profileID, _ := cmd.Flags().GetString("profile-id") profileName, _ := cmd.Flags().GetString("profile-name") saveChanges, _ := cmd.Flags().GetBool("save-changes") + proxyID, _ := cmd.Flags().GetString("proxy-id") in := BrowsersCreateInput{ PersistenceID: persistenceID, @@ -1355,6 +1363,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { ProfileID: profileID, ProfileName: profileName, ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, + ProxyID: proxyID, } svc := client.Browsers diff --git a/cmd/invoke.go b/cmd/invoke.go index 17cd032..e73c95e 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -117,7 +117,7 @@ func runInvoke(cmd *cobra.Command, args []string) error { }) // Start following events - stream := client.Invocations.FollowStreaming(cmd.Context(), resp.ID, option.WithMaxRetries(0)) + stream := client.Invocations.FollowStreaming(cmd.Context(), resp.ID, kernel.InvocationFollowParams{}, option.WithMaxRetries(0)) for stream.Next() { ev := stream.Current() diff --git a/cmd/proxies/create.go b/cmd/proxies/create.go new file mode 100644 index 0000000..24085d2 --- /dev/null +++ b/cmd/proxies/create.go @@ -0,0 +1,210 @@ +package proxies + +import ( + "context" + "fmt" + + "github.com/onkernel/cli/pkg/util" + "github.com/onkernel/kernel-go-sdk" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { + // Validate proxy type + var proxyType kernel.ProxyNewParamsType + switch in.Type { + case "datacenter": + proxyType = kernel.ProxyNewParamsTypeDatacenter + case "isp": + proxyType = kernel.ProxyNewParamsTypeIsp + case "residential": + proxyType = kernel.ProxyNewParamsTypeResidential + case "mobile": + proxyType = kernel.ProxyNewParamsTypeMobile + case "custom": + proxyType = kernel.ProxyNewParamsTypeCustom + default: + return fmt.Errorf("invalid proxy type: %s", in.Type) + } + + params := kernel.ProxyNewParams{ + Type: proxyType, + } + + if in.Name != "" { + params.Name = kernel.Opt(in.Name) + } + + // Build config based on type + switch proxyType { + case kernel.ProxyNewParamsTypeDatacenter: + if in.Country == "" { + return fmt.Errorf("--country is required for datacenter proxy type") + } + params.Config = kernel.ProxyNewParamsConfigUnion{ + OfProxyNewsConfigDatacenterProxyConfig: &kernel.ProxyNewParamsConfigDatacenterProxyConfig{ + Country: in.Country, + }, + } + + case kernel.ProxyNewParamsTypeIsp: + if in.Country == "" { + return fmt.Errorf("--country is required for ISP proxy type") + } + params.Config = kernel.ProxyNewParamsConfigUnion{ + OfProxyNewsConfigIspProxyConfig: &kernel.ProxyNewParamsConfigIspProxyConfig{ + Country: in.Country, + }, + } + + case kernel.ProxyNewParamsTypeResidential: + config := kernel.ProxyNewParamsConfigResidentialProxyConfig{} + + // Validate that if city is provided, country must also be provided + if in.City != "" && in.Country == "" { + return fmt.Errorf("--country is required when --city is specified") + } + + if in.Country != "" { + config.Country = kernel.Opt(in.Country) + } + if in.City != "" { + config.City = kernel.Opt(in.City) + } + if in.State != "" { + config.State = kernel.Opt(in.State) + } + if in.Zip != "" { + config.Zip = kernel.Opt(in.Zip) + } + if in.ASN != "" { + config.Asn = kernel.Opt(in.ASN) + } + if in.OS != "" { + // Validate OS value + switch in.OS { + case "windows", "macos", "android": + config.Os = in.OS + default: + return fmt.Errorf("invalid OS value: %s (must be windows, macos, or android)", in.OS) + } + } + params.Config = kernel.ProxyNewParamsConfigUnion{ + OfProxyNewsConfigResidentialProxyConfig: &config, + } + + case kernel.ProxyNewParamsTypeMobile: + config := kernel.ProxyNewParamsConfigMobileProxyConfig{} + + // Validate that if city is provided, country must also be provided + if in.City != "" && in.Country == "" { + return fmt.Errorf("--country is required when --city is specified") + } + + if in.Country != "" { + config.Country = kernel.Opt(in.Country) + } + if in.City != "" { + config.City = kernel.Opt(in.City) + } + if in.State != "" { + config.State = kernel.Opt(in.State) + } + if in.Zip != "" { + config.Zip = kernel.Opt(in.Zip) + } + if in.ASN != "" { + config.Asn = kernel.Opt(in.ASN) + } + if in.Carrier != "" { + // The API will validate the carrier value + config.Carrier = in.Carrier + } + params.Config = kernel.ProxyNewParamsConfigUnion{ + OfProxyNewsConfigMobileProxyConfig: &config, + } + + case kernel.ProxyNewParamsTypeCustom: + if in.Host == "" { + return fmt.Errorf("--host is required for custom proxy type") + } + if in.Port == 0 { + return fmt.Errorf("--port is required for custom proxy type") + } + + config := kernel.ProxyNewParamsConfigCreateCustomProxyConfig{ + Host: in.Host, + Port: int64(in.Port), + } + if in.Username != "" { + config.Username = kernel.Opt(in.Username) + } + if in.Password != "" { + config.Password = kernel.Opt(in.Password) + } + params.Config = kernel.ProxyNewParamsConfigUnion{ + OfProxyNewsConfigCreateCustomProxyConfig: &config, + } + } + + pterm.Info.Printf("Creating %s proxy...\n", proxyType) + + proxy, err := p.proxies.New(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + pterm.Success.Printf("Successfully created proxy\n") + + // Display created proxy details + rows := pterm.TableData{{"Property", "Value"}} + rows = append(rows, []string{"ID", proxy.ID}) + + name := proxy.Name + if name == "" { + name = "-" + } + rows = append(rows, []string{"Name", name}) + rows = append(rows, []string{"Type", string(proxy.Type)}) + + PrintTableNoPad(rows, true) + return nil +} + +func runProxiesCreate(cmd *cobra.Command, args []string) error { + client := GetKernelClient(cmd) + + // Get all flag values + proxyType, _ := cmd.Flags().GetString("type") + name, _ := cmd.Flags().GetString("name") + country, _ := cmd.Flags().GetString("country") + city, _ := cmd.Flags().GetString("city") + state, _ := cmd.Flags().GetString("state") + zip, _ := cmd.Flags().GetString("zip") + asn, _ := cmd.Flags().GetString("asn") + os, _ := cmd.Flags().GetString("os") + carrier, _ := cmd.Flags().GetString("carrier") + host, _ := cmd.Flags().GetString("host") + port, _ := cmd.Flags().GetInt("port") + username, _ := cmd.Flags().GetString("username") + password, _ := cmd.Flags().GetString("password") + + svc := client.Proxies + p := ProxyCmd{proxies: &svc} + return p.Create(cmd.Context(), ProxyCreateInput{ + Name: name, + Type: proxyType, + Country: country, + City: city, + State: state, + Zip: zip, + ASN: asn, + OS: os, + Carrier: carrier, + Host: host, + Port: port, + Username: username, + Password: password, + }) +} diff --git a/cmd/proxies/delete.go b/cmd/proxies/delete.go new file mode 100644 index 0000000..66a7b15 --- /dev/null +++ b/cmd/proxies/delete.go @@ -0,0 +1,64 @@ +package proxies + +import ( + "context" + "fmt" + + "github.com/onkernel/cli/pkg/util" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +func (p ProxyCmd) Delete(ctx context.Context, in ProxyDeleteInput) error { + if !in.SkipConfirm { + // Try to get the proxy details for better confirmation message + proxy, err := p.proxies.Get(ctx, in.ID) + if err != nil { + // If we can't get the proxy, just use the ID + if !util.IsNotFound(err) { + return util.CleanedUpSdkError{Err: err} + } + proxy = nil + } + + var confirmMsg string + if proxy != nil && proxy.Name != "" { + confirmMsg = fmt.Sprintf("Are you sure you want to delete proxy '%s' (ID: %s)?", proxy.Name, in.ID) + } else { + confirmMsg = fmt.Sprintf("Are you sure you want to delete proxy '%s'?", in.ID) + } + + pterm.DefaultInteractiveConfirm.DefaultText = confirmMsg + result, _ := pterm.DefaultInteractiveConfirm.Show() + if !result { + pterm.Info.Println("Deletion cancelled") + return nil + } + } + + pterm.Info.Printf("Deleting proxy: %s\n", in.ID) + + err := p.proxies.Delete(ctx, in.ID) + if err != nil { + if util.IsNotFound(err) { + pterm.Warning.Printf("Proxy '%s' not found\n", in.ID) + return nil + } + return util.CleanedUpSdkError{Err: err} + } + + pterm.Success.Printf("Successfully deleted proxy: %s\n", in.ID) + return nil +} + +func runProxiesDelete(cmd *cobra.Command, args []string) error { + client := GetKernelClient(cmd) + skipConfirm, _ := cmd.Flags().GetBool("yes") + + svc := client.Proxies + p := ProxyCmd{proxies: &svc} + return p.Delete(cmd.Context(), ProxyDeleteInput{ + ID: args[0], + SkipConfirm: skipConfirm, + }) +} diff --git a/cmd/proxies/get.go b/cmd/proxies/get.go new file mode 100644 index 0000000..8cde89d --- /dev/null +++ b/cmd/proxies/get.go @@ -0,0 +1,120 @@ +package proxies + +import ( + "context" + "fmt" + + "github.com/onkernel/cli/pkg/util" + "github.com/onkernel/kernel-go-sdk" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +func (p ProxyCmd) Get(ctx context.Context, in ProxyGetInput) error { + item, err := p.proxies.Get(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + // Display proxy details + rows := pterm.TableData{{"Property", "Value"}} + + rows = append(rows, []string{"ID", item.ID}) + + name := item.Name + if name == "" { + name = "-" + } + rows = append(rows, []string{"Name", name}) + rows = append(rows, []string{"Type", string(item.Type)}) + + // Display type-specific config details + rows = append(rows, getProxyConfigRows(item)...) + + PrintTableNoPad(rows, true) + return nil +} + +func getProxyConfigRows(proxy *kernel.ProxyGetResponse) [][]string { + var rows [][]string + + switch proxy.Type { + case kernel.ProxyGetResponseTypeDatacenter: + dc := proxy.Config.AsProxyGetResponseConfigDatacenterProxyConfig() + if dc.Country != "" { + rows = append(rows, []string{"Country", dc.Country}) + } + case kernel.ProxyGetResponseTypeIsp: + isp := proxy.Config.AsProxyGetResponseConfigIspProxyConfig() + if isp.Country != "" { + rows = append(rows, []string{"Country", isp.Country}) + } + case kernel.ProxyGetResponseTypeResidential: + res := proxy.Config.AsProxyGetResponseConfigResidentialProxyConfig() + if res.Country != "" || res.City != "" || res.State != "" || res.Zip != "" || res.Asn != "" || res.Os != "" { + if res.Country != "" { + rows = append(rows, []string{"Country", res.Country}) + } + if res.City != "" { + rows = append(rows, []string{"City", res.City}) + } + if res.State != "" { + rows = append(rows, []string{"State", res.State}) + } + if res.Zip != "" { + rows = append(rows, []string{"ZIP", res.Zip}) + } + if res.Asn != "" { + rows = append(rows, []string{"ASN", res.Asn}) + } + if res.Os != "" { + rows = append(rows, []string{"OS", res.Os}) + } + } + case kernel.ProxyGetResponseTypeMobile: + mob := proxy.Config.AsProxyGetResponseConfigMobileProxyConfig() + if mob.Country != "" || mob.City != "" || mob.State != "" || mob.Zip != "" || mob.Asn != "" || mob.Carrier != "" { + if mob.Country != "" { + rows = append(rows, []string{"Country", mob.Country}) + } + if mob.City != "" { + rows = append(rows, []string{"City", mob.City}) + } + if mob.State != "" { + rows = append(rows, []string{"State", mob.State}) + } + if mob.Zip != "" { + rows = append(rows, []string{"ZIP", mob.Zip}) + } + if mob.Asn != "" { + rows = append(rows, []string{"ASN", mob.Asn}) + } + if mob.Carrier != "" { + rows = append(rows, []string{"Carrier", mob.Carrier}) + } + } + case kernel.ProxyGetResponseTypeCustom: + custom := proxy.Config.AsProxyGetResponseConfigCustomProxyConfig() + if custom.Host != "" { + rows = append(rows, []string{"Host", custom.Host}) + rows = append(rows, []string{"Port", fmt.Sprintf("%d", custom.Port)}) + if custom.Username != "" { + rows = append(rows, []string{"Username", custom.Username}) + } + hasPassword := "No" + if custom.HasPassword { + hasPassword = "Yes" + } + rows = append(rows, []string{"Has Password", hasPassword}) + } + } + + return rows +} + +func runProxiesGet(cmd *cobra.Command, args []string) error { + client := GetKernelClient(cmd) + svc := client.Proxies + p := ProxyCmd{proxies: &svc} + return p.Get(cmd.Context(), ProxyGetInput{ID: args[0]}) +} diff --git a/cmd/proxies/helpers.go b/cmd/proxies/helpers.go new file mode 100644 index 0000000..c4bc98e --- /dev/null +++ b/cmd/proxies/helpers.go @@ -0,0 +1,25 @@ +package proxies + +import ( + "github.com/onkernel/kernel-go-sdk" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type contextKey string + +const KernelClientKey contextKey = "kernel_client" + +// GetKernelClient retrieves the kernel client from the command context +func GetKernelClient(cmd *cobra.Command) kernel.Client { + return cmd.Context().Value(KernelClientKey).(kernel.Client) +} + +// PrintTableNoPad prints a table without padding (delegating to cmd package) +func PrintTableNoPad(data pterm.TableData, withRowSeparators bool) { + table := pterm.DefaultTable.WithHasHeader().WithData(data) + if withRowSeparators { + table = table.WithRowSeparator("-") + } + _ = pterm.DefaultTable.WithHasHeader().WithData(data).WithRowSeparator("-").Render() +} diff --git a/cmd/proxies/list.go b/cmd/proxies/list.go new file mode 100644 index 0000000..f059257 --- /dev/null +++ b/cmd/proxies/list.go @@ -0,0 +1,110 @@ +package proxies + +import ( + "context" + "fmt" + "strings" + + "github.com/onkernel/cli/pkg/util" + "github.com/onkernel/kernel-go-sdk" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +func (p ProxyCmd) List(ctx context.Context) error { + pterm.Info.Println("Fetching proxy configurations...") + + items, err := p.proxies.List(ctx) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if items == nil || len(*items) == 0 { + pterm.Info.Println("No proxy configurations found") + return nil + } + + // Prepare table data + tableData := pterm.TableData{ + {"ID", "Name", "Type", "Config"}, + } + + for _, proxy := range *items { + name := proxy.Name + if name == "" { + name = "-" + } + + // Format config based on type + configStr := formatProxyConfig(&proxy) + + tableData = append(tableData, []string{ + proxy.ID, + name, + string(proxy.Type), + configStr, + }) + } + + PrintTableNoPad(tableData, true) + return nil +} + +func formatProxyConfig(proxy *kernel.ProxyListResponse) string { + switch proxy.Type { + case kernel.ProxyListResponseTypeDatacenter: + dc := proxy.Config.AsProxyListResponseConfigDatacenterProxyConfig() + if dc.Country != "" { + return fmt.Sprintf("Country: %s", dc.Country) + } + case kernel.ProxyListResponseTypeIsp: + isp := proxy.Config.AsProxyListResponseConfigIspProxyConfig() + if isp.Country != "" { + return fmt.Sprintf("Country: %s", isp.Country) + } + case kernel.ProxyListResponseTypeResidential: + res := proxy.Config.AsProxyListResponseConfigResidentialProxyConfig() + if res.Country != "" || res.City != "" || res.State != "" { + parts := []string{} + if res.Country != "" { + parts = append(parts, fmt.Sprintf("Country: %s", res.Country)) + } + if res.City != "" { + parts = append(parts, fmt.Sprintf("City: %s", res.City)) + } + if res.State != "" { + parts = append(parts, fmt.Sprintf("State: %s", res.State)) + } + return strings.Join(parts, ", ") + } + case kernel.ProxyListResponseTypeMobile: + mob := proxy.Config.AsProxyListResponseConfigMobileProxyConfig() + if mob.Country != "" || mob.Carrier != "" { + parts := []string{} + if mob.Country != "" { + parts = append(parts, fmt.Sprintf("Country: %s", mob.Country)) + } + if mob.Carrier != "" { + parts = append(parts, fmt.Sprintf("Carrier: %s", mob.Carrier)) + } + return strings.Join(parts, ", ") + } + case kernel.ProxyListResponseTypeCustom: + custom := proxy.Config.AsProxyListResponseConfigCustomProxyConfig() + if custom.Host != "" { + auth := "" + if custom.Username != "" { + auth = fmt.Sprintf(", Auth: %s", custom.Username) + } + return fmt.Sprintf("%s:%d%s", custom.Host, custom.Port, auth) + } + } + return "-" +} + +func runProxiesList(cmd *cobra.Command, args []string) error { + client := GetKernelClient(cmd) + svc := client.Proxies + p := ProxyCmd{proxies: &svc} + return p.List(cmd.Context()) +} diff --git a/cmd/proxies/proxies.go b/cmd/proxies/proxies.go new file mode 100644 index 0000000..e15b5ee --- /dev/null +++ b/cmd/proxies/proxies.go @@ -0,0 +1,95 @@ +package proxies + +import ( + "github.com/spf13/cobra" +) + +// ProxiesCmd is the parent command for proxy operations +var ProxiesCmd = &cobra.Command{ + Use: "proxies", + Short: "Manage proxy configurations", + Long: "Commands for managing proxy configurations for browser sessions", + Run: func(cmd *cobra.Command, args []string) { + // If called without subcommands, show help + _ = cmd.Help() + }, +} + +var proxiesListCmd = &cobra.Command{ + Use: "list", + Short: "List proxy configurations", + RunE: runProxiesList, +} + +var proxiesGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get proxy configuration by ID", + Args: cobra.ExactArgs(1), + RunE: runProxiesGet, +} + +var proxiesCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new proxy configuration", + Long: `Create a new proxy configuration for browser sessions. + +Proxy types (from best to worst for bot detection): +- mobile: Mobile carrier proxies +- residential: Residential IP proxies +- isp: ISP proxies +- datacenter: Datacenter proxies +- custom: Your own proxy server + +Examples: + # Create a datacenter proxy + kernel beta proxies create --type datacenter --country US --name "US Datacenter" + + # Create a custom proxy + kernel beta proxies create --type custom --host proxy.example.com --port 8080 --username myuser --password mypass + + # Create a residential proxy with location + kernel beta proxies create --type residential --country US --city sanfrancisco --state CA`, + RunE: runProxiesCreate, +} + +var proxiesDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a proxy configuration", + Args: cobra.ExactArgs(1), + RunE: runProxiesDelete, +} + +func init() { + // Add subcommands + ProxiesCmd.AddCommand(proxiesListCmd) + ProxiesCmd.AddCommand(proxiesGetCmd) + ProxiesCmd.AddCommand(proxiesCreateCmd) + ProxiesCmd.AddCommand(proxiesDeleteCmd) + + // Add flags for create command + proxiesCreateCmd.Flags().String("name", "", "Proxy configuration name") + proxiesCreateCmd.Flags().String("type", "", "Proxy type (datacenter|isp|residential|mobile|custom)") + _ = proxiesCreateCmd.MarkFlagRequired("type") + + // Location flags (datacenter, isp, residential, mobile) + proxiesCreateCmd.Flags().String("country", "", "ISO 3166 country code or EU") + proxiesCreateCmd.Flags().String("city", "", "City name (no spaces, e.g. sanfrancisco)") + proxiesCreateCmd.Flags().String("state", "", "Two-letter state code") + proxiesCreateCmd.Flags().String("zip", "", "US ZIP code") + proxiesCreateCmd.Flags().String("asn", "", "Autonomous system number (e.g. AS15169)") + + // OS flag (residential) + proxiesCreateCmd.Flags().String("os", "", "Operating system (windows|macos|android)") + + // Carrier flag (mobile) + proxiesCreateCmd.Flags().String("carrier", "", "Mobile carrier (see help for full list)") + + // Custom proxy flags + proxiesCreateCmd.Flags().String("host", "", "Proxy host address or IP") + proxiesCreateCmd.Flags().Int("port", 0, "Proxy port") + proxiesCreateCmd.Flags().String("username", "", "Username for proxy authentication") + proxiesCreateCmd.Flags().String("password", "", "Password for proxy authentication") + + // Delete flags + proxiesDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") +} diff --git a/cmd/proxies/types.go b/cmd/proxies/types.go new file mode 100644 index 0000000..eb792fc --- /dev/null +++ b/cmd/proxies/types.go @@ -0,0 +1,53 @@ +package proxies + +import ( + "context" + + "github.com/onkernel/kernel-go-sdk" + "github.com/onkernel/kernel-go-sdk/option" +) + +// ProxyService defines the subset of the Kernel SDK proxy client that we use. +type ProxyService interface { + List(ctx context.Context, opts ...option.RequestOption) (res *[]kernel.ProxyListResponse, err error) + Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.ProxyGetResponse, err error) + New(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (res *kernel.ProxyNewResponse, err error) + Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) +} + +// ProxyCmd handles proxy operations independent of cobra. +type ProxyCmd struct { + proxies ProxyService +} + +// Input types for proxy operations +type ProxyListInput struct{} + +type ProxyGetInput struct { + ID string +} + +type ProxyCreateInput struct { + Name string + Type string + // Datacenter/ISP config + Country string + // Residential/Mobile config + City string + State string + Zip string + ASN string + OS string + // Mobile specific + Carrier string + // Custom proxy config + Host string + Port int + Username string + Password string +} + +type ProxyDeleteInput struct { + ID string + SkipConfirm bool +} diff --git a/cmd/root.go b/cmd/root.go index a3ac908..b19291d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -127,6 +127,7 @@ func init() { rootCmd.AddCommand(browsersCmd) rootCmd.AddCommand(appCmd) rootCmd.AddCommand(profilesCmd) + rootCmd.AddCommand(betaCmd) rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { // running synchronously so we never slow the command diff --git a/go.mod b/go.mod index 3032472..55f133c 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/charmbracelet/fang v0.2.0 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/onkernel/kernel-go-sdk v0.11.1 + github.com/onkernel/kernel-go-sdk v0.11.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index f9360bc..9ba756e 100644 --- a/go.sum +++ b/go.sum @@ -91,10 +91,8 @@ github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= -github.com/onkernel/kernel-go-sdk v0.11.0 h1:7KUKHiz5t4jdnNCwA8NM1dTtEYdk/AV/RIe8T/HjJwg= -github.com/onkernel/kernel-go-sdk v0.11.0/go.mod h1:q7wsAf+yjpY+w8jbAMciWCtCM0ZUxiw/5o2MSPTZS9E= -github.com/onkernel/kernel-go-sdk v0.11.1 h1:gTxhXtsXrJcrM7KEobEVXa8mPPtRFMlxQwNqkyoCrDI= -github.com/onkernel/kernel-go-sdk v0.11.1/go.mod h1:q7wsAf+yjpY+w8jbAMciWCtCM0ZUxiw/5o2MSPTZS9E= +github.com/onkernel/kernel-go-sdk v0.11.4 h1:vgDcPtldfEcRh+a1wlOSOY2bBWjxLFUwHqeXHHQ4OjM= +github.com/onkernel/kernel-go-sdk v0.11.4/go.mod h1:MjUR92i8UPqjrmneyVykae6GuB3GGSmnQtnjf1v74Dc= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= From 69d54e05e8b8ae65e646c8a38e9659020c7cbbdc Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Fri, 26 Sep 2025 15:33:03 -0400 Subject: [PATCH 2/6] Add tests --- cmd/proxies/common_test.go | 110 ++++++++++++++ cmd/proxies/create_test.go | 292 +++++++++++++++++++++++++++++++++++++ cmd/proxies/delete_test.go | 75 ++++++++++ cmd/proxies/get.go | 112 +++++++------- cmd/proxies/get_test.go | 206 ++++++++++++++++++++++++++ cmd/proxies/list.go | 62 ++++---- cmd/proxies/list_test.go | 115 +++++++++++++++ 7 files changed, 876 insertions(+), 96 deletions(-) create mode 100644 cmd/proxies/common_test.go create mode 100644 cmd/proxies/create_test.go create mode 100644 cmd/proxies/delete_test.go create mode 100644 cmd/proxies/get_test.go create mode 100644 cmd/proxies/list_test.go diff --git a/cmd/proxies/common_test.go b/cmd/proxies/common_test.go new file mode 100644 index 0000000..0e4d607 --- /dev/null +++ b/cmd/proxies/common_test.go @@ -0,0 +1,110 @@ +package proxies + +import ( + "bytes" + "context" + "os" + "testing" + + "github.com/onkernel/kernel-go-sdk" + "github.com/onkernel/kernel-go-sdk/option" + "github.com/pterm/pterm" +) + +// captureOutput sets pterm writers for tests +func captureOutput(t *testing.T) *bytes.Buffer { + var buf bytes.Buffer + pterm.SetDefaultOutput(&buf) + pterm.Info.Writer = &buf + pterm.Error.Writer = &buf + pterm.Success.Writer = &buf + pterm.Warning.Writer = &buf + pterm.Debug.Writer = &buf + pterm.Fatal.Writer = &buf + pterm.DefaultTable = *pterm.DefaultTable.WithWriter(&buf) + t.Cleanup(func() { + pterm.SetDefaultOutput(os.Stdout) + pterm.Info.Writer = os.Stdout + pterm.Error.Writer = os.Stdout + pterm.Success.Writer = os.Stdout + pterm.Warning.Writer = os.Stdout + pterm.Debug.Writer = os.Stdout + pterm.Fatal.Writer = os.Stdout + pterm.DefaultTable = *pterm.DefaultTable.WithWriter(os.Stdout) + }) + return &buf +} + +// FakeProxyService implements ProxyService for testing +type FakeProxyService struct { + ListFunc func(ctx context.Context, opts ...option.RequestOption) (*[]kernel.ProxyListResponse, error) + GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) + NewFunc func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) + DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error +} + +func (f *FakeProxyService) List(ctx context.Context, opts ...option.RequestOption) (*[]kernel.ProxyListResponse, error) { + if f.ListFunc != nil { + return f.ListFunc(ctx, opts...) + } + empty := []kernel.ProxyListResponse{} + return &empty, nil +} + +func (f *FakeProxyService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) { + if f.GetFunc != nil { + return f.GetFunc(ctx, id, opts...) + } + return &kernel.ProxyGetResponse{ID: id, Type: kernel.ProxyGetResponseTypeDatacenter}, nil +} + +func (f *FakeProxyService) New(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + if f.NewFunc != nil { + return f.NewFunc(ctx, body, opts...) + } + return &kernel.ProxyNewResponse{ID: "new-proxy", Type: kernel.ProxyNewResponseTypeDatacenter}, nil +} + +func (f *FakeProxyService) Delete(ctx context.Context, id string, opts ...option.RequestOption) error { + if f.DeleteFunc != nil { + return f.DeleteFunc(ctx, id, opts...) + } + return nil +} + +// Helper function to create test proxy responses +func createDatacenterProxy(id, name, country string) kernel.ProxyListResponse { + return kernel.ProxyListResponse{ + ID: id, + Name: name, + Type: kernel.ProxyListResponseTypeDatacenter, + Config: kernel.ProxyListResponseConfigUnion{ + Country: country, + }, + } +} + +func createResidentialProxy(id, name, country, city, state string) kernel.ProxyListResponse { + return kernel.ProxyListResponse{ + ID: id, + Name: name, + Type: kernel.ProxyListResponseTypeResidential, + Config: kernel.ProxyListResponseConfigUnion{ + Country: country, + City: city, + State: state, + }, + } +} + +func createCustomProxy(id, name, host string, port int64) kernel.ProxyListResponse { + return kernel.ProxyListResponse{ + ID: id, + Name: name, + Type: kernel.ProxyListResponseTypeCustom, + Config: kernel.ProxyListResponseConfigUnion{ + Host: host, + Port: port, + }, + } +} diff --git a/cmd/proxies/create_test.go b/cmd/proxies/create_test.go new file mode 100644 index 0000000..1710c19 --- /dev/null +++ b/cmd/proxies/create_test.go @@ -0,0 +1,292 @@ +package proxies + +import ( + "context" + "errors" + "testing" + + "github.com/onkernel/kernel-go-sdk" + "github.com/onkernel/kernel-go-sdk/option" + "github.com/stretchr/testify/assert" +) + +func TestProxyCreate_Datacenter_Success(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + // Verify the request + assert.Equal(t, kernel.ProxyNewParamsTypeDatacenter, body.Type) + assert.Equal(t, "My DC Proxy", body.Name.Value) + + // Check config + dcConfig := body.Config.OfProxyNewsConfigDatacenterProxyConfig + assert.NotNil(t, dcConfig) + assert.Equal(t, "US", dcConfig.Country) + + return &kernel.ProxyNewResponse{ + ID: "dc-new", + Name: "My DC Proxy", + Type: kernel.ProxyNewResponseTypeDatacenter, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Name: "My DC Proxy", + Type: "datacenter", + Country: "US", + }) + + assert.NoError(t, err) + output := buf.String() + + assert.Contains(t, output, "Creating datacenter proxy") + assert.Contains(t, output, "Successfully created proxy") + assert.Contains(t, output, "dc-new") + assert.Contains(t, output, "My DC Proxy") +} + +func TestProxyCreate_Datacenter_MissingCountry(t *testing.T) { + _ = captureOutput(t) + fake := &FakeProxyService{} + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Name: "My DC Proxy", + Type: "datacenter", + // Missing required Country + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "--country is required for datacenter proxy type") +} + +func TestProxyCreate_Residential_Success(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + // Verify residential config + resConfig := body.Config.OfProxyNewsConfigResidentialProxyConfig + assert.NotNil(t, resConfig) + assert.Equal(t, "US", resConfig.Country.Value) + assert.Equal(t, "sanfrancisco", resConfig.City.Value) + assert.Equal(t, "CA", resConfig.State.Value) + assert.Equal(t, "94107", resConfig.Zip.Value) + assert.Equal(t, "AS15169", resConfig.Asn.Value) + assert.Equal(t, "windows", resConfig.Os) + + return &kernel.ProxyNewResponse{ + ID: "res-new", + Name: "SF Residential", + Type: kernel.ProxyNewResponseTypeResidential, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Name: "SF Residential", + Type: "residential", + Country: "US", + City: "sanfrancisco", + State: "CA", + Zip: "94107", + ASN: "AS15169", + OS: "windows", + }) + + assert.NoError(t, err) + output := buf.String() + assert.Contains(t, output, "Successfully created proxy") +} + +func TestProxyCreate_Residential_CityWithoutCountry(t *testing.T) { + fake := &FakeProxyService{} + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Type: "residential", + City: "sanfrancisco", + // Missing country + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "--country is required when --city is specified") +} + +func TestProxyCreate_Residential_InvalidOS(t *testing.T) { + fake := &FakeProxyService{} + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Type: "residential", + OS: "linux", // Invalid OS + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid OS value: linux (must be windows, macos, or android)") +} + +func TestProxyCreate_Mobile_Success(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + // Verify mobile config + mobConfig := body.Config.OfProxyNewsConfigMobileProxyConfig + assert.NotNil(t, mobConfig) + assert.Equal(t, "US", mobConfig.Country.Value) + assert.Equal(t, "verizon", mobConfig.Carrier) + + return &kernel.ProxyNewResponse{ + ID: "mobile-new", + Name: "Mobile Proxy", + Type: kernel.ProxyNewResponseTypeMobile, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Name: "Mobile Proxy", + Type: "mobile", + Country: "US", + Carrier: "verizon", + }) + + assert.NoError(t, err) + output := buf.String() + assert.Contains(t, output, "Creating mobile proxy") + assert.Contains(t, output, "Successfully created proxy") +} + +func TestProxyCreate_Custom_Success(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + // Verify custom config + customConfig := body.Config.OfProxyNewsConfigCreateCustomProxyConfig + assert.NotNil(t, customConfig) + assert.Equal(t, "proxy.example.com", customConfig.Host) + assert.Equal(t, int64(8080), customConfig.Port) + assert.Equal(t, "user123", customConfig.Username.Value) + assert.Equal(t, "secret", customConfig.Password.Value) + + return &kernel.ProxyNewResponse{ + ID: "custom-new", + Name: "My Custom Proxy", + Type: kernel.ProxyNewResponseTypeCustom, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Name: "My Custom Proxy", + Type: "custom", + Host: "proxy.example.com", + Port: 8080, + Username: "user123", + Password: "secret", + }) + + assert.NoError(t, err) + output := buf.String() + assert.Contains(t, output, "Creating custom proxy") + assert.Contains(t, output, "Successfully created proxy") +} + +func TestProxyCreate_Custom_MissingHost(t *testing.T) { + fake := &FakeProxyService{} + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Type: "custom", + Port: 8080, + // Missing required host + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "--host is required for custom proxy type") +} + +func TestProxyCreate_Custom_MissingPort(t *testing.T) { + fake := &FakeProxyService{} + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Type: "custom", + Host: "proxy.example.com", + // Missing required port (will be 0) + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "--port is required for custom proxy type") +} + +func TestProxyCreate_InvalidType(t *testing.T) { + fake := &FakeProxyService{} + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Type: "invalid", + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid proxy type: invalid") +} + +func TestProxyCreate_APIError(t *testing.T) { + _ = captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + return nil, errors.New("API error") + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Name: "Test", + Type: "datacenter", + Country: "US", + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "API error") +} + +func TestProxyCreate_ISP_Success(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + // Verify ISP config + ispConfig := body.Config.OfProxyNewsConfigIspProxyConfig + assert.NotNil(t, ispConfig) + assert.Equal(t, "EU", ispConfig.Country) + + return &kernel.ProxyNewResponse{ + ID: "isp-new", + Name: "EU ISP", + Type: kernel.ProxyNewResponseTypeIsp, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Name: "EU ISP", + Type: "isp", + Country: "EU", + }) + + assert.NoError(t, err) + output := buf.String() + assert.Contains(t, output, "Creating isp proxy") + assert.Contains(t, output, "Successfully created proxy") +} diff --git a/cmd/proxies/delete_test.go b/cmd/proxies/delete_test.go new file mode 100644 index 0000000..192e893 --- /dev/null +++ b/cmd/proxies/delete_test.go @@ -0,0 +1,75 @@ +package proxies + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/onkernel/kernel-go-sdk" + "github.com/onkernel/kernel-go-sdk/option" + "github.com/stretchr/testify/assert" +) + +func TestProxyDelete_SkipConfirm_Success(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + DeleteFunc: func(ctx context.Context, id string, opts ...option.RequestOption) error { + assert.Equal(t, "proxy-1", id) + return nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Delete(context.Background(), ProxyDeleteInput{ + ID: "proxy-1", + SkipConfirm: true, + }) + + assert.NoError(t, err) + output := buf.String() + + assert.Contains(t, output, "Deleting proxy: proxy-1") + assert.Contains(t, output, "Successfully deleted proxy: proxy-1") +} + +func TestProxyDelete_SkipConfirm_NotFound(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + DeleteFunc: func(ctx context.Context, id string, opts ...option.RequestOption) error { + return &kernel.Error{StatusCode: http.StatusNotFound} + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Delete(context.Background(), ProxyDeleteInput{ + ID: "not-found", + SkipConfirm: true, + }) + + assert.NoError(t, err) // Not found returns nil + output := buf.String() + + assert.Contains(t, output, "Proxy 'not-found' not found") +} + +func TestProxyDelete_SkipConfirm_APIError(t *testing.T) { + _ = captureOutput(t) + + fake := &FakeProxyService{ + DeleteFunc: func(ctx context.Context, id string, opts ...option.RequestOption) error { + return errors.New("API error") + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Delete(context.Background(), ProxyDeleteInput{ + ID: "proxy-1", + SkipConfirm: true, + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "API error") +} diff --git a/cmd/proxies/get.go b/cmd/proxies/get.go index 8cde89d..12dfb10 100644 --- a/cmd/proxies/get.go +++ b/cmd/proxies/get.go @@ -37,76 +37,66 @@ func (p ProxyCmd) Get(ctx context.Context, in ProxyGetInput) error { func getProxyConfigRows(proxy *kernel.ProxyGetResponse) [][]string { var rows [][]string + config := &proxy.Config switch proxy.Type { - case kernel.ProxyGetResponseTypeDatacenter: - dc := proxy.Config.AsProxyGetResponseConfigDatacenterProxyConfig() - if dc.Country != "" { - rows = append(rows, []string{"Country", dc.Country}) - } - case kernel.ProxyGetResponseTypeIsp: - isp := proxy.Config.AsProxyGetResponseConfigIspProxyConfig() - if isp.Country != "" { - rows = append(rows, []string{"Country", isp.Country}) + case kernel.ProxyGetResponseTypeDatacenter, kernel.ProxyGetResponseTypeIsp: + if config.Country != "" { + rows = append(rows, []string{"Country", config.Country}) } case kernel.ProxyGetResponseTypeResidential: - res := proxy.Config.AsProxyGetResponseConfigResidentialProxyConfig() - if res.Country != "" || res.City != "" || res.State != "" || res.Zip != "" || res.Asn != "" || res.Os != "" { - if res.Country != "" { - rows = append(rows, []string{"Country", res.Country}) - } - if res.City != "" { - rows = append(rows, []string{"City", res.City}) - } - if res.State != "" { - rows = append(rows, []string{"State", res.State}) - } - if res.Zip != "" { - rows = append(rows, []string{"ZIP", res.Zip}) - } - if res.Asn != "" { - rows = append(rows, []string{"ASN", res.Asn}) - } - if res.Os != "" { - rows = append(rows, []string{"OS", res.Os}) - } + if config.Country != "" { + rows = append(rows, []string{"Country", config.Country}) + } + if config.City != "" { + rows = append(rows, []string{"City", config.City}) + } + if config.State != "" { + rows = append(rows, []string{"State", config.State}) + } + if config.Zip != "" { + rows = append(rows, []string{"ZIP", config.Zip}) + } + if config.Asn != "" { + rows = append(rows, []string{"ASN", config.Asn}) + } + if config.Os != "" { + rows = append(rows, []string{"OS", config.Os}) } case kernel.ProxyGetResponseTypeMobile: - mob := proxy.Config.AsProxyGetResponseConfigMobileProxyConfig() - if mob.Country != "" || mob.City != "" || mob.State != "" || mob.Zip != "" || mob.Asn != "" || mob.Carrier != "" { - if mob.Country != "" { - rows = append(rows, []string{"Country", mob.Country}) - } - if mob.City != "" { - rows = append(rows, []string{"City", mob.City}) - } - if mob.State != "" { - rows = append(rows, []string{"State", mob.State}) - } - if mob.Zip != "" { - rows = append(rows, []string{"ZIP", mob.Zip}) - } - if mob.Asn != "" { - rows = append(rows, []string{"ASN", mob.Asn}) - } - if mob.Carrier != "" { - rows = append(rows, []string{"Carrier", mob.Carrier}) - } + if config.Country != "" { + rows = append(rows, []string{"Country", config.Country}) + } + if config.City != "" { + rows = append(rows, []string{"City", config.City}) + } + if config.State != "" { + rows = append(rows, []string{"State", config.State}) + } + if config.Zip != "" { + rows = append(rows, []string{"ZIP", config.Zip}) + } + if config.Asn != "" { + rows = append(rows, []string{"ASN", config.Asn}) + } + if config.Carrier != "" { + rows = append(rows, []string{"Carrier", config.Carrier}) } case kernel.ProxyGetResponseTypeCustom: - custom := proxy.Config.AsProxyGetResponseConfigCustomProxyConfig() - if custom.Host != "" { - rows = append(rows, []string{"Host", custom.Host}) - rows = append(rows, []string{"Port", fmt.Sprintf("%d", custom.Port)}) - if custom.Username != "" { - rows = append(rows, []string{"Username", custom.Username}) - } - hasPassword := "No" - if custom.HasPassword { - hasPassword = "Yes" - } - rows = append(rows, []string{"Has Password", hasPassword}) + if config.Host != "" { + rows = append(rows, []string{"Host", config.Host}) + } + if config.Port != 0 { + rows = append(rows, []string{"Port", fmt.Sprintf("%d", config.Port)}) + } + if config.Username != "" { + rows = append(rows, []string{"Username", config.Username}) + } + hasPassword := "No" + if config.HasPassword { + hasPassword = "Yes" } + rows = append(rows, []string{"Has Password", hasPassword}) } return rows diff --git a/cmd/proxies/get_test.go b/cmd/proxies/get_test.go new file mode 100644 index 0000000..9f726c9 --- /dev/null +++ b/cmd/proxies/get_test.go @@ -0,0 +1,206 @@ +package proxies + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/onkernel/kernel-go-sdk" + "github.com/onkernel/kernel-go-sdk/option" + "github.com/stretchr/testify/assert" +) + +func TestProxyGet_Datacenter(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) { + return &kernel.ProxyGetResponse{ + ID: "dc-1", + Name: "US Datacenter", + Type: kernel.ProxyGetResponseTypeDatacenter, + Config: kernel.ProxyGetResponseConfigUnion{ + Country: "US", + }, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Get(context.Background(), ProxyGetInput{ID: "dc-1"}) + + assert.NoError(t, err) + output := buf.String() + + // Check all fields are displayed + assert.Contains(t, output, "ID") + assert.Contains(t, output, "dc-1") + assert.Contains(t, output, "Name") + assert.Contains(t, output, "US Datacenter") + assert.Contains(t, output, "Type") + assert.Contains(t, output, "datacenter") + assert.Contains(t, output, "Country") + assert.Contains(t, output, "US") +} + +func TestProxyGet_Residential(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) { + return &kernel.ProxyGetResponse{ + ID: "res-1", + Name: "SF Residential", + Type: kernel.ProxyGetResponseTypeResidential, + Config: kernel.ProxyGetResponseConfigUnion{ + Country: "US", + City: "sanfrancisco", + State: "CA", + Zip: "94107", + Asn: "AS15169", + Os: "windows", + }, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Get(context.Background(), ProxyGetInput{ID: "res-1"}) + + assert.NoError(t, err) + output := buf.String() + + // Check all residential-specific fields + assert.Contains(t, output, "Country") + assert.Contains(t, output, "US") + assert.Contains(t, output, "City") + assert.Contains(t, output, "sanfrancisco") + assert.Contains(t, output, "State") + assert.Contains(t, output, "CA") + assert.Contains(t, output, "ZIP") + assert.Contains(t, output, "94107") + assert.Contains(t, output, "ASN") + assert.Contains(t, output, "AS15169") + assert.Contains(t, output, "OS") + assert.Contains(t, output, "windows") +} + +func TestProxyGet_Mobile(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) { + return &kernel.ProxyGetResponse{ + ID: "mobile-1", + Name: "Mobile Proxy", + Type: kernel.ProxyGetResponseTypeMobile, + Config: kernel.ProxyGetResponseConfigUnion{ + Country: "US", + Carrier: "verizon", + }, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Get(context.Background(), ProxyGetInput{ID: "mobile-1"}) + + assert.NoError(t, err) + output := buf.String() + + assert.Contains(t, output, "Carrier") + assert.Contains(t, output, "verizon") +} + +func TestProxyGet_Custom(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) { + return &kernel.ProxyGetResponse{ + ID: "custom-1", + Name: "My Proxy", + Type: kernel.ProxyGetResponseTypeCustom, + Config: kernel.ProxyGetResponseConfigUnion{ + Host: "proxy.example.com", + Port: 8080, + Username: "user123", + HasPassword: true, + }, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Get(context.Background(), ProxyGetInput{ID: "custom-1"}) + + assert.NoError(t, err) + output := buf.String() + + assert.Contains(t, output, "Host") + assert.Contains(t, output, "proxy.example.com") + assert.Contains(t, output, "Port") + assert.Contains(t, output, "8080") + assert.Contains(t, output, "Username") + assert.Contains(t, output, "user123") + assert.Contains(t, output, "Has Password") + assert.Contains(t, output, "Yes") +} + +func TestProxyGet_EmptyName(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) { + return &kernel.ProxyGetResponse{ + ID: "proxy-1", + Name: "", // Empty name + Type: kernel.ProxyGetResponseTypeIsp, + Config: kernel.ProxyGetResponseConfigUnion{ + Country: "US", + }, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Get(context.Background(), ProxyGetInput{ID: "proxy-1"}) + + assert.NoError(t, err) + output := buf.String() + + assert.Contains(t, output, "Name") + assert.Contains(t, output, "-") // Empty name shows as "-" +} + +func TestProxyGet_NotFound(t *testing.T) { + _ = captureOutput(t) + + fake := &FakeProxyService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) { + return nil, &kernel.Error{StatusCode: http.StatusNotFound} + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Get(context.Background(), ProxyGetInput{ID: "not-found"}) + + assert.Error(t, err) +} + +func TestProxyGet_Error(t *testing.T) { + _ = captureOutput(t) + + fake := &FakeProxyService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) { + return nil, errors.New("API error") + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Get(context.Background(), ProxyGetInput{ID: "proxy-1"}) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "API error") +} diff --git a/cmd/proxies/list.go b/cmd/proxies/list.go index f059257..b4cdb2a 100644 --- a/cmd/proxies/list.go +++ b/cmd/proxies/list.go @@ -51,52 +51,44 @@ func (p ProxyCmd) List(ctx context.Context) error { } func formatProxyConfig(proxy *kernel.ProxyListResponse) string { + config := &proxy.Config switch proxy.Type { - case kernel.ProxyListResponseTypeDatacenter: - dc := proxy.Config.AsProxyListResponseConfigDatacenterProxyConfig() - if dc.Country != "" { - return fmt.Sprintf("Country: %s", dc.Country) - } - case kernel.ProxyListResponseTypeIsp: - isp := proxy.Config.AsProxyListResponseConfigIspProxyConfig() - if isp.Country != "" { - return fmt.Sprintf("Country: %s", isp.Country) + case kernel.ProxyListResponseTypeDatacenter, kernel.ProxyListResponseTypeIsp: + if config.Country != "" { + return fmt.Sprintf("Country: %s", config.Country) } case kernel.ProxyListResponseTypeResidential: - res := proxy.Config.AsProxyListResponseConfigResidentialProxyConfig() - if res.Country != "" || res.City != "" || res.State != "" { - parts := []string{} - if res.Country != "" { - parts = append(parts, fmt.Sprintf("Country: %s", res.Country)) - } - if res.City != "" { - parts = append(parts, fmt.Sprintf("City: %s", res.City)) - } - if res.State != "" { - parts = append(parts, fmt.Sprintf("State: %s", res.State)) - } + parts := []string{} + if config.Country != "" { + parts = append(parts, fmt.Sprintf("Country: %s", config.Country)) + } + if config.City != "" { + parts = append(parts, fmt.Sprintf("City: %s", config.City)) + } + if config.State != "" { + parts = append(parts, fmt.Sprintf("State: %s", config.State)) + } + if len(parts) > 0 { return strings.Join(parts, ", ") } case kernel.ProxyListResponseTypeMobile: - mob := proxy.Config.AsProxyListResponseConfigMobileProxyConfig() - if mob.Country != "" || mob.Carrier != "" { - parts := []string{} - if mob.Country != "" { - parts = append(parts, fmt.Sprintf("Country: %s", mob.Country)) - } - if mob.Carrier != "" { - parts = append(parts, fmt.Sprintf("Carrier: %s", mob.Carrier)) - } + parts := []string{} + if config.Country != "" { + parts = append(parts, fmt.Sprintf("Country: %s", config.Country)) + } + if config.Carrier != "" { + parts = append(parts, fmt.Sprintf("Carrier: %s", config.Carrier)) + } + if len(parts) > 0 { return strings.Join(parts, ", ") } case kernel.ProxyListResponseTypeCustom: - custom := proxy.Config.AsProxyListResponseConfigCustomProxyConfig() - if custom.Host != "" { + if config.Host != "" { auth := "" - if custom.Username != "" { - auth = fmt.Sprintf(", Auth: %s", custom.Username) + if config.Username != "" { + auth = fmt.Sprintf(", Auth: %s", config.Username) } - return fmt.Sprintf("%s:%d%s", custom.Host, custom.Port, auth) + return fmt.Sprintf("%s:%d%s", config.Host, config.Port, auth) } } return "-" diff --git a/cmd/proxies/list_test.go b/cmd/proxies/list_test.go new file mode 100644 index 0000000..569ddc5 --- /dev/null +++ b/cmd/proxies/list_test.go @@ -0,0 +1,115 @@ +package proxies + +import ( + "context" + "errors" + "testing" + + "github.com/onkernel/kernel-go-sdk" + "github.com/onkernel/kernel-go-sdk/option" + "github.com/stretchr/testify/assert" +) + +func TestProxyList_Empty(t *testing.T) { + buf := captureOutput(t) + fake := &FakeProxyService{ + ListFunc: func(ctx context.Context, opts ...option.RequestOption) (*[]kernel.ProxyListResponse, error) { + empty := []kernel.ProxyListResponse{} + return &empty, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.List(context.Background()) + + assert.NoError(t, err) + assert.Contains(t, buf.String(), "No proxy configurations found") +} + +func TestProxyList_WithProxies(t *testing.T) { + buf := captureOutput(t) + + proxies := []kernel.ProxyListResponse{ + createDatacenterProxy("dc-1", "US Datacenter", "US"), + createResidentialProxy("res-1", "SF Residential", "US", "sanfrancisco", "CA"), + createCustomProxy("custom-1", "My Proxy", "proxy.example.com", 8080), + { + ID: "mobile-1", + Name: "Mobile Proxy", + Type: kernel.ProxyListResponseTypeMobile, + Config: kernel.ProxyListResponseConfigUnion{ + Country: "US", + Carrier: "verizon", + }, + }, + { + ID: "isp-1", + Name: "", // Test empty name + Type: kernel.ProxyListResponseTypeIsp, + Config: kernel.ProxyListResponseConfigUnion{ + Country: "EU", + }, + }, + } + + fake := &FakeProxyService{ + ListFunc: func(ctx context.Context, opts ...option.RequestOption) (*[]kernel.ProxyListResponse, error) { + return &proxies, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.List(context.Background()) + + assert.NoError(t, err) + output := buf.String() + + // Check table headers + assert.Contains(t, output, "ID") + assert.Contains(t, output, "Name") + assert.Contains(t, output, "Type") + assert.Contains(t, output, "Config") + + // Check proxy data + assert.Contains(t, output, "dc-1") + assert.Contains(t, output, "US Datacenter") + assert.Contains(t, output, "datacenter") + assert.Contains(t, output, "Country: US") + + assert.Contains(t, output, "res-1") + assert.Contains(t, output, "SF Residential") + assert.Contains(t, output, "residential") + assert.Contains(t, output, "City: sanfrancisco") + assert.Contains(t, output, "State: CA") + + assert.Contains(t, output, "custom-1") + assert.Contains(t, output, "My Proxy") + assert.Contains(t, output, "custom") + assert.Contains(t, output, "proxy.example.com:8080") + + assert.Contains(t, output, "mobile-1") + assert.Contains(t, output, "Mobile Proxy") + assert.Contains(t, output, "mobile") + assert.Contains(t, output, "Carrier: verizon") + + assert.Contains(t, output, "isp-1") + assert.Contains(t, output, "-") // Empty name shows as "-" + assert.Contains(t, output, "isp") + assert.Contains(t, output, "Country: EU") +} + +func TestProxyList_Error(t *testing.T) { + _ = captureOutput(t) + + fake := &FakeProxyService{ + ListFunc: func(ctx context.Context, opts ...option.RequestOption) (*[]kernel.ProxyListResponse, error) { + return nil, errors.New("API error") + }, + } + + p := ProxyCmd{proxies: fake} + err := p.List(context.Background()) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "API error") +} From 7e4501ac8d118fada9badfd30aecc1cc99e0071f Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Mon, 29 Sep 2025 12:53:13 -0700 Subject: [PATCH 3/6] Fix client --- cmd/proxies/helpers.go | 8 +++----- cmd/root.go | 8 ++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/cmd/proxies/helpers.go b/cmd/proxies/helpers.go index c4bc98e..694c7e8 100644 --- a/cmd/proxies/helpers.go +++ b/cmd/proxies/helpers.go @@ -6,13 +6,11 @@ import ( "github.com/spf13/cobra" ) -type contextKey string - -const KernelClientKey contextKey = "kernel_client" - // GetKernelClient retrieves the kernel client from the command context func GetKernelClient(cmd *cobra.Command) kernel.Client { - return cmd.Context().Value(KernelClientKey).(kernel.Client) + // Use the raw string key to match what cmd package uses + // This avoids type mismatch between different contextKey types + return cmd.Context().Value("kernel_client").(kernel.Client) } // PrintTableNoPad prints a table without padding (delegating to cmd package) diff --git a/cmd/root.go b/cmd/root.go index b19291d..ca3b479 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -65,12 +65,8 @@ func logLevelToPterm(level string) pterm.LogLevel { } } -type contextKey string - -const KernelClientKey contextKey = "kernel_client" - func getKernelClient(cmd *cobra.Command) kernel.Client { - return cmd.Context().Value(KernelClientKey).(kernel.Client) + return cmd.Context().Value("kernel_client").(kernel.Client) } // isAuthExempt returns true if the command or any of its parents should skip auth. @@ -116,7 +112,7 @@ func init() { return fmt.Errorf("authentication required: %w", err) } - ctx := context.WithValue(cmd.Context(), KernelClientKey, *client) + ctx := context.WithValue(cmd.Context(), "kernel_client", *client) cmd.SetContext(ctx) return nil } From bbfbc967154f9903ceeaf00b771425d7614a5dce Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Mon, 29 Sep 2025 12:58:47 -0700 Subject: [PATCH 4/6] Remove beta prefix --- cmd/beta.go | 23 ----------------------- cmd/root.go | 3 ++- 2 files changed, 2 insertions(+), 24 deletions(-) delete mode 100644 cmd/beta.go diff --git a/cmd/beta.go b/cmd/beta.go deleted file mode 100644 index edc0439..0000000 --- a/cmd/beta.go +++ /dev/null @@ -1,23 +0,0 @@ -package cmd - -import ( - "github.com/onkernel/cli/cmd/proxies" - "github.com/spf13/cobra" -) - -// betaCmd is the parent command for experimental features -var betaCmd = &cobra.Command{ - Use: "beta", - Short: "Experimental features (subject to change)", - Long: `The beta command provides access to experimental features that are still under development. -These features may change, break, or be removed in future versions without notice.`, - Run: func(cmd *cobra.Command, args []string) { - // If called without subcommands, show help - _ = cmd.Help() - }, -} - -func init() { - // Add proxy commands under beta - betaCmd.AddCommand(proxies.ProxiesCmd) -} diff --git a/cmd/root.go b/cmd/root.go index ca3b479..527737d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,7 @@ import ( "time" "github.com/charmbracelet/fang" + "github.com/onkernel/cli/cmd/proxies" "github.com/onkernel/cli/pkg/auth" "github.com/onkernel/cli/pkg/update" "github.com/onkernel/kernel-go-sdk" @@ -123,7 +124,7 @@ func init() { rootCmd.AddCommand(browsersCmd) rootCmd.AddCommand(appCmd) rootCmd.AddCommand(profilesCmd) - rootCmd.AddCommand(betaCmd) + rootCmd.AddCommand(proxies.ProxiesCmd) rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { // running synchronously so we never slow the command From 503fc29c37a05305e6f8d475d23bd8665bd3aa79 Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Mon, 29 Sep 2025 14:20:34 -0700 Subject: [PATCH 5/6] Revert client changes --- cmd/proxies/create.go | 32 ++++++++++++++++---------------- cmd/proxies/delete.go | 9 +++++---- cmd/proxies/get.go | 6 +++--- cmd/proxies/helpers.go | 13 ++----------- cmd/proxies/list.go | 6 +++--- cmd/root.go | 10 ++++++++-- 6 files changed, 37 insertions(+), 39 deletions(-) diff --git a/cmd/proxies/create.go b/cmd/proxies/create.go index 24085d2..c36c27a 100644 --- a/cmd/proxies/create.go +++ b/cmd/proxies/create.go @@ -172,27 +172,27 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { return nil } -func runProxiesCreate(cmd *cobra.Command, args []string) error { - client := GetKernelClient(cmd) +func runProxiesCreate(command *cobra.Command, args []string) error { + client := command.Context().Value("kernel_client").(kernel.Client) // Get all flag values - proxyType, _ := cmd.Flags().GetString("type") - name, _ := cmd.Flags().GetString("name") - country, _ := cmd.Flags().GetString("country") - city, _ := cmd.Flags().GetString("city") - state, _ := cmd.Flags().GetString("state") - zip, _ := cmd.Flags().GetString("zip") - asn, _ := cmd.Flags().GetString("asn") - os, _ := cmd.Flags().GetString("os") - carrier, _ := cmd.Flags().GetString("carrier") - host, _ := cmd.Flags().GetString("host") - port, _ := cmd.Flags().GetInt("port") - username, _ := cmd.Flags().GetString("username") - password, _ := cmd.Flags().GetString("password") + proxyType, _ := command.Flags().GetString("type") + name, _ := command.Flags().GetString("name") + country, _ := command.Flags().GetString("country") + city, _ := command.Flags().GetString("city") + state, _ := command.Flags().GetString("state") + zip, _ := command.Flags().GetString("zip") + asn, _ := command.Flags().GetString("asn") + os, _ := command.Flags().GetString("os") + carrier, _ := command.Flags().GetString("carrier") + host, _ := command.Flags().GetString("host") + port, _ := command.Flags().GetInt("port") + username, _ := command.Flags().GetString("username") + password, _ := command.Flags().GetString("password") svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.Create(cmd.Context(), ProxyCreateInput{ + return p.Create(command.Context(), ProxyCreateInput{ Name: name, Type: proxyType, Country: country, diff --git a/cmd/proxies/delete.go b/cmd/proxies/delete.go index 66a7b15..ce80862 100644 --- a/cmd/proxies/delete.go +++ b/cmd/proxies/delete.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/onkernel/cli/pkg/util" + "github.com/onkernel/kernel-go-sdk" "github.com/pterm/pterm" "github.com/spf13/cobra" ) @@ -51,13 +52,13 @@ func (p ProxyCmd) Delete(ctx context.Context, in ProxyDeleteInput) error { return nil } -func runProxiesDelete(cmd *cobra.Command, args []string) error { - client := GetKernelClient(cmd) - skipConfirm, _ := cmd.Flags().GetBool("yes") +func runProxiesDelete(command *cobra.Command, args []string) error { + client := command.Context().Value("kernel_client").(kernel.Client) + skipConfirm, _ := command.Flags().GetBool("yes") svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.Delete(cmd.Context(), ProxyDeleteInput{ + return p.Delete(command.Context(), ProxyDeleteInput{ ID: args[0], SkipConfirm: skipConfirm, }) diff --git a/cmd/proxies/get.go b/cmd/proxies/get.go index 12dfb10..bd37f7e 100644 --- a/cmd/proxies/get.go +++ b/cmd/proxies/get.go @@ -102,9 +102,9 @@ func getProxyConfigRows(proxy *kernel.ProxyGetResponse) [][]string { return rows } -func runProxiesGet(cmd *cobra.Command, args []string) error { - client := GetKernelClient(cmd) +func runProxiesGet(command *cobra.Command, args []string) error { + client := command.Context().Value("kernel_client").(kernel.Client) svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.Get(cmd.Context(), ProxyGetInput{ID: args[0]}) + return p.Get(command.Context(), ProxyGetInput{ID: args[0]}) } diff --git a/cmd/proxies/helpers.go b/cmd/proxies/helpers.go index 694c7e8..59b4853 100644 --- a/cmd/proxies/helpers.go +++ b/cmd/proxies/helpers.go @@ -1,23 +1,14 @@ package proxies import ( - "github.com/onkernel/kernel-go-sdk" "github.com/pterm/pterm" - "github.com/spf13/cobra" ) -// GetKernelClient retrieves the kernel client from the command context -func GetKernelClient(cmd *cobra.Command) kernel.Client { - // Use the raw string key to match what cmd package uses - // This avoids type mismatch between different contextKey types - return cmd.Context().Value("kernel_client").(kernel.Client) -} - -// PrintTableNoPad prints a table without padding (delegating to cmd package) +// PrintTableNoPad prints a table without padding func PrintTableNoPad(data pterm.TableData, withRowSeparators bool) { table := pterm.DefaultTable.WithHasHeader().WithData(data) if withRowSeparators { table = table.WithRowSeparator("-") } - _ = pterm.DefaultTable.WithHasHeader().WithData(data).WithRowSeparator("-").Render() + _ = table.Render() } diff --git a/cmd/proxies/list.go b/cmd/proxies/list.go index b4cdb2a..d151a9e 100644 --- a/cmd/proxies/list.go +++ b/cmd/proxies/list.go @@ -94,9 +94,9 @@ func formatProxyConfig(proxy *kernel.ProxyListResponse) string { return "-" } -func runProxiesList(cmd *cobra.Command, args []string) error { - client := GetKernelClient(cmd) +func runProxiesList(command *cobra.Command, args []string) error { + client := command.Context().Value("kernel_client").(kernel.Client) svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.List(cmd.Context()) + return p.List(command.Context()) } diff --git a/cmd/root.go b/cmd/root.go index 527737d..2fedf42 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -66,8 +66,14 @@ func logLevelToPterm(level string) pterm.LogLevel { } } +// ContextKey is the type for context keys +type ContextKey string + +// KernelClientKey is the context key for the kernel client +const KernelClientKey ContextKey = "kernel_client" + func getKernelClient(cmd *cobra.Command) kernel.Client { - return cmd.Context().Value("kernel_client").(kernel.Client) + return cmd.Context().Value(KernelClientKey).(kernel.Client) } // isAuthExempt returns true if the command or any of its parents should skip auth. @@ -113,7 +119,7 @@ func init() { return fmt.Errorf("authentication required: %w", err) } - ctx := context.WithValue(cmd.Context(), "kernel_client", *client) + ctx := context.WithValue(cmd.Context(), KernelClientKey, *client) cmd.SetContext(ctx) return nil } From d7b76a78e7349e0669eb3aea27b364533a02a289 Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Mon, 29 Sep 2025 14:35:18 -0700 Subject: [PATCH 6/6] Clean up client --- cmd/proxies/create.go | 32 ++++++++++++++++---------------- cmd/proxies/delete.go | 9 ++++----- cmd/proxies/get.go | 6 +++--- cmd/proxies/list.go | 6 +++--- cmd/root.go | 11 +++-------- pkg/util/client.go | 12 ++++++++++++ 6 files changed, 41 insertions(+), 35 deletions(-) diff --git a/cmd/proxies/create.go b/cmd/proxies/create.go index c36c27a..3b035a0 100644 --- a/cmd/proxies/create.go +++ b/cmd/proxies/create.go @@ -172,27 +172,27 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { return nil } -func runProxiesCreate(command *cobra.Command, args []string) error { - client := command.Context().Value("kernel_client").(kernel.Client) +func runProxiesCreate(cmd *cobra.Command, args []string) error { + client := util.GetKernelClient(cmd) // Get all flag values - proxyType, _ := command.Flags().GetString("type") - name, _ := command.Flags().GetString("name") - country, _ := command.Flags().GetString("country") - city, _ := command.Flags().GetString("city") - state, _ := command.Flags().GetString("state") - zip, _ := command.Flags().GetString("zip") - asn, _ := command.Flags().GetString("asn") - os, _ := command.Flags().GetString("os") - carrier, _ := command.Flags().GetString("carrier") - host, _ := command.Flags().GetString("host") - port, _ := command.Flags().GetInt("port") - username, _ := command.Flags().GetString("username") - password, _ := command.Flags().GetString("password") + proxyType, _ := cmd.Flags().GetString("type") + name, _ := cmd.Flags().GetString("name") + country, _ := cmd.Flags().GetString("country") + city, _ := cmd.Flags().GetString("city") + state, _ := cmd.Flags().GetString("state") + zip, _ := cmd.Flags().GetString("zip") + asn, _ := cmd.Flags().GetString("asn") + os, _ := cmd.Flags().GetString("os") + carrier, _ := cmd.Flags().GetString("carrier") + host, _ := cmd.Flags().GetString("host") + port, _ := cmd.Flags().GetInt("port") + username, _ := cmd.Flags().GetString("username") + password, _ := cmd.Flags().GetString("password") svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.Create(command.Context(), ProxyCreateInput{ + return p.Create(cmd.Context(), ProxyCreateInput{ Name: name, Type: proxyType, Country: country, diff --git a/cmd/proxies/delete.go b/cmd/proxies/delete.go index ce80862..04bd1ee 100644 --- a/cmd/proxies/delete.go +++ b/cmd/proxies/delete.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/onkernel/cli/pkg/util" - "github.com/onkernel/kernel-go-sdk" "github.com/pterm/pterm" "github.com/spf13/cobra" ) @@ -52,13 +51,13 @@ func (p ProxyCmd) Delete(ctx context.Context, in ProxyDeleteInput) error { return nil } -func runProxiesDelete(command *cobra.Command, args []string) error { - client := command.Context().Value("kernel_client").(kernel.Client) - skipConfirm, _ := command.Flags().GetBool("yes") +func runProxiesDelete(cmd *cobra.Command, args []string) error { + client := util.GetKernelClient(cmd) + skipConfirm, _ := cmd.Flags().GetBool("yes") svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.Delete(command.Context(), ProxyDeleteInput{ + return p.Delete(cmd.Context(), ProxyDeleteInput{ ID: args[0], SkipConfirm: skipConfirm, }) diff --git a/cmd/proxies/get.go b/cmd/proxies/get.go index bd37f7e..6850b68 100644 --- a/cmd/proxies/get.go +++ b/cmd/proxies/get.go @@ -102,9 +102,9 @@ func getProxyConfigRows(proxy *kernel.ProxyGetResponse) [][]string { return rows } -func runProxiesGet(command *cobra.Command, args []string) error { - client := command.Context().Value("kernel_client").(kernel.Client) +func runProxiesGet(cmd *cobra.Command, args []string) error { + client := util.GetKernelClient(cmd) svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.Get(command.Context(), ProxyGetInput{ID: args[0]}) + return p.Get(cmd.Context(), ProxyGetInput{ID: args[0]}) } diff --git a/cmd/proxies/list.go b/cmd/proxies/list.go index d151a9e..616c49d 100644 --- a/cmd/proxies/list.go +++ b/cmd/proxies/list.go @@ -94,9 +94,9 @@ func formatProxyConfig(proxy *kernel.ProxyListResponse) string { return "-" } -func runProxiesList(command *cobra.Command, args []string) error { - client := command.Context().Value("kernel_client").(kernel.Client) +func runProxiesList(cmd *cobra.Command, args []string) error { + client := util.GetKernelClient(cmd) svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.List(command.Context()) + return p.List(cmd.Context()) } diff --git a/cmd/root.go b/cmd/root.go index 2fedf42..e72d732 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,6 +11,7 @@ import ( "github.com/onkernel/cli/cmd/proxies" "github.com/onkernel/cli/pkg/auth" "github.com/onkernel/cli/pkg/update" + "github.com/onkernel/cli/pkg/util" "github.com/onkernel/kernel-go-sdk" "github.com/onkernel/kernel-go-sdk/option" "github.com/pterm/pterm" @@ -66,14 +67,8 @@ func logLevelToPterm(level string) pterm.LogLevel { } } -// ContextKey is the type for context keys -type ContextKey string - -// KernelClientKey is the context key for the kernel client -const KernelClientKey ContextKey = "kernel_client" - func getKernelClient(cmd *cobra.Command) kernel.Client { - return cmd.Context().Value(KernelClientKey).(kernel.Client) + return util.GetKernelClient(cmd) } // isAuthExempt returns true if the command or any of its parents should skip auth. @@ -119,7 +114,7 @@ func init() { return fmt.Errorf("authentication required: %w", err) } - ctx := context.WithValue(cmd.Context(), KernelClientKey, *client) + ctx := context.WithValue(cmd.Context(), util.KernelClientKey, *client) cmd.SetContext(ctx) return nil } diff --git a/pkg/util/client.go b/pkg/util/client.go index ceb022e..6b12f1d 100644 --- a/pkg/util/client.go +++ b/pkg/util/client.go @@ -12,10 +12,22 @@ import ( kernel "github.com/onkernel/kernel-go-sdk" "github.com/onkernel/kernel-go-sdk/option" "github.com/pterm/pterm" + "github.com/spf13/cobra" ) var printedUpgradeMessage atomic.Bool +// ContextKey is the type for context keys +type ContextKey string + +// KernelClientKey is the context key for the kernel client +const KernelClientKey ContextKey = "kernel_client" + +// GetKernelClient retrieves the kernel client from the command context +func GetKernelClient(cmd *cobra.Command) kernel.Client { + return cmd.Context().Value(KernelClientKey).(kernel.Client) +} + // NewClient returns a kernel API client preconfigured with middleware that // detects when a newer CLI/SDK version is required and informs the user. //