Skip to content
Merged
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ Per-category updates are partial — only categories you name are changed; other
- `kernel proxies create` - Create a new proxy configuration
- `--output json`, `-o json` - Output raw JSON object

- `--name <name>` - Proxy configuration name
- `--name <name>` - Proxy configuration name (required)
- `--type <type>` - Proxy type: datacenter, isp, residential, mobile, custom (required)
- `--protocol <http|https>` - Protocol to use (default: https)
- `--country <code>` - ISO 3166 country code or "EU" (location-based types)
Expand All @@ -516,6 +516,7 @@ Per-category updates are partial — only categories you name are changed; other
- `--port <port>` - Proxy port (custom; required)
- `--username <username>` - Username for proxy authentication (custom)
- `--password <password>` - Password for proxy authentication (custom)
- `--ca-bundle <path>` - Path to a PEM-encoded CA certificate bundle (custom TLS-terminating proxies)

- `kernel proxies update <id>` - Rename a proxy configuration (recreate the proxy to change its type or config)
- `--name <name>` - New proxy name (required)
Expand Down Expand Up @@ -873,6 +874,9 @@ kernel proxies create --type datacenter --country US --protocol http --name "US
# Create a custom proxy
kernel proxies create --type custom --host proxy.example.com --port 8080 --username myuser --password mypass --name "My Custom Proxy"

# Create a custom TLS-terminating proxy with a CA bundle
kernel proxies create --type custom --host proxy.example.com --port 8080 --ca-bundle ./proxy-ca.pem --name "My TLS Proxy"

# 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"

Expand Down
51 changes: 32 additions & 19 deletions cmd/proxies/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package proxies
import (
"context"
"fmt"
"os"
"strings"

"github.com/kernel/cli/pkg/table"
Expand Down Expand Up @@ -33,14 +34,14 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
default:
return fmt.Errorf("invalid proxy type: %s", in.Type)
}
if in.Name == "" {
return fmt.Errorf("--name is required")
}

params := kernel.ProxyNewParams{
Name: kernel.Opt(in.Name),
Type: proxyType,
}

if in.Name != "" {
params.Name = kernel.Opt(in.Name)
}
if len(in.BypassHosts) > 0 {
params.BypassHosts = normalizeBypassHosts(in.BypassHosts)
}
Expand Down Expand Up @@ -143,6 +144,16 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
if in.Password != "" {
config.Password = kernel.Opt(in.Password)
}
if in.CaBundleFile != "" {
caBundle, err := os.ReadFile(in.CaBundleFile)
if err != nil {
return fmt.Errorf("failed to read CA bundle file %q: %w", in.CaBundleFile, err)
}
if len(caBundle) == 0 {
return fmt.Errorf("CA bundle file %q is empty", in.CaBundleFile)
}
config.CaBundle = kernel.Opt(string(caBundle))
}
params.Config = kernel.ProxyNewParamsConfigUnion{
OfCustom: &config,
}
Expand Down Expand Up @@ -216,28 +227,30 @@ func runProxiesCreate(cmd *cobra.Command, args []string) error {
port, _ := cmd.Flags().GetInt("port")
username, _ := cmd.Flags().GetString("username")
password, _ := cmd.Flags().GetString("password")
caBundle, _ := cmd.Flags().GetString("ca-bundle")
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,
BypassHosts: bypassHosts,
Country: country,
City: city,
State: state,
Zip: zip,
ASN: asn,
OS: os,
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,
Host: host,
Port: port,
Username: username,
Password: password,
CaBundleFile: caBundle,
Output: output,
})
}

Expand Down
77 changes: 77 additions & 0 deletions cmd/proxies/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package proxies
import (
"context"
"errors"
"os"
"path/filepath"
"testing"

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

func TestProxyCreate_Datacenter_Success(t *testing.T) {
Expand Down Expand Up @@ -133,6 +136,7 @@ func TestProxyCreate_Residential_CityWithoutCountry(t *testing.T) {

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "Residential Proxy",
Type: "residential",
City: "sanfrancisco",
// Missing country
Expand All @@ -147,6 +151,7 @@ func TestProxyCreate_Residential_InvalidOS(t *testing.T) {

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "Residential Proxy",
Type: "residential",
OS: "linux", // Invalid OS
})
Expand All @@ -155,6 +160,16 @@ func TestProxyCreate_Residential_InvalidOS(t *testing.T) {
assert.Contains(t, err.Error(), "invalid OS value: linux (must be windows, macos, or android)")
}

func TestProxyCreate_MissingName(t *testing.T) {
p := ProxyCmd{proxies: &FakeProxyService{}}
err := p.Create(context.Background(), ProxyCreateInput{
Type: "datacenter",
Country: "US",
})

assert.EqualError(t, err, "--name is required")
}

func TestProxyCreate_Mobile_Success(t *testing.T) {
buf := captureOutput(t)

Expand Down Expand Up @@ -225,11 +240,69 @@ func TestProxyCreate_Custom_Success(t *testing.T) {
assert.Contains(t, output, "Successfully created proxy")
}

func TestProxyCreate_Custom_WithCaBundle(t *testing.T) {
caBundle := "-----BEGIN CERTIFICATE-----\ntest certificate\n-----END CERTIFICATE-----\n"
caBundlePath := filepath.Join(t.TempDir(), "proxy-ca.pem")
require.NoError(t, os.WriteFile(caBundlePath, []byte(caBundle), 0o600))

fake := &FakeProxyService{
NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) {
customConfig := body.Config.OfCustom
assert.NotNil(t, customConfig)
assert.True(t, customConfig.CaBundle.Valid())
assert.Equal(t, caBundle, customConfig.CaBundle.Value)

return &kernel.ProxyNewResponse{ID: "custom-ca-new", Type: kernel.ProxyNewResponseTypeCustom}, nil
},
}

err := ProxyCmd{proxies: fake}.Create(context.Background(), ProxyCreateInput{
Name: "Custom Proxy",
Type: "custom",
Host: "proxy.example.com",
Port: 8080,
CaBundleFile: caBundlePath,
})

assert.NoError(t, err)
}

func TestProxyCreate_Custom_CaBundleFileReadError(t *testing.T) {
p := ProxyCmd{proxies: &FakeProxyService{}}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "Custom Proxy",
Type: "custom",
Host: "proxy.example.com",
Port: 8080,
CaBundleFile: "/does/not/exist/proxy-ca.pem",
})

assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to read CA bundle file")
}

func TestProxyCreate_Custom_EmptyCaBundleFile(t *testing.T) {
caBundlePath := filepath.Join(t.TempDir(), "proxy-ca.pem")
require.NoError(t, os.WriteFile(caBundlePath, nil, 0o600))

p := ProxyCmd{proxies: &FakeProxyService{}}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "Custom Proxy",
Type: "custom",
Host: "proxy.example.com",
Port: 8080,
CaBundleFile: caBundlePath,
})

assert.EqualError(t, err, "CA bundle file \""+caBundlePath+"\" is empty")
}

func TestProxyCreate_Custom_MissingHost(t *testing.T) {
fake := &FakeProxyService{}

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "Custom Proxy",
Type: "custom",
Port: 8080,
// Missing required host
Expand All @@ -244,6 +317,7 @@ func TestProxyCreate_Custom_MissingPort(t *testing.T) {

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "Custom Proxy",
Type: "custom",
Host: "proxy.example.com",
// Missing required port (will be 0)
Expand Down Expand Up @@ -289,6 +363,7 @@ func TestProxyCreate_Protocol_Valid(t *testing.T) {

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "Test Proxy",
Type: "datacenter",
Country: "US",
Protocol: tt.protocol,
Expand All @@ -303,6 +378,7 @@ func TestProxyCreate_Protocol_Invalid(t *testing.T) {
fake := &FakeProxyService{}
p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "Test Proxy",
Type: "datacenter",
Country: "US",
Protocol: "ftp",
Expand All @@ -325,6 +401,7 @@ func TestProxyCreate_BypassHosts_Normalized(t *testing.T) {

p := ProxyCmd{proxies: fake}
err := p.Create(context.Background(), ProxyCreateInput{
Name: "Test Proxy",
Type: "datacenter",
Country: "US",
BypassHosts: []string{" localhost ", "", "internal.service.local"},
Expand Down
13 changes: 9 additions & 4 deletions cmd/proxies/proxies.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,16 @@ Examples:
kernel proxies create --type datacenter --country US --name "US Datacenter"

# Create a custom proxy
kernel 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 --name "My Custom Proxy"

# Create a custom TLS-terminating proxy with a CA bundle
kernel proxies create --type custom --host proxy.example.com --port 8080 --ca-bundle ./proxy-ca.pem --name "My TLS Proxy"

# Create a residential proxy with location
kernel proxies create --type residential --country US --city sanfrancisco --state CA
kernel proxies create --type residential --country US --city sanfrancisco --state CA --name "SF Residential"

# Create a proxy with bypass hosts
kernel proxies create --type datacenter --country US --bypass-host localhost,internal.service.local`,
kernel proxies create --type datacenter --country US --bypass-host localhost,internal.service.local --name "Internal Services"`,
RunE: runProxiesCreate,
}

Expand Down Expand Up @@ -96,7 +99,8 @@ func init() {
addJSONOutputFlag(proxiesCreateCmd)

// Add flags for create command
proxiesCreateCmd.Flags().String("name", "", "Proxy configuration name")
proxiesCreateCmd.Flags().String("name", "", "Proxy configuration name (required)")
_ = proxiesCreateCmd.MarkFlagRequired("name")
proxiesCreateCmd.Flags().String("type", "", "Proxy type (datacenter|isp|residential|mobile|custom)")
_ = proxiesCreateCmd.MarkFlagRequired("type")
proxiesCreateCmd.Flags().String("protocol", "https", "Protocol to use for the proxy connection (http|https)")
Expand All @@ -116,6 +120,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().String("ca-bundle", "", "Path to a PEM-encoded CA certificate bundle for a TLS-terminating custom proxy")
proxiesCreateCmd.Flags().StringSlice("bypass-host", nil, "Hostname(s) to bypass proxy and connect directly (repeat or comma-separated)")

// Update flags
Expand Down
11 changes: 6 additions & 5 deletions cmd/proxies/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@ type ProxyCreateInput struct {
ASN string
OS string
// Custom proxy config
Host string
Port int
Username string
Password string
Output string
Host string
Port int
Username string
Password string
CaBundleFile string
Output string
}

type ProxyUpdateInput struct {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.83.1-0.20260724213320-12b3ec62f63e
github.com/kernel/kernel-go-sdk v0.85.0
github.com/klauspost/compress v1.18.5
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/pterm/pterm v0.12.80
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.83.1-0.20260724213320-12b3ec62f63e h1:Pe1fRnIM4Zf7ddVg/wd+qTD0X4t2NmXRIeXyY98AILI=
github.com/kernel/kernel-go-sdk v0.83.1-0.20260724213320-12b3ec62f63e/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
github.com/kernel/kernel-go-sdk v0.85.0 h1:rACZOx5dcjO4rasFmMoh2GS14w4yRBbYXxb92eGvD2E=
github.com/kernel/kernel-go-sdk v0.85.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
Expand Down
Loading