Files
wild-cloud/api/internal/apps/infrastructure.go

156 lines
4.3 KiB
Go

package apps
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
"github.com/wild-cloud/wild-central/daemon/internal/operations"
)
// InfrastructureOrder returns infrastructure packages from the apps directory
// sorted in dependency order (topological sort by requires).
func (m *Manager) InfrastructureOrder() ([]AppManifest, error) {
if m.appsDir == "" {
return nil, fmt.Errorf("apps directory not configured")
}
entries, err := os.ReadDir(m.appsDir)
if err != nil {
return nil, fmt.Errorf("reading apps directory: %w", err)
}
// Collect all infrastructure manifests
manifests := map[string]AppManifest{}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
manifestPath := filepath.Join(m.appsDir, entry.Name(), "manifest.yaml")
data, err := os.ReadFile(manifestPath)
if err != nil {
continue
}
var manifest AppManifest
if err := yaml.Unmarshal(data, &manifest); err != nil {
continue
}
if manifest.Category != "services" {
continue
}
manifest.Name = entry.Name()
manifests[entry.Name()] = manifest
}
return topoSort(manifests)
}
// topoSort returns manifests in dependency order using Kahn's algorithm.
// Only considers dependencies that are themselves infrastructure packages.
func topoSort(manifests map[string]AppManifest) ([]AppManifest, error) {
// Build adjacency list and in-degree count
inDegree := map[string]int{}
dependents := map[string][]string{} // dep -> list of packages that depend on it
for name := range manifests {
inDegree[name] = 0
}
for name, manifest := range manifests {
for _, req := range manifest.Requires {
if _, isInfra := manifests[req.Name]; isInfra {
inDegree[name]++
dependents[req.Name] = append(dependents[req.Name], name)
}
}
}
// Start with packages that have no infrastructure dependencies
var queue []string
for name, deg := range inDegree {
if deg == 0 {
queue = append(queue, name)
}
}
var sorted []AppManifest
for len(queue) > 0 {
// Pop first
name := queue[0]
queue = queue[1:]
sorted = append(sorted, manifests[name])
for _, dep := range dependents[name] {
inDegree[dep]--
if inDegree[dep] == 0 {
queue = append(queue, dep)
}
}
}
if len(sorted) != len(manifests) {
return nil, fmt.Errorf("circular dependency detected in infrastructure packages")
}
return sorted, nil
}
// DeployInfrastructure adds and deploys all infrastructure packages in dependency order.
// It skips packages that are already deployed. Progress is reported via the broadcaster.
func (m *Manager) DeployInfrastructure(instanceName, opID string, broadcaster *operations.Broadcaster) error {
packages, err := m.InfrastructureOrder()
if err != nil {
return fmt.Errorf("resolving infrastructure order: %w", err)
}
total := len(packages)
for i, pkg := range packages {
// Skip if already added and deployed
if m.isDeployed(instanceName, pkg.Name) {
slog.Info("already deployed, skipping", "component", "infrastructure", "package", pkg.Name)
if broadcaster != nil {
broadcaster.Publish(opID, []byte(fmt.Sprintf("Skipping %s (already deployed)\n", pkg.Name)))
}
continue
}
slog.Info("installing package", "component", "infrastructure", "package", pkg.Name, "progress", fmt.Sprintf("%d/%d", i+1, total))
if broadcaster != nil {
broadcaster.Publish(opID, []byte(fmt.Sprintf("Installing %s (%d/%d)...\n", pkg.Name, i+1, total)))
}
if err := m.Install(instanceName, pkg.Name, false, true, nil, nil, opID, broadcaster); err != nil {
return fmt.Errorf("installing %s: %w", pkg.Name, err)
}
progress := ((i + 1) * 100) / total
if broadcaster != nil {
broadcaster.Publish(opID, []byte(fmt.Sprintf("Installed %s (%d%%)\n", pkg.Name, progress)))
}
}
return nil
}
// isDeployed checks if a package is deployed by looking for its namespace in the cluster.
func (m *Manager) isDeployed(instanceName, appName string) bool {
instancePath := filepath.Join(m.dataDir, "instances", instanceName)
appDir := filepath.Join(instancePath, "apps", appName)
// Not even added locally
if _, err := os.Stat(appDir); os.IsNotExist(err) {
return false
}
// Check if deployed by looking at cluster state
status, err := m.GetStatus(instanceName, appName)
if err != nil {
return false
}
return status.Status == "deployed" || status.Status == "running"
}