From bb1eb372ef763959c972a660c0e76c459da13ad5 Mon Sep 17 00:00:00 2001 From: Roxie Gibson 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 3dcdef39..4d540ffb 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 00000000..1226c5df --- /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 00000000..36906f56 --- /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 00000000..9f8defa1 --- /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 34da587a..d76ad986 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 3f3bd1d8..dc58e3b2 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) - } -}