Thanks to visit codestin.com
Credit goes to github.com

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
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ PROXY_PASSWORD=changeme
# Optional: Proxy age limit in seconds (0 = disabled)
# AGE_LIMIT=0

# ====================
# Rolling Deployment Settings
# ====================
# Optional: Enable rolling deployments for zero-downtime proxy recycling
# ROLLING_DEPLOYMENT=False

# Optional: Minimum number of proxies to keep available during recycling
# ROLLING_MIN_AVAILABLE=3

# Optional: Maximum number of proxies to recycle simultaneously
# ROLLING_BATCH_SIZE=2

# ====================
# DigitalOcean Provider
# ====================
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
- [Web Interface](#web-interface)
- [API Documentation](#api-documentation)
- [Programmatic Usage](#programmatic-usage)
- [Rolling Deployments](#rolling-deployments)
- [Multi-Account Provider Support](#multi-account-provider-support)
- [API Examples](#cloudproxy-api-examples)
- [Roadmap](#roadmap)
Expand Down Expand Up @@ -85,6 +86,7 @@ CloudProxy exposes an API and modern UI for managing your proxy infrastructure.
* Multi-provider support
* Multiple accounts per provider
* Automatic proxy rotation
* **Rolling deployments** - Zero-downtime proxy recycling
* Health monitoring
* Fixed proxy pool management (maintains target count)

Expand Down Expand Up @@ -293,6 +295,47 @@ my_request = requests.get("https://api.ipify.org", proxies=proxies)

For more detailed examples of using CloudProxy as a Python package, see the [Python Package Usage Guide](docs/python-package-usage.md).

## Rolling Deployments

CloudProxy supports rolling deployments to ensure zero-downtime proxy recycling. This feature maintains a minimum number of healthy proxies during age-based recycling operations.

### Configuration

Enable rolling deployments with these environment variables:

```bash
# Enable rolling deployments
ROLLING_DEPLOYMENT=True

# Minimum proxies to keep available during recycling
ROLLING_MIN_AVAILABLE=3

# Maximum proxies to recycle simultaneously
ROLLING_BATCH_SIZE=2
```

### How It Works

When proxies reach their age limit:
1. The system checks if recycling would violate minimum availability
2. Proxies are recycled in batches to maintain service continuity
3. New proxies are created as old ones are removed
4. The process continues until all aged proxies are replaced

### Monitoring

Check rolling deployment status via the API:

```bash
# Get overall status
curl http://localhost:8000/rolling

# Get provider-specific status
curl http://localhost:8000/rolling/digitalocean
```

For detailed documentation, see the [Rolling Deployments Guide](docs/rolling-deployments.md).

## Multi-Account Provider Support

CloudProxy now supports multiple accounts per provider, allowing you to:
Expand Down
3 changes: 3 additions & 0 deletions cloudproxy-ui/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
</div>

<div class="container">
<RollingConfig />
<div class="card main-card">
<div class="card-body p-0">
<ListProxies />
Expand All @@ -46,11 +47,13 @@

<script>
import ListProxies from "./components/ListProxies.vue";
import RollingConfig from "./components/RollingConfig.vue";

export default {
name: "App",
components: {
ListProxies,
RollingConfig,
},
setup() {
const reloadPage = () => {
Expand Down
119 changes: 118 additions & 1 deletion cloudproxy-ui/src/components/ListProxies.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
<i class="bi bi-hdd-stack me-1" />
{{ provider.data.ips.length }} Active
</span>
<span
v-if="getRollingInfo(provider.providerKey, provider.instanceKey)"
class="status-badge ms-2"
>
<i class="bi bi-arrow-repeat me-1" />
{{ getRollingInfo(provider.providerKey, provider.instanceKey) }}
</span>
<label
for="sb-inline"
class="mx-3"
Expand Down Expand Up @@ -97,6 +104,12 @@
>
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<span
:class="getStatusBadgeClass(getProxyStatus(provider.providerKey, provider.instanceKey, ips))"
class="badge me-2"
>
{{ getStatusLabel(getProxyStatus(provider.providerKey, provider.instanceKey, ips)) }}
</span>
<div
v-tooltip="'Proxy is active and responding'"
class="proxy-status"
Expand Down Expand Up @@ -192,6 +205,7 @@ export default {
const toast = useToast();
const data = ref({});
const listremove_data = ref([]);
const rollingStatus = ref({});
const auth = ref({
username: '',
password: '',
Expand Down Expand Up @@ -277,6 +291,9 @@ export default {
const res = await fetch("/providers");
const responseData = await res.json();
data.value = responseData.providers;

// Also fetch rolling status
fetchRollingStatus();
} catch (error) {
toast.show('Failed to fetch providers', {
title: 'Error',
Expand All @@ -286,6 +303,71 @@ export default {
});
}
};

const fetchRollingStatus = async () => {
try {
const res = await fetch("/rolling");
const responseData = await res.json();
rollingStatus.value = responseData.status || {};
} catch (error) {
// Silently fail - rolling status is optional
console.error('Failed to fetch rolling status:', error);
}
};

const getProxyStatus = (providerKey, instanceKey, proxyIp) => {
const statusKey = `${providerKey}/${instanceKey}`;
const status = rollingStatus.value[statusKey];

if (!status) return 'healthy';

if (status.recycling_ips && status.recycling_ips.includes(proxyIp)) {
return 'recycling';
}
if (status.pending_recycle_ips && status.pending_recycle_ips.includes(proxyIp)) {
return 'pending_recycle';
}

return 'healthy';
};

const getStatusBadgeClass = (status) => {
switch(status) {
case 'recycling':
return 'bg-warning text-dark';
case 'pending_recycle':
return 'bg-info';
default:
return 'bg-success';
}
};

const getStatusLabel = (status) => {
switch(status) {
case 'recycling':
return 'Recycling';
case 'pending_recycle':
return 'Pending Recycle';
default:
return 'Healthy';
}
};

const getRollingInfo = (providerKey, instanceKey) => {
const statusKey = `${providerKey}/${instanceKey}`;
const status = rollingStatus.value[statusKey];

if (!status) return null;

const recyclingCount = status.recycling || 0;
const pendingCount = status.pending_recycle || 0;

if (recyclingCount > 0 || pendingCount > 0) {
return `${recyclingCount} recycling, ${pendingCount} pending`;
}

return null;
};

const removeProxy = async (proxy) => {
try {
Expand Down Expand Up @@ -401,9 +483,13 @@ export default {
};

onMounted(() => {
// Initial load
getAuthSettings();

setInterval(() => {
getName();
listremoveProxy();
fetchRollingStatus();
}, 3000);
});

Expand All @@ -417,13 +503,19 @@ export default {
data,
listremove_data,
auth,
rollingStatus,
sortedProviderInstances,
formatProviderName,
getProviderIcon,
removeProxy,
updateProvider,
makeToast,
copyToClipboard
copyToClipboard,
fetchRollingStatus,
getProxyStatus,
getStatusBadgeClass,
getStatusLabel,
getRollingInfo
};
}
};
Expand Down Expand Up @@ -563,4 +655,29 @@ h2 {
border-radius: 8px;
color: var(--text-gray);
}

/* Status badges for proxy health */
.badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}

.badge.bg-success {
background-color: #28a745;
color: white;
}

.badge.bg-warning {
background-color: #ffc107;
color: #212529;
}

.badge.bg-info {
background-color: #17a2b8;
color: white;
}
</style>
Loading