init projectmycelium
This commit is contained in:
@@ -0,0 +1,633 @@
|
||||
# ThreeFold Grid Deployment Automation Specification
|
||||
|
||||
**Document Purpose**: Technical specification for automated deployment pipeline from TFC payment to ThreeFold Grid service activation.
|
||||
|
||||
**Last Updated**: 2025-08-04
|
||||
**Status**: Implementation Ready
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the automated deployment pipeline that converts TFC credits to TFT tokens and deploys services on the ThreeFold Grid, providing seamless Web2-to-Web3 bridge functionality.
|
||||
|
||||
---
|
||||
|
||||
## Deployment Pipeline Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[User Purchase Complete] --> B[TFC Deducted]
|
||||
B --> C[Deployment Queued]
|
||||
C --> D[Commission Calculated]
|
||||
D --> E[USD → TFT Conversion]
|
||||
E --> F[Grid Node Selection]
|
||||
F --> G[Resource Allocation]
|
||||
G --> H[Service Deployment]
|
||||
H --> I[Health Check]
|
||||
I --> J{Deployment Success?}
|
||||
J -->|Yes| K[Service Active]
|
||||
J -->|No| L[Rollback & Refund]
|
||||
K --> M[User Notification]
|
||||
L --> N[Error Notification]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Deployment Queue Service
|
||||
|
||||
```rust
|
||||
// src/services/deployment_queue.rs
|
||||
use tokio::sync::mpsc;
|
||||
use std::collections::VecDeque;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeploymentJob {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub service_spec: ServiceSpec,
|
||||
pub tfc_amount: Decimal,
|
||||
pub commission_amount: Decimal,
|
||||
pub grid_payment_amount: Decimal,
|
||||
pub priority: DeploymentPriority,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub max_retries: u32,
|
||||
pub retry_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DeploymentPriority {
|
||||
Low,
|
||||
Normal,
|
||||
High,
|
||||
Critical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceSpec {
|
||||
pub service_type: ServiceType,
|
||||
pub cpu_cores: u32,
|
||||
pub memory_gb: u32,
|
||||
pub storage_gb: u32,
|
||||
pub network_config: NetworkConfig,
|
||||
pub environment_vars: HashMap<String, String>,
|
||||
pub docker_image: Option<String>,
|
||||
pub kubernetes_manifest: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ServiceType {
|
||||
VM { os: String, version: String },
|
||||
Container { image: String, ports: Vec<u16> },
|
||||
KubernetesCluster { node_count: u32 },
|
||||
Storage { storage_type: String },
|
||||
Application { app_id: String },
|
||||
}
|
||||
|
||||
pub struct DeploymentQueueService {
|
||||
queue: Arc<Mutex<VecDeque<DeploymentJob>>>,
|
||||
workers: Vec<DeploymentWorker>,
|
||||
tx: mpsc::UnboundedSender<DeploymentJob>,
|
||||
rx: Arc<Mutex<mpsc::UnboundedReceiver<DeploymentJob>>>,
|
||||
}
|
||||
|
||||
impl DeploymentQueueService {
|
||||
pub fn new(worker_count: usize) -> Self {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let queue = Arc::new(Mutex::new(VecDeque::new()));
|
||||
|
||||
let mut workers = Vec::new();
|
||||
for i in 0..worker_count {
|
||||
workers.push(DeploymentWorker::new(i, rx.clone()));
|
||||
}
|
||||
|
||||
Self {
|
||||
queue,
|
||||
workers,
|
||||
tx,
|
||||
rx: Arc::new(Mutex::new(rx)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn queue_deployment(&self, job: DeploymentJob) -> Result<(), DeploymentError> {
|
||||
// Add to persistent queue
|
||||
self.queue.lock().await.push_back(job.clone());
|
||||
|
||||
// Send to worker pool
|
||||
self.tx.send(job).map_err(|_| DeploymentError::QueueFull)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start_workers(&self) {
|
||||
for worker in &self.workers {
|
||||
worker.start().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. ThreeFold Grid Integration
|
||||
|
||||
```rust
|
||||
// src/services/threefold_grid.rs
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
|
||||
pub struct ThreeFoldGridService {
|
||||
client: Client,
|
||||
grid_proxy_url: String,
|
||||
tft_wallet: TFTWallet,
|
||||
}
|
||||
|
||||
impl ThreeFoldGridService {
|
||||
pub async fn deploy_vm(
|
||||
&self,
|
||||
spec: &ServiceSpec,
|
||||
tft_amount: Decimal,
|
||||
) -> Result<GridDeployment, GridError> {
|
||||
// 1. Find suitable nodes
|
||||
let nodes = self.find_suitable_nodes(spec).await?;
|
||||
let selected_node = self.select_best_node(&nodes, spec)?;
|
||||
|
||||
// 2. Create deployment contract
|
||||
let contract = self.create_deployment_contract(spec, &selected_node, tft_amount).await?;
|
||||
|
||||
// 3. Deploy on grid
|
||||
let deployment = self.execute_deployment(contract).await?;
|
||||
|
||||
// 4. Wait for deployment to be ready
|
||||
self.wait_for_deployment_ready(&deployment.id, Duration::from_secs(300)).await?;
|
||||
|
||||
// 5. Get connection details
|
||||
let connection_info = self.get_deployment_info(&deployment.id).await?;
|
||||
|
||||
Ok(GridDeployment {
|
||||
id: deployment.id,
|
||||
node_id: selected_node.id,
|
||||
contract_id: contract.id,
|
||||
connection_info,
|
||||
status: DeploymentStatus::Active,
|
||||
created_at: Utc::now(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn find_suitable_nodes(&self, spec: &ServiceSpec) -> Result<Vec<GridNode>, GridError> {
|
||||
let query = json!({
|
||||
"cpu": spec.cpu_cores,
|
||||
"memory": spec.memory_gb * 1024 * 1024 * 1024, // Convert GB to bytes
|
||||
"storage": spec.storage_gb * 1024 * 1024 * 1024,
|
||||
"status": "up",
|
||||
"available": true
|
||||
});
|
||||
|
||||
let response = self.client
|
||||
.post(&format!("{}/nodes/search", self.grid_proxy_url))
|
||||
.json(&query)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let nodes: Vec<GridNode> = response.json().await?;
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
fn select_best_node(&self, nodes: &[GridNode], spec: &ServiceSpec) -> Result<GridNode, GridError> {
|
||||
// Node selection algorithm:
|
||||
// 1. Filter by resource availability
|
||||
// 2. Prefer nodes with better uptime
|
||||
// 3. Consider geographic proximity
|
||||
// 4. Balance load across nodes
|
||||
|
||||
let suitable_nodes: Vec<_> = nodes.iter()
|
||||
.filter(|node| {
|
||||
node.available_cpu >= spec.cpu_cores &&
|
||||
node.available_memory >= spec.memory_gb * 1024 * 1024 * 1024 &&
|
||||
node.available_storage >= spec.storage_gb * 1024 * 1024 * 1024 &&
|
||||
node.uptime_percentage > 95.0
|
||||
})
|
||||
.collect();
|
||||
|
||||
if suitable_nodes.is_empty() {
|
||||
return Err(GridError::NoSuitableNodes);
|
||||
}
|
||||
|
||||
// Select node with best score (uptime + available resources)
|
||||
let best_node = suitable_nodes.iter()
|
||||
.max_by(|a, b| {
|
||||
let score_a = a.uptime_percentage + (a.available_cpu as f64 * 10.0);
|
||||
let score_b = b.uptime_percentage + (b.available_cpu as f64 * 10.0);
|
||||
score_a.partial_cmp(&score_b).unwrap()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Ok((*best_node).clone())
|
||||
}
|
||||
|
||||
async fn create_deployment_contract(
|
||||
&self,
|
||||
spec: &ServiceSpec,
|
||||
node: &GridNode,
|
||||
tft_amount: Decimal,
|
||||
) -> Result<DeploymentContract, GridError> {
|
||||
match &spec.service_type {
|
||||
ServiceType::VM { os, version } => {
|
||||
self.create_vm_contract(spec, node, os, version, tft_amount).await
|
||||
}
|
||||
ServiceType::Container { image, ports } => {
|
||||
self.create_container_contract(spec, node, image, ports, tft_amount).await
|
||||
}
|
||||
ServiceType::KubernetesCluster { node_count } => {
|
||||
self.create_k8s_contract(spec, node, *node_count, tft_amount).await
|
||||
}
|
||||
ServiceType::Storage { storage_type } => {
|
||||
self.create_storage_contract(spec, node, storage_type, tft_amount).await
|
||||
}
|
||||
ServiceType::Application { app_id } => {
|
||||
self.create_app_contract(spec, node, app_id, tft_amount).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_vm_contract(
|
||||
&self,
|
||||
spec: &ServiceSpec,
|
||||
node: &GridNode,
|
||||
os: &str,
|
||||
version: &str,
|
||||
tft_amount: Decimal,
|
||||
) -> Result<DeploymentContract, GridError> {
|
||||
let vm_config = json!({
|
||||
"type": "vm",
|
||||
"node_id": node.id,
|
||||
"cpu": spec.cpu_cores,
|
||||
"memory": spec.memory_gb,
|
||||
"storage": spec.storage_gb,
|
||||
"os": os,
|
||||
"version": version,
|
||||
"network": spec.network_config,
|
||||
"environment": spec.environment_vars,
|
||||
"payment": {
|
||||
"amount": tft_amount,
|
||||
"currency": "TFT"
|
||||
}
|
||||
});
|
||||
|
||||
let response = self.client
|
||||
.post(&format!("{}/deployments/vm", self.grid_proxy_url))
|
||||
.json(&vm_config)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let contract: DeploymentContract = response.json().await?;
|
||||
Ok(contract)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. TFT Conversion Service
|
||||
|
||||
```rust
|
||||
// src/services/tft_conversion.rs
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
pub struct TFTConversionService {
|
||||
grid_client: ThreeFoldGridService,
|
||||
price_oracle: TFTPriceOracle,
|
||||
}
|
||||
|
||||
impl TFTConversionService {
|
||||
pub async fn convert_usd_to_tft(&self, usd_amount: Decimal) -> Result<TFTConversion, ConversionError> {
|
||||
// Get current TFT/USD rate
|
||||
let tft_rate = self.price_oracle.get_tft_usd_rate().await?;
|
||||
let tft_amount = usd_amount / tft_rate;
|
||||
|
||||
// Add small buffer for price fluctuations (2%)
|
||||
let tft_with_buffer = tft_amount * Decimal::from_str("1.02")?;
|
||||
|
||||
Ok(TFTConversion {
|
||||
usd_amount,
|
||||
tft_rate,
|
||||
tft_amount: tft_with_buffer,
|
||||
conversion_timestamp: Utc::now(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn execute_conversion(&self, conversion: &TFTConversion) -> Result<TFTTransaction, ConversionError> {
|
||||
// Execute the actual TFT purchase/conversion
|
||||
// This would integrate with TFT DEX or direct wallet operations
|
||||
|
||||
let transaction = self.grid_client.tft_wallet.convert_usd_to_tft(
|
||||
conversion.usd_amount,
|
||||
conversion.tft_amount,
|
||||
).await?;
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TFTConversion {
|
||||
pub usd_amount: Decimal,
|
||||
pub tft_rate: Decimal,
|
||||
pub tft_amount: Decimal,
|
||||
pub conversion_timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub struct TFTPriceOracle {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl TFTPriceOracle {
|
||||
pub async fn get_tft_usd_rate(&self) -> Result<Decimal, PriceError> {
|
||||
// Get TFT price from multiple sources and average
|
||||
let sources = vec![
|
||||
self.get_rate_from_dex().await,
|
||||
self.get_rate_from_coingecko().await,
|
||||
self.get_rate_from_grid_stats().await,
|
||||
];
|
||||
|
||||
let valid_rates: Vec<Decimal> = sources.into_iter()
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
if valid_rates.is_empty() {
|
||||
return Err(PriceError::NoValidSources);
|
||||
}
|
||||
|
||||
// Calculate average rate
|
||||
let sum: Decimal = valid_rates.iter().sum();
|
||||
let average = sum / Decimal::from(valid_rates.len());
|
||||
|
||||
Ok(average)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Deployment Worker
|
||||
|
||||
```rust
|
||||
// src/services/deployment_worker.rs
|
||||
pub struct DeploymentWorker {
|
||||
id: usize,
|
||||
grid_service: Arc<ThreeFoldGridService>,
|
||||
tft_service: Arc<TFTConversionService>,
|
||||
notification_service: Arc<NotificationService>,
|
||||
}
|
||||
|
||||
impl DeploymentWorker {
|
||||
pub async fn process_deployment(&self, job: DeploymentJob) -> Result<(), DeploymentError> {
|
||||
log::info!("Worker {} processing deployment {}", self.id, job.id);
|
||||
|
||||
// Update status to "processing"
|
||||
self.update_deployment_status(&job.id, DeploymentStatus::Processing).await?;
|
||||
|
||||
// 1. Convert USD to TFT
|
||||
let conversion = self.tft_service.convert_usd_to_tft(job.grid_payment_amount).await?;
|
||||
let tft_transaction = self.tft_service.execute_conversion(&conversion).await?;
|
||||
|
||||
// 2. Deploy on ThreeFold Grid
|
||||
let grid_deployment = self.grid_service.deploy_vm(&job.service_spec, conversion.tft_amount).await?;
|
||||
|
||||
// 3. Update deployment record
|
||||
self.update_deployment_with_grid_info(&job.id, &grid_deployment, &tft_transaction).await?;
|
||||
|
||||
// 4. Verify deployment health
|
||||
self.verify_deployment_health(&grid_deployment).await?;
|
||||
|
||||
// 5. Update status to "active"
|
||||
self.update_deployment_status(&job.id, DeploymentStatus::Active).await?;
|
||||
|
||||
// 6. Notify user
|
||||
self.notification_service.send_deployment_success(&job.user_id, &job.id).await?;
|
||||
|
||||
log::info!("Worker {} completed deployment {}", self.id, job.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_deployment_failure(&self, job: &DeploymentJob, error: &DeploymentError) {
|
||||
log::error!("Deployment {} failed: {:?}", job.id, error);
|
||||
|
||||
// Update status to failed
|
||||
self.update_deployment_status(&job.id, DeploymentStatus::Failed).await.ok();
|
||||
|
||||
// Refund TFC credits to user
|
||||
if let Err(refund_error) = self.refund_tfc_credits(job).await {
|
||||
log::error!("Failed to refund TFC for deployment {}: {:?}", job.id, refund_error);
|
||||
}
|
||||
|
||||
// Notify user of failure
|
||||
self.notification_service.send_deployment_failure(&job.user_id, &job.id, error).await.ok();
|
||||
}
|
||||
|
||||
async fn refund_tfc_credits(&self, job: &DeploymentJob) -> Result<(), RefundError> {
|
||||
// Refund the full TFC amount back to user
|
||||
self.tfc_service.add_tfc_credits(
|
||||
&job.user_id,
|
||||
job.tfc_amount,
|
||||
&format!("Refund for failed deployment {}", job.id),
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Management Dashboard
|
||||
|
||||
### 1. Real-time Deployment Status
|
||||
|
||||
```html
|
||||
<!-- Deployment Status Component -->
|
||||
<div class="deployment-status-card">
|
||||
<div class="card-header">
|
||||
<h5>Deployment Status</h5>
|
||||
<span class="status-badge status-{{deployment.status}}">{{deployment.status}}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="progress-timeline">
|
||||
<div class="timeline-step {{#if deployment.payment_completed}}completed{{/if}}">
|
||||
<i class="bi bi-credit-card"></i>
|
||||
<span>Payment Processed</span>
|
||||
</div>
|
||||
<div class="timeline-step {{#if deployment.queued}}completed{{/if}}">
|
||||
<i class="bi bi-clock"></i>
|
||||
<span>Queued for Deployment</span>
|
||||
</div>
|
||||
<div class="timeline-step {{#if deployment.converting}}completed{{/if}}">
|
||||
<i class="bi bi-arrow-left-right"></i>
|
||||
<span>Converting TFC → TFT</span>
|
||||
</div>
|
||||
<div class="timeline-step {{#if deployment.deploying}}completed{{/if}}">
|
||||
<i class="bi bi-cloud-upload"></i>
|
||||
<span>Deploying on Grid</span>
|
||||
</div>
|
||||
<div class="timeline-step {{#if deployment.active}}completed{{/if}}">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
<span>Service Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if deployment.active}}
|
||||
<div class="connection-details">
|
||||
<h6>Connection Details</h6>
|
||||
<div class="detail-item">
|
||||
<label>IP Address:</label>
|
||||
<span class="copyable">{{deployment.ip_address}}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>SSH Access:</label>
|
||||
<code class="copyable">ssh root@{{deployment.ip_address}}</code>
|
||||
</div>
|
||||
{{#if deployment.web_url}}
|
||||
<div class="detail-item">
|
||||
<label>Web Interface:</label>
|
||||
<a href="{{deployment.web_url}}" target="_blank">{{deployment.web_url}}</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="deployment-metrics">
|
||||
<div class="metric">
|
||||
<label>TFC Spent:</label>
|
||||
<span>{{deployment.tfc_amount}} TFC</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<label>TFT Converted:</label>
|
||||
<span>{{deployment.tft_amount}} TFT</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<label>Grid Node:</label>
|
||||
<span>{{deployment.node_id}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2. Service Management Controls
|
||||
|
||||
```javascript
|
||||
// Service Management Functions
|
||||
class ServiceManager {
|
||||
constructor(deploymentId) {
|
||||
this.deploymentId = deploymentId;
|
||||
this.statusPolling = null;
|
||||
}
|
||||
|
||||
startStatusPolling() {
|
||||
this.statusPolling = setInterval(async () => {
|
||||
await this.updateDeploymentStatus();
|
||||
}, 5000); // Poll every 5 seconds
|
||||
}
|
||||
|
||||
async updateDeploymentStatus() {
|
||||
try {
|
||||
const response = await fetch(`/api/deployments/${this.deploymentId}/status`);
|
||||
const deployment = await response.json();
|
||||
|
||||
this.updateStatusDisplay(deployment);
|
||||
|
||||
// Stop polling if deployment is complete or failed
|
||||
if (deployment.status === 'Active' || deployment.status === 'Failed') {
|
||||
this.stopStatusPolling();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update deployment status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusDisplay(deployment) {
|
||||
// Update progress timeline
|
||||
document.querySelectorAll('.timeline-step').forEach((step, index) => {
|
||||
const stepStates = ['payment_completed', 'queued', 'converting', 'deploying', 'active'];
|
||||
if (deployment[stepStates[index]]) {
|
||||
step.classList.add('completed');
|
||||
}
|
||||
});
|
||||
|
||||
// Update status badge
|
||||
const statusBadge = document.querySelector('.status-badge');
|
||||
statusBadge.className = `status-badge status-${deployment.status.toLowerCase()}`;
|
||||
statusBadge.textContent = deployment.status;
|
||||
|
||||
// Update connection details if active
|
||||
if (deployment.status === 'Active' && deployment.connection_info) {
|
||||
this.showConnectionDetails(deployment.connection_info);
|
||||
}
|
||||
}
|
||||
|
||||
async restartService() {
|
||||
try {
|
||||
const response = await fetch(`/api/deployments/${this.deploymentId}/restart`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showSuccessToast('Service restart initiated');
|
||||
this.startStatusPolling();
|
||||
} else {
|
||||
showErrorToast('Failed to restart service');
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast('Failed to restart service');
|
||||
}
|
||||
}
|
||||
|
||||
async stopService() {
|
||||
if (confirm('Are you sure you want to stop this service? This action cannot be undone.')) {
|
||||
try {
|
||||
const response = await fetch(`/api/deployments/${this.deploymentId}/stop`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showSuccessToast('Service stopped successfully');
|
||||
window.location.href = '/dashboard/deployments';
|
||||
} else {
|
||||
showErrorToast('Failed to stop service');
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast('Failed to stop service');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration & Environment
|
||||
|
||||
```toml
|
||||
# config/deployment.toml
|
||||
[deployment_queue]
|
||||
worker_count = 4
|
||||
max_queue_size = 1000
|
||||
retry_attempts = 3
|
||||
retry_delay_seconds = 30
|
||||
|
||||
[threefold_grid]
|
||||
grid_proxy_url = "https://gridproxy.grid.tf"
|
||||
substrate_url = "wss://tfchain.grid.tf/ws"
|
||||
relay_url = "wss://relay.grid.tf"
|
||||
|
||||
[tft_conversion]
|
||||
price_sources = ["dex", "coingecko", "grid_stats"]
|
||||
conversion_buffer_percent = 2.0
|
||||
max_slippage_percent = 5.0
|
||||
|
||||
[notifications]
|
||||
email_enabled = true
|
||||
webhook_enabled = true
|
||||
slack_enabled = false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
This deployment automation specification provides the complete technical foundation for seamless TFC-to-Grid deployment automation, ensuring reliable and scalable service provisioning on the ThreeFold Grid.
|
Reference in New Issue
Block a user