Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd45bcf5c5 | |||
| 8423cf5f69 | |||
| 62be5ddda7 | |||
| 3a88c8d878 | |||
| 25d01298e5 | |||
| fd53e28143 | |||
| c5e773a694 | |||
| cd47863622 | |||
| 3775894a5b |
17
.drone.yml
17
.drone.yml
@ -1,17 +0,0 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: coopcloud.tech/abra-wizard
|
||||
steps:
|
||||
|
||||
- name: publish image
|
||||
image: plugins/docker
|
||||
settings:
|
||||
auto_tag: true
|
||||
username: abra-wizard-bot
|
||||
password:
|
||||
from_secret: git_coopcloud_tech_token_abra_wizard
|
||||
repo: git.coopcloud.tech/toolshed/coop-cloud-backend
|
||||
registry: git.coopcloud.tech
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +0,0 @@
|
||||
dot_abra/
|
||||
ssh_config
|
||||
id_*
|
||||
app
|
||||
coop-cloud-backend
|
||||
24
Dockerfile
24
Dockerfile
@ -7,22 +7,24 @@ FROM node
|
||||
RUN mkdir /home/node/wizard
|
||||
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
|
||||
|
||||
COPY --parents web /home/node/wizard
|
||||
WORKDIR /home/node/wizard/web
|
||||
USER root
|
||||
RUN npm install .
|
||||
|
||||
USER node
|
||||
RUN curl https://install.abra.coopcloud.tech | bash
|
||||
ENV ABRA_BIN=/home/node/.local/bin/abra
|
||||
RUN $ABRA_BIN recipe fetch -a
|
||||
|
||||
COPY ./start.sh /
|
||||
COPY dot_abra /home/node/.abra/
|
||||
COPY ssh_config /home/node/.ssh/config
|
||||
COPY id_ed25519_* /home/node/.ssh/
|
||||
|
||||
USER root
|
||||
RUN chown -R node /home/node/
|
||||
USER node
|
||||
|
||||
EXPOSE 5173 3000
|
||||
# ENV VITE_API_URL=http://localhost:3000/api
|
||||
CMD [ "/command.sh" ]
|
||||
CMD [ "/start.sh" ]
|
||||
|
||||
42
README.md
42
README.md
@ -1,48 +1,14 @@
|
||||
# coop-cloud-backend
|
||||
A Go service that exposes RESTful API endpoints to manage Abra programmatically.
|
||||
Integrates with https://git.coopcloud.tech/toolshed/coop-cloud-front.
|
||||
Integrates with https://git.coopcloud.tech/BornDeleuze/coop-cloud-front.
|
||||
|
||||
## Starting the service with Docker
|
||||
Build the container:
|
||||
```bash
|
||||
docker build -t abra-wizard:latest
|
||||
```
|
||||
## Getting started
|
||||
|
||||
Run the container:
|
||||
```bash
|
||||
docker run abra-wizard
|
||||
```
|
||||
|
||||
## Getting started with development
|
||||
- Clone the front end application `git clone https://git.coopcloud.tech/toolshed/coop-cloud-front.git`
|
||||
- Checkout the `dev-nomock` branch `cd coop-cloud-front && git checkout dev-nomock`
|
||||
- Edit the front-end application to turn off mock mode in `src/routes/Authenticated/App.tsx` and `src/routes/Authenticated/Apps.tsx`.
|
||||
- Launch the front-end application `npm run dev`
|
||||
- Start this Go app `go run .`
|
||||
- Navigate to the React App (http://localhost:5173)
|
||||
|
||||
> [!WARNING]
|
||||
> This is an extremely early prototype, only viewing/deploying apps is supported
|
||||
> This is an extremely early prototype, only viewing apps is supported
|
||||
> and may fail for your local machine
|
||||
|
||||
## Notes:
|
||||
`api/` is a deprecated path, currently has no function
|
||||
|
||||
# Roadmap
|
||||
## General Roadmap:
|
||||
Currently refactored the backend to be much more Abra CLI reliant. This is ok but is not ideal from an engineering standpoint (probably).
|
||||
First step is just to polish this backend & frontend, estimate 1 - 2 months to be in a fairly usable state.
|
||||
Write test suite for the full stack combination.
|
||||
Then we want to package this app into a dockerized form, easily to deploy and use on one's own server.
|
||||
Slowly introduce full Abra CLI functionality into app.
|
||||
Repackaging
|
||||
## Basic Abra Features
|
||||
| Feature | Status | Follow-up |
|
||||
| ----------- | ----------- | ----------- |
|
||||
| More options for app deploy (esp chaos) | TODO | |
|
||||
| View and pull in recipes from catalogue | In Progress | |
|
||||
| Deploy should show deployment status similar to Abra CLI | In Progress | |
|
||||
| App screen should display services associated with app and their status | TODO | |
|
||||
| Easily show service logs | In Progress | |
|
||||
| Modify App config | TODO | |
|
||||
| Manage updates and rollbacks | TODO | |
|
||||
| Manage secrets and Abra commands | In Progress | |
|
||||
|
||||
462
api/api.go
462
api/api.go
@ -1,143 +1,439 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
configPkg "coopcloud.tech/abra/pkg/config"
|
||||
deployPkg "coopcloud.tech/abra/pkg/deploy"
|
||||
|
||||
"net/http"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
|
||||
"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{
|
||||
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-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
case http.MethodGet:
|
||||
h.handleListApps(w, r)
|
||||
default:
|
||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
h.mux.HandleFunc("/api/abra/apps/{appId}/deploy", func(w http.ResponseWriter, r *http.Request) {
|
||||
h.mux.HandleFunc("/api/abra/apps/{serverId}/{appId}/deploy", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
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{
|
||||
|
||||
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"))
|
||||
h.handleDeployApp(w, r, r.PathValue("appId"), r.PathValue("serverId"))
|
||||
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-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
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)
|
||||
log.Println("Server started on port 3000")
|
||||
log.Fatal(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)
|
||||
}
|
||||
|
||||
// func (h *appHandler) handleStartApp(w http.ResponseWriter, r *http.Request) {
|
||||
// appName := r.PathValue("appName")
|
||||
// w.Write([]byte("starting app: " + appName))
|
||||
|
||||
// }
|
||||
|
||||
func (h *abraHandler) handleDeployApp(w http.ResponseWriter, r *http.Request, appName string, serverId string) {
|
||||
log.Printf("App Id: %s | Server Id: %s", appName, serverId)
|
||||
app, err := GetApp(appName)
|
||||
if err != nil {
|
||||
log.Printf("Error getting app %s: %s\n", appName, err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
if deployMeta.IsDeployed {
|
||||
log.Fatalf("%s is already deployed", app.Name)
|
||||
InternalServerErrorHandler(w, r)
|
||||
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.Fatal(err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateSecrets(cl, app); err != nil {
|
||||
log.Fatal(err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := deployPkg.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
|
||||
log.Fatal(err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
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.Fatal(err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
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.Fatal(err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
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 := configPkg.MISSING_DEFAULT
|
||||
if deployMeta.IsDeployed {
|
||||
deployedVersion = deployMeta.Version
|
||||
if deployMeta.IsChaos {
|
||||
deployedVersion = deployMeta.ChaosVersion
|
||||
}
|
||||
}
|
||||
ShowUnchanged := false
|
||||
secretInfo, err := deployPkg.GatherSecretsForDeploy(cl, app, ShowUnchanged)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Gather configs
|
||||
configInfo, err := deployPkg.GatherConfigsForDeploy(cl, app, compose, app.Env, ShowUnchanged)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Gather images
|
||||
imageInfo, err := deployPkg.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.Fatal(err)
|
||||
}
|
||||
|
||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 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.Fatal(err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
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("GetAppStatuses Falied\n")
|
||||
log.Println("Error: ", err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
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)
|
||||
serverApps.Apps = append(serverApps.Apps, appTransposeUndeployed(app))
|
||||
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("Error converting to json: %s\n", err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(jsonBytes)
|
||||
}
|
||||
func appTransposeUndeployed(app appPkg.App) AbraApp {
|
||||
config, err := app.Recipe.GetComposeConfig(app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(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",
|
||||
}
|
||||
}
|
||||
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 {
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
serverNames, err := ReadServerNames()
|
||||
if err != nil {
|
||||
InternalServerErrorHandler(w, r)
|
||||
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 {
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(jsonBytes)
|
||||
}
|
||||
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 configPkg.GetServers()
|
||||
}
|
||||
|
||||
func ReadServerNames() ([]string, error) {
|
||||
return configPkg.ReadServerNames()
|
||||
}
|
||||
|
||||
@ -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
api/deploy.go
299
api/deploy.go
@ -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,
|
||||
}
|
||||
}
|
||||
153
api/list.go
153
api/list.go
@ -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)
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package api
|
||||
|
||||
// Represents an App, follows the format of
|
||||
// https://git.coopcloud.tech/toolshed/coop-cloud-front
|
||||
// https://git.coopcloud.tech/BornDeleuze/coop-cloud-front
|
||||
type AbraApp struct {
|
||||
Server string `json:"server"`
|
||||
Recipe string `json:"recipe`
|
||||
@ -28,23 +28,4 @@ type ServerApps struct {
|
||||
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
api/new.go
32
api/new.go
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
12
app.go
12
app.go
@ -17,7 +17,7 @@ import (
|
||||
// "github.com/spf13/cobra"
|
||||
|
||||
// "coopcloud.tech/abra/pkg/log"
|
||||
"coop-cloud-backend/cli"
|
||||
"coop-cloud-backend/api"
|
||||
)
|
||||
|
||||
// getEnv reads env variables from docker services.
|
||||
@ -49,7 +49,7 @@ import (
|
||||
// return envMap, nil
|
||||
// }
|
||||
func printApp(app appPkg.App) error {
|
||||
fmt.Printf("Name: %s | Domain: %s | Server: %s | Path: %s",
|
||||
log.Printf("Name: %s | Domain: %s | Server: %s | Path: %s",
|
||||
app.Name, app.Domain, app.Server, app.Path)
|
||||
return nil
|
||||
}
|
||||
@ -64,13 +64,13 @@ func connectToContainer(app appPkg.App, serviceName string) {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", stackAndServiceName)
|
||||
|
||||
fmt.Printf("HI?")
|
||||
log.Printf("HI?")
|
||||
|
||||
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Container ID: %s", targetContainer.ID)
|
||||
log.Printf("Container ID: %s", targetContainer.ID)
|
||||
}
|
||||
func main() {
|
||||
appNames, err := appPkg.GetAppNames()
|
||||
@ -79,8 +79,8 @@ func main() {
|
||||
}
|
||||
for _, appName := range appNames {
|
||||
val := fmt.Sprintf("app , %v", appName)
|
||||
fmt.Println(val)
|
||||
log.Println(val)
|
||||
}
|
||||
|
||||
cli.StartAPI()
|
||||
api.StartAPI()
|
||||
}
|
||||
|
||||
180
cli/api.go
180
cli/api.go
@ -1,180 +0,0 @@
|
||||
package cli
|
||||
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("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("API Path not found")
|
||||
http.Error(w, "API Path not found", http.StatusMethodNotAllowed)
|
||||
})
|
||||
h.mux.HandleFunc("/api/apps", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
h.handleListApps(w, r)
|
||||
default:
|
||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
h.mux.HandleFunc("/api/apps/{appId}/deploy", func(w http.ResponseWriter, r *http.Request) {
|
||||
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"))
|
||||
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}/stop", func(w http.ResponseWriter, r *http.Request) {
|
||||
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/apps/{appId}/remove", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method{
|
||||
case http.MethodPost:
|
||||
h.handleRemoveApp(w, r, r.PathValue("appId"))
|
||||
default:
|
||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
h.mux.HandleFunc("/api/servers", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
h.handleListServers(w, r)
|
||||
default:
|
||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
h.mux.HandleFunc("/api/catalogue", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
h.handleListCatalogue(w, r)
|
||||
default:
|
||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
h.mux.HandleFunc("/api/apps/{appId}/new", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method{
|
||||
case http.MethodPost:
|
||||
h.handleNewApp(w, r, r.PathValue("appId"))
|
||||
default:
|
||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
h.mux.HandleFunc("/api/apps/{appId}/config", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method{
|
||||
case http.MethodPost:
|
||||
h.handlePostConfig(w, r, r.PathValue("appId"))
|
||||
case http.MethodGet:
|
||||
h.handleGetConfig(w, r, r.PathValue("appId"))
|
||||
default:
|
||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
h.mux.HandleFunc("/api/apps/{appId}/secret", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method{
|
||||
case http.MethodPost:
|
||||
h.handleInsertAppSecret(w, r, r.PathValue("appId"))
|
||||
case http.MethodGet:
|
||||
h.handleGetAppSecrets(w, r, r.PathValue("appId"))
|
||||
default:
|
||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
h.mux.HandleFunc("/api/apps/{appId}/services", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method{
|
||||
case http.MethodGet:
|
||||
h.handleGetAppServices(w, r, r.PathValue("appId"))
|
||||
default:
|
||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
h.mux.HandleFunc("/api/apps/{appId}/{serviceId}/logs", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method{
|
||||
case http.MethodGet:
|
||||
h.handleGetLogs(w, r, r.PathValue("appId"), r.PathValue("serviceId"))
|
||||
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, X-Requested-With")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
// Delegate to internal mux
|
||||
h.mux.ServeHTTP(w, r)
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
package cli
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"maps"
|
||||
"encoding/json"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
|
||||
)
|
||||
|
||||
func (h *abraHandler) handleListCatalogue(w http.ResponseWriter, r *http.Request) {
|
||||
offline := false
|
||||
catl, err := recipe.ReadRecipeCatalogue(offline)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
vals := slices.Collect(maps.Values(catl))
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(jsonBytes)
|
||||
}
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
package cli
|
||||
import (
|
||||
"log"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
)
|
||||
type FileResponse struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (h *abraHandler) handleGetConfig(w http.ResponseWriter, r *http.Request, appName string) {
|
||||
files, err := appPkg.LoadAppFiles("")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
appFile, exists := files[appName]
|
||||
if !exists {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
log.Printf("path: %s | server: %s", appFile.Path, appFile.Server)
|
||||
log.Printf("Ending...")
|
||||
// TODO: sanitize
|
||||
file, err := ioutil.ReadFile(appFile.Path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
resp := FileResponse{Content: string(file)}
|
||||
jsonBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(jsonBytes)
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
appFile, exists := files[appName]
|
||||
if !exists {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
d := json.NewDecoder(r.Body)
|
||||
d.DisallowUnknownFields() // catch unwanted fields
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err = os.WriteFile(appFile.Path, []byte(config.Content), 0644)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to write file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
139
cli/deploy.go
139
cli/deploy.go
@ -1,139 +0,0 @@
|
||||
package cli
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"coop-cloud-backend/internal"
|
||||
"coop-cloud-backend/cli/status"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"context"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
//"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"coopcloud.tech/abra/pkg/ui"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
dclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
|
||||
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
|
||||
//"github.com/gorilla/websocket"
|
||||
)
|
||||
func (h *abraHandler) handleDeployApp(w http.ResponseWriter, r *http.Request, appName string) {
|
||||
log.Printf("Handling App Deploy!")
|
||||
args := []string{"app", "deploy", appName, "-n", "-c"}
|
||||
if internal.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)
|
||||
return
|
||||
}
|
||||
log.Printf("Finishing app deploy!")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
func (h *abraHandler) getDeployLogs(w http.ResponseWriter, r *http.Request, appName string) {
|
||||
log.Printf("Get deploy logs!")
|
||||
app, err := appPkg.Get(appName)
|
||||
if 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
|
||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
log.Fatal(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)
|
||||
return
|
||||
}
|
||||
|
||||
var serviceIDs []ui.ServiceMeta
|
||||
stackName := app.StackName()
|
||||
namespace := convert.NewNamespace(stackName)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
for _, service := range existingServices {
|
||||
log.Printf("existingServices contains: %s", service.Spec.Name)
|
||||
serviceIDs = append(serviceIDs, ui.ServiceMeta{
|
||||
Name: service.Spec.Name,
|
||||
ID: service.ID,
|
||||
})
|
||||
}
|
||||
ctx := r.Context()
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
log.Printf("Waiting on service...")
|
||||
|
||||
stream := make (chan status.StreamEvent, 50)
|
||||
go status.WaitOnServices(ctx, cl, serviceIDs, f, stream)
|
||||
for {
|
||||
select{
|
||||
case <- ctx.Done():
|
||||
log.Printf("deploy cancelled or done")
|
||||
return
|
||||
case msg, ok := <- stream:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
test, err := json.MarshalIndent(msg, "", " ")
|
||||
fmt.Printf(string(test), err);
|
||||
b, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
// TODO: send error through onerror handler
|
||||
log.Printf("error?: %s", err)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, "data: %s\n\n", b)
|
||||
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func getStackFilter(namespace string) filters.Args {
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", convert.LabelNamespace+"="+namespace)
|
||||
return filter
|
||||
}
|
||||
func getStackServiceFilter(namespace string) filters.Args {
|
||||
return getStackFilter(namespace)
|
||||
}
|
||||
|
||||
func GetStackServices(ctx context.Context, dockerclient dclient.APIClient, namespace string) ([]swarm.Service, error) {
|
||||
return dockerclient.ServiceList(ctx, types.ServiceListOptions{Filters: getStackServiceFilter(namespace)})
|
||||
}
|
||||
13
cli/error.go
13
cli/error.go
@ -1,13 +0,0 @@
|
||||
package cli
|
||||
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"))
|
||||
}
|
||||
192
cli/list.go
192
cli/list.go
@ -1,192 +0,0 @@
|
||||
package cli
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sort"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/tagcmp"
|
||||
|
||||
)
|
||||
|
||||
func (h *abraHandler) handleListApps(w http.ResponseWriter, r *http.Request) {
|
||||
cmd := exec.Command("abra", "app", "ls", "-m")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("Error: ", err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
var resp ServerAppsResponse
|
||||
// no filtering
|
||||
listAppServer := ""
|
||||
recipeFilter := ""
|
||||
|
||||
if err := json.Unmarshal(output, &resp); err != nil {
|
||||
http.Error(w, "invalid JSON from CLI", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
appFiles, err := appPkg.LoadAppFiles(listAppServer)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
apps, err := appPkg.GetApps(appFiles, recipeFilter)
|
||||
if err != nil {
|
||||
log.Fatal(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
|
||||
}
|
||||
}
|
||||
|
||||
statuses, err = appPkg.GetAppStatuses(apps, true)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, app := range apps {
|
||||
var stats ServerApps
|
||||
|
||||
stats = resp[app.Server]
|
||||
var appStats *AbraApp
|
||||
for _, sapp := range stats.Apps {
|
||||
if sapp.AppName == app.Name {
|
||||
appStats = &sapp
|
||||
}
|
||||
}
|
||||
|
||||
status := "unknown"
|
||||
version := "unknown"
|
||||
chaos := "unknown"
|
||||
chaosVersion := "unknown"
|
||||
if statusMeta, ok := statuses[app.StackName()]; ok {
|
||||
if currentVersion, exists := statusMeta["version"]; exists {
|
||||
if currentVersion != "" {
|
||||
version = currentVersion
|
||||
}
|
||||
}
|
||||
if chaosDeploy, exists := statusMeta["chaos"]; exists {
|
||||
chaos = chaosDeploy
|
||||
}
|
||||
if chaosDeployVersion, exists := statusMeta["chaosVersion"]; exists {
|
||||
chaosVersion = chaosDeployVersion
|
||||
}
|
||||
if statusMeta["status"] != "" {
|
||||
status = statusMeta["status"]
|
||||
}
|
||||
stats.VersionCount++
|
||||
} else {
|
||||
stats.UnversionedCount++
|
||||
}
|
||||
|
||||
appStats.Status = status
|
||||
appStats.Chaos = chaos
|
||||
appStats.ChaosVersion = chaosVersion
|
||||
appStats.Version = version
|
||||
localApp := true
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
updates, err := app.Recipe.Tags()
|
||||
if err != nil {
|
||||
localApp = false
|
||||
} else {
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, update := range updates {
|
||||
if ok := tagcmp.IsParsable(update); !ok {
|
||||
log.Printf("unable to parse %s, skipping as upgrade option", update)
|
||||
continue
|
||||
}
|
||||
|
||||
parsedUpdate, err := tagcmp.Parse(update)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
|
||||
newUpdates = append(newUpdates, update)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(newUpdates) == 0 {
|
||||
if version == "unknown" {
|
||||
appStats.Upgrade = "unknown"
|
||||
} else if localApp {
|
||||
appStats.Upgrade = "latest"
|
||||
stats.LatestCount++
|
||||
} else {
|
||||
appStats.Upgrade = "unknown"
|
||||
}
|
||||
} else {
|
||||
newUpdates = SortVersionsDesc(newUpdates)
|
||||
appStats.Upgrade = strings.Join(newUpdates, "\n")
|
||||
stats.UpgradeCount++
|
||||
}
|
||||
|
||||
for i, sapp := range stats.Apps {
|
||||
if sapp.AppName == app.Name {
|
||||
stats.Apps[i] = *appStats
|
||||
}
|
||||
}
|
||||
resp[app.Server] = stats
|
||||
}
|
||||
jsonBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
log.Printf("JSON conversion failed: %s\n", err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(jsonBytes)
|
||||
}
|
||||
func (h *abraHandler) handleListServers (w http.ResponseWriter, r *http.Request) {
|
||||
cmd := exec.Command("abra", "server", "ls", "-m")
|
||||
abraServers, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("Error: ", err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(abraServers)
|
||||
}
|
||||
|
||||
func SortVersionsDesc(versions []string) []string {
|
||||
var tags []tagcmp.Tag
|
||||
|
||||
for _, v := range versions {
|
||||
parsed, _ := tagcmp.Parse(v) // skips unsupported tags
|
||||
tags = append(tags, parsed)
|
||||
}
|
||||
|
||||
sort.Sort(tagcmp.ByTagDesc(tags))
|
||||
|
||||
var desc []string
|
||||
for _, t := range tags {
|
||||
desc = append(desc, t.String())
|
||||
}
|
||||
|
||||
return desc
|
||||
}
|
||||
86
cli/logs.go
86
cli/logs.go
@ -1,86 +0,0 @@
|
||||
package cli
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coop-cloud-backend/cli/status"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
|
||||
)
|
||||
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)
|
||||
return
|
||||
}
|
||||
stackName := app.StackName()
|
||||
|
||||
if err := app.Recipe.EnsureExists(); err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !deployMeta.IsDeployed {
|
||||
log.Printf(fmt.Sprintf("%s is not deployed?", app.Name))
|
||||
return
|
||||
}
|
||||
serviceNames := []string{serviceName}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ctx := r.Context()
|
||||
|
||||
services, err := cl.ServiceList(
|
||||
context.Background(),
|
||||
types.ServiceListOptions{Filters: f},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
logCh := make (chan string, 50)
|
||||
go status.StreamLogs(ctx, cl, services, logCh)
|
||||
for {
|
||||
select{
|
||||
case <- ctx.Done():
|
||||
log.Printf("log streaming done")
|
||||
return
|
||||
case line, ok := <- logCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "data: %s\n\n", line)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
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`
|
||||
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"`
|
||||
}
|
||||
|
||||
type AppSecret struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type AbraAppService struct {
|
||||
Service string `json:"service"`
|
||||
Chaos bool `json:"chaos"`
|
||||
Created string `json:"created"`
|
||||
Image string `json:"image"`
|
||||
Ports string `json:"ports"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// type RecipeMeta struct {
|
||||
// Category string `json:"category"`
|
||||
// DefaultBranch string `json:"default_branch"`
|
||||
// Description string `json:"description"`
|
||||
// Features Features `json:"features"`
|
||||
// Icon string `json:"icon"`
|
||||
// Name string `json:"name"`
|
||||
// Repository string `json:"repository"`
|
||||
// SSHURL string `json:"ssh_url"`
|
||||
// Versions RecipeVersions `json:"versions"`
|
||||
// Website string `json:"website"`
|
||||
// }
|
||||
|
||||
// type RecipeCatalogue map[Name]RecipeMeta
|
||||
115
cli/new.go
115
cli/new.go
@ -1,115 +0,0 @@
|
||||
package cli
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/secret"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
)
|
||||
|
||||
func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appName string) {
|
||||
args := []string{"app", "new", appName, "-n"}
|
||||
d := json.NewDecoder(r.Body)
|
||||
d.DisallowUnknownFields() // catch unwanted fields
|
||||
|
||||
// anonymous struct type: handy for one-time use
|
||||
body := struct {
|
||||
Domain *string `json:"domain"`
|
||||
Server *string `json:"server"`
|
||||
Chaos *bool `json:"chaos"`
|
||||
Secrets *bool `json:"secrets"`
|
||||
}{}
|
||||
|
||||
err := d.Decode(&body)
|
||||
if err != nil {
|
||||
// bad JSON or unrecognized json field
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Domain == nil {
|
||||
http.Error(w, "missing field 'domain' from JSON object", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Server == nil {
|
||||
http.Error(w, "missing field 'server' from JSON object", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
args = append(args, fmt.Sprintf("--domain=%s", *body.Domain))
|
||||
args = append(args, fmt.Sprintf("--server=%s", *body.Server))
|
||||
if body.Chaos != nil && *body.Chaos == true {
|
||||
args = append(args, "-C")
|
||||
}
|
||||
log.Printf("%v", args)
|
||||
cmd := exec.Command("abra", args...)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Printf("Error: ", string(output))
|
||||
InternalServerErrorHandler(w, r)
|
||||
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)
|
||||
return
|
||||
}
|
||||
for k, v := range appSecrets {
|
||||
fs = append(fs, AppSecret{
|
||||
Name: k,
|
||||
Value: v,
|
||||
Version: "v1",
|
||||
})
|
||||
}
|
||||
}
|
||||
jsonBytes, err := json.Marshal(fs)
|
||||
if err != nil {
|
||||
log.Printf("JSON conversion failed: %s\n", err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(jsonBytes)
|
||||
}
|
||||
|
||||
func createSecrets(appName string, domain string, server string) (map[string]string, error) {
|
||||
cl, err := client.New(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recipe := recipe.Get(appName)
|
||||
|
||||
|
||||
sampleEnv, err := recipe.SampleEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
composeFiles, err := recipe.GetComposeFiles(sampleEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//sanitisedAppName := appPkg.SanitiseAppName(domain)
|
||||
secretsConfig, err := secret.ReadSecretsConfig(
|
||||
recipe.SampleEnvPath,
|
||||
composeFiles,
|
||||
appPkg.StackName(domain),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secrets, err := secret.GenerateSecrets(cl, secretsConfig, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
48
cli/ping.go
48
cli/ping.go
@ -1,48 +0,0 @@
|
||||
package cli
|
||||
import (
|
||||
"os/exec"
|
||||
"log"
|
||||
"sort"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
)
|
||||
func (h *abraHandler) handleGetAppServices (w http.ResponseWriter, r *http.Request, appName string) {
|
||||
var tmpMsg map[string]map[string]string
|
||||
cmd := exec.Command("abra", "app", "ps", appName, "-m")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("Error: ", err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(output, &tmpMsg); err != nil {
|
||||
log.Printf("Error: ", err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
var services []AbraAppService
|
||||
for _,v := range tmpMsg {
|
||||
services = append(services, AbraAppService{
|
||||
Service: v["service"],
|
||||
Chaos: v["chaos"] == "true",
|
||||
Created: v["created"],
|
||||
Image: v["image"],
|
||||
Ports: v["ports"],
|
||||
State: v["state"],
|
||||
Status: v["status"],
|
||||
Version: v["version"],
|
||||
})
|
||||
}
|
||||
sort.Slice(services, func(i, j int) bool {
|
||||
return services[i].Service < services[j].Service
|
||||
})
|
||||
jsonBytes, err := json.Marshal(services)
|
||||
if err != nil {
|
||||
log.Printf("JSON conversion failed: %s\n", err)
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(jsonBytes)
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package cli
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"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)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
package cli
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"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)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(secrets)
|
||||
}
|
||||
|
||||
func (h *abraHandler) handleInsertAppSecret(w http.ResponseWriter, r *http.Request, appName string) {
|
||||
d := json.NewDecoder(r.Body)
|
||||
d.DisallowUnknownFields() // catch unwanted fields
|
||||
|
||||
// anonymous struct type: handy for one-time use
|
||||
body := struct {
|
||||
Name *string `json:"name"`
|
||||
Version *string `json:"version"`
|
||||
Value *string `json:"value"`
|
||||
}{}
|
||||
|
||||
err := d.Decode(&body)
|
||||
if err != nil {
|
||||
log.Printf("???\n")
|
||||
// bad JSON or unrecognized json field
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if body.Name == nil {
|
||||
http.Error(w, "missing field 'name' from JSON object", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Version == nil {
|
||||
http.Error(w, "missing field 'version' from JSON object", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Value == nil {
|
||||
http.Error(w, "missing field 'value' from JSON object", http.StatusBadRequest)
|
||||
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)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
@ -1,305 +0,0 @@
|
||||
package status
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"io"
|
||||
"log"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"encoding/json"
|
||||
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
containerTypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
|
||||
"coopcloud.tech/abra/pkg/ui"
|
||||
)
|
||||
type Event interface {
|
||||
ServiceID() string
|
||||
}
|
||||
|
||||
type ProgressEvent struct {
|
||||
Service ServiceState
|
||||
Err error
|
||||
Failed bool
|
||||
}
|
||||
func (e ProgressEvent) ServiceID() string { return e.Service.Id }
|
||||
|
||||
type HealthEvent struct {
|
||||
Service ServiceState
|
||||
Err error
|
||||
Health string
|
||||
}
|
||||
func (e HealthEvent) ServiceID() string { return e.Service.Id }
|
||||
|
||||
type StatusEvent struct {
|
||||
Service ServiceState
|
||||
Err error
|
||||
JsonMsg jsonmessage.JSONMessage
|
||||
}
|
||||
func (e StatusEvent) ServiceID() string { return e.Service.Id }
|
||||
|
||||
type StreamEvent struct {
|
||||
Type string `json:"type"` // "service" | "done"
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type ServiceState struct {
|
||||
Name string `json:"name"`
|
||||
Err error `json:"err"`
|
||||
Id string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Retries int `json:"retries"`
|
||||
Health string `json:"health"`
|
||||
Rollback bool `json:"rollback"`
|
||||
Failed bool `json:"failed"`
|
||||
}
|
||||
|
||||
type DeployState struct {
|
||||
Count int `json:"count"`
|
||||
Total int `json:"total"`
|
||||
Failed bool `json:"failed"`
|
||||
Quit bool `json:"quit"`
|
||||
}
|
||||
|
||||
type DeployMsg int
|
||||
const (
|
||||
FailMsg DeployMsg = iota
|
||||
CompleteMsg
|
||||
QuitMsg
|
||||
)
|
||||
|
||||
func (ds DeployState) complete() bool {
|
||||
return ds.Count == ds.Total
|
||||
}
|
||||
|
||||
func progressProducer(ctx context.Context, cl *dockerClient.Client, s ServiceState, w *io.PipeWriter, ch chan<- Event) {
|
||||
go func() {
|
||||
log.Printf("producing progress...")
|
||||
err := progress.ServiceProgress(ctx, cl, s.Id, w)
|
||||
log.Printf("got some service progress here")
|
||||
if err != nil {
|
||||
log.Printf("Error in service progress")
|
||||
ch <- ProgressEvent{
|
||||
Service: s,
|
||||
Failed: true,
|
||||
Err: err,
|
||||
}
|
||||
} else {
|
||||
ch <- ProgressEvent{
|
||||
Service: s,
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func statusProducer(ctx context.Context, decoder *json.Decoder, s ServiceState, ch chan<- Event) {
|
||||
go func() {
|
||||
log.Printf("producing status...")
|
||||
for {
|
||||
var msg jsonmessage.JSONMessage
|
||||
|
||||
if err := decoder.Decode(&msg); err != nil {
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
|
||||
ch <- StatusEvent{
|
||||
Service: s,
|
||||
Err: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("Maybe writing JSON message")
|
||||
msg.Display(os.Stdout, false);
|
||||
ch <- StatusEvent{
|
||||
Service: s,
|
||||
JsonMsg: msg,
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func healthProducer(ctx context.Context, cl *dockerClient.Client, s ServiceState, ch chan<- Event) {
|
||||
go func() {
|
||||
log.Printf("producing health...")
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
health := ""
|
||||
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("^%s", s.Name))
|
||||
|
||||
containers, err := cl.ContainerList(ctx, containerTypes.ListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
ch <- HealthEvent{Service: s, Err: err}
|
||||
continue
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
ch <- HealthEvent{Service: s}
|
||||
continue
|
||||
}
|
||||
|
||||
containerState, err := cl.ContainerInspect(ctx, containers[0].ID)
|
||||
if err != nil {
|
||||
ch <- HealthEvent{Service: s, Err: err}
|
||||
continue
|
||||
}
|
||||
|
||||
if containerState.State.Health != nil {
|
||||
health = containerState.State.Health.Status
|
||||
}
|
||||
ch <- HealthEvent{
|
||||
Service: s,
|
||||
Health: health,
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func processEvent(ctx context.Context, events <- chan Event, info chan <- DeployMsg, s ServiceState, stream chan <- StreamEvent) error {
|
||||
for {
|
||||
select {
|
||||
case event := <- events:
|
||||
if event.ServiceID() != s.Id {
|
||||
continue
|
||||
}
|
||||
switch event := event.(type) {
|
||||
case ProgressEvent:
|
||||
if event.Err != nil {
|
||||
s.Err = event.Err
|
||||
log.Printf("Error in progress event: %s", s.Err)
|
||||
}
|
||||
if event.Failed {
|
||||
info <- FailMsg
|
||||
}
|
||||
// print for debugging purposes
|
||||
log.Printf("Service: %s is complete", event.Service.Id)
|
||||
// end print
|
||||
info <- CompleteMsg
|
||||
|
||||
case StatusEvent:
|
||||
if event.Err != nil {
|
||||
s.Err = event.Err
|
||||
log.Printf("Error in status event: %s", s.Err)
|
||||
}
|
||||
|
||||
// print for debugging purposes
|
||||
b, err := json.MarshalIndent(event.JsonMsg, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("Problem with json message: %s", err)
|
||||
}
|
||||
log.Printf(string(b))
|
||||
// end print
|
||||
|
||||
if event.JsonMsg.ID == "rollback" {
|
||||
log.Printf("Failed in rollback")
|
||||
s.Failed = true
|
||||
s.Rollback = true
|
||||
}
|
||||
|
||||
if event.JsonMsg.ID != "overall progress" {
|
||||
newStatus := strings.ToLower(event.JsonMsg.Status)
|
||||
currentStatus := s.Status
|
||||
|
||||
if !strings.Contains(currentStatus, "starting") &&
|
||||
strings.Contains(newStatus, "starting") {
|
||||
s.Retries += 1
|
||||
}
|
||||
|
||||
if s.Rollback {
|
||||
if event.JsonMsg.ID == "rollback" {
|
||||
s.Status = newStatus
|
||||
}
|
||||
} else {
|
||||
s.Status = newStatus
|
||||
}
|
||||
}
|
||||
|
||||
case HealthEvent:
|
||||
if event.Err != nil {
|
||||
s.Err = event.Err
|
||||
log.Printf("Error in health event: %s", s.Err)
|
||||
}
|
||||
h := "?"
|
||||
if s.Health != "" {
|
||||
h = s.Health
|
||||
}
|
||||
if event.Health != "" {
|
||||
h = event.Health
|
||||
}
|
||||
s.Health = h
|
||||
}
|
||||
stream <- StreamEvent{Type: "service", Data: s}
|
||||
case <- ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
func processService(ctx context.Context, info chan <- DeployMsg, cl *dockerClient.Client, s ServiceState, decoder *json.Decoder, writer *io.PipeWriter, stream chan <- StreamEvent) {
|
||||
events := make (chan Event, 50)
|
||||
progressProducer(ctx, cl, s, writer, events)
|
||||
statusProducer(ctx, decoder, s, events)
|
||||
healthProducer(ctx, cl, s, events)
|
||||
|
||||
go processEvent(ctx, events, info, s, stream)
|
||||
}
|
||||
|
||||
func WaitOnServices(pctx context.Context, cl *dockerClient.Client, services []ui.ServiceMeta, filters filters.Args, stream chan <- StreamEvent) {
|
||||
ctx, cancel := context.WithCancel(pctx)
|
||||
defer cancel()
|
||||
log.Printf("What???")
|
||||
ds := DeployState{
|
||||
Count: 0,
|
||||
Total: len(services),
|
||||
Failed: false,
|
||||
Quit: false,
|
||||
}
|
||||
info := make (chan DeployMsg, 50)
|
||||
for _, service := range services {
|
||||
r, w := io.Pipe()
|
||||
d := json.NewDecoder(r)
|
||||
s := ServiceState{
|
||||
Name: service.Name,
|
||||
Id: service.ID,
|
||||
Retries: -1,
|
||||
Health: "?",
|
||||
}
|
||||
log.Printf("Processing Service: %s", s.Id)
|
||||
processService(ctx, info, cl, s, d, w, stream)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case msg := <-info:
|
||||
switch msg {
|
||||
case FailMsg:
|
||||
ds.Failed = true
|
||||
case CompleteMsg:
|
||||
ds.Count += 1
|
||||
if ds.complete() {
|
||||
log.Printf("deploy completed")
|
||||
cancel()
|
||||
}
|
||||
case QuitMsg:
|
||||
ds.Quit = true
|
||||
cancel()
|
||||
}
|
||||
case <-ctx.Done():
|
||||
log.Printf("%v", ds)
|
||||
stream <- StreamEvent{Type: "done", Data: ds}
|
||||
log.Printf("Context finished because: %s", ctx.Err())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package status
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
containerTypes "github.com/docker/docker/api/types/container"
|
||||
)
|
||||
func StreamLogs(
|
||||
ctx context.Context,
|
||||
cl *dockerClient.Client,
|
||||
services []swarm.Service,
|
||||
logCh chan <- string,
|
||||
) error {
|
||||
waitCh := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
for _, service := range services {
|
||||
wg.Add(1)
|
||||
go func(serviceID string) {
|
||||
defer wg.Done()
|
||||
tail := "50"
|
||||
|
||||
logs, err := cl.ServiceLogs(ctx, serviceID, containerTypes.LogsOptions{
|
||||
ShowStderr: true,
|
||||
ShowStdout: true,
|
||||
Until: "",
|
||||
Timestamps: true,
|
||||
Follow: true,
|
||||
Tail: tail,
|
||||
Details: false,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
buf := bufio.NewScanner(logs)
|
||||
for buf.Scan() {
|
||||
select {
|
||||
case <- ctx.Done():
|
||||
return
|
||||
default:
|
||||
line := fmt.Sprintf("%s: %s", service.Spec.Name, buf.Text())
|
||||
logCh <- line
|
||||
}
|
||||
}
|
||||
logs.Close()
|
||||
return
|
||||
}
|
||||
}(service.ID)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(waitCh)
|
||||
close(logCh)
|
||||
return nil
|
||||
}
|
||||
@ -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)
|
||||
// }
|
||||
@ -1,16 +0,0 @@
|
||||
package cli
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"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()
|
||||
if err != nil {
|
||||
log.Printf("Error: ", string(output))
|
||||
InternalServerErrorHandler(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
3
go.mod
3
go.mod
@ -5,9 +5,7 @@ go 1.25.6
|
||||
require (
|
||||
coopcloud.tech/abra v0.0.0-20260305102834-9d401202b4fb
|
||||
github.com/charmbracelet/log v0.4.2
|
||||
github.com/docker/cli v28.4.0+incompatible
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -41,6 +39,7 @@ require (
|
||||
github.com/cyphar/filepath-securejoin v0.5.0 // indirect
|
||||
github.com/decentral1se/passgen v1.0.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v28.4.0+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
|
||||
1
go.sum
1
go.sum
@ -253,7 +253,6 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
|
||||
@ -14,7 +14,7 @@ var (
|
||||
DontWaitConverge bool
|
||||
Dry bool
|
||||
Force bool
|
||||
Secrets bool
|
||||
MachineReadable bool
|
||||
Major bool
|
||||
Minor bool
|
||||
NoDomainChecks bool
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
export PATH=/home/node/.local/bin/:$PATH
|
||||
|
||||
/home/node/wizard/gobackend &
|
||||
|
||||
npm run dev -- --host
|
||||
2
web
2
web
Submodule web updated: e09aa9a1ca...08510b136e
Reference in New Issue
Block a user