From bb1eb372ef763959c972a660c0e76c459da13ad5 Mon Sep 17 00:00:00 2001
From: Roxie Gibson <me@roxxers.xyz>
Date: Mon, 2 Aug 2021 05:51:58 +0100
Subject: [PATCH] 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
---
 cli/app/new.go     |   9 +-
 client/stack.go    |  36 ++++++++
 config/app.go      | 206 ++++++++++++++++++++++++++++++++++++++++++
 config/app_test.go |  37 ++++++++
 config/env.go      | 221 ---------------------------------------------
 config/env_test.go |  31 -------
 6 files changed, 281 insertions(+), 259 deletions(-)
 create mode 100644 client/stack.go
 create mode 100644 config/app.go
 create mode 100644 config/app_test.go

diff --git a/cli/app/new.go b/cli/app/new.go
index 3dcdef39a..4d540ffb4 100644
--- a/cli/app/new.go
+++ b/cli/app/new.go
@@ -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)
 	}
diff --git a/client/stack.go b/client/stack.go
new file mode 100644
index 000000000..1226c5df6
--- /dev/null
+++ b/client/stack.go
@@ -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}
+}
diff --git a/config/app.go b/config/app.go
new file mode 100644
index 000000000..36906f564
--- /dev/null
+++ b/config/app.go
@@ -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
+}
diff --git a/config/app_test.go b/config/app_test.go
new file mode 100644
index 000000000..9f8defa19
--- /dev/null
+++ b/config/app_test.go
@@ -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)
+	}
+}
diff --git a/config/env.go b/config/env.go
index 34da587af..d76ad986e 100644
--- a/config/env.go
+++ b/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 {
diff --git a/config/env_test.go b/config/env_test.go
index 3f3bd1d8d..dc58e3b2a 100644
--- a/config/env_test.go
+++ b/config/env_test.go
@@ -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)
-	}
-}