156 lines
4.3 KiB
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"
|
|
}
|