fix(auth): complete Task 13 deployment review
This commit is contained in:
@@ -394,7 +394,8 @@ type renderedCompose struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"volumes"`
|
||||
Services map[string]struct {
|
||||
Image string `json:"image"`
|
||||
Image string `json:"image"`
|
||||
ContainerName string `json:"container_name"`
|
||||
} `json:"services"`
|
||||
}
|
||||
|
||||
@@ -474,18 +475,13 @@ func imageIdentities(ctx context.Context, installation config.Installation, runn
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matching := make([]composeImageIdentity, 0, len(inspected))
|
||||
for _, item := range inspected {
|
||||
if item.Service == "" || item.Service == name {
|
||||
matching = append(matching, item)
|
||||
}
|
||||
expectedContainer := rendered.Services[name].ContainerName
|
||||
if expectedContainer == "" {
|
||||
expectedContainer = installation.ProjectName() + "-" + name + "-1"
|
||||
}
|
||||
if len(matching) > 1 {
|
||||
return nil, errors.New("Docker Compose returned invalid image identities")
|
||||
}
|
||||
id := ""
|
||||
if len(matching) == 1 {
|
||||
id = matching[0].ID
|
||||
id, err := selectComposeImageIdentity(name, expectedContainer, inspected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
images = append(images, ImageIdentity{Service: name, Reference: rendered.Services[name].Image, ID: id})
|
||||
}
|
||||
@@ -500,7 +496,7 @@ type composeImageIdentity struct {
|
||||
|
||||
func decodeComposeImageIdentities(value string) ([]composeImageIdentity, error) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
if trimmed == "" || trimmed == "null" {
|
||||
return nil, nil
|
||||
}
|
||||
var identities []composeImageIdentity
|
||||
@@ -530,6 +526,22 @@ func decodeComposeImageIdentities(value string) ([]composeImageIdentity, error)
|
||||
return identities, nil
|
||||
}
|
||||
|
||||
func selectComposeImageIdentity(service string, expectedContainer string, identities []composeImageIdentity) (string, error) {
|
||||
if len(identities) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
matching := make([]composeImageIdentity, 0, len(identities))
|
||||
for _, item := range identities {
|
||||
if item.Service == service || item.Service == "" && item.ContainerName == expectedContainer {
|
||||
matching = append(matching, item)
|
||||
}
|
||||
}
|
||||
if len(matching) != 1 {
|
||||
return "", errors.New("Docker Compose returned invalid image identities")
|
||||
}
|
||||
return matching[0].ID, nil
|
||||
}
|
||||
|
||||
func installationRunning(ctx context.Context, installation config.Installation, runner archiveRunner) (bool, error) {
|
||||
result, err := runner.Run(ctx, installation.ComposeArgs("ps", "--all", "--format", "json"), nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
|
||||
var requiredTestVolumes = []string{"settings", "pi-state", "workspace-registry", "workspace-secrets", "sessions", "qdrant-data", "embedding-models"}
|
||||
|
||||
func TestDecodeComposeImageIdentitiesAcceptsArrayAndStreamingJSON(t *testing.T) {
|
||||
func TestDecodeComposeImageIdentitiesAcceptsNonEmptyArrayAndStreamingJSON(t *testing.T) {
|
||||
for name, input := range map[string]string{
|
||||
"array": `[{"ContainerName":"project-core-1","ID":"sha256:core"},{"ContainerName":"project-frontend-1","ID":"sha256:frontend"}]`,
|
||||
"streaming": "{\"Service\":\"core\",\"ID\":\"sha256:core\"}\n{\"Service\":\"frontend\",\"ID\":\"sha256:frontend\"}\n",
|
||||
@@ -41,11 +41,72 @@ func TestDecodeComposeImageIdentitiesAcceptsArrayAndStreamingJSON(t *testing.T)
|
||||
}
|
||||
|
||||
func TestDecodeComposeImageIdentitiesAcceptsNoContainerForProfiledService(t *testing.T) {
|
||||
for _, input := range []string{"", "[]"} {
|
||||
identities, err := decodeComposeImageIdentities(input)
|
||||
if err != nil || len(identities) != 0 {
|
||||
t.Fatalf("decodeComposeImageIdentities(%q) = %#v, %v", input, identities, err)
|
||||
}
|
||||
for name, input := range map[string]string{
|
||||
"empty output": "",
|
||||
"empty array": "[]",
|
||||
"null": "null",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
identities, err := decodeComposeImageIdentities(input)
|
||||
if err != nil || len(identities) != 0 {
|
||||
t.Fatalf("decodeComposeImageIdentities(%q) = %#v, %v", input, identities, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeComposeImageIdentitiesRejectsMalformedOrIncompleteOutput(t *testing.T) {
|
||||
for name, input := range map[string]string{
|
||||
"malformed JSON": "[",
|
||||
"scalar JSON": "true",
|
||||
"empty object": "{}",
|
||||
"null array element": "[null]",
|
||||
"missing image ID": `[{"Service":"core"}]`,
|
||||
"trailing document": "null\n{}",
|
||||
"trailing malformed bytes": `null garbage`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if identities, err := decodeComposeImageIdentities(input); err == nil {
|
||||
t.Fatalf("decodeComposeImageIdentities(%q) = %#v, nil; want error", input, identities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectComposeImageIdentityUsesExactComposeContainerWhenServiceIsAbsent(t *testing.T) {
|
||||
identities := []composeImageIdentity{
|
||||
{ContainerName: "project-core-1", ID: "sha256:core"},
|
||||
{ContainerName: "project-llm", ID: "sha256:shared-image"},
|
||||
}
|
||||
id, err := selectComposeImageIdentity("core", "project-core-1", identities)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id != "sha256:core" {
|
||||
t.Fatalf("selected image ID = %q, want sha256:core", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectComposeImageIdentityDoesNotFailOpen(t *testing.T) {
|
||||
for name, identities := range map[string][]composeImageIdentity{
|
||||
"unrelated container only": {{ContainerName: "project-llm", ID: "sha256:shared-image"}},
|
||||
"duplicate service": {
|
||||
{Service: "core", ID: "sha256:first"},
|
||||
{Service: "core", ID: "sha256:second"},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if id, err := selectComposeImageIdentity("core", "project-core-1", identities); err == nil {
|
||||
t.Fatalf("selectComposeImageIdentity() = %q, nil; want error", id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectComposeImageIdentityAcceptsSemanticallyEmptyOutput(t *testing.T) {
|
||||
id, err := selectComposeImageIdentity("workspace-maintenance", "project-workspace-maintenance-1", nil)
|
||||
if err != nil || id != "" {
|
||||
t.Fatalf("selectComposeImageIdentity() = %q, %v; want empty identity", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -572,6 +572,13 @@ func validateVolumeTar(ctx context.Context, member *zip.File) error {
|
||||
return fmt.Errorf("read volume archive: %w", err)
|
||||
}
|
||||
name := strings.TrimSuffix(header.Name, "/")
|
||||
if name == "." {
|
||||
if header.Typeflag != tar.TypeDir {
|
||||
return errors.New("volume archive root marker is not a directory")
|
||||
}
|
||||
continue
|
||||
}
|
||||
name = strings.TrimPrefix(name, "./")
|
||||
if _, err := validateArchiveMemberPath(name); err != nil {
|
||||
return fmt.Errorf("volume archive path is unsafe: %w", err)
|
||||
}
|
||||
|
||||
@@ -233,6 +233,60 @@ func TestPreflightRejectsTraversalAndSymlinkInsideVolumeTar(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreflightAcceptsCanonicalTarRootDirectoryAndRelativeMembers(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
var payload strings.Builder
|
||||
writer := tar.NewWriter(&stringWriter{value: &payload})
|
||||
for _, header := range []tar.Header{
|
||||
{Name: "./", Mode: 0o755, Typeflag: tar.TypeDir},
|
||||
{Name: "./payload", Mode: 0o600, Size: 1, Typeflag: tar.TypeReg},
|
||||
} {
|
||||
if err := writer.WriteHeader(&header); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if header.Size > 0 {
|
||||
if _, err := writer.Write([]byte("x")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archive := filepath.Join(t.TempDir(), "canonical-volume.zip")
|
||||
writePreflightArchive(t, archive, preflightArchiveSpec{entries: []preflightArchiveEntry{{
|
||||
path: "volumes/sessions.tar", body: []byte(payload.String()), kind: EntryVolume,
|
||||
}}})
|
||||
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result.CloseArchive()
|
||||
}
|
||||
|
||||
func TestPreflightRejectsNonDirectoryTarRootMarker(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
var payload strings.Builder
|
||||
writer := tar.NewWriter(&stringWriter{value: &payload})
|
||||
header := tar.Header{Name: ".", Mode: 0o600, Size: 1, Typeflag: tar.TypeReg}
|
||||
if err := writer.WriteHeader(&header); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := writer.Write([]byte("x")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archive := filepath.Join(t.TempDir(), "unsafe-root-volume.zip")
|
||||
writePreflightArchive(t, archive, preflightArchiveSpec{entries: []preflightArchiveEntry{{
|
||||
path: "volumes/sessions.tar", body: []byte(payload.String()), kind: EntryVolume,
|
||||
}}})
|
||||
if _, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()); err == nil {
|
||||
t.Fatal("Preflight accepted a non-directory TAR root marker")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreflightRejectsArchivesThatExceedConfiguredProcessingLimits(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
tests := []struct {
|
||||
|
||||
@@ -285,7 +285,7 @@ func safeRestoreParent(target string) bool {
|
||||
|
||||
func volumeRestoreCommand(volume string) []string {
|
||||
return []string{
|
||||
"run", "--rm", "--network", "none", "--mount", "type=volume,src=" + volume + ",dst=/target",
|
||||
"run", "--rm", "--interactive", "--network", "none", "--mount", "type=volume,src=" + volume + ",dst=/target",
|
||||
helperImage, "sh", "-ceu",
|
||||
"rm -rf -- /target/* /target/.[!.]* /target/..?*; tar --numeric-owner -C /target -xf -",
|
||||
}
|
||||
|
||||
@@ -30,6 +30,19 @@ func TestRestorePublicPathUsesConcreteProductionPreflight(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolumeRestoreCommandKeepsTarInputOpenWithoutTTY(t *testing.T) {
|
||||
args := volumeRestoreCommand("project_sessions")
|
||||
wantPrefix := []string{"run", "--rm", "--interactive", "--network", "none"}
|
||||
if len(args) < len(wantPrefix) || !equalStrings(args[:len(wantPrefix)], wantPrefix) {
|
||||
t.Fatalf("volumeRestoreCommand() prefix = %q, want %q", args, wantPrefix)
|
||||
}
|
||||
for _, arg := range args {
|
||||
if arg == "--tty" || arg == "-t" {
|
||||
t.Fatalf("volumeRestoreCommand() requests a TTY: %q", args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreRestoresVerifiedVolumesInManifestOrderBeforeAuthenticationReset(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
archive := filepath.Join(t.TempDir(), "restore-volumes.zip")
|
||||
|
||||
@@ -477,7 +477,8 @@ func setMaintenance(ctx context.Context, runner Runner, enabled bool) error {
|
||||
if enabled {
|
||||
path = "activate"
|
||||
}
|
||||
args := []string{"exec", "-T", "core", "curl", "-fsS", "-X", "POST", "http://127.0.0.1:8787/internal/maintenance/" + path}
|
||||
args := append([]string{"exec", "-T", "core", "curl", "-fsS"}, internalIdentityHeaders...)
|
||||
args = append(args, "-X", "POST", "http://127.0.0.1:8787/internal/maintenance/"+path)
|
||||
result, err := runCompose(ctx, runner, args...)
|
||||
status, valid := parseMaintenanceStatus(result.Stdout)
|
||||
if err == nil && valid && status.Active == enabled && status.Admissions == 0 && !status.RecoveryRequired {
|
||||
@@ -517,7 +518,9 @@ func parseMaintenanceStatus(value string) (MaintenanceState, bool) {
|
||||
}
|
||||
|
||||
func MaintenanceStatus(ctx context.Context, runner Runner) (MaintenanceState, error) {
|
||||
result, err := runCompose(ctx, runner, "exec", "-T", "core", "curl", "-fsS", "http://127.0.0.1:8787/internal/maintenance/status")
|
||||
args := append([]string{"exec", "-T", "core", "curl", "-fsS"}, internalIdentityHeaders...)
|
||||
args = append(args, "http://127.0.0.1:8787/internal/maintenance/status")
|
||||
result, err := runCompose(ctx, runner, args...)
|
||||
if err != nil {
|
||||
return MaintenanceState{}, commandError("maintenance status check", result, err)
|
||||
}
|
||||
|
||||
@@ -779,6 +779,39 @@ func TestUpdateRequiresConfirmationAndDrainsActiveSessions(t *testing.T) {
|
||||
assertCalled(t, fake.calls, "/internal/maintenance/deactivate")
|
||||
}
|
||||
|
||||
func TestMaintenanceControlUsesExactLoopbackOperatorIdentity(t *testing.T) {
|
||||
fake := newFakeRunner()
|
||||
if err := setMaintenance(context.Background(), fake, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fake.maintenance = true
|
||||
if _, err := MaintenanceStatus(context.Background(), fake); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []string{"/internal/maintenance/activate", "/internal/maintenance/status"} {
|
||||
found := false
|
||||
for _, call := range fake.calls {
|
||||
if !strings.Contains(call, path) {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
for _, header := range []string{
|
||||
"x-thoth-principal-issuer: tht",
|
||||
"x-thoth-principal-subject: tht-maintenance",
|
||||
"x-thoth-principal-display-name: Tht maintenance",
|
||||
"x-thoth-is-admin: 1",
|
||||
} {
|
||||
if !strings.Contains(call, header) {
|
||||
t.Fatalf("maintenance call %q lacks %q", call, header)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("maintenance call %q was not made", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRollbackRestoresInterruptedOrPreviouslyRecordedState(t *testing.T) {
|
||||
fake := newFakeRunner()
|
||||
statePath := filepath.Join(t.TempDir(), "state.json")
|
||||
|
||||
Reference in New Issue
Block a user