forked from toolshed/coop-cloud-backend
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd45bcf5c5 | |||
| 8423cf5f69 | |||
| 62be5ddda7 | |||
| 3a88c8d878 | |||
| 25d01298e5 | |||
| fd53e28143 | |||
| c5e773a694 | |||
| cd47863622 | |||
| 3775894a5b |
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +0,0 @@
|
|||||||
dot_abra/
|
|
||||||
ssh_config
|
|
||||||
id_*
|
|
||||||
app
|
|
||||||
coop-cloud-backend
|
|
||||||
24
Dockerfile
24
Dockerfile
@ -7,22 +7,24 @@ FROM node
|
|||||||
RUN mkdir /home/node/wizard
|
RUN mkdir /home/node/wizard
|
||||||
COPY --from=0 /backend/gobackend /home/node/wizard/gobackend
|
COPY --from=0 /backend/gobackend /home/node/wizard/gobackend
|
||||||
|
|
||||||
USER node
|
|
||||||
COPY ./command.sh /
|
|
||||||
COPY ssh_config /home/node/.ssh/config
|
|
||||||
COPY id_ed25519_* /home/node/.ssh/
|
|
||||||
COPY dot_abra /home/node/.abra/
|
|
||||||
RUN curl https://install.abra.coopcloud.tech | bash
|
|
||||||
ENV ABRA_BIN=/home/node/.local/bin/abra
|
|
||||||
# RUN $ABRA_BIN recipe fetch -a
|
|
||||||
|
|
||||||
COPY --parents web /home/node/wizard
|
COPY --parents web /home/node/wizard
|
||||||
WORKDIR /home/node/wizard/web
|
WORKDIR /home/node/wizard/web
|
||||||
USER root
|
|
||||||
RUN npm install .
|
RUN npm install .
|
||||||
|
|
||||||
|
USER node
|
||||||
|
RUN curl https://install.abra.coopcloud.tech | bash
|
||||||
|
ENV ABRA_BIN=/home/node/.local/bin/abra
|
||||||
|
RUN $ABRA_BIN recipe fetch -a
|
||||||
|
|
||||||
|
COPY ./start.sh /
|
||||||
|
COPY dot_abra /home/node/.abra/
|
||||||
|
COPY ssh_config /home/node/.ssh/config
|
||||||
|
COPY id_ed25519_* /home/node/.ssh/
|
||||||
|
|
||||||
|
USER root
|
||||||
RUN chown -R node /home/node/
|
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 [ "/command.sh" ]
|
CMD [ "/start.sh" ]
|
||||||
|
|||||||
40
README.md
40
README.md
@ -2,47 +2,13 @@
|
|||||||
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.
|
||||||
|
|
||||||
## Starting the service with Docker
|
## Getting started
|
||||||
Build the container:
|
|
||||||
```bash
|
|
||||||
docker build -t abra-wizard:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the container:
|
- Edit the front-end application to turn off mock mode in `src/routes/Authenticated/App.tsx` and `src/routes/Authenticated/Apps.tsx`.
|
||||||
```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/deploying apps is supported
|
> This is an extremely early prototype, only viewing 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 | |
|
|
||||||
|
|||||||
462
api/api.go
462
api/api.go
@ -1,143 +1,439 @@
|
|||||||
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.
|
|
||||||
var upgrader = websocket.Upgrader{
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Upgrade the HTTP connection to a WebSocket connection
|
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error upgrading:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
// Listen for incoming messages
|
|
||||||
for {
|
|
||||||
// Read message from the client
|
|
||||||
_, message, err := conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error reading message:", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fmt.Printf("Received: %s\\n", message)
|
|
||||||
// Echo the message back to the client
|
|
||||||
if err := conn.WriteMessage(websocket.TextMessage, message); err != nil {
|
|
||||||
fmt.Println("Error writing message:", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type abraHandler struct{
|
type abraHandler struct {
|
||||||
mux *http.ServeMux
|
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/{appId}/deploy", func(w http.ResponseWriter, r *http.Request) {
|
h.mux.HandleFunc("/api/abra/apps/{serverId}/{appId}/deploy", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
w.Header().Set("Access-Control-Allow-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"{
|
|
||||||
internal.Chaos = true
|
switch r.Method {
|
||||||
} else {
|
|
||||||
internal.Chaos = false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.Method{
|
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
h.handleDeployApp(w, r, r.PathValue("appId"))
|
h.handleDeployApp(w, r, r.PathValue("appId"), r.PathValue("serverId"))
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
h.mux.HandleFunc("/api/abra/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()
|
||||||
fmt.Println("Server started on port 3000")
|
log.Println("Server started on port 3000")
|
||||||
http.ListenAndServe(":3000", h)
|
log.Fatal(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()
|
||||||
|
}
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
package api
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"encoding/json"
|
|
||||||
"coopcloud.tech/abra/pkg/client"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
|
||||||
|
|
||||||
"coop-cloud-backend/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *abraHandler) handleListCatalogue(w http.ResponseWriter, r *http.Request) {
|
|
||||||
catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
recipes := catalogue.Flatten()
|
|
||||||
|
|
||||||
jsonBytes, err := json.Marshal(recipes)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("JSON conversion failed: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("JSON conversion failed: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
299
api/deploy.go
299
api/deploy.go
@ -1,299 +0,0 @@
|
|||||||
package api
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"encoding/json"
|
|
||||||
"coopcloud.tech/abra/pkg/client"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/ui"
|
|
||||||
|
|
||||||
appPkg "coopcloud.tech/abra/pkg/app"
|
|
||||||
"coopcloud.tech/abra/pkg/deploy"
|
|
||||||
|
|
||||||
"net/http"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
|
|
||||||
"coop-cloud-backend/internal"
|
|
||||||
)
|
|
||||||
func (h *abraHandler) handleDeployApp(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
log.Printf("App Id: %s", appName)
|
|
||||||
app, err := GetApp(appName)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cl, err := client.New(app.Server)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error Connecting to Docker client: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error Connecting to Docker client: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error checking deploy status: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error checking deploy status: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if deployMeta.IsDeployed {
|
|
||||||
log.Printf("App already deployed\n")
|
|
||||||
http.Error(w, "App already deployed\n", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// logic differs from CLI, we only want to take either
|
|
||||||
// 1. the chaos version
|
|
||||||
// 2. TODO: the version in the .env file
|
|
||||||
// 3. the latest version
|
|
||||||
// we never take: specific CLI verison (maybe will support this) or the deployed version
|
|
||||||
internal.Chaos = false
|
|
||||||
toDeployVersion, err := getDeployVersion(deployMeta, app)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateSecrets(cl, app); err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stackName := app.StackName()
|
|
||||||
deployOpts := stack.Deploy{
|
|
||||||
Composefiles: composeFiles,
|
|
||||||
Namespace: stackName,
|
|
||||||
Prune: false,
|
|
||||||
ResolveImage: stack.ResolveImageAlways,
|
|
||||||
Detach: false,
|
|
||||||
}
|
|
||||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
|
|
||||||
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
|
|
||||||
if internal.Chaos {
|
|
||||||
appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
versionLabel := toDeployVersion
|
|
||||||
if internal.Chaos {
|
|
||||||
for _, service := range compose.Services {
|
|
||||||
if service.Name == "app" {
|
|
||||||
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
|
|
||||||
// NOTE(d1): keep non-chaos version labbeling when doing chaos ops
|
|
||||||
versionLabel = service.Deploy.Labels[labelKey]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
appPkg.SetVersionLabel(compose, stackName, versionLabel)
|
|
||||||
|
|
||||||
newRecipeWithDeployVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, toDeployVersion)
|
|
||||||
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDeployVersion)
|
|
||||||
|
|
||||||
envVars, err := appPkg.CheckEnv(app)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// doesn't really get used at all right now
|
|
||||||
deployWarnMessages := []string{}
|
|
||||||
for _, envVar := range envVars {
|
|
||||||
if !envVar.Present {
|
|
||||||
deployWarnMessages = append(deployWarnMessages,
|
|
||||||
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//skipping domain checks like crazy
|
|
||||||
//commented code is to show deploy overview before deploy
|
|
||||||
|
|
||||||
/*
|
|
||||||
deployedVersion := config.MISSING_DEFAULT
|
|
||||||
if deployMeta.IsDeployed {
|
|
||||||
deployedVersion = deployMeta.Version
|
|
||||||
if deployMeta.IsChaos {
|
|
||||||
deployedVersion = deployMeta.ChaosVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ShowUnchanged := false
|
|
||||||
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, ShowUnchanged)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gather configs
|
|
||||||
configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, ShowUnchanged)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gather images
|
|
||||||
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, ShowUnchanged)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}*/
|
|
||||||
|
|
||||||
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := app.Filters(true, false, serviceNames...)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// in future allow user input?
|
|
||||||
DontWaitConverge := true
|
|
||||||
NoInput := true
|
|
||||||
if err := stack.RunDeploy(
|
|
||||||
cl,
|
|
||||||
deployOpts,
|
|
||||||
compose,
|
|
||||||
app.Name,
|
|
||||||
app.Server,
|
|
||||||
DontWaitConverge,
|
|
||||||
NoInput,
|
|
||||||
f,
|
|
||||||
); err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Implement my own WaitOnServices in order to wrap ui.Model in order to emit JSON updates
|
|
||||||
// over Websocket connection
|
|
||||||
var serviceIDs []ui.ServiceMeta
|
|
||||||
namespace := convert.NewNamespace(deployOpts.Namespace)
|
|
||||||
|
|
||||||
existingServices, err := stack.GetStackServices(context.Background(), cl, namespace.Name())
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, service := range existingServices {
|
|
||||||
serviceIDs = append(serviceIDs, ui.ServiceMeta{
|
|
||||||
Name: service.Spec.Name,
|
|
||||||
ID: service.ID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
waitOpts := WaitOpts{
|
|
||||||
Services: serviceIDs,
|
|
||||||
AppName: app.Name,
|
|
||||||
ServerName: app.Server,
|
|
||||||
NoInput: NoInput,
|
|
||||||
Filters: f,
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
type StateEmitter interface {
|
|
||||||
Emit(DeployState)
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebsocketEmitter struct {
|
|
||||||
conn *websocket.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WebsocketEmitter) Emit(state DeployState) {
|
|
||||||
b, _ := json.Marshal(state)
|
|
||||||
w.conn.WriteMessage(websocket.TextMessage, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
type WrappedModel struct {
|
|
||||||
inner tea.Model
|
|
||||||
emitter StateEmitter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m WrappedModel) Init() tea.Cmd {
|
|
||||||
return m.inner.Init()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m WrappedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
innerState, cmds := m.inner.Update(msg)
|
|
||||||
|
|
||||||
m.inner = innerState
|
|
||||||
|
|
||||||
m.emitter.Emit(s.State())
|
|
||||||
|
|
||||||
return m, cmds
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m WrappedModel) View() string {
|
|
||||||
return m.inner.View()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ui.Model) State() DeployState {
|
|
||||||
var streams []DeployStream
|
|
||||||
if m.Streams != nil {
|
|
||||||
for _, s := range *m.Streams {
|
|
||||||
streams = append(streams, DeployStream{
|
|
||||||
Name: s.Name,
|
|
||||||
id: s.id,
|
|
||||||
status: s.status,
|
|
||||||
retries: s.retries,
|
|
||||||
health: s.health,
|
|
||||||
rollback: s.rollback,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var logs []string
|
|
||||||
if m.Logs != nil {
|
|
||||||
logs = *m.Logs
|
|
||||||
}
|
|
||||||
return DeployState{
|
|
||||||
AppName: m.appName,
|
|
||||||
Streams: streams,
|
|
||||||
Logs: logs,
|
|
||||||
Failed: m.Failed,
|
|
||||||
TimedOut: m.TimedOut,
|
|
||||||
Quit: m.Quit,
|
|
||||||
Count: m.count,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
153
api/list.go
153
api/list.go
@ -1,153 +0,0 @@
|
|||||||
package api
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"encoding/json"
|
|
||||||
appPkg "coopcloud.tech/abra/pkg/app"
|
|
||||||
"coop-cloud-backend/internal"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *abraHandler) handleListApps(w http.ResponseWriter, r *http.Request) {
|
|
||||||
appNames, err := GetAppNames()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error getting app names: %s\n", err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// counts the number of apps in a server to initialize slices later
|
|
||||||
serverAppCount := make(map[string]int)
|
|
||||||
|
|
||||||
remoteApps := make([]appPkg.App, 0, len(appNames))
|
|
||||||
for _, appName := range appNames {
|
|
||||||
remoteApp, err := GetApp(appName)
|
|
||||||
serverAppCount[remoteApp.Server] += 1
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error getting app %s: %s\n", appName, err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
remoteApps = append(remoteApps, remoteApp)
|
|
||||||
}
|
|
||||||
|
|
||||||
appStatuses, err := GetAppStatuses(remoteApps)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
abraAppResponse := ServerAppsResponse{}
|
|
||||||
for _, app := range remoteApps {
|
|
||||||
serverApps, ok := abraAppResponse[app.Server]
|
|
||||||
if !ok { // create the slice
|
|
||||||
// ever other field initializes to 0
|
|
||||||
serverApps = ServerApps{
|
|
||||||
Apps: make([]AbraApp, 0, serverAppCount[app.Server]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
appInfo, ok := appStatuses[app.StackName()]
|
|
||||||
if ok {
|
|
||||||
log.Printf("app %s is deployed\n", app.Name)
|
|
||||||
serverApps.Apps = append(serverApps.Apps, appTranspose(app, appInfo))
|
|
||||||
serverApps.AppCount += 1
|
|
||||||
// assume these are true rn idk
|
|
||||||
serverApps.VersionCount += 1
|
|
||||||
serverApps.LatestCount += 1
|
|
||||||
} else {
|
|
||||||
log.Printf("app %s is undeployed\n", app.Name)
|
|
||||||
appT, err := appTransposeUndeployed(app)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("app transpose failed: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("app transpose failed: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
serverApps.Apps = append(serverApps.Apps, appT)
|
|
||||||
serverApps.AppCount += 1
|
|
||||||
// assume these are true rn idk
|
|
||||||
serverApps.VersionCount += 1
|
|
||||||
serverApps.LatestCount += 1
|
|
||||||
}
|
|
||||||
abraAppResponse[app.Server] = serverApps
|
|
||||||
}
|
|
||||||
jsonBytes, err := json.Marshal(abraAppResponse)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("JSON conversion failed: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("JSON conversion failed: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(jsonBytes)
|
|
||||||
}
|
|
||||||
func appTransposeUndeployed(app appPkg.App) (AbraApp, error) {
|
|
||||||
config, err := app.Recipe.GetComposeConfig(app.Env)
|
|
||||||
if err != nil {
|
|
||||||
return AbraApp{}, err
|
|
||||||
}
|
|
||||||
version := GetLabel(config, app.StackName(), "version")
|
|
||||||
if version == "" {
|
|
||||||
version = "unknown"
|
|
||||||
}
|
|
||||||
return AbraApp{
|
|
||||||
AppName: app.Name,
|
|
||||||
Server: app.Server,
|
|
||||||
Recipe: app.Recipe.Name,
|
|
||||||
Domain: app.Domain,
|
|
||||||
Chaos: "false",
|
|
||||||
Status: "undeployed",
|
|
||||||
ChaosVersion: "unknown",
|
|
||||||
Version: version,
|
|
||||||
Upgrade: "latest",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
func appTranspose(app appPkg.App, psInfo map[string]string) AbraApp {
|
|
||||||
return AbraApp{
|
|
||||||
AppName: app.Name,
|
|
||||||
Server: app.Server,
|
|
||||||
Recipe: app.Recipe.Name,
|
|
||||||
Domain: app.Domain,
|
|
||||||
Chaos: psInfo["chaos"],
|
|
||||||
Status: psInfo["status"],
|
|
||||||
ChaosVersion: getOrDefault(psInfo, "chaosVersion", "unknown"),
|
|
||||||
Version: psInfo["version"],
|
|
||||||
Upgrade: "latest",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOrDefault(m map[string]string, key, def string) string {
|
|
||||||
if v, ok := m[key]; ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *abraHandler) handleListServers (w http.ResponseWriter, r *http.Request) {
|
|
||||||
servers, err := GetServers()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
serverNames, err := ReadServerNames()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
abraServers := make([]AbraServer, 0, len(servers))
|
|
||||||
for i := range len(servers) {
|
|
||||||
abraServers = append(abraServers,
|
|
||||||
AbraServer{
|
|
||||||
Name: serverNames[i],
|
|
||||||
Host: servers[i],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
jsonBytes, err := json.Marshal(abraServers)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(jsonBytes)
|
|
||||||
}
|
|
||||||
@ -28,23 +28,4 @@ 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
32
api/new.go
@ -1,32 +0,0 @@
|
|||||||
package api
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"encoding/json"
|
|
||||||
"coopcloud.tech/abra/pkg/client"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
|
||||||
|
|
||||||
"coop-cloud-backend/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
recipes := catalogue.Flatten()
|
|
||||||
|
|
||||||
jsonBytes, err := json.Marshal(recipes)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("JSON conversion failed: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("JSON conversion failed: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
package api
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"coopcloud.tech/abra/pkg/client"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
func (h *abraHandler) handleUndeployApp(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
log.Printf("App Id: %s", appName)
|
|
||||||
app, err := GetApp(appName)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error getting app %s: %s\n", appName, err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// think this just checks to make sure we have the recipe for this app
|
|
||||||
if err := app.Recipe.EnsureExists(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !deployMeta.IsDeployed {
|
|
||||||
log.Fatal("%s is not deployed?", app.Name)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
version := deployMeta.Version
|
|
||||||
if deployMeta.IsChaos {
|
|
||||||
version = deployMeta.ChaosVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
rmOpts := stack.Remove{
|
|
||||||
Namespaces: []string{app.StackName()},
|
|
||||||
Detach: false,
|
|
||||||
}
|
|
||||||
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.WriteRecipeVersion(version, false); err != nil {
|
|
||||||
log.Printf("writing recipe version failed: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("writing recipe version failed: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package api
|
|
||||||
import (
|
|
||||||
appPkg "coopcloud.tech/abra/pkg/app"
|
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
|
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
|
||||||
)
|
|
||||||
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
|
|
||||||
return appPkg.GetLabel(compose, stackName, label)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAppStatuses(apps []appPkg.App) (map[string]map[string]string, error) {
|
|
||||||
return appPkg.GetAppStatuses(apps, true)
|
|
||||||
}
|
|
||||||
func GetApp(appName string) (appPkg.App, error) {
|
|
||||||
return appPkg.Get(appName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAppNames() ([]string, error) {
|
|
||||||
return appPkg.GetAppNames()
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAppServiceNames(appName string) ([]string, error) {
|
|
||||||
return appPkg.GetAppServiceNames(appName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetServers() ([]string, error) {
|
|
||||||
return config.GetServers()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadServerNames() ([]string, error) {
|
|
||||||
return config.ReadServerNames()
|
|
||||||
}
|
|
||||||
12
app.go
12
app.go
@ -17,7 +17,7 @@ import (
|
|||||||
// "github.com/spf13/cobra"
|
// "github.com/spf13/cobra"
|
||||||
|
|
||||||
// "coopcloud.tech/abra/pkg/log"
|
// "coopcloud.tech/abra/pkg/log"
|
||||||
"coop-cloud-backend/cli"
|
"coop-cloud-backend/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 {
|
||||||
fmt.Printf("Name: %s | Domain: %s | Server: %s | Path: %s",
|
log.Printf("Name: %s | Domain: %s | Server: %s | Path: %s",
|
||||||
app.Name, app.Domain, app.Server, app.Path)
|
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)
|
||||||
|
|
||||||
fmt.Printf("HI?")
|
log.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)
|
||||||
}
|
}
|
||||||
fmt.Printf("Container ID: %s", targetContainer.ID)
|
log.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)
|
||||||
fmt.Println(val)
|
log.Println(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.StartAPI()
|
api.StartAPI()
|
||||||
}
|
}
|
||||||
|
|||||||
180
cli/api.go
180
cli/api.go
@ -1,180 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
|
|
||||||
"coop-cloud-backend/internal"
|
|
||||||
)
|
|
||||||
// Upgrader is used to upgrade HTTP connections to WebSocket connections.
|
|
||||||
var upgrader = websocket.Upgrader{
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Upgrade the HTTP connection to a WebSocket connection
|
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error upgrading:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
// Listen for incoming messages
|
|
||||||
for {
|
|
||||||
// Read message from the client
|
|
||||||
_, message, err := conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error reading message:", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fmt.Printf("Received: %s\\n", message)
|
|
||||||
// Echo the message back to the client
|
|
||||||
if err := conn.WriteMessage(websocket.TextMessage, message); err != nil {
|
|
||||||
fmt.Println("Error writing message:", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type abraHandler struct{
|
|
||||||
mux *http.ServeMux
|
|
||||||
}
|
|
||||||
func newAbraHandler() *abraHandler {
|
|
||||||
h := &abraHandler{
|
|
||||||
mux: http.NewServeMux(),
|
|
||||||
}
|
|
||||||
h.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("API Path not found")
|
|
||||||
http.Error(w, "API Path not found", http.StatusMethodNotAllowed)
|
|
||||||
})
|
|
||||||
h.mux.HandleFunc("/api/apps", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodGet:
|
|
||||||
h.handleListApps(w, r)
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
h.mux.HandleFunc("/api/apps/{appId}/deploy", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Header.Get("Chaos") == "true"{
|
|
||||||
internal.Chaos = true
|
|
||||||
} else {
|
|
||||||
internal.Chaos = false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.Method{
|
|
||||||
case http.MethodPost:
|
|
||||||
h.handleDeployApp(w, r, r.PathValue("appId"))
|
|
||||||
case http.MethodGet:
|
|
||||||
h.getDeployLogs(w, r, r.PathValue("appId"))
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
h.mux.HandleFunc("/api/apps/{appId}/stop", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method{
|
|
||||||
case http.MethodPost:
|
|
||||||
h.handleUndeployApp(w, r, r.PathValue("appId"))
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
h.mux.HandleFunc("/api/apps/{appId}/remove", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method{
|
|
||||||
case http.MethodPost:
|
|
||||||
h.handleRemoveApp(w, r, r.PathValue("appId"))
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
h.mux.HandleFunc("/api/servers", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodGet:
|
|
||||||
h.handleListServers(w, r)
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
h.mux.HandleFunc("/api/catalogue", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodGet:
|
|
||||||
h.handleListCatalogue(w, r)
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
h.mux.HandleFunc("/api/apps/{appId}/new", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method{
|
|
||||||
case http.MethodPost:
|
|
||||||
h.handleNewApp(w, r, r.PathValue("appId"))
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
h.mux.HandleFunc("/api/apps/{appId}/config", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method{
|
|
||||||
case http.MethodPost:
|
|
||||||
h.handlePostConfig(w, r, r.PathValue("appId"))
|
|
||||||
case http.MethodGet:
|
|
||||||
h.handleGetConfig(w, r, r.PathValue("appId"))
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
h.mux.HandleFunc("/api/apps/{appId}/secret", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method{
|
|
||||||
case http.MethodPost:
|
|
||||||
h.handleInsertAppSecret(w, r, r.PathValue("appId"))
|
|
||||||
case http.MethodGet:
|
|
||||||
h.handleGetAppSecrets(w, r, r.PathValue("appId"))
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
h.mux.HandleFunc("/api/apps/{appId}/services", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method{
|
|
||||||
case http.MethodGet:
|
|
||||||
h.handleGetAppServices(w, r, r.PathValue("appId"))
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
h.mux.HandleFunc("/api/apps/{appId}/{serviceId}/logs", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method{
|
|
||||||
case http.MethodGet:
|
|
||||||
h.handleGetLogs(w, r, r.PathValue("appId"), r.PathValue("serviceId"))
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
func StartAPI() {
|
|
||||||
/*http.HandleFunc("/ws", wsHandler)
|
|
||||||
fmt.Println("WebSocket server started on :3001")
|
|
||||||
err := http.ListenAndServe(":3001", nil)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error starting server:", err)
|
|
||||||
}*/
|
|
||||||
|
|
||||||
h := newAbraHandler()
|
|
||||||
fmt.Println("Server started on port 3000")
|
|
||||||
http.ListenAndServe(":3000", h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *abraHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Pre-processing: logging
|
|
||||||
log.Printf("Incoming %s request: %s\n", r.Method, r.URL.Path)
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
||||||
if r.Method == http.MethodOptions {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Delegate to internal mux
|
|
||||||
h.mux.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"slices"
|
|
||||||
"maps"
|
|
||||||
"encoding/json"
|
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *abraHandler) handleListCatalogue(w http.ResponseWriter, r *http.Request) {
|
|
||||||
offline := false
|
|
||||||
catl, err := recipe.ReadRecipeCatalogue(offline)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
vals := slices.Collect(maps.Values(catl))
|
|
||||||
|
|
||||||
jsonBytes, err := json.Marshal(vals)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"net/http"
|
|
||||||
"encoding/json"
|
|
||||||
appPkg "coopcloud.tech/abra/pkg/app"
|
|
||||||
)
|
|
||||||
type FileResponse struct {
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *abraHandler) handleGetConfig(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
files, err := appPkg.LoadAppFiles("")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
appFile, exists := files[appName]
|
|
||||||
if !exists {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("path: %s | server: %s", appFile.Path, appFile.Server)
|
|
||||||
log.Printf("Ending...")
|
|
||||||
// TODO: sanitize
|
|
||||||
file, err := ioutil.ReadFile(appFile.Path)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp := FileResponse{Content: string(file)}
|
|
||||||
jsonBytes, err := json.Marshal(resp)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *abraHandler) handlePostConfig(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
files, err := appPkg.LoadAppFiles("")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
appFile, exists := files[appName]
|
|
||||||
if !exists {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d := json.NewDecoder(r.Body)
|
|
||||||
d.DisallowUnknownFields() // catch unwanted fields
|
|
||||||
|
|
||||||
var config FileResponse
|
|
||||||
err = d.Decode(&config)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("err: %s", err);
|
|
||||||
http.Error(w, "Failed to read request", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(appFile.Path, []byte(config.Content), 0644)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to write file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
139
cli/deploy.go
139
cli/deploy.go
@ -1,139 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os/exec"
|
|
||||||
"coop-cloud-backend/internal"
|
|
||||||
"coop-cloud-backend/cli/status"
|
|
||||||
"net/http"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/client"
|
|
||||||
//"coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
|
||||||
"coopcloud.tech/abra/pkg/ui"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
|
||||||
"github.com/docker/docker/api/types/swarm"
|
|
||||||
dclient "github.com/docker/docker/client"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
|
|
||||||
|
|
||||||
appPkg "coopcloud.tech/abra/pkg/app"
|
|
||||||
|
|
||||||
//"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
func (h *abraHandler) handleDeployApp(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
log.Printf("Handling App Deploy!")
|
|
||||||
args := []string{"app", "deploy", appName, "-n", "-c"}
|
|
||||||
if internal.Chaos {
|
|
||||||
args = append(args, "-C")
|
|
||||||
}
|
|
||||||
cmd := exec.Command("abra", args...)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: ", string(output))
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("Finishing app deploy!")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
func (h *abraHandler) getDeployLogs(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
log.Printf("Get deploy logs!")
|
|
||||||
app, err := appPkg.Get(appName)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Implement my own WaitOnServices in order to wrap ui.Model in order to emit JSON updates
|
|
||||||
// over Websocket connection
|
|
||||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := app.Filters(true, false, serviceNames...)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP 1: collect information about app being deployed
|
|
||||||
cl, err := client.New(app.Server)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var serviceIDs []ui.ServiceMeta
|
|
||||||
stackName := app.StackName()
|
|
||||||
namespace := convert.NewNamespace(stackName)
|
|
||||||
log.Printf("stack name: %s | namespace: %s", stackName, namespace.Name())
|
|
||||||
existingServices, err := GetStackServices(context.Background(), cl, namespace.Name())
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: %s\n", err)
|
|
||||||
http.Error(w, fmt.Sprintf("Error: %s\n", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, service := range existingServices {
|
|
||||||
log.Printf("existingServices contains: %s", service.Spec.Name)
|
|
||||||
serviceIDs = append(serviceIDs, ui.ServiceMeta{
|
|
||||||
Name: service.Spec.Name,
|
|
||||||
ID: service.ID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
flusher, ok := w.(http.Flusher)
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
|
||||||
w.Header().Set("Connection", "keep-alive")
|
|
||||||
log.Printf("Waiting on service...")
|
|
||||||
|
|
||||||
stream := make (chan status.StreamEvent, 50)
|
|
||||||
go status.WaitOnServices(ctx, cl, serviceIDs, f, stream)
|
|
||||||
for {
|
|
||||||
select{
|
|
||||||
case <- ctx.Done():
|
|
||||||
log.Printf("deploy cancelled or done")
|
|
||||||
return
|
|
||||||
case msg, ok := <- stream:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
test, err := json.MarshalIndent(msg, "", " ")
|
|
||||||
fmt.Printf(string(test), err);
|
|
||||||
b, err := json.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
// TODO: send error through onerror handler
|
|
||||||
log.Printf("error?: %s", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "data: %s\n\n", b)
|
|
||||||
|
|
||||||
flusher.Flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func getStackFilter(namespace string) filters.Args {
|
|
||||||
filter := filters.NewArgs()
|
|
||||||
filter.Add("label", convert.LabelNamespace+"="+namespace)
|
|
||||||
return filter
|
|
||||||
}
|
|
||||||
func getStackServiceFilter(namespace string) filters.Args {
|
|
||||||
return getStackFilter(namespace)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetStackServices(ctx context.Context, dockerclient dclient.APIClient, namespace string) ([]swarm.Service, error) {
|
|
||||||
return dockerclient.ServiceList(ctx, types.ServiceListOptions{Filters: getStackServiceFilter(namespace)})
|
|
||||||
}
|
|
||||||
13
cli/error.go
13
cli/error.go
@ -1,13 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
func InternalServerErrorHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
w.Write([]byte("500 Internal Server Error"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
w.Write([]byte("404 Not Found"))
|
|
||||||
}
|
|
||||||
192
cli/list.go
192
cli/list.go
@ -1,192 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os/exec"
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"sort"
|
|
||||||
appPkg "coopcloud.tech/abra/pkg/app"
|
|
||||||
"coopcloud.tech/tagcmp"
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *abraHandler) handleListApps(w http.ResponseWriter, r *http.Request) {
|
|
||||||
cmd := exec.Command("abra", "app", "ls", "-m")
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: ", err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var resp ServerAppsResponse
|
|
||||||
// no filtering
|
|
||||||
listAppServer := ""
|
|
||||||
recipeFilter := ""
|
|
||||||
|
|
||||||
if err := json.Unmarshal(output, &resp); err != nil {
|
|
||||||
http.Error(w, "invalid JSON from CLI", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
appFiles, err := appPkg.LoadAppFiles(listAppServer)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
apps, err := appPkg.GetApps(appFiles, recipeFilter)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
statuses := make(map[string]map[string]string)
|
|
||||||
alreadySeen := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, app := range apps {
|
|
||||||
if _, ok := alreadySeen[app.Server]; !ok {
|
|
||||||
alreadySeen[app.Server] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
statuses, err = appPkg.GetAppStatuses(apps, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, app := range apps {
|
|
||||||
var stats ServerApps
|
|
||||||
|
|
||||||
stats = resp[app.Server]
|
|
||||||
var appStats *AbraApp
|
|
||||||
for _, sapp := range stats.Apps {
|
|
||||||
if sapp.AppName == app.Name {
|
|
||||||
appStats = &sapp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
status := "unknown"
|
|
||||||
version := "unknown"
|
|
||||||
chaos := "unknown"
|
|
||||||
chaosVersion := "unknown"
|
|
||||||
if statusMeta, ok := statuses[app.StackName()]; ok {
|
|
||||||
if currentVersion, exists := statusMeta["version"]; exists {
|
|
||||||
if currentVersion != "" {
|
|
||||||
version = currentVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if chaosDeploy, exists := statusMeta["chaos"]; exists {
|
|
||||||
chaos = chaosDeploy
|
|
||||||
}
|
|
||||||
if chaosDeployVersion, exists := statusMeta["chaosVersion"]; exists {
|
|
||||||
chaosVersion = chaosDeployVersion
|
|
||||||
}
|
|
||||||
if statusMeta["status"] != "" {
|
|
||||||
status = statusMeta["status"]
|
|
||||||
}
|
|
||||||
stats.VersionCount++
|
|
||||||
} else {
|
|
||||||
stats.UnversionedCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
appStats.Status = status
|
|
||||||
appStats.Chaos = chaos
|
|
||||||
appStats.ChaosVersion = chaosVersion
|
|
||||||
appStats.Version = version
|
|
||||||
localApp := true
|
|
||||||
|
|
||||||
var newUpdates []string
|
|
||||||
if version != "unknown" && chaos == "false" {
|
|
||||||
if err := app.Recipe.EnsureExists(); err != nil {
|
|
||||||
log.Printf("unable to clone %s: %s", app.Name, err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updates, err := app.Recipe.Tags()
|
|
||||||
if err != nil {
|
|
||||||
localApp = false
|
|
||||||
} else {
|
|
||||||
parsedVersion, err := tagcmp.Parse(version)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, update := range updates {
|
|
||||||
if ok := tagcmp.IsParsable(update); !ok {
|
|
||||||
log.Printf("unable to parse %s, skipping as upgrade option", update)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedUpdate, err := tagcmp.Parse(update)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
|
|
||||||
newUpdates = append(newUpdates, update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(newUpdates) == 0 {
|
|
||||||
if version == "unknown" {
|
|
||||||
appStats.Upgrade = "unknown"
|
|
||||||
} else if localApp {
|
|
||||||
appStats.Upgrade = "latest"
|
|
||||||
stats.LatestCount++
|
|
||||||
} else {
|
|
||||||
appStats.Upgrade = "unknown"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newUpdates = SortVersionsDesc(newUpdates)
|
|
||||||
appStats.Upgrade = strings.Join(newUpdates, "\n")
|
|
||||||
stats.UpgradeCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, sapp := range stats.Apps {
|
|
||||||
if sapp.AppName == app.Name {
|
|
||||||
stats.Apps[i] = *appStats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp[app.Server] = stats
|
|
||||||
}
|
|
||||||
jsonBytes, err := json.Marshal(resp)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("JSON conversion failed: %s\n", err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(jsonBytes)
|
|
||||||
}
|
|
||||||
func (h *abraHandler) handleListServers (w http.ResponseWriter, r *http.Request) {
|
|
||||||
cmd := exec.Command("abra", "server", "ls", "-m")
|
|
||||||
abraServers, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: ", err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(abraServers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SortVersionsDesc(versions []string) []string {
|
|
||||||
var tags []tagcmp.Tag
|
|
||||||
|
|
||||||
for _, v := range versions {
|
|
||||||
parsed, _ := tagcmp.Parse(v) // skips unsupported tags
|
|
||||||
tags = append(tags, parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(tagcmp.ByTagDesc(tags))
|
|
||||||
|
|
||||||
var desc []string
|
|
||||||
for _, t := range tags {
|
|
||||||
desc = append(desc, t.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return desc
|
|
||||||
}
|
|
||||||
86
cli/logs.go
86
cli/logs.go
@ -1,86 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
appPkg "coopcloud.tech/abra/pkg/app"
|
|
||||||
"coop-cloud-backend/cli/status"
|
|
||||||
"coopcloud.tech/abra/pkg/client"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
|
|
||||||
)
|
|
||||||
func (h *abraHandler) handleGetLogs(w http.ResponseWriter, r *http.Request, appName string, serviceName string) {
|
|
||||||
app, err := appPkg.Get(appName)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stackName := app.StackName()
|
|
||||||
|
|
||||||
if err := app.Recipe.EnsureExists(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cl, err := client.New(app.Server)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !deployMeta.IsDeployed {
|
|
||||||
log.Printf(fmt.Sprintf("%s is not deployed?", app.Name))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
serviceNames := []string{serviceName}
|
|
||||||
|
|
||||||
f, err := app.Filters(true, false, serviceNames...)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
services, err := cl.ServiceList(
|
|
||||||
context.Background(),
|
|
||||||
types.ServiceListOptions{Filters: f},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
flusher, ok := w.(http.Flusher)
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
|
||||||
w.Header().Set("Connection", "keep-alive")
|
|
||||||
|
|
||||||
logCh := make (chan string, 50)
|
|
||||||
go status.StreamLogs(ctx, cl, services, logCh)
|
|
||||||
for {
|
|
||||||
select{
|
|
||||||
case <- ctx.Done():
|
|
||||||
log.Printf("log streaming done")
|
|
||||||
return
|
|
||||||
case line, ok := <- logCh:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "data: %s\n\n", line)
|
|
||||||
flusher.Flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
// Represents an App, follows the format of
|
|
||||||
// https://git.coopcloud.tech/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
115
cli/new.go
@ -1,115 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os/exec"
|
|
||||||
"net/http"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
appPkg "coopcloud.tech/abra/pkg/app"
|
|
||||||
"coopcloud.tech/abra/pkg/secret"
|
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
|
||||||
"coopcloud.tech/abra/pkg/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *abraHandler) handleNewApp(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
args := []string{"app", "new", appName, "-n"}
|
|
||||||
d := json.NewDecoder(r.Body)
|
|
||||||
d.DisallowUnknownFields() // catch unwanted fields
|
|
||||||
|
|
||||||
// anonymous struct type: handy for one-time use
|
|
||||||
body := struct {
|
|
||||||
Domain *string `json:"domain"`
|
|
||||||
Server *string `json:"server"`
|
|
||||||
Chaos *bool `json:"chaos"`
|
|
||||||
Secrets *bool `json:"secrets"`
|
|
||||||
}{}
|
|
||||||
|
|
||||||
err := d.Decode(&body)
|
|
||||||
if err != nil {
|
|
||||||
// bad JSON or unrecognized json field
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if body.Domain == nil {
|
|
||||||
http.Error(w, "missing field 'domain' from JSON object", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if body.Server == nil {
|
|
||||||
http.Error(w, "missing field 'server' from JSON object", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
args = append(args, fmt.Sprintf("--domain=%s", *body.Domain))
|
|
||||||
args = append(args, fmt.Sprintf("--server=%s", *body.Server))
|
|
||||||
if body.Chaos != nil && *body.Chaos == true {
|
|
||||||
args = append(args, "-C")
|
|
||||||
}
|
|
||||||
log.Printf("%v", args)
|
|
||||||
cmd := exec.Command("abra", args...)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: ", string(output))
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var fs []AppSecret
|
|
||||||
if body.Secrets != nil && *body.Secrets == true {
|
|
||||||
appSecrets, err := createSecrets(appName, *body.Domain, *body.Server)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error creating secrets: %s", err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for k, v := range appSecrets {
|
|
||||||
fs = append(fs, AppSecret{
|
|
||||||
Name: k,
|
|
||||||
Value: v,
|
|
||||||
Version: "v1",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsonBytes, err := json.Marshal(fs)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("JSON conversion failed: %s\n", err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createSecrets(appName string, domain string, server string) (map[string]string, error) {
|
|
||||||
cl, err := client.New(server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
recipe := recipe.Get(appName)
|
|
||||||
|
|
||||||
|
|
||||||
sampleEnv, err := recipe.SampleEnv()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
composeFiles, err := recipe.GetComposeFiles(sampleEnv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
//sanitisedAppName := appPkg.SanitiseAppName(domain)
|
|
||||||
secretsConfig, err := secret.ReadSecretsConfig(
|
|
||||||
recipe.SampleEnvPath,
|
|
||||||
composeFiles,
|
|
||||||
appPkg.StackName(domain),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
secrets, err := secret.GenerateSecrets(cl, secretsConfig, server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return secrets, nil
|
|
||||||
}
|
|
||||||
48
cli/ping.go
48
cli/ping.go
@ -1,48 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"log"
|
|
||||||
"sort"
|
|
||||||
"net/http"
|
|
||||||
"encoding/json"
|
|
||||||
)
|
|
||||||
func (h *abraHandler) handleGetAppServices (w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
var tmpMsg map[string]map[string]string
|
|
||||||
cmd := exec.Command("abra", "app", "ps", appName, "-m")
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: ", err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = json.Unmarshal(output, &tmpMsg); err != nil {
|
|
||||||
log.Printf("Error: ", err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var services []AbraAppService
|
|
||||||
for _,v := range tmpMsg {
|
|
||||||
services = append(services, AbraAppService{
|
|
||||||
Service: v["service"],
|
|
||||||
Chaos: v["chaos"] == "true",
|
|
||||||
Created: v["created"],
|
|
||||||
Image: v["image"],
|
|
||||||
Ports: v["ports"],
|
|
||||||
State: v["state"],
|
|
||||||
Status: v["status"],
|
|
||||||
Version: v["version"],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
sort.Slice(services, func(i, j int) bool {
|
|
||||||
return services[i].Service < services[j].Service
|
|
||||||
})
|
|
||||||
jsonBytes, err := json.Marshal(services)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("JSON conversion failed: %s\n", err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(jsonBytes)
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os/exec"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
func (h *abraHandler) handleRemoveApp(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
cmd := exec.Command("abra", "app", "remove", appName, "-n")
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: ", string(output))
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os/exec"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
func (h *abraHandler) handleGetAppSecrets(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
cmd := exec.Command("abra", "app", "secret", "list", appName, "-m")
|
|
||||||
secrets, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: ", err)
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(secrets)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *abraHandler) handleInsertAppSecret(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
d := json.NewDecoder(r.Body)
|
|
||||||
d.DisallowUnknownFields() // catch unwanted fields
|
|
||||||
|
|
||||||
// anonymous struct type: handy for one-time use
|
|
||||||
body := struct {
|
|
||||||
Name *string `json:"name"`
|
|
||||||
Version *string `json:"version"`
|
|
||||||
Value *string `json:"value"`
|
|
||||||
}{}
|
|
||||||
|
|
||||||
err := d.Decode(&body)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("???\n")
|
|
||||||
// bad JSON or unrecognized json field
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if body.Name == nil {
|
|
||||||
http.Error(w, "missing field 'name' from JSON object", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if body.Version == nil {
|
|
||||||
http.Error(w, "missing field 'version' from JSON object", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if body.Value == nil {
|
|
||||||
http.Error(w, "missing field 'value' from JSON object", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("%s %s %s", *body.Name, *body.Version, *body.Value)
|
|
||||||
cmd := exec.Command("abra", "app", "secret", "insert", appName, *body.Name, *body.Version, *body.Value)
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: ", string(output))
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
@ -1,305 +0,0 @@
|
|||||||
package status
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
dockerClient "github.com/docker/docker/client"
|
|
||||||
containerTypes "github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/cli/cli/command/service/progress"
|
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/ui"
|
|
||||||
)
|
|
||||||
type Event interface {
|
|
||||||
ServiceID() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProgressEvent struct {
|
|
||||||
Service ServiceState
|
|
||||||
Err error
|
|
||||||
Failed bool
|
|
||||||
}
|
|
||||||
func (e ProgressEvent) ServiceID() string { return e.Service.Id }
|
|
||||||
|
|
||||||
type HealthEvent struct {
|
|
||||||
Service ServiceState
|
|
||||||
Err error
|
|
||||||
Health string
|
|
||||||
}
|
|
||||||
func (e HealthEvent) ServiceID() string { return e.Service.Id }
|
|
||||||
|
|
||||||
type StatusEvent struct {
|
|
||||||
Service ServiceState
|
|
||||||
Err error
|
|
||||||
JsonMsg jsonmessage.JSONMessage
|
|
||||||
}
|
|
||||||
func (e StatusEvent) ServiceID() string { return e.Service.Id }
|
|
||||||
|
|
||||||
type StreamEvent struct {
|
|
||||||
Type string `json:"type"` // "service" | "done"
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServiceState struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Err error `json:"err"`
|
|
||||||
Id string `json:"id"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Retries int `json:"retries"`
|
|
||||||
Health string `json:"health"`
|
|
||||||
Rollback bool `json:"rollback"`
|
|
||||||
Failed bool `json:"failed"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeployState struct {
|
|
||||||
Count int `json:"count"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
Failed bool `json:"failed"`
|
|
||||||
Quit bool `json:"quit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeployMsg int
|
|
||||||
const (
|
|
||||||
FailMsg DeployMsg = iota
|
|
||||||
CompleteMsg
|
|
||||||
QuitMsg
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ds DeployState) complete() bool {
|
|
||||||
return ds.Count == ds.Total
|
|
||||||
}
|
|
||||||
|
|
||||||
func progressProducer(ctx context.Context, cl *dockerClient.Client, s ServiceState, w *io.PipeWriter, ch chan<- Event) {
|
|
||||||
go func() {
|
|
||||||
log.Printf("producing progress...")
|
|
||||||
err := progress.ServiceProgress(ctx, cl, s.Id, w)
|
|
||||||
log.Printf("got some service progress here")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error in service progress")
|
|
||||||
ch <- ProgressEvent{
|
|
||||||
Service: s,
|
|
||||||
Failed: true,
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ch <- ProgressEvent{
|
|
||||||
Service: s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func statusProducer(ctx context.Context, decoder *json.Decoder, s ServiceState, ch chan<- Event) {
|
|
||||||
go func() {
|
|
||||||
log.Printf("producing status...")
|
|
||||||
for {
|
|
||||||
var msg jsonmessage.JSONMessage
|
|
||||||
|
|
||||||
if err := decoder.Decode(&msg); err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ch <- StatusEvent{
|
|
||||||
Service: s,
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("Maybe writing JSON message")
|
|
||||||
msg.Display(os.Stdout, false);
|
|
||||||
ch <- StatusEvent{
|
|
||||||
Service: s,
|
|
||||||
JsonMsg: msg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func healthProducer(ctx context.Context, cl *dockerClient.Client, s ServiceState, ch chan<- Event) {
|
|
||||||
go func() {
|
|
||||||
log.Printf("producing health...")
|
|
||||||
ticker := time.NewTicker(2 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-ticker.C:
|
|
||||||
health := ""
|
|
||||||
|
|
||||||
filters := filters.NewArgs()
|
|
||||||
filters.Add("name", fmt.Sprintf("^%s", s.Name))
|
|
||||||
|
|
||||||
containers, err := cl.ContainerList(ctx, containerTypes.ListOptions{Filters: filters})
|
|
||||||
if err != nil {
|
|
||||||
ch <- HealthEvent{Service: s, Err: err}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(containers) == 0 {
|
|
||||||
ch <- HealthEvent{Service: s}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
containerState, err := cl.ContainerInspect(ctx, containers[0].ID)
|
|
||||||
if err != nil {
|
|
||||||
ch <- HealthEvent{Service: s, Err: err}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if containerState.State.Health != nil {
|
|
||||||
health = containerState.State.Health.Status
|
|
||||||
}
|
|
||||||
ch <- HealthEvent{
|
|
||||||
Service: s,
|
|
||||||
Health: health,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func processEvent(ctx context.Context, events <- chan Event, info chan <- DeployMsg, s ServiceState, stream chan <- StreamEvent) error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <- events:
|
|
||||||
if event.ServiceID() != s.Id {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch event := event.(type) {
|
|
||||||
case ProgressEvent:
|
|
||||||
if event.Err != nil {
|
|
||||||
s.Err = event.Err
|
|
||||||
log.Printf("Error in progress event: %s", s.Err)
|
|
||||||
}
|
|
||||||
if event.Failed {
|
|
||||||
info <- FailMsg
|
|
||||||
}
|
|
||||||
// print for debugging purposes
|
|
||||||
log.Printf("Service: %s is complete", event.Service.Id)
|
|
||||||
// end print
|
|
||||||
info <- CompleteMsg
|
|
||||||
|
|
||||||
case StatusEvent:
|
|
||||||
if event.Err != nil {
|
|
||||||
s.Err = event.Err
|
|
||||||
log.Printf("Error in status event: %s", s.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// print for debugging purposes
|
|
||||||
b, err := json.MarshalIndent(event.JsonMsg, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Problem with json message: %s", err)
|
|
||||||
}
|
|
||||||
log.Printf(string(b))
|
|
||||||
// end print
|
|
||||||
|
|
||||||
if event.JsonMsg.ID == "rollback" {
|
|
||||||
log.Printf("Failed in rollback")
|
|
||||||
s.Failed = true
|
|
||||||
s.Rollback = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.JsonMsg.ID != "overall progress" {
|
|
||||||
newStatus := strings.ToLower(event.JsonMsg.Status)
|
|
||||||
currentStatus := s.Status
|
|
||||||
|
|
||||||
if !strings.Contains(currentStatus, "starting") &&
|
|
||||||
strings.Contains(newStatus, "starting") {
|
|
||||||
s.Retries += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Rollback {
|
|
||||||
if event.JsonMsg.ID == "rollback" {
|
|
||||||
s.Status = newStatus
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
s.Status = newStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case HealthEvent:
|
|
||||||
if event.Err != nil {
|
|
||||||
s.Err = event.Err
|
|
||||||
log.Printf("Error in health event: %s", s.Err)
|
|
||||||
}
|
|
||||||
h := "?"
|
|
||||||
if s.Health != "" {
|
|
||||||
h = s.Health
|
|
||||||
}
|
|
||||||
if event.Health != "" {
|
|
||||||
h = event.Health
|
|
||||||
}
|
|
||||||
s.Health = h
|
|
||||||
}
|
|
||||||
stream <- StreamEvent{Type: "service", Data: s}
|
|
||||||
case <- ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func processService(ctx context.Context, info chan <- DeployMsg, cl *dockerClient.Client, s ServiceState, decoder *json.Decoder, writer *io.PipeWriter, stream chan <- StreamEvent) {
|
|
||||||
events := make (chan Event, 50)
|
|
||||||
progressProducer(ctx, cl, s, writer, events)
|
|
||||||
statusProducer(ctx, decoder, s, events)
|
|
||||||
healthProducer(ctx, cl, s, events)
|
|
||||||
|
|
||||||
go processEvent(ctx, events, info, s, stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
func WaitOnServices(pctx context.Context, cl *dockerClient.Client, services []ui.ServiceMeta, filters filters.Args, stream chan <- StreamEvent) {
|
|
||||||
ctx, cancel := context.WithCancel(pctx)
|
|
||||||
defer cancel()
|
|
||||||
log.Printf("What???")
|
|
||||||
ds := DeployState{
|
|
||||||
Count: 0,
|
|
||||||
Total: len(services),
|
|
||||||
Failed: false,
|
|
||||||
Quit: false,
|
|
||||||
}
|
|
||||||
info := make (chan DeployMsg, 50)
|
|
||||||
for _, service := range services {
|
|
||||||
r, w := io.Pipe()
|
|
||||||
d := json.NewDecoder(r)
|
|
||||||
s := ServiceState{
|
|
||||||
Name: service.Name,
|
|
||||||
Id: service.ID,
|
|
||||||
Retries: -1,
|
|
||||||
Health: "?",
|
|
||||||
}
|
|
||||||
log.Printf("Processing Service: %s", s.Id)
|
|
||||||
processService(ctx, info, cl, s, d, w, stream)
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case msg := <-info:
|
|
||||||
switch msg {
|
|
||||||
case FailMsg:
|
|
||||||
ds.Failed = true
|
|
||||||
case CompleteMsg:
|
|
||||||
ds.Count += 1
|
|
||||||
if ds.complete() {
|
|
||||||
log.Printf("deploy completed")
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
case QuitMsg:
|
|
||||||
ds.Quit = true
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.Printf("%v", ds)
|
|
||||||
stream <- StreamEvent{Type: "done", Data: ds}
|
|
||||||
log.Printf("Context finished because: %s", ctx.Err())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
package status
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/swarm"
|
|
||||||
dockerClient "github.com/docker/docker/client"
|
|
||||||
containerTypes "github.com/docker/docker/api/types/container"
|
|
||||||
)
|
|
||||||
func StreamLogs(
|
|
||||||
ctx context.Context,
|
|
||||||
cl *dockerClient.Client,
|
|
||||||
services []swarm.Service,
|
|
||||||
logCh chan <- string,
|
|
||||||
) error {
|
|
||||||
waitCh := make(chan struct{})
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for _, service := range services {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(serviceID string) {
|
|
||||||
defer wg.Done()
|
|
||||||
tail := "50"
|
|
||||||
|
|
||||||
logs, err := cl.ServiceLogs(ctx, serviceID, containerTypes.LogsOptions{
|
|
||||||
ShowStderr: true,
|
|
||||||
ShowStdout: true,
|
|
||||||
Until: "",
|
|
||||||
Timestamps: true,
|
|
||||||
Follow: true,
|
|
||||||
Tail: tail,
|
|
||||||
Details: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
buf := bufio.NewScanner(logs)
|
|
||||||
for buf.Scan() {
|
|
||||||
select {
|
|
||||||
case <- ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
line := fmt.Sprintf("%s: %s", service.Spec.Name, buf.Text())
|
|
||||||
logCh <- line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logs.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}(service.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(waitCh)
|
|
||||||
close(logCh)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
// package ui
|
|
||||||
// import (
|
|
||||||
// "github.com/gorilla/websocket"
|
|
||||||
// "time"
|
|
||||||
// "encoding/json"
|
|
||||||
// "github.com/docker/docker/pkg/jsonmessage"
|
|
||||||
|
|
||||||
// )
|
|
||||||
// type WSMessage struct {
|
|
||||||
// MsgType string `json:"msgType"`
|
|
||||||
// Timestamp time.Time `json:"timestamp"`
|
|
||||||
// Payload interface{} `json:"payload"`
|
|
||||||
// }
|
|
||||||
// type DonePayload struct {
|
|
||||||
// Success bool `json:"success"`
|
|
||||||
// Failed bool `json:"failed"`
|
|
||||||
// TimedOut bool `json:"timed_out"`
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func makeWsEmit(conn *websocket.Conn) func([]byte) error {
|
|
||||||
// return func(b []byte) error {
|
|
||||||
// if conn == nil {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// // Send as a text message
|
|
||||||
// return conn.WriteMessage(websocket.TextMessage, b)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// func send(wsEmit func([]byte) error, msgType string, payload interface{}) {
|
|
||||||
// if wsEmit == nil {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// msg := WSMessage{
|
|
||||||
// MsgType: msgType,
|
|
||||||
// Timestamp: time.Now().UTC(),
|
|
||||||
// Payload: payload,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// b, _ := json.Marshal(msg)
|
|
||||||
// _ = wsEmit(b)
|
|
||||||
// }
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package cli
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os/exec"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
func (h *abraHandler) handleUndeployApp(w http.ResponseWriter, r *http.Request, appName string) {
|
|
||||||
cmd := exec.Command("abra", "app", "undeploy", appName, "-n")
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error: ", string(output))
|
|
||||||
InternalServerErrorHandler(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
3
go.mod
3
go.mod
@ -5,9 +5,7 @@ go 1.25.6
|
|||||||
require (
|
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 (
|
||||||
@ -41,6 +39,7 @@ 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,7 +253,6 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
|||||||
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.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
|
||||||
Secrets bool
|
MachineReadable bool
|
||||||
Major bool
|
Major bool
|
||||||
Minor bool
|
Minor bool
|
||||||
NoDomainChecks bool
|
NoDomainChecks bool
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
#!/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
|
||||||
2
web
2
web
Submodule web updated: e09aa9a1ca...08510b136e
Reference in New Issue
Block a user