7 Commits

Author SHA1 Message Date
hey 916c3036de fix docker volume 2026-07-14 19:00:20 -04:00
hey 3d658103b6 remove local changes to dockerfile 2026-07-14 18:53:52 -04:00
hey 619b107a49 remove unnecessary files 2026-07-14 18:50:07 -04:00
hey 4a8d4b281c various changes, including readme change, fixed unreachable server situation, working updates and rollbacks, etc. 2026-07-14 18:49:38 -04:00
hey aa1bd46039 merge remote changes 2026-06-05 16:08:54 -04:00
hey 491d9ffc10 confusing 2026-06-05 16:05:04 -04:00
hey d46144c4f2 clean up changes, add update, start testing, change app list process 2026-06-05 16:03:35 -04:00
32 changed files with 467 additions and 1094 deletions
+1 -4
View File
@@ -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,6 @@ 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/
RUN curl https://install.abra.coopcloud.tech | bash
ENV ABRA_BIN=/home/node/.local/bin/abra
# RUN $ABRA_BIN recipe fetch -a
+2 -2
View File
@@ -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/.ssh:/home/node/.ssh -v $HOME/.abra:/home/node/.abra coop-cloud-wizard
```
## Getting started with development
-143
View File
@@ -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)
}
-32
View File
@@ -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)
}
-299
View File
@@ -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,
}
}
-13
View File
@@ -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"))
}
-153
View File
@@ -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)
}
-50
View File
@@ -1,50 +0,0 @@
package api
// 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"`
}
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"`
}
-32
View File
@@ -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)
}
-66
View File
@@ -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)
}
-88
View File
@@ -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
}
-33
View File
@@ -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()
}
+67 -39
View File
@@ -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:
@@ -105,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:
@@ -150,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() {
@@ -177,4 +205,4 @@ func (h *abraHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// Delegate to internal mux
h.mux.ServeHTTP(w, r)
}
}
+3 -6
View File
@@ -1,9 +1,8 @@
package cli
import (
"log"
"fmt"
"net/http"
"slices"
"log"
"maps"
"encoding/json"
"coopcloud.tech/abra/pkg/recipe"
@@ -20,12 +19,10 @@ 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")
w.WriteHeader(http.StatusOK)
w.Write(jsonBytes)
}
}
+3 -4
View File
@@ -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)
+10 -15
View File
@@ -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)
+14 -7
View File
@@ -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)
}
+9
View File
@@ -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()
}
+99 -20
View File
@@ -1,12 +1,20 @@
package cli
import (
"log"
"errors"
"fmt"
"net/http"
"os/exec"
"encoding/json"
"strings"
"sort"
"github.com/samber/lo"
"sync"
"time"
appPkg "coopcloud.tech/abra/pkg/app"
client "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp"
)
@@ -15,8 +23,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 +32,71 @@ 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()
statuses, err = appPkg.GetAppStatuses(apps, true)
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)
// TODO: I think this will check every server for every app, need to remove some first.
// rapps is all reachable apps
rapps := lo.Filter(apps, func(app appPkg.App, index int) bool {
return reachable[app.Server]
})
statuses, err = appPkg.GetAppStatuses(rapps, true)
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
@@ -63,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"
@@ -96,8 +155,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 +210,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 +221,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")
@@ -173,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
+7 -7
View File
@@ -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
}
+6 -5
View File
@@ -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"`
@@ -41,9 +41,9 @@ type DeployState struct {
}
type DeployStream struct {
Name string `json:"Name"`
id string `json:"id"`
status string `json:"status"`
Name string `json:"Name"`
id string `json:"id"`
status string `json:"status"`
retries int `json:"retries"`
health string `json:"health"`
rollback bool `json:"rollback"`
@@ -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 {
+5 -6
View File
@@ -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 {
@@ -65,13 +64,13 @@ func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appNa
Name: k,
Value: v,
Version: "v1",
Created: true,
})
}
}
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")
+3 -7
View File
@@ -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")
+3 -3
View File
@@ -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)
+48 -12
View File
@@ -2,20 +2,60 @@ package cli
import (
"log"
"os/exec"
"errors"
"fmt"
"encoding/json"
"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 {
log.Printf("Error: ", err)
InternalServerErrorHandler(w, r)
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) {
@@ -31,31 +71,27 @@ 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
}
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)
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)
-41
View File
@@ -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)
// }
+4 -6
View File
@@ -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)
+62
View File
@@ -0,0 +1,62 @@
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()
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{
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...)
if err != nil {
InternalServerError(w, errors.New(fmt.Sprintf("Error: %s", string(output))))
return
}
log.Printf("Finishing app rollback!")
w.WriteHeader(http.StatusOK)
}
+117
View File
@@ -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,
)
}
})
}
}
+1 -1
View File
@@ -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
+1
View File
@@ -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
+2
View File
@@ -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=