Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demos/acme-commerce-lenders/docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Status: Ready to rehearse

Check first: `make kit APP=acme-commerce-lenders ARGS=ready`. It lists each app emulator, who serves it and whether it is healthy, and says what to do about one that is not. Each row below is a failure seen while building or rehearsing the demo.
Check first: `make kit APP=acme-commerce-lenders ARGS=doctor`. It checks the web UI build, the state file, the port, each app emulator and the seeded world, and says what to do about each failure. `ARGS=ready` checks only the app emulators. Each row below is a failure seen while building or rehearsing the demo.

| Symptom | Cause | Fix |
|---|---|---|
Expand Down
5 changes: 4 additions & 1 deletion demos/acme-commerce-lenders/internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Commands:
ready check that the Stripe, Plaid and Xero app emulators are up
seed seed the world into the app emulators
reset wipe the app emulators and seed the world again
doctor check this machine and the app emulators, and say how to fix each problem

Each vendor is served by LocalStack unless <VENDOR>_EMULATOR_URL says you run
its app emulator yourself, for example PLAID_EMULATOR_URL=http://127.0.0.1:5101.
Expand All @@ -49,7 +50,7 @@ func Run(ctx context.Context, args []string, getenv func(string) string, stdout,
addr := fs.String("addr", "127.0.0.1:8080", "address the UI listens on (serve only)")

switch cmd {
case "serve", "ready", "seed", "reset":
case "serve", "ready", "seed", "reset", "doctor":
case "help":
fmt.Fprint(stdout, usage)
return 0
Expand All @@ -71,6 +72,8 @@ func Run(ctx context.Context, args []string, getenv func(string) string, stdout,
err = app.seed()
case "reset":
err = app.reset()
case "doctor":
err = app.doctor(*addr)
}
if err != nil {
fmt.Fprintln(stderr, "error:", err)
Expand Down
55 changes: 55 additions & 0 deletions demos/acme-commerce-lenders/internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"io"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
Expand All @@ -12,6 +13,7 @@ import (
"testing"

"github.com/localstack-samples/localstack-app-emulators/demos/acme-commerce-lenders/internal/fakevendors"
"github.com/localstack-samples/localstack-app-emulators/demos/acme-commerce-lenders/internal/ui"
"github.com/localstack-samples/localstack-app-emulators/demos/acme-commerce-lenders/world"
)

Expand Down Expand Up @@ -172,3 +174,56 @@ func TestTheServerServesTheAPIAndTheUI(t *testing.T) {
t.Fatalf("the connect calls should be recorded as events: %d %s", code, body)
}
}

func TestDoctorPassesWhenEverythingIsUpAndSeeded(t *testing.T) {
var resets atomic.Int32
env := envFor(fakevendors.New(t, &resets).URL)
state := filepath.Join(t.TempDir(), "state.json")
getenv := func(k string) string { return env[k] }
var o, e bytes.Buffer
if code := Run(context.Background(), []string{"seed", "--state", state}, getenv, &o, &e); code != 0 {
t.Fatalf("seed: %s", e.String())
}
o.Reset()
code := Run(context.Background(), []string{"doctor", "--state", state, "--addr", "127.0.0.1:0"}, getenv, &o, &e)
if strings.Contains(o.String(), "stripe") == false || !strings.Contains(o.String(), "the world is seeded and every app emulator still holds it") {
t.Fatalf("code=%d\n%s", code, o.String())
}
// Only the UI check can fail, and only when this checkout has not run make web.
wantFailed := 1
if ui.Built() {
wantFailed = 0
}
if strings.Count(o.String(), "FAILED") != wantFailed {
t.Fatalf("want %d failed check(s):\n%s", wantFailed, o.String())
}
}

func TestDoctorNamesEachProblemAndItsFix(t *testing.T) {
var resets atomic.Int32
up := fakevendors.New(t, &resets)
down := httptest.NewServer(http.NotFoundHandler())
downURL := down.URL
down.Close()
env := envFor(up.URL)
env["XERO_EMULATOR_URL"] = downURL

busy, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer busy.Close()

code, out, errOut := run(t, env, "doctor", "--addr", busy.Addr().String())
if code != 1 {
t.Fatalf("code=%d", code)
}
for _, want := range []string{"serve --addr", "XERO_EMULATOR_URL", "the world is seeded", "run: reset", "XERO_EMULATOR_VERSION"} {
if !strings.Contains(out, want) {
t.Errorf("doctor should say %q:\n%s", want, out)
}
}
if !strings.Contains(errOut, "problem(s) found") {
t.Errorf("err=%q", errOut)
}
}
102 changes: 102 additions & 0 deletions demos/acme-commerce-lenders/internal/cli/doctor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package cli

import (
"fmt"
"net"
"os"
"path/filepath"

"github.com/localstack-samples/localstack-app-emulators/demos/acme-commerce-lenders/internal/api"
"github.com/localstack-samples/localstack-app-emulators/demos/acme-commerce-lenders/internal/ui"
"github.com/localstack-samples/localstack-app-emulators/shared/vendorcfg"
)

// doctor runs every check, prints one line each with the fix for a failure, and
// fails if any check failed. It changes nothing.
func (a *app) doctor(addr string) error {
bad := 0
report := func(ok bool, what, fix string) {
if ok {
fmt.Fprintf(a.out, " ok %s\n", what)
return
}
bad++
fmt.Fprintf(a.out, " FAILED %s\n %s\n", what, fix)
}

fmt.Fprintln(a.out, "This machine:")
report(ui.Built(), "the web UI is built into this binary",
"this binary has only the placeholder page; run: make web APP=acme-commerce-lenders, then rebuild")
report(a.stateWritable(), "the state file can be written at "+a.statePath,
"make that folder writable, or choose another with --state")
if err := portFree(addr); err != nil {
report(false, "the demo can listen on "+addr,
"something is already on that port, often an earlier run; stop it, or use serve --addr 127.0.0.1:8081")
} else {
report(true, "the demo can listen on "+addr, "")
}

fmt.Fprintln(a.out, "App emulators:")
store, err := a.store()
if err != nil {
return err
}
st, err := api.WorldAdmin{Targets: a.targets, HC: a.httpClient(), Store: store}.Status(a.ctx)
if err != nil {
report(false, "the state file can be read", err.Error()+"; delete it and run: reset")
return fmt.Errorf("%d problem(s) found", bad)
}
for _, e := range st.Emulators {
report(e.OK(), fmt.Sprintf("%s (%s) %s at %s", e.Vendor, e.Source, e.State, e.URL), e.Hint)
}
for _, t := range a.targets.All() {
if t.Source == "localstack" {
continue
}
if a.getenv(vendorcfg.EmulatorVersionEnvName(t.Vendor)) == "" {
fmt.Fprintf(a.out, " note %s has no version recorded; set %s to show it in the inspector\n", t.Vendor, vendorcfg.EmulatorVersionEnvName(t.Vendor))
}
}

fmt.Fprintln(a.out, "The world:")
switch {
case !st.Seeded:
report(false, "the world is seeded", "run: reset (or press Reset in the UI)")
case len(st.Stale) > 0:
for _, s := range st.Stale {
report(false, fmt.Sprintf("%s still holds the seeded world", s.Vendor), s.Reason+"; run: reset")
}
default:
report(true, "the world is seeded and every app emulator still holds it", "")
}

if bad > 0 {
return fmt.Errorf("%d problem(s) found", bad)
}
fmt.Fprintln(a.out, "all checks passed")
return nil
}

// stateWritable reports whether the state file's folder exists or can be made,
// and can be written to. It leaves nothing behind.
func (a *app) stateWritable() bool {
dir := filepath.Dir(a.statePath)
if err := os.MkdirAll(dir, 0o750); err != nil {
return false
}
f, err := os.CreateTemp(dir, ".doctor-*")
if err != nil {
return false
}
name := f.Name()
_ = f.Close()
return os.Remove(name) == nil
}

func portFree(addr string) error {
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return l.Close()
}
7 changes: 7 additions & 0 deletions demos/acme-commerce-lenders/internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ func Handler() http.Handler {
return handler(sub)
}

// Built reports whether the binary carries the built web UI rather than the
// placeholder page.
func Built() bool {
sub, err := fs.Sub(assets, "assets")
return err == nil && exists(sub, "index.html")
}

// handler serves fsys as a single-page app:
// - a file that exists is served as it is;
// - a path with no file extension that does not exist, such as /inspector, is
Expand Down
Loading