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/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.go b/cmd/proxies/create.go new file mode 100644 index 0000000..3b035a0 --- /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 := util.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/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.go b/cmd/proxies/delete.go new file mode 100644 index 0000000..04bd1ee --- /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 := util.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/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 new file mode 100644 index 0000000..6850b68 --- /dev/null +++ b/cmd/proxies/get.go @@ -0,0 +1,110 @@ +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 + config := &proxy.Config + + switch proxy.Type { + case kernel.ProxyGetResponseTypeDatacenter, kernel.ProxyGetResponseTypeIsp: + if config.Country != "" { + rows = append(rows, []string{"Country", config.Country}) + } + case kernel.ProxyGetResponseTypeResidential: + 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: + 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: + 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 +} + +func runProxiesGet(cmd *cobra.Command, args []string) error { + client := util.GetKernelClient(cmd) + svc := client.Proxies + p := ProxyCmd{proxies: &svc} + return p.Get(cmd.Context(), ProxyGetInput{ID: args[0]}) +} 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/helpers.go b/cmd/proxies/helpers.go new file mode 100644 index 0000000..59b4853 --- /dev/null +++ b/cmd/proxies/helpers.go @@ -0,0 +1,14 @@ +package proxies + +import ( + "github.com/pterm/pterm" +) + +// PrintTableNoPad prints a table without padding +func PrintTableNoPad(data pterm.TableData, withRowSeparators bool) { + table := pterm.DefaultTable.WithHasHeader().WithData(data) + if withRowSeparators { + table = table.WithRowSeparator("-") + } + _ = table.Render() +} diff --git a/cmd/proxies/list.go b/cmd/proxies/list.go new file mode 100644 index 0000000..616c49d --- /dev/null +++ b/cmd/proxies/list.go @@ -0,0 +1,102 @@ +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 { + config := &proxy.Config + switch proxy.Type { + case kernel.ProxyListResponseTypeDatacenter, kernel.ProxyListResponseTypeIsp: + if config.Country != "" { + return fmt.Sprintf("Country: %s", config.Country) + } + case kernel.ProxyListResponseTypeResidential: + 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: + 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: + if config.Host != "" { + auth := "" + if config.Username != "" { + auth = fmt.Sprintf(", Auth: %s", config.Username) + } + return fmt.Sprintf("%s:%d%s", config.Host, config.Port, auth) + } + } + return "-" +} + +func runProxiesList(cmd *cobra.Command, args []string) error { + client := util.GetKernelClient(cmd) + svc := client.Proxies + p := ProxyCmd{proxies: &svc} + return p.List(cmd.Context()) +} 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") +} 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..e72d732 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,8 +8,10 @@ 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/cli/pkg/util" "github.com/onkernel/kernel-go-sdk" "github.com/onkernel/kernel-go-sdk/option" "github.com/pterm/pterm" @@ -65,12 +67,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 util.GetKernelClient(cmd) } // isAuthExempt returns true if the command or any of its parents should skip auth. @@ -116,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 } @@ -127,6 +125,7 @@ func init() { rootCmd.AddCommand(browsersCmd) rootCmd.AddCommand(appCmd) rootCmd.AddCommand(profilesCmd) + rootCmd.AddCommand(proxies.ProxiesCmd) rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { // running synchronously so we never slow the command 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. //