From 4ecf49d0ceb3a34e21bac6b95f55df31892f2307 Mon Sep 17 00:00:00 2001 From: hey Date: Thu, 3 Sep 2026 21:57:42 -0400 Subject: [PATCH 1/2] tests --- cli/api.go | 80 ++++++++---- cli/deploy.go | 65 +++++----- cli/external/app_package.go | 64 +++++++++ cli/external/docker_client.go | 55 ++++++++ cli/external/recipe_package.go | 35 +++++ cli/external/server_package.go | 73 +++++++++++ cli/interface/command.go | 9 -- cli/list.go | 103 +++++---------- cli/list_test.go | 173 +++++++++++++++++++++++++ cli/new.go | 51 ++++---- cli/new_test.go | 123 ++++++++++++++++++ cli/ping.go | 26 ++-- cli/ping_test.go | 119 +++++++++++++++++ cli/remove.go | 6 +- cli/secret.go | 52 ++++---- cli/secret_test.go | 215 +++++++++++++++++++++++++++++++ cli/undeploy.go | 14 -- cli/upgrade.go | 28 ++-- cli/upgrade_test.go | 229 ++++++++++++++++++++------------- data/mock_apps.go | 99 ++++++++++++++ go.mod | 42 ++++-- go.sum | 63 +++++++++ 22 files changed, 1398 insertions(+), 326 deletions(-) create mode 100644 cli/external/app_package.go create mode 100644 cli/external/docker_client.go create mode 100644 cli/external/recipe_package.go create mode 100644 cli/external/server_package.go delete mode 100644 cli/interface/command.go create mode 100644 cli/list_test.go create mode 100644 cli/new_test.go create mode 100644 cli/ping_test.go create mode 100644 cli/secret_test.go delete mode 100644 cli/undeploy.go create mode 100644 data/mock_apps.go diff --git a/cli/api.go b/cli/api.go index 717ca74..9b418f9 100644 --- a/cli/api.go +++ b/cli/api.go @@ -1,11 +1,15 @@ package cli + import ( + "coop-cloud-backend/cli/external" "fmt" "log" - "os/exec" "net/http" + "os/exec" + "github.com/stretchr/testify/mock" ) + // // Upgrader is used to upgrade HTTP connections to WebSocket connections. // var upgrader = websocket.Upgrader{ // CheckOrigin: func(r *http.Request) bool { @@ -37,6 +41,20 @@ import ( // } // } +type MockCommandRunner struct { + mock.Mock +} + +func (m *MockCommandRunner) Run(name string, args ...string) ([]byte, error) { + callArgs := []interface{}{name} + for _, arg := range args { + callArgs = append(callArgs, arg) + } + + resp := m.Called(callArgs...) + return resp.Get(0).([]byte), resp.Error(1) +} + // abstracted out interface into this runner so i can write tests type CommandRunner interface { Run(name string, args ...string) ([]byte, error) @@ -45,16 +63,26 @@ type CommandRunner interface { type ExecRunner struct{} func (ExecRunner) Run(name string, args ...string) ([]byte, error) { - return exec.Command(name, args...).Output() + return exec.Command(name, args...).CombinedOutput() } -type abraHandler struct{ - mux *http.ServeMux - cmd ExecRunner +type abraHandler struct { + mux *http.ServeMux + cmd CommandRunner + secret SecretUtilsWrapper + ap external.AppPackage + sp external.ServerPackage + rp external.RecipePackage } + func newAbraHandler() *abraHandler { h := &abraHandler{ - mux: http.NewServeMux(), + mux: http.NewServeMux(), + cmd: ExecRunner{}, + secret: &SecretUtils{}, + ap: &external.AbraAppPackage{}, + sp: &external.AbraServerPackage{}, + rp: &external.AbraRecipePackage{}, } h.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { log.Printf("API Path not found") @@ -62,14 +90,14 @@ func newAbraHandler() *abraHandler { }) h.mux.HandleFunc("/api/apps", func(w http.ResponseWriter, r *http.Request) { switch r.Method { - case http.MethodGet: + case http.MethodGet: h.handleListApps(w, r) default: http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) } }) h.mux.HandleFunc("/api/apps/{appId}/deploy", func(w http.ResponseWriter, r *http.Request) { - switch r.Method{ + switch r.Method { case http.MethodPost: h.handleDeployApp(w, r, r.PathValue("appId"), r.URL.Query().Get("chaos") == "true") case http.MethodGet: @@ -79,7 +107,7 @@ func newAbraHandler() *abraHandler { } }) h.mux.HandleFunc("/api/apps/{appId}/upgrade", func(w http.ResponseWriter, r *http.Request) { - switch r.Method{ + switch r.Method { case http.MethodPost: h.handleUpgradeApp(w, r, r.PathValue("appId"), r.URL.Query().Get("force") == "true") default: @@ -87,7 +115,7 @@ func newAbraHandler() *abraHandler { } }) h.mux.HandleFunc("/api/apps/{appId}/rollback", func(w http.ResponseWriter, r *http.Request) { - switch r.Method{ + switch r.Method { case http.MethodPost: h.handleRollbackApp(w, r, r.PathValue("appId"), r.URL.Query().Get("force") == "true") default: @@ -95,7 +123,7 @@ func newAbraHandler() *abraHandler { } }) h.mux.HandleFunc("/api/apps/{appId}/stop", func(w http.ResponseWriter, r *http.Request) { - switch r.Method{ + switch r.Method { case http.MethodPost: h.handleUndeployApp(w, r, r.PathValue("appId")) default: @@ -103,7 +131,7 @@ func newAbraHandler() *abraHandler { } }) h.mux.HandleFunc("/api/apps/{appId}/remove", func(w http.ResponseWriter, r *http.Request) { - switch r.Method{ + switch r.Method { case http.MethodPost: h.handleRemoveApp(w, r, r.PathValue("appId")) default: @@ -112,7 +140,7 @@ func newAbraHandler() *abraHandler { }) h.mux.HandleFunc("/api/servers", func(w http.ResponseWriter, r *http.Request) { switch r.Method { - case http.MethodGet: + case http.MethodGet: h.handleListServers(w, r) default: http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) @@ -120,14 +148,14 @@ func newAbraHandler() *abraHandler { }) h.mux.HandleFunc("/api/catalogue", func(w http.ResponseWriter, r *http.Request) { switch r.Method { - case http.MethodGet: + case http.MethodGet: h.handleListCatalogue(w, r) default: http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) } }) h.mux.HandleFunc("/api/apps/{appId}/new", func(w http.ResponseWriter, r *http.Request) { - switch r.Method{ + switch r.Method { case http.MethodPost: h.handleNewApp(w, r, r.PathValue("appId")) default: @@ -135,7 +163,7 @@ func newAbraHandler() *abraHandler { } }) h.mux.HandleFunc("/api/apps/{appId}/config", func(w http.ResponseWriter, r *http.Request) { - switch r.Method{ + switch r.Method { case http.MethodPost: h.handlePostConfig(w, r, r.PathValue("appId")) case http.MethodGet: @@ -145,7 +173,7 @@ func newAbraHandler() *abraHandler { } }) h.mux.HandleFunc("/api/apps/{appId}/secret", func(w http.ResponseWriter, r *http.Request) { - switch r.Method{ + switch r.Method { case http.MethodPost: h.handleInsertAppSecret(w, r, r.PathValue("appId")) case http.MethodGet: @@ -155,7 +183,7 @@ func newAbraHandler() *abraHandler { } }) h.mux.HandleFunc("/api/apps/{appId}/services", func(w http.ResponseWriter, r *http.Request) { - switch r.Method{ + switch r.Method { case http.MethodGet: h.handleGetAppServices(w, r, r.PathValue("appId")) default: @@ -163,7 +191,7 @@ func newAbraHandler() *abraHandler { } }) h.mux.HandleFunc("/api/apps/{appId}/{serviceId}/logs", func(w http.ResponseWriter, r *http.Request) { - switch r.Method{ + switch r.Method { case http.MethodGet: h.handleGetLogs(w, r, r.PathValue("appId"), r.PathValue("serviceId")) default: @@ -171,7 +199,7 @@ func newAbraHandler() *abraHandler { } }) h.mux.HandleFunc("/api/apps/{appId}/versions", func(w http.ResponseWriter, r *http.Request) { - switch r.Method{ + switch r.Method { case http.MethodGet: h.handleListVersions(w, r, r.PathValue("appId")) default: @@ -182,11 +210,11 @@ func newAbraHandler() *abraHandler { } func StartAPI() { /*http.HandleFunc("/ws", wsHandler) - fmt.Println("WebSocket server started on :3001") - err := http.ListenAndServe(":3001", nil) - if err != nil { - fmt.Println("Error starting server:", err) - }*/ + fmt.Println("WebSocket server started on :3001") + err := http.ListenAndServe(":3001", nil) + if err != nil { + fmt.Println("Error starting server:", err) + }*/ h := newAbraHandler() fmt.Println("Server started on port 3000") @@ -205,4 +233,4 @@ func (h *abraHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Delegate to internal mux h.mux.ServeHTTP(w, r) -} \ No newline at end of file +} diff --git a/cli/deploy.go b/cli/deploy.go index cd3ee79..5084ecd 100644 --- a/cli/deploy.go +++ b/cli/deploy.go @@ -1,36 +1,34 @@ package cli + import ( - "log" - "os/exec" - "coop-cloud-backend/cli/status" - "net/http" - "encoding/json" - "fmt" - "errors" "context" + "coop-cloud-backend/cli/status" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" "coopcloud.tech/abra/pkg/client" //"coopcloud.tech/abra/pkg/upstream/stack" - "coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/ui" + "coopcloud.tech/abra/pkg/upstream/convert" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" dclient "github.com/docker/docker/client" - "github.com/docker/docker/api/types" - appPkg "coopcloud.tech/abra/pkg/app" - //"github.com/gorilla/websocket" ) + func (h *abraHandler) handleDeployApp(w http.ResponseWriter, r *http.Request, appName string, chaos bool) { log.Printf("Handling App Deploy!") args := []string{"app", "deploy", appName, "-n", "-c"} if chaos { args = append(args, "-C") } - cmd := exec.Command("abra", args...) - output, err := cmd.Output() + output, err := h.cmd.Run("abra", args...) if err != nil { InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) return @@ -38,9 +36,17 @@ func (h *abraHandler) handleDeployApp(w http.ResponseWriter, r *http.Request, ap log.Printf("Finishing app deploy!") w.WriteHeader(http.StatusOK) } +func (h *abraHandler) handleUndeployApp(w http.ResponseWriter, r *http.Request, appName string) { + output, err := h.cmd.Run("abra", "app", "undeploy", appName, "-n") + if err != nil { + InternalServerError(w, fmt.Errorf("Error: %s", string(output))) + return + } + w.WriteHeader(http.StatusOK) +} func (h *abraHandler) getDeployLogs(w http.ResponseWriter, r *http.Request, appName string) { log.Printf("Get deploy logs!") - app, err := appPkg.Get(appName) + app, err := appPkg.Get(appName) if err != nil { InternalServerError(w, err) return @@ -78,34 +84,34 @@ func (h *abraHandler) getDeployLogs(w http.ResponseWriter, r *http.Request, appN log.Printf("existingServices contains: %s", service.Spec.Name) serviceIDs = append(serviceIDs, ui.ServiceMeta{ Name: service.Spec.Name, - ID: service.ID, + ID: service.ID, }) } ctx := r.Context() flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "Streaming unsupported", http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") + if !ok { + http.Error(w, "Streaming unsupported", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") log.Printf("Waiting on service...") - - stream := make (chan status.StreamEvent, 50) + + stream := make(chan status.StreamEvent, 50) go status.WaitOnServices(ctx, cl, serviceIDs, f, stream) for { - select{ - case <- ctx.Done(): + select { + case <-ctx.Done(): log.Printf("deploy cancelled or done") return - case msg, ok := <- stream: + case msg, ok := <-stream: if !ok { return } test, err := json.MarshalIndent(msg, "", " ") - fmt.Printf(string(test), err); + fmt.Printf(string(test), err) b, err := json.Marshal(msg) if err != nil { OnError(err) @@ -119,7 +125,6 @@ func (h *abraHandler) getDeployLogs(w http.ResponseWriter, r *http.Request, appN } - func getStackFilter(namespace string) filters.Args { filter := filters.NewArgs() filter.Add("label", convert.LabelNamespace+"="+namespace) @@ -131,4 +136,4 @@ func getStackServiceFilter(namespace string) filters.Args { func GetStackServices(ctx context.Context, dockerclient dclient.APIClient, namespace string) ([]swarm.Service, error) { return dockerclient.ServiceList(ctx, types.ServiceListOptions{Filters: getStackServiceFilter(namespace)}) -} \ No newline at end of file +} diff --git a/cli/external/app_package.go b/cli/external/app_package.go new file mode 100644 index 0000000..229caee --- /dev/null +++ b/cli/external/app_package.go @@ -0,0 +1,64 @@ +package external + +import ( + appPkg "coopcloud.tech/abra/pkg/app" + "github.com/stretchr/testify/mock" +) + +type AppPackage interface { + GetApps(recipeFilter string, servers ...string) ([]appPkg.App, error) + GetAppStatuses(apps []appPkg.App, MachineReadable bool) (map[string]map[string]string, error) + StackName(app appPkg.App) string +} + +type AbraAppPackage struct{} + +func (a *AbraAppPackage) GetApps(recipeFilter string, servers ...string) ([]appPkg.App, error) { + appFiles, err := appPkg.LoadAppFiles(servers...) + if err != nil { + return nil, err + } + apps, err := appPkg.GetApps(appFiles, recipeFilter) + if err != nil { + return nil, err + } + return apps, nil +} + +func (a *AbraAppPackage) GetAppStatuses(apps []appPkg.App, MachineReadable bool) (map[string]map[string]string, error) { + return appPkg.GetAppStatuses(apps, MachineReadable) +} + +func (a *AbraAppPackage) StackName(app appPkg.App) string { + return app.StackName() +} + +type MockAppPackage struct { + mock.Mock +} + +func (a *MockAppPackage) GetApps(recipeFilter string, servers ...string) ([]appPkg.App, error) { + callArgs := []interface{}{recipeFilter} + for _, arg := range servers { + callArgs = append(callArgs, arg) + } + args := a.Called(callArgs...) + return args.Get(0).([]appPkg.App), args.Error(1) + +} + +func (a *MockAppPackage) GetAppStatuses(apps []appPkg.App, MachineReadable bool) (map[string]map[string]string, error) { + callArgs := []interface{}{} + for _, arg := range apps { + callArgs = append(callArgs, arg) + } + callArgs = append(callArgs, MachineReadable) + args := a.Called(callArgs...) + return args.Get(0).(map[string]map[string]string), args.Error(1) +} + +func (a *MockAppPackage) StackName(app appPkg.App) string { + callArgs := []interface{}{app} + args := a.Called(callArgs...) + return args.Get(0).(string) +} diff --git a/cli/external/docker_client.go b/cli/external/docker_client.go new file mode 100644 index 0000000..572d12c --- /dev/null +++ b/cli/external/docker_client.go @@ -0,0 +1,55 @@ +package external + +import ( + "context" + + "coopcloud.tech/abra/pkg/upstream/convert" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + dockerClient "github.com/docker/docker/client" + "github.com/stretchr/testify/mock" +) + +type DockerClient interface { + GetStackServices(ctx context.Context, dockerclient dockerClient.APIClient, namespace string) ([]swarm.Service, error) +} +type AbraDockerClient struct{} + +func (a *AbraDockerClient) GetStackServices(ctx context.Context, dockerclient dockerClient.APIClient, namespace string) ([]swarm.Service, error) { + return dockerclient.ServiceList(ctx, types.ServiceListOptions{Filters: a.getStackServiceFilter(namespace)}) +} + +func (a *AbraDockerClient) getStackFilter(namespace string) filters.Args { + filter := filters.NewArgs() + filter.Add("label", convert.LabelNamespace+"="+namespace) + return filter +} + +func (a *AbraDockerClient) getStackServiceFilter(namespace string) filters.Args { + return a.getStackFilter(namespace) +} + +type MockDockerClient struct { + mock.Mock +} + +func (a *MockDockerClient) GetStackServices(ctx context.Context, dockerclient dockerClient.APIClient, namespace string) ([]swarm.Service, error) { + callArgs := []interface{}{ctx} + callArgs = append(callArgs, dockerclient) + callArgs = append(callArgs, namespace) + args := a.Called(callArgs...) + return args.Get(0).([]swarm.Service), args.Error(1) +} + +func (a *MockDockerClient) getStackFilter(namespace string) filters.Args { + callArgs := []interface{}{namespace} + args := a.Called(callArgs...) + return args.Get(0).(filters.Args) +} + +func (a *MockDockerClient) getStackServiceFilter(namespace string) filters.Args { + callArgs := []interface{}{namespace} + args := a.Called(callArgs...) + return args.Get(0).(filters.Args) +} diff --git a/cli/external/recipe_package.go b/cli/external/recipe_package.go new file mode 100644 index 0000000..3a26064 --- /dev/null +++ b/cli/external/recipe_package.go @@ -0,0 +1,35 @@ +package external + +import ( + appPkg "coopcloud.tech/abra/pkg/app" + "github.com/stretchr/testify/mock" +) + +type RecipePackage interface { + EnsureExists(appPkg.App) error + Tags(appPkg.App) ([]string, error) +} + +type AbraRecipePackage struct{} + +func (a *AbraRecipePackage) EnsureExists(app appPkg.App) error { + return app.Recipe.EnsureExists() +} +func (a *AbraRecipePackage) Tags(app appPkg.App) ([]string, error) { + return app.Recipe.Tags() +} + +type MockRecipePackage struct { + mock.Mock +} + +func (a *MockRecipePackage) EnsureExists(app appPkg.App) error { + callArgs := []interface{}{app} + args := a.Called(callArgs...) + return args.Error(0) +} +func (a *MockRecipePackage) Tags(app appPkg.App) ([]string, error) { + callArgs := []interface{}{app} + args := a.Called(callArgs...) + return args.Get(0).([]string), args.Error(1) +} diff --git a/cli/external/server_package.go b/cli/external/server_package.go new file mode 100644 index 0000000..c53d87a --- /dev/null +++ b/cli/external/server_package.go @@ -0,0 +1,73 @@ +package external + +import ( + "log" + "sync" + "time" + + appPkg "coopcloud.tech/abra/pkg/app" + "coopcloud.tech/abra/pkg/client" + "coopcloud.tech/abra/pkg/config" + "github.com/stretchr/testify/mock" +) + +type ServerPackage interface { + GetReachableServers(apps []appPkg.App) (map[string]bool, error) +} + +type AbraServerPackage struct{} + +func (a *AbraServerPackage) GetReachableServers(apps []appPkg.App) (map[string]bool, error) { + // servers that are unreachable.. + reachable := make(map[string]bool) + servers, err := config.ReadServerNames() + if err != nil { + return nil, err + } + start := time.Now() + + reachableCh := make(chan string) + //errCh := make(chan error) + var wg sync.WaitGroup + wg.Add(len(servers)) + + for _, server := range servers { + // check if its unreachable.. + // this connection is retried, so takes 10 seconds per connection + // TODO: figure out a way to make this faster + // this is retried every server and connection so it really should be quicker + go func(server string) { + defer wg.Done() + timeout := client.WithTimeout(5) + if _, err := client.New(server, timeout); err != nil { + //errCh <- err + reachable[server] = false + } else { + reachableCh <- server + } + }(server) + } + go func() { + for server := range reachableCh { + reachable[server] = true + } + }() + wg.Wait() + + elapsed := time.Since(start) + log.Printf("Checking server availability took %s", elapsed) + return reachable, nil +} + +type MockServerPackage struct { + mock.Mock +} + +func (a *MockServerPackage) GetReachableServers(apps []appPkg.App) (map[string]bool, error) { + callArgs := []interface{}{} + for _, arg := range apps { + callArgs = append(callArgs, arg) + } + args := a.Called(callArgs...) + return args.Get(0).(map[string]bool), args.Error(1) +} diff --git a/cli/interface/command.go b/cli/interface/command.go deleted file mode 100644 index eb2872e..0000000 --- a/cli/interface/command.go +++ /dev/null @@ -1,9 +0,0 @@ -package interface - -import "os/exec" - -type RealCommandRunner struct{} - -func (r *RealCommandRunner) Run(name string, args ...string) ([]byte, error) { - return exec.Command(name, args...).Output() -} \ No newline at end of file diff --git a/cli/list.go b/cli/list.go index 9cdace0..d2439f5 100644 --- a/cli/list.go +++ b/cli/list.go @@ -1,27 +1,23 @@ package cli + import ( - "log" + "encoding/json" "errors" "fmt" + "log" "net/http" - "os/exec" - "encoding/json" - "strings" "sort" - "github.com/samber/lo" - "sync" + "strings" "time" - appPkg "coopcloud.tech/abra/pkg/app" - client "coopcloud.tech/abra/pkg/client" - "coopcloud.tech/abra/pkg/config" - "coopcloud.tech/tagcmp" + "github.com/samber/lo" + appPkg "coopcloud.tech/abra/pkg/app" + "coopcloud.tech/tagcmp" ) - func (h *abraHandler) handleListApps(w http.ResponseWriter, r *http.Request) { - cmd := exec.Command("abra", "app", "ls", "-m") - output, err := cmd.CombinedOutput() +func (h *abraHandler) handleListApps(w http.ResponseWriter, r *http.Request) { + output, err := h.cmd.Run("abra", "app", "ls", "-m") if err != nil { InternalServerError(w, err) return @@ -32,69 +28,33 @@ import ( recipeFilter := "" if err := json.Unmarshal(output, &resp); err != nil { - InternalServerError(w, errors.New(fmt.Sprintf("invalid JSON from CLI"))) + InternalServerError(w, fmt.Errorf("invalid JSON from CLI")) return } - - appFiles, err := appPkg.LoadAppFiles(listAppServer) + // Get abra apps from remote + apps, err := h.ap.GetApps(recipeFilter, listAppServer) if err != nil { InternalServerError(w, err) } - apps, err := appPkg.GetApps(appFiles, recipeFilter) + start := time.Now() + + // Returns map of all servers that are reachable/don't time out + reachable, err := h.sp.GetReachableServers(apps) if err != nil { InternalServerError(w, err) } - statuses := make(map[string]map[string]string) - // servers that are unreachable.. - reachable := make(map[string]bool) - servers, err := config.ReadServerNames() - if err != nil { - InternalServerError(w, err) - } - start := time.Now() - reachableCh := make(chan string) - //errCh := make(chan error) - var wg sync.WaitGroup - wg.Add(len(servers)) - - for _, server := range servers { - // check if its unreachable.. - // this connection is retried, so takes 10 seconds per connection - // TODO: figure out a way to make this faster - // this is retried every server and connection so it really should be quicker - go func(server string) { - defer wg.Done() - log.Printf("working on: %s", server) - timeout := client.WithTimeout(5) - if _, err := client.New(server, timeout); err != nil { - //errCh <- err - - OnError(errors.New(fmt.Sprintf("connection failed or timed out at %s: %s", server, err))) - reachable[server] = false - } else { - reachableCh <- server - } - }(server) - } - go func() { - for server := range reachableCh { - log.Printf("%s is reachable", server) - reachable[server] = true - } - }() - wg.Wait() - - elapsed := time.Since(start) - log.Printf("Checking server availability took %s", elapsed) + elapsed := time.Since(start) + log.Printf("Checking server availability took %s", elapsed) // TODO: I think this will check every server for every app, need to remove some first. // rapps is all reachable apps rapps := lo.Filter(apps, func(app appPkg.App, index int) bool { return reachable[app.Server] }) - statuses, err = appPkg.GetAppStatuses(rapps, true) + statuses := make(map[string]map[string]string) + statuses, err = h.ap.GetAppStatuses(rapps, true) if err != nil { - log.Fatal(err) + InternalServerError(w, err) } // TODO: if a server is unreachable, skip its apps, maybe flag something? for _, app := range apps { @@ -121,12 +81,11 @@ import ( resp[app.Server] = stats continue } - status := "unknown" version := "unknown" chaos := "unknown" chaosVersion := "unknown" - if statusMeta, ok := statuses[app.StackName()]; ok { + if statusMeta, ok := statuses[h.ap.StackName(app)]; ok { if currentVersion, exists := statusMeta["version"]; exists { if currentVersion != "" { version = currentVersion @@ -145,6 +104,7 @@ import ( } else { stats.UnversionedCount++ } + stats.AppCount++ appStats.Status = status appStats.Chaos = chaos @@ -154,12 +114,12 @@ import ( var newUpdates []string if version != "unknown" && chaos == "false" { - if err := app.Recipe.EnsureExists(); err != nil { - InternalServerError(w, errors.New(fmt.Sprintf("unable to clone %s: %s", app.Name, err))) + if err := h.rp.EnsureExists(app); err != nil { + InternalServerError(w, fmt.Errorf("unable to clone %s: %s", app.Name, err)) return } - updates, err := app.Recipe.Tags() + updates, err := h.rp.Tags(app) if err != nil { localApp = false } else { @@ -189,7 +149,7 @@ import ( if len(newUpdates) == 0 { if version == "unknown" { appStats.Upgrade = "unknown" - } else if localApp { + } else if localApp { appStats.Upgrade = "latest" stats.LatestCount++ } else { @@ -200,7 +160,7 @@ import ( appStats.Upgrade = strings.Join(newUpdates, "\n") stats.UpgradeCount++ } - + for i, sapp := range stats.Apps { if sapp.AppName == app.Name { stats.Apps[i] = *appStats @@ -217,9 +177,8 @@ import ( w.WriteHeader(http.StatusOK) w.Write(jsonBytes) } -func (h *abraHandler) handleListServers (w http.ResponseWriter, r *http.Request) { - cmd := exec.Command("abra", "server", "ls", "-m") - abraServers, err := cmd.CombinedOutput() +func (h *abraHandler) handleListServers(w http.ResponseWriter, r *http.Request) { + abraServers, err := h.cmd.Run("abra", "server", "ls", "-m") if err != nil { InternalServerError(w, err) return @@ -229,7 +188,7 @@ func (h *abraHandler) handleListServers (w http.ResponseWriter, r *http.Request) w.Write(abraServers) } -func (h *abraHandler) handleListVersions (w http.ResponseWriter, r *http.Request, appName string) { +func (h *abraHandler) handleListVersions(w http.ResponseWriter, r *http.Request, appName string) { app, err := appPkg.Get(appName) if err != nil { InternalServerError(w, err) diff --git a/cli/list_test.go b/cli/list_test.go new file mode 100644 index 0000000..7ae23d1 --- /dev/null +++ b/cli/list_test.go @@ -0,0 +1,173 @@ +package cli + +import ( + "coop-cloud-backend/cli/external" + "coop-cloud-backend/data" + "encoding/json" + "log" + "net/http" + "net/http/httptest" + "testing" + + appPkg "coopcloud.tech/abra/pkg/app" + "github.com/stretchr/testify/assert" +) + +func ToAny[T any](s []T) []any { + out := make([]any, len(s)) + for i, v := range s { + out[i] = v + } + return out +} + +// The mock call flow is +// h.cmd.Run -> h.ap.getApps -> h.ap.GetReachableServers -> h.ap.GetAppStatuses +// -(for each reachable app)> h.ap.StackName(app) +// -(reachable apps with version != "unknown" && chaos == "false")> h.rp.EnsureExists(app) and h.rp.Tags(app) +func TestListApps(t *testing.T) { + tests := []struct { + name string + // these are all of our "external" package function calls + getAppsArgs []string + getAppsOut []appPkg.App + getAppsErr error + getServsArgs []appPkg.App + getServsOut map[string]bool + getServsErr error + getStatusesArgs []interface{} + getStatusesOut map[string]map[string]string + getStatusesErr error + stackNameArgs []appPkg.App + stackNameOut []string + // these are arrays for all apps + ensureArgs []appPkg.App + ensureErrs []error + tagsArgs []appPkg.App + tagsOuts [][]string + tagsErrs []error + // these are the details for h.cmd.Run's output. Luckily the input isalways fixed + reqBody string + runOutput []byte + runErr error + // this is the final information i guess, but i need the final json thingy too + expectedBody ServerAppsResponse + expectedCode int + }{ + { + name: "success one simple app", + getAppsArgs: []string{"", ""}, + getAppsOut: []appPkg.App{data.App()}, + getServsArgs: []appPkg.App{data.App()}, + getServsOut: map[string]bool{"example.com": true}, + getStatusesArgs: []interface{}{data.App(), true}, + getStatusesOut: data.AppStatus(), + stackNameArgs: []appPkg.App{data.App()}, + stackNameOut: []string{"nextcloud"}, + ensureArgs: []appPkg.App{data.App()}, + ensureErrs: []error{nil}, + tagsArgs: []appPkg.App{data.App()}, + tagsOuts: [][]string{{"1.2.0"}}, + tagsErrs: []error{nil}, + + runOutput: []byte(` + { + "example.com": { + "apps": [{ + "server": "example.com", + "recipe": "nextcloud", + "appName": "nextcloud.example.com", + "domain": "nextcloud.example.com", + "status": "", + "chaos": "", + "chaosVersion": "", + "version": "", + "upgrade": "" + }] + } + } + `), + expectedCode: http.StatusOK, + expectedBody: ServerAppsResponse{ + "example.com": { + Apps: []AbraApp{AbraApp{ + Server: "example.com", + Recipe: "nextcloud", + AppName: "nextcloud.example.com", + Domain: "nextcloud.example.com", + Status: "running", + Chaos: "false", + ChaosVersion: "unknown", + Version: "1.0.0", + Upgrade: "1.2.0", + }}, + AppCount: 1, + VersionCount: 1, + UnversionedCount: 0, + LatestCount: 0, + UpgradeCount: 1, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCmd := new(MockCommandRunner) + mockAppPkg := new(external.MockAppPackage) + mockServerPkg := new(external.MockServerPackage) + mockRecipePkg := new(external.MockRecipePackage) + + mockCmd. + On("Run", "abra", "app", "ls", "-m"). + Return(tt.runOutput, tt.runErr) + + mockAppPkg. + On("GetApps", ToAny(tt.getAppsArgs)...). + Return(tt.getAppsOut, tt.getAppsErr) + + mockServerPkg. + On("GetReachableServers", ToAny(tt.getServsArgs)...). + Return(tt.getServsOut, tt.getServsErr) + + mockAppPkg. + On("GetAppStatuses", tt.getStatusesArgs...). + Return(tt.getStatusesOut, tt.getStatusesErr) + + for i := range tt.stackNameArgs { + mockAppPkg. + On("StackName", []interface{}{tt.stackNameArgs[i]}...). + Return(tt.stackNameOut[i]) + } + + for i, ensureArg := range tt.ensureArgs { + mockRecipePkg. + On("EnsureExists", ensureArg). + Return(tt.ensureErrs[i]) + log.Printf("tag output %v", tt.tagsOuts[i]) + mockRecipePkg. + On("Tags", tt.tagsArgs[i]). + Return(tt.tagsOuts[i], tt.tagsErrs[i]) + } + + h := &abraHandler{cmd: mockCmd, ap: mockAppPkg, sp: mockServerPkg, rp: mockRecipePkg} + req := httptest.NewRequest(http.MethodPost, "/", nil) + rec := httptest.NewRecorder() + + h.handleListApps(rec, req) + + assert.Equal(t, tt.expectedCode, rec.Code) + var got ServerAppsResponse + if assert.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got)) { + assert.Equal(t, tt.expectedBody, got) + } + + // may need to have "expectedRun" flags later + + mockCmd.AssertExpectations(t) + mockAppPkg.AssertExpectations(t) + mockServerPkg.AssertExpectations(t) + mockRecipePkg.AssertExpectations(t) + }) + } +} diff --git a/cli/new.go b/cli/new.go index 56115d5..6258af3 100644 --- a/cli/new.go +++ b/cli/new.go @@ -1,16 +1,15 @@ package cli + import ( - "log" - "os/exec" - "net/http" "encoding/json" - "errors" "fmt" + "log" + "net/http" appPkg "coopcloud.tech/abra/pkg/app" - "coopcloud.tech/abra/pkg/secret" - "coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/client" + "coopcloud.tech/abra/pkg/recipe" + "coopcloud.tech/abra/pkg/secret" ) func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appName string) { @@ -20,23 +19,24 @@ func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appNa // anonymous struct type: handy for one-time use body := struct { - Domain *string `json:"domain"` - Server *string `json:"server"` - Chaos *bool `json:"chaos"` - Secrets *bool `json:"secrets"` + Domain *string `json:"domain"` + Server *string `json:"server"` + Chaos *bool `json:"chaos"` + Secrets *bool `json:"secrets"` }{} - + err := d.Decode(&body) if err != nil { // bad JSON or unrecognized json field - http.Error(w, err.Error(), http.StatusBadRequest) + log.Printf("%v", err) + BadRequest(w, err) return } - if body.Domain == nil { + if body.Domain == nil { http.Error(w, "missing field 'domain' from JSON object", http.StatusBadRequest) return } - if body.Server == nil { + if body.Server == nil { http.Error(w, "missing field 'server' from JSON object", http.StatusBadRequest) return } @@ -46,23 +46,23 @@ func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appNa args = append(args, "-C") } log.Printf("%v", args) - cmd := exec.Command("abra", args...) - output, err := cmd.Output() + output, err := h.cmd.Run("abra", args...) if err != nil { - InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) + InternalServerError(w, fmt.Errorf("Error: %s", string(output))) return } var fs []AppSecret if body.Secrets != nil && *body.Secrets == true { - appSecrets, err := createSecrets(appName, *body.Domain, *body.Server) + log.Printf("testing: %v %v", *body.Domain, *body.Server) + appSecrets, err := h.secret.createSecrets(appName, *body.Domain, *body.Server) if err != nil { InternalServerError(w, err) return } for k, v := range appSecrets { fs = append(fs, AppSecret{ - Name: k, - Value: v, + Name: k, + Value: v, Version: "v1", Created: true, }) @@ -78,14 +78,19 @@ func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appNa w.Write(jsonBytes) } -func createSecrets(appName string, domain string, server string) (map[string]string, error) { +type SecretUtilsWrapper interface { + createSecrets(appName string, domain string, server string) (map[string]string, error) +} + +type SecretUtils struct{} + +func (s *SecretUtils) createSecrets(appName string, domain string, server string) (map[string]string, error) { cl, err := client.New(server) if err != nil { return nil, err } recipe := recipe.Get(appName) - sampleEnv, err := recipe.SampleEnv() if err != nil { return nil, err @@ -111,4 +116,4 @@ func createSecrets(appName string, domain string, server string) (map[string]str return nil, err } return secrets, nil -} \ No newline at end of file +} diff --git a/cli/new_test.go b/cli/new_test.go new file mode 100644 index 0000000..9fad2f7 --- /dev/null +++ b/cli/new_test.go @@ -0,0 +1,123 @@ +package cli + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type MockSecretUtils struct { + mock.Mock +} + +func (su *MockSecretUtils) createSecrets(appName string, domain string, server string) (map[string]string, error) { + args := su.Called(appName, domain, server) + return args.Get(0).(map[string]string), args.Error(1) + +} +func TestHandleNewApp(t *testing.T) { + tests := []struct { + name string + reqBody string + runOutput []byte + runErr error + runSecrets bool + expectedSecretArgs []string + secretOutput map[string]string + secretErr error + expectedArgs []string + expectedRun bool + expectedCode int + }{ + { + name: "success no chaos no secrets", + reqBody: `{ + "domain": "app.xyz", + "server": "server.xyz", + "chaos": false, + "secrets": false + }`, + runOutput: []byte(nil), + expectedArgs: []string{"app", "new", "myapp", "-n", "--domain=app.xyz", "--server=server.xyz"}, + expectedRun: true, + expectedCode: http.StatusOK, + }, + { + name: "success with chaos no secrets", + reqBody: `{ + "domain": "app.xyz", + "server": "server.xyz", + "chaos": true, + "secrets": false + }`, + runOutput: []byte(nil), + expectedArgs: []string{"app", "new", "myapp", "-n", "--domain=app.xyz", "--server=server.xyz", "-C"}, + expectedRun: true, + expectedCode: http.StatusOK, + }, + { + name: "success no chaos with secrets", + reqBody: `{ + "domain": "app.xyz", + "server": "server.xyz", + "chaos": false, + "secrets": true + }`, + runSecrets: true, + expectedSecretArgs: []string{"myapp", "app.xyz", "server.xyz"}, + secretOutput: map[string]string{ + "name": "secret", + "value": "secret-value", + }, + runOutput: []byte(nil), + expectedArgs: []string{"app", "new", "myapp", "-n", "--domain=app.xyz", "--server=server.xyz"}, + expectedRun: true, + expectedCode: http.StatusOK, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCmd := new(MockCommandRunner) + mockSecrets := new(MockSecretUtils) + + args := []interface{}{"abra"} + for _, arg := range tt.expectedArgs { + args = append(args, arg) + } + secretArgs := []interface{}{} + for _, arg := range tt.expectedSecretArgs { + secretArgs = append(secretArgs, arg) + } + mockCmd. + On("Run", args...). + Return(tt.runOutput, tt.runErr) + + if tt.runSecrets { + mockSecrets. + On("createSecrets", secretArgs...). + Return(tt.secretOutput, tt.secretErr) + } + + h := &abraHandler{cmd: mockCmd, secret: mockSecrets} + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.reqBody)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + h.handleNewApp(rec, req, "myapp") + + assert.Equal(t, tt.expectedCode, rec.Code) + + if tt.expectedRun { + mockCmd.AssertExpectations(t) + } + if tt.runSecrets { + mockSecrets.AssertExpectations(t) + } + }) + } +} diff --git a/cli/ping.go b/cli/ping.go index 3cb1817..ae10536 100644 --- a/cli/ping.go +++ b/cli/ping.go @@ -1,14 +1,14 @@ package cli + import ( - "os/exec" - "sort" - "net/http" "encoding/json" + "net/http" + "sort" ) -func (h *abraHandler) handleGetAppServices (w http.ResponseWriter, r *http.Request, appName string) { + +func (h *abraHandler) handleGetAppServices(w http.ResponseWriter, r *http.Request, appName string) { var tmpMsg map[string]map[string]string - cmd := exec.Command("abra", "app", "ps", appName, "-m") - output, err := cmd.CombinedOutput() + output, err := h.cmd.Run("abra", "app", "ps", appName, "-m") if err != nil { InternalServerError(w, err) return @@ -18,15 +18,15 @@ func (h *abraHandler) handleGetAppServices (w http.ResponseWriter, r *http.Reque return } var services []AbraAppService - for _,v := range tmpMsg { + for _, v := range tmpMsg { services = append(services, AbraAppService{ Service: v["service"], - Chaos: v["chaos"] == "true", + Chaos: v["chaos"] == "true", Created: v["created"], - Image: v["image"], - Ports: v["ports"], - State: v["state"], - Status: v["status"], + Image: v["image"], + Ports: v["ports"], + State: v["state"], + Status: v["status"], Version: v["version"], }) } @@ -41,4 +41,4 @@ func (h *abraHandler) handleGetAppServices (w http.ResponseWriter, r *http.Reque w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(jsonBytes) -} \ No newline at end of file +} diff --git a/cli/ping_test.go b/cli/ping_test.go new file mode 100644 index 0000000..e289bf2 --- /dev/null +++ b/cli/ping_test.go @@ -0,0 +1,119 @@ +package cli + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHandlePing(t *testing.T) { + tests := []struct { + name string + runOutput []byte + runErr error + expectedArgs []string + expectedBody []AbraAppService + expectedCode int + }{ + { + name: "success", + runOutput: []byte(` + {"app":{ + "chaos":"false", + "created":"3 days ago", + "image":"image:4.1.0-22", + "ports":"1880/tcp", + "service":"app", + "state":"running", + "status":"Up 3 days (healthy)", + "version":"1.0.0+4.1.0-22"} + } + `), + expectedArgs: []string{"app", "ps", "myapp", "-m"}, + expectedBody: []AbraAppService{{ + Service: "app", + Chaos: false, + Created: "3 days ago", + Image: "image:4.1.0-22", + Ports: "1880/tcp", + State: "running", + Status: "Up 3 days (healthy)", + Version: "1.0.0+4.1.0-22", + }}, + expectedCode: http.StatusOK, + }, + { + name: "success sorted", + runOutput: []byte(` + {"app":{ + "chaos":"false", + "created":"3 days ago", + "image":"image:4.1.0-22", + "ports":"1880/tcp", + "service":"app", + "state":"running", + "status":"Up 3 days (healthy)", + "version":"1.0.0+4.1.0-22" + }, + "base": { + "chaos":"false", + "created":"3 days ago", + "image":"image:6.1.0", + "ports":"1880/tcp", + "service":"base", + "state":"running", + "status":"Up 3 days (healthy)", + "version":"1.0.0+6.1.0" + } + }`), + expectedArgs: []string{"app", "ps", "myapp", "-m"}, + expectedBody: []AbraAppService{{ + Service: "app", + Chaos: false, + Created: "3 days ago", + Image: "image:4.1.0-22", + Ports: "1880/tcp", + State: "running", + Status: "Up 3 days (healthy)", + Version: "1.0.0+4.1.0-22", + }, { + Service: "base", + Chaos: false, + Created: "3 days ago", + Image: "image:6.1.0", + Ports: "1880/tcp", + State: "running", + Status: "Up 3 days (healthy)", + Version: "1.0.0+6.1.0", + }}, + expectedCode: http.StatusOK, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCmd := new(MockCommandRunner) + + args := []interface{}{"abra"} + for _, arg := range tt.expectedArgs { + args = append(args, arg) + } + + mockCmd. + On("Run", args...). + Return(tt.runOutput, tt.runErr) + + h := &abraHandler{cmd: mockCmd} + req := httptest.NewRequest(http.MethodPost, "/", nil) + rec := httptest.NewRecorder() + + h.handleGetAppServices(rec, req, "myapp") + + assert.Equal(t, tt.expectedCode, rec.Code) + + mockCmd.AssertExpectations(t) + }) + } +} diff --git a/cli/remove.go b/cli/remove.go index 426d8d4..ea28764 100644 --- a/cli/remove.go +++ b/cli/remove.go @@ -1,13 +1,13 @@ package cli + import ( - "os/exec" "errors" "fmt" "net/http" ) + func (h *abraHandler) handleRemoveApp(w http.ResponseWriter, r *http.Request, appName string) { - cmd := exec.Command("abra", "app", "remove", appName, "-n") - output, err := cmd.Output() + output, err := h.cmd.Run("abra", "app", "remove", appName, "-n") if err != nil { InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) return diff --git a/cli/secret.go b/cli/secret.go index 14e5c7e..5e33dda 100644 --- a/cli/secret.go +++ b/cli/secret.go @@ -1,53 +1,55 @@ package cli + import ( - "log" - "os/exec" + "encoding/json" "errors" "fmt" - "encoding/json" + "log" "net/http" ) + func (h *abraHandler) handleGetAppSecrets(w http.ResponseWriter, r *http.Request, appName string) { - cmd := exec.Command("abra", "app", "secret", "list", appName, "-m") - secrets, err := cmd.CombinedOutput() + secrets, err := h.cmd.Run("abra", "app", "secret", "list", appName, "-m") if err != nil { InternalServerError(w, err) return } resp := []struct { - Created *string `json:"created on server"` - GenName *string `json:"generated name"` - Name *string `json:"name"` - Version *string `json:"version"` + Created *string `json:"created on server"` + GenName *string `json:"generated name"` + Name *string `json:"name"` + Version *string `json:"version"` }{} - + err = json.Unmarshal(secrets, &resp) if err != nil { InternalServerError(w, err) + return } - var fs []AppSecret + fs := []AppSecret{} for _, secret := range resp { - if secret.Created == nil { + if secret.Created == nil { InternalServerError(w, errors.New("Failed to decode field 'created on server' from abra response")) return } - if secret.Name == nil { + if secret.Name == nil { InternalServerError(w, errors.New("Failed to decode field 'name' from abra response")) return } - if secret.Version == nil { + if secret.Version == nil { InternalServerError(w, errors.New("Failed to decode field 'version' from abra response")) return } fs = append(fs, AppSecret{ - Name: *secret.Name, + Name: *secret.Name, Version: *secret.Version, Created: *secret.Created == "true", - Value: "", + Value: "", }) } + log.Printf("%v", fs) jsonBytes, err := json.Marshal(fs) if err != nil { InternalServerError(w, err) @@ -64,34 +66,34 @@ func (h *abraHandler) handleInsertAppSecret(w http.ResponseWriter, r *http.Reque // anonymous struct type: handy for one-time use body := struct { - Name *string `json:"name"` - Version *string `json:"version"` - Value *string `json:"value"` + Name *string `json:"name"` + Version *string `json:"version"` + Value *string `json:"value"` }{} err := d.Decode(&body) if err != nil { + log.Printf("%v", err) BadRequest(w, err) return } - if body.Name == nil { + if body.Name == nil { BadRequest(w, errors.New("missing field 'name' from JSON object")) return } - if body.Version == nil { + if body.Version == nil { BadRequest(w, errors.New("missing field 'version' from JSON object")) return } - if body.Value == nil { + if body.Value == nil { BadRequest(w, errors.New("missing field 'value' from JSON object")) return } log.Printf("%s %s %s", *body.Name, *body.Version, *body.Value) - cmd := exec.Command("abra", "app", "secret", "insert", appName, *body.Name, *body.Version, *body.Value) - output, err := cmd.CombinedOutput() + output, err := h.cmd.Run("abra", "app", "secret", "insert", appName, *body.Name, *body.Version, *body.Value) if err != nil { - InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) + InternalServerError(w, fmt.Errorf("Error: %s", string(output))) return } w.WriteHeader(http.StatusOK) diff --git a/cli/secret_test.go b/cli/secret_test.go new file mode 100644 index 0000000..229235f --- /dev/null +++ b/cli/secret_test.go @@ -0,0 +1,215 @@ +package cli + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHandleGetSecrets(t *testing.T) { + tests := []struct { + name string + runOutput []byte + runErr error + expectedArgs []string + expectedCode int + expectedBody []AppSecret + }{ + { + name: "success no secrets", + runOutput: []byte(`[]`), + expectedArgs: []string{"app", "secret", "list", "myapp", "-m"}, + expectedCode: http.StatusOK, + expectedBody: []AppSecret{}, + }, + { + name: "success with secrets", + runOutput: []byte(`[ + { + "created on server": "true", + "generated name": "myapp_secret", + "name": "secret", + "version": "v1" + }, + { + "created on server": "false", + "generated name": "myapp_unknown", + "name": "unknown", + "version": "v1" + } + ]`), + expectedArgs: []string{"app", "secret", "list", "myapp", "-m"}, + expectedCode: http.StatusOK, + expectedBody: []AppSecret{ + { + Name: "secret", + Version: "v1", + Value: "", + Created: true}, + { + Name: "unknown", + Version: "v1", + Value: "", + Created: false}, + }, + }, + { + name: "failure bad json", + runOutput: []byte(` + [ + { + "created on server": "true", + } + ]`), + expectedArgs: []string{"app", "secret", "list", "myapp", "-m"}, + expectedCode: http.StatusInternalServerError, + }, + { + name: "failure missing fields", + runOutput: []byte(` + [ + { + "created on server": "true", + "generated name": "myapp_fail" + } + ]`), + expectedArgs: []string{"app", "secret", "list", "myapp", "-m"}, + expectedCode: http.StatusInternalServerError, + }, + { + name: "failure null field", + runOutput: []byte(` + [ + { + "created on server": null, + "generated name": "myapp_nil", + "name": "nil", + "version": "v1" + } + ]`), + expectedArgs: []string{"app", "secret", "list", "myapp", "-m"}, + expectedCode: http.StatusInternalServerError, + }, + { + name: "command failure", + runOutput: []byte("error"), + runErr: errors.New("failed"), + expectedArgs: []string{"app", "secret", "list", "myapp", "-m"}, + expectedCode: http.StatusInternalServerError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCmd := new(MockCommandRunner) + + args := []interface{}{"abra"} + for _, arg := range tt.expectedArgs { + args = append(args, arg) + } + + mockCmd. + On("Run", args...). + Return(tt.runOutput, tt.runErr) + + h := &abraHandler{cmd: mockCmd} + + req := httptest.NewRequest(http.MethodPost, "/", nil) + rec := httptest.NewRecorder() + + h.handleGetAppSecrets(rec, req, "myapp") + + assert.Equal(t, tt.expectedCode, rec.Code) + if tt.expectedCode == http.StatusOK { + var res []AppSecret + err := json.Unmarshal(rec.Body.Bytes(), &res) + require.NoError(t, err) + assert.Equal(t, tt.expectedBody, res) + } + + mockCmd.AssertExpectations(t) + }) + } +} + +func TestHandleInsert(t *testing.T) { + tests := []struct { + name string + reqBody string + runOutput []byte + runErr error + expectedArgs []string + expectedRun bool + expectedCode int + }{ + { + name: "success", + reqBody: `{ + "name": "myapp_secret", + "version": "v1", + "value": "secret" + }`, + runOutput: []byte(nil), + expectedArgs: []string{"app", "secret", "insert", "myapp", "myapp_secret", "v1", "secret"}, + expectedRun: true, + expectedCode: http.StatusOK, + }, + { + name: "failure missing fields", + reqBody: `{ + "name": "myapp_secret", + }`, + runOutput: []byte(nil), + expectedArgs: []string{"app", "secret", "insert", "myapp", "myapp_secret", "v1", "secret"}, + expectedRun: false, + expectedCode: http.StatusBadRequest, + }, + { + name: "failure abra", + reqBody: `{ + "name": "myapp_secret", + "version": "v1", + "value": "secret" + }`, + runOutput: []byte(nil), + runErr: errors.New("error"), + expectedArgs: []string{"app", "secret", "insert", "myapp", "myapp_secret", "v1", "secret"}, + expectedRun: true, + expectedCode: http.StatusInternalServerError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCmd := new(MockCommandRunner) + + args := []interface{}{"abra"} + for _, arg := range tt.expectedArgs { + args = append(args, arg) + } + + mockCmd. + On("Run", args...). + Return(tt.runOutput, tt.runErr) + + h := &abraHandler{cmd: mockCmd} + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.reqBody)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + h.handleInsertAppSecret(rec, req, "myapp") + + assert.Equal(t, tt.expectedCode, rec.Code) + + if tt.expectedRun { + mockCmd.AssertExpectations(t) + } + }) + } +} diff --git a/cli/undeploy.go b/cli/undeploy.go deleted file mode 100644 index a3d9603..0000000 --- a/cli/undeploy.go +++ /dev/null @@ -1,14 +0,0 @@ -package cli -import ( - "errors" - "fmt" - "net/http" -) -func (h *abraHandler) handleUndeployApp(w http.ResponseWriter, r *http.Request, appName string) { - output, err := h.cmd.Run("abra", "app", "undeploy", appName, "-n") - if err != nil { - InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) - return - } - w.WriteHeader(http.StatusOK) -} diff --git a/cli/upgrade.go b/cli/upgrade.go index aaf3951..dcfadbb 100644 --- a/cli/upgrade.go +++ b/cli/upgrade.go @@ -1,13 +1,14 @@ package cli + import ( - "log" - "fmt" - "errors" "encoding/json" + "errors" + "fmt" + "log" "net/http" ) -func (h* abraHandler) handleUpgradeApp(w http.ResponseWriter, r *http.Request, appName string, force bool) { +func (h *abraHandler) handleUpgradeApp(w http.ResponseWriter, r *http.Request, appName string, force bool) { log.Printf("Handling App Upgrade!") args := []string{"app", "upgrade", appName, "-n", "-c"} @@ -23,14 +24,14 @@ func (h* abraHandler) handleUpgradeApp(w http.ResponseWriter, r *http.Request, a w.WriteHeader(http.StatusOK) } -func (h* abraHandler) handleRollbackApp(w http.ResponseWriter, r *http.Request, appName string, force bool) { +func (h *abraHandler) handleRollbackApp(w http.ResponseWriter, r *http.Request, appName string, force bool) { log.Printf("Handling App rollback!") d := json.NewDecoder(r.Body) - d.DisallowUnknownFields() - + d.DisallowUnknownFields() + body := struct { - Version *string `json:"version"` + Version *string `json:"version"` }{} err := d.Decode(&body) if err != nil { @@ -38,13 +39,16 @@ func (h* abraHandler) handleRollbackApp(w http.ResponseWriter, r *http.Request, BadRequest(w, err) return } - + if body.Version == nil { + BadRequest(w, fmt.Errorf("Error: no version for rollback")) + return + } args := []string{"app", "rollback", appName, "-n", "-c"} if force { args = append(args, "-f") } - if body.Version != nil{ + if body.Version != nil { if *body.Version == "Auto" { log.Printf("leaving abra to determine rollback version") } else { @@ -54,9 +58,9 @@ func (h* abraHandler) handleRollbackApp(w http.ResponseWriter, r *http.Request, output, err := h.cmd.Run("abra", args...) if err != nil { - InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) + InternalServerError(w, fmt.Errorf("Error: %s", string(output))) return } log.Printf("Finishing app rollback!") w.WriteHeader(http.StatusOK) -} \ No newline at end of file +} diff --git a/cli/upgrade_test.go b/cli/upgrade_test.go index 3c3012c..f5b2e3f 100644 --- a/cli/upgrade_test.go +++ b/cli/upgrade_test.go @@ -1,117 +1,168 @@ package cli + import ( - "testing" + "errors" "net/http" "net/http/httptest" - "errors" - "reflect" + "strings" + "testing" + + "github.com/stretchr/testify/assert" ) -type MockRunner struct { - Name string - Args []string - - OutputBytes []byte - Err error -} - -func (m *MockRunner) Run(name string, args ...string) ([]byte, error) { - m.Name = name - m.Args = args - - return m.OutputBytes, m.Err -} func TestHandleUpgradeApp(t *testing.T) { tests := []struct { - name string - appName string - force bool - mockErr error - expectedStatus int - expectedArgs []string + name string + force bool + runOutput []byte + runErr error + expectedArgs []string + expectedCode int }{ { - name: "success", - appName: "myapp", - force: false, - expectedStatus: http.StatusOK, - expectedArgs: []string{ - "app", - "upgrade", - "myapp", - "-n", - "-c", - }, + name: "success", + force: false, + runOutput: []byte("ok"), + expectedArgs: []string{"app", "upgrade", "myapp", "-n", "-c"}, + expectedCode: http.StatusOK, }, { - name: "force success", - appName: "myapp", - force: true, - expectedStatus: http.StatusOK, - expectedArgs: []string{ - "app", - "upgrade", - "myapp", - "-n", - "-c", - "-f", - }, + name: "success with force", + force: true, + runOutput: []byte("ok"), + expectedArgs: []string{"app", "upgrade", "myapp", "-n", "-c", "-f"}, + expectedCode: http.StatusOK, }, { - name: "command failure", - appName: "myapp", - force: false, - mockErr: errors.New("dead"), - expectedStatus: http.StatusInternalServerError, - expectedArgs: []string{ - "app", - "upgrade", - "myapp", - "-n", - "-c", - }, + name: "command failure", + force: false, + runOutput: []byte("error"), + runErr: errors.New("failed"), + expectedArgs: []string{"app", "upgrade", "myapp", "-n", "-c"}, + expectedCode: http.StatusInternalServerError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockCmd := &MockRunner{ - Err: tt.mockErr, + mockCmd := new(MockCommandRunner) + + args := []interface{}{"abra"} + for _, arg := range tt.expectedArgs { + args = append(args, arg) } - h := &abraHandler{ - cmd: mockCmd, - } + mockCmd. + On("Run", args...). + Return(tt.runOutput, tt.runErr) + + h := &abraHandler{cmd: mockCmd} req := httptest.NewRequest(http.MethodPost, "/", nil) - w := httptest.NewRecorder() + rec := httptest.NewRecorder() - h.handleUpgradeApp(w, req, tt.appName, tt.force) + h.handleUpgradeApp(rec, req, "myapp", tt.force) - res := w.Result() + assert.Equal(t, tt.expectedCode, rec.Code) - if res.StatusCode != tt.expectedStatus { - t.Fatalf( - "expected status %d, got %d", - tt.expectedStatus, - res.StatusCode, - ) - } - - if mockCmd.Name != "abra" { - t.Fatalf( - "expected command abra, got %s", - mockCmd.Name, - ) - } - - if !reflect.DeepEqual(mockCmd.Args, tt.expectedArgs) { - t.Fatalf( - "expected args %v, got %v", - tt.expectedArgs, - mockCmd.Args, - ) - } + mockCmd.AssertExpectations(t) }) } -} \ No newline at end of file +} + +func TestHandleRollbackApp(t *testing.T) { + tests := []struct { + name string + body string + force bool + runOutput []byte + runErr error + expectedArgs []string + expectedCode int + expectedRun bool + }{ + { + name: "success with auto version no force", + body: `{ + "version": "Auto" + }`, + force: false, + runOutput: []byte("ok"), + expectedArgs: []string{"app", "rollback", "myapp", "-n", "-c"}, + expectedCode: http.StatusOK, + expectedRun: true, + }, + { + name: "success with auto version with force", + body: `{ + "version": "Auto" + }`, + force: true, + runOutput: []byte("ok"), + expectedArgs: []string{"app", "rollback", "myapp", "-n", "-c", "-f"}, + expectedCode: http.StatusOK, + expectedRun: true, + }, + { + name: "failure no body", + body: `{ + }`, + force: false, + runOutput: []byte("ok"), + expectedArgs: []string{"app", "rollback", "myapp", "-n", "-c"}, + expectedCode: http.StatusBadRequest, + expectedRun: false, + }, + { + name: "failure unknown field", + body: `{ + "unknown": "unknown" + }`, + force: false, + expectedArgs: []string{"app", "rollback", "myapp", "-n", "-c"}, + expectedCode: http.StatusBadRequest, + expectedRun: false, + }, + { + name: "failure abra error", + body: `{ + "version": "1.0.0" + }`, + force: false, + runOutput: []byte("error"), + runErr: errors.New("failed"), + expectedArgs: []string{"app", "rollback", "myapp", "-n", "-c", "1.0.0"}, + expectedCode: http.StatusInternalServerError, + expectedRun: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCmd := new(MockCommandRunner) + + args := []interface{}{"abra"} + for _, arg := range tt.expectedArgs { + args = append(args, arg) + } + if tt.expectedRun { + mockCmd. + On("Run", args...). + Return(tt.runOutput, tt.runErr). + Once() + } + + h := &abraHandler{cmd: mockCmd} + + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + h.handleRollbackApp(rec, req, "myapp", tt.force) + + assert.Equal(t, tt.expectedCode, rec.Code) + + mockCmd.AssertExpectations(t) + }) + } +} diff --git a/data/mock_apps.go b/data/mock_apps.go new file mode 100644 index 0000000..0c7c98f --- /dev/null +++ b/data/mock_apps.go @@ -0,0 +1,99 @@ +package data + +import ( + "fmt" + + "coopcloud.tech/abra/pkg/app" + "coopcloud.tech/abra/pkg/envfile" + "coopcloud.tech/abra/pkg/recipe" +) + +type AppModifier func(*app.App) + +// func App() app.App { +// return app.App{ +// Name: "nextcloud", +// Domain: "cloud.example.com", +// Server: "server.example.com", +// Path: "/home/test/.abra/servers/hetzner/cloud.example.com.env", +// Env: envfile.AppEnv{ +// "DOMAIN": "cloud.example.com", +// "LETS_ENCRYPT_ENABLED": "1", +// }, +// Recipe: recipe.Recipe{ +// Name: "nextcloud", +// }, +// } +// } +func App(modifiers ...AppModifier) app.App { + a := app.App{ + Name: "nextcloud.example.com", + Domain: "cloud.example.com", + Server: "example.com", + Path: "/home/test/.abra/servers/hetzner/cloud.example.com.env", + Env: envfile.AppEnv{ + "DOMAIN": "cloud.example.com", + "LETS_ENCRYPT_ENABLED": "1", + }, + Recipe: recipe.Recipe{ + Name: "nextcloud", + }, + } + for _, mod := range modifiers { + mod(&a) + } + return a +} + +func RecipeName(name string) AppModifier { + return func(a *app.App) { + domainName := fmt.Sprintf("%s.%s", name, a.Server) + a.Name = name + a.Domain = domainName + a.Recipe.Name = name + a.Env["DOMAIN"] = domainName + } +} + +func Broken() AppModifier { + return func(a *app.App) { + delete(a.Env, "DOMAIN") + } +} +func WithServer(server string) AppModifier { + return func(a *app.App) { + a.Server = server + } +} + +func WithDomain(domain string) AppModifier { + return func(a *app.App) { + a.Domain = domain + a.Env["DOMAIN"] = domain + } +} + +// version, chaosVersion, chaos, status +type AppStatusModifier func(map[string]map[string]string) + +func AppStatus(modifiers ...AppStatusModifier) map[string]map[string]string { + a := map[string]map[string]string{ + "nextcloud": { + "version": "1.0.0", + "chaos": "false", + "chaosVersion": "unknown", + "status": "running", + }, + } + for _, mod := range modifiers { + mod(a) + } + return a +} + +func WithChaos(app string) AppStatusModifier { + return func(a map[string]map[string]string) { + a[app]["chaos"] = "true" + a[app]["chaosVersion"] = "version" + } +} diff --git a/go.mod b/go.mod index caec758..e992fef 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,9 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/containers/image v3.0.2+incompatible // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/cyphar/filepath-securejoin v0.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/decentral1se/passgen v1.0.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect @@ -48,6 +50,7 @@ require ( github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect + github.com/ebitengine/purego v0.10.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -59,6 +62,7 @@ require ( github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect @@ -72,9 +76,12 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leonelquinteros/gotext v1.7.2 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -83,9 +90,15 @@ require ( github.com/miekg/pkcs11 v1.1.1 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.54.2 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect - github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect @@ -97,6 +110,8 @@ require ( github.com/opencontainers/runtime-spec v1.1.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect @@ -105,36 +120,43 @@ require ( github.com/samber/lo v1.53.0 // indirect github.com/schollz/progressbar/v3 v3.18.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/shirou/gopsutil/v4 v4.26.5 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/spf13/cobra v1.10.1 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect + github.com/testcontainers/testcontainers-go v0.43.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect go.opentelemetry.io/proto/otlp v1.8.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.42.0 // indirect + golang.org/x/crypto v0.51.0 // indirect golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.35.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.13.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect diff --git a/go.sum b/go.sum index b2634d8..23ffeed 100644 --- a/go.sum +++ b/go.sum @@ -116,6 +116,8 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -167,6 +169,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= @@ -211,6 +215,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -242,6 +248,7 @@ github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519 github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -301,6 +308,8 @@ github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7Dmvb github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -320,9 +329,13 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.5.3 h1:C8fxWnhYyME3n0klPOhVM7PtYUB3eV1W3DeFmN3j53Y= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -355,14 +368,27 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= @@ -418,6 +444,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= @@ -472,6 +500,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM= +github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -479,6 +509,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= @@ -511,6 +543,10 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -520,10 +556,17 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A= +github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -542,6 +585,8 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= @@ -553,6 +598,8 @@ go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= @@ -571,6 +618,8 @@ go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6b go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= @@ -583,6 +632,8 @@ go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+ go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= @@ -609,6 +660,8 @@ golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= @@ -640,6 +693,8 @@ golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -658,10 +713,12 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -680,6 +737,8 @@ golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -691,6 +750,8 @@ golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -702,6 +763,8 @@ golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= From aaaae0d0a32701cb2df24c40e4c29ff18948b936 Mon Sep 17 00:00:00 2001 From: hey Date: Wed, 7 Oct 2026 13:05:31 -0400 Subject: [PATCH 2/2] added backups support and refactored code --- cli/api.go | 32 ++++ cli/backup-utils.go | 20 +++ cli/backup.go | 284 +++++++++++++++++++++++++++++++++ cli/external/server_package.go | 3 +- cli/list.go | 13 +- cli/models.go | 93 ++++++----- cli/remove.go | 3 +- cli/secret.go | 4 +- go.mod | 1 + go.sum | 2 + 10 files changed, 402 insertions(+), 53 deletions(-) create mode 100644 cli/backup-utils.go create mode 100644 cli/backup.go diff --git a/cli/api.go b/cli/api.go index 9b418f9..e42d7ef 100644 --- a/cli/api.go +++ b/cli/api.go @@ -122,6 +122,14 @@ func newAbraHandler() *abraHandler { http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) } }) + h.mux.HandleFunc("/api/apps/{appId}/backup/create", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + h.handleBackupCreateApp(w, r, r.PathValue("appId")) + default: + http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) + } + }) h.mux.HandleFunc("/api/apps/{appId}/stop", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: @@ -172,6 +180,30 @@ func newAbraHandler() *abraHandler { http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) } }) + h.mux.HandleFunc("/api/apps/{appId}/backup/list", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + h.handleBackupListApp(w, r, r.PathValue("appId")) + default: + http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) + } + }) + h.mux.HandleFunc("/api/apps/{appId}/backup/restore", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + h.handleBackupRestoreApp(w, r, r.PathValue("appId")) + default: + http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) + } + }) + h.mux.HandleFunc("/api/apps/{appId}/backup/download", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + h.handleBackupDownloadSnapshot(w, r, r.PathValue("appId")) + default: + http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) + } + }) h.mux.HandleFunc("/api/apps/{appId}/secret", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: diff --git a/cli/backup-utils.go b/cli/backup-utils.go new file mode 100644 index 0000000..de5ba42 --- /dev/null +++ b/cli/backup-utils.go @@ -0,0 +1,20 @@ +package cli + +import ( + "context" + "errors" + "io" + "log" + + "coopcloud.tech/abra/pkg/i18n" + dockerClient "github.com/docker/docker/client" +) + +func CopyFromContainer(cl *dockerClient.Client, containerID, srcPath string) (io.ReadCloser, int64, error) { + log.Printf("%v %v", containerID, srcPath) + content, stat, err := cl.CopyFromContainer(context.Background(), containerID, srcPath) + if err != nil { + return nil, 0, errors.New(i18n.G("copy: %s", err)) + } + return content, stat.Size, nil +} diff --git a/cli/backup.go b/cli/backup.go new file mode 100644 index 0000000..8fdc5c7 --- /dev/null +++ b/cli/backup.go @@ -0,0 +1,284 @@ +package cli + +import ( + "archive/tar" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "path/filepath" + "strconv" + "strings" + + // used for replacing abra code + "coopcloud.tech/abra/pkg/app" + "coopcloud.tech/abra/pkg/client" + "coopcloud.tech/abra/pkg/config" + containerPkg "coopcloud.tech/abra/pkg/container" + "coopcloud.tech/abra/pkg/i18n" + "coopcloud.tech/abra/pkg/service" + "github.com/docker/docker/api/types" + containertypes "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + dockerClient "github.com/docker/docker/client" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/pkg/stdcopy" +) + +func (h *abraHandler) pingBackupBot(w http.ResponseWriter, r *http.Request, server string) { + cl, err := client.New(server) + if err != nil { + InternalServerError(w, err) + } + + _, err = RetrieveBackupBotContainer(cl) + if err != nil { + InternalServerError(w, err) + } + w.WriteHeader(http.StatusOK) +} +func (h *abraHandler) handleBackupCreateApp(w http.ResponseWriter, r *http.Request, appName string) { + log.Printf("Handling App Backup!") + args := []string{} + + _, err := handleBackupCommand(appName, args, "create") + if err != nil { + InternalServerError(w, err) + } + + log.Printf("Finishing app backup!") + w.WriteHeader(http.StatusOK) +} + +func (h *abraHandler) handleBackupRestoreApp(w http.ResponseWriter, r *http.Request, appName string) { + log.Printf("Handling Backup Restore!") + + args := []string{} + d := json.NewDecoder(r.Body) + d.DisallowUnknownFields() + + body := struct { + Snapshot *string `json:"snapshot"` + }{} + err := d.Decode(&body) + if err != nil { + log.Printf("error: %v", err) + BadRequest(w, err) + return + } + if body.Snapshot != nil { + args = append(args, fmt.Sprintf("SNAPSHOT=%s", *body.Snapshot)) + } + args = append(args, fmt.Sprintf("NONINTERACTIVE=%v", true)) + _, err = handleBackupCommand(appName, args, "restore") + if err != nil { + InternalServerError(w, err) + } + + log.Printf("Finishing app restore!") + w.WriteHeader(http.StatusOK) +} + +func (h *abraHandler) handleBackupListApp(w http.ResponseWriter, r *http.Request, appName string) { + log.Printf("Handling App List Backup!") + args := []string{} + output, err := handleBackupCommand(appName, args, "snapshots") + if err != nil { + InternalServerError(w, err) + } + snapshots := strings.Split(output, "\n") + resp := []AppSnapshot{} + for _, snapshot := range snapshots { + items := strings.Split(snapshot, " ") + if len(items) < 3 { + break + } + resp = append(resp, AppSnapshot{ + CreatedAt: items[0], + Snapshot: items[1], + Version: items[2], + }) + } + jsonBytes, err := json.Marshal(resp) + if err != nil { + InternalServerError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(jsonBytes) +} +func (h *abraHandler) handleBackupDownloadSnapshot(w http.ResponseWriter, r *http.Request, appName string) { + log.Printf("Handling Snapshot Download!") + app, err := app.Get(appName) + if err != nil { + InternalServerError(w, err) + } + cl, err := client.New(app.Server) + if err != nil { + InternalServerError(w, err) + } + + targetContainer, err := RetrieveBackupBotContainer(cl) + if err != nil { + InternalServerError(w, err) + } + + execEnv := []string{ + fmt.Sprintf("SERVICE=%s", app.Domain), + "MACHINE_LOGS=true", + } + // TODO: add args + _, err = RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv) + if err != nil { + InternalServerError(w, err) + } + + remoteBackupDir := "/tmp/backup.tar.gz" + content, size, err := CopyFromContainer(cl, targetContainer.ID, remoteBackupDir) + if err != nil { + InternalServerError(w, fmt.Errorf("error copying from container: %v", err)) + } + defer content.Close() + + w.Header().Set("Content-type", "application/x-tar") + tr := tar.NewReader(content) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + InternalServerError(w, err) + } + if filepath.Base(header.Name) == "backup.tar.gz" { + w.Header().Set( + "Content-Disposition", + `attachment; filename="backup.tar.gz"`, + ) + w.Header().Set("Content-Type", "application/gzip") + w.Header().Set( + "Content-Length", + strconv.FormatInt(size, 10), + ) + if _, err := io.Copy(w, tr); err != nil { + InternalServerError(w, err) + } + return + } + } +} + +func handleBackupCommand(appName string, args []string, action string) (string, error) { + app, err := app.Get(appName) + if err != nil { + return "", err + } + cl, err := client.New(app.Server) + if err != nil { + return "", err + } + + targetContainer, err := RetrieveBackupBotContainer(cl) + if err != nil { + return "", err + } + + execEnv := []string{ + fmt.Sprintf("SERVICE=%s", app.Domain), + "MACHINE_LOGS=true", + } + for _, arg := range args { + execEnv = append(execEnv, arg) + } + output, err := RunBackupCmdRemote(cl, action, targetContainer.ID, execEnv) + if err != nil { + return "", err + } + return output, nil +} + +// copied from https://git.coopcloud.tech/toolshed/abra/src/branch/main/cli/internal/backup.go +// tty is set to false +func RunBackupCmdRemote( + cl *dockerClient.Client, + backupCmd string, + containerID string, + execEnv []string) (string, error) { + execBackupListOpts := containertypes.ExecOptions{ + AttachStderr: true, + AttachStdout: true, + Cmd: []string{"/usr/bin/backup", "--", backupCmd}, + Env: execEnv, + Tty: false, + } + + stdout, err := Exec(context.Background(), cl, containerID, &execBackupListOpts) + log.Printf("stdout: %v | err: %v", stdout, err) + if err != nil { + return "", err + } + return stdout, nil +} + +func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error) { + ctx := context.Background() + chosenService, err := service.GetServiceByLabel(ctx, cl, config.BackupbotLabel, true) + if err != nil { + return types.Container{}, errors.New(i18n.G("no backupbot discovered, is it deployed?")) + } + + filters := filters.NewArgs() + filters.Add("name", chosenService.Spec.Name) + targetContainer, err := containerPkg.GetContainer( + ctx, + cl, + filters, + true, + ) + if err != nil { + return types.Container{}, err + } + + return targetContainer, nil +} + +func Exec(ctx context.Context, cli *dockerClient.Client, containerID string, execOptions *container.ExecOptions) (string, error) { + execResp, err := cli.ContainerExecCreate(ctx, containerID, *execOptions) + if err != nil { + return "", err + } + + resp, err := cli.ContainerExecAttach(ctx, execResp.ID, container.ExecStartOptions{ + Tty: false, + }) + if err != nil { + return "", err + } + defer resp.Close() + + var stdout, stderr bytes.Buffer + + _, err = stdcopy.StdCopy(&stdout, &stderr, resp.Reader) + if err != nil { + return "", err + } + + inspect, err := cli.ContainerExecInspect(ctx, execResp.ID) + if err != nil { + return "", err + } + + if inspect.ExitCode != 0 { + return stdout.String(), + fmt.Errorf("command exited with status %d and error %s", inspect.ExitCode, stderr.String()) + } + + return stdout.String(), nil +} diff --git a/cli/external/server_package.go b/cli/external/server_package.go index c53d87a..8a7cb9f 100644 --- a/cli/external/server_package.go +++ b/cli/external/server_package.go @@ -38,10 +38,9 @@ func (a *AbraServerPackage) GetReachableServers(apps []appPkg.App) (map[string]b // this is retried every server and connection so it really should be quicker go func(server string) { defer wg.Done() - timeout := client.WithTimeout(5) + timeout := client.WithTimeout(3) if _, err := client.New(server, timeout); err != nil { //errCh <- err - reachable[server] = false } else { reachableCh <- server } diff --git a/cli/list.go b/cli/list.go index d2439f5..70a53c2 100644 --- a/cli/list.go +++ b/cli/list.go @@ -2,7 +2,6 @@ package cli import ( "encoding/json" - "errors" "fmt" "log" "net/http" @@ -49,7 +48,8 @@ func (h *abraHandler) handleListApps(w http.ResponseWriter, r *http.Request) { // TODO: I think this will check every server for every app, need to remove some first. // rapps is all reachable apps rapps := lo.Filter(apps, func(app appPkg.App, index int) bool { - return reachable[app.Server] + _, ok := reachable[app.Server] + return ok }) statuses := make(map[string]map[string]string) statuses, err = h.ap.GetAppStatuses(rapps, true) @@ -67,8 +67,8 @@ func (h *abraHandler) handleListApps(w http.ResponseWriter, r *http.Request) { appStats = &sapp } } - - if !reachable[app.Server] { + _, ok := reachable[app.Server] + if !ok { appStats.Chaos = "unknown" appStats.ChaosVersion = "unknown" appStats.Version = "unknown" @@ -81,6 +81,9 @@ func (h *abraHandler) handleListApps(w http.ResponseWriter, r *http.Request) { resp[app.Server] = stats continue } + if app.Recipe.Name == "backup-bot-two" { + stats.BackupEnabled = true + } status := "unknown" version := "unknown" chaos := "unknown" @@ -170,7 +173,7 @@ func (h *abraHandler) handleListApps(w http.ResponseWriter, r *http.Request) { } jsonBytes, err := json.Marshal(resp) if err != nil { - InternalServerError(w, errors.New(fmt.Sprintf("JSON conversion failed: %s\n", err))) + InternalServerError(w, fmt.Errorf("JSON conversion failed: %s\n", err)) return } w.Header().Set("Content-Type", "application/json") diff --git a/cli/models.go b/cli/models.go index 636350c..59232df 100644 --- a/cli/models.go +++ b/cli/models.go @@ -1,70 +1,77 @@ package cli -// Represents an App, follows the format of +// Represents an App, follows the format of // https://git.coopcloud.tech/toolshed/coop-cloud-front type AbraApp struct { - Server string `json:"server"` - Recipe string `json:"recipe` - AppName string `json:"appName"` - Domain string `json:"domain"` - Status string `json:"status"` - Chaos string `json:"chaos"` - ChaosVersion string `json:"chaosVersion"` - Version string `json:"version"` - Upgrade string `json:"upgrade"` + Server string `json:"server"` + Recipe string `json:"recipe` + AppName string `json:"appName"` + Domain string `json:"domain"` + Status string `json:"status"` + Chaos string `json:"chaos"` + ChaosVersion string `json:"chaosVersion"` + Version string `json:"version"` + Upgrade string `json:"upgrade"` } type AbraServer struct { - Name string `json:"name"` - Host string `json:"host"` + Name string `json:"name"` + Host string `json:"host"` } type ServerAppsResponse map[string]ServerApps type ServerApps struct { - Apps []AbraApp `json:"apps"` - AppCount int `json:"appCount"` - VersionCount int `json:"versionCount"` - UnversionedCount int `json:"unversionedCount"` - LatestCount int `json:"latestCount"` - UpgradeCount int `json:"upgradeCount"` + Apps []AbraApp `json:"apps"` + AppCount int `json:"appCount"` + VersionCount int `json:"versionCount"` + UnversionedCount int `json:"unversionedCount"` + LatestCount int `json:"latestCount"` + UpgradeCount int `json:"upgradeCount"` + BackupEnabled bool `json:"backupEnabled"` } type DeployState struct { - AppName string `json:"appName"` + AppName string `json:"appName"` Streams []DeployStream `json:"streams"` - Logs []string `json:"logs"` - Failed bool `json:"failed"` - TimedOut bool `json:"timedOut"` - Quit bool `json:"quit"` - Count int `json:"count"` + Logs []string `json:"logs"` + Failed bool `json:"failed"` + TimedOut bool `json:"timedOut"` + Quit bool `json:"quit"` + Count int `json:"count"` } type DeployStream struct { - Name string `json:"Name"` - id string `json:"id"` - status string `json:"status"` - retries int `json:"retries"` - health string `json:"health"` - rollback bool `json:"rollback"` -} + Name string `json:"Name"` + id string `json:"id"` + status string `json:"status"` + retries int `json:"retries"` + health string `json:"health"` + rollback bool `json:"rollback"` +} type AppSecret struct { - Name string `json:"name"` - Version string `json:"version"` - Value string `json:"value"` - Created bool `json:"created"` + Name string `json:"name"` + Version string `json:"version"` + Value string `json:"value"` + Created bool `json:"created"` } type AbraAppService struct { - Service string `json:"service"` - Chaos bool `json:"chaos"` - Created string `json:"created"` - Image string `json:"image"` - Ports string `json:"ports"` - State string `json:"state"` - Status string `json:"status"` - Version string `json:"version"` + Service string `json:"service"` + Chaos bool `json:"chaos"` + Created string `json:"created"` + Image string `json:"image"` + Ports string `json:"ports"` + State string `json:"state"` + Status string `json:"status"` + Version string `json:"version"` +} + +type AppSnapshot struct { + CreatedAt string `json:"createdAt"` + Snapshot string `json:"snapshot"` + Version string `json:"version"` } // type RecipeMeta struct { diff --git a/cli/remove.go b/cli/remove.go index ea28764..44e3a7d 100644 --- a/cli/remove.go +++ b/cli/remove.go @@ -1,7 +1,6 @@ package cli import ( - "errors" "fmt" "net/http" ) @@ -9,7 +8,7 @@ import ( func (h *abraHandler) handleRemoveApp(w http.ResponseWriter, r *http.Request, appName string) { output, err := h.cmd.Run("abra", "app", "remove", appName, "-n") if err != nil { - InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) + InternalServerError(w, fmt.Errorf("Error: %s", string(output))) return } w.WriteHeader(http.StatusOK) diff --git a/cli/secret.go b/cli/secret.go index 5e33dda..56e4270 100644 --- a/cli/secret.go +++ b/cli/secret.go @@ -9,9 +9,10 @@ import ( ) func (h *abraHandler) handleGetAppSecrets(w http.ResponseWriter, r *http.Request, appName string) { - + log.Printf("handling secrets!") secrets, err := h.cmd.Run("abra", "app", "secret", "list", appName, "-m") if err != nil { + log.Printf("??") InternalServerError(w, err) return } @@ -25,6 +26,7 @@ func (h *abraHandler) handleGetAppSecrets(w http.ResponseWriter, r *http.Request err = json.Unmarshal(secrets, &resp) if err != nil { + log.Printf("???") InternalServerError(w, err) return } diff --git a/go.mod b/go.mod index e992fef..c2c024e 100644 --- a/go.mod +++ b/go.mod @@ -97,6 +97,7 @@ require ( github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/signal v0.7.1 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect diff --git a/go.sum b/go.sum index 23ffeed..cf88352 100644 --- a/go.sum +++ b/go.sum @@ -385,6 +385,8 @@ github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5 github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= +github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=