feat(auth): integrate authentication with installation lifecycle
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/authconfig"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/compose"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/doctor"
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
func productionRestoreDependencies(installation config.Installation) restoreDependencies {
|
||||
runner := hostRunner{runner: compose.NewRunner(""), binary: "docker", profile: installation.Profile}
|
||||
return restoreDependencies{
|
||||
deps := restoreDependencies{
|
||||
preflight: func(ctx context.Context, target config.Installation, request PreflightRequest) (PreflightResult, error) {
|
||||
return Preflight(ctx, target, request, PreflightDependencies{
|
||||
FreeBytes: restoreFreeBytes,
|
||||
@@ -47,6 +47,7 @@ func productionRestoreDependencies(installation config.Installation) restoreDepe
|
||||
request.Output = path
|
||||
return Create(ctx, target, request)
|
||||
},
|
||||
cleanupCheckpoint: cleanupRecoveryCheckpoint,
|
||||
acquireLock: func(target config.Installation) (restoreLock, error) {
|
||||
return lifecycle.Acquire(target)
|
||||
},
|
||||
@@ -71,6 +72,46 @@ func productionRestoreDependencies(installation config.Installation) restoreDepe
|
||||
"workspace": verifyRestoreWorkspace,
|
||||
},
|
||||
}
|
||||
deps.prepareRecovery = func(ctx context.Context, target config.Installation, path string) (PreflightResult, error) {
|
||||
return deps.preflight(ctx, target, PreflightRequest{Archive: path, Confirm: true, AllowExternalSecrets: true})
|
||||
}
|
||||
deps.recover = func(ctx context.Context, target config.Installation, recovery PreflightResult, wasRunning bool) error {
|
||||
return recoverRestoreTransaction(ctx, target, recovery, wasRunning, deps)
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
func cleanupRecoveryCheckpoint(path string) error {
|
||||
if err := safeio.RemoveCanonicalPrivateRegular(path); err != nil {
|
||||
return errors.New("private recovery checkpoint could not be destroyed safely")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func recoverRestoreTransaction(ctx context.Context, installation config.Installation, recovery PreflightResult, wasRunning bool, deps restoreDependencies) error {
|
||||
var resultErr error
|
||||
if err := runCompose(ctx, installation, deps.runner, "stop"); err != nil {
|
||||
resultErr = errors.Join(resultErr, err)
|
||||
}
|
||||
archive, err := recovery.RevalidateArchive()
|
||||
if err != nil {
|
||||
return errors.Join(resultErr, err)
|
||||
}
|
||||
if err := restoreVerifiedEntries(ctx, installation, recovery, archive, deps.restoreFile, deps.restoreVolume); err != nil {
|
||||
return errors.Join(resultErr, err)
|
||||
}
|
||||
// Recovery restores only configuration/secret files and durable application volumes. Runtime
|
||||
// authentication state is deliberately reset again so neither sessions nor OIDC transactions
|
||||
// survive a failed restore attempt.
|
||||
if err := deps.resetAuthenticationState(ctx, installation, deps.runner); err != nil {
|
||||
return errors.Join(resultErr, err)
|
||||
}
|
||||
if wasRunning {
|
||||
if err := composeStartAndVerify(ctx, installation, deps.runner); err != nil {
|
||||
return errors.Join(resultErr, err)
|
||||
}
|
||||
}
|
||||
return resultErr
|
||||
}
|
||||
|
||||
func restoreCheckpointPath(installation config.Installation, now time.Time) (string, error) {
|
||||
@@ -131,14 +172,14 @@ func validateRestoreReference(installation config.Installation, entry Entry) err
|
||||
}
|
||||
|
||||
func validateRestoreVolumes(ctx context.Context, installation config.Installation, manifest Manifest, runner archiveRunner) error {
|
||||
if len(manifest.Volumes) != len(requiredVolumes) {
|
||||
if len(manifest.Volumes) != len(requiredBackupVolumes(installation)) {
|
||||
return errors.New("backup volume set is incomplete")
|
||||
}
|
||||
rendered, err := renderedConfiguration(ctx, installation, runner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current, err := inspectRequiredVolumes(ctx, runner, rendered)
|
||||
current, err := inspectRequiredVolumes(ctx, installation, runner, rendered)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -313,6 +354,13 @@ func verifyRestoreDoctor(ctx context.Context, installation config.Installation,
|
||||
if configErr != nil {
|
||||
return dockerError("verify restored Compose configuration", result, configErr)
|
||||
}
|
||||
diagnostics, diagnosticErr := authconfig.Check(ctx, installation, runner, false, false)
|
||||
if diagnosticErr != nil || !diagnostics.Ready {
|
||||
if diagnosticErr != nil {
|
||||
return diagnosticErr
|
||||
}
|
||||
return errors.New("authentication diagnostics did not pass after restore")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
report, err := doctor.Run(ctx, installation, runner)
|
||||
@@ -320,7 +368,16 @@ func verifyRestoreDoctor(ctx context.Context, installation config.Installation,
|
||||
return err
|
||||
}
|
||||
if !report.OK {
|
||||
return errors.New("aggregate doctor did not pass after restore")
|
||||
failed := make([]string, 0, len(report.Checks))
|
||||
for _, check := range report.Checks {
|
||||
if check.Status == doctor.StatusFailed {
|
||||
failed = append(failed, check.Name)
|
||||
}
|
||||
}
|
||||
if len(failed) == 0 {
|
||||
return errors.New("aggregate doctor did not pass after restore")
|
||||
}
|
||||
return fmt.Errorf("aggregate doctor failed checks: %s", strings.Join(failed, ","))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -328,25 +385,36 @@ func verifyRestoreDoctor(ctx context.Context, installation config.Installation,
|
||||
func verifyRestorePi(ctx context.Context, installation config.Installation, runner archiveRunner) error {
|
||||
running, err := restoreVerificationRunning(ctx, installation, runner)
|
||||
if err != nil || !running {
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, runErr := runner.Run(ctx, installation.ComposeArgs(
|
||||
"run", "--rm", "--no-deps", "--no-TTY", "core", "pi", "--version",
|
||||
), nil)
|
||||
if runErr != nil || strings.TrimSpace(result.Stdout) == "" {
|
||||
if runErr == nil {
|
||||
runErr = errors.New("Pi version probe returned no version")
|
||||
}
|
||||
return dockerError("verify restored Pi runtime", result, runErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return pi.Doctor(ctx, compose.InstallationRunner{Installation: installation, Runner: runner})
|
||||
}
|
||||
|
||||
func verifyRestoreWorkspace(ctx context.Context, installation config.Installation, runner archiveRunner) error {
|
||||
running, err := restoreVerificationRunning(ctx, installation, runner)
|
||||
if err != nil || !running {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := runner.Run(ctx, installation.ComposeArgs(
|
||||
"exec", "-T", "core", "curl", "-fsS", "--max-time", "5", "http://127.0.0.1:8787/workspaces",
|
||||
), nil)
|
||||
if err != nil {
|
||||
return dockerError("inspect restored workspaces", result, err)
|
||||
command := []string{"exec", "-T", "core", "node", "-e"}
|
||||
if !running {
|
||||
command = []string{"run", "--rm", "--no-deps", "--no-TTY", "core", "node", "-e"}
|
||||
}
|
||||
var workspaces []json.RawMessage
|
||||
if json.Unmarshal([]byte(result.Stdout), &workspaces) != nil {
|
||||
return errors.New("restored workspace inspection returned invalid JSON")
|
||||
command = append(command, `const fs=require("node:fs");const p="/data/workspace-registry/state/active.json";const s=JSON.parse(fs.readFileSync(p,"utf8"));const h=/^[0-9a-f]{40}$/;if(!h.test(s.head)||!Array.isArray(s.revisions)||s.revisions.some(r=>!r||typeof r.id!=="string"||!r.id||!h.test(r.commit)||!h.test(r.blob)))process.exit(1);for(const r of s.revisions)fs.accessSync("/data/workspace-registry/snapshots/"+r.commit+"/"+r.id+".yaml",fs.constants.R_OK)`)
|
||||
result, err := runner.Run(ctx, installation.ComposeArgs(command...), nil)
|
||||
if err != nil {
|
||||
return dockerError("validate restored workspace registry", result, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user