A self-hosted orchestration system crafted for power users. composia.xyz
Something went wrong. Try again.
7.1 kB · 182 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183package controller
import ( "encoding/json" "errors" "fmt" "path/filepath" "strings"
backupcfg "forgejo.alexma.top/alexma233/composia/internal/core/backup" "forgejo.alexma.top/alexma233/composia/internal/core/config" "forgejo.alexma.top/alexma233/composia/internal/core/repo" secretutil "forgejo.alexma.top/alexma233/composia/internal/platform/secret")
// persistentServiceExtraFiles renders deployment secrets without task-specific filtering or temporary payloads.func persistentServiceExtraFiles(cfg *config.ControllerConfig, revision, serviceDir string) (map[string]string, error) { encFiles, err := listEncryptedFiles(cfg.RepoDir, revision, serviceDir) if err != nil { return nil, err } return renderServiceSecrets(cfg, revision, serviceDir, encFiles)}
func renderServiceSecrets(cfg *config.ControllerConfig, revision, serviceDir string, encFiles []string) (map[string]string, error) { extraFiles := map[string]string{} if len(encFiles) > 0 && cfg.Secrets == nil { return nil, errSecretsNotConfigured } if cfg.Secrets != nil { for _, encFile := range encFiles { fullPath := filepath.ToSlash(filepath.Join(serviceDir, encFile)) decrypted, err := decryptFileAtRevision(cfg, revision, fullPath) if err != nil { return nil, err } decryptedPath := repo.RuntimeFilePath(encFile) extraFiles[filepath.ToSlash(filepath.Join(serviceDir, decryptedPath))] = decrypted } } return extraFiles, nil}
func listEncryptedFiles(repoDir, revision, serviceDir string) ([]string, error) { files, err := repo.ListFilesAtRevision(repoDir, revision, serviceDir) if err != nil { if isMissingRevisionPathError(err) { return nil, nil } return nil, err } var encFiles []string for _, f := range files { if repo.IsEncryptedFilePath(f) { if !repo.IsValidEncryptedFilePath(f) { return nil, fmt.Errorf("invalid encrypted file path %q", f) } encFiles = append(encFiles, f) } } return encFiles, nil}
func decryptFileAtRevision(cfg *config.ControllerConfig, revision, encFilePath string) (string, error) { secretContent, err := repo.ReadFileAtRevision(cfg.RepoDir, revision, encFilePath) if err != nil { if isMissingRevisionPathError(err) { return "", nil } return "", err } plaintext, err := secretutil.Decrypt([]byte(secretContent), cfg.Secrets) if err != nil { return "", err } return plaintext, nil}
func isMissingRevisionPathError(err error) bool { message := err.Error() return strings.Contains(message, "does not exist") || strings.Contains(message, "exists on disk, but not in") || strings.Contains(message, "pathspec") || strings.Contains(message, "invalid object name") || strings.Contains(message, "not found")}
func buildBackupRuntimePayload(cfg *config.ControllerConfig, serviceName, nodeID, revision string, params serviceTaskParams) (string, error) { if params.ServiceDir == "" { return "", errors.New("backup runtime payload requires service_dir") } service, err := repo.FindServiceAtRevision(cfg.RepoDir, revision, params.ServiceDir, configuredNodeIDs(cfg)) if err != nil { return "", err } rusticService, err := repo.FindRusticInfraServiceAtRevision(cfg.RepoDir, revision, configuredNodeIDs(cfg)) if err != nil { return "", err } if err := validateRusticServiceTargetNode(rusticService, nodeID); err != nil { return "", err } rusticServiceDir, err := filepath.Rel(cfg.RepoDir, rusticService.Directory) if err != nil { return "", fmt.Errorf("resolve rustic service directory: %w", err) } selected := make(map[string]struct{}, len(params.DataNames)) for _, name := range params.DataNames { selected[name] = struct{}{} } items := make([]backupcfg.RuntimeItem, 0, len(params.DataNames)) for _, data := range service.Meta.DataProtect.Data { if _, ok := selected[data.Name]; !ok || data.Backup == nil { continue } provider := backupProviderRustic if service.Meta.Backup != nil { for _, backupItem := range service.Meta.Backup.Data { if backupItem.Name == data.Name { if backupItem.Provider != "" { provider = backupItem.Provider } break } } } if provider != backupProviderRustic { return "", fmt.Errorf("backup provider %q is not implemented", provider) } items = append(items, backupcfg.RuntimeItem{Name: data.Name, Strategy: data.Backup.Strategy, Service: data.Backup.Service, User: data.Backup.User, Include: append([]string(nil), data.Backup.Include...), Provider: provider, Tags: []string{"composia-service:" + serviceName, "composia-data:" + data.Name}}) } payload, err := json.Marshal(backupcfg.RuntimeConfig{Rustic: &backupcfg.RusticConfig{ServiceName: rusticService.Name, ServiceDir: rusticServiceDir, ComposeService: rusticService.Meta.RusticComposeService(), Profile: rusticService.Meta.RusticProfile(), DataProtectDir: rusticService.Meta.RusticDataProtectDir(), NodeID: nodeID}, Items: items}) if err != nil { return "", fmt.Errorf("marshal backup runtime config for %s at %s: %w", serviceName, revision, err) } return string(payload), nil}
func buildRestoreRuntimePayload(cfg *config.ControllerConfig, serviceName, nodeID, revision string, params serviceTaskParams) (string, error) { if params.ServiceDir == "" { return "", errors.New("restore runtime payload requires service_dir") } service, err := repo.FindServiceAtRevision(cfg.RepoDir, revision, params.ServiceDir, configuredNodeIDs(cfg)) if err != nil { return "", err } rusticService, err := repo.FindRusticInfraServiceAtRevision(cfg.RepoDir, revision, configuredNodeIDs(cfg)) if err != nil { return "", err } if err := validateRusticServiceTargetNode(rusticService, nodeID); err != nil { return "", err } rusticServiceDir, err := filepath.Rel(cfg.RepoDir, rusticService.Directory) if err != nil { return "", fmt.Errorf("resolve rustic service directory: %w", err) } artifactsByName := make(map[string]string, len(params.RestoreItems)) for _, item := range params.RestoreItems { if item.DataName == "" || item.ArtifactRef == "" { continue } artifactsByName[item.DataName] = item.ArtifactRef } items := make([]backupcfg.RestoreItem, 0, len(params.RestoreItems)) for _, data := range service.Meta.DataProtect.Data { artifactRef, ok := artifactsByName[data.Name] if !ok || data.Restore == nil { continue } items = append(items, backupcfg.RestoreItem{ Name: data.Name, Strategy: data.Restore.Strategy, Service: data.Restore.Service, User: data.Restore.User, Include: append([]string(nil), data.Restore.Include...), Provider: backupProviderRustic, ArtifactRef: artifactRef, }) } payload, err := json.Marshal(backupcfg.RestoreConfig{Rustic: &backupcfg.RusticConfig{ServiceName: rusticService.Name, ServiceDir: rusticServiceDir, ComposeService: rusticService.Meta.RusticComposeService(), Profile: rusticService.Meta.RusticProfile(), DataProtectDir: rusticService.Meta.RusticDataProtectDir(), NodeID: nodeID}, Items: items}) if err != nil { return "", fmt.Errorf("marshal restore runtime config for %s at %s: %w", serviceName, revision, err) } return string(payload), nil}