feat(cli): prepare and validate application documents offline

This commit is contained in:
Codex
2026-09-28 16:33:07 +02:00
parent 64e6b9664a
commit b9c3369e7b
20 changed files with 1164 additions and 49 deletions
+129
View File
@@ -0,0 +1,129 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"path/filepath"
"slices"
"strings"
"github.com/aritmolab/thothii/tools/tht/internal/preparation"
)
func installationDocumentsCommand(ctx context.Context, installationPath string, args []string, stdout io.Writer) int {
report := preparation.NewReport()
status := 0
options := map[string]string{}
valid := len(args) > 0
for index := 1; index < len(args); index++ {
key := args[index]
if _, duplicate := options[key]; duplicate {
valid = false
break
}
if key == "--json" {
options[key] = "true"
continue
}
if (key != "--directory" && key != "--workspaces" && key != "--bootstrap") || index+1 == len(args) {
valid = false
break
}
options[key] = args[index+1]
index++
}
if !valid || (args[0] == "validate" && (installationPath == "" || options["--workspaces"] == "" || options["--directory"] != "")) || (args[0] != "validate" && (options["--directory"] == "" || options["--workspaces"] != "" || options["--bootstrap"] != "")) {
report.Add("CLI", "$", "usage", "Use installation prepare|credentials --directory PATH [--json], or tht --installation ABSOLUTE_PATH installation validate --workspaces PATH [--bootstrap PATH] [--json].")
status = 2
} else if args[0] == "validate" {
installation, checkedReport := preparation.Validate(installationPath)
report = checkedReport
if report.OK {
bootstrap := options["--bootstrap"]
if bootstrap == "" {
bootstrap = filepath.Join(filepath.Dir(installationPath), "database-bootstrap.yaml")
}
bootstrap, _ = filepath.Abs(bootstrap)
workspace, _ := filepath.Abs(options["--workspaces"])
if resolved, err := filepath.EvalSymlinks(workspace); err == nil {
workspace = resolved
}
outsideWorkspace := func(path, document, field string) bool {
relative, err := filepath.Rel(workspace, path)
if err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
report.Add(document, field, "installation_file_in_workspace", "Keep installation documents and credential files outside the shared workspace repository.")
return false
}
return true
}
for _, path := range []string{installationPath, installation.EnvFile, installation.AuthenticationDirectory(), bootstrap} {
outsideWorkspace(path, "thothii-installation.yaml", "local-files")
}
files, _ := installation.SecretFiles()
for _, path := range files {
outsideWorkspace(path, "operator.env", "protected-file-reference")
}
if preparation.CheckYAML(bootstrap, "database-bootstrap.yaml", &report) {
var output, discarded bytes.Buffer
code := workspaceDocumentsCommand(ctx, []string{"bootstrap", "--directory", workspace, "--bootstrap", bootstrap, "--json"}, &output, &discarded)
var checked struct {
OK bool `json:"ok"`
Issues []preparation.Issue `json:"issues"`
Warnings []string `json:"warnings"`
SecretFiles []struct {
Field string `json:"field"`
Path string `json:"path"`
} `json:"secret_files"`
}
if json.Unmarshal(output.Bytes(), &checked) != nil {
report.Add("CLI", "$", "validator_unavailable", "Reinstall the matching tht and workspace helper pair.")
} else if code != 0 || !checked.OK {
report.Issues = append(report.Issues, checked.Issues...)
if len(checked.Issues) == 0 {
report.Add("database-bootstrap.yaml", "$", "bootstrap_invalid", "Correct workspace and binding documents, then validate again.")
}
} else {
report.Warnings = append(report.Warnings, checked.Warnings...)
for _, file := range checked.SecretFiles {
if outsideWorkspace(file.Path, "database-bootstrap.yaml", file.Field) {
preparation.CheckSecret(file.Path, "database-bootstrap.yaml", file.Field, false, &report)
}
}
}
}
}
} else {
directory, err := filepath.Abs(options["--directory"])
if err == nil {
if args[0] == "prepare" {
err = preparation.Prepare(directory)
} else {
err = preparation.Credentials(ctx, directory)
}
}
if err != nil {
report.Add("preparation", "$", "preparation_refused", err.Error())
}
}
report.OK = len(report.Issues) == 0
if !report.OK && status == 0 {
status = 1
}
if slices.Contains(args, "--json") {
_ = json.NewEncoder(stdout).Encode(report)
} else {
if report.OK {
fmt.Fprintln(stdout, "Document operation completed. Local validation does not establish runtime readiness.")
}
for _, issue := range report.Issues {
fmt.Fprintf(stdout, "%s [%s] %s: %s\n", issue.Document, issue.Field, issue.Code, issue.Correction)
}
for _, warning := range report.Warnings {
fmt.Fprintln(stdout, warning)
}
}
return status
}
@@ -0,0 +1,96 @@
package main
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"runtime"
"testing"
)
func TestInstallationPrepareDocumentsBeforeRuntime(t *testing.T) {
t.Setenv("PATH", "")
root, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
destination := filepath.Join(root, "installation")
var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"installation", "prepare", "--directory", destination, "--json"}, &stdout, &stderr)
if status != 0 {
t.Fatalf("status=%d stdout=%s stderr=%s", status, &stdout, &stderr)
}
var report map[string]any
if json.Unmarshal(stdout.Bytes(), &report) != nil || report["ok"] != true {
t.Fatalf("report=%s", &stdout)
}
for _, name := range []string{"thothii-installation.yaml", "operator.env", "database-bootstrap.yaml", "README.md"} {
if _, err := os.Stat(filepath.Join(destination, name)); err != nil {
t.Fatal(err)
}
}
before, _ := os.ReadFile(filepath.Join(destination, "thothii-installation.yaml"))
stdout.Reset()
stderr.Reset()
if run(context.Background(), []string{"installation", "prepare", "--directory", destination, "--json"}, &stdout, &stderr) == 0 {
t.Fatal("overwrote existing documents")
}
after, _ := os.ReadFile(filepath.Join(destination, "thothii-installation.yaml"))
if !bytes.Equal(before, after) {
t.Fatal("existing document changed")
}
}
func TestInstallationCredentialsAreExplicitPrivateAndNeverReplaced(t *testing.T) {
t.Setenv("PATH", "")
root, _ := filepath.EvalSymlinks(t.TempDir())
destination := filepath.Join(root, "installation")
var stdout, stderr bytes.Buffer
if run(context.Background(), []string{"installation", "prepare", "--directory", destination}, &stdout, &stderr) != 0 {
t.Fatal(&stdout, &stderr)
}
secret := filepath.Join(destination, "secrets", "catalog-runtime-password")
if _, err := os.Stat(secret); !os.IsNotExist(err) {
t.Fatal("prepare generated a secret implicitly")
}
stdout.Reset()
stderr.Reset()
if run(context.Background(), []string{"installation", "credentials", "--directory", destination, "--json"}, &stdout, &stderr) != 0 {
t.Fatal(&stdout, &stderr)
}
before, err := os.ReadFile(secret)
if err != nil || len(bytes.TrimSpace(before)) < 32 {
t.Fatal("missing strong technical credential", err)
}
if bytes.Contains(stdout.Bytes(), bytes.TrimSpace(before)) || bytes.Contains(stderr.Bytes(), bytes.TrimSpace(before)) {
t.Fatal("secret leaked")
}
info, _ := os.Stat(secret)
if runtime.GOOS != "windows" && info.Mode().Perm()&0o077 != 0 {
t.Fatal("credential is not private")
}
stdout.Reset()
stderr.Reset()
if run(context.Background(), []string{"installation", "credentials", "--directory", destination, "--json"}, &stdout, &stderr) != 0 {
t.Fatal(&stdout, &stderr)
}
after, _ := os.ReadFile(secret)
if !bytes.Equal(before, after) {
t.Fatal("credential was replaced")
}
}
func TestInstallationValidateRejectsWrongEnvironmentFieldType(t *testing.T) {
root, _ := filepath.EvalSymlinks(t.TempDir())
path := filepath.Join(root, "thothii-installation.yaml")
if err := os.WriteFile(path, []byte("schemaVersion: 2\nenvFile: []\n"), 0o600); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"--installation", path, "installation", "validate", "--workspaces", root, "--json"}, &stdout, &stderr)
if status != 1 {
t.Fatalf("invalid field accepted: %d %s", status, &stdout)
}
}
+9
View File
@@ -43,6 +43,12 @@ Commands:
setup [--complete|--configure-only] [--installation-id ID] [--profile local|server]
[--shell-mode full|embedded] [--shell-default-locale BCP47-TAG] [--shell-adapter omics-portal]
Create, validate, and optionally complete the local installation.
installation prepare --directory NEW_PATH [--json]
Create commented installation, environment and database bootstrap templates.
installation credentials --directory PATH [--json]
Explicitly generate protected technical credentials before setup.
installation validate --workspaces PATH [--bootstrap PATH] [--json]
Check prepared application documents; requires --installation.
installation migrate --output PATH --session-default PROVIDER/MODEL
--embedding-id PROVIDER/MODEL --embedding-dimensions N
Create a review-only schema-v2 candidate from all three legacy model sources.
@@ -138,6 +144,9 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
return versionCommand(commandArgs, stdout, stderr)
}
if command == "installation" {
if len(commandArgs) > 0 && (commandArgs[0] == "prepare" || commandArgs[0] == "credentials" || commandArgs[0] == "validate") {
return installationDocumentsCommand(ctx, installationPath, commandArgs, stdout)
}
if len(commandArgs) > 0 && commandArgs[0] == "generate" {
return installationGenerationCommand(installationPath, commandArgs[1:], stdout, stderr)
}