forked from toolshed/coop-cloud-backend
440 lines
11 KiB
Go
440 lines
11 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
|
|
"coopcloud.tech/abra/pkg/client"
|
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
|
|
appPkg "coopcloud.tech/abra/pkg/app"
|
|
configPkg "coopcloud.tech/abra/pkg/config"
|
|
deployPkg "coopcloud.tech/abra/pkg/deploy"
|
|
|
|
"net/http"
|
|
|
|
composetypes "github.com/docker/cli/cli/compose/types"
|
|
|
|
"coop-cloud-backend/internal"
|
|
)
|
|
|
|
type abraHandler struct {
|
|
mux *http.ServeMux
|
|
}
|
|
|
|
func newAbraHandler() *abraHandler {
|
|
h := &abraHandler{
|
|
mux: http.NewServeMux(),
|
|
}
|
|
h.mux.HandleFunc("/api/abra/apps", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
h.handleListApps(w, r)
|
|
default:
|
|
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
}
|
|
})
|
|
h.mux.HandleFunc("/api/abra/apps/{serverId}/{appId}/deploy", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodPost:
|
|
h.handleDeployApp(w, r, r.PathValue("appId"), r.PathValue("serverId"))
|
|
default:
|
|
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
}
|
|
})
|
|
h.mux.HandleFunc("/api/abra/servers", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
h.handleListServers(w, r)
|
|
default:
|
|
http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
|
|
}
|
|
})
|
|
return h
|
|
}
|
|
func StartAPI() {
|
|
h := newAbraHandler()
|
|
log.Println("Server started on port 3000")
|
|
log.Fatal(http.ListenAndServe(":3000", h))
|
|
}
|
|
|
|
func (h *abraHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Pre-processing: logging
|
|
log.Printf("Incoming %s request: %s\n", r.Method, r.URL.Path)
|
|
|
|
// Delegate to internal mux
|
|
h.mux.ServeHTTP(w, r)
|
|
}
|
|
|
|
// func (h *appHandler) handleStartApp(w http.ResponseWriter, r *http.Request) {
|
|
// appName := r.PathValue("appName")
|
|
// w.Write([]byte("starting app: " + appName))
|
|
|
|
// }
|
|
|
|
func (h *abraHandler) handleDeployApp(w http.ResponseWriter, r *http.Request, appName string, serverId string) {
|
|
log.Printf("App Id: %s | Server Id: %s", appName, serverId)
|
|
app, err := GetApp(appName)
|
|
if err != nil {
|
|
log.Printf("Error getting app %s: %s\n", appName, err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
cl, err := client.New(app.Server)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
if deployMeta.IsDeployed {
|
|
log.Fatalf("%s is already deployed", app.Name)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
|
|
// logic differs from CLI, we only want to take either
|
|
// 1. the chaos version
|
|
// 2. TODO: the version in the .env file
|
|
// 3. the latest version
|
|
// we never take: specific CLI verison (maybe will support this) or the deployed version
|
|
internal.Chaos = false
|
|
toDeployVersion, err := getDeployVersion(deployMeta, app)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
|
|
if err := validateSecrets(cl, app); err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
|
|
if err := deployPkg.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
|
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
|
|
stackName := app.StackName()
|
|
deployOpts := stack.Deploy{
|
|
Composefiles: composeFiles,
|
|
Namespace: stackName,
|
|
Prune: false,
|
|
ResolveImage: stack.ResolveImageAlways,
|
|
Detach: false,
|
|
}
|
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
|
|
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
|
|
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
|
|
if internal.Chaos {
|
|
appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
|
|
}
|
|
|
|
versionLabel := toDeployVersion
|
|
if internal.Chaos {
|
|
for _, service := range compose.Services {
|
|
if service.Name == "app" {
|
|
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
|
|
// NOTE(d1): keep non-chaos version labbeling when doing chaos ops
|
|
versionLabel = service.Deploy.Labels[labelKey]
|
|
}
|
|
}
|
|
}
|
|
appPkg.SetVersionLabel(compose, stackName, versionLabel)
|
|
|
|
newRecipeWithDeployVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, toDeployVersion)
|
|
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDeployVersion)
|
|
|
|
envVars, err := appPkg.CheckEnv(app)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
// doesn't really get used at all right now
|
|
deployWarnMessages := []string{}
|
|
for _, envVar := range envVars {
|
|
if !envVar.Present {
|
|
deployWarnMessages = append(deployWarnMessages,
|
|
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
|
|
)
|
|
}
|
|
}
|
|
|
|
//skipping domain checks like crazy
|
|
//commented code is to show deploy overview before deploy
|
|
|
|
/*
|
|
deployedVersion := configPkg.MISSING_DEFAULT
|
|
if deployMeta.IsDeployed {
|
|
deployedVersion = deployMeta.Version
|
|
if deployMeta.IsChaos {
|
|
deployedVersion = deployMeta.ChaosVersion
|
|
}
|
|
}
|
|
ShowUnchanged := false
|
|
secretInfo, err := deployPkg.GatherSecretsForDeploy(cl, app, ShowUnchanged)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
|
|
// Gather configs
|
|
configInfo, err := deployPkg.GatherConfigsForDeploy(cl, app, compose, app.Env, ShowUnchanged)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
|
|
// Gather images
|
|
imageInfo, err := deployPkg.GatherImagesForDeploy(cl, app, compose, ShowUnchanged)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}*/
|
|
|
|
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
f, err := app.Filters(true, false, serviceNames...)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// in future allow user input?
|
|
DontWaitConverge := true
|
|
NoInput := true
|
|
if err := stack.RunDeploy(
|
|
cl,
|
|
deployOpts,
|
|
compose,
|
|
app.Name,
|
|
app.Server,
|
|
DontWaitConverge,
|
|
NoInput,
|
|
f,
|
|
); err != nil {
|
|
log.Fatal(err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
func (h *abraHandler) handleListApps(w http.ResponseWriter, r *http.Request) {
|
|
appNames, err := GetAppNames()
|
|
if err != nil {
|
|
log.Printf("Error getting app names: %s\n", err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
// counts the number of apps in a server to initialize slices later
|
|
serverAppCount := make(map[string]int)
|
|
|
|
remoteApps := make([]appPkg.App, 0, len(appNames))
|
|
for _, appName := range appNames {
|
|
remoteApp, err := GetApp(appName)
|
|
serverAppCount[remoteApp.Server] += 1
|
|
if err != nil {
|
|
log.Printf("Error getting app %s: %s\n", appName, err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
remoteApps = append(remoteApps, remoteApp)
|
|
}
|
|
|
|
appStatuses, err := GetAppStatuses(remoteApps)
|
|
if err != nil {
|
|
log.Printf("GetAppStatuses Falied\n")
|
|
log.Println("Error: ", err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
abraAppResponse := ServerAppsResponse{}
|
|
for _, app := range remoteApps {
|
|
serverApps, ok := abraAppResponse[app.Server]
|
|
if !ok { // create the slice
|
|
// ever other field initializes to 0
|
|
serverApps = ServerApps{
|
|
Apps: make([]AbraApp, 0, serverAppCount[app.Server]),
|
|
}
|
|
}
|
|
appInfo, ok := appStatuses[app.StackName()]
|
|
if ok {
|
|
log.Printf("app %s is deployed\n", app.Name)
|
|
serverApps.Apps = append(serverApps.Apps, appTranspose(app, appInfo))
|
|
serverApps.AppCount += 1
|
|
// assume these are true rn idk
|
|
serverApps.VersionCount += 1
|
|
serverApps.LatestCount += 1
|
|
} else {
|
|
log.Printf("app %s is undeployed\n", app.Name)
|
|
serverApps.Apps = append(serverApps.Apps, appTransposeUndeployed(app))
|
|
serverApps.AppCount += 1
|
|
// assume these are true rn idk
|
|
serverApps.VersionCount += 1
|
|
serverApps.LatestCount += 1
|
|
}
|
|
abraAppResponse[app.Server] = serverApps
|
|
}
|
|
jsonBytes, err := json.Marshal(abraAppResponse)
|
|
if err != nil {
|
|
log.Printf("Error converting to json: %s\n", err)
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(jsonBytes)
|
|
}
|
|
func appTransposeUndeployed(app appPkg.App) AbraApp {
|
|
config, err := app.Recipe.GetComposeConfig(app.Env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
version := GetLabel(config, app.StackName(), "version")
|
|
if version == "" {
|
|
version = "unknown"
|
|
}
|
|
return AbraApp{
|
|
AppName: app.Name,
|
|
Server: app.Server,
|
|
Recipe: app.Recipe.Name,
|
|
Domain: app.Domain,
|
|
Chaos: "false",
|
|
Status: "undeployed",
|
|
ChaosVersion: "unknown",
|
|
Version: version,
|
|
Upgrade: "latest",
|
|
}
|
|
}
|
|
func appTranspose(app appPkg.App, psInfo map[string]string) AbraApp {
|
|
return AbraApp{
|
|
AppName: app.Name,
|
|
Server: app.Server,
|
|
Recipe: app.Recipe.Name,
|
|
Domain: app.Domain,
|
|
Chaos: psInfo["chaos"],
|
|
Status: psInfo["status"],
|
|
ChaosVersion: getOrDefault(psInfo, "chaosVersion", "unknown"),
|
|
Version: psInfo["version"],
|
|
Upgrade: "latest",
|
|
}
|
|
}
|
|
|
|
func getOrDefault(m map[string]string, key, def string) string {
|
|
if v, ok := m[key]; ok {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func (h *abraHandler) handleListServers(w http.ResponseWriter, r *http.Request) {
|
|
servers, err := GetServers()
|
|
if err != nil {
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
serverNames, err := ReadServerNames()
|
|
if err != nil {
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
abraServers := make([]AbraServer, 0, len(servers))
|
|
for i := range len(servers) {
|
|
abraServers = append(abraServers,
|
|
AbraServer{
|
|
Name: serverNames[i],
|
|
Host: servers[i],
|
|
},
|
|
)
|
|
}
|
|
jsonBytes, err := json.Marshal(abraServers)
|
|
if err != nil {
|
|
InternalServerErrorHandler(w, r)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(jsonBytes)
|
|
}
|
|
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
|
|
return appPkg.GetLabel(compose, stackName, label)
|
|
}
|
|
|
|
func GetAppStatuses(apps []appPkg.App) (map[string]map[string]string, error) {
|
|
return appPkg.GetAppStatuses(apps, true)
|
|
}
|
|
func GetApp(appName string) (appPkg.App, error) {
|
|
return appPkg.Get(appName)
|
|
}
|
|
|
|
func GetAppNames() ([]string, error) {
|
|
return appPkg.GetAppNames()
|
|
}
|
|
|
|
func GetAppServiceNames(appName string) ([]string, error) {
|
|
return appPkg.GetAppServiceNames(appName)
|
|
}
|
|
|
|
func GetServers() ([]string, error) {
|
|
return configPkg.GetServers()
|
|
}
|
|
|
|
func ReadServerNames() ([]string, error) {
|
|
return configPkg.ReadServerNames()
|
|
}
|