Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ Per-category updates are partial — only categories you name are changed; other
- `--city <name>` - City name (no spaces, e.g. sanfrancisco) (residential, mobile; requires `--country`)
- `--state <code>` - Two-letter state code (residential, mobile)
- `--zip <zip>` - US ZIP code (residential)
- `--asn <asn>` - Autonomous system number (e.g., AS15169) (residential)
- `--asn <asn>` - Autonomous system number, e.g. AS6079 (isp, residential). The ISP pool is a fixed set of static IPs, so only ASNs present in it can be requested; an unsupported value is rejected with the list of available ASNs.
- `--os <os>` - Operating system: windows, macos, android (residential)
- `--host <host>` - Proxy host (custom; required)
- `--port <port>` - Proxy port (custom; required)
Expand Down Expand Up @@ -880,6 +880,9 @@ kernel proxies create --type custom --host proxy.example.com --port 8080 --ca-bu
# Create a residential proxy with location and OS
kernel proxies create --type residential --country US --city sanfrancisco --state CA --zip 94107 --asn AS15169 --os windows --name "SF Residential"

# Create an ISP proxy pinned to a specific ASN
kernel proxies create --type isp --asn AS6079 --name "RCN ISP"

# Create a mobile proxy
kernel proxies create --type mobile --country US --city sanfrancisco --name "US Mobile"

Expand Down
19 changes: 17 additions & 2 deletions cmd/proxies/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
// Build config based on type
switch proxyType {
case kernel.ProxyNewParamsTypeDatacenter:
if in.ASN != "" {
return fmt.Errorf("--asn is not supported for datacenter proxies")
}
config := kernel.ProxyNewParamsConfigDatacenter{}
if in.Country != "" {
config.Country = kernel.Opt(in.Country)
Expand All @@ -62,6 +65,12 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
if in.Country != "" {
config.Country = kernel.Opt(in.Country)
}
if in.ASN != "" {
// The generated ISP config has no Asn field until Stainless regenerates
// against the spec. Send it as an extra field so the flag works against
// the deployed API now; replace with config.Asn on the next SDK bump.
config.SetExtraFields(map[string]any{"asn": in.ASN})
}
params.Config = kernel.ProxyNewParamsConfigUnion{
OfIsp: &config,
}
Expand Down Expand Up @@ -109,8 +118,11 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
if in.City != "" && in.Country == "" {
return fmt.Errorf("--country is required when --city is specified")
}
if in.Zip != "" || in.ASN != "" {
pterm.Warning.Println("--zip and --asn are not supported for mobile proxies and will be ignored")
if in.Zip != "" {
return fmt.Errorf("--zip is not supported for mobile proxies")
}
if in.ASN != "" {
return fmt.Errorf("--asn is not supported for mobile proxies")
}

if in.Country != "" {
Expand All @@ -127,6 +139,9 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
}

case kernel.ProxyNewParamsTypeCustom:
if in.ASN != "" {
return fmt.Errorf("--asn is not supported for custom proxies; the upstream proxy determines the egress network")
}
if in.Host == "" {
return fmt.Errorf("--host is required for custom proxy type")
}
Expand Down
141 changes: 141 additions & 0 deletions cmd/proxies/create_asn_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package proxies

import (
"context"
"encoding/json"
"testing"

"github.com/kernel/kernel-go-sdk"
"github.com/kernel/kernel-go-sdk/option"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// The ASN currently travels as an extra field rather than a generated one, so assert on
// the serialized request: that is what the API sees, and it stays valid once the field
// becomes typed.
func ispConfigJSON(t *testing.T, body kernel.ProxyNewParams) map[string]any {
t.Helper()

raw, err := json.Marshal(body)
require.NoError(t, err)

var decoded struct {
Config map[string]any `json:"config"`
}
require.NoError(t, json.Unmarshal(raw, &decoded))
return decoded.Config
}

func TestProxyCreate_ISP_SendsASN(t *testing.T) {
captureOutput(t)

called := false
fake := &FakeProxyService{
NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) {
called = true
assert.Equal(t, "AS6079", ispConfigJSON(t, body)["asn"])

return &kernel.ProxyNewResponse{ID: "isp-new", Name: "RCN ISP", Type: kernel.ProxyNewResponseTypeIsp}, nil
},
}

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "RCN ISP",
Type: "isp",
ASN: "AS6079",
})

require.NoError(t, err)
assert.True(t, called, "expected the create request to be sent")
}

func TestProxyCreate_ISP_SendsASNAlongsideCountry(t *testing.T) {
captureOutput(t)

fake := &FakeProxyService{
NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) {
config := ispConfigJSON(t, body)
assert.Equal(t, "AS6079", config["asn"])
assert.Equal(t, "US", config["country"], "the extra field must not displace country")

return &kernel.ProxyNewResponse{ID: "isp-new", Name: "RCN ISP", Type: kernel.ProxyNewResponseTypeIsp}, nil
},
}

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "RCN ISP",
Type: "isp",
Country: "US",
ASN: "AS6079",
})
require.NoError(t, err)
}

// Omitting --asn must not start sending an empty ASN, which the API would reject as
// malformed rather than treating as "no preference".
func TestProxyCreate_ISP_OmitsEmptyASN(t *testing.T) {
captureOutput(t)

fake := &FakeProxyService{
NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) {
_, present := ispConfigJSON(t, body)["asn"]
assert.False(t, present, "asn should be absent when not requested")

return &kernel.ProxyNewResponse{ID: "isp-new", Name: "Any ISP", Type: kernel.ProxyNewResponseTypeIsp}, nil
},
}

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{Name: "Any ISP", Type: "isp"})
require.NoError(t, err)
}

// The flag is registered on every type, so the types that cannot honour it have to say so
// rather than drop it and hand back a proxy on an arbitrary network.
func TestProxyCreate_ASNRejectedForUnsupportedTypes(t *testing.T) {
tests := []struct {
proxyType string
input ProxyCreateInput
}{
{"datacenter", ProxyCreateInput{Name: "dc", Type: "datacenter", ASN: "AS6079"}},
{"mobile", ProxyCreateInput{Name: "mob", Type: "mobile", Country: "US", ASN: "AS6079"}},
{"custom", ProxyCreateInput{Name: "cus", Type: "custom", Host: "proxy.example.com", Port: 8080, ASN: "AS6079"}},
}

for _, tt := range tests {
t.Run(tt.proxyType, func(t *testing.T) {
captureOutput(t)

fake := &FakeProxyService{
NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) {
require.Failf(t, "unexpected request", "create should not be attempted for %s with --asn", tt.proxyType)
return nil, nil
},
}

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), tt.input)
require.Error(t, err)
assert.Contains(t, err.Error(), "--asn is not supported")
})
}
}

func TestProxyCreate_ZipRejectedForMobile(t *testing.T) {
captureOutput(t)

fake := &FakeProxyService{
NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) {
require.Fail(t, "unexpected request", "create should not be attempted for mobile with --zip")
return nil, nil
},
}

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{Name: "mob", Type: "mobile", Country: "US", Zip: "94107"})
require.Error(t, err)
assert.Contains(t, err.Error(), "--zip is not supported")
}
2 changes: 1 addition & 1 deletion cmd/proxies/proxies.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func init() {
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)")
proxiesCreateCmd.Flags().String("asn", "", "Autonomous system number, e.g. AS6079 (isp, residential)")

// OS flag (residential)
proxiesCreateCmd.Flags().String("os", "", "Operating system (windows|macos|android)")
Expand Down
Loading