346 lines
16 KiB
Go
346 lines
16 KiB
Go
package app_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"proxyctl/internal/app"
|
|
"proxyctl/internal/dockerclient"
|
|
"proxyctl/internal/operationlock"
|
|
"proxyctl/internal/profile"
|
|
"proxyctl/internal/shell"
|
|
"proxyctl/internal/target"
|
|
)
|
|
|
|
type coordinatedTarget struct {
|
|
id string
|
|
state target.State
|
|
applied []string
|
|
appliedName string
|
|
failName string
|
|
availabilityCalls int
|
|
inspectCalls int
|
|
}
|
|
|
|
func (t *coordinatedTarget) ID() string { return t.id }
|
|
func (t *coordinatedTarget) Description() target.Description {
|
|
return target.Description{Name: t.id, Summary: "summary", Details: "effects, non-effects, ownership, permissions, and side effects"}
|
|
}
|
|
func (t *coordinatedTarget) Availability(context.Context) target.Availability {
|
|
t.availabilityCalls++
|
|
return target.Availability{Available: true, Evidence: "detected"}
|
|
}
|
|
func (t *coordinatedTarget) Inspect(_ context.Context, active *profile.Profile) target.Inspection {
|
|
t.inspectCalls++
|
|
if t.state == target.Enabled && (active == nil || (t.appliedName != "" && active.Name != t.appliedName)) {
|
|
return target.Inspection{State: target.Drifted, Evidence: "managed configuration does not match the Active Profile"}
|
|
}
|
|
return target.Inspection{State: t.state, Evidence: "fresh actual configuration"}
|
|
}
|
|
func (t *coordinatedTarget) Confirmation(bool) string { return "" }
|
|
func (t *coordinatedTarget) SetDesired(_ context.Context, enabled bool, active *profile.Profile) target.OperationResult {
|
|
if !enabled {
|
|
t.state = target.Disabled
|
|
return target.OperationResult{Inspection: target.Inspection{State: t.state, Evidence: "disabled"}}
|
|
}
|
|
t.applied = append(t.applied, active.Name)
|
|
if active.Name == t.failName {
|
|
t.state = target.Drifted
|
|
return target.OperationResult{Inspection: target.Inspection{State: target.Enabled, Evidence: "stale operation state"}, Err: errors.New("failed using http://alice:secret@proxy.example")}
|
|
}
|
|
t.state = target.Enabled
|
|
t.appliedName = active.Name
|
|
return target.OperationResult{Inspection: target.Inspection{State: t.state, Evidence: "matches " + active.Name}}
|
|
}
|
|
|
|
func TestSecondApplicationUsesPersistedActiveProfileChangedByFirstApplication(t *testing.T) {
|
|
root := t.TempDir()
|
|
storePath := filepath.Join(root, "profiles.json")
|
|
bootstrap, err := app.New(profile.NewFileStore(storePath))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range []string{"old", "new"} {
|
|
result := bootstrap.Dispatch(app.Intent{Kind: app.CreateProfile, Profile: profile.Profile{Name: name, Mode: profile.SingleMode, SingleURL: "http://" + name + ".example:8080"}})
|
|
if result.Err != nil {
|
|
t.Fatalf("create %s Profile: %v", name, result.Err)
|
|
}
|
|
}
|
|
if result := bootstrap.Dispatch(app.Intent{Kind: app.ActivateProfile, Name: "old"}); result.Err != nil {
|
|
t.Fatalf("activate old Profile: %v", result.Err)
|
|
}
|
|
|
|
firstTarget := &coordinatedTarget{id: "shell", state: target.Disabled}
|
|
secondTarget := &coordinatedTarget{id: "shell", state: target.Disabled}
|
|
first, err := app.NewWithTargetsAndLock(profile.NewFileStore(storePath), []target.Target{firstTarget}, operationlock.New(filepath.Join(root, "operation.lock")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := app.NewWithTargetsAndLock(profile.NewFileStore(storePath), []target.Target{secondTarget}, operationlock.New(filepath.Join(root, "operation.lock")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := second.Snapshot().ActiveProfile; got != "old" {
|
|
t.Fatalf("second Application starting Active Profile = %q, want old", got)
|
|
}
|
|
refreshApplicationTargets(t, second)
|
|
|
|
if result := first.Dispatch(app.Intent{Kind: app.ActivateProfile, Name: "new"}); result.Err != nil {
|
|
t.Fatalf("first Application activates new Profile: %v", result.Err)
|
|
}
|
|
result := second.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: "shell", Enabled: true})
|
|
if result.Err != nil {
|
|
t.Fatalf("second Application enables Target: %v", result.Err)
|
|
}
|
|
if secondTarget.appliedName != "new" {
|
|
t.Fatalf("second Application applied stale Profile %q, want freshly persisted Active Profile new", secondTarget.appliedName)
|
|
}
|
|
if result.Snapshot.ActiveProfile != "new" {
|
|
t.Fatalf("second Application snapshot Active Profile = %q, want new", result.Snapshot.ActiveProfile)
|
|
}
|
|
}
|
|
|
|
func TestActiveProfileSwitchCoordinatesEnabledTargetsWithoutRollback(t *testing.T) {
|
|
first := &coordinatedTarget{id: "first", state: target.Disabled}
|
|
second := &coordinatedTarget{id: "second", state: target.Disabled, failName: "new"}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{first, second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range []string{"old", "new"} {
|
|
result := application.Dispatch(app.Intent{Kind: app.CreateProfile, Profile: profile.Profile{Name: name, SingleURL: "http://alice:secret@proxy.example:8080"}})
|
|
if result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
}
|
|
if result := application.Dispatch(app.Intent{Kind: app.ActivateProfile, Name: "old"}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
refreshApplicationTargets(t, application)
|
|
for _, id := range []string{"first", "second"} {
|
|
if result := application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: id, Enabled: true}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
}
|
|
|
|
inspectCallsBeforeSwitch := second.inspectCalls
|
|
var final app.OperationUpdate
|
|
var failedCompletion app.OperationUpdate
|
|
stream := application.StartOperation(app.Intent{Kind: app.ActivateProfile, Name: "new"})
|
|
for update := range stream.Updates {
|
|
final = update
|
|
progress := update.Progress()
|
|
if len(progress) == 1 && progress[0].TargetID == "second" && progress[0].Phase == app.TargetProgressFailed {
|
|
failedCompletion = update
|
|
}
|
|
}
|
|
if final.Snapshot.ActiveProfile != "new" {
|
|
t.Fatalf("Active Profile = %q", final.Snapshot.ActiveProfile)
|
|
}
|
|
if final.Err() == nil || strings.Contains(final.Err().Error(), "secret") || !strings.Contains(final.Err().Error(), "second") {
|
|
t.Fatalf("redacted partial failure = %v", final.Err())
|
|
}
|
|
if got := final.Snapshot.Targets; got[0].State != target.Enabled || got[1].State != target.Drifted {
|
|
t.Fatalf("truthful partial states = %#v", got)
|
|
}
|
|
progress := final.Progress()
|
|
if len(progress) != 2 || progress[0].TargetID != "first" || progress[0].State != target.Enabled || progress[1].TargetID != "second" || progress[1].State != target.Drifted || progress[1].Evidence != "fresh actual configuration" {
|
|
t.Fatalf("per-Target progress = %#v", progress)
|
|
}
|
|
failedProgress := failedCompletion.Progress()
|
|
if len(failedProgress) != 1 || failedProgress[0].State != target.Drifted || failedProgress[0].Evidence != "fresh actual configuration" || failedProgress[0].Err == nil {
|
|
t.Fatalf("failed completion event was not truthful: %#v", failedCompletion)
|
|
}
|
|
if failedCompletion.Snapshot.Targets[1].State != target.Drifted || failedCompletion.Snapshot.Targets[1].Evidence != "fresh actual configuration" {
|
|
t.Fatalf("failed completion snapshot was not truthful: %#v", failedCompletion.Snapshot.Targets[1])
|
|
}
|
|
if second.inspectCalls < inspectCallsBeforeSwitch+2 {
|
|
t.Fatalf("failed SetDesired was not explicitly re-inspected: Inspect calls before=%d after=%d", inspectCallsBeforeSwitch, second.inspectCalls)
|
|
}
|
|
if !strings.Contains(final.Err().Error(), "fresh actual configuration") || !strings.Contains(final.Err().Error(), "Drifted") {
|
|
t.Fatalf("failure summary omitted fresh inspection: %v", final.Err())
|
|
}
|
|
if len(first.applied) != 2 || first.applied[1] != "new" {
|
|
t.Fatalf("successful Target was not updated: %#v", first.applied)
|
|
}
|
|
}
|
|
|
|
func TestApplicationCoordinatesMultipleRealAndFakeHermeticAdaptersThroughPartialFailure(t *testing.T) {
|
|
directory := t.TempDir()
|
|
shellAdapter := shell.New(shell.Configuration{
|
|
Shell: "/bin/bash", EnvironmentFile: filepath.Join(directory, "shell.env"), RecoveryFile: filepath.Join(directory, "shell.json"), TrustedRoots: []string{directory},
|
|
}, nil, shell.MapEnvironment{})
|
|
dockerAdapter := dockerclient.New(dockerclient.Configuration{
|
|
ConfigFile: filepath.Join(directory, "docker", "config.json"), RecoveryFile: filepath.Join(directory, "docker", "recovery.json"), TrustedRoots: []string{directory},
|
|
}, nil)
|
|
failure := &coordinatedTarget{id: "failing-fake", state: target.Disabled, failName: "new"}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(directory, "profiles.json")), []target.Target{shellAdapter, dockerAdapter, failure})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, endpoint := range map[string]string{"old": "http://old-proxy.example:8080", "new": "http://new-proxy.example:9090"} {
|
|
if result := application.Dispatch(app.Intent{Kind: app.CreateProfile, Profile: profile.Profile{Name: name, SingleURL: endpoint}}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
}
|
|
if result := application.Dispatch(app.Intent{Kind: app.ActivateProfile, Name: "old"}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
refreshApplicationTargets(t, application)
|
|
for _, id := range []string{shellAdapter.ID(), dockerAdapter.ID(), failure.ID()} {
|
|
if result := application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: id, Enabled: true}); result.Err != nil {
|
|
t.Fatalf("enable %s: %v", id, result.Err)
|
|
}
|
|
}
|
|
|
|
result := application.Dispatch(app.Intent{Kind: app.ActivateProfile, Name: "new"})
|
|
if result.Err == nil || result.Snapshot.ActiveProfile != "new" {
|
|
t.Fatalf("partial switch = %#v", result)
|
|
}
|
|
states := map[string]target.State{}
|
|
for _, item := range result.Snapshot.Targets {
|
|
states[item.ID] = item.State
|
|
}
|
|
if states["shell"] != target.Enabled || states["docker-containers"] != target.Enabled || states["failing-fake"] != target.Drifted {
|
|
t.Fatalf("real/fake Target states = %#v", states)
|
|
}
|
|
generated, err := os.ReadFile(filepath.Join(directory, "shell.env"))
|
|
if err != nil || !strings.Contains(string(generated), "new-proxy.example:9090") {
|
|
t.Fatalf("real Shell effect = %q, %v", generated, err)
|
|
}
|
|
dockerConfig, err := os.ReadFile(filepath.Join(directory, "docker", "config.json"))
|
|
if err != nil || !strings.Contains(string(dockerConfig), "new-proxy.example:9090") {
|
|
t.Fatalf("real Docker client effect = %q, %v", dockerConfig, err)
|
|
}
|
|
}
|
|
|
|
func TestActiveProfileProgressReusesAvailabilityCache(t *testing.T) {
|
|
adapter := &coordinatedTarget{id: "shell", state: target.Disabled}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range []string{"old", "new"} {
|
|
if result := application.Dispatch(app.Intent{Kind: app.CreateProfile, Profile: profile.Profile{Name: name, SingleURL: "http://proxy.example:8080"}}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
}
|
|
if result := application.Dispatch(app.Intent{Kind: app.ActivateProfile, Name: "old"}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
refreshApplicationTargets(t, application)
|
|
if result := application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: "shell", Enabled: true}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
|
|
callsBeforeSwitch := adapter.availabilityCalls
|
|
var updates int
|
|
for range application.StartOperation(app.Intent{Kind: app.ActivateProfile, Name: "new"}).Updates {
|
|
updates++
|
|
}
|
|
if updates < 3 {
|
|
t.Fatalf("progress updates = %d", updates)
|
|
}
|
|
if adapter.availabilityCalls != callsBeforeSwitch {
|
|
t.Fatalf("availability probes during progress = %d, want cached count %d", adapter.availabilityCalls, callsBeforeSwitch)
|
|
}
|
|
application.Dispatch(app.Intent{Kind: app.RefreshTargets})
|
|
if adapter.availabilityCalls != callsBeforeSwitch+1 {
|
|
t.Fatalf("explicit refresh probe count = %d, want %d", adapter.availabilityCalls, callsBeforeSwitch+1)
|
|
}
|
|
}
|
|
|
|
func TestActiveProfileSwitchStreamsApplyingAndCompletionForEachTarget(t *testing.T) {
|
|
adapter := &coordinatedTarget{id: "shell", state: target.Disabled}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range []string{"old", "new"} {
|
|
if result := application.Dispatch(app.Intent{Kind: app.CreateProfile, Profile: profile.Profile{Name: name, SingleURL: "http://proxy.example:8080"}}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
}
|
|
if result := application.Dispatch(app.Intent{Kind: app.ActivateProfile, Name: "old"}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
refreshApplicationTargets(t, application)
|
|
if result := application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: "shell", Enabled: true}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
|
|
updates := application.StartOperation(app.Intent{Kind: app.ActivateProfile, Name: "new"}).Updates
|
|
applying := <-updates
|
|
completed := <-updates
|
|
final := <-updates
|
|
applyingProgress := applying.Progress()
|
|
if len(applyingProgress) != 1 || applyingProgress[0].Phase != app.TargetProgressRunning || applyingProgress[0].TargetID != "shell" {
|
|
t.Fatalf("applying update = %#v", applying)
|
|
}
|
|
completedProgress := completed.Progress()
|
|
if len(completedProgress) != 1 || completedProgress[0].Phase != app.TargetProgressSucceeded || completedProgress[0].State != target.Enabled {
|
|
t.Fatalf("completion update = %#v", completed)
|
|
}
|
|
if !final.IsSucceeded() || final.Snapshot.ActiveProfile != "new" {
|
|
t.Fatalf("final update = %#v", final)
|
|
}
|
|
}
|
|
|
|
func TestDeletingFinalProfileLeavesManagedTargetsDriftedAndDisableStillRestoresExternal(t *testing.T) {
|
|
directory := t.TempDir()
|
|
configuration := dockerclient.Configuration{
|
|
ConfigFile: filepath.Join(directory, "docker", "config.json"), RecoveryFile: filepath.Join(directory, "state", "docker.json"), TrustedRoots: []string{directory},
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(configuration.ConfigFile), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const externalConfig = `{"proxies":{"default":{"httpProxy":"http://external.example:3128"}},"auths":{"registry.example":{}}}`
|
|
if err := os.WriteFile(configuration.ConfigFile, []byte(externalConfig), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
adapter := dockerclient.New(configuration, nil)
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(directory, "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
created := application.Dispatch(app.Intent{Kind: app.CreateProfile, Profile: profile.Profile{Name: "only", SingleURL: "http://proxy.example:8080"}})
|
|
if created.Err != nil {
|
|
t.Fatal(created.Err)
|
|
}
|
|
if activated := application.Dispatch(app.Intent{Kind: app.ActivateProfile, Name: "only"}); activated.Err != nil {
|
|
t.Fatal(activated.Err)
|
|
}
|
|
refreshApplicationTargets(t, application)
|
|
if enabled := application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: adapter.ID(), Enabled: true}); enabled.Err != nil {
|
|
t.Fatal(enabled.Err)
|
|
}
|
|
|
|
unconfirmed := application.Dispatch(app.Intent{Kind: app.DeleteProfile, Name: "only"})
|
|
if unconfirmed.Err == nil || unconfirmed.Snapshot.ActiveProfile != "only" {
|
|
t.Fatalf("unconfirmed final delete = %#v", unconfirmed)
|
|
}
|
|
deleted := application.Dispatch(app.Intent{Kind: app.DeleteProfile, Name: "only", Confirm: true})
|
|
if deleted.Err != nil || deleted.Snapshot.ActiveProfile != "" || deleted.Snapshot.Targets[0].State != target.Drifted {
|
|
t.Fatalf("confirmed final delete = %#v", deleted)
|
|
}
|
|
enable := application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: adapter.ID(), Enabled: true})
|
|
if enable.Err == nil || !strings.Contains(enable.Err.Error(), "Profile management") {
|
|
t.Fatalf("re-enable without Active Profile = %v", enable.Err)
|
|
}
|
|
disabled := application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: adapter.ID(), Enabled: false})
|
|
if disabled.Err != nil || disabled.Snapshot.Targets[0].State != target.External {
|
|
t.Fatalf("disable after final delete = %#v", disabled)
|
|
}
|
|
restored, err := os.ReadFile(configuration.ConfigFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(restored), "external.example:3128") || !strings.Contains(string(restored), "registry.example") {
|
|
t.Fatalf("External Docker client configuration was not restored: %s", restored)
|
|
}
|
|
}
|