From d46144c4f2825f84a14202fd689d120f09b7cd5d Mon Sep 17 00:00:00 2001 From: hey Date: Fri, 5 Jun 2026 16:03:35 -0400 Subject: [PATCH 1/6] clean up changes, add update, start testing, change app list process --- api/api.go | 143 ------------------- api/catalogue.go | 32 ----- api/deploy.go | 299 --------------------------------------- api/error.go | 13 -- api/list.go | 153 -------------------- api/models.go | 50 ------- api/new.go | 32 ----- api/undeploy.go | 66 --------- api/utils.go | 88 ------------ api/wrappers.go | 33 ----- cli/api.go | 97 ++++++++----- cli/catalogue.go | 6 +- cli/config.go | 7 +- cli/deploy.go | 25 ++-- cli/error.go | 21 ++- cli/interface/command.go | 9 ++ cli/list.go | 74 +++++++--- cli/logs.go | 14 +- cli/new.go | 10 +- cli/ping.go | 10 +- cli/remove.go | 6 +- cli/secret.go | 12 +- cli/status/ws.txt | 41 ------ cli/undeploy.go | 10 +- cli/upgrade.go | 59 ++++++++ cli/upgrade_test.go | 117 +++++++++++++++ 26 files changed, 354 insertions(+), 1073 deletions(-) delete mode 100644 api/api.go delete mode 100644 api/catalogue.go delete mode 100644 api/deploy.go delete mode 100644 api/error.go delete mode 100644 api/list.go delete mode 100644 api/models.go delete mode 100644 api/new.go delete mode 100644 api/undeploy.go delete mode 100644 api/utils.go delete mode 100644 api/wrappers.go create mode 100644 cli/interface/command.go delete mode 100644 cli/status/ws.txt create mode 100644 cli/upgrade.go create mode 100644 cli/upgrade_test.go diff --git a/api/api.go b/api/api.go deleted file mode 100644 index a5f55b3..0000000 --- a/api/api.go +++ /dev/null @@ -1,143 +0,0 @@ -package api -import ( - "fmt" - "log" - "net/http" - "github.com/gorilla/websocket" - - "coop-cloud-backend/internal" -) -// Upgrader is used to upgrade HTTP connections to WebSocket connections. -var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, -} -func wsHandler(w http.ResponseWriter, r *http.Request) { - // Upgrade the HTTP connection to a WebSocket connection - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - fmt.Println("Error upgrading:", err) - return - } - defer conn.Close() - // Listen for incoming messages - for { - // Read message from the client - _, message, err := conn.ReadMessage() - if err != nil { - fmt.Println("Error reading message:", err) - break - } - fmt.Printf("Received: %s\\n", message) - // Echo the message back to the client - if err := conn.WriteMessage(websocket.TextMessage, message); err != nil { - fmt.Println("Error writing message:", err) - break - } - } -} - -type abraHandler struct{ - mux *http.ServeMux -} -func newAbraHandler() *abraHandler { - h := &abraHandler{ - mux: http.NewServeMux(), - } - h.mux.HandleFunc("/api/abra/apps", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return - } - switch r.Method { - case http.MethodGet: - h.handleListApps(w, r) - default: - http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) - } - }) - h.mux.HandleFunc("/api/abra/apps/{appId}/deploy", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return - } - if r.Header.Get("Chaos") == "true"{ - internal.Chaos = true - } else { - internal.Chaos = false - } - - switch r.Method{ - case http.MethodPost: - h.handleDeployApp(w, r, r.PathValue("appId")) - default: - http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) - } - }) - h.mux.HandleFunc("/api/abra/apps/{appId}/stop", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return - } - - switch r.Method{ - case http.MethodPost: - h.handleUndeployApp(w, r, r.PathValue("appId")) - default: - http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) - } - }) - h.mux.HandleFunc("/api/abra/servers", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return - } - switch r.Method { - case http.MethodGet: - h.handleListServers(w, r) - default: - http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) - } - }) - h.mux.HandleFunc("/api/abra/catalogue", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return - } - switch r.Method { - case http.MethodGet: - h.handleListCatalogue(w, r) - default: - http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) - } - }) - return h -} -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) - } - - h := newAbraHandler() - fmt.Println("Server started on port 3000") - http.ListenAndServe(":3000", h) -} - -func (h *abraHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Pre-processing: logging - log.Printf("Incoming %s request: %s\n", r.Method, r.URL.Path) - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") - - // Delegate to internal mux - h.mux.ServeHTTP(w, r) -} diff --git a/api/catalogue.go b/api/catalogue.go deleted file mode 100644 index abe97be..0000000 --- a/api/catalogue.go +++ /dev/null @@ -1,32 +0,0 @@ -package api -import ( - "context" - "fmt" - "log" - "encoding/json" - "coopcloud.tech/abra/pkg/client" - "coopcloud.tech/abra/pkg/upstream/stack" - "coopcloud.tech/abra/pkg/upstream/convert" - "coopcloud.tech/abra/pkg/recipe" - - "coop-cloud-backend/internal" -) - -func (h *abraHandler) handleListCatalogue(w http.ResponseWriter, r *http.Request) { - catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline) - if err != nil { - log.Fatal(err) - } - - recipes := catalogue.Flatten() - - jsonBytes, err := json.Marshal(recipes) - if err != nil { - log.Printf("JSON conversion failed: %s\n", err) - http.Error(w, fmt.Sprintf("JSON conversion failed: %s\n", err), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - w.Write(jsonBytes) -} - diff --git a/api/deploy.go b/api/deploy.go deleted file mode 100644 index 4bdc978..0000000 --- a/api/deploy.go +++ /dev/null @@ -1,299 +0,0 @@ -package api -import ( - "context" - "fmt" - "log" - "encoding/json" - "coopcloud.tech/abra/pkg/client" - "coopcloud.tech/abra/pkg/upstream/stack" - "coopcloud.tech/abra/pkg/upstream/convert" - - "coopcloud.tech/abra/pkg/ui" - - appPkg "coopcloud.tech/abra/pkg/app" - "coopcloud.tech/abra/pkg/deploy" - - "net/http" - "github.com/gorilla/websocket" - tea "github.com/charmbracelet/bubbletea" - - "coop-cloud-backend/internal" -) -func (h *abraHandler) handleDeployApp(w http.ResponseWriter, r *http.Request, appName string) { - log.Printf("App Id: %s", appName) - app, err := GetApp(appName) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - cl, err := client.New(app.Server) - if err != nil { - log.Printf("Error Connecting to Docker client: %s\n", err) - http.Error(w, fmt.Sprintf("Error Connecting to Docker client: %s\n", err), http.StatusInternalServerError) - return - } - deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName()) - if err != nil { - log.Printf("Error checking deploy status: %s\n", err) - http.Error(w, fmt.Sprintf("Error checking deploy status: %s\n", err), http.StatusInternalServerError) - return - } - if deployMeta.IsDeployed { - log.Printf("App already deployed\n") - http.Error(w, "App already deployed\n", http.StatusInternalServerError) - return - } - - // logic differs from CLI, we only want to take either - // 1. the chaos version - // 2. TODO: the version in the .env file - // 3. the latest version - // we never take: specific CLI verison (maybe will support this) or the deployed version - internal.Chaos = false - toDeployVersion, err := getDeployVersion(deployMeta, app) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - - if err := validateSecrets(cl, app); err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - - if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - - composeFiles, err := app.Recipe.GetComposeFiles(app.Env) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - - stackName := app.StackName() - deployOpts := stack.Deploy{ - Composefiles: composeFiles, - Namespace: stackName, - Prune: false, - ResolveImage: stack.ResolveImageAlways, - Detach: false, - } - compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - - appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) - appPkg.SetChaosLabel(compose, stackName, internal.Chaos) - if internal.Chaos { - appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion) - } - - versionLabel := toDeployVersion - if internal.Chaos { - for _, service := range compose.Services { - if service.Name == "app" { - labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName) - // NOTE(d1): keep non-chaos version labbeling when doing chaos ops - versionLabel = service.Deploy.Labels[labelKey] - } - } - } - appPkg.SetVersionLabel(compose, stackName, versionLabel) - - newRecipeWithDeployVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, toDeployVersion) - appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDeployVersion) - - envVars, err := appPkg.CheckEnv(app) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - // doesn't really get used at all right now - deployWarnMessages := []string{} - for _, envVar := range envVars { - if !envVar.Present { - deployWarnMessages = append(deployWarnMessages, - fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain), - ) - } - } - - //skipping domain checks like crazy - //commented code is to show deploy overview before deploy - - /* - deployedVersion := config.MISSING_DEFAULT - if deployMeta.IsDeployed { - deployedVersion = deployMeta.Version - if deployMeta.IsChaos { - deployedVersion = deployMeta.ChaosVersion - } - } - ShowUnchanged := false - secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, ShowUnchanged) - if err != nil { - log.Fatal(err) - InternalServerErrorHandler(w, r) - return - } - - // Gather configs - configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, ShowUnchanged) - if err != nil { - log.Fatal(err) - InternalServerErrorHandler(w, r) - return - } - - // Gather images - imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, ShowUnchanged) - if err != nil { - log.Fatal(err) - InternalServerErrorHandler(w, r) - return - }*/ - - stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - - serviceNames, err := appPkg.GetAppServiceNames(app.Name) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - - f, err := app.Filters(true, false, serviceNames...) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - - // in future allow user input? - DontWaitConverge := true - NoInput := true - if err := stack.RunDeploy( - cl, - deployOpts, - compose, - app.Name, - app.Server, - DontWaitConverge, - NoInput, - f, - ); err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - // Implement my own WaitOnServices in order to wrap ui.Model in order to emit JSON updates - // over Websocket connection - var serviceIDs []ui.ServiceMeta - namespace := convert.NewNamespace(deployOpts.Namespace) - - existingServices, err := stack.GetStackServices(context.Background(), cl, namespace.Name()) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - - for _, service := range existingServices { - serviceIDs = append(serviceIDs, ui.ServiceMeta{ - Name: service.Spec.Name, - ID: service.ID, - }) - } - - - waitOpts := WaitOpts{ - Services: serviceIDs, - AppName: app.Name, - ServerName: app.Server, - NoInput: NoInput, - Filters: f, - } - - w.WriteHeader(http.StatusOK) -} - -type StateEmitter interface { - Emit(DeployState) -} - -type WebsocketEmitter struct { - conn *websocket.Conn -} - -func (w *WebsocketEmitter) Emit(state DeployState) { - b, _ := json.Marshal(state) - w.conn.WriteMessage(websocket.TextMessage, b) -} - -type WrappedModel struct { - inner tea.Model - emitter StateEmitter -} - -func (m WrappedModel) Init() tea.Cmd { - return m.inner.Init() -} - -func (m WrappedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - innerState, cmds := m.inner.Update(msg) - - m.inner = innerState - - m.emitter.Emit(s.State()) - - return m, cmds -} - -func (m WrappedModel) View() string { - return m.inner.View() -} - -func (m *ui.Model) State() DeployState { - var streams []DeployStream - if m.Streams != nil { - for _, s := range *m.Streams { - streams = append(streams, DeployStream{ - Name: s.Name, - id: s.id, - status: s.status, - retries: s.retries, - health: s.health, - rollback: s.rollback, - }) - } - } - var logs []string - if m.Logs != nil { - logs = *m.Logs - } - return DeployState{ - AppName: m.appName, - Streams: streams, - Logs: logs, - Failed: m.Failed, - TimedOut: m.TimedOut, - Quit: m.Quit, - Count: m.count, - } -} \ No newline at end of file diff --git a/api/error.go b/api/error.go deleted file mode 100644 index abc7540..0000000 --- a/api/error.go +++ /dev/null @@ -1,13 +0,0 @@ -package api -import ( - "net/http" -) -func InternalServerErrorHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("500 Internal Server Error")) -} - -func NotFoundHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("404 Not Found")) -} \ No newline at end of file diff --git a/api/list.go b/api/list.go deleted file mode 100644 index c5638ff..0000000 --- a/api/list.go +++ /dev/null @@ -1,153 +0,0 @@ -package api -import ( - "fmt" - "log" - "encoding/json" - appPkg "coopcloud.tech/abra/pkg/app" - "coop-cloud-backend/internal" - "net/http" -) - -func (h *abraHandler) handleListApps(w http.ResponseWriter, r *http.Request) { - appNames, err := GetAppNames() - if err != nil { - log.Printf("Error getting app names: %s\n", err) - InternalServerErrorHandler(w, r) - return - } - // counts the number of apps in a server to initialize slices later - serverAppCount := make(map[string]int) - - remoteApps := make([]appPkg.App, 0, len(appNames)) - for _, appName := range appNames { - remoteApp, err := GetApp(appName) - serverAppCount[remoteApp.Server] += 1 - if err != nil { - log.Printf("Error getting app %s: %s\n", appName, err) - InternalServerErrorHandler(w, r) - return - } - remoteApps = append(remoteApps, remoteApp) - } - - appStatuses, err := GetAppStatuses(remoteApps) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - abraAppResponse := ServerAppsResponse{} - for _, app := range remoteApps { - serverApps, ok := abraAppResponse[app.Server] - if !ok { // create the slice - // ever other field initializes to 0 - serverApps = ServerApps{ - Apps: make([]AbraApp, 0, serverAppCount[app.Server]), - } - } - appInfo, ok := appStatuses[app.StackName()] - if ok { - log.Printf("app %s is deployed\n", app.Name) - serverApps.Apps = append(serverApps.Apps, appTranspose(app, appInfo)) - serverApps.AppCount += 1 - // assume these are true rn idk - serverApps.VersionCount += 1 - serverApps.LatestCount += 1 - } else { - log.Printf("app %s is undeployed\n", app.Name) - appT, err := appTransposeUndeployed(app) - if err != nil { - log.Printf("app transpose failed: %s\n", err) - http.Error(w, fmt.Sprintf("app transpose failed: %s\n", err), http.StatusInternalServerError) - return - } - serverApps.Apps = append(serverApps.Apps, appT) - serverApps.AppCount += 1 - // assume these are true rn idk - serverApps.VersionCount += 1 - serverApps.LatestCount += 1 - } - abraAppResponse[app.Server] = serverApps - } - jsonBytes, err := json.Marshal(abraAppResponse) - if err != nil { - log.Printf("JSON conversion failed: %s\n", err) - http.Error(w, fmt.Sprintf("JSON conversion failed: %s\n", err), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - w.Write(jsonBytes) -} -func appTransposeUndeployed(app appPkg.App) (AbraApp, error) { - config, err := app.Recipe.GetComposeConfig(app.Env) - if err != nil { - return AbraApp{}, err - } - version := GetLabel(config, app.StackName(), "version") - if version == "" { - version = "unknown" - } - return AbraApp{ - AppName: app.Name, - Server: app.Server, - Recipe: app.Recipe.Name, - Domain: app.Domain, - Chaos: "false", - Status: "undeployed", - ChaosVersion: "unknown", - Version: version, - Upgrade: "latest", - }, nil -} -func appTranspose(app appPkg.App, psInfo map[string]string) AbraApp { - return AbraApp{ - AppName: app.Name, - Server: app.Server, - Recipe: app.Recipe.Name, - Domain: app.Domain, - Chaos: psInfo["chaos"], - Status: psInfo["status"], - ChaosVersion: getOrDefault(psInfo, "chaosVersion", "unknown"), - Version: psInfo["version"], - Upgrade: "latest", - } -} - -func getOrDefault(m map[string]string, key, def string) string { - if v, ok := m[key]; ok { - return v - } - return def -} - -func (h *abraHandler) handleListServers (w http.ResponseWriter, r *http.Request) { - servers, err := GetServers() - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - serverNames, err := ReadServerNames() - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - abraServers := make([]AbraServer, 0, len(servers)) - for i := range len(servers) { - abraServers = append(abraServers, - AbraServer{ - Name: serverNames[i], - Host: servers[i], - }, - ) - } - jsonBytes, err := json.Marshal(abraServers) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - w.Write(jsonBytes) -} \ No newline at end of file diff --git a/api/models.go b/api/models.go deleted file mode 100644 index c0ce3ab..0000000 --- a/api/models.go +++ /dev/null @@ -1,50 +0,0 @@ -package api - -// Represents an App, follows the format of -// https://git.coopcloud.tech/BornDeleuze/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"` -} - -type AbraServer struct { - 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"` -} - -type DeployState struct { - 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"` -} - -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"` -} \ No newline at end of file diff --git a/api/new.go b/api/new.go deleted file mode 100644 index 1bebe16..0000000 --- a/api/new.go +++ /dev/null @@ -1,32 +0,0 @@ -package api -import ( - "context" - "fmt" - "log" - "encoding/json" - "coopcloud.tech/abra/pkg/client" - "coopcloud.tech/abra/pkg/upstream/stack" - "coopcloud.tech/abra/pkg/upstream/convert" - "coopcloud.tech/abra/pkg/recipe" - - "coop-cloud-backend/internal" -) - -func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appName string) { - catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline) - if err != nil { - log.Fatal(err) - } - - recipes := catalogue.Flatten() - - jsonBytes, err := json.Marshal(recipes) - if err != nil { - log.Printf("JSON conversion failed: %s\n", err) - http.Error(w, fmt.Sprintf("JSON conversion failed: %s\n", err), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - w.Write(jsonBytes) -} - diff --git a/api/undeploy.go b/api/undeploy.go deleted file mode 100644 index 99ed3e1..0000000 --- a/api/undeploy.go +++ /dev/null @@ -1,66 +0,0 @@ -package api -import ( - "context" - "fmt" - "log" - "coopcloud.tech/abra/pkg/client" - "coopcloud.tech/abra/pkg/upstream/stack" - - "net/http" -) -func (h *abraHandler) handleUndeployApp(w http.ResponseWriter, r *http.Request, appName string) { - log.Printf("App Id: %s", appName) - app, err := GetApp(appName) - if err != nil { - log.Printf("Error getting app %s: %s\n", appName, err) - InternalServerErrorHandler(w, r) - return - } - // think this just checks to make sure we have the recipe for this app - if err := app.Recipe.EnsureExists(); err != nil { - log.Fatal(err) - InternalServerErrorHandler(w, r) - return - } - - cl, err := client.New(app.Server) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - - deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName()) - if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - if !deployMeta.IsDeployed { - log.Fatal("%s is not deployed?", app.Name) - InternalServerErrorHandler(w, r) - return - } - - version := deployMeta.Version - if deployMeta.IsChaos { - version = deployMeta.ChaosVersion - } - - rmOpts := stack.Remove{ - Namespaces: []string{app.StackName()}, - Detach: false, - } - if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) - return - } - - if err := app.WriteRecipeVersion(version, false); err != nil { - log.Printf("writing recipe version failed: %s\n", err) - http.Error(w, fmt.Sprintf("writing recipe version failed: %s\n", err), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) -} diff --git a/api/utils.go b/api/utils.go deleted file mode 100644 index bdf5f0f..0000000 --- a/api/utils.go +++ /dev/null @@ -1,88 +0,0 @@ -package api -import ( - "fmt" - "errors" - "log" - "coopcloud.tech/abra/pkg/upstream/stack" - - appPkg "coopcloud.tech/abra/pkg/app" - formatter "coopcloud.tech/abra/pkg/formatter" - secret "coopcloud.tech/abra/pkg/secret" - - dockerClient "github.com/docker/docker/client" - - "coop-cloud-backend/internal" -) -func getDeployVersion(deployMeta stack.DeployMeta, app appPkg.App) (string, error) { - if internal.Chaos { - v, err := app.Recipe.ChaosVersion() - if err != nil { - return "", err - } - log.Printf("version: taking chaos version: %s", v) - return v, nil - } - - v, err := getLatestVersionOrCommit(app) - log.Printf("version: taking new recipe version: %s", v) - if err != nil { - return "", err - } - return v, nil -} - - -func getLatestVersionOrCommit(app appPkg.App) (string, error) { - recipeVersions, warnings, err := app.Recipe.GetRecipeVersions() - if err != nil { - return "", err - } - - for _, warning := range warnings { - log.Printf(warning) - } - - if len(recipeVersions) > 0 && !internal.Chaos { - latest := recipeVersions[len(recipeVersions)-1] - for tag := range latest { - log.Printf("selected latest recipe version: %s (from %d available versions)", tag, len(recipeVersions)) - return tag, nil - } - } - - head, err := app.Recipe.Head() - if err != nil { - return "", err - } - - return formatter.SmallSHA(head.String()), nil -} - -func validateSecrets(cl *dockerClient.Client, app appPkg.App) error { - composeFiles, err := app.Recipe.GetComposeFiles(app.Env) - if err != nil { - return err - } - - secretsConfig, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName()) - if err != nil { - return err - } - - secStats, err := secret.PollSecretsStatus(cl, app) - if err != nil { - return err - } - - for _, secStat := range secStats { - if !secStat.CreatedOnRemote { - secretConfig := secretsConfig[secStat.LocalName] - if secretConfig.SkipGenerate { - return errors.New(fmt.Sprintf("secret not inserted (#generate=false): %s", secStat.LocalName)) - } - return errors.New(fmt.Sprintf("secret not generated: %s", secStat.LocalName)) - } - } - - return nil -} diff --git a/api/wrappers.go b/api/wrappers.go deleted file mode 100644 index 39338d2..0000000 --- a/api/wrappers.go +++ /dev/null @@ -1,33 +0,0 @@ -package api -import ( - appPkg "coopcloud.tech/abra/pkg/app" - "coopcloud.tech/abra/pkg/config" - - composetypes "github.com/docker/cli/cli/compose/types" -) -func GetLabel(compose *composetypes.Config, stackName string, label string) string { - return appPkg.GetLabel(compose, stackName, label) -} - -func GetAppStatuses(apps []appPkg.App) (map[string]map[string]string, error) { - return appPkg.GetAppStatuses(apps, true) -} -func GetApp(appName string) (appPkg.App, error) { - return appPkg.Get(appName) -} - -func GetAppNames() ([]string, error) { - return appPkg.GetAppNames() -} - -func GetAppServiceNames(appName string) ([]string, error) { - return appPkg.GetAppServiceNames(appName) -} - -func GetServers() ([]string, error) { - return config.GetServers() -} - -func ReadServerNames() ([]string, error) { - return config.ReadServerNames() -} diff --git a/cli/api.go b/cli/api.go index 3b0b0cb..73dbc94 100644 --- a/cli/api.go +++ b/cli/api.go @@ -2,44 +2,55 @@ package cli import ( "fmt" "log" + "os/exec" "net/http" - "github.com/gorilla/websocket" - "coop-cloud-backend/internal" ) -// Upgrader is used to upgrade HTTP connections to WebSocket connections. -var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, +// // Upgrader is used to upgrade HTTP connections to WebSocket connections. +// var upgrader = websocket.Upgrader{ +// CheckOrigin: func(r *http.Request) bool { +// return true +// }, +// } +// func wsHandler(w http.ResponseWriter, r *http.Request) { +// // Upgrade the HTTP connection to a WebSocket connection +// conn, err := upgrader.Upgrade(w, r, nil) +// if err != nil { +// fmt.Println("Error upgrading:", err) +// return +// } +// defer conn.Close() +// // Listen for incoming messages +// for { +// // Read message from the client +// _, message, err := conn.ReadMessage() +// if err != nil { +// fmt.Println("Error reading message:", err) +// break +// } +// fmt.Printf("Received: %s\\n", message) +// // Echo the message back to the client +// if err := conn.WriteMessage(websocket.TextMessage, message); err != nil { +// fmt.Println("Error writing message:", err) +// break +// } +// } +// } + +// abstracted out interface into this runner so i can write tests +type CommandRunner interface { + Run(name string, args ...string) ([]byte, error) } -func wsHandler(w http.ResponseWriter, r *http.Request) { - // Upgrade the HTTP connection to a WebSocket connection - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - fmt.Println("Error upgrading:", err) - return - } - defer conn.Close() - // Listen for incoming messages - for { - // Read message from the client - _, message, err := conn.ReadMessage() - if err != nil { - fmt.Println("Error reading message:", err) - break - } - fmt.Printf("Received: %s\\n", message) - // Echo the message back to the client - if err := conn.WriteMessage(websocket.TextMessage, message); err != nil { - fmt.Println("Error writing message:", err) - break - } - } + +type ExecRunner struct{} + +func (ExecRunner) Run(name string, args ...string) ([]byte, error) { + return exec.Command(name, args...).Output() } type abraHandler struct{ mux *http.ServeMux + cmd ExecRunner } func newAbraHandler() *abraHandler { h := &abraHandler{ @@ -58,21 +69,31 @@ func newAbraHandler() *abraHandler { } }) h.mux.HandleFunc("/api/apps/{appId}/deploy", func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Chaos") == "true"{ - internal.Chaos = true - } else { - internal.Chaos = false - } - switch r.Method{ case http.MethodPost: - h.handleDeployApp(w, r, r.PathValue("appId")) + h.handleDeployApp(w, r, r.PathValue("appId"), r.URL.Query().Get("chaos") == "true") case http.MethodGet: h.getDeployLogs(w, r, r.PathValue("appId")) default: http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) } }) + h.mux.HandleFunc("/api/apps/{appId}/upgrade", func(w http.ResponseWriter, r *http.Request) { + switch r.Method{ + case http.MethodPost: + h.handleUpgradeApp(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}/rollback", func(w http.ResponseWriter, r *http.Request) { + 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}/stop", func(w http.ResponseWriter, r *http.Request) { switch r.Method{ case http.MethodPost: @@ -177,4 +198,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/catalogue.go b/cli/catalogue.go index e03dc7f..18735e2 100644 --- a/cli/catalogue.go +++ b/cli/catalogue.go @@ -1,9 +1,8 @@ package cli import ( - "log" - "fmt" "net/http" "slices" + "log" "maps" "encoding/json" "coopcloud.tech/abra/pkg/recipe" @@ -20,8 +19,7 @@ func (h *abraHandler) handleListCatalogue(w http.ResponseWriter, r *http.Request jsonBytes, err := json.Marshal(vals) if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) + InternalServerError(w, err) return } w.Header().Set("Content-Type", "application/json") diff --git a/cli/config.go b/cli/config.go index 1ae35cd..137e5ea 100644 --- a/cli/config.go +++ b/cli/config.go @@ -46,7 +46,7 @@ func (h *abraHandler) handleGetConfig(w http.ResponseWriter, r *http.Request, ap func (h *abraHandler) handlePostConfig(w http.ResponseWriter, r *http.Request, appName string) { files, err := appPkg.LoadAppFiles("") if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + InternalServerError(w, err) return } @@ -61,9 +61,8 @@ func (h *abraHandler) handlePostConfig(w http.ResponseWriter, r *http.Request, a var config FileResponse err = d.Decode(&config) if err != nil { - log.Printf("err: %s", err); - http.Error(w, "Failed to read request", http.StatusBadRequest) - return + BadRequest(w, err) + return } err = os.WriteFile(appFile.Path, []byte(config.Content), 0644) diff --git a/cli/deploy.go b/cli/deploy.go index f2b6595..cd3ee79 100644 --- a/cli/deploy.go +++ b/cli/deploy.go @@ -2,11 +2,11 @@ package cli import ( "log" "os/exec" - "coop-cloud-backend/internal" "coop-cloud-backend/cli/status" "net/http" "encoding/json" "fmt" + "errors" "context" "coopcloud.tech/abra/pkg/client" @@ -23,17 +23,16 @@ import ( //"github.com/gorilla/websocket" ) -func (h *abraHandler) handleDeployApp(w http.ResponseWriter, r *http.Request, appName string) { +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 internal.Chaos { + if chaos { args = append(args, "-C") } cmd := exec.Command("abra", args...) output, err := cmd.Output() if err != nil { - log.Printf("Error: ", string(output)) - InternalServerErrorHandler(w, r) + InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) return } log.Printf("Finishing app deploy!") @@ -43,27 +42,25 @@ func (h *abraHandler) getDeployLogs(w http.ResponseWriter, r *http.Request, appN log.Printf("Get deploy logs!") app, err := appPkg.Get(appName) if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) + InternalServerError(w, err) return } // Implement my own WaitOnServices in order to wrap ui.Model in order to emit JSON updates // over Websocket connection serviceNames, err := appPkg.GetAppServiceNames(app.Name) if err != nil { - log.Fatal(err) + InternalServerError(w, err) } f, err := app.Filters(true, false, serviceNames...) if err != nil { - log.Fatal(err) + InternalServerError(w, err) } // STEP 1: collect information about app being deployed cl, err := client.New(app.Server) if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) + InternalServerError(w, err) return } @@ -73,8 +70,7 @@ func (h *abraHandler) getDeployLogs(w http.ResponseWriter, r *http.Request, appN log.Printf("stack name: %s | namespace: %s", stackName, namespace.Name()) existingServices, err := GetStackServices(context.Background(), cl, namespace.Name()) if err != nil { - log.Printf("Error: %s\n", err) - http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError) + InternalServerError(w, err) return } @@ -112,8 +108,7 @@ func (h *abraHandler) getDeployLogs(w http.ResponseWriter, r *http.Request, appN fmt.Printf(string(test), err); b, err := json.Marshal(msg) if err != nil { - // TODO: send error through onerror handler - log.Printf("error?: %s", err) + OnError(err) continue } fmt.Fprintf(w, "data: %s\n\n", b) diff --git a/cli/error.go b/cli/error.go index d8cb450..31da3cd 100644 --- a/cli/error.go +++ b/cli/error.go @@ -1,13 +1,20 @@ package cli import ( - "net/http" + "net/http" + "log" ) -func InternalServerErrorHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("500 Internal Server Error")) +func OnError(err error) { + // handle errors that continue + log.Printf("Error: %v", err) +} +func InternalServerError(w http.ResponseWriter, err error) { + log.Printf("Error: %v", err) + http.Error(w, "internal server error", http.StatusInternalServerError) } -func NotFoundHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("404 Not Found")) +func NotFound(w http.ResponseWriter, err error) { + http.Error(w, "not found", http.StatusNotFound) +} +func BadRequest(w http.ResponseWriter, err error) { + http.Error(w, "bad request", http.StatusBadRequest) } \ No newline at end of file diff --git a/cli/interface/command.go b/cli/interface/command.go new file mode 100644 index 0000000..eb2872e --- /dev/null +++ b/cli/interface/command.go @@ -0,0 +1,9 @@ +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 a606c88..ee751a9 100644 --- a/cli/list.go +++ b/cli/list.go @@ -1,12 +1,19 @@ package cli import ( "log" + "errors" + "fmt" "net/http" "os/exec" "encoding/json" "strings" "sort" + "sync" + "time" + appPkg "coopcloud.tech/abra/pkg/app" + client "coopcloud.tech/abra/pkg/client" + "coopcloud.tech/abra/pkg/config" "coopcloud.tech/tagcmp" ) @@ -15,8 +22,7 @@ import ( cmd := exec.Command("abra", "app", "ls", "-m") output, err := cmd.CombinedOutput() if err != nil { - log.Printf("Error: ", err) - InternalServerErrorHandler(w, r) + InternalServerError(w, err) return } var resp ServerAppsResponse @@ -25,33 +31,66 @@ import ( recipeFilter := "" if err := json.Unmarshal(output, &resp); err != nil { - http.Error(w, "invalid JSON from CLI", http.StatusInternalServerError) + InternalServerError(w, errors.New(fmt.Sprintf("invalid JSON from CLI"))) return } appFiles, err := appPkg.LoadAppFiles(listAppServer) if err != nil { - log.Fatal(err) + InternalServerError(w, err) } - apps, err := appPkg.GetApps(appFiles, recipeFilter) if err != nil { - log.Fatal(err) + InternalServerError(w, err) } statuses := make(map[string]map[string]string) - alreadySeen := make(map[string]bool) - - for _, app := range apps { - if _, ok := alreadySeen[app.Server]; !ok { - alreadySeen[app.Server] = true - } + // 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", app.Server, err))) + //reachable[server] = false + } + reachableCh <- server + }(server) + } + go func() { + for server := range reachableCh { + log.Printf(server) + reachable[server] = true + } + }() + wg.Wait() + + 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. statuses, err = appPkg.GetAppStatuses(apps, true) if err != nil { log.Fatal(err) } - + // TODO: if a server is unreachable, skip its apps, maybe flag something? for _, app := range apps { var stats ServerApps @@ -96,8 +135,7 @@ import ( var newUpdates []string if version != "unknown" && chaos == "false" { if err := app.Recipe.EnsureExists(); err != nil { - log.Printf("unable to clone %s: %s", app.Name, err) - InternalServerErrorHandler(w, r) + InternalServerError(w, errors.New(fmt.Sprintf("unable to clone %s: %s", app.Name, err))) return } @@ -152,8 +190,7 @@ import ( } jsonBytes, err := json.Marshal(resp) if err != nil { - log.Printf("JSON conversion failed: %s\n", err) - InternalServerErrorHandler(w, r) + InternalServerError(w, errors.New(fmt.Sprintf("JSON conversion failed: %s\n", err))) return } w.Header().Set("Content-Type", "application/json") @@ -164,8 +201,7 @@ func (h *abraHandler) handleListServers (w http.ResponseWriter, r *http.Request) cmd := exec.Command("abra", "server", "ls", "-m") abraServers, err := cmd.CombinedOutput() if err != nil { - log.Printf("Error: ", err) - InternalServerErrorHandler(w, r) + InternalServerError(w, err) return } w.Header().Set("Content-Type", "application/json") diff --git a/cli/logs.go b/cli/logs.go index 89a05a0..1468e96 100644 --- a/cli/logs.go +++ b/cli/logs.go @@ -16,36 +16,36 @@ import ( func (h *abraHandler) handleGetLogs(w http.ResponseWriter, r *http.Request, appName string, serviceName string) { app, err := appPkg.Get(appName) if err != nil { - log.Printf("error: %s", err) + InternalServerError(w, err) return } stackName := app.StackName() if err := app.Recipe.EnsureExists(); err != nil { - log.Fatal(err) + InternalServerError(w, err) return } cl, err := client.New(app.Server) if err != nil { - log.Fatal(err) + InternalServerError(w, err) return } deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName) if err != nil { - log.Fatal(err) + InternalServerError(w, err) return } if !deployMeta.IsDeployed { - log.Printf(fmt.Sprintf("%s is not deployed?", app.Name)) + log.Printf("%s is not deployed?", app.Name) return } serviceNames := []string{serviceName} f, err := app.Filters(true, false, serviceNames...) if err != nil { - log.Fatal(err) + InternalServerError(w, err) } ctx := r.Context() @@ -54,7 +54,7 @@ func (h *abraHandler) handleGetLogs(w http.ResponseWriter, r *http.Request, appN types.ServiceListOptions{Filters: f}, ) if err != nil { - log.Fatal(err) + InternalServerError(w, err) return } diff --git a/cli/new.go b/cli/new.go index 7042be6..2a54bdb 100644 --- a/cli/new.go +++ b/cli/new.go @@ -4,6 +4,7 @@ import ( "os/exec" "net/http" "encoding/json" + "errors" "fmt" appPkg "coopcloud.tech/abra/pkg/app" @@ -48,16 +49,14 @@ func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appNa cmd := exec.Command("abra", args...) output, err := cmd.Output() if err != nil { - log.Printf("Error: ", string(output)) - InternalServerErrorHandler(w, r) + InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) return } var fs []AppSecret if body.Secrets != nil && *body.Secrets == true { appSecrets, err := createSecrets(appName, *body.Domain, *body.Server) if err != nil { - log.Printf("Error creating secrets: %s", err) - InternalServerErrorHandler(w, r) + InternalServerError(w, err) return } for k, v := range appSecrets { @@ -70,8 +69,7 @@ func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appNa } jsonBytes, err := json.Marshal(fs) if err != nil { - log.Printf("JSON conversion failed: %s\n", err) - InternalServerErrorHandler(w, r) + InternalServerError(w, err) return } w.Header().Set("Content-Type", "application/json") diff --git a/cli/ping.go b/cli/ping.go index bbd8fa7..3cb1817 100644 --- a/cli/ping.go +++ b/cli/ping.go @@ -1,7 +1,6 @@ package cli import ( "os/exec" - "log" "sort" "net/http" "encoding/json" @@ -11,13 +10,11 @@ func (h *abraHandler) handleGetAppServices (w http.ResponseWriter, r *http.Reque cmd := exec.Command("abra", "app", "ps", appName, "-m") output, err := cmd.CombinedOutput() if err != nil { - log.Printf("Error: ", err) - InternalServerErrorHandler(w, r) + InternalServerError(w, err) return } if err = json.Unmarshal(output, &tmpMsg); err != nil { - log.Printf("Error: ", err) - InternalServerErrorHandler(w, r) + InternalServerError(w, err) return } var services []AbraAppService @@ -38,8 +35,7 @@ func (h *abraHandler) handleGetAppServices (w http.ResponseWriter, r *http.Reque }) jsonBytes, err := json.Marshal(services) if err != nil { - log.Printf("JSON conversion failed: %s\n", err) - InternalServerErrorHandler(w, r) + InternalServerError(w, err) return } w.Header().Set("Content-Type", "application/json") diff --git a/cli/remove.go b/cli/remove.go index 5146e55..426d8d4 100644 --- a/cli/remove.go +++ b/cli/remove.go @@ -1,15 +1,15 @@ package cli import ( - "log" "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() if err != nil { - log.Printf("Error: ", string(output)) - InternalServerErrorHandler(w, r) + InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) return } w.WriteHeader(http.StatusOK) diff --git a/cli/secret.go b/cli/secret.go index f51ce48..49ebaaf 100644 --- a/cli/secret.go +++ b/cli/secret.go @@ -2,6 +2,8 @@ package cli import ( "log" "os/exec" + "errors" + "fmt" "encoding/json" "net/http" ) @@ -9,8 +11,7 @@ func (h *abraHandler) handleGetAppSecrets(w http.ResponseWriter, r *http.Request cmd := exec.Command("abra", "app", "secret", "list", appName, "-m") secrets, err := cmd.CombinedOutput() if err != nil { - log.Printf("Error: ", err) - InternalServerErrorHandler(w, r) + InternalServerError(w, err) return } w.Header().Set("Content-Type", "application/json") @@ -31,9 +32,7 @@ func (h *abraHandler) handleInsertAppSecret(w http.ResponseWriter, r *http.Reque err := d.Decode(&body) if err != nil { - log.Printf("???\n") - // bad JSON or unrecognized json field - http.Error(w, err.Error(), http.StatusBadRequest) + BadRequest(w, err) return } @@ -54,8 +53,7 @@ func (h *abraHandler) handleInsertAppSecret(w http.ResponseWriter, r *http.Reque cmd := exec.Command("abra", "app", "secret", "insert", appName, *body.Name, *body.Version, *body.Value) output, err := cmd.CombinedOutput() if err != nil { - log.Printf("Error: ", string(output)) - InternalServerErrorHandler(w, r) + InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) return } w.WriteHeader(http.StatusOK) diff --git a/cli/status/ws.txt b/cli/status/ws.txt deleted file mode 100644 index fd1541f..0000000 --- a/cli/status/ws.txt +++ /dev/null @@ -1,41 +0,0 @@ -// package ui -// import ( -// "github.com/gorilla/websocket" -// "time" -// "encoding/json" -// "github.com/docker/docker/pkg/jsonmessage" - -// ) -// type WSMessage struct { -// MsgType string `json:"msgType"` -// Timestamp time.Time `json:"timestamp"` -// Payload interface{} `json:"payload"` -// } -// type DonePayload struct { -// Success bool `json:"success"` -// Failed bool `json:"failed"` -// TimedOut bool `json:"timed_out"` -// } - -// func makeWsEmit(conn *websocket.Conn) func([]byte) error { -// return func(b []byte) error { -// if conn == nil { -// return nil -// } -// // Send as a text message -// return conn.WriteMessage(websocket.TextMessage, b) -// } -// } -// func send(wsEmit func([]byte) error, msgType string, payload interface{}) { -// if wsEmit == nil { -// return -// } -// msg := WSMessage{ -// MsgType: msgType, -// Timestamp: time.Now().UTC(), -// Payload: payload, -// } - -// b, _ := json.Marshal(msg) -// _ = wsEmit(b) -// } \ No newline at end of file diff --git a/cli/undeploy.go b/cli/undeploy.go index 20ea8ce..a3d9603 100644 --- a/cli/undeploy.go +++ b/cli/undeploy.go @@ -1,15 +1,13 @@ package cli import ( - "log" - "os/exec" + "errors" + "fmt" "net/http" ) func (h *abraHandler) handleUndeployApp(w http.ResponseWriter, r *http.Request, appName string) { - cmd := exec.Command("abra", "app", "undeploy", appName, "-n") - output, err := cmd.Output() + output, err := h.cmd.Run("abra", "app", "undeploy", appName, "-n") if err != nil { - log.Printf("Error: ", string(output)) - InternalServerErrorHandler(w, r) + 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 new file mode 100644 index 0000000..612f64c --- /dev/null +++ b/cli/upgrade.go @@ -0,0 +1,59 @@ +package cli +import ( + "log" + "fmt" + "errors" + "encoding/json" + "net/http" +) + +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"} + if force { + args = append(args, "-f") + } + output, err := h.cmd.Run("abra", args...) + if err != nil { + InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output)))) + return + } + log.Printf("Finishing app upgrade!") + w.WriteHeader(http.StatusOK) +} + +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() // catch unwanted fields + + // anonymous struct type: handy for one-time use + body := struct { + Version *string `json:"version"` + }{} + err := d.Decode(&body) + if err != nil { + log.Printf("error: %v", err) + BadRequest(w, err) + return + } + + + args := []string{"app", "rollback", appName, "-n", "-c"} + if force { + args = append(args, "-f") + } + if body.Version != nil { + args = append(args, *body.Version) + } + + output, err := h.cmd.Run("abra", args...) + if err != nil { + InternalServerError(w, errors.New(fmt.Sprintf("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 new file mode 100644 index 0000000..3c3012c --- /dev/null +++ b/cli/upgrade_test.go @@ -0,0 +1,117 @@ +package cli +import ( + "testing" + "net/http" + "net/http/httptest" + "errors" + "reflect" +) +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: "success", + appName: "myapp", + force: false, + expectedStatus: http.StatusOK, + expectedArgs: []string{ + "app", + "upgrade", + "myapp", + "-n", + "-c", + }, + }, + { + name: "force success", + appName: "myapp", + force: true, + expectedStatus: http.StatusOK, + expectedArgs: []string{ + "app", + "upgrade", + "myapp", + "-n", + "-c", + "-f", + }, + }, + { + name: "command failure", + appName: "myapp", + force: false, + mockErr: errors.New("dead"), + expectedStatus: http.StatusInternalServerError, + expectedArgs: []string{ + "app", + "upgrade", + "myapp", + "-n", + "-c", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCmd := &MockRunner{ + Err: tt.mockErr, + } + + h := &abraHandler{ + cmd: mockCmd, + } + + req := httptest.NewRequest(http.MethodPost, "/", nil) + w := httptest.NewRecorder() + + h.handleUpgradeApp(w, req, tt.appName, tt.force) + + res := w.Result() + + 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, + ) + } + }) + } +} \ No newline at end of file -- 2.52.0 From 491d9ffc10c148a4ac71b580007d917d09ea0c21 Mon Sep 17 00:00:00 2001 From: hey Date: Fri, 5 Jun 2026 16:05:04 -0400 Subject: [PATCH 2/6] confusing --- cli/upgrade.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cli/upgrade.go b/cli/upgrade.go index 612f64c..7cd4d5a 100644 --- a/cli/upgrade.go +++ b/cli/upgrade.go @@ -27,9 +27,8 @@ func (h* abraHandler) handleRollbackApp(w http.ResponseWriter, r *http.Request, log.Printf("Handling App rollback!") d := json.NewDecoder(r.Body) - d.DisallowUnknownFields() // catch unwanted fields - - // anonymous struct type: handy for one-time use + d.DisallowUnknownFields() + body := struct { Version *string `json:"version"` }{} -- 2.52.0 From 4a8d4b281c28b99014fed70db133b12b03ad8f25 Mon Sep 17 00:00:00 2001 From: hey Date: Tue, 14 Jul 2026 18:49:38 -0400 Subject: [PATCH 3/6] various changes, including readme change, fixed unreachable server situation, working updates and rollbacks, etc. --- Dockerfile | 9 ++++--- README.md | 4 +-- cli/api.go | 9 ++++++- cli/catalogue.go | 3 +-- cli/list.go | 57 +++++++++++++++++++++++++++++++++++++------ cli/models.go | 5 ++-- cli/new.go | 1 + cli/secret.go | 48 ++++++++++++++++++++++++++++++++---- cli/upgrade.go | 8 ++++-- command.sh | 2 +- coopcloud-front@0.0.0 | 0 go.mod | 1 + go.sum | 2 ++ vite | 0 14 files changed, 123 insertions(+), 26 deletions(-) create mode 100644 coopcloud-front@0.0.0 create mode 100644 vite diff --git a/Dockerfile b/Dockerfile index 4e726ba..5b650ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM golang:1.25 RUN mkdir /backend -COPY --parents ["api", "cli", "internal", "go.mod", "go.sum", "app.go", "/backend/"] +COPY --parents ["cli", "internal", "go.mod", "go.sum", "app.go", "/backend/"] RUN go build -C /backend -o gobackend FROM node @@ -9,9 +9,9 @@ COPY --from=0 /backend/gobackend /home/node/wizard/gobackend USER node COPY ./command.sh / -COPY ssh_config /home/node/.ssh/config -COPY id_ed25519_* /home/node/.ssh/ -COPY dot_abra /home/node/.abra/ +#COPY /var/home/thebastard/.ssh/config /home/node/.ssh/config +#COPY /var/home/thebastard/.ssh/id_* /home/node/.ssh/ +#COPY /var/home/thebastard/.abra /home/node/.abra/ RUN curl https://install.abra.coopcloud.tech | bash ENV ABRA_BIN=/home/node/.local/bin/abra # RUN $ABRA_BIN recipe fetch -a @@ -19,6 +19,7 @@ ENV ABRA_BIN=/home/node/.local/bin/abra COPY --parents web /home/node/wizard WORKDIR /home/node/wizard/web USER root +RUN apt-get update && apt-get install -y net-tools iproute2 RUN npm install . RUN chown -R node /home/node/ USER node diff --git a/README.md b/README.md index 6628bd4..fc32415 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,12 @@ Integrates with https://git.coopcloud.tech/toolshed/coop-cloud-front. ## Starting the service with Docker Build the container: ```bash -docker build -t abra-wizard:latest +docker build -t coop-cloud-wizard:latest . ``` Run the container: ```bash -docker run abra-wizard +docker run -u 1000:1000 -p 5173:5173 -p 3000:3000 -v $HOME/.abra:/home/node/.ssh -v $HOME/.abra:/home/node/.abra coop-cloud-wizard ``` ## Getting started with development diff --git a/cli/api.go b/cli/api.go index 73dbc94..717ca74 100644 --- a/cli/api.go +++ b/cli/api.go @@ -126,7 +126,6 @@ func newAbraHandler() *abraHandler { 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{ case http.MethodPost: @@ -171,6 +170,14 @@ func newAbraHandler() *abraHandler { http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) } }) + h.mux.HandleFunc("/api/apps/{appId}/versions", func(w http.ResponseWriter, r *http.Request) { + switch r.Method{ + case http.MethodGet: + h.handleListVersions(w, r, r.PathValue("appId")) + default: + http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) + } + }) return h } func StartAPI() { diff --git a/cli/catalogue.go b/cli/catalogue.go index 18735e2..69a8804 100644 --- a/cli/catalogue.go +++ b/cli/catalogue.go @@ -25,5 +25,4 @@ func (h *abraHandler) handleListCatalogue(w http.ResponseWriter, r *http.Request w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(jsonBytes) -} - +} \ No newline at end of file diff --git a/cli/list.go b/cli/list.go index ee751a9..9cdace0 100644 --- a/cli/list.go +++ b/cli/list.go @@ -8,6 +8,7 @@ import ( "encoding/json" "strings" "sort" + "github.com/samber/lo" "sync" "time" @@ -53,7 +54,7 @@ import ( start := time.Now() reachableCh := make(chan string) - errCh := make(chan error) + //errCh := make(chan error) var wg sync.WaitGroup wg.Add(len(servers)) @@ -67,17 +68,18 @@ import ( log.Printf("working on: %s", server) timeout := client.WithTimeout(5) if _, err := client.New(server, timeout); err != nil { - errCh <- err + //errCh <- err - //OnError(errors.New(fmt.Sprintf("connection failed or timed out at %s: %s", app.Server, err))) - //reachable[server] = false + OnError(errors.New(fmt.Sprintf("connection failed or timed out at %s: %s", server, err))) + reachable[server] = false + } else { + reachableCh <- server } - reachableCh <- server }(server) } go func() { for server := range reachableCh { - log.Printf(server) + log.Printf("%s is reachable", server) reachable[server] = true } }() @@ -86,7 +88,11 @@ import ( 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. - statuses, err = appPkg.GetAppStatuses(apps, true) + // 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) if err != nil { log.Fatal(err) } @@ -102,6 +108,20 @@ import ( } } + if !reachable[app.Server] { + appStats.Chaos = "unknown" + appStats.ChaosVersion = "unknown" + appStats.Version = "unknown" + appStats.Status = "Server unreachable" + for i, sapp := range stats.Apps { + if sapp.AppName == app.Name { + stats.Apps[i] = *appStats + } + } + resp[app.Server] = stats + continue + } + status := "unknown" version := "unknown" chaos := "unknown" @@ -209,6 +229,29 @@ 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) { + app, err := appPkg.Get(appName) + if err != nil { + InternalServerError(w, err) + return + } + versions, err := app.Recipe.Tags() + if err != nil { + InternalServerError(w, err) + return + } + + jsonBytes, err := json.Marshal(versions) + if err != nil { + InternalServerError(w, err) + return + } + + log.Printf("%v", versions) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(jsonBytes) +} func SortVersionsDesc(versions []string) []string { var tags []tagcmp.Tag diff --git a/cli/models.go b/cli/models.go index cfa839a..636350c 100644 --- a/cli/models.go +++ b/cli/models.go @@ -3,8 +3,8 @@ package cli // 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` + Server string `json:"server"` + Recipe string `json:"recipe` AppName string `json:"appName"` Domain string `json:"domain"` Status string `json:"status"` @@ -53,6 +53,7 @@ type AppSecret struct { Name string `json:"name"` Version string `json:"version"` Value string `json:"value"` + Created bool `json:"created"` } type AbraAppService struct { diff --git a/cli/new.go b/cli/new.go index 2a54bdb..56115d5 100644 --- a/cli/new.go +++ b/cli/new.go @@ -64,6 +64,7 @@ func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appNa Name: k, Value: v, Version: "v1", + Created: true, }) } } diff --git a/cli/secret.go b/cli/secret.go index 49ebaaf..14e5c7e 100644 --- a/cli/secret.go +++ b/cli/secret.go @@ -8,15 +8,54 @@ import ( "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() 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"` + }{} + + err = json.Unmarshal(secrets, &resp) + if err != nil { + InternalServerError(w, err) + } + var fs []AppSecret + for _, secret := range resp { + if secret.Created == nil { + InternalServerError(w, errors.New("Failed to decode field 'created on server' from abra response")) + return + } + if secret.Name == nil { + InternalServerError(w, errors.New("Failed to decode field 'name' from abra response")) + return + } + if secret.Version == nil { + InternalServerError(w, errors.New("Failed to decode field 'version' from abra response")) + return + } + fs = append(fs, AppSecret{ + Name: *secret.Name, + Version: *secret.Version, + Created: *secret.Created == "true", + Value: "", + }) + } + jsonBytes, err := json.Marshal(fs) + if err != nil { + InternalServerError(w, err) + return + } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write(secrets) + w.Write(jsonBytes) } func (h *abraHandler) handleInsertAppSecret(w http.ResponseWriter, r *http.Request, appName string) { @@ -36,17 +75,16 @@ func (h *abraHandler) handleInsertAppSecret(w http.ResponseWriter, r *http.Reque return } - if body.Name == nil { - http.Error(w, "missing field 'name' from JSON object", http.StatusBadRequest) + BadRequest(w, errors.New("missing field 'name' from JSON object")) return } if body.Version == nil { - http.Error(w, "missing field 'version' from JSON object", http.StatusBadRequest) + BadRequest(w, errors.New("missing field 'version' from JSON object")) return } if body.Value == nil { - http.Error(w, "missing field 'value' from JSON object", http.StatusBadRequest) + BadRequest(w, errors.New("missing field 'value' from JSON object")) return } log.Printf("%s %s %s", *body.Name, *body.Version, *body.Value) diff --git a/cli/upgrade.go b/cli/upgrade.go index 7cd4d5a..aaf3951 100644 --- a/cli/upgrade.go +++ b/cli/upgrade.go @@ -44,8 +44,12 @@ func (h* abraHandler) handleRollbackApp(w http.ResponseWriter, r *http.Request, if force { args = append(args, "-f") } - if body.Version != nil { - args = append(args, *body.Version) + if body.Version != nil{ + if *body.Version == "Auto" { + log.Printf("leaving abra to determine rollback version") + } else { + args = append(args, *body.Version) + } } output, err := h.cmd.Run("abra", args...) diff --git a/command.sh b/command.sh index 1ec9dae..effd08e 100755 --- a/command.sh +++ b/command.sh @@ -4,4 +4,4 @@ export PATH=/home/node/.local/bin/:$PATH /home/node/wizard/gobackend & -npm run dev -- --host +npm run dev -- --host 0.0.0.0 --port 5173 diff --git a/coopcloud-front@0.0.0 b/coopcloud-front@0.0.0 new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod index 192e83a..caec758 100644 --- a/go.mod +++ b/go.mod @@ -102,6 +102,7 @@ require ( github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + 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 diff --git a/go.sum b/go.sum index 59d69fe..b2634d8 100644 --- a/go.sum +++ b/go.sum @@ -462,6 +462,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74= github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI= github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= diff --git a/vite b/vite new file mode 100644 index 0000000..e69de29 -- 2.52.0 From 619b107a49868ced4bb1d6903d333ab849138bec Mon Sep 17 00:00:00 2001 From: hey Date: Tue, 14 Jul 2026 18:50:07 -0400 Subject: [PATCH 4/6] remove unnecessary files --- coopcloud-front@0.0.0 | 0 vite | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 coopcloud-front@0.0.0 delete mode 100644 vite diff --git a/coopcloud-front@0.0.0 b/coopcloud-front@0.0.0 deleted file mode 100644 index e69de29..0000000 diff --git a/vite b/vite deleted file mode 100644 index e69de29..0000000 -- 2.52.0 From 3d658103b660501d7f91a34742dce5afc8da4d5c Mon Sep 17 00:00:00 2001 From: hey Date: Tue, 14 Jul 2026 18:53:52 -0400 Subject: [PATCH 5/6] remove local changes to dockerfile --- Dockerfile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5b650ef..4c63022 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,9 +9,6 @@ COPY --from=0 /backend/gobackend /home/node/wizard/gobackend USER node COPY ./command.sh / -#COPY /var/home/thebastard/.ssh/config /home/node/.ssh/config -#COPY /var/home/thebastard/.ssh/id_* /home/node/.ssh/ -#COPY /var/home/thebastard/.abra /home/node/.abra/ RUN curl https://install.abra.coopcloud.tech | bash ENV ABRA_BIN=/home/node/.local/bin/abra # RUN $ABRA_BIN recipe fetch -a @@ -19,7 +16,6 @@ ENV ABRA_BIN=/home/node/.local/bin/abra COPY --parents web /home/node/wizard WORKDIR /home/node/wizard/web USER root -RUN apt-get update && apt-get install -y net-tools iproute2 RUN npm install . RUN chown -R node /home/node/ USER node -- 2.52.0 From 916c3036de3462028bf8f722c7f1d56d1d29e860 Mon Sep 17 00:00:00 2001 From: hey Date: Tue, 14 Jul 2026 19:00:20 -0400 Subject: [PATCH 6/6] fix docker volume --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc32415..2f70c32 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ docker build -t coop-cloud-wizard:latest . Run the container: ```bash -docker run -u 1000:1000 -p 5173:5173 -p 3000:3000 -v $HOME/.abra:/home/node/.ssh -v $HOME/.abra:/home/node/.abra coop-cloud-wizard +docker run -u 1000:1000 -p 5173:5173 -p 3000:3000 -v $HOME/.ssh:/home/node/.ssh -v $HOME/.abra:/home/node/.abra coop-cloud-wizard ``` ## Getting started with development -- 2.52.0