@ -91,6 +91,17 @@ type App struct {
|
||||
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
|
||||
@ -492,13 +503,13 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv
|
||||
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) {
|
||||
for _, service := range compose.Services {
|
||||
if service.Name == "app" {
|
||||
log.Debugf("add the following environment to the app service config of %s:", stackName)
|
||||
log.Debugf("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.Debugf("add env var: %s value: %s to %s", k, value, stackName)
|
||||
log.Debugf("%s: %s: %s", stackName, k, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -567,16 +578,19 @@ func ReadAbraShCmdNames(abraSh string) ([]string, error) {
|
||||
return cmdNames, nil
|
||||
}
|
||||
|
||||
func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||
// 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()
|
||||
|
||||
skipped := false
|
||||
scanner := bufio.NewScanner(file)
|
||||
lines := []string{}
|
||||
var (
|
||||
lines []string
|
||||
scanner = bufio.NewScanner(file)
|
||||
)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
|
||||
@ -589,13 +603,71 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, version) {
|
||||
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.Debugf("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, ":")
|
||||
|
||||
if a.Recipe.Dirty {
|
||||
dirtyVersion = fmt.Sprintf("%s%s", version, config.DIRTY_DEFAULT)
|
||||
if strings.Contains(line, dirtyVersion) {
|
||||
skipped = true
|
||||
lines = append(lines, line)
|
||||
continue
|
||||
}
|
||||
|
||||
line = fmt.Sprintf("%s:%s", splitted[0], dirtyVersion)
|
||||
lines = append(lines, line)
|
||||
continue
|
||||
}
|
||||
|
||||
line = fmt.Sprintf("%s:%s", splitted[0], version)
|
||||
lines = append(lines, line)
|
||||
}
|
||||
@ -604,6 +676,10 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||
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)
|
||||
|
@ -198,3 +198,41 @@ func compareFilter(t *testing.T, f1 filters.Args, f2 map[string]map[string]bool)
|
||||
t.Errorf("filters mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteRecipeVersionOverwrite(t *testing.T) {
|
||||
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer t.Cleanup(func() {
|
||||
if err := app.WipeRecipeVersion(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
assert.Equal(t, "", app.Recipe.Version)
|
||||
|
||||
if err := app.WriteRecipeVersion("foo", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app, err = appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "foo", app.Recipe.Version)
|
||||
|
||||
app.Recipe.Dirty = true
|
||||
if err := app.WriteRecipeVersion("foo+U", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app, err = appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "foo+U", app.Recipe.Version)
|
||||
}
|
||||
|
@ -114,6 +114,8 @@ var (
|
||||
// complained yet!
|
||||
CHAOS_DEFAULT = "false"
|
||||
|
||||
DIRTY_DEFAULT = "+U"
|
||||
|
||||
NO_DOMAIN_DEFAULT = "N/A"
|
||||
NO_VERSION_DEFAULT = "N/A"
|
||||
)
|
||||
|
@ -13,11 +13,15 @@ import (
|
||||
"github.com/docker/go-units"
|
||||
"golang.org/x/term"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
)
|
||||
|
||||
var BoldStyle = lipgloss.NewStyle().
|
||||
Bold(true)
|
||||
|
||||
var BoldUnderlineStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Underline(true)
|
||||
|
||||
@ -102,7 +106,6 @@ func CreateOverview(header string, rows [][]string) string {
|
||||
var borderStyle = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
Padding(0, 1, 0, 1).
|
||||
MaxWidth(79).
|
||||
BorderForeground(lipgloss.Color("63"))
|
||||
|
||||
var headerStyle = lipgloss.NewStyle().
|
||||
@ -110,9 +113,7 @@ func CreateOverview(header string, rows [][]string) string {
|
||||
Bold(true).
|
||||
PaddingBottom(1)
|
||||
|
||||
var leftStyle = lipgloss.NewStyle().
|
||||
Bold(true)
|
||||
|
||||
var leftStyle = lipgloss.NewStyle()
|
||||
var rightStyle = lipgloss.NewStyle()
|
||||
|
||||
var longest int
|
||||
@ -138,10 +139,20 @@ func CreateOverview(header string, rows [][]string) string {
|
||||
offset = offset + " "
|
||||
}
|
||||
|
||||
renderedRows = append(
|
||||
renderedRows,
|
||||
horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1])),
|
||||
)
|
||||
rendered := horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1]))
|
||||
if row[1] == "---" {
|
||||
rendered = horizontal(
|
||||
leftStyle.
|
||||
Bold(true).
|
||||
Underline(true).
|
||||
PaddingTop(1).
|
||||
Render(row[0]),
|
||||
offset,
|
||||
rightStyle.Render(""),
|
||||
)
|
||||
}
|
||||
|
||||
renderedRows = append(renderedRows, rendered)
|
||||
}
|
||||
|
||||
body := strings.Builder{}
|
||||
@ -242,3 +253,13 @@ func ByteCountSI(b uint64) string {
|
||||
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
// BoldDirtyDefault ensures a dirty modifier is rendered in bold.
|
||||
func BoldDirtyDefault(v string) string {
|
||||
if strings.HasSuffix(v, config.DIRTY_DEFAULT) {
|
||||
vBold := BoldStyle.Render(config.DIRTY_DEFAULT)
|
||||
v = strings.Replace(v, config.DIRTY_DEFAULT, vBold, 1)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
11
pkg/formatter/formatter_test.go
Normal file
11
pkg/formatter/formatter_test.go
Normal file
@ -0,0 +1,11 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBoldDirtyDefault(t *testing.T) {
|
||||
assert.Equal(t, "foo", BoldDirtyDefault("foo"))
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
@ -17,12 +19,16 @@ import (
|
||||
func IsClean(repoPath string) (bool, error) {
|
||||
repo, err := git.PlainOpen(repoPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
if errors.Is(err, git.ErrRepositoryNotExists) {
|
||||
return false, git.ErrRepositoryNotExists
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("unable to open %s: %s", repoPath, err)
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("unable to open worktree of %s: %s", repoPath, err)
|
||||
}
|
||||
|
||||
patterns, err := GetExcludesFiles()
|
||||
@ -36,14 +42,14 @@ func IsClean(repoPath string) (bool, error) {
|
||||
|
||||
status, err := worktree.Status()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("unable to query status of %s: %s", repoPath, err)
|
||||
}
|
||||
|
||||
if status.String() != "" {
|
||||
noNewline := strings.TrimSuffix(status.String(), "\n")
|
||||
log.Debugf("discovered git status in %s: %s", repoPath, noNewline)
|
||||
log.Debugf("git status: %s: %s", repoPath, noNewline)
|
||||
} else {
|
||||
log.Debugf("discovered clean git status in %s", repoPath)
|
||||
log.Debugf("git status: %s: clean", repoPath)
|
||||
}
|
||||
|
||||
return status.IsClean(), nil
|
||||
|
15
pkg/git/read_test.go
Normal file
15
pkg/git/read_test.go
Normal file
@ -0,0 +1,15 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsClean(t *testing.T) {
|
||||
isClean, err := IsClean("/tmp")
|
||||
assert.Equal(t, isClean, false)
|
||||
assert.True(t, errors.Is(err, git.ErrRepositoryNotExists))
|
||||
}
|
@ -137,8 +137,7 @@ func (r Recipe) EnsureIsClean() error {
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
|
||||
return fmt.Errorf(msg, r.Name, r.Dir)
|
||||
return fmt.Errorf("%s (%s) has locally unstaged changes?", r.Name, r.Dir)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -230,8 +229,23 @@ func (r Recipe) EnsureUpToDate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsDirty checks whether a recipe is dirty or not. N.B., if you call IsDirty
|
||||
// from another Recipe method, you should propagate the pointer reference (*).
|
||||
func (r *Recipe) IsDirty() error {
|
||||
isClean, err := gitPkg.IsClean(r.Dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
r.Dirty = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChaosVersion constructs a chaos mode recipe version.
|
||||
func (r Recipe) ChaosVersion() (string, error) {
|
||||
func (r *Recipe) ChaosVersion() (string, error) {
|
||||
var version string
|
||||
|
||||
head, err := r.Head()
|
||||
@ -241,15 +255,10 @@ func (r Recipe) ChaosVersion() (string, error) {
|
||||
|
||||
version = formatter.SmallSHA(head.String())
|
||||
|
||||
isClean, err := gitPkg.IsClean(r.Dir)
|
||||
if err != nil {
|
||||
if err := r.IsDirty(); err != nil {
|
||||
return version, err
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
version = fmt.Sprintf("%s+U", version)
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
|
39
pkg/recipe/git_test.go
Normal file
39
pkg/recipe/git_test.go
Normal file
@ -0,0 +1,39 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsDirty(t *testing.T) {
|
||||
r := Get("abra-test-recipe")
|
||||
|
||||
if err := r.EnsureExists(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := r.IsDirty(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.False(t, r.Dirty)
|
||||
|
||||
fpath := filepath.Join(r.Dir, "foo.txt")
|
||||
f, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
defer t.Cleanup(func() {
|
||||
os.Remove(fpath)
|
||||
})
|
||||
|
||||
if err := r.IsDirty(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.True(t, r.Dirty)
|
||||
}
|
@ -2,6 +2,7 @@ package recipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
@ -20,6 +21,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/web"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
// RecipeCatalogueURL is the only current recipe catalogue available.
|
||||
@ -131,7 +133,12 @@ func Get(name string) Recipe {
|
||||
log.Fatalf("version seems invalid: %s", name)
|
||||
}
|
||||
name = split[0]
|
||||
|
||||
version = split[1]
|
||||
if strings.HasSuffix(version, config.DIRTY_DEFAULT) {
|
||||
version = strings.Replace(split[1], config.DIRTY_DEFAULT, "", 1)
|
||||
log.Debugf("removed dirty suffix from .env version: %s -> %s", split[1], version)
|
||||
}
|
||||
}
|
||||
|
||||
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, name)
|
||||
@ -151,7 +158,7 @@ func Get(name string) Recipe {
|
||||
|
||||
dir := path.Join(config.RECIPES_DIR, escapeRecipeName(name))
|
||||
|
||||
return Recipe{
|
||||
r := Recipe{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Dir: dir,
|
||||
@ -163,11 +170,18 @@ func Get(name string) Recipe {
|
||||
SampleEnvPath: path.Join(dir, ".env.sample"),
|
||||
AbraShPath: path.Join(dir, "abra.sh"),
|
||||
}
|
||||
|
||||
if err := r.IsDirty(); err != nil && !errors.Is(err, git.ErrRepositoryNotExists) {
|
||||
log.Fatalf("failed to check git status of %s: %s", r.Name, err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type Recipe struct {
|
||||
Name string
|
||||
Version string
|
||||
Dirty bool // NOTE(d1): git terminology for unstaged changes
|
||||
Dir string
|
||||
GitURL string
|
||||
SSHURL string
|
||||
@ -178,6 +192,21 @@ type Recipe struct {
|
||||
AbraShPath string
|
||||
}
|
||||
|
||||
// String outputs a human-friendly string representation.
|
||||
func (r Recipe) String() string {
|
||||
out := fmt.Sprintf("{name: %s, ", r.Name)
|
||||
out += fmt.Sprintf("version : %s, ", r.Version)
|
||||
out += fmt.Sprintf("dirty: %v, ", r.Dirty)
|
||||
out += fmt.Sprintf("dir: %s, ", r.Dir)
|
||||
out += fmt.Sprintf("git url: %s, ", r.GitURL)
|
||||
out += fmt.Sprintf("ssh url: %s, ", r.SSHURL)
|
||||
out += fmt.Sprintf("compose: %s, ", r.ComposePath)
|
||||
out += fmt.Sprintf("readme: %s, ", r.ReadmePath)
|
||||
out += fmt.Sprintf("sample env: %s, ", r.SampleEnvPath)
|
||||
out += fmt.Sprintf("abra.sh: %s}", r.AbraShPath)
|
||||
return out
|
||||
}
|
||||
|
||||
func escapeRecipeName(recipeName string) string {
|
||||
recipeName = strings.ReplaceAll(recipeName, "/", "_")
|
||||
recipeName = strings.ReplaceAll(recipeName, ".", "_")
|
||||
|
@ -105,3 +105,8 @@ func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
|
||||
assert.NotEqual(t, label, defaultTimeoutLabel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirtyMarkerRemoved(t *testing.T) {
|
||||
r := Get("abra-test-recipe:1e83340e+U")
|
||||
assert.Equal(t, "1e83340e", r.Version)
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
|
||||
stdlibErr "errors"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
@ -112,7 +113,7 @@ func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string)
|
||||
IsDeployed: false,
|
||||
Version: "unknown",
|
||||
IsChaos: false,
|
||||
ChaosVersion: "false", // NOTE(d1): match string type used on label
|
||||
ChaosVersion: config.CHAOS_DEFAULT,
|
||||
}
|
||||
|
||||
filter := filters.NewArgs()
|
||||
|
Reference in New Issue
Block a user