All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			
		
			
				
	
	
		
			688 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			688 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package app
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"regexp"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 
 | |
| 	"coopcloud.tech/abra/pkg/client"
 | |
| 	"coopcloud.tech/abra/pkg/config"
 | |
| 	"coopcloud.tech/abra/pkg/envfile"
 | |
| 	"coopcloud.tech/abra/pkg/formatter"
 | |
| 	"coopcloud.tech/abra/pkg/i18n"
 | |
| 	"coopcloud.tech/abra/pkg/recipe"
 | |
| 	"coopcloud.tech/abra/pkg/upstream/convert"
 | |
| 	"coopcloud.tech/abra/pkg/upstream/stack"
 | |
| 
 | |
| 	"coopcloud.tech/abra/pkg/log"
 | |
| 	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"
 | |
| )
 | |
| 
 | |
| // Get retrieves an app
 | |
| func Get(appName string) (App, error) {
 | |
| 	files, err := LoadAppFiles("")
 | |
| 	if err != nil {
 | |
| 		return App{}, err
 | |
| 	}
 | |
| 
 | |
| 	app, err := GetApp(files, appName)
 | |
| 	if err != nil {
 | |
| 		return App{}, err
 | |
| 	}
 | |
| 
 | |
| 	log.Debug(i18n.G("loaded app %s: %s", appName, app))
 | |
| 
 | |
| 	return app, 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{}, errors.New(i18n.G("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.Name == 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 recipe.Recipe
 | |
| 	Domain string
 | |
| 	Env    envfile.AppEnv
 | |
| 	Server string
 | |
| 	Path   string
 | |
| }
 | |
| 
 | |
| // String outputs a human-friendly string representation.
 | |
| func (a App) String() string {
 | |
| 	out := fmt.Sprintf("{name: %s, ", a.Name)
 | |
| 	out += fmt.Sprintf("recipe: %s, ", a.Recipe)
 | |
| 	out += fmt.Sprintf("domain: %s, ", a.Domain)
 | |
| 	out += fmt.Sprintf("env %s, ", a.Env)
 | |
| 	out += fmt.Sprintf("server %s, ", a.Server)
 | |
| 	out += fmt.Sprintf("path %s}", a.Path)
 | |
| 	return out
 | |
| }
 | |
| 
 | |
| // Type aliases to make code hints easier to understand
 | |
| 
 | |
| // 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 {
 | |
| 		log.Debug(i18n.G("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 := a.Recipe.GetComposeFiles(a.Env)
 | |
| 	if err != nil {
 | |
| 		return filters, err
 | |
| 	}
 | |
| 
 | |
| 	opts := stack.Deploy{Composefiles: composeFiles}
 | |
| 	compose, err := GetAppComposeConfig(a.Recipe.Name, 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.Name) < strings.ToLower(a[j].Recipe.Name)
 | |
| 	}
 | |
| 	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.Name) < strings.ToLower(a[j].Recipe.Name)
 | |
| }
 | |
| 
 | |
| // 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 := envfile.ReadEnv(appFile.Path)
 | |
| 	if err != nil {
 | |
| 		return App{}, errors.New(i18n.G("env file for %s couldn't be read: %s", name, err.Error()))
 | |
| 	}
 | |
| 
 | |
| 	app, err := NewApp(env, name, appFile)
 | |
| 	if err != nil {
 | |
| 		return App{}, errors.New(i18n.G("env file for %s has issues: %s", name, err.Error()))
 | |
| 	}
 | |
| 
 | |
| 	return app, nil
 | |
| }
 | |
| 
 | |
| // NewApp creates new App object
 | |
| func NewApp(env envfile.AppEnv, name string, appFile AppFile) (App, error) {
 | |
| 	domain := env["DOMAIN"]
 | |
| 
 | |
| 	recipeName, exists := env["RECIPE"]
 | |
| 	if !exists {
 | |
| 		recipeName, exists = env["TYPE"]
 | |
| 		if !exists {
 | |
| 			return App{}, errors.New(i18n.G("%s is missing the TYPE env var?", name))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return App{
 | |
| 		Name:   name,
 | |
| 		Domain: domain,
 | |
| 		Recipe: recipe.Get(recipeName),
 | |
| 		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
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	log.Debug(i18n.G("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, errors.New(i18n.G("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 := app.Recipe.GetComposeFiles(app.Env)
 | |
| 	if err != nil {
 | |
| 		return serviceNames, err
 | |
| 	}
 | |
| 
 | |
| 	opts := stack.Deploy{Composefiles: composeFiles}
 | |
| 	compose, err := GetAppComposeConfig(app.Recipe.Name, 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(r recipe.Recipe, appName, server, domain string) error {
 | |
| 	envSample, err := os.ReadFile(r.SampleEnvPath)
 | |
| 	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 errors.New(i18n.G("%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),
 | |
| 		fmt.Sprintf("%s.example.com", r.Name),
 | |
| 		domain,
 | |
| 		-1,
 | |
| 	)
 | |
| 
 | |
| 	err = os.WriteFile(appEnvPath, []byte(newContents), 0)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	log.Debug(i18n.G("copied & templated %s to %s", r.SampleEnvPath, 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), i18n.G("querying remote servers..."))
 | |
| 	}
 | |
| 
 | |
| 	ch := make(chan stack.StackStatus, len(servers))
 | |
| 	for server := range servers {
 | |
| 		cl, err := client.New(server)
 | |
| 		if err != nil {
 | |
| 			log.Warn(err)
 | |
| 			ch <- stack.StackStatus{}
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		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.version", name)
 | |
| 			if version, ok := service.Spec.Labels[labelKey]; ok {
 | |
| 				result["version"] = version
 | |
| 			} else {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			statuses[name] = result
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	log.Debug(i18n.G("retrieved app statuses: %s", statuses))
 | |
| 
 | |
| 	return statuses, 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 envfile.AppEnv) (*composetypes.Config, error) {
 | |
| 	compose, err := loader.LoadComposefile(opts, appEnv)
 | |
| 	if err != nil {
 | |
| 		return &composetypes.Config{}, err
 | |
| 	}
 | |
| 
 | |
| 	log.Debug(i18n.G("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 envfile.AppEnv) {
 | |
| 	for _, service := range compose.Services {
 | |
| 		if service.Name == "app" {
 | |
| 			log.Debug(i18n.G("adding env vars to %s service config", stackName))
 | |
| 			for k, v := range appEnv {
 | |
| 				_, exists := service.Environment[k]
 | |
| 				if !exists {
 | |
| 					value := v
 | |
| 					service.Environment[k] = &value
 | |
| 					log.Debug(i18n.G("%s: %s: %s", stackName, k, value))
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func CheckEnv(app App) ([]envfile.EnvVar, error) {
 | |
| 	var envVars []envfile.EnvVar
 | |
| 
 | |
| 	envSample, err := app.Recipe.SampleEnv()
 | |
| 	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, envfile.EnvVar{Name: key, Present: true})
 | |
| 		} else {
 | |
| 			envVars = append(envVars, envfile.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 {
 | |
| 		log.Debug(i18n.G("read %s from %s", strings.Join(cmdNames, " "), abraSh))
 | |
| 	} else {
 | |
| 		log.Debug(i18n.G("read 0 command names from %s", abraSh))
 | |
| 	}
 | |
| 
 | |
| 	return cmdNames, nil
 | |
| }
 | |
| 
 | |
| // Wipe removes the version from the app .env file.
 | |
| func (a App) WipeRecipeVersion() error {
 | |
| 	file, err := os.Open(a.Path)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	var (
 | |
| 		lines   []string
 | |
| 		scanner = bufio.NewScanner(file)
 | |
| 	)
 | |
| 
 | |
| 	for scanner.Scan() {
 | |
| 		line := scanner.Text()
 | |
| 		if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
 | |
| 			lines = append(lines, line)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if strings.HasPrefix(line, "#") {
 | |
| 			lines = append(lines, line)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		splitted := strings.Split(line, ":")
 | |
| 		lines = append(lines, splitted[0])
 | |
| 	}
 | |
| 
 | |
| 	if err := scanner.Err(); err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	log.Debug(i18n.G("version wiped from %s.env", a.Domain))
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // WriteRecipeVersion writes the recipe version to the app .env file.
 | |
| func (a App) WriteRecipeVersion(version string, dryRun bool) error {
 | |
| 	file, err := os.Open(a.Path)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	var (
 | |
| 		dirtyVersion string
 | |
| 		skipped      bool
 | |
| 		lines        []string
 | |
| 		scanner      = bufio.NewScanner(file)
 | |
| 	)
 | |
| 
 | |
| 	for scanner.Scan() {
 | |
| 		line := scanner.Text()
 | |
| 		if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
 | |
| 			lines = append(lines, line)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if strings.HasPrefix(line, "#") {
 | |
| 			lines = append(lines, line)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if strings.Contains(line, version) && !a.Recipe.Dirty && !strings.HasSuffix(line, config.DIRTY_DEFAULT) {
 | |
| 			skipped = true
 | |
| 			lines = append(lines, line)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		splitted := strings.Split(line, ":")
 | |
| 
 | |
| 		line = fmt.Sprintf("%s:%s", splitted[0], version)
 | |
| 		lines = append(lines, line)
 | |
| 	}
 | |
| 
 | |
| 	if err := scanner.Err(); err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	if a.Recipe.Dirty && dirtyVersion != "" {
 | |
| 		version = dirtyVersion
 | |
| 	}
 | |
| 
 | |
| 	if !dryRun {
 | |
| 		if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		log.Debug(i18n.G("skipping writing version %s because dry run", version))
 | |
| 	}
 | |
| 
 | |
| 	if !skipped {
 | |
| 		log.Debug(i18n.G("version %s saved to %s.env", version, a.Domain))
 | |
| 	} else {
 | |
| 		log.Debug(i18n.G("skipping version %s write as already exists in %s.env", version, a.Domain))
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 |