Files
osmedeus/internal/cloud/digitalocean.go
T
j3ssie 459d5939fb feat: add cloud infrastructure and nmap/tmux function support
- Implement cloud provider infrastructure (DigitalOcean, AWS, GCP, Linode, Azure) with Pulumi integration for distributed scanning
- Add nmap and tmux utility functions for port scanning results processing and long-running background session management
- Introduce webhook-triggered run execution with unique UUID and authentication key support for external integrations
2026-02-16 15:23:24 +07:00

193 lines
5.8 KiB
Go

package cloud
import (
"context"
"fmt"
"time"
"github.com/digitalocean/godo"
"github.com/pulumi/pulumi-digitalocean/sdk/v4/go/digitalocean"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"golang.org/x/oauth2"
)
// DigitalOceanProvider implements the Provider interface for DigitalOcean
type DigitalOceanProvider struct {
token string
region string
size string
snapshotID string
sshKeyFingerprint string
client *godo.Client
}
// NewDigitalOceanProvider creates a new DigitalOcean provider
func NewDigitalOceanProvider(token, region, size, snapshotID, sshKeyFingerprint string) (*DigitalOceanProvider, error) {
if token == "" {
return nil, fmt.Errorf("DigitalOcean token is required")
}
// Create DigitalOcean client
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
oauthClient := oauth2.NewClient(context.Background(), tokenSource)
client := godo.NewClient(oauthClient)
return &DigitalOceanProvider{
token: token,
region: region,
size: size,
snapshotID: snapshotID,
sshKeyFingerprint: sshKeyFingerprint,
client: client,
}, nil
}
// Validate checks if the provider configuration is valid
func (p *DigitalOceanProvider) Validate(ctx context.Context) error {
// Test API access by getting account info
_, _, err := p.client.Account.Get(ctx)
if err != nil {
return fmt.Errorf("failed to validate DigitalOcean credentials: %w", err)
}
return nil
}
// EstimateCost estimates the cost for the given configuration
func (p *DigitalOceanProvider) EstimateCost(mode ExecutionMode, instanceCount int) (*CostEstimate, error) {
if mode != ModeVM {
return nil, fmt.Errorf("only VM mode is supported for DigitalOcean")
}
// Default pricing for common sizes (USD per hour)
pricing := map[string]float64{
"s-1vcpu-1gb": 0.00744, // $5/month
"s-1vcpu-2gb": 0.01116, // $7.5/month
"s-2vcpu-2gb": 0.01488, // $10/month
"s-2vcpu-4gb": 0.02232, // $15/month
"s-4vcpu-8gb": 0.04464, // $30/month
"s-8vcpu-16gb": 0.08928, // $60/month
"s-16vcpu-32gb": 0.17856, // $120/month
}
hourlyRate, ok := pricing[p.size]
if !ok {
// Default to s-2vcpu-4gb pricing if unknown
hourlyRate = 0.02232
}
totalHourlyRate := hourlyRate * float64(instanceCount)
return &CostEstimate{
HourlyCost: totalHourlyRate,
DailyCost: totalHourlyRate * 24,
Currency: "USD",
Breakdown: map[string]float64{
"compute": totalHourlyRate,
},
Notes: []string{
fmt.Sprintf("%d x %s droplets @ $%.4f/hr each", instanceCount, p.size, hourlyRate),
},
}, nil
}
// CreateInfrastructure provisions DigitalOcean droplets
func (p *DigitalOceanProvider) CreateInfrastructure(ctx context.Context, opts *CreateOptions) (*Infrastructure, error) {
// Generate unique infrastructure ID
infraID := fmt.Sprintf("cloud-do-%d", time.Now().Unix())
// Placeholder implementation - actual Pulumi logic will be added
// This demonstrates the structure
infra := &Infrastructure{
ID: infraID,
Provider: ProviderDigitalOcean,
Mode: opts.Mode,
CreatedAt: time.Now(),
PulumiStackID: fmt.Sprintf("osmedeus-cloud/%s", infraID),
Resources: []Resource{},
Metadata: map[string]interface{}{},
}
return infra, fmt.Errorf("DigitalOcean infrastructure creation not yet fully implemented")
}
// DestroyInfrastructure tears down DigitalOcean resources
func (p *DigitalOceanProvider) DestroyInfrastructure(ctx context.Context, infra *Infrastructure) error {
// Placeholder implementation
return fmt.Errorf("DigitalOcean infrastructure destruction not yet fully implemented")
}
// GetStatus retrieves the current status of infrastructure
func (p *DigitalOceanProvider) GetStatus(ctx context.Context, infra *Infrastructure) (*InfraStatus, error) {
// Placeholder implementation
return &InfraStatus{
Status: "unknown",
ReadyCount: 0,
TotalCount: len(infra.Resources),
WorkersRegistered: 0,
Details: []ResourceStatus{},
}, nil
}
// Type returns the provider type
func (p *DigitalOceanProvider) Type() ProviderType {
return ProviderDigitalOcean
}
// generateCloudInit generates the cloud-init user data script
// TODO: This method will be used when the Create() method is fully implemented
//
//nolint:unused // Reserved for future implementation
func (p *DigitalOceanProvider) generateCloudInit(redisURL, sshPublicKey string, setupCommands []string) string {
script := `#!/bin/bash
set -e
# Install osmedeus
curl -fsSL https://www.osmedeus.org/install.sh | bash
# Setup SSH keys
mkdir -p ~/.ssh
echo "` + sshPublicKey + `" >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
# Join as worker
osmedeus worker join --redis-url ` + redisURL + ` --get-public-ip
`
// Add custom setup commands
for _, cmd := range setupCommands {
script += cmd + "\n"
}
return script
}
// createDropletProgram creates a Pulumi program for DigitalOcean droplets
// TODO: This method will be used when the Create() method is fully implemented
//
//nolint:unused // Reserved for future implementation
func (p *DigitalOceanProvider) createDropletProgram(opts *CreateOptions) pulumi.RunFunc {
return func(ctx *pulumi.Context) error {
// This will be implemented with actual Pulumi DigitalOcean resource creation
// For now, just a placeholder to demonstrate the pattern
userData := p.generateCloudInit(opts.RedisURL, opts.SSHPublicKey, opts.SetupCommands)
for i := 0; i < opts.InstanceCount; i++ {
dropletName := fmt.Sprintf("osmedeus-worker-%d", i)
_, err := digitalocean.NewDroplet(ctx, dropletName, &digitalocean.DropletArgs{
Image: pulumi.String(opts.ImageID),
Region: pulumi.String(p.region),
Size: pulumi.String(p.size),
UserData: pulumi.String(userData),
})
if err != nil {
return err
}
}
return nil
}
}