Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
This commit is contained in:
@@ -36,6 +36,7 @@ var atomicWriteNewFile = writeNewFileAtomically
|
||||
var effectiveUID = currentEffectiveUID
|
||||
|
||||
type answers struct {
|
||||
shell config.Shell
|
||||
installationID, profile string
|
||||
workspaceRemote, workspaceBranch string
|
||||
workspaceAccess string
|
||||
@@ -65,6 +66,7 @@ type generatedDescriptor struct {
|
||||
} `yaml:"runtimeProjection,omitempty"`
|
||||
} `yaml:"authentication"`
|
||||
ModelCatalog config.ModelCatalog `yaml:"modelCatalog"`
|
||||
Shell config.Shell `yaml:"shell,omitempty"`
|
||||
Overrides []string `yaml:"overrides"`
|
||||
}
|
||||
|
||||
@@ -226,6 +228,11 @@ func collectAnswers(request Request, input io.Reader, output io.Writer, root str
|
||||
func answersFromRequest(request Request) answers {
|
||||
answer := request.Answers
|
||||
return answers{
|
||||
shell: config.Shell{
|
||||
Mode: firstNonEmpty(answer.ShellMode, os.Getenv("THT_SETUP_SHELL_MODE")),
|
||||
DefaultLocale: firstNonEmpty(answer.ShellDefaultLocale, os.Getenv("THT_SETUP_SHELL_DEFAULT_LOCALE")),
|
||||
Adapter: firstNonEmpty(answer.ShellAdapter, os.Getenv("THT_SETUP_SHELL_ADAPTER")),
|
||||
},
|
||||
workspaceRemote: firstNonEmpty(answer.WorkspaceRemote, os.Getenv("THT_SETUP_WORKSPACE_REMOTE")),
|
||||
workspaceBranch: firstNonEmpty(answer.WorkspaceBranch, os.Getenv("THT_SETUP_WORKSPACE_BRANCH")),
|
||||
workspaceAccess: firstNonEmpty(answer.WorkspaceAccess, os.Getenv("THT_SETUP_WORKSPACE_ACCESS")),
|
||||
@@ -272,6 +279,9 @@ func prompt(scanner *bufio.Scanner, output io.Writer, question, defaultValue str
|
||||
}
|
||||
|
||||
func validateAnswers(value answers) error {
|
||||
if err := value.shell.NormalizeDefaults(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !installationIDPattern.MatchString(value.installationID) {
|
||||
return errors.New("installation ID must contain only letters, numbers, dashes, and underscores")
|
||||
}
|
||||
@@ -322,6 +332,13 @@ func installationDirectory(root, id string) (string, error) {
|
||||
|
||||
func render(root, descriptorPath string, value answers) ([]byte, []byte, error) {
|
||||
descriptor := generatedDescriptor{SchemaVersion: 2, Profile: value.profile, ProjectDirectory: root, EnvFile: filepath.Join(filepath.Dir(descriptorPath), environmentName), ModelCatalog: defaultModelCatalog()}
|
||||
// Preserve the legacy descriptor bytes when no shell setting was requested.
|
||||
if value.shell != (config.Shell{}) {
|
||||
if err := value.shell.NormalizeDefaults(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
descriptor.Shell = value.shell
|
||||
}
|
||||
descriptor.Workspace.Remote, descriptor.Workspace.Branch, descriptor.Workspace.Access = value.workspaceRemote, value.workspaceBranch, value.workspaceAccess
|
||||
descriptor.Authentication.ConfigDirectory = filepath.Join(filepath.Dir(descriptorPath), "auth")
|
||||
if value.profile == "server" {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||
)
|
||||
|
||||
func TestEnsureFilesProjectsShellAnswersThroughInstallationLoad(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
answers Answers
|
||||
want config.Shell
|
||||
}{
|
||||
{"defaults", Answers{}, config.Shell{Mode: "embedded", DefaultLocale: "en", Adapter: "omics-portal"}},
|
||||
{"full", Answers{ShellMode: "full"}, config.Shell{Mode: "full", DefaultLocale: "en"}},
|
||||
{"locale without UI catalog", Answers{ShellMode: "full", ShellDefaultLocale: "fr-CA"}, config.Shell{Mode: "full", DefaultLocale: "fr-CA"}},
|
||||
{"embedded Italian", Answers{ShellMode: "embedded", ShellDefaultLocale: "it", ShellAdapter: "omics-portal"}, config.Shell{Mode: "embedded", DefaultLocale: "it", Adapter: "omics-portal"}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
root := newProject(t, "shell setup")
|
||||
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
|
||||
for _, key := range []string{"THT_SETUP_SHELL_MODE", "THT_SETUP_SHELL_DEFAULT_LOCALE", "THT_SETUP_SHELL_ADAPTER"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
request := Request{ProjectRoot: root, InstallationID: "shell", Profile: "local", NonInteractive: true, Answers: test.answers}
|
||||
result, err := EnsureFiles(request, strings.NewReader(""), ioDiscard{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
installation, err := config.Load(result.DescriptorPath)
|
||||
if err != nil || installation.Shell != test.want {
|
||||
t.Fatalf("Load() shell = %#v, %v, want %#v", installation.Shell, err, test.want)
|
||||
}
|
||||
if repeated, err := EnsureFiles(request, strings.NewReader(""), ioDiscard{}); err != nil || len(repeated.Created) != 0 {
|
||||
t.Fatalf("repeated setup = %#v, %v", repeated, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureFilesAcceptsShellEnvironmentAndExplicitPrecedence(t *testing.T) {
|
||||
root := newProject(t, "shell environment")
|
||||
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
|
||||
t.Setenv("THT_SETUP_SHELL_MODE", "embedded")
|
||||
t.Setenv("THT_SETUP_SHELL_DEFAULT_LOCALE", "it")
|
||||
t.Setenv("THT_SETUP_SHELL_ADAPTER", "omics-portal")
|
||||
result, err := EnsureFiles(Request{
|
||||
ProjectRoot: root, InstallationID: "shell", Profile: "local", NonInteractive: true,
|
||||
Answers: Answers{ShellMode: "full"},
|
||||
}, strings.NewReader(""), ioDiscard{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
installation, err := config.Load(result.DescriptorPath)
|
||||
if err != nil || installation.Shell != (config.Shell{Mode: "full", DefaultLocale: "it"}) {
|
||||
t.Fatalf("Load() shell = %#v, %v", installation.Shell, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureFilesRejectsInvalidShellBeforeWritingInstallation(t *testing.T) {
|
||||
for _, answers := range []Answers{{ShellMode: "fullscreen"}, {ShellDefaultLocale: "de_DE"}, {ShellAdapter: "unknown"}} {
|
||||
root := newProject(t, "invalid shell")
|
||||
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
|
||||
_, err := EnsureFiles(Request{
|
||||
ProjectRoot: root, InstallationID: "shell", Profile: "local", NonInteractive: true, Answers: answers,
|
||||
}, strings.NewReader(""), ioDiscard{})
|
||||
if err == nil || !strings.Contains(err.Error(), "shell.") {
|
||||
t.Fatalf("EnsureFiles() error = %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "deploy", "shell")); !os.IsNotExist(err) {
|
||||
t.Fatalf("invalid shell created installation directory: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ type Request struct {
|
||||
// Answers are optional explicit answers supplied by command flags. Empty values may be supplied
|
||||
// through THT_SETUP_* environment variables or collected interactively.
|
||||
type Answers struct {
|
||||
ShellMode string
|
||||
ShellDefaultLocale string
|
||||
ShellAdapter string
|
||||
WorkspaceRemote string
|
||||
WorkspaceBranch string
|
||||
WorkspaceAccess string
|
||||
|
||||
Reference in New Issue
Block a user