abra/config/env.go
decentral1se 932803453e
WIP: still hacking on the app new command
Finally had to fork godotenv because it strips comments and we need
those to parse length values (e.g. "FOO=v1  # length=10") (or in other
words, motivation to move to the YAML format).

There is a new secret module now, with functionality for dealing with
generation and parsing of secrets.

The final output needs some work and there is also the final step of
implementing the sending of secrets to the docker daemon. Coming Soon
™️.
2021-07-31 12:49:22 +02:00

327 lines
8.2 KiB
Go

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{})
for _, appFile := range a {
if _, ok := servers[appFile.Server]; !ok {
servers[appFile.Server] = struct{}{}
unique = append(unique, appFile.Server)
}
}
return unique
}
func LoadAppFiles(servers ...string) (AppFiles, error) {
appFiles := make(AppFiles)
if len(servers) == 1 && 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 and
// because this exits with code 1 if the file cannot be found or is malformed
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))
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)
if err != nil {
return nil, err
}
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 {
return nil, err
}
return serverNames, nil
}
// getAllFilesInDirectory returns filenames of all files in directory
func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
var realFiles []fs.FileInfo
files, err := ioutil.ReadDir(directory)
if err != nil {
return nil, err
}
for _, file := range files {
// Follow any symlinks
filePath := path.Join(directory, file.Name())
realPath, err := filepath.EvalSymlinks(filePath)
if err != nil {
logrus.Warningf("broken symlink in your abra config folders: '%s'", filePath)
} else {
realFile, err := os.Stat(realPath)
if err != nil {
return nil, err
}
if !realFile.IsDir() {
realFiles = append(realFiles, file)
}
}
}
return realFiles, nil
}
// getAllFoldersInDirectory returns both folder and symlink paths
func getAllFoldersInDirectory(directory string) ([]string, error) {
var folders []string
files, err := ioutil.ReadDir(directory)
if err != nil {
return nil, err
}
if len(files) == 0 {
return nil, fmt.Errorf("directory is empty: '%s'", directory)
}
for _, file := range files {
// Check if file is directory or symlink
if file.IsDir() || file.Mode()&fs.ModeSymlink != 0 {
filePath := path.Join(directory, file.Name())
realDir, err := filepath.EvalSymlinks(filePath)
if err != nil {
logrus.Warningf("broken symlink in your abra config folders: '%s'", filePath)
} else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() {
// path is a directory
folders = append(folders, file.Name())
}
}
}
return folders, nil
}
func EnsureAbraDirExists() error {
if _, err := os.Stat(ABRA_DIR); os.IsNotExist(err) {
if err := os.Mkdir(ABRA_DIR, 0777); err != nil {
return err
}
}
return nil
}