From 1ce818d9881a964ed71eb1b02bf4b3f2bc168d52 Mon Sep 17 00:00:00 2001 From: Dragan Milic Date: Tue, 17 Jan 2023 09:27:17 +0000 Subject: [PATCH 1/2] added dump handler --- go.mod | 2 +- go.sum | 4 +-- server/dump.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ server/server.go | 2 ++ 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 server/dump.go diff --git a/go.mod b/go.mod index e55778b..e7faac2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/cbroglie/mustache v1.3.1 github.com/dop251/goja v0.0.0-20221229151140-b95230a9dbad - github.com/draganm/bolted v0.9.2 + github.com/draganm/bolted v0.10.1 github.com/fsnotify/fsnotify v1.5.4 github.com/go-logr/zapr v1.2.3 github.com/gorilla/mux v1.8.0 diff --git a/go.sum b/go.sum index 7ead888..cb91b59 100644 --- a/go.sum +++ b/go.sum @@ -58,8 +58,8 @@ github.com/dop251/goja v0.0.0-20221229151140-b95230a9dbad h1:EikyYzLzjRNW8lz9VAI github.com/dop251/goja v0.0.0-20221229151140-b95230a9dbad/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= -github.com/draganm/bolted v0.9.2 h1:4T9FjEZFVF1rpcjP4J5TNOAyn8ayFDFHWo9k3TLwXfM= -github.com/draganm/bolted v0.9.2/go.mod h1:JzpeZ2BmuDuMggRz3gVL+1qX2C6b8+6pTgILbrZPztw= +github.com/draganm/bolted v0.10.1 h1:SzG/e88ElhABlfrqxzDB9e+qt6Ql+TMIwLDxPR5cC1M= +github.com/draganm/bolted v0.10.1/go.mod h1:JzpeZ2BmuDuMggRz3gVL+1qX2C6b8+6pTgILbrZPztw= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= diff --git a/server/dump.go b/server/dump.go new file mode 100644 index 0000000..e1e149e --- /dev/null +++ b/server/dump.go @@ -0,0 +1,80 @@ +package server + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "net/http" + "sort" + + "github.com/draganm/bolted" +) + +func (s Server) dumpHandler(w http.ResponseWriter, r *http.Request) { + gzw, err := gzip.NewWriterLevel(w, gzip.BestSpeed) + if err != nil { + err = fmt.Errorf("could not create gzip writer: %w", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + s.log.Error(err, "could not backup") + return + } + + tw := tar.NewWriter(gzw) + defer tw.Close() + + s.mu.Lock() + + type dumpWriter struct { + name string + applier func(func(tx bolted.SugaredReadTx) error) error + } + + dumpWriters := []dumpWriter{ + dumpWriter{ + "__server", + func(fn func(bolted.SugaredReadTx) error) error { + return bolted.SugaredRead(s.db, fn) + }, + }, + } + + for name, k := range s.kartusches { + dumpWriters = append(dumpWriters, dumpWriter{name: name, applier: k.runtime.Read}) + } + + s.mu.Unlock() + + sort.Slice(dumpWriters, func(i, j int) bool { + return dumpWriters[i].name < dumpWriters[j].name + }) + + w.Header().Set("Trailer", "X-KartuscheDumpComplete") + + for _, dw := range dumpWriters { + err := dw.applier(func(tx bolted.SugaredReadTx) error { + err := tw.WriteHeader(&tar.Header{ + Name: dw.name, + Typeflag: tar.TypeReg, + Size: tx.FileSize(), + }) + + if err != nil { + return fmt.Errorf("could not write dump header for %s: %w", dw.name, err) + } + + tx.Dump(tw) + return nil + }) + if err != nil { + s.log.Error(err, "could not write dump") + return + } + } + err = tw.Close() + if err != nil { + s.log.Error(err, "could not close dump tar writer") + } + + w.Header().Set("X-KartuscheDumpComplete", "true") + +} diff --git a/server/server.go b/server/server.go index 45219f9..4158928 100644 --- a/server/server.go +++ b/server/server.go @@ -132,6 +132,8 @@ func Open(path string, domain string, verifier verifier.AuthenticationProvider, r.Methods("DELETE").Path("/kartusches/{name}").HandlerFunc(s.rm) r.Methods("PATCH").Path("/kartusches/{name}/code").HandlerFunc(s.updateCode) + r.Methods("POST").Path("/dump").HandlerFunc(s.dumpHandler) + r.Use(s.authMiddleware) go s.runtimeManager() From bff0a7bad4425e0e874ba16953c744e030a1ac22 Mon Sep 17 00:00:00 2001 From: Dragan Milic Date: Wed, 18 Jan 2023 16:16:37 +0000 Subject: [PATCH 2/2] started adding integration tests for backup/restore --- command/auth/login/command.go | 7 +- command/clone/command.go | 11 +- command/info/dbstats/command.go | 11 +- command/info/handlers/command.go | 9 +- command/ls/command.go | 9 +- command/rm/command.go | 10 +- command/update/code/command.go | 9 +- command/upload/command.go | 9 +- common/client/api_client.go | 18 +- server/features/backup_and_restore.feature | 8 + server/integration_test.go | 165 +++++++++++++++++ server/server.go | 25 ++- server/testrig/test_server_instance.go | 196 +++++++++++++++++++++ 13 files changed, 464 insertions(+), 23 deletions(-) create mode 100644 server/features/backup_and_restore.feature create mode 100644 server/integration_test.go create mode 100644 server/testrig/test_server_instance.go diff --git a/command/auth/login/command.go b/command/auth/login/command.go index 5104eca..900f0fa 100644 --- a/command/auth/login/command.go +++ b/command/auth/login/command.go @@ -1,6 +1,7 @@ package login import ( + "context" "fmt" "net/url" "time" @@ -23,6 +24,8 @@ var Command = &cli.Command{ }, Action: func(c *cli.Context) (err error) { + ctx := context.Background() + defer func() { if err != nil { err = cli.Exit(fmt.Errorf("while logging in: %w", err), 1) @@ -36,7 +39,7 @@ var Command = &cli.Command{ serverURL := c.Args().First() loginStartResponse := &server.LoginStartResponse{} - err = client.CallAPI(serverURL, "POST", "auth/login", nil, nil, client.JSONDecoder(loginStartResponse), 200) + err = client.CallAPI(ctx, serverURL, "", "POST", "auth/login", nil, nil, client.JSONDecoder(loginStartResponse), 200) if err != nil { return fmt.Errorf("while starting login process: %w", err) } @@ -60,7 +63,9 @@ var Command = &cli.Command{ var tr server.AccessTokenResponse err = client.CallAPI( + ctx, serverURL, + "", "POST", "auth/access_token", nil, client.JSONEncoder( diff --git a/command/clone/command.go b/command/clone/command.go index da02384..19ef381 100644 --- a/command/clone/command.go +++ b/command/clone/command.go @@ -2,6 +2,7 @@ package clone import ( "archive/tar" + "context" "errors" "fmt" "io" @@ -11,6 +12,7 @@ import ( "path/filepath" "strings" + "github.com/draganm/kartusche/common/auth" "github.com/draganm/kartusche/common/client" "github.com/draganm/kartusche/config" "github.com/urfave/cli/v2" @@ -28,6 +30,8 @@ var Command = &cli.Command{ } }() + ctx := context.Background() + if c.NArg() != 1 { return errors.New("kartusche url must be provided") } @@ -108,7 +112,12 @@ var Command = &cli.Command{ } - err = client.CallAPI(baseURL.String(), "GET", u.Path, nil, nil, dumpWriter, 200) + tkn, err := auth.GetTokenForServer(baseURL.String()) + if err != nil { + return fmt.Errorf("could not get token for server: %w", err) + } + + err = client.CallAPI(ctx, baseURL.String(), tkn, "GET", u.Path, nil, nil, dumpWriter, 200) if err != nil { return err } diff --git a/command/info/dbstats/command.go b/command/info/dbstats/command.go index 5f026d0..b2a0387 100644 --- a/command/info/dbstats/command.go +++ b/command/info/dbstats/command.go @@ -1,11 +1,13 @@ package dbstats import ( + "context" "errors" "fmt" "os" "path" + "github.com/draganm/kartusche/common/auth" "github.com/draganm/kartusche/common/client" "github.com/draganm/kartusche/common/serverurl" "github.com/draganm/kartusche/runtime" @@ -24,6 +26,8 @@ var Command = &cli.Command{ } }() + ctx := context.Background() + serverBaseURL, err := serverurl.BaseServerURL("") if err != nil { return err @@ -39,8 +43,13 @@ var Command = &cli.Command{ return errors.New("name of kartusche must be provided") } + tkn, err := auth.GetTokenForServer(serverBaseURL) + if err != nil { + return fmt.Errorf("could not get token for server: %w", err) + } + dbs := &runtime.DBStats{} - err = client.CallAPI(serverBaseURL, "GET", path.Join("kartusches", name, "info", "dbstats"), nil, nil, client.JSONDecoder(&dbs), 200) + err = client.CallAPI(ctx, serverBaseURL, tkn, "GET", path.Join("kartusches", name, "info", "dbstats"), nil, nil, client.JSONDecoder(&dbs), 200) if err != nil { return fmt.Errorf("while starting login process: %w", err) } diff --git a/command/info/handlers/command.go b/command/info/handlers/command.go index 8415719..752c247 100644 --- a/command/info/handlers/command.go +++ b/command/info/handlers/command.go @@ -1,10 +1,12 @@ package handlers import ( + "context" "errors" "fmt" "path" + "github.com/draganm/kartusche/common/auth" "github.com/draganm/kartusche/common/client" "github.com/draganm/kartusche/common/serverurl" "github.com/draganm/kartusche/server" @@ -37,8 +39,13 @@ var Command = &cli.Command{ return errors.New("name of kartusche must be provided") } + tkn, err := auth.GetTokenForServer(serverBaseURL) + if err != nil { + return fmt.Errorf("could not get token for server: %w", err) + } + handlers := []server.HandlerInfo{} - err = client.CallAPI(serverBaseURL, "GET", path.Join("kartusches", name, "info", "handlers"), nil, nil, client.JSONDecoder(&handlers), 200) + err = client.CallAPI(context.Background(), serverBaseURL, tkn, "GET", path.Join("kartusches", name, "info", "handlers"), nil, nil, client.JSONDecoder(&handlers), 200) if err != nil { return fmt.Errorf("while starting login process: %w", err) } diff --git a/command/ls/command.go b/command/ls/command.go index 9177a32..7708d4e 100644 --- a/command/ls/command.go +++ b/command/ls/command.go @@ -1,8 +1,10 @@ package ls import ( + "context" "fmt" + "github.com/draganm/kartusche/common/auth" "github.com/draganm/kartusche/common/client" "github.com/draganm/kartusche/common/serverurl" "github.com/urfave/cli/v2" @@ -24,8 +26,13 @@ var Command = &cli.Command{ return err } + tkn, err := auth.GetTokenForServer(serverBaseURL) + if err != nil { + return fmt.Errorf("could not get token for server: %w", err) + } + kl := []string{} - err = client.CallAPI(serverBaseURL, "GET", "kartusches", nil, nil, client.JSONDecoder(&kl), 200) + err = client.CallAPI(context.Background(), serverBaseURL, tkn, "GET", "kartusches", nil, nil, client.JSONDecoder(&kl), 200) if err != nil { return err } diff --git a/command/rm/command.go b/command/rm/command.go index cbb47d1..a74a813 100644 --- a/command/rm/command.go +++ b/command/rm/command.go @@ -1,11 +1,13 @@ package rm import ( + "context" "errors" "fmt" "path" "strings" + "github.com/draganm/kartusche/common/auth" "github.com/draganm/kartusche/common/client" "github.com/draganm/kartusche/common/serverurl" "github.com/urfave/cli/v2" @@ -43,7 +45,13 @@ var Command = &cli.Command{ if err != nil { return err } - err = client.CallAPI(serverBaseURL, "DELETE", path.Join("kartusches", name), nil, nil, nil, 204) + + tkn, err := auth.GetTokenForServer(serverBaseURL) + if err != nil { + return fmt.Errorf("could not get token for server: %w", err) + } + + err = client.CallAPI(context.Background(), serverBaseURL, tkn, "DELETE", path.Join("kartusches", name), nil, nil, nil, 204) if err != nil { return err } diff --git a/command/update/code/command.go b/command/update/code/command.go index 7bb9cff..2d491c1 100644 --- a/command/update/code/command.go +++ b/command/update/code/command.go @@ -2,6 +2,7 @@ package code import ( "archive/tar" + "context" "fmt" "io" "os" @@ -9,6 +10,7 @@ import ( "path/filepath" "strings" + "github.com/draganm/kartusche/common/auth" "github.com/draganm/kartusche/common/client" "github.com/draganm/kartusche/common/paths" "github.com/draganm/kartusche/common/serverurl" @@ -42,6 +44,11 @@ var Command = &cli.Command{ return err } + tkn, err := auth.GetTokenForServer(serverBaseURL) + if err != nil { + return fmt.Errorf("could not get token for server: %w", err) + } + tf, err := os.CreateTemp("", "") if err != nil { return fmt.Errorf("while creating temp file: %w", err) @@ -128,7 +135,7 @@ var Command = &cli.Command{ return fmt.Errorf("while seeking tar file to beginning: %w", err) } - err = client.CallAPI(serverBaseURL, "PATCH", path.Join("kartusches", cfg.Name, "code"), nil, func() (io.Reader, error) { return tf, nil }, nil, 204) + err = client.CallAPI(context.Background(), serverBaseURL, tkn, "PATCH", path.Join("kartusches", cfg.Name, "code"), nil, func() (io.Reader, error) { return tf, nil }, nil, 204) if err != nil { return err } diff --git a/command/upload/command.go b/command/upload/command.go index fef5a65..afdaf49 100644 --- a/command/upload/command.go +++ b/command/upload/command.go @@ -1,12 +1,14 @@ package upload import ( + "context" "fmt" "io" "os" "path" "path/filepath" + "github.com/draganm/kartusche/common/auth" "github.com/draganm/kartusche/common/client" "github.com/draganm/kartusche/common/serverurl" "github.com/draganm/kartusche/config" @@ -58,6 +60,11 @@ var Command = &cli.Command{ return err } + tkn, err := auth.GetTokenForServer(serverBaseURL) + if err != nil { + return fmt.Errorf("could not get token for server: %w", err) + } + kf, err := os.Open(kartuscheFileName) if err != nil { return err @@ -65,7 +72,7 @@ var Command = &cli.Command{ defer kf.Close() - err = client.CallAPI(serverBaseURL, "PUT", path.Join("kartusches", cfg.Name), nil, func() (io.Reader, error) { return kf, nil }, nil, 204) + err = client.CallAPI(context.Background(), serverBaseURL, tkn, "PUT", path.Join("kartusches", cfg.Name), nil, func() (io.Reader, error) { return kf, nil }, nil, 204) if err != nil { return err } diff --git a/common/client/api_client.go b/common/client/api_client.go index 306a252..502161e 100644 --- a/common/client/api_client.go +++ b/common/client/api_client.go @@ -1,33 +1,23 @@ package client import ( + "context" "fmt" "io" "net/http" "net/url" "path" - "strings" - - "github.com/draganm/kartusche/common/auth" ) func CallAPI( - baseURL, method, pth string, + ctx context.Context, + baseURL, tkn, method, pth string, q url.Values, bodyEncoder func() (io.Reader, error), responseCallback func(io.Reader) error, expectedStatus int, ) error { - var tkn string - if !(strings.HasPrefix(pth, "/auth/") || strings.HasPrefix(pth, "auth/")) { - var err error - tkn, err = auth.GetTokenForServer(baseURL) - if err != nil { - return err - } - } - bu, err := url.Parse(baseURL) if err != nil { return fmt.Errorf("while parsing server base url: %w", err) @@ -47,7 +37,7 @@ func CallAPI( } } - req, err := http.NewRequest(method, bu.String(), body) + req, err := http.NewRequestWithContext(ctx, method, bu.String(), body) if err != nil { return fmt.Errorf("while creating request: %w", err) } diff --git a/server/features/backup_and_restore.feature b/server/features/backup_and_restore.feature new file mode 100644 index 0000000..f57eee8 --- /dev/null +++ b/server/features/backup_and_restore.feature @@ -0,0 +1,8 @@ +Feature: backup and restore + + Scenario: backup and restore of a single kartusche + Given a server with a single kartusche + When I backup the server + When I delete the kartusche + And I restore the server + Then the kartusche should existd \ No newline at end of file diff --git a/server/integration_test.go b/server/integration_test.go new file mode 100644 index 0000000..b8b25f8 --- /dev/null +++ b/server/integration_test.go @@ -0,0 +1,165 @@ +package server_test + +import ( + "context" + "fmt" + "os" + "runtime" + "testing" + + "github.com/cucumber/godog" + "github.com/draganm/kartusche/server/testrig" + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + "github.com/spf13/pflag" + "go.uber.org/zap" +) + +func init() { + logger, _ := zap.NewDevelopment(zap.AddStacktrace(zap.ErrorLevel)) + if true { + opts.DefaultContext = logr.NewContext(context.Background(), zapr.NewLogger(logger)) + } +} + +var opts = godog.Options{ + Output: os.Stdout, + StopOnFailure: true, + Strict: true, + Format: "progress", + Paths: []string{"features"}, + NoColors: true, + Concurrency: runtime.NumCPU() * 2, +} + +func init() { + godog.BindCommandLineFlags("godog.", &opts) +} + +func TestMain(m *testing.M) { + pflag.Parse() + opts.Paths = pflag.Args() + + status := godog.TestSuite{ + Name: "godogs", + ScenarioInitializer: InitializeScenario, + Options: &opts, + }.Run() + + os.Exit(status) +} + +type State struct { + si *testrig.TestServerInstance + serverDump []byte +} + +// func (s *State) get(path string) (int, string, error) { +// u, err := url.JoinPath(s.ti.GetURL(), path) +// if err != nil { +// return -1, "", fmt.Errorf("could not join path for GET request: %w", err) +// } + +// req, err := http.NewRequest("GET", u, nil) +// if err != nil { +// return -1, "", fmt.Errorf("could not create GET request: %w", err) +// } + +// res, err := http.DefaultClient.Do(req) +// if err != nil { +// return -1, "", fmt.Errorf("could not perform GET request: %w", err) +// } + +// defer res.Body.Close() + +// d, err := io.ReadAll(res.Body) +// if err != nil { +// return -1, "", fmt.Errorf("could not read response body: %w", err) +// } + +// return res.StatusCode, string(d), nil + +// } + +type StateKeyType string + +const stateKey = StateKeyType("") + +func InitializeScenario(ctx *godog.ScenarioContext) { + var cancel context.CancelFunc + + ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) { + ctx, cancel = context.WithCancel(ctx) + + return ctx, nil + + }) + + ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) { + cancel() + return ctx, nil + }) + + state := &State{} + + ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) { + + ts, err := testrig.NewTestServerInstance(ctx, logr.FromContextOrDiscard(ctx)) + if err != nil { + return ctx, fmt.Errorf("could not start test rig: %w", err) + } + + state.si = ts + + ctx = context.WithValue(ctx, stateKey, state) + + return ctx, nil + }) + + ctx.Step(`^a server with a single kartusche$`, aServerWithASingleKartusche) + ctx.Step(`^I backup the server$`, iBackupTheServer) + ctx.Step(`^I delete the kartusche$`, iDeleteTheKartusche) + ctx.Step(`^I restore the server$`, iRestoreTheServer) + ctx.Step(`^the kartusche should existd$`, theKartuscheShouldExistd) + +} + +func getState(ctx context.Context) *State { + return ctx.Value(stateKey).(*State) +} + +func aServerWithASingleKartusche(ctx context.Context) error { + s := getState(ctx) + err := s.si.CreateKartusche(ctx, "k1") + if err != nil { + return err + } + return nil +} + +func iBackupTheServer(ctx context.Context) error { + s := getState(ctx) + d, err := s.si.DumpServer(ctx) + if err != nil { + return err + } + s.serverDump = d + return nil +} + +func iDeleteTheKartusche(ctx context.Context) error { + s := getState(ctx) + err := s.si.DeleteKartusche(ctx, "k1") + if err != nil { + return err + } + return nil +} + +func iRestoreTheServer() error { + return godog.ErrPending +} + +func theKartuscheShouldExistd() error { + return godog.ErrPending +} diff --git a/server/server.go b/server/server.go index 4158928..bc4cded 100644 --- a/server/server.go +++ b/server/server.go @@ -12,6 +12,7 @@ import ( "github.com/draganm/kartusche/server/verifier" "github.com/go-logr/logr" "github.com/gorilla/mux" + "go.uber.org/multierr" ) type Server struct { @@ -132,7 +133,7 @@ func Open(path string, domain string, verifier verifier.AuthenticationProvider, r.Methods("DELETE").Path("/kartusches/{name}").HandlerFunc(s.rm) r.Methods("PATCH").Path("/kartusches/{name}/code").HandlerFunc(s.updateCode) - r.Methods("POST").Path("/dump").HandlerFunc(s.dumpHandler) + r.Methods("GET").Path("/dump").HandlerFunc(s.dumpHandler) r.Use(s.authMiddleware) @@ -147,3 +148,25 @@ var authPath = dbpath.ToPath("auth") var openTokenRequests = authPath.Append("open_token_requests") var tokensPath = authPath.Append("tokens") var usersPath = authPath.Append("users") + +func (s *Server) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + var err error + for name, k := range s.kartusches { + e := k.runtime.Shutdown() + if e != nil { + err = multierr.Append(err, fmt.Errorf("could not shutdown kartusche %s: %w", name, e)) + } + } + e := s.db.Close() + if e != nil { + err = multierr.Append(err, fmt.Errorf("could not close server db: %w", err)) + } + + if err != nil { + return err + } + + return nil +} diff --git a/server/testrig/test_server_instance.go b/server/testrig/test_server_instance.go new file mode 100644 index 0000000..44bafa0 --- /dev/null +++ b/server/testrig/test_server_instance.go @@ -0,0 +1,196 @@ +package testrig + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "path/filepath" + "time" + + "github.com/draganm/kartusche/common/client" + "github.com/draganm/kartusche/runtime" + "github.com/draganm/kartusche/server" + "github.com/draganm/kartusche/server/verifier" + "github.com/go-logr/logr" +) + +type TestServerInstance struct { + s *server.Server + apiURL string + kartuscheURL string + adminToken string +} + +func NewTestServerInstance(ctx context.Context, log logr.Logger) (*TestServerInstance, error) { + td, err := os.MkdirTemp("", "") + if err != nil { + return nil, fmt.Errorf("could not open test server instance dir: %w", err) + } + + s, err := server.Open(td, "127.0.0.1.nip.io", verifier.NewMockProvider(), log) + if err != nil { + return nil, fmt.Errorf("could not start test server instance: %w", err) + } + + ks := httptest.NewServer(s) + as := httptest.NewServer(s.ServerRouter) + + tctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + + loginStartResponse := &server.LoginStartResponse{} + err = client.CallAPI(tctx, as.URL, "", "POST", "auth/login", nil, nil, client.JSONDecoder(loginStartResponse), 200) + if err != nil { + return nil, fmt.Errorf("while starting login process: %w", err) + } + + ur, err := url.Parse(loginStartResponse.VerificationURI) + if err != nil { + return nil, fmt.Errorf("while parsing verification URI(%s): %w", loginStartResponse.VerificationURI, err) + } + + q := url.Values{} + q.Set("request_id", loginStartResponse.TokenRequestID) + ur.RawQuery = q.Encode() + + res, err := http.Get(ur.String()) + if err != nil { + return nil, fmt.Errorf("could not execute get for the verification URI: %w", err) + } + + // return nil, fmt.Errorf("%s %s", ur.String(), res.Status) + + res.Body.Close() + + if res.StatusCode != 200 { + return nil, fmt.Errorf("failed to finish auth flow, unexpected status %s", res.Status) + } + + var token string + + for tctx.Err() == nil { + + var tr server.AccessTokenResponse + err = client.CallAPI( + tctx, + as.URL, + "", + "POST", "auth/access_token", + nil, + client.JSONEncoder( + server.RequestTokenParameters{ + TokenRequestID: loginStartResponse.TokenRequestID, + }, + ), + client.JSONDecoder(&tr), + 200, + ) + + if err != nil { + return nil, fmt.Errorf("while fetching token: %w", err) + } + + if tr.Error == "authorization_pending" { + time.Sleep(300 * time.Millisecond) + continue + } + + if tr.Error != "" { + return nil, fmt.Errorf("unexpected token error: %s", tr.Error) + } + + token = tr.AccessToken + + break + + } + + if tctx.Err() != nil { + return nil, tctx.Err() + } + + go func() { + <-ctx.Done() + ks.Close() + as.Close() + err = s.Close() + if err != nil { + log.Error(err, "could not shut down server") + } + os.RemoveAll(td) + }() + + return &TestServerInstance{ + s: s, + kartuscheURL: ks.URL, + apiURL: as.URL, + adminToken: token, + }, nil +} + +func (s *TestServerInstance) CreateKartusche(ctx context.Context, name string) error { + + td, err := os.MkdirTemp("", "") + + if err != nil { + return fmt.Errorf("could not create temp dir: %w", err) + } + + defer func() { + os.RemoveAll(td) + }() + + fileName := filepath.Join(td, "kartusche") + + err = runtime.InitializeEmpty(fileName) + if err != nil { + return fmt.Errorf("could not initialize kartusche: %w", err) + } + + kf, err := os.Open(fileName) + if err != nil { + return fmt.Errorf("could not open kartusche: %w", err) + } + + defer kf.Close() + + err = client.CallAPI(ctx, s.apiURL, s.adminToken, "PUT", path.Join("kartusches", name), nil, func() (io.Reader, error) { return kf, nil }, nil, 204) + if err != nil { + return fmt.Errorf("could not create kartusche: %w", err) + } + + return nil +} + +func (s *TestServerInstance) DumpServer(ctx context.Context) ([]byte, error) { + + var dump []byte + + err := client.CallAPI(ctx, s.apiURL, s.adminToken, "GET", "dump", nil, nil, func(r io.Reader) error { + var err error + dump, err = io.ReadAll(r) + if err != nil { + return err + } + return nil + }, 200) + if err != nil { + return nil, fmt.Errorf("could not get dump: %w", err) + } + + return dump, nil +} + +func (s *TestServerInstance) DeleteKartusche(ctx context.Context, name string) error { + err := client.CallAPI(ctx, s.apiURL, s.adminToken, "DELETE", path.Join("kartusches", name), nil, nil, nil, 204) + if err != nil { + return fmt.Errorf("could not delete kartusche %s: %w", name, err) + } + + return nil +}