145 lines
3.6 KiB
Go
145 lines
3.6 KiB
Go
package stack
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type Local struct {
|
|
Name string
|
|
ComposePath string
|
|
EnvExample string // path to .env.example, may not exist
|
|
}
|
|
|
|
// Discover finds all stacks in the services directory.
|
|
func Discover(servicesPath string) ([]Local, error) {
|
|
entries, err := os.ReadDir(servicesPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading services dir %s: %w", servicesPath, err)
|
|
}
|
|
|
|
var stacks []Local
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
composePath := filepath.Join(servicesPath, e.Name(), "docker-compose.yml")
|
|
if _, err := os.Stat(composePath); err != nil {
|
|
continue // no compose file, skip
|
|
}
|
|
s := Local{
|
|
Name: e.Name(),
|
|
ComposePath: composePath,
|
|
EnvExample: filepath.Join(servicesPath, e.Name(), ".env.example"),
|
|
}
|
|
stacks = append(stacks, s)
|
|
}
|
|
return stacks, nil
|
|
}
|
|
|
|
// Get returns a single local stack by name.
|
|
func Get(servicesPath, name string) (*Local, error) {
|
|
composePath := filepath.Join(servicesPath, name, "docker-compose.yml")
|
|
if _, err := os.Stat(composePath); err != nil {
|
|
return nil, fmt.Errorf("no docker-compose.yml found for stack %q in %s", name, servicesPath)
|
|
}
|
|
return &Local{
|
|
Name: name,
|
|
ComposePath: composePath,
|
|
EnvExample: filepath.Join(servicesPath, name, ".env.example"),
|
|
}, nil
|
|
}
|
|
|
|
// ReadCompose returns the raw compose file content.
|
|
func (s *Local) ReadCompose() (string, error) {
|
|
b, err := os.ReadFile(s.ComposePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
// EnvExampleKeys returns the variable names defined in .env.example.
|
|
// Returns empty slice if the file doesn't exist.
|
|
func (s *Local) EnvExampleKeys() ([]string, error) {
|
|
f, err := os.Open(s.EnvExample)
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
var keys []string
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
key, _, _ := strings.Cut(line, "=")
|
|
keys = append(keys, strings.TrimSpace(key))
|
|
}
|
|
return keys, scanner.Err()
|
|
}
|
|
|
|
// WriteEnvExample creates a .env.example file from a list of keys (values left empty).
|
|
// Existing file is overwritten.
|
|
func (s *Local) WriteEnvExample(keys []string) error {
|
|
var sb strings.Builder
|
|
sb.WriteString("# Generated by knecht from Portainer env vars\n")
|
|
for _, k := range keys {
|
|
sb.WriteString(k + "=\n")
|
|
}
|
|
return os.WriteFile(s.EnvExample, []byte(sb.String()), 0644)
|
|
}
|
|
|
|
// AppendEnvExample appends missing keys to an existing .env.example file.
|
|
func (s *Local) AppendEnvExample(keys []string) error {
|
|
f, err := os.OpenFile(s.EnvExample, os.O_APPEND|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
f.WriteString("\n# Added by knecht\n")
|
|
for _, k := range keys {
|
|
f.WriteString(k + "=\n")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ServicesPath resolves the services directory: uses cfg value if set,
|
|
// otherwise walks up from cwd to find the git root and appends /services.
|
|
func ServicesPath(cfgPath string) (string, error) {
|
|
if cfgPath != "" {
|
|
return cfgPath, nil
|
|
}
|
|
return findServicesDir()
|
|
}
|
|
|
|
func findServicesDir() (string, error) {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for {
|
|
if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
|
|
p := filepath.Join(dir, "services")
|
|
if _, err := os.Stat(p); err == nil {
|
|
return p, nil
|
|
}
|
|
return "", fmt.Errorf("found git root at %s but no services/ directory", dir)
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
break
|
|
}
|
|
dir = parent
|
|
}
|
|
return "", fmt.Errorf("could not find git root with a services/ directory")
|
|
}
|