Skip to content

Commit dd88c41

Browse files
authored
Add proxies feature (#18)
<!-- mesa-description-start --> ## TL;DR This PR introduces a new `proxies` command, allowing users to create, list, get, and delete various types of proxies. The `browsers create` command is also updated to allow launching a browser session with a specified proxy. ## Why we made these changes To provide users with integrated tools for managing and utilizing proxies directly within the CLI. This simplifies workflows that require running browser sessions from different geolocations or under specific network configurations, removing the need for external proxy management. ## What changed? - **New `proxies` Command (`cmd/proxies/`)**: - Introduced a new command for full lifecycle management of proxies (`proxies.go`, `types.go`). - `create`: Added functionality to create various proxy types (datacenter, residential, etc.) with validation (`create.go`). - `list`: Implemented a command to display all configured proxies in a formatted table (`list.go`). - `get`: Added a command to retrieve and display detailed information for a single proxy by ID (`get.go`). - `delete`: Implemented a command to remove a proxy with an interactive confirmation prompt (`delete.go`). - **Browser Integration (`cmd/browsers.go`)**: - Updated the `browsers create` command with a `--proxy-id` flag to attach a managed proxy to a new browser session. - **CLI Integration (`cmd/root.go`)**: - Registered the new `proxies` command into the root CLI application. - **Dependencies (`go.mod`, `go.sum`)**: - Updated module dependencies to support the new functionality. ## Validation - **Comprehensive Unit Tests (`cmd/proxies/*_test.go`)**: - Added extensive tests for all `proxies` subcommands, covering successful creation, retrieval, listing, and deletion of all supported proxy types. - Ensured robust error handling for invalid input, missing fields, and API failures. - **Mock Service (`cmd/proxies/common_test.go`)**: - Implemented a `FakeProxyService` to mock API interactions, ensuring reliable and isolated testing. <sup>_Description generated by Mesa. [Update settings](https://app.mesa.dev/onkernel/settings/pull-requests)_</sup> <!-- mesa-description-end -->
1 parent 86424ea commit dd88c41

File tree

15 files changed

+1472
-6
lines changed

15 files changed

+1472
-6
lines changed

cmd/browsers.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ type BrowsersCreateInput struct {
8282
ProfileID string
8383
ProfileName string
8484
ProfileSaveChanges BoolFlag
85+
ProxyID string
8586
}
8687

8788
type BrowsersDeleteInput struct {
@@ -178,6 +179,11 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
178179
}
179180
}
180181

182+
// Add proxy if specified
183+
if in.ProxyID != "" {
184+
params.ProxyID = kernel.Opt(in.ProxyID)
185+
}
186+
181187
browser, err := b.browsers.New(ctx, params)
182188
if err != nil {
183189
return util.CleanedUpSdkError{Err: err}
@@ -1321,6 +1327,7 @@ func init() {
13211327
browsersCreateCmd.Flags().String("profile-id", "", "Profile ID to load into the browser session (mutually exclusive with --profile-name)")
13221328
browsersCreateCmd.Flags().String("profile-name", "", "Profile name to load into the browser session (mutually exclusive with --profile-id)")
13231329
browsersCreateCmd.Flags().Bool("save-changes", false, "If set, save changes back to the profile when the session ends")
1330+
browsersCreateCmd.Flags().String("proxy-id", "", "Proxy ID to use for the browser session")
13241331

13251332
// Add flags for delete command
13261333
browsersDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
@@ -1346,6 +1353,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
13461353
profileID, _ := cmd.Flags().GetString("profile-id")
13471354
profileName, _ := cmd.Flags().GetString("profile-name")
13481355
saveChanges, _ := cmd.Flags().GetBool("save-changes")
1356+
proxyID, _ := cmd.Flags().GetString("proxy-id")
13491357

13501358
in := BrowsersCreateInput{
13511359
PersistenceID: persistenceID,
@@ -1355,6 +1363,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
13551363
ProfileID: profileID,
13561364
ProfileName: profileName,
13571365
ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges},
1366+
ProxyID: proxyID,
13581367
}
13591368

13601369
svc := client.Browsers

cmd/proxies/common_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package proxies
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"os"
7+
"testing"
8+
9+
"github.com/onkernel/kernel-go-sdk"
10+
"github.com/onkernel/kernel-go-sdk/option"
11+
"github.com/pterm/pterm"
12+
)
13+
14+
// captureOutput sets pterm writers for tests
15+
func captureOutput(t *testing.T) *bytes.Buffer {
16+
var buf bytes.Buffer
17+
pterm.SetDefaultOutput(&buf)
18+
pterm.Info.Writer = &buf
19+
pterm.Error.Writer = &buf
20+
pterm.Success.Writer = &buf
21+
pterm.Warning.Writer = &buf
22+
pterm.Debug.Writer = &buf
23+
pterm.Fatal.Writer = &buf
24+
pterm.DefaultTable = *pterm.DefaultTable.WithWriter(&buf)
25+
t.Cleanup(func() {
26+
pterm.SetDefaultOutput(os.Stdout)
27+
pterm.Info.Writer = os.Stdout
28+
pterm.Error.Writer = os.Stdout
29+
pterm.Success.Writer = os.Stdout
30+
pterm.Warning.Writer = os.Stdout
31+
pterm.Debug.Writer = os.Stdout
32+
pterm.Fatal.Writer = os.Stdout
33+
pterm.DefaultTable = *pterm.DefaultTable.WithWriter(os.Stdout)
34+
})
35+
return &buf
36+
}
37+
38+
// FakeProxyService implements ProxyService for testing
39+
type FakeProxyService struct {
40+
ListFunc func(ctx context.Context, opts ...option.RequestOption) (*[]kernel.ProxyListResponse, error)
41+
GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error)
42+
NewFunc func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error)
43+
DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error
44+
}
45+
46+
func (f *FakeProxyService) List(ctx context.Context, opts ...option.RequestOption) (*[]kernel.ProxyListResponse, error) {
47+
if f.ListFunc != nil {
48+
return f.ListFunc(ctx, opts...)
49+
}
50+
empty := []kernel.ProxyListResponse{}
51+
return &empty, nil
52+
}
53+
54+
func (f *FakeProxyService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) {
55+
if f.GetFunc != nil {
56+
return f.GetFunc(ctx, id, opts...)
57+
}
58+
return &kernel.ProxyGetResponse{ID: id, Type: kernel.ProxyGetResponseTypeDatacenter}, nil
59+
}
60+
61+
func (f *FakeProxyService) New(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) {
62+
if f.NewFunc != nil {
63+
return f.NewFunc(ctx, body, opts...)
64+
}
65+
return &kernel.ProxyNewResponse{ID: "new-proxy", Type: kernel.ProxyNewResponseTypeDatacenter}, nil
66+
}
67+
68+
func (f *FakeProxyService) Delete(ctx context.Context, id string, opts ...option.RequestOption) error {
69+
if f.DeleteFunc != nil {
70+
return f.DeleteFunc(ctx, id, opts...)
71+
}
72+
return nil
73+
}
74+
75+
// Helper function to create test proxy responses
76+
func createDatacenterProxy(id, name, country string) kernel.ProxyListResponse {
77+
return kernel.ProxyListResponse{
78+
ID: id,
79+
Name: name,
80+
Type: kernel.ProxyListResponseTypeDatacenter,
81+
Config: kernel.ProxyListResponseConfigUnion{
82+
Country: country,
83+
},
84+
}
85+
}
86+
87+
func createResidentialProxy(id, name, country, city, state string) kernel.ProxyListResponse {
88+
return kernel.ProxyListResponse{
89+
ID: id,
90+
Name: name,
91+
Type: kernel.ProxyListResponseTypeResidential,
92+
Config: kernel.ProxyListResponseConfigUnion{
93+
Country: country,
94+
City: city,
95+
State: state,
96+
},
97+
}
98+
}
99+
100+
func createCustomProxy(id, name, host string, port int64) kernel.ProxyListResponse {
101+
return kernel.ProxyListResponse{
102+
ID: id,
103+
Name: name,
104+
Type: kernel.ProxyListResponseTypeCustom,
105+
Config: kernel.ProxyListResponseConfigUnion{
106+
Host: host,
107+
Port: port,
108+
},
109+
}
110+
}

cmd/proxies/create.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package proxies
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/onkernel/cli/pkg/util"
8+
"github.com/onkernel/kernel-go-sdk"
9+
"github.com/pterm/pterm"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
14+
// Validate proxy type
15+
var proxyType kernel.ProxyNewParamsType
16+
switch in.Type {
17+
case "datacenter":
18+
proxyType = kernel.ProxyNewParamsTypeDatacenter
19+
case "isp":
20+
proxyType = kernel.ProxyNewParamsTypeIsp
21+
case "residential":
22+
proxyType = kernel.ProxyNewParamsTypeResidential
23+
case "mobile":
24+
proxyType = kernel.ProxyNewParamsTypeMobile
25+
case "custom":
26+
proxyType = kernel.ProxyNewParamsTypeCustom
27+
default:
28+
return fmt.Errorf("invalid proxy type: %s", in.Type)
29+
}
30+
31+
params := kernel.ProxyNewParams{
32+
Type: proxyType,
33+
}
34+
35+
if in.Name != "" {
36+
params.Name = kernel.Opt(in.Name)
37+
}
38+
39+
// Build config based on type
40+
switch proxyType {
41+
case kernel.ProxyNewParamsTypeDatacenter:
42+
if in.Country == "" {
43+
return fmt.Errorf("--country is required for datacenter proxy type")
44+
}
45+
params.Config = kernel.ProxyNewParamsConfigUnion{
46+
OfProxyNewsConfigDatacenterProxyConfig: &kernel.ProxyNewParamsConfigDatacenterProxyConfig{
47+
Country: in.Country,
48+
},
49+
}
50+
51+
case kernel.ProxyNewParamsTypeIsp:
52+
if in.Country == "" {
53+
return fmt.Errorf("--country is required for ISP proxy type")
54+
}
55+
params.Config = kernel.ProxyNewParamsConfigUnion{
56+
OfProxyNewsConfigIspProxyConfig: &kernel.ProxyNewParamsConfigIspProxyConfig{
57+
Country: in.Country,
58+
},
59+
}
60+
61+
case kernel.ProxyNewParamsTypeResidential:
62+
config := kernel.ProxyNewParamsConfigResidentialProxyConfig{}
63+
64+
// Validate that if city is provided, country must also be provided
65+
if in.City != "" && in.Country == "" {
66+
return fmt.Errorf("--country is required when --city is specified")
67+
}
68+
69+
if in.Country != "" {
70+
config.Country = kernel.Opt(in.Country)
71+
}
72+
if in.City != "" {
73+
config.City = kernel.Opt(in.City)
74+
}
75+
if in.State != "" {
76+
config.State = kernel.Opt(in.State)
77+
}
78+
if in.Zip != "" {
79+
config.Zip = kernel.Opt(in.Zip)
80+
}
81+
if in.ASN != "" {
82+
config.Asn = kernel.Opt(in.ASN)
83+
}
84+
if in.OS != "" {
85+
// Validate OS value
86+
switch in.OS {
87+
case "windows", "macos", "android":
88+
config.Os = in.OS
89+
default:
90+
return fmt.Errorf("invalid OS value: %s (must be windows, macos, or android)", in.OS)
91+
}
92+
}
93+
params.Config = kernel.ProxyNewParamsConfigUnion{
94+
OfProxyNewsConfigResidentialProxyConfig: &config,
95+
}
96+
97+
case kernel.ProxyNewParamsTypeMobile:
98+
config := kernel.ProxyNewParamsConfigMobileProxyConfig{}
99+
100+
// Validate that if city is provided, country must also be provided
101+
if in.City != "" && in.Country == "" {
102+
return fmt.Errorf("--country is required when --city is specified")
103+
}
104+
105+
if in.Country != "" {
106+
config.Country = kernel.Opt(in.Country)
107+
}
108+
if in.City != "" {
109+
config.City = kernel.Opt(in.City)
110+
}
111+
if in.State != "" {
112+
config.State = kernel.Opt(in.State)
113+
}
114+
if in.Zip != "" {
115+
config.Zip = kernel.Opt(in.Zip)
116+
}
117+
if in.ASN != "" {
118+
config.Asn = kernel.Opt(in.ASN)
119+
}
120+
if in.Carrier != "" {
121+
// The API will validate the carrier value
122+
config.Carrier = in.Carrier
123+
}
124+
params.Config = kernel.ProxyNewParamsConfigUnion{
125+
OfProxyNewsConfigMobileProxyConfig: &config,
126+
}
127+
128+
case kernel.ProxyNewParamsTypeCustom:
129+
if in.Host == "" {
130+
return fmt.Errorf("--host is required for custom proxy type")
131+
}
132+
if in.Port == 0 {
133+
return fmt.Errorf("--port is required for custom proxy type")
134+
}
135+
136+
config := kernel.ProxyNewParamsConfigCreateCustomProxyConfig{
137+
Host: in.Host,
138+
Port: int64(in.Port),
139+
}
140+
if in.Username != "" {
141+
config.Username = kernel.Opt(in.Username)
142+
}
143+
if in.Password != "" {
144+
config.Password = kernel.Opt(in.Password)
145+
}
146+
params.Config = kernel.ProxyNewParamsConfigUnion{
147+
OfProxyNewsConfigCreateCustomProxyConfig: &config,
148+
}
149+
}
150+
151+
pterm.Info.Printf("Creating %s proxy...\n", proxyType)
152+
153+
proxy, err := p.proxies.New(ctx, params)
154+
if err != nil {
155+
return util.CleanedUpSdkError{Err: err}
156+
}
157+
158+
pterm.Success.Printf("Successfully created proxy\n")
159+
160+
// Display created proxy details
161+
rows := pterm.TableData{{"Property", "Value"}}
162+
rows = append(rows, []string{"ID", proxy.ID})
163+
164+
name := proxy.Name
165+
if name == "" {
166+
name = "-"
167+
}
168+
rows = append(rows, []string{"Name", name})
169+
rows = append(rows, []string{"Type", string(proxy.Type)})
170+
171+
PrintTableNoPad(rows, true)
172+
return nil
173+
}
174+
175+
func runProxiesCreate(cmd *cobra.Command, args []string) error {
176+
client := util.GetKernelClient(cmd)
177+
178+
// Get all flag values
179+
proxyType, _ := cmd.Flags().GetString("type")
180+
name, _ := cmd.Flags().GetString("name")
181+
country, _ := cmd.Flags().GetString("country")
182+
city, _ := cmd.Flags().GetString("city")
183+
state, _ := cmd.Flags().GetString("state")
184+
zip, _ := cmd.Flags().GetString("zip")
185+
asn, _ := cmd.Flags().GetString("asn")
186+
os, _ := cmd.Flags().GetString("os")
187+
carrier, _ := cmd.Flags().GetString("carrier")
188+
host, _ := cmd.Flags().GetString("host")
189+
port, _ := cmd.Flags().GetInt("port")
190+
username, _ := cmd.Flags().GetString("username")
191+
password, _ := cmd.Flags().GetString("password")
192+
193+
svc := client.Proxies
194+
p := ProxyCmd{proxies: &svc}
195+
return p.Create(cmd.Context(), ProxyCreateInput{
196+
Name: name,
197+
Type: proxyType,
198+
Country: country,
199+
City: city,
200+
State: state,
201+
Zip: zip,
202+
ASN: asn,
203+
OS: os,
204+
Carrier: carrier,
205+
Host: host,
206+
Port: port,
207+
Username: username,
208+
Password: password,
209+
})
210+
}

0 commit comments

Comments
 (0)