diff --git a/cmd/proxies/check.go b/cmd/proxies/check.go index 047e6f66..5f47fae6 100644 --- a/cmd/proxies/check.go +++ b/cmd/proxies/check.go @@ -40,6 +40,7 @@ func (p ProxyCmd) Check(ctx context.Context, in ProxyCheckInput) error { } rows = append(rows, []string{"Name", name}) rows = append(rows, []string{"Type", string(proxy.Type)}) + rows = append(rows, []string{"Bypass Hosts", formatBypassHosts(proxy.BypassHosts)}) // Display protocol (default to https if not set) protocol := string(proxy.Protocol) diff --git a/cmd/proxies/check_test.go b/cmd/proxies/check_test.go new file mode 100644 index 00000000..8f24ecbf --- /dev/null +++ b/cmd/proxies/check_test.go @@ -0,0 +1,36 @@ +package proxies + +import ( + "context" + "testing" + + "github.com/kernel/kernel-go-sdk" + "github.com/kernel/kernel-go-sdk/option" + "github.com/stretchr/testify/assert" +) + +func TestProxyCheck_ShowsBypassHosts(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + CheckFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { + return &kernel.ProxyCheckResponse{ + ID: id, + Name: "Proxy 1", + Type: kernel.ProxyCheckResponseTypeDatacenter, + BypassHosts: []string{"localhost", "internal.service.local"}, + Status: kernel.ProxyCheckResponseStatusAvailable, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Check(context.Background(), ProxyCheckInput{ID: "proxy-1"}) + + assert.NoError(t, err) + output := buf.String() + assert.Contains(t, output, "Bypass Hosts") + assert.Contains(t, output, "localhost") + assert.Contains(t, output, "internal.service.local") + assert.Contains(t, output, "Proxy health check passed") +} diff --git a/cmd/proxies/create.go b/cmd/proxies/create.go index 96606b20..746e1d58 100644 --- a/cmd/proxies/create.go +++ b/cmd/proxies/create.go @@ -3,6 +3,7 @@ package proxies import ( "context" "fmt" + "strings" "github.com/kernel/cli/pkg/table" "github.com/kernel/cli/pkg/util" @@ -40,6 +41,9 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { if in.Name != "" { params.Name = kernel.Opt(in.Name) } + if len(in.BypassHosts) > 0 { + params.BypassHosts = normalizeBypassHosts(in.BypassHosts) + } // Build config based on type switch proxyType { @@ -189,6 +193,7 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { } rows = append(rows, []string{"Name", name}) rows = append(rows, []string{"Type", string(proxy.Type)}) + rows = append(rows, []string{"Bypass Hosts", formatBypassHosts(proxy.BypassHosts)}) // Display protocol (default to https if not set) protocol := string(proxy.Protocol) @@ -219,26 +224,40 @@ func runProxiesCreate(cmd *cobra.Command, args []string) error { port, _ := cmd.Flags().GetInt("port") username, _ := cmd.Flags().GetString("username") password, _ := cmd.Flags().GetString("password") + bypassHosts, _ := cmd.Flags().GetStringSlice("bypass-host") output, _ := cmd.Flags().GetString("output") svc := client.Proxies p := ProxyCmd{proxies: &svc} return p.Create(cmd.Context(), ProxyCreateInput{ - Name: name, - Type: proxyType, - Protocol: protocol, - Country: country, - City: city, - State: state, - Zip: zip, - ASN: asn, - OS: os, - Carrier: carrier, - Host: host, - Port: port, - Username: username, - Password: password, - Output: output, + Name: name, + Type: proxyType, + Protocol: protocol, + BypassHosts: bypassHosts, + Country: country, + City: city, + State: state, + Zip: zip, + ASN: asn, + OS: os, + Carrier: carrier, + Host: host, + Port: port, + Username: username, + Password: password, + Output: output, }) } + +func normalizeBypassHosts(hosts []string) []string { + normalized := make([]string, 0, len(hosts)) + for _, host := range hosts { + trimmed := strings.TrimSpace(host) + if trimmed != "" { + normalized = append(normalized, trimmed) + } + } + + return normalized +} diff --git a/cmd/proxies/create_test.go b/cmd/proxies/create_test.go index cda254f0..bdb3b306 100644 --- a/cmd/proxies/create_test.go +++ b/cmd/proxies/create_test.go @@ -18,6 +18,7 @@ func TestProxyCreate_Datacenter_Success(t *testing.T) { // Verify the request assert.Equal(t, kernel.ProxyNewParamsTypeDatacenter, body.Type) assert.Equal(t, "My DC Proxy", body.Name.Value) + assert.Equal(t, []string{"localhost", "internal.service.local"}, body.BypassHosts) // Check config dcConfig := body.Config.OfProxyNewsConfigDatacenterProxyConfig @@ -25,18 +26,20 @@ func TestProxyCreate_Datacenter_Success(t *testing.T) { assert.Equal(t, "US", dcConfig.Country.Value) return &kernel.ProxyNewResponse{ - ID: "dc-new", - Name: "My DC Proxy", - Type: kernel.ProxyNewResponseTypeDatacenter, + ID: "dc-new", + Name: "My DC Proxy", + Type: kernel.ProxyNewResponseTypeDatacenter, + BypassHosts: []string{"localhost", "internal.service.local"}, }, nil }, } p := ProxyCmd{proxies: fake} err := p.Create(context.Background(), ProxyCreateInput{ - Name: "My DC Proxy", - Type: "datacenter", - Country: "US", + Name: "My DC Proxy", + Type: "datacenter", + Country: "US", + BypassHosts: []string{"localhost", "internal.service.local"}, }) assert.NoError(t, err) @@ -46,6 +49,9 @@ func TestProxyCreate_Datacenter_Success(t *testing.T) { assert.Contains(t, output, "Successfully created proxy") assert.Contains(t, output, "dc-new") assert.Contains(t, output, "My DC Proxy") + assert.Contains(t, output, "Bypass Hosts") + assert.Contains(t, output, "localhost") + assert.Contains(t, output, "internal.service.local") } func TestProxyCreate_Datacenter_WithoutCountry(t *testing.T) { @@ -306,6 +312,27 @@ func TestProxyCreate_Protocol_Invalid(t *testing.T) { assert.Contains(t, err.Error(), "invalid protocol: ftp") } +func TestProxyCreate_BypassHosts_Normalized(t *testing.T) { + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + assert.Equal(t, []string{"localhost", "internal.service.local"}, body.BypassHosts) + return &kernel.ProxyNewResponse{ + ID: "test-proxy", + Type: kernel.ProxyNewResponseTypeDatacenter, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Type: "datacenter", + Country: "US", + BypassHosts: []string{" localhost ", "", "internal.service.local"}, + }) + + assert.NoError(t, err) +} + func TestProxyCreate_APIError(t *testing.T) { _ = captureOutput(t) diff --git a/cmd/proxies/format.go b/cmd/proxies/format.go new file mode 100644 index 00000000..9657c3a6 --- /dev/null +++ b/cmd/proxies/format.go @@ -0,0 +1,11 @@ +package proxies + +import "strings" + +func formatBypassHosts(hosts []string) string { + if len(hosts) == 0 { + return "-" + } + + return strings.Join(hosts, ", ") +} diff --git a/cmd/proxies/get.go b/cmd/proxies/get.go index 0b7b0914..1c5a54b4 100644 --- a/cmd/proxies/get.go +++ b/cmd/proxies/get.go @@ -36,6 +36,7 @@ func (p ProxyCmd) Get(ctx context.Context, in ProxyGetInput) error { } rows = append(rows, []string{"Name", name}) rows = append(rows, []string{"Type", string(item.Type)}) + rows = append(rows, []string{"Bypass Hosts", formatBypassHosts(item.BypassHosts)}) // Display protocol (default to https if not set) protocol := string(item.Protocol) diff --git a/cmd/proxies/get_test.go b/cmd/proxies/get_test.go index 922abe37..92c9cb91 100644 --- a/cmd/proxies/get_test.go +++ b/cmd/proxies/get_test.go @@ -17,9 +17,10 @@ func TestProxyGet_Datacenter(t *testing.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, + ID: "dc-1", + Name: "US Datacenter", + Type: kernel.ProxyGetResponseTypeDatacenter, + BypassHosts: []string{"localhost", "internal.service.local"}, Config: kernel.ProxyGetResponseConfigUnion{ Country: "US", }, @@ -40,6 +41,9 @@ func TestProxyGet_Datacenter(t *testing.T) { assert.Contains(t, output, "US Datacenter") assert.Contains(t, output, "Type") assert.Contains(t, output, "datacenter") + assert.Contains(t, output, "Bypass Hosts") + assert.Contains(t, output, "localhost") + assert.Contains(t, output, "internal.service.local") assert.Contains(t, output, "Country") assert.Contains(t, output, "US") } diff --git a/cmd/proxies/list.go b/cmd/proxies/list.go index f34a5d79..7300611c 100644 --- a/cmd/proxies/list.go +++ b/cmd/proxies/list.go @@ -41,7 +41,7 @@ func (p ProxyCmd) List(ctx context.Context, in ProxyListInput) error { // Prepare table data tableData := pterm.TableData{ - {"ID", "Name", "Type", "Protocol", "Config", "Status", "Last Checked"}, + {"ID", "Name", "Type", "Protocol", "Bypass Hosts", "Config", "Status", "Last Checked"}, } for _, proxy := range *items { @@ -77,6 +77,7 @@ func (p ProxyCmd) List(ctx context.Context, in ProxyListInput) error { name, string(proxy.Type), protocol, + formatBypassHosts(proxy.BypassHosts), configStr, status, lastChecked, diff --git a/cmd/proxies/list_test.go b/cmd/proxies/list_test.go index 7a235f63..455069a8 100644 --- a/cmd/proxies/list_test.go +++ b/cmd/proxies/list_test.go @@ -34,9 +34,10 @@ func TestProxyList_WithProxies(t *testing.T) { 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, + ID: "mobile-1", + Name: "Mobile Proxy", + Type: kernel.ProxyListResponseTypeMobile, + BypassHosts: []string{"abc"}, Config: kernel.ProxyListResponseConfigUnion{ Country: "US", Carrier: "verizon", @@ -85,6 +86,7 @@ func TestProxyList_WithProxies(t *testing.T) { assert.Contains(t, output, "mobile-1") assert.Contains(t, output, "mobile") + assert.Contains(t, output, "abc") assert.Contains(t, output, "isp-1") assert.Contains(t, output, "-") // Empty name shows as "-" diff --git a/cmd/proxies/proxies.go b/cmd/proxies/proxies.go index a266a6e8..2440d3a9 100644 --- a/cmd/proxies/proxies.go +++ b/cmd/proxies/proxies.go @@ -43,13 +43,16 @@ Proxy types (from best to worst for bot detection): Examples: # Create a datacenter proxy - kernel beta proxies create --type datacenter --country US --name "US Datacenter" + kernel 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 + kernel 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`, + kernel proxies create --type residential --country US --city sanfrancisco --state CA + + # Create a proxy with bypass hosts + kernel proxies create --type datacenter --country US --bypass-host localhost,internal.service.local`, RunE: runProxiesCreate, } @@ -105,6 +108,7 @@ func init() { proxiesCreateCmd.Flags().Int("port", 0, "Proxy port") proxiesCreateCmd.Flags().String("username", "", "Username for proxy authentication") proxiesCreateCmd.Flags().String("password", "", "Password for proxy authentication") + proxiesCreateCmd.Flags().StringSlice("bypass-host", nil, "Hostname(s) to bypass proxy and connect directly (repeat or comma-separated)") // Delete flags proxiesDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") diff --git a/cmd/proxies/types.go b/cmd/proxies/types.go index 1583023f..c8e7a38d 100644 --- a/cmd/proxies/types.go +++ b/cmd/proxies/types.go @@ -35,6 +35,8 @@ type ProxyCreateInput struct { Name string Type string Protocol string + // Hostnames that should bypass the parent proxy and connect directly. + BypassHosts []string // Datacenter/ISP config Country string // Residential/Mobile config diff --git a/go.mod b/go.mod index 2eae5da3..617ac4b7 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.37.0 + github.com/kernel/kernel-go-sdk v0.39.0 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 067be8d8..a3ec8b4b 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.37.0 h1:90/AJUSSY0P09S2qO9GLP3xPr0qS8z0Fb7frDbVnJGQ= -github.com/kernel/kernel-go-sdk v0.37.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.39.0 h1:P7R7dG1b3T9MQ420mhESWwbp+KgRTbQCTD5dVBQnkm4= +github.com/kernel/kernel-go-sdk v0.39.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=