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:
Roxie Gibson 2021-08-02 05:51:58 +01:00
parent d777eb2af1
commit bb1eb372ef
Signed by untrusted user: roxxers
GPG Key ID: 5D0140EDEE123F4D
6 changed files with 281 additions and 259 deletions

View File

@ -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)
}

36
client/stack.go Normal file
View File

@ -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}
}

206
config/app.go Normal file
View File

@ -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
}

37
config/app_test.go Normal file
View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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)
}
}