feat(setup): generate local installation configuration
This commit is contained in:
@@ -18,7 +18,9 @@ import (
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/output"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/pi"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/project"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/serverops"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/setup"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/workspaceops"
|
||||
)
|
||||
|
||||
@@ -28,6 +30,8 @@ When --installation is omitted, tht uses THOTHII_INSTALLATION or discovers one v
|
||||
descriptor in the current project tree.
|
||||
|
||||
Commands:
|
||||
setup [--configure-only] [--installation-id ID] [--profile local|server]
|
||||
Create or validate the local non-secret installation configuration.
|
||||
status Show the Compose service state.
|
||||
doctor Validate Docker, Compose, rendered configuration, line endings, volumes, and health.
|
||||
logs Show the latest 200 sanitized service log lines (bounded; no follow mode).
|
||||
@@ -86,6 +90,9 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
fmt.Fprint(stdout, usage)
|
||||
return 0
|
||||
}
|
||||
if command == "setup" {
|
||||
return setupCommand(installationPath, commandArgs, stdout, stderr)
|
||||
}
|
||||
return commandUsageError(stderr, fmt.Sprintf("unknown command %q", command))
|
||||
}
|
||||
workingDirectory, err := os.Getwd()
|
||||
@@ -198,6 +205,120 @@ func isBootstrapCommand(command string) bool {
|
||||
return command == "help" || command == "setup" || command == "version"
|
||||
}
|
||||
|
||||
func setupCommand(installationPath string, args []string, stdout, stderr io.Writer) int {
|
||||
request, err := parseSetupArgs(args)
|
||||
if err != nil {
|
||||
return commandUsageError(stderr, err.Error())
|
||||
}
|
||||
workingDirectory, err := os.Getwd()
|
||||
if err != nil {
|
||||
return commandUsageError(stderr, "current directory is unavailable")
|
||||
}
|
||||
root, err := project.Discover(workingDirectory)
|
||||
if err != nil {
|
||||
return commandUsageError(stderr, err.Error())
|
||||
}
|
||||
request.ProjectRoot = root.Path
|
||||
if installationPath != "" {
|
||||
id, pathErr := installationIDFromPath(root.Path, installationPath)
|
||||
if pathErr != nil {
|
||||
return commandUsageError(stderr, pathErr.Error())
|
||||
}
|
||||
if request.InstallationID != "" && request.InstallationID != id {
|
||||
return commandUsageError(stderr, "--installation and --installation-id must identify the same installation")
|
||||
}
|
||||
request.InstallationID = id
|
||||
}
|
||||
result, err := setup.EnsureFiles(request, os.Stdin, stdout)
|
||||
if err != nil {
|
||||
return commandUsageError(stderr, err.Error())
|
||||
}
|
||||
fmt.Fprintf(stdout, "Configuration is ready: %s\n", result.DescriptorPath)
|
||||
return 0
|
||||
}
|
||||
|
||||
func installationIDFromPath(root, installationPath string) (string, error) {
|
||||
if filepath.Base(installationPath) != "thothii-installation.yaml" {
|
||||
return "", errors.New("--installation must name thothii-installation.yaml below deploy/<installation-id>")
|
||||
}
|
||||
relative, err := filepath.Rel(filepath.Join(root, "deploy"), installationPath)
|
||||
if err != nil {
|
||||
return "", errors.New("--installation must be below this project's deploy directory")
|
||||
}
|
||||
parts := strings.Split(filepath.Clean(relative), string(filepath.Separator))
|
||||
if len(parts) != 2 || parts[0] == "." || parts[0] == ".." || parts[1] != "thothii-installation.yaml" {
|
||||
return "", errors.New("--installation must be below this project's deploy/<installation-id> directory")
|
||||
}
|
||||
return parts[0], nil
|
||||
}
|
||||
|
||||
func parseSetupArgs(args []string) (setup.Request, error) {
|
||||
request := setup.Request{}
|
||||
for len(args) > 0 {
|
||||
flag := args[0]
|
||||
args = args[1:]
|
||||
switch flag {
|
||||
case "--configure-only":
|
||||
if request.ConfigureOnly {
|
||||
return setup.Request{}, errors.New("--configure-only may be supplied once")
|
||||
}
|
||||
request.ConfigureOnly = true
|
||||
case "--non-interactive":
|
||||
if request.NonInteractive {
|
||||
return setup.Request{}, errors.New("--non-interactive may be supplied once")
|
||||
}
|
||||
request.NonInteractive = true
|
||||
case "--create-secret-templates":
|
||||
if request.Answers.CreateSecretTemplates {
|
||||
return setup.Request{}, errors.New("--create-secret-templates may be supplied once")
|
||||
}
|
||||
request.Answers.CreateSecretTemplates = true
|
||||
default:
|
||||
if len(args) == 0 {
|
||||
return setup.Request{}, fmt.Errorf("%s requires a value", flag)
|
||||
}
|
||||
value := args[0]
|
||||
args = args[1:]
|
||||
var target *string
|
||||
switch flag {
|
||||
case "--installation-id":
|
||||
target = &request.InstallationID
|
||||
case "--profile":
|
||||
target = &request.Profile
|
||||
case "--workspace-remote":
|
||||
target = &request.Answers.WorkspaceRemote
|
||||
case "--workspace-branch":
|
||||
target = &request.Answers.WorkspaceBranch
|
||||
case "--workspace-access":
|
||||
target = &request.Answers.WorkspaceAccess
|
||||
case "--dwh-rest-url":
|
||||
target = &request.Answers.DWHRESTURL
|
||||
case "--llm-url":
|
||||
target = &request.Answers.LLMURL
|
||||
case "--secrets-file":
|
||||
target = &request.Answers.SecretsFile
|
||||
case "--pi-auth-file":
|
||||
target = &request.Answers.PiAuthFile
|
||||
case "--git-credentials-file":
|
||||
target = &request.Answers.GitCredentialsFile
|
||||
case "--git-ca-file":
|
||||
target = &request.Answers.GitCAFile
|
||||
case "--git-ssh-key-file":
|
||||
target = &request.Answers.GitSSHKeyFile
|
||||
case "--git-known-hosts-file":
|
||||
target = &request.Answers.GitKnownHostsFile
|
||||
default:
|
||||
return setup.Request{}, fmt.Errorf("unknown setup option %q", flag)
|
||||
}
|
||||
if *target != "" {
|
||||
return setup.Request{}, fmt.Errorf("%s may be supplied once", flag)
|
||||
}
|
||||
*target = value
|
||||
}
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func writeRemovalTargets(outputWriter io.Writer, project string, targets []serverops.Container) {
|
||||
fmt.Fprintf(outputWriter, "Removal targets for installation project %s:\n", project)
|
||||
if len(targets) == 0 {
|
||||
|
||||
@@ -174,6 +174,24 @@ func TestParseArgsMakesInstallationOptionalAndAcceptsOverrideAfterCommand(t *tes
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSetupArgsAcceptsExplicitNonInteractiveAnswers(t *testing.T) {
|
||||
request, err := parseSetupArgs([]string{
|
||||
"--configure-only", "--non-interactive", "--installation-id", "ci", "--profile", "server",
|
||||
"--workspace-remote", "https://git.example.invalid/workspaces.git", "--workspace-branch", "release",
|
||||
"--workspace-access", "https", "--secrets-file", "/tmp/thothii.secrets", "--pi-auth-file", "/tmp/pi-auth.json",
|
||||
"--git-credentials-file", "/tmp/git-credentials", "--git-ca-file", "/tmp/git-ca.pem",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !request.ConfigureOnly || !request.NonInteractive || request.InstallationID != "ci" || request.Profile != "server" {
|
||||
t.Fatalf("setup request = %#v", request)
|
||||
}
|
||||
if request.Answers.WorkspaceBranch != "release" || request.Answers.GitCAFile != "/tmp/git-ca.pem" {
|
||||
t.Fatalf("setup answers = %#v", request.Answers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPiStatusUsesInstallationEnvironmentWithoutFlag(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n")
|
||||
fixture.setEnvironment(t)
|
||||
|
||||
Reference in New Issue
Block a user