forked from toolshed/coop-cloud-backend
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82269195ed | |||
| 39b4e323dd | |||
| da0863fda7 | |||
| fbf5b90cf8 | |||
| fb72298f71 | |||
| d753bf873d | |||
| 3fbb283624 | |||
| 8e55df779e | |||
| 23775a9221 | |||
| d088a4be7b | |||
| 131161b262 | |||
| cf55b7908f | |||
| 4f2880d0df |
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
dot_abra/
|
||||||
|
ssh_config
|
||||||
|
id_*
|
||||||
|
app
|
||||||
|
coop-cloud-backend
|
||||||
20
Dockerfile
20
Dockerfile
@ -7,24 +7,22 @@ FROM node
|
|||||||
RUN mkdir /home/node/wizard
|
RUN mkdir /home/node/wizard
|
||||||
COPY --from=0 /backend/gobackend /home/node/wizard/gobackend
|
COPY --from=0 /backend/gobackend /home/node/wizard/gobackend
|
||||||
|
|
||||||
COPY --parents web /home/node/wizard
|
|
||||||
WORKDIR /home/node/wizard/web
|
|
||||||
RUN npm install .
|
|
||||||
|
|
||||||
USER node
|
USER node
|
||||||
RUN curl https://install.abra.coopcloud.tech | bash
|
COPY ./command.sh /
|
||||||
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 ssh_config /home/node/.ssh/config
|
||||||
COPY id_ed25519_* /home/node/.ssh/
|
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
|
USER root
|
||||||
|
RUN npm install .
|
||||||
RUN chown -R node /home/node/
|
RUN chown -R node /home/node/
|
||||||
USER node
|
USER node
|
||||||
|
|
||||||
EXPOSE 5173 3000
|
EXPOSE 5173 3000
|
||||||
# ENV VITE_API_URL=http://localhost:3000/api
|
# ENV VITE_API_URL=http://localhost:3000/api
|
||||||
CMD [ "/start.sh" ]
|
CMD [ "/command.sh" ]
|
||||||
|
|||||||
40
README.md
40
README.md
@ -2,13 +2,47 @@
|
|||||||
A Go service that exposes RESTful API endpoints to manage Abra programmatically.
|
A Go service that exposes RESTful API endpoints to manage Abra programmatically.
|
||||||
Integrates with https://git.coopcloud.tech/BornDeleuze/coop-cloud-front.
|
Integrates with https://git.coopcloud.tech/BornDeleuze/coop-cloud-front.
|
||||||
|
|
||||||
## Getting started
|
## Starting the service with Docker
|
||||||
|
Build the container:
|
||||||
|
```bash
|
||||||
|
docker build -t abra-wizard:latest
|
||||||
|
```
|
||||||
|
|
||||||
- Edit the front-end application to turn off mock mode in `src/routes/Authenticated/App.tsx` and `src/routes/Authenticated/Apps.tsx`.
|
Run the container:
|
||||||
|
```bash
|
||||||
|
docker run abra-wizard
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting started with development
|
||||||
|
- Clone the front end application `git clone https://git.coopcloud.tech/BornDeleuze/coop-cloud-front.git`
|
||||||
|
- Checkout the `dev-nomock` branch `cd coop-cloud-front && git checkout dev-nomock`
|
||||||
- Launch the front-end application `npm run dev`
|
- Launch the front-end application `npm run dev`
|
||||||
- Start this Go app `go run .`
|
- Start this Go app `go run .`
|
||||||
- Navigate to the React App (http://localhost:5173)
|
- Navigate to the React App (http://localhost:5173)
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> This is an extremely early prototype, only viewing apps is supported
|
> This is an extremely early prototype, only viewing/deploying apps is supported
|
||||||
> and may fail for your local machine
|
> 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 | |
|
||||||
|
|||||||
464
api/api.go
464
api/api.go
@ -1,439 +1,143 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"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"
|
"net/http"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
|
||||||
|
|
||||||
"coop-cloud-backend/internal"
|
"coop-cloud-backend/internal"
|
||||||
)
|
)
|
||||||
|
// Upgrader is used to upgrade HTTP connections to WebSocket connections.
|
||||||
type abraHandler struct {
|
var upgrader = websocket.Upgrader{
|
||||||
mux *http.ServeMux
|
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 {
|
func newAbraHandler() *abraHandler {
|
||||||
h := &abraHandler{
|
h := &abraHandler{
|
||||||
mux: http.NewServeMux(),
|
mux: http.NewServeMux(),
|
||||||
}
|
}
|
||||||
h.mux.HandleFunc("/api/abra/apps", func(w http.ResponseWriter, r *http.Request) {
|
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-Methods", "GET, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
||||||
if r.Method == http.MethodOptions {
|
if r.Method == http.MethodOptions {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
h.handleListApps(w, r)
|
h.handleListApps(w, r)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
h.mux.HandleFunc("/api/abra/apps/{serverId}/{appId}/deploy", func(w http.ResponseWriter, r *http.Request) {
|
h.mux.HandleFunc("/api/abra/apps/{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-Methods", "POST, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
||||||
if r.Method == http.MethodOptions {
|
if r.Method == http.MethodOptions {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if r.Header.Get("Chaos") == "true"{
|
||||||
switch r.Method {
|
internal.Chaos = true
|
||||||
|
} else {
|
||||||
|
internal.Chaos = false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method{
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
h.handleDeployApp(w, r, r.PathValue("appId"), r.PathValue("serverId"))
|
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:
|
default:
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
h.mux.HandleFunc("/api/abra/servers", func(w http.ResponseWriter, r *http.Request) {
|
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-Methods", "GET, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
||||||
if r.Method == http.MethodOptions {
|
if r.Method == http.MethodOptions {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
h.handleListServers(w, r)
|
h.handleListServers(w, r)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
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
|
return h
|
||||||
}
|
}
|
||||||
func StartAPI() {
|
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()
|
h := newAbraHandler()
|
||||||
log.Println("Server started on port 3000")
|
fmt.Println("Server started on port 3000")
|
||||||
log.Fatal(http.ListenAndServe(":3000", h))
|
http.ListenAndServe(":3000", h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *abraHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *abraHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// Pre-processing: logging
|
// Pre-processing: logging
|
||||||
log.Printf("Incoming %s request: %s\n", r.Method, r.URL.Path)
|
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
|
// Delegate to internal mux
|
||||||
h.mux.ServeHTTP(w, r)
|
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()
|
|
||||||
}
|
|
||||||
|
|||||||
32
api/catalogue.go
Normal file
32
api/catalogue.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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
Normal file
299
api/deploy.go
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
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
Normal file
153
api/list.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@ -28,4 +28,23 @@ type ServerApps struct {
|
|||||||
UnversionedCount int `json:"unversionedCount"`
|
UnversionedCount int `json:"unversionedCount"`
|
||||||
LatestCount int `json:"latestCount"`
|
LatestCount int `json:"latestCount"`
|
||||||
UpgradeCount int `json:"upgradeCount"`
|
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
Normal file
32
api/new.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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
api/undeploy.go
Normal file
66
api/undeploy.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
33
api/wrappers.go
Normal file
33
api/wrappers.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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"
|
// "github.com/spf13/cobra"
|
||||||
|
|
||||||
// "coopcloud.tech/abra/pkg/log"
|
// "coopcloud.tech/abra/pkg/log"
|
||||||
"coop-cloud-backend/api"
|
"coop-cloud-backend/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getEnv reads env variables from docker services.
|
// getEnv reads env variables from docker services.
|
||||||
@ -49,7 +49,7 @@ import (
|
|||||||
// return envMap, nil
|
// return envMap, nil
|
||||||
// }
|
// }
|
||||||
func printApp(app appPkg.App) error {
|
func printApp(app appPkg.App) error {
|
||||||
log.Printf("Name: %s | Domain: %s | Server: %s | Path: %s",
|
fmt.Printf("Name: %s | Domain: %s | Server: %s | Path: %s",
|
||||||
app.Name, app.Domain, app.Server, app.Path)
|
app.Name, app.Domain, app.Server, app.Path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -64,13 +64,13 @@ func connectToContainer(app appPkg.App, serviceName string) {
|
|||||||
filters := filters.NewArgs()
|
filters := filters.NewArgs()
|
||||||
filters.Add("name", stackAndServiceName)
|
filters.Add("name", stackAndServiceName)
|
||||||
|
|
||||||
log.Printf("HI?")
|
fmt.Printf("HI?")
|
||||||
|
|
||||||
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
|
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
log.Printf("Container ID: %s", targetContainer.ID)
|
fmt.Printf("Container ID: %s", targetContainer.ID)
|
||||||
}
|
}
|
||||||
func main() {
|
func main() {
|
||||||
appNames, err := appPkg.GetAppNames()
|
appNames, err := appPkg.GetAppNames()
|
||||||
@ -79,8 +79,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
for _, appName := range appNames {
|
for _, appName := range appNames {
|
||||||
val := fmt.Sprintf("app , %v", appName)
|
val := fmt.Sprintf("app , %v", appName)
|
||||||
log.Println(val)
|
fmt.Println(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
api.StartAPI()
|
cli.StartAPI()
|
||||||
}
|
}
|
||||||
|
|||||||
180
cli/api.go
Normal file
180
cli/api.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
31
cli/catalogue.go
Normal file
31
cli/catalogue.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
75
cli/config.go
Normal file
75
cli/config.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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
Normal file
139
cli/deploy.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
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
Normal file
13
cli/error.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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
Normal file
192
cli/list.go
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
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
Normal file
86
cli/logs.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
cli/models.go
Normal file
82
cli/models.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
// Represents an App, follows the format of
|
||||||
|
// https://git.coopcloud.tech/BornDeleuze/coop-cloud-front
|
||||||
|
type AbraApp struct {
|
||||||
|
Server string `json:"server"`
|
||||||
|
Recipe string `json:"recipe`
|
||||||
|
AppName string `json:"appName"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Chaos string `json:"chaos"`
|
||||||
|
ChaosVersion string `json:"chaosVersion"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Upgrade string `json:"upgrade"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AbraServer struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerAppsResponse map[string]ServerApps
|
||||||
|
|
||||||
|
type ServerApps struct {
|
||||||
|
Apps []AbraApp `json:"apps"`
|
||||||
|
AppCount int `json:"appCount"`
|
||||||
|
VersionCount int `json:"versionCount"`
|
||||||
|
UnversionedCount int `json:"unversionedCount"`
|
||||||
|
LatestCount int `json:"latestCount"`
|
||||||
|
UpgradeCount int `json:"upgradeCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeployState struct {
|
||||||
|
AppName string `json:"appName"`
|
||||||
|
Streams []DeployStream `json:"streams"`
|
||||||
|
Logs []string `json:"logs"`
|
||||||
|
Failed bool `json:"failed"`
|
||||||
|
TimedOut bool `json:"timedOut"`
|
||||||
|
Quit bool `json:"quit"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeployStream struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
id string `json:"id"`
|
||||||
|
status string `json:"status"`
|
||||||
|
retries int `json:"retries"`
|
||||||
|
health string `json:"health"`
|
||||||
|
rollback bool `json:"rollback"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
Normal file
115
cli/new.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
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
Normal file
48
cli/ping.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
16
cli/remove.go
Normal file
16
cli/remove.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
62
cli/secret.go
Normal file
62
cli/secret.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
305
cli/status/events.go
Normal file
305
cli/status/events.go
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
cli/status/logs.go
Normal file
57
cli/status/logs.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
41
cli/status/ws.txt
Normal file
41
cli/status/ws.txt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// 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)
|
||||||
|
// }
|
||||||
16
cli/undeploy.go
Normal file
16
cli/undeploy.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
export PATH=/home/node/.local/bin/:$PATH
|
||||||
|
|
||||||
/home/node/wizard/gobackend &
|
/home/node/wizard/gobackend &
|
||||||
|
|
||||||
npm run dev -- --host
|
npm run dev -- --host
|
||||||
3
go.mod
3
go.mod
@ -5,7 +5,9 @@ go 1.25.6
|
|||||||
require (
|
require (
|
||||||
coopcloud.tech/abra v0.0.0-20260305102834-9d401202b4fb
|
coopcloud.tech/abra v0.0.0-20260305102834-9d401202b4fb
|
||||||
github.com/charmbracelet/log v0.4.2
|
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/docker/docker v28.5.2+incompatible
|
||||||
|
github.com/gorilla/websocket v1.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -39,7 +41,6 @@ require (
|
|||||||
github.com/cyphar/filepath-securejoin v0.5.0 // indirect
|
github.com/cyphar/filepath-securejoin v0.5.0 // indirect
|
||||||
github.com/decentral1se/passgen v1.0.1 // indirect
|
github.com/decentral1se/passgen v1.0.1 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // 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/distribution v2.8.3+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||||
|
|||||||
1
go.sum
1
go.sum
@ -253,6 +253,7 @@ 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.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 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
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/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-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=
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
|||||||
@ -14,7 +14,7 @@ var (
|
|||||||
DontWaitConverge bool
|
DontWaitConverge bool
|
||||||
Dry bool
|
Dry bool
|
||||||
Force bool
|
Force bool
|
||||||
MachineReadable bool
|
Secrets bool
|
||||||
Major bool
|
Major bool
|
||||||
Minor bool
|
Minor bool
|
||||||
NoDomainChecks bool
|
NoDomainChecks bool
|
||||||
|
|||||||
2
web
2
web
Submodule web updated: 08510b136e...e09aa9a1ca
Reference in New Issue
Block a user