Merge branch 'jjsfunhouse-backup'

This commit is contained in:
hey
2026-10-07 13:20:11 -04:00
25 changed files with 1799 additions and 378 deletions
+86 -26
View File
@@ -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,15 +115,23 @@ 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:
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{
switch r.Method {
case http.MethodPost:
h.handleUndeployApp(w, r, r.PathValue("appId"))
default:
@@ -103,7 +139,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 +148,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 +156,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 +171,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:
@@ -144,8 +180,32 @@ 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{
switch r.Method {
case http.MethodPost:
h.handleInsertAppSecret(w, r, r.PathValue("appId"))
case http.MethodGet:
@@ -155,7 +215,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 +223,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 +231,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 +242,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 +265,4 @@ func (h *abraHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// Delegate to internal mux
h.mux.ServeHTTP(w, r)
}
}
+20
View File
@@ -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
}
+284
View File
@@ -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
}
+35 -30
View File
@@ -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)})
}
}
+64
View File
@@ -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)
}
+55
View File
@@ -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)
}
+35
View File
@@ -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)
}
+72
View File
@@ -0,0 +1,72 @@
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(3)
if _, err := client.New(server, timeout); err != nil {
//errCh <- err
} 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)
}
-9
View File
@@ -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()
}
+40 -78
View File
@@ -1,27 +1,22 @@
package cli
import (
"log"
"errors"
"fmt"
"net/http"
"os/exec"
"encoding/json"
"strings"
"fmt"
"log"
"net/http"
"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 +27,34 @@ 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]
_, ok := reachable[app.Server]
return ok
})
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 {
@@ -107,8 +67,8 @@ import (
appStats = &sapp
}
}
if !reachable[app.Server] {
_, ok := reachable[app.Server]
if !ok {
appStats.Chaos = "unknown"
appStats.ChaosVersion = "unknown"
appStats.Version = "unknown"
@@ -121,12 +81,14 @@ import (
resp[app.Server] = stats
continue
}
if app.Recipe.Name == "backup-bot-two" {
stats.BackupEnabled = true
}
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 +107,7 @@ import (
} else {
stats.UnversionedCount++
}
stats.AppCount++
appStats.Status = status
appStats.Chaos = chaos
@@ -154,12 +117,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 +152,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 +163,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
@@ -210,16 +173,15 @@ import (
}
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")
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 +191,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)
+173
View File
@@ -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)
})
}
}
+50 -43
View File
@@ -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 {
+28 -23
View File
@@ -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
}
}
+123
View File
@@ -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)
}
})
}
}
+13 -13
View File
@@ -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)
}
}
+119
View File
@@ -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)
})
}
}
+4 -5
View File
@@ -1,15 +1,14 @@
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))))
InternalServerError(w, fmt.Errorf("Error: %s", string(output)))
return
}
w.WriteHeader(http.StatusOK)
+30 -26
View File
@@ -1,53 +1,57 @@
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()
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
}
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 {
log.Printf("???")
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 +68,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)
+215
View File
@@ -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)
}
})
}
}
-14
View File
@@ -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)
}
+16 -12
View File
@@ -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)
}
}
+140 -89
View File
@@ -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)
})
}
}
}
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)
})
}
}
+99
View File
@@ -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"
}
}
+33 -10
View File
@@ -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,16 @@ 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/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
github.com/morikuni/aec v1.0.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
@@ -97,6 +111,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 +121,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
+65
View File
@@ -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,29 @@ 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/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=
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 +446,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 +502,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 +511,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 +545,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 +558,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 +587,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 +600,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 +620,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 +634,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 +662,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 +695,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 +715,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 +739,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 +752,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 +765,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=