refactor: stack func to client, mv app to new file
Stack interaction is now under client. App types and functions moved from env to app under config
This commit is contained in:
parent
d777eb2af1
commit
bb1eb372ef
|
@ -4,7 +4,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/catalogue"
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
|
@ -53,10 +52,6 @@ var appNewCommand = &cli.Command{
|
|||
Action: action,
|
||||
}
|
||||
|
||||
func sanitiseAppName(name string) string {
|
||||
return strings.ReplaceAll(name, ".", "_")
|
||||
}
|
||||
|
||||
func appLookup(appType string) (catalogue.App, error) {
|
||||
catl, err := catalogue.ReadAppsCatalogue()
|
||||
if err != nil {
|
||||
|
@ -110,7 +105,7 @@ func ensureAppNameFlag() error {
|
|||
if internal.AppName == "" {
|
||||
prompt := &survey.Input{
|
||||
Message: "Specify app name:",
|
||||
Default: sanitiseAppName(internal.Domain),
|
||||
Default: config.SanitiseAppName(internal.Domain),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &internal.AppName); err != nil {
|
||||
return err
|
||||
|
@ -176,7 +171,7 @@ func action(c *cli.Context) error {
|
|||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
sanitisedAppName := sanitiseAppName(internal.AppName)
|
||||
sanitisedAppName := config.SanitiseAppName(internal.AppName)
|
||||
if len(sanitisedAppName) > 45 {
|
||||
logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
)
|
||||
|
||||
const StackNamespace = "com.docker.stack.namespace"
|
||||
|
||||
type StackStatus struct {
|
||||
Services []swarm.Service
|
||||
Err error
|
||||
}
|
||||
|
||||
func QueryStackStatus(contextName string) StackStatus {
|
||||
cl, err := NewClientWithContext(contextName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, bail out gracefully
|
||||
return StackStatus{[]swarm.Service{}, nil}
|
||||
}
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
ctx := context.Background()
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", StackNamespace)
|
||||
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
return StackStatus{services, nil}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/client"
|
||||
)
|
||||
|
||||
// Type aliases to make code hints easier to understand
|
||||
type AppEnv = map[string]string
|
||||
type AppName = string
|
||||
|
||||
type AppFile struct {
|
||||
Path string
|
||||
Server string
|
||||
}
|
||||
|
||||
type AppFiles map[AppName]AppFile
|
||||
|
||||
type App struct {
|
||||
Name AppName
|
||||
Type string
|
||||
Domain string
|
||||
Env AppEnv
|
||||
File AppFile
|
||||
}
|
||||
|
||||
func (a App) StackName() string {
|
||||
return SanitiseAppName(a.Name)
|
||||
}
|
||||
|
||||
// SORTING TYPES
|
||||
|
||||
type ByServer []App
|
||||
|
||||
func (a ByServer) Len() int { return len(a) }
|
||||
func (a ByServer) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByServer) Less(i, j int) bool {
|
||||
return strings.ToLower(a[i].File.Server) < strings.ToLower(a[j].File.Server)
|
||||
}
|
||||
|
||||
type ByServerAndType []App
|
||||
|
||||
func (a ByServerAndType) Len() int { return len(a) }
|
||||
func (a ByServerAndType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByServerAndType) Less(i, j int) bool {
|
||||
if a[i].File.Server == a[j].File.Server {
|
||||
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
|
||||
} else {
|
||||
return strings.ToLower(a[i].File.Server) < strings.ToLower(a[j].File.Server)
|
||||
}
|
||||
}
|
||||
|
||||
type ByType []App
|
||||
|
||||
func (a ByType) Len() int { return len(a) }
|
||||
func (a ByType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByType) Less(i, j int) bool {
|
||||
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
|
||||
}
|
||||
|
||||
type ByName []App
|
||||
|
||||
func (a ByName) Len() int { return len(a) }
|
||||
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByName) Less(i, j int) bool {
|
||||
return strings.ToLower(a[i].Name) < strings.ToLower(a[j].Name)
|
||||
}
|
||||
|
||||
func readAppEnvFile(appFile AppFile, name AppName) (App, error) {
|
||||
env, err := ReadEnv(appFile.Path)
|
||||
if err != nil {
|
||||
return App{}, fmt.Errorf("env file for '%s' couldn't be read: %s", name, err.Error())
|
||||
}
|
||||
app, err := newApp(env, name, appFile)
|
||||
if err != nil {
|
||||
return App{}, fmt.Errorf("env file for '%s' has issues: %s", name, err.Error())
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// newApp creates new App object
|
||||
func newApp(env AppEnv, name string, appFile AppFile) (App, error) {
|
||||
// Checking for type as it is required - apps wont work without it
|
||||
domain := env["DOMAIN"]
|
||||
apptype, ok := env["TYPE"]
|
||||
if !ok {
|
||||
return App{}, errors.New("missing TYPE variable")
|
||||
}
|
||||
return App{
|
||||
Name: name,
|
||||
Domain: domain,
|
||||
Type: apptype,
|
||||
Env: env,
|
||||
File: appFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func LoadAppFiles(servers ...string) (AppFiles, error) {
|
||||
appFiles := make(AppFiles)
|
||||
if len(servers) == 1 {
|
||||
if servers[0] == "" {
|
||||
// Empty servers flag, one string will always be passed
|
||||
var err error
|
||||
servers, err = getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, server := range servers {
|
||||
serverDir := path.Join(ABRA_SERVER_FOLDER, server)
|
||||
files, err := getAllFilesInDirectory(serverDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
appName := strings.TrimSuffix(file.Name(), ".env")
|
||||
appFilePath := path.Join(ABRA_SERVER_FOLDER, server, file.Name())
|
||||
appFiles[appName] = AppFile{
|
||||
Path: appFilePath,
|
||||
Server: server,
|
||||
}
|
||||
}
|
||||
}
|
||||
return appFiles, nil
|
||||
}
|
||||
|
||||
// GetApp loads an apps settings, reading it from file, in preparation to use it
|
||||
//
|
||||
// ONLY use when ready to use the env file to keep IO down
|
||||
func GetApp(apps AppFiles, name AppName) (App, error) {
|
||||
appFile, exists := apps[name]
|
||||
if !exists {
|
||||
return App{}, fmt.Errorf("cannot find app file with name '%s'", name)
|
||||
}
|
||||
app, err := readAppEnvFile(appFile, name)
|
||||
if err != nil {
|
||||
return App{}, err
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func GetApps(appFiles AppFiles) ([]App, error) {
|
||||
var apps []App
|
||||
for name := range appFiles {
|
||||
app, err := GetApp(appFiles, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apps = append(apps, app)
|
||||
}
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func CopyAppEnvSample(appType, appName, server string) error {
|
||||
envSamplePath := path.Join(ABRA_DIR, "apps", appType, ".env.sample")
|
||||
envSample, err := ioutil.ReadFile(envSamplePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appEnvPath := path.Join(ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
|
||||
if _, err := os.Stat(appEnvPath); err == nil {
|
||||
return fmt.Errorf("%s already exists?", appEnvPath)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(appEnvPath, envSample, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SanitiseAppName(name string) string {
|
||||
return strings.ReplaceAll(name, ".", "_")
|
||||
}
|
||||
|
||||
func GetAppStatuses(appFiles AppFiles) (map[string]string, error) {
|
||||
servers := appFiles.GetServers()
|
||||
ch := make(chan client.StackStatus, len(servers))
|
||||
for _, server := range servers {
|
||||
go func(s string) {
|
||||
ch <- client.QueryStackStatus(s)
|
||||
}(server)
|
||||
}
|
||||
|
||||
statuses := map[string]string{}
|
||||
for range servers {
|
||||
status := <-ch
|
||||
for _, service := range status.Services {
|
||||
name := service.Spec.Labels[client.StackNamespace]
|
||||
if _, ok := statuses[name]; !ok {
|
||||
statuses[name] = "deployed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statuses, nil
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewApp(t *testing.T) {
|
||||
app, err := newApp(expectedAppEnv, appName, expectedAppFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(app, expectedApp) {
|
||||
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAppEnvFile(t *testing.T) {
|
||||
app, err := readAppEnvFile(expectedAppFile, appName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(app, expectedApp) {
|
||||
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetApp(t *testing.T) {
|
||||
// TODO: Test failures as well as successes
|
||||
app, err := GetApp(expectedAppFiles, appName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(app, expectedApp) {
|
||||
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
|
||||
}
|
||||
}
|
221
config/env.go
221
config/env.go
|
@ -1,91 +1,23 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/client"
|
||||
"github.com/Autonomic-Cooperative/godotenv"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const dockerStackNamespace = "com.docker.stack.namespace"
|
||||
|
||||
var ABRA_DIR = os.ExpandEnv("$HOME/.abra")
|
||||
var ABRA_SERVER_FOLDER = path.Join(ABRA_DIR, "servers")
|
||||
var APPS_JSON = path.Join(ABRA_DIR, "apps.json")
|
||||
var APPS_DIR = path.Join(ABRA_DIR, "apps")
|
||||
var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
|
||||
|
||||
// Type aliases to make code hints easier to understand
|
||||
type AppEnv = map[string]string
|
||||
type AppName = string
|
||||
|
||||
type App struct {
|
||||
Name AppName
|
||||
Type string
|
||||
Domain string
|
||||
Env AppEnv
|
||||
File AppFile
|
||||
}
|
||||
|
||||
func (a App) StackName() string {
|
||||
return strings.ReplaceAll(a.Name, ".", "_")
|
||||
}
|
||||
|
||||
type ByServer []App
|
||||
|
||||
func (a ByServer) Len() int { return len(a) }
|
||||
func (a ByServer) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByServer) Less(i, j int) bool {
|
||||
return strings.ToLower(a[i].File.Server) < strings.ToLower(a[j].File.Server)
|
||||
}
|
||||
|
||||
type ByServerAndType []App
|
||||
|
||||
func (a ByServerAndType) Len() int { return len(a) }
|
||||
func (a ByServerAndType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByServerAndType) Less(i, j int) bool {
|
||||
if a[i].File.Server == a[j].File.Server {
|
||||
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
|
||||
} else {
|
||||
return strings.ToLower(a[i].File.Server) < strings.ToLower(a[j].File.Server)
|
||||
}
|
||||
}
|
||||
|
||||
type ByType []App
|
||||
|
||||
func (a ByType) Len() int { return len(a) }
|
||||
func (a ByType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByType) Less(i, j int) bool {
|
||||
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
|
||||
}
|
||||
|
||||
type ByName []App
|
||||
|
||||
func (a ByName) Len() int { return len(a) }
|
||||
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByName) Less(i, j int) bool {
|
||||
return strings.ToLower(a[i].Name) < strings.ToLower(a[j].Name)
|
||||
}
|
||||
|
||||
type AppFile struct {
|
||||
Path string
|
||||
Server string
|
||||
}
|
||||
|
||||
type AppFiles map[AppName]AppFile
|
||||
|
||||
func (a AppFiles) GetServers() []string {
|
||||
var unique []string
|
||||
servers := make(map[string]struct{})
|
||||
|
@ -98,143 +30,6 @@ func (a AppFiles) GetServers() []string {
|
|||
return unique
|
||||
}
|
||||
|
||||
func LoadAppFiles(servers ...string) (AppFiles, error) {
|
||||
appFiles := make(AppFiles)
|
||||
if len(servers) == 1 {
|
||||
if servers[0] == "" {
|
||||
// Empty servers flag, one string will always be passed
|
||||
var err error
|
||||
servers, err = getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, server := range servers {
|
||||
serverDir := path.Join(ABRA_SERVER_FOLDER, server)
|
||||
files, err := getAllFilesInDirectory(serverDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
appName := strings.TrimSuffix(file.Name(), ".env")
|
||||
appFilePath := path.Join(ABRA_SERVER_FOLDER, server, file.Name())
|
||||
appFiles[appName] = AppFile{
|
||||
Path: appFilePath,
|
||||
Server: server,
|
||||
}
|
||||
}
|
||||
}
|
||||
return appFiles, nil
|
||||
}
|
||||
|
||||
// GetApp loads an apps settings, reading it from file, in preparation to use it
|
||||
//
|
||||
// ONLY use when ready to use the env file to keep IO down
|
||||
func GetApp(apps AppFiles, name AppName) (App, error) {
|
||||
appFile, exists := apps[name]
|
||||
if !exists {
|
||||
return App{}, fmt.Errorf("cannot find app file with name '%s'", name)
|
||||
}
|
||||
app, err := readAppFile(appFile, name)
|
||||
if err != nil {
|
||||
return App{}, err
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func GetApps(appFiles AppFiles) ([]App, error) {
|
||||
var apps []App
|
||||
for name := range appFiles {
|
||||
app, err := GetApp(appFiles, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apps = append(apps, app)
|
||||
}
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func GetAppStatuses(appFiles AppFiles) (map[string]string, error) {
|
||||
type status struct {
|
||||
services []swarm.Service
|
||||
err error
|
||||
}
|
||||
|
||||
servers := appFiles.GetServers()
|
||||
ch := make(chan status, len(servers))
|
||||
for _, server := range servers {
|
||||
go func(s string) {
|
||||
cl, err := client.NewClientWithContext(s)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, bail out gracefully
|
||||
ch <- status{services: []swarm.Service{}, err: nil}
|
||||
return
|
||||
}
|
||||
ch <- status{services: []swarm.Service{}, err: err}
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", dockerStackNamespace)
|
||||
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
ch <- status{services: []swarm.Service{}, err: err}
|
||||
return
|
||||
}
|
||||
ch <- status{services: services, err: nil}
|
||||
}(server)
|
||||
}
|
||||
|
||||
statuses := map[string]string{}
|
||||
for range servers {
|
||||
status := <-ch
|
||||
for _, service := range status.services {
|
||||
name := service.Spec.Labels[dockerStackNamespace]
|
||||
if _, ok := statuses[name]; !ok {
|
||||
statuses[name] = "deployed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func CopyAppEnvSample(appType, appName, server string) error {
|
||||
envSamplePath := path.Join(ABRA_DIR, "apps", appType, ".env.sample")
|
||||
envSample, err := ioutil.ReadFile(envSamplePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appEnvPath := path.Join(ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
|
||||
if _, err := os.Stat(appEnvPath); err == nil {
|
||||
return fmt.Errorf("%s already exists?", appEnvPath)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(appEnvPath, envSample, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: maybe better names than read and get
|
||||
|
||||
func readAppFile(appFile AppFile, name AppName) (App, error) {
|
||||
env, err := ReadEnv(appFile.Path)
|
||||
if err != nil {
|
||||
return App{}, fmt.Errorf("env file for '%s' couldn't be read: %s", name, err.Error())
|
||||
}
|
||||
app, err := makeApp(env, name, appFile)
|
||||
if err != nil {
|
||||
return App{}, fmt.Errorf("env file for '%s' has issues: %s", name, err.Error())
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func ReadEnv(filePath string) (AppEnv, error) {
|
||||
var envFile AppEnv
|
||||
envFile, err := godotenv.Read(filePath)
|
||||
|
@ -244,22 +39,6 @@ func ReadEnv(filePath string) (AppEnv, error) {
|
|||
return envFile, nil
|
||||
}
|
||||
|
||||
func makeApp(env AppEnv, name string, appFile AppFile) (App, error) {
|
||||
// Checking for type as it is required - apps wont work without it
|
||||
domain := env["DOMAIN"]
|
||||
apptype, ok := env["TYPE"]
|
||||
if !ok {
|
||||
return App{}, errors.New("missing TYPE variable")
|
||||
}
|
||||
return App{
|
||||
Name: name,
|
||||
Domain: domain,
|
||||
Type: apptype,
|
||||
Env: env,
|
||||
File: appFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ReadServerNames() ([]string, error) {
|
||||
serverNames, err := getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
|
||||
if err != nil {
|
||||
|
|
|
@ -81,34 +81,3 @@ func TestReadEnv(t *testing.T) {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeApp(t *testing.T) {
|
||||
app, err := makeApp(expectedAppEnv, appName, expectedAppFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(app, expectedApp) {
|
||||
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAppFile(t *testing.T) {
|
||||
app, err := readAppFile(expectedAppFile, appName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(app, expectedApp) {
|
||||
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetApp(t *testing.T) {
|
||||
// TODO: Test failures as well as successes
|
||||
app, err := GetApp(expectedAppFiles, appName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(app, expectedApp) {
|
||||
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue