refactor: move app files from config to app package

This commit is contained in:
2024-07-05 16:04:01 +02:00
parent ac695ae28e
commit f18c642226
31 changed files with 915 additions and 912 deletions

View File

@ -1,22 +1,34 @@
package app
import (
"fmt"
"os"
"path"
"strings"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/convert"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/filters"
"github.com/schollz/progressbar/v3"
"github.com/sirupsen/logrus"
)
// Get retrieves an app
func Get(appName string) (config.App, error) {
files, err := config.LoadAppFiles("")
func Get(appName string) (App, error) {
files, err := LoadAppFiles("")
if err != nil {
return config.App{}, err
return App{}, err
}
app, err := config.GetApp(files, appName)
app, err := GetApp(files, appName)
if err != nil {
return config.App{}, err
return App{}, err
}
logrus.Debugf("retrieved %s for %s", app, appName)
@ -24,19 +36,530 @@ func Get(appName string) (config.App, error) {
return app, nil
}
// deployedServiceSpec represents a deployed service of an app.
type deployedServiceSpec struct {
Name string
Version string
// GetApp loads an apps settings, reading it from file, in preparation to use
// it. It should only be used when ready to use the env file to keep IO
// operations down.
func GetApp(apps AppFiles, name AppName) (App, error) {
appFile, exists := apps[name]
if !exists {
return App{}, fmt.Errorf("cannot find app with name %s", name)
}
app, err := ReadAppEnvFile(appFile, name)
if err != nil {
return App{}, err
}
return app, nil
}
// VersionSpec represents a deployed app and associated metadata.
type VersionSpec map[string]deployedServiceSpec
// GetApps returns a slice of Apps with their env files read from a given
// slice of AppFiles.
func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) {
var apps []App
// ParseServiceName parses a $STACK_NAME_$SERVICE_NAME service label.
func ParseServiceName(label string) string {
idx := strings.LastIndex(label, "_")
serviceName := label[idx+1:]
logrus.Debugf("parsed %s as service name from %s", serviceName, label)
return serviceName
for name := range appFiles {
app, err := GetApp(appFiles, name)
if err != nil {
return nil, err
}
if recipeFilter != "" {
if app.Recipe == recipeFilter {
apps = append(apps, app)
}
} else {
apps = append(apps, app)
}
}
return apps, nil
}
// App reprents an app with its env file read into memory
type App struct {
Name AppName
Recipe string
Domain string
Env AppEnv
Server string
Path string
}
// Type aliases to make code hints easier to understand
// AppEnv is a map of the values in an apps env config
type AppEnv = map[string]string
// AppModifiers is a map of modifiers in an apps env config
type AppModifiers = map[string]map[string]string
// AppName is AppName
type AppName = string
// AppFile represents app env files on disk without reading the contents
type AppFile struct {
Path string
Server string
}
// AppFiles is a slice of appfiles
type AppFiles map[AppName]AppFile
// See documentation of config.StackName
func (a App) StackName() string {
if _, exists := a.Env["STACK_NAME"]; exists {
return a.Env["STACK_NAME"]
}
stackName := StackName(a.Name)
a.Env["STACK_NAME"] = stackName
return stackName
}
// StackName gets whatever the docker safe (uses the right delimiting
// character, e.g. "_") stack name is for the app. In general, you don't want
// to use this to show anything to end-users, you want use a.Name instead.
func StackName(appName string) string {
stackName := SanitiseAppName(appName)
if len(stackName) > config.MAX_SANITISED_APP_NAME_LENGTH {
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:config.MAX_SANITISED_APP_NAME_LENGTH])
stackName = stackName[:config.MAX_SANITISED_APP_NAME_LENGTH]
}
return stackName
}
// Filters retrieves app filters for querying the container runtime. By default
// it filters on all services in the app. It is also possible to pass an
// otional list of service names, which get filtered instead.
//
// Due to upstream issues, filtering works different depending on what you're
// querying. So, for example, secrets don't work with regex! The caller needs
// to implement their own validation that the right secrets are matched. In
// order to handle these cases, we provide the `appendServiceNames` /
// `exactMatch` modifiers.
func (a App) Filters(appendServiceNames, exactMatch bool, services ...string) (filters.Args, error) {
filters := filters.NewArgs()
if len(services) > 0 {
for _, serviceName := range services {
filters.Add("name", ServiceFilter(a.StackName(), serviceName, exactMatch))
}
return filters, nil
}
// When not appending the service name, just add one filter for the whole
// stack.
if !appendServiceNames {
f := fmt.Sprintf("%s", a.StackName())
if exactMatch {
f = fmt.Sprintf("^%s", f)
}
filters.Add("name", f)
return filters, nil
}
composeFiles, err := GetComposeFiles(a.Recipe, a.Env)
if err != nil {
return filters, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(a.Recipe, opts, a.Env)
if err != nil {
return filters, err
}
for _, service := range compose.Services {
f := ServiceFilter(a.StackName(), service.Name, exactMatch)
filters.Add("name", f)
}
return filters, nil
}
// ServiceFilter creates a filter string for filtering a service in the docker
// container runtime. When exact match is true, it uses regex to match the
// string exactly.
func ServiceFilter(stack, service string, exact bool) string {
if exact {
return fmt.Sprintf("^%s_%s", stack, service)
}
return fmt.Sprintf("%s_%s", stack, service)
}
// ByServer sort a slice of Apps
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].Server) < strings.ToLower(a[j].Server)
}
// ByServerAndRecipe sort a slice of Apps
type ByServerAndRecipe []App
func (a ByServerAndRecipe) Len() int { return len(a) }
func (a ByServerAndRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByServerAndRecipe) Less(i, j int) bool {
if a[i].Server == a[j].Server {
return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
}
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
}
// ByRecipe sort a slice of Apps
type ByRecipe []App
func (a ByRecipe) Len() int { return len(a) }
func (a ByRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByRecipe) Less(i, j int) bool {
return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
}
// ByName sort a slice of Apps
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())
}
logrus.Debugf("read env %s from %s", env, appFile.Path)
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) {
domain := env["DOMAIN"]
recipe, exists := env["RECIPE"]
if !exists {
recipe, exists = env["TYPE"]
if !exists {
return App{}, fmt.Errorf("%s is missing the TYPE env var?", name)
}
}
return App{
Name: name,
Domain: domain,
Recipe: recipe,
Env: env,
Server: appFile.Server,
Path: appFile.Path,
}, nil
}
// LoadAppFiles gets all app files for a given set of servers or all servers.
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 = config.GetAllFoldersInDirectory(config.SERVERS_DIR)
if err != nil {
return appFiles, err
}
}
}
logrus.Debugf("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", "))
for _, server := range servers {
serverDir := path.Join(config.SERVERS_DIR, server)
files, err := config.GetAllFilesInDirectory(serverDir)
if err != nil {
return appFiles, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server)
}
for _, file := range files {
appName := strings.TrimSuffix(file.Name(), ".env")
appFilePath := path.Join(config.SERVERS_DIR, server, file.Name())
appFiles[appName] = AppFile{
Path: appFilePath,
Server: server,
}
}
}
return appFiles, nil
}
// GetAppServiceNames retrieves a list of app service names.
func GetAppServiceNames(appName string) ([]string, error) {
var serviceNames []string
appFiles, err := LoadAppFiles("")
if err != nil {
return serviceNames, err
}
app, err := GetApp(appFiles, appName)
if err != nil {
return serviceNames, err
}
composeFiles, err := GetComposeFiles(app.Recipe, app.Env)
if err != nil {
return serviceNames, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(app.Recipe, opts, app.Env)
if err != nil {
return serviceNames, err
}
for _, service := range compose.Services {
serviceNames = append(serviceNames, service.Name)
}
return serviceNames, nil
}
// GetAppNames retrieves a list of app names.
func GetAppNames() ([]string, error) {
var appNames []string
appFiles, err := LoadAppFiles("")
if err != nil {
return appNames, err
}
apps, err := GetApps(appFiles, "")
if err != nil {
return appNames, err
}
for _, app := range apps {
appNames = append(appNames, app.Name)
}
return appNames, nil
}
// TemplateAppEnvSample copies the example env file for the app into the users
// env files.
func TemplateAppEnvSample(recipeName, appName, server, domain string) error {
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
envSample, err := os.ReadFile(envSamplePath)
if err != nil {
return err
}
appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
return fmt.Errorf("%s already exists?", appEnvPath)
}
err = os.WriteFile(appEnvPath, envSample, 0o664)
if err != nil {
return err
}
read, err := os.ReadFile(appEnvPath)
if err != nil {
return err
}
newContents := strings.Replace(string(read), recipeName+".example.com", domain, -1)
err = os.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil {
return err
}
logrus.Debugf("copied & templated %s to %s", envSamplePath, appEnvPath)
return nil
}
// SanitiseAppName makes a app name usable with Docker by replacing illegal
// characters.
func SanitiseAppName(name string) string {
return strings.ReplaceAll(name, ".", "_")
}
// GetAppStatuses queries servers to check the deployment status of given apps.
func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]string, error) {
statuses := make(map[string]map[string]string)
servers := make(map[string]struct{})
for _, app := range apps {
if _, ok := servers[app.Server]; !ok {
servers[app.Server] = struct{}{}
}
}
var bar *progressbar.ProgressBar
if !MachineReadable {
bar = formatter.CreateProgressbar(len(servers), "querying remote servers...")
}
ch := make(chan stack.StackStatus, len(servers))
for server := range servers {
cl, err := client.New(server)
if err != nil {
return statuses, err
}
go func(s string) {
ch <- stack.GetAllDeployedServices(cl, s)
if !MachineReadable {
bar.Add(1)
}
}(server)
}
for range servers {
status := <-ch
if status.Err != nil {
return statuses, status.Err
}
for _, service := range status.Services {
result := make(map[string]string)
name := service.Spec.Labels[convert.LabelNamespace]
if _, ok := statuses[name]; !ok {
result["status"] = "deployed"
}
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name)
chaos, ok := service.Spec.Labels[labelKey]
if ok {
result["chaos"] = chaos
}
labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name)
if chaosVersion, ok := service.Spec.Labels[labelKey]; ok {
result["chaosVersion"] = chaosVersion
}
labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name)
if autoUpdate, ok := service.Spec.Labels[labelKey]; ok {
result["autoUpdate"] = autoUpdate
} else {
result["autoUpdate"] = "false"
}
labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
if version, ok := service.Spec.Labels[labelKey]; ok {
result["version"] = version
} else {
continue
}
statuses[name] = result
}
}
logrus.Debugf("retrieved app statuses: %s", statuses)
return statuses, nil
}
// ensurePathExists ensures that a path exists.
func ensurePathExists(path string) error {
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
return err
}
return nil
}
// GetComposeFiles gets the list of compose files for an app (or recipe if you
// don't already have an app) which should be merged into a composetypes.Config
// while respecting the COMPOSE_FILE env var.
func GetComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
var composeFiles []string
composeFileEnvVar, ok := appEnv["COMPOSE_FILE"]
if !ok {
path := fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, recipe)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
logrus.Debugf("no COMPOSE_FILE detected, loading default: %s", path)
composeFiles = append(composeFiles, path)
return composeFiles, nil
}
if !strings.Contains(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipe, composeFileEnvVar)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
logrus.Debugf("COMPOSE_FILE detected, loading %s", path)
composeFiles = append(composeFiles, path)
return composeFiles, nil
}
numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
if len(envVars) != numComposeFiles {
return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar)
}
for _, file := range envVars {
path := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipe, file)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
composeFiles = append(composeFiles, path)
}
logrus.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
logrus.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), recipe)
return composeFiles, nil
}
// GetAppComposeConfig retrieves a compose specification for a recipe. This
// specification is the result of a merge of all the compose.**.yml files in
// the recipe repository.
func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv AppEnv) (*composetypes.Config, error) {
compose, err := loader.LoadComposefile(opts, appEnv)
if err != nil {
return &composetypes.Config{}, err
}
logrus.Debugf("retrieved %s for %s", compose.Filename, recipe)
return compose, nil
}
// ExposeAllEnv exposes all env variables to the app container
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("Add the following environment to the app service config of %s:", stackName)
for k, v := range appEnv {
_, exists := service.Environment[k]
if !exists {
value := v
service.Environment[k] = &value
logrus.Debugf("Add Key: %s Value: %s to %s", k, value, stackName)
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package config_test
package app_test
import (
"encoding/json"
@ -6,6 +6,7 @@ import (
"reflect"
"testing"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"github.com/docker/docker/api/types/filters"
@ -14,7 +15,7 @@ import (
)
func TestNewApp(t *testing.T) {
app, err := config.NewApp(ExpectedAppEnv, AppName, ExpectedAppFile)
app, err := appPkg.NewApp(ExpectedAppEnv, AppName, ExpectedAppFile)
if err != nil {
t.Fatal(err)
}
@ -24,7 +25,7 @@ func TestNewApp(t *testing.T) {
}
func TestReadAppEnvFile(t *testing.T) {
app, err := config.ReadAppEnvFile(ExpectedAppFile, AppName)
app, err := appPkg.ReadAppEnvFile(ExpectedAppFile, AppName)
if err != nil {
t.Fatal(err)
}
@ -34,7 +35,7 @@ func TestReadAppEnvFile(t *testing.T) {
}
func TestGetApp(t *testing.T) {
app, err := config.GetApp(ExpectedAppFiles, AppName)
app, err := appPkg.GetApp(ExpectedAppFiles, AppName)
if err != nil {
t.Fatal(err)
}
@ -82,7 +83,7 @@ func TestGetComposeFiles(t *testing.T) {
}
for _, test := range tests {
composeFiles, err := config.GetComposeFiles(r.Name, test.appEnv)
composeFiles, err := appPkg.GetComposeFiles(r.Name, test.appEnv)
if err != nil {
t.Fatal(err)
}
@ -103,7 +104,7 @@ func TestGetComposeFilesError(t *testing.T) {
}
for _, test := range tests {
_, err := config.GetComposeFiles(r.Name, test.appEnv)
_, err := appPkg.GetComposeFiles(r.Name, test.appEnv)
if err == nil {
t.Fatalf("should have failed: %v", test.appEnv)
}
@ -112,16 +113,16 @@ func TestGetComposeFilesError(t *testing.T) {
func TestFilters(t *testing.T) {
oldDir := config.RECIPES_DIR
config.RECIPES_DIR = "./testdir"
config.RECIPES_DIR = "./testdata"
defer func() {
config.RECIPES_DIR = oldDir
}()
app, err := config.NewApp(config.AppEnv{
app, err := appPkg.NewApp(appPkg.AppEnv{
"DOMAIN": "test.example.com",
"RECIPE": "test-recipe",
}, "test_example_com", config.AppFile{
Path: "./testdir/filtertest.end",
}, "test_example_com", appPkg.AppFile{
Path: "./testdata/filtertest.end",
Server: "local",
})
if err != nil {

87
pkg/app/compose.go Normal file
View File

@ -0,0 +1,87 @@
package app
import (
"fmt"
"strconv"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/sirupsen/logrus"
)
// SetRecipeLabel adds the label 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' to the app container
// to signal which recipe is connected to the deployed app
func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName)
service.Deploy.Labels[labelKey] = recipe
}
}
}
// SetChaosLabel adds the label 'coop-cloud.${STACK_NAME}.chaos=true/false' to the app container
// to signal if the app is deployed in chaos mode
func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", stackName)
service.Deploy.Labels[labelKey] = strconv.FormatBool(chaos)
}
}
}
// SetChaosVersionLabel adds the label 'coop-cloud.${STACK_NAME}.chaos-version=$(GIT_COMMIT)' to the app container
func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos-version", stackName)
service.Deploy.Labels[labelKey] = chaosVersion
}
}
}
// SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the
// auto update process for this app. The default if this variable is not set is to disable
// the auto update process.
func SetUpdateLabel(compose *composetypes.Config, stackName string, appEnv AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
enable_auto_update, exists := appEnv["ENABLE_AUTO_UPDATE"]
if !exists {
enable_auto_update = "false"
}
logrus.Debugf("set label 'coop-cloud.%s.autoupdate' to %s for %s", stackName, enable_auto_update, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.autoupdate", stackName)
service.Deploy.Labels[labelKey] = enable_auto_update
}
}
}
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
for _, service := range compose.Services {
if service.Name == "app" {
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
logrus.Debugf("get label '%s'", labelKey)
if labelValue, ok := service.Deploy.Labels[labelKey]; ok {
return labelValue
}
}
}
logrus.Debugf("no %s label found for %s", label, stackName)
return ""
}
// GetTimeoutFromLabel reads the timeout value from docker label "coop-cloud.${STACK_NAME}.TIMEOUT" and returns 50 as default value
func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) {
timeout := 50 // Default Timeout
var err error = nil
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
logrus.Debugf("timeout label: %s", timeoutLabel)
timeout, err = strconv.Atoi(timeoutLabel)
}
return timeout, err
}

159
pkg/app/env.go Normal file
View File

@ -0,0 +1,159 @@
package app
import (
"bufio"
"fmt"
"os"
"path"
"regexp"
"sort"
"strings"
"coopcloud.tech/abra/pkg/config"
"git.coopcloud.tech/coop-cloud/godotenv"
"github.com/sirupsen/logrus"
)
// ReadEnv loads an app envivornment into a map.
func ReadEnv(filePath string) (AppEnv, error) {
var envVars AppEnv
envVars, _, err := godotenv.Read(filePath)
if err != nil {
return nil, err
}
logrus.Debugf("read %s from %s", envVars, filePath)
return envVars, nil
}
// ReadEnv loads an app envivornment and their modifiers in two different maps.
func ReadEnvWithModifiers(filePath string) (AppEnv, AppModifiers, error) {
var envVars AppEnv
envVars, mods, err := godotenv.Read(filePath)
if err != nil {
return nil, mods, err
}
logrus.Debugf("read %s from %s", envVars, filePath)
return envVars, mods, nil
}
// ReadAbraShEnvVars reads env vars from an abra.sh recipe file.
func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
envVars := make(map[string]string)
file, err := os.Open(abraSh)
if err != nil {
if os.IsNotExist(err) {
return envVars, nil
}
return envVars, err
}
defer file.Close()
exportRegex, err := regexp.Compile(`^export\s+(\w+=\w+)`)
if err != nil {
return envVars, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
txt := scanner.Text()
if exportRegex.MatchString(txt) {
splitVals := strings.Split(txt, "export ")
envVarDef := splitVals[len(splitVals)-1]
keyVal := strings.Split(envVarDef, "=")
if len(keyVal) != 2 {
return envVars, fmt.Errorf("couldn't parse %s", txt)
}
envVars[keyVal[0]] = keyVal[1]
}
}
if len(envVars) > 0 {
logrus.Debugf("read %s from %s", envVars, abraSh)
} else {
logrus.Debugf("read 0 env var exports from %s", abraSh)
}
return envVars, nil
}
type EnvVar struct {
Name string
Present bool
}
func CheckEnv(app App) ([]EnvVar, error) {
var envVars []EnvVar
envSamplePath := path.Join(config.RECIPES_DIR, app.Recipe, ".env.sample")
if _, err := os.Stat(envSamplePath); err != nil {
if os.IsNotExist(err) {
return envVars, fmt.Errorf("%s does not exist?", envSamplePath)
}
return envVars, err
}
envSample, err := ReadEnv(envSamplePath)
if err != nil {
return envVars, err
}
var keys []string
for key := range envSample {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if _, ok := app.Env[key]; ok {
envVars = append(envVars, EnvVar{Name: key, Present: true})
} else {
envVars = append(envVars, EnvVar{Name: key, Present: false})
}
}
return envVars, nil
}
// ReadAbraShCmdNames reads the names of commands.
func ReadAbraShCmdNames(abraSh string) ([]string, error) {
var cmdNames []string
file, err := os.Open(abraSh)
if err != nil {
if os.IsNotExist(err) {
return cmdNames, nil
}
return cmdNames, err
}
defer file.Close()
cmdNameRegex, err := regexp.Compile(`(\w+)(\(\).*\{)`)
if err != nil {
return cmdNames, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
matches := cmdNameRegex.FindStringSubmatch(line)
if len(matches) > 0 {
cmdNames = append(cmdNames, matches[1])
}
}
if len(cmdNames) > 0 {
logrus.Debugf("read %s from %s", strings.Join(cmdNames, " "), abraSh)
} else {
logrus.Debugf("read 0 command names from %s", abraSh)
}
return cmdNames, nil
}

View File

@ -1,4 +1,4 @@
package config_test
package app_test
import (
"fmt"
@ -9,6 +9,8 @@ import (
"strings"
"testing"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
)
@ -29,12 +31,12 @@ var (
ServerName = "evil.corp"
)
var ExpectedAppEnv = config.AppEnv{
var ExpectedAppEnv = app.AppEnv{
"DOMAIN": "ecloud.evil.corp",
"RECIPE": "ecloud",
}
var ExpectedApp = config.App{
var ExpectedApp = app.App{
Name: AppName,
Recipe: ExpectedAppEnv["RECIPE"],
Domain: ExpectedAppEnv["DOMAIN"],
@ -43,12 +45,12 @@ var ExpectedApp = config.App{
Server: ExpectedAppFile.Server,
}
var ExpectedAppFile = config.AppFile{
var ExpectedAppFile = app.AppFile{
Path: path.Join(ValidAbraConf, "servers", ServerName, AppName+".env"),
Server: ServerName,
}
var ExpectedAppFiles = map[string]config.AppFile{
var ExpectedAppFiles = map[string]app.AppFile{
AppName: ExpectedAppFile,
}
@ -77,7 +79,7 @@ func TestGetAllFilesInDirectory(t *testing.T) {
}
func TestReadEnv(t *testing.T) {
env, err := config.ReadEnv(ExpectedAppFile.Path)
env, err := app.ReadEnv(ExpectedAppFile.Path)
if err != nil {
t.Fatal(err)
}
@ -100,7 +102,7 @@ func TestReadAbraShEnvVars(t *testing.T) {
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
abraShEnv, err := app.ReadAbraShEnvVars(abraShPath)
if err != nil {
t.Fatal(err)
}
@ -130,7 +132,7 @@ func TestReadAbraShCmdNames(t *testing.T) {
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, "abra.sh")
cmdNames, err := config.ReadAbraShCmdNames(abraShPath)
cmdNames, err := app.ReadAbraShCmdNames(abraShPath)
if err != nil {
t.Fatal(err)
}
@ -155,12 +157,12 @@ func TestCheckEnv(t *testing.T) {
}
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, err := config.ReadEnv(envSamplePath)
envSample, err := app.ReadEnv(envSamplePath)
if err != nil {
t.Fatal(err)
}
app := config.App{
app := app.App{
Name: "test-app",
Recipe: r.Name,
Domain: "example.com",
@ -169,7 +171,7 @@ func TestCheckEnv(t *testing.T) {
Server: "example.com",
}
envVars, err := config.CheckEnv(app)
envVars, err := appPkg.CheckEnv(app)
if err != nil {
t.Fatal(err)
}
@ -189,14 +191,14 @@ func TestCheckEnvError(t *testing.T) {
}
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, err := config.ReadEnv(envSamplePath)
envSample, err := app.ReadEnv(envSamplePath)
if err != nil {
t.Fatal(err)
}
delete(envSample, "DOMAIN")
app := config.App{
app := app.App{
Name: "test-app",
Recipe: r.Name,
Domain: "example.com",
@ -205,7 +207,7 @@ func TestCheckEnvError(t *testing.T) {
Server: "example.com",
}
envVars, err := config.CheckEnv(app)
envVars, err := appPkg.CheckEnv(app)
if err != nil {
t.Fatal(err)
}
@ -225,7 +227,7 @@ func TestEnvVarCommentsRemoved(t *testing.T) {
}
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, err := config.ReadEnv(envSamplePath)
envSample, err := app.ReadEnv(envSamplePath)
if err != nil {
t.Fatal(err)
}
@ -257,7 +259,7 @@ func TestEnvVarModifiersIncluded(t *testing.T) {
}
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, modifiers, err := config.ReadEnvWithModifiers(envSamplePath)
envSample, modifiers, err := app.ReadEnvWithModifiers(envSamplePath)
if err != nil {
t.Fatal(err)
}

View File

@ -3,7 +3,7 @@ package autocomplete
import (
"fmt"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
@ -11,7 +11,7 @@ import (
// AppNameComplete copletes app names.
func AppNameComplete(c *cli.Context) {
appNames, err := config.GetAppNames()
appNames, err := app.GetAppNames()
if err != nil {
logrus.Warn(err)
}
@ -26,7 +26,7 @@ func AppNameComplete(c *cli.Context) {
}
func ServiceNameComplete(appName string) {
serviceNames, err := config.GetAppServiceNames(appName)
serviceNames, err := app.GetAppServiceNames(appName)
if err != nil {
return
}
@ -67,7 +67,7 @@ func RecipeVersionComplete(recipeName string) {
// ServerNameComplete completes server names.
func ServerNameComplete(c *cli.Context) {
files, err := config.LoadAppFiles("")
files, err := app.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}

View File

@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/stack"
@ -29,7 +30,7 @@ func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
sampleEnv, err := app.ReadEnv(envSamplePath)
if err != nil {
return false, err
}
@ -97,7 +98,7 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
sampleEnv, err := app.ReadEnv(envSamplePath)
if err != nil {
return err
}

View File

@ -1,627 +0,0 @@
package config
import (
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
"strings"
"github.com/schollz/progressbar/v3"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/convert"
loader "coopcloud.tech/abra/pkg/upstream/stack"
stack "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
)
// Type aliases to make code hints easier to understand
// AppEnv is a map of the values in an apps env config
type AppEnv = map[string]string
// AppModifiers is a map of modifiers in an apps env config
type AppModifiers = map[string]map[string]string
// AppName is AppName
type AppName = string
// AppFile represents app env files on disk without reading the contents
type AppFile struct {
Path string
Server string
}
// AppFiles is a slice of appfiles
type AppFiles map[AppName]AppFile
// App reprents an app with its env file read into memory
type App struct {
Name AppName
Recipe string
Domain string
Env AppEnv
Server string
Path string
}
// See documentation of config.StackName
func (a App) StackName() string {
if _, exists := a.Env["STACK_NAME"]; exists {
return a.Env["STACK_NAME"]
}
stackName := StackName(a.Name)
a.Env["STACK_NAME"] = stackName
return stackName
}
// StackName gets whatever the docker safe (uses the right delimiting
// character, e.g. "_") stack name is for the app. In general, you don't want
// to use this to show anything to end-users, you want use a.Name instead.
func StackName(appName string) string {
stackName := SanitiseAppName(appName)
if len(stackName) > MAX_SANITISED_APP_NAME_LENGTH {
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:MAX_SANITISED_APP_NAME_LENGTH])
stackName = stackName[:MAX_SANITISED_APP_NAME_LENGTH]
}
return stackName
}
// Filters retrieves app filters for querying the container runtime. By default
// it filters on all services in the app. It is also possible to pass an
// otional list of service names, which get filtered instead.
//
// Due to upstream issues, filtering works different depending on what you're
// querying. So, for example, secrets don't work with regex! The caller needs
// to implement their own validation that the right secrets are matched. In
// order to handle these cases, we provide the `appendServiceNames` /
// `exactMatch` modifiers.
func (a App) Filters(appendServiceNames, exactMatch bool, services ...string) (filters.Args, error) {
filters := filters.NewArgs()
if len(services) > 0 {
for _, serviceName := range services {
filters.Add("name", ServiceFilter(a.StackName(), serviceName, exactMatch))
}
return filters, nil
}
// When not appending the service name, just add one filter for the whole
// stack.
if !appendServiceNames {
f := fmt.Sprintf("%s", a.StackName())
if exactMatch {
f = fmt.Sprintf("^%s", f)
}
filters.Add("name", f)
return filters, nil
}
composeFiles, err := GetComposeFiles(a.Recipe, a.Env)
if err != nil {
return filters, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(a.Recipe, opts, a.Env)
if err != nil {
return filters, err
}
for _, service := range compose.Services {
f := ServiceFilter(a.StackName(), service.Name, exactMatch)
filters.Add("name", f)
}
return filters, nil
}
// ServiceFilter creates a filter string for filtering a service in the docker
// container runtime. When exact match is true, it uses regex to match the
// string exactly.
func ServiceFilter(stack, service string, exact bool) string {
if exact {
return fmt.Sprintf("^%s_%s", stack, service)
}
return fmt.Sprintf("%s_%s", stack, service)
}
// ByServer sort a slice of Apps
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].Server) < strings.ToLower(a[j].Server)
}
// ByServerAndRecipe sort a slice of Apps
type ByServerAndRecipe []App
func (a ByServerAndRecipe) Len() int { return len(a) }
func (a ByServerAndRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByServerAndRecipe) Less(i, j int) bool {
if a[i].Server == a[j].Server {
return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
}
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
}
// ByRecipe sort a slice of Apps
type ByRecipe []App
func (a ByRecipe) Len() int { return len(a) }
func (a ByRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByRecipe) Less(i, j int) bool {
return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
}
// ByName sort a slice of Apps
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())
}
logrus.Debugf("read env %s from %s", env, appFile.Path)
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) {
domain := env["DOMAIN"]
recipe, exists := env["RECIPE"]
if !exists {
recipe, exists = env["TYPE"]
if !exists {
return App{}, fmt.Errorf("%s is missing the TYPE env var?", name)
}
}
return App{
Name: name,
Domain: domain,
Recipe: recipe,
Env: env,
Server: appFile.Server,
Path: appFile.Path,
}, nil
}
// LoadAppFiles gets all app files for a given set of servers or all servers.
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(SERVERS_DIR)
if err != nil {
return appFiles, err
}
}
}
logrus.Debugf("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", "))
for _, server := range servers {
serverDir := path.Join(SERVERS_DIR, server)
files, err := GetAllFilesInDirectory(serverDir)
if err != nil {
return appFiles, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server)
}
for _, file := range files {
appName := strings.TrimSuffix(file.Name(), ".env")
appFilePath := path.Join(SERVERS_DIR, 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. It should only be used when ready to use the env file to keep IO
// operations down.
func GetApp(apps AppFiles, name AppName) (App, error) {
appFile, exists := apps[name]
if !exists {
return App{}, fmt.Errorf("cannot find app with name %s", name)
}
app, err := ReadAppEnvFile(appFile, name)
if err != nil {
return App{}, err
}
return app, nil
}
// GetApps returns a slice of Apps with their env files read from a given
// slice of AppFiles.
func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) {
var apps []App
for name := range appFiles {
app, err := GetApp(appFiles, name)
if err != nil {
return nil, err
}
if recipeFilter != "" {
if app.Recipe == recipeFilter {
apps = append(apps, app)
}
} else {
apps = append(apps, app)
}
}
return apps, nil
}
// GetAppServiceNames retrieves a list of app service names.
func GetAppServiceNames(appName string) ([]string, error) {
var serviceNames []string
appFiles, err := LoadAppFiles("")
if err != nil {
return serviceNames, err
}
app, err := GetApp(appFiles, appName)
if err != nil {
return serviceNames, err
}
composeFiles, err := GetComposeFiles(app.Recipe, app.Env)
if err != nil {
return serviceNames, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(app.Recipe, opts, app.Env)
if err != nil {
return serviceNames, err
}
for _, service := range compose.Services {
serviceNames = append(serviceNames, service.Name)
}
return serviceNames, nil
}
// GetAppNames retrieves a list of app names.
func GetAppNames() ([]string, error) {
var appNames []string
appFiles, err := LoadAppFiles("")
if err != nil {
return appNames, err
}
apps, err := GetApps(appFiles, "")
if err != nil {
return appNames, err
}
for _, app := range apps {
appNames = append(appNames, app.Name)
}
return appNames, nil
}
// TemplateAppEnvSample copies the example env file for the app into the users
// env files.
func TemplateAppEnvSample(recipeName, appName, server, domain string) error {
envSamplePath := path.Join(RECIPES_DIR, recipeName, ".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); !os.IsNotExist(err) {
return fmt.Errorf("%s already exists?", appEnvPath)
}
err = ioutil.WriteFile(appEnvPath, envSample, 0o664)
if err != nil {
return err
}
read, err := ioutil.ReadFile(appEnvPath)
if err != nil {
return err
}
newContents := strings.Replace(string(read), recipeName+".example.com", domain, -1)
err = ioutil.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil {
return err
}
logrus.Debugf("copied & templated %s to %s", envSamplePath, appEnvPath)
return nil
}
// SanitiseAppName makes a app name usable with Docker by replacing illegal
// characters.
func SanitiseAppName(name string) string {
return strings.ReplaceAll(name, ".", "_")
}
// GetAppStatuses queries servers to check the deployment status of given apps.
func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]string, error) {
statuses := make(map[string]map[string]string)
servers := make(map[string]struct{})
for _, app := range apps {
if _, ok := servers[app.Server]; !ok {
servers[app.Server] = struct{}{}
}
}
var bar *progressbar.ProgressBar
if !MachineReadable {
bar = formatter.CreateProgressbar(len(servers), "querying remote servers...")
}
ch := make(chan stack.StackStatus, len(servers))
for server := range servers {
cl, err := client.New(server)
if err != nil {
return statuses, err
}
go func(s string) {
ch <- stack.GetAllDeployedServices(cl, s)
if !MachineReadable {
bar.Add(1)
}
}(server)
}
for range servers {
status := <-ch
if status.Err != nil {
return statuses, status.Err
}
for _, service := range status.Services {
result := make(map[string]string)
name := service.Spec.Labels[convert.LabelNamespace]
if _, ok := statuses[name]; !ok {
result["status"] = "deployed"
}
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name)
chaos, ok := service.Spec.Labels[labelKey]
if ok {
result["chaos"] = chaos
}
labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name)
if chaosVersion, ok := service.Spec.Labels[labelKey]; ok {
result["chaosVersion"] = chaosVersion
}
labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name)
if autoUpdate, ok := service.Spec.Labels[labelKey]; ok {
result["autoUpdate"] = autoUpdate
} else {
result["autoUpdate"] = "false"
}
labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
if version, ok := service.Spec.Labels[labelKey]; ok {
result["version"] = version
} else {
continue
}
statuses[name] = result
}
}
logrus.Debugf("retrieved app statuses: %s", statuses)
return statuses, nil
}
// ensurePathExists ensures that a path exists.
func ensurePathExists(path string) error {
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
return err
}
return nil
}
// GetComposeFiles gets the list of compose files for an app (or recipe if you
// don't already have an app) which should be merged into a composetypes.Config
// while respecting the COMPOSE_FILE env var.
func GetComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
var composeFiles []string
composeFileEnvVar, ok := appEnv["COMPOSE_FILE"]
if !ok {
path := fmt.Sprintf("%s/%s/compose.yml", RECIPES_DIR, recipe)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
logrus.Debugf("no COMPOSE_FILE detected, loading default: %s", path)
composeFiles = append(composeFiles, path)
return composeFiles, nil
}
if !strings.Contains(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s/%s", RECIPES_DIR, recipe, composeFileEnvVar)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
logrus.Debugf("COMPOSE_FILE detected, loading %s", path)
composeFiles = append(composeFiles, path)
return composeFiles, nil
}
numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
if len(envVars) != numComposeFiles {
return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar)
}
for _, file := range envVars {
path := fmt.Sprintf("%s/%s/%s", RECIPES_DIR, recipe, file)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
composeFiles = append(composeFiles, path)
}
logrus.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
logrus.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), recipe)
return composeFiles, nil
}
// GetAppComposeConfig retrieves a compose specification for a recipe. This
// specification is the result of a merge of all the compose.**.yml files in
// the recipe repository.
func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv AppEnv) (*composetypes.Config, error) {
compose, err := loader.LoadComposefile(opts, appEnv)
if err != nil {
return &composetypes.Config{}, err
}
logrus.Debugf("retrieved %s for %s", compose.Filename, recipe)
return compose, nil
}
// ExposeAllEnv exposes all env variables to the app container
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("Add the following environment to the app service config of %s:", stackName)
for k, v := range appEnv {
_, exists := service.Environment[k]
if !exists {
value := v
service.Environment[k] = &value
logrus.Debugf("Add Key: %s Value: %s to %s", k, value, stackName)
}
}
}
}
}
// SetRecipeLabel adds the label 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' to the app container
// to signal which recipe is connected to the deployed app
func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName)
service.Deploy.Labels[labelKey] = recipe
}
}
}
// SetChaosLabel adds the label 'coop-cloud.${STACK_NAME}.chaos=true/false' to the app container
// to signal if the app is deployed in chaos mode
func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", stackName)
service.Deploy.Labels[labelKey] = strconv.FormatBool(chaos)
}
}
}
// SetChaosVersionLabel adds the label 'coop-cloud.${STACK_NAME}.chaos-version=$(GIT_COMMIT)' to the app container
func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos-version", stackName)
service.Deploy.Labels[labelKey] = chaosVersion
}
}
}
// SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the
// auto update process for this app. The default if this variable is not set is to disable
// the auto update process.
func SetUpdateLabel(compose *composetypes.Config, stackName string, appEnv AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
enable_auto_update, exists := appEnv["ENABLE_AUTO_UPDATE"]
if !exists {
enable_auto_update = "false"
}
logrus.Debugf("set label 'coop-cloud.%s.autoupdate' to %s for %s", stackName, enable_auto_update, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.autoupdate", stackName)
service.Deploy.Labels[labelKey] = enable_auto_update
}
}
}
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
for _, service := range compose.Services {
if service.Name == "app" {
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
logrus.Debugf("get label '%s'", labelKey)
if labelValue, ok := service.Deploy.Labels[labelKey]; ok {
return labelValue
}
}
}
logrus.Debugf("no %s label found for %s", label, stackName)
return ""
}
// GetTimeoutFromLabel reads the timeout value from docker label "coop-cloud.${STACK_NAME}.TIMEOUT" and returns 50 as default value
func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) {
timeout := 50 // Default Timeout
var err error = nil
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
logrus.Debugf("timeout label: %s", timeoutLabel)
timeout, err = strconv.Atoi(timeoutLabel)
}
return timeout, err
}

View File

@ -1,18 +1,14 @@
package config
import (
"bufio"
"fmt"
"io/fs"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"git.coopcloud.tech/coop-cloud/godotenv"
"github.com/sirupsen/logrus"
)
@ -21,11 +17,6 @@ const MAX_DOCKER_SECRET_LENGTH = 64
var BackupbotLabel = "coop-cloud.backupbot.enabled"
// envVarModifiers is a list of env var modifier strings. These are added to
// env vars as comments and modify their processing by Abra, e.g. determining
// how long secrets should be.
var envVarModifiers = []string{"length"}
// GetServers retrieves all servers.
func GetServers() ([]string, error) {
var servers []string
@ -40,34 +31,6 @@ func GetServers() ([]string, error) {
return servers, nil
}
// ReadEnv loads an app envivornment into a map.
func ReadEnv(filePath string) (AppEnv, error) {
var envVars AppEnv
envVars, _, err := godotenv.Read(filePath)
if err != nil {
return nil, err
}
logrus.Debugf("read %s from %s", envVars, filePath)
return envVars, nil
}
// ReadEnv loads an app envivornment and their modifiers in two different maps.
func ReadEnvWithModifiers(filePath string) (AppEnv, AppModifiers, error) {
var envVars AppEnv
envVars, mods, err := godotenv.Read(filePath)
if err != nil {
return nil, mods, err
}
logrus.Debugf("read %s from %s", envVars, filePath)
return envVars, mods, nil
}
// ReadServerNames retrieves all server names.
func ReadServerNames() ([]string, error) {
serverNames, err := GetAllFoldersInDirectory(SERVERS_DIR)
@ -143,119 +106,3 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
return folders, nil
}
// ReadAbraShEnvVars reads env vars from an abra.sh recipe file.
func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
envVars := make(map[string]string)
file, err := os.Open(abraSh)
if err != nil {
if os.IsNotExist(err) {
return envVars, nil
}
return envVars, err
}
defer file.Close()
exportRegex, err := regexp.Compile(`^export\s+(\w+=\w+)`)
if err != nil {
return envVars, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
txt := scanner.Text()
if exportRegex.MatchString(txt) {
splitVals := strings.Split(txt, "export ")
envVarDef := splitVals[len(splitVals)-1]
keyVal := strings.Split(envVarDef, "=")
if len(keyVal) != 2 {
return envVars, fmt.Errorf("couldn't parse %s", txt)
}
envVars[keyVal[0]] = keyVal[1]
}
}
if len(envVars) > 0 {
logrus.Debugf("read %s from %s", envVars, abraSh)
} else {
logrus.Debugf("read 0 env var exports from %s", abraSh)
}
return envVars, nil
}
type EnvVar struct {
Name string
Present bool
}
func CheckEnv(app App) ([]EnvVar, error) {
var envVars []EnvVar
envSamplePath := path.Join(RECIPES_DIR, app.Recipe, ".env.sample")
if _, err := os.Stat(envSamplePath); err != nil {
if os.IsNotExist(err) {
return envVars, fmt.Errorf("%s does not exist?", envSamplePath)
}
return envVars, err
}
envSample, err := ReadEnv(envSamplePath)
if err != nil {
return envVars, err
}
var keys []string
for key := range envSample {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if _, ok := app.Env[key]; ok {
envVars = append(envVars, EnvVar{Name: key, Present: true})
} else {
envVars = append(envVars, EnvVar{Name: key, Present: false})
}
}
return envVars, nil
}
// ReadAbraShCmdNames reads the names of commands.
func ReadAbraShCmdNames(abraSh string) ([]string, error) {
var cmdNames []string
file, err := os.Open(abraSh)
if err != nil {
if os.IsNotExist(err) {
return cmdNames, nil
}
return cmdNames, err
}
defer file.Close()
cmdNameRegex, err := regexp.Compile(`(\w+)(\(\).*\{)`)
if err != nil {
return cmdNames, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
matches := cmdNameRegex.FindStringSubmatch(line)
if len(matches) > 0 {
cmdNames = append(cmdNames, matches[1])
}
}
if len(cmdNames) > 0 {
logrus.Debugf("read %s from %s", strings.Join(cmdNames, " "), abraSh)
} else {
logrus.Debugf("read 0 command names from %s", abraSh)
}
return cmdNames, nil
}

View File

@ -6,6 +6,7 @@ import (
"os"
"path"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
@ -234,7 +235,7 @@ func LintAppService(recipe recipe.Recipe) (bool, error) {
// therefore no matching traefik deploy label will be present.
func LintTraefikEnabledSkipCondition(recipe recipe.Recipe) (bool, error) {
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
sampleEnv, err := app.ReadEnv(envSamplePath)
if err != nil {
return false, fmt.Errorf("Unable to discover .env.sample for %s", recipe.Name)
}

View File

@ -12,6 +12,7 @@ import (
"strconv"
"strings"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/compose"
"coopcloud.tech/abra/pkg/config"
@ -227,7 +228,7 @@ func Get(recipeName string, offline bool) (Recipe, error) {
}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
sampleEnv, err := app.ReadEnv(envSamplePath)
if err != nil {
return Recipe{}, err
}
@ -257,7 +258,7 @@ func Get(recipeName string, offline bool) (Recipe, error) {
func (r Recipe) SampleEnv() (map[string]string, error) {
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
sampleEnv, err := app.ReadEnv(envSamplePath)
if err != nil {
return sampleEnv, fmt.Errorf("unable to discover .env.sample for %s", r.Name)
}

View File

@ -11,6 +11,7 @@ import (
"strings"
"sync"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/upstream/stack"
@ -81,7 +82,7 @@ func GeneratePassphrases(count uint) ([]string, error) {
// "app new" case where we pass in the .env.sample and the "secret generate"
// case where the app is created.
func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName string) (map[string]Secret, error) {
appEnv, appModifiers, err := config.ReadEnvWithModifiers(appEnvPath)
appEnv, appModifiers, err := appPkg.ReadEnvWithModifiers(appEnvPath)
if err != nil {
return nil, err
}
@ -240,10 +241,10 @@ type secretStatuses []secretStatus
// PollSecretsStatus checks status of secrets by comparing the local recipe
// config and deploymend server state.
func PollSecretsStatus(cl *dockerClient.Client, app config.App) (secretStatuses, error) {
func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses, error) {
var secStats secretStatuses
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
composeFiles, err := appPkg.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
return secStats, err
}