Skip to content

Commit 68684f7

Browse files
committed
Merge remote-tracking branch 'origin/main' into test-script
2 parents faaab88 + bf18df0 commit 68684f7

31 files changed

+4342
-635
lines changed

README.md

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ CloudProxy exposes an API and modern UI for managing your proxy infrastructure.
6060
* Modern UI with real-time updates
6161
* Interactive API documentation
6262
* Multi-provider support
63+
* Multiple accounts per provider
6364
* Automatic proxy rotation
6465
* Health monitoring
6566
* Easy scaling controls
@@ -158,6 +159,43 @@ my_request = requests.get("https://api.ipify.org", proxies=proxies)
158159

159160
For detailed API documentation, see [API Documentation](docs/api.md).
160161

162+
## Multi-Account Provider Support
163+
164+
CloudProxy now supports multiple accounts per provider, allowing you to:
165+
166+
- Use multiple API keys or access tokens for the same provider
167+
- Configure different regions, sizes, and scaling parameters per account
168+
- Organize proxies by account/instance for better management
169+
- Scale each account independently
170+
171+
Each provider can have multiple "instances", which represent different accounts or configurations. Each instance has its own:
172+
173+
- Scaling parameters (min/max)
174+
- Region settings
175+
- Size configuration
176+
- API credentials
177+
- IP addresses
178+
179+
To configure multiple instances, use environment variables with the instance name in the format:
180+
```
181+
PROVIDERNAME_INSTANCENAME_VARIABLE
182+
```
183+
184+
For example, to configure two DigitalOcean accounts:
185+
```shell
186+
# Default DigitalOcean account
187+
DIGITALOCEAN_ENABLED=True
188+
DIGITALOCEAN_ACCESS_TOKEN=your_first_token
189+
DIGITALOCEAN_DEFAULT_REGION=lon1
190+
DIGITALOCEAN_DEFAULT_MIN_SCALING=2
191+
192+
# Second DigitalOcean account
193+
DIGITALOCEAN_SECONDACCOUNT_ENABLED=True
194+
DIGITALOCEAN_SECONDACCOUNT_ACCESS_TOKEN=your_second_token
195+
DIGITALOCEAN_SECONDACCOUNT_REGION=nyc1
196+
DIGITALOCEAN_SECONDACCOUNT_MIN_SCALING=3
197+
```
198+
161199
## CloudProxy API Examples
162200

163201
### List available proxy servers
@@ -287,7 +325,31 @@ For detailed API documentation, see [API Documentation](docs/api.md).
287325
"max_scaling": 2
288326
},
289327
"size": "s-1vcpu-1gb",
290-
"region": "lon1"
328+
"region": "lon1",
329+
"instances": {
330+
"default": {
331+
"enabled": true,
332+
"ips": ["192.168.1.1"],
333+
"scaling": {
334+
"min_scaling": 2,
335+
"max_scaling": 2
336+
},
337+
"size": "s-1vcpu-1gb",
338+
"region": "lon1",
339+
"display_name": "Default Account"
340+
},
341+
"secondary": {
342+
"enabled": true,
343+
"ips": ["192.168.1.2"],
344+
"scaling": {
345+
"min_scaling": 1,
346+
"max_scaling": 3
347+
},
348+
"size": "s-1vcpu-1gb",
349+
"region": "nyc1",
350+
"display_name": "US Account"
351+
}
352+
}
291353
},
292354
"aws": {
293355
"enabled": false,
@@ -335,6 +397,72 @@ For detailed API documentation, see [API Documentation](docs/api.md).
335397
}
336398
}
337399
```
400+
401+
### Get provider instance
402+
#### Request
403+
404+
`GET /providers/digitalocean/secondary`
405+
406+
curl -X 'GET' 'http://localhost:8000/providers/digitalocean/secondary' -H 'accept: application/json'
407+
408+
#### Response
409+
```json
410+
{
411+
"metadata": {
412+
"request_id": "123e4567-e89b-12d3-a456-426614174000",
413+
"timestamp": "2024-02-24T08:00:00Z"
414+
},
415+
"message": "Provider 'digitalocean' instance 'secondary' configuration retrieved successfully",
416+
"provider": "digitalocean",
417+
"instance": "secondary",
418+
"config": {
419+
"enabled": true,
420+
"ips": ["192.168.1.2"],
421+
"scaling": {
422+
"min_scaling": 1,
423+
"max_scaling": 3
424+
},
425+
"size": "s-1vcpu-1gb",
426+
"region": "nyc1",
427+
"display_name": "US Account"
428+
}
429+
}
430+
```
431+
432+
### Update provider instance scaling
433+
#### Request
434+
435+
`PATCH /providers/digitalocean/secondary`
436+
437+
curl -X 'PATCH' 'http://localhost:8000/providers/digitalocean/secondary' \
438+
-H 'accept: application/json' \
439+
-H 'Content-Type: application/json' \
440+
-d '{"min_scaling": 2, "max_scaling": 5}'
441+
442+
#### Response
443+
```json
444+
{
445+
"metadata": {
446+
"request_id": "123e4567-e89b-12d3-a456-426614174000",
447+
"timestamp": "2024-02-24T08:00:00Z"
448+
},
449+
"message": "Provider 'digitalocean' instance 'secondary' scaling configuration updated successfully",
450+
"provider": "digitalocean",
451+
"instance": "secondary",
452+
"config": {
453+
"enabled": true,
454+
"ips": ["192.168.1.2"],
455+
"scaling": {
456+
"min_scaling": 2,
457+
"max_scaling": 5
458+
},
459+
"size": "s-1vcpu-1gb",
460+
"region": "nyc1",
461+
"display_name": "US Account"
462+
}
463+
}
464+
```
465+
338466
CloudProxy runs on a schedule of every 30 seconds, it will check if the minimum scaling has been met, if not then it will deploy the required number of proxies. The new proxy info will appear in IPs once they are deployed and ready to be used.
339467

340468
<!-- ROADMAP -->

cloudproxy-ui/src/components/ListProxies.vue

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
<template>
22
<div>
33
<div
4-
v-for="provider in sortedProviders"
5-
:key="provider.key"
4+
v-for="provider in sortedProviderInstances"
5+
:key="`${provider.providerKey}-${provider.instanceKey}`"
66
class="provider-section"
77
>
88
<div class="provider-header">
99
<div class="d-flex align-items-center justify-content-between">
1010
<div class="d-flex align-items-center">
1111
<div class="provider-icon-wrapper me-2">
1212
<i
13-
:class="'bi bi-' + getProviderIcon(provider.key)"
13+
:class="'bi bi-' + getProviderIcon(provider.providerKey)"
1414
class="provider-icon"
1515
style="font-size: 1.5rem;"
1616
/>
1717
</div>
1818
<h2 class="mb-0">
19-
{{ formatProviderName(provider.key) }}
19+
{{ provider.data.display_name || formatProviderName(provider.providerKey, provider.instanceKey) }}
2020
</h2>
2121
</div>
2222
<form
2323
class="scaling-control"
24-
@submit.prevent="updateProvider(provider.key, provider.data.scaling.min_scaling)"
24+
@submit.prevent="updateProvider(provider.providerKey, provider.instanceKey, provider.data.scaling.min_scaling)"
2525
>
2626
<div class="d-flex align-items-center">
2727
<span
@@ -46,7 +46,7 @@
4646
min="0"
4747
max="100"
4848
class="form-control custom-spinbutton"
49-
@change="updateProvider(provider.key, $event.target.value)"
49+
@change="updateProvider(provider.providerKey, provider.instanceKey, $event.target.value)"
5050
>
5151
</div>
5252
</form>
@@ -198,37 +198,74 @@ export default {
198198
auth_enabled: true
199199
});
200200
201-
const sortedProviders = computed(() => {
202-
// Convert data object to array of {key, data} pairs
203-
const providers = Object.entries(data.value).map(([key, providerData]) => ({
204-
key,
205-
data: providerData
206-
}));
201+
const sortedProviderInstances = computed(() => {
202+
// Create array of provider instances
203+
const providers = [];
204+
205+
// Loop through each provider
206+
Object.entries(data.value).forEach(([providerKey, providerData]) => {
207+
// Handle both old format (without instances) and new format (with instances)
208+
if (providerData.instances) {
209+
// New format with instances
210+
Object.entries(providerData.instances).forEach(([instanceKey, instanceData]) => {
211+
providers.push({
212+
providerKey,
213+
instanceKey,
214+
data: instanceData
215+
});
216+
});
217+
} else {
218+
// Old format for backward compatibility
219+
providers.push({
220+
providerKey,
221+
instanceKey: 'default',
222+
data: providerData
223+
});
224+
}
225+
});
207226
208227
// Sort enabled providers first, then by name
209228
return providers.sort((a, b) => {
210229
if (a.data.enabled && !b.data.enabled) return -1;
211230
if (!a.data.enabled && b.data.enabled) return 1;
212-
return a.key.localeCompare(b.key);
231+
232+
// If same provider type, sort by instance name
233+
if (a.providerKey === b.providerKey) {
234+
// Keep 'default' instance first
235+
if (a.instanceKey === 'default') return -1;
236+
if (b.instanceKey === 'default') return 1;
237+
return a.instanceKey.localeCompare(b.instanceKey);
238+
}
239+
240+
return a.providerKey.localeCompare(b.providerKey);
213241
});
214242
});
215243
216-
const formatProviderName = (name) => {
244+
const formatProviderName = (name, instance = 'default') => {
217245
const specialCases = {
218246
'digitalocean': 'DigitalOcean',
219247
'aws': 'AWS',
220248
'gcp': 'GCP',
221-
'hetzner': 'Hetzner'
249+
'hetzner': 'Hetzner',
250+
'azure': 'Azure'
222251
};
223-
return specialCases[name] || name.charAt(0).toUpperCase() + name.slice(1);
252+
253+
const providerName = specialCases[name] || name.charAt(0).toUpperCase() + name.slice(1);
254+
255+
if (instance === 'default') {
256+
return providerName;
257+
} else {
258+
return `${providerName} (${instance})`;
259+
}
224260
};
225261
226262
const getProviderIcon = (provider) => {
227263
const icons = {
228264
digitalocean: 'water',
229265
aws: 'cloud-fill',
230266
gcp: 'google',
231-
hetzner: 'hdd-rack'
267+
hetzner: 'hdd-rack',
268+
azure: 'microsoft'
232269
};
233270
return icons[provider] || 'cloud-fill';
234271
};
@@ -289,10 +326,15 @@ export default {
289326
}
290327
};
291328
292-
const updateProvider = async (provider, min_scaling) => {
329+
const updateProvider = async (provider, instance, min_scaling) => {
293330
try {
331+
let update_url = `/providers/${provider}`;
332+
if (instance !== 'default') {
333+
update_url += `/${instance}`;
334+
}
335+
294336
const update_res = await fetch(
295-
"/providers/" + provider,
337+
update_url,
296338
{
297339
method: "PATCH",
298340
headers: {
@@ -373,7 +415,7 @@ export default {
373415
data,
374416
listremove_data,
375417
auth,
376-
sortedProviders,
418+
sortedProviderInstances,
377419
formatProviderName,
378420
getProviderIcon,
379421
removeProxy,

cloudproxy/check.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def fetch_ip(ip_address):
5151

5252
def check_alive(ip_address):
5353
try:
54-
result = requests.get("http://ipecho.net/plain", proxies={'http': "http://" + ip_address + ":8899"}, timeout=3)
54+
result = requests.get("http://ipecho.net/plain", proxies={'http': "http://" + ip_address + ":8899"}, timeout=10)
5555
if result.status_code in (200, 407):
5656
return True
5757
else:

0 commit comments

Comments
 (0)