Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 70 additions & 6 deletions internal/cli/adopt.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package cli
import (
"context"
"errors"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/ansibleforms/installer/internal/compose"
"github.com/ansibleforms/installer/internal/detect"
"github.com/ansibleforms/installer/internal/i18n"
"github.com/ansibleforms/installer/internal/kube"
"github.com/ansibleforms/installer/internal/releases"
"github.com/ansibleforms/installer/internal/state"
"github.com/ansibleforms/installer/internal/ui"
)
Expand Down Expand Up @@ -80,7 +83,11 @@ func reason(i detect.Install) string {
// an RTE tells its version only with its token
key = "detect.why_unmanagedRunner"
}
return i18n.T(key, "dir", i.Dir, "port", i.Port, "version", orStr(i.Version, i18n.T("detect.versionUnknown")), "app", orStr(i.ArgoApp, "?"))
if i.Problem == detect.NoComposeFile && os.Geteuid() == 0 {
// root may read any file : what stops it is the file's own (a mode, an SELinux label)
key = "detect.why_noComposeFileRoot"
}
return i18n.T(key, "dir", i.Dir, "file", fileOf(i), "port", i.Port, "version", orStr(i.Version, i18n.T("detect.versionUnknown")), "app", orStr(i.ArgoApp, "?"))
}

// foundLine is an installation found on one line: what and where, and why it cannot be
Expand All @@ -98,7 +105,44 @@ func foundLine(i detect.Install) string {
if i.Adoptable() || i.Problem == detect.Unmanaged {
return describe(i)
}
return describe(i) + ": " + i18n.T("detect.short_"+i.Problem, "app", orStr(i.ArgoApp, "?"))
return describe(i) + ": " + i18n.T("detect.short_"+i.Problem, "app", orStr(i.ArgoApp, "?"), "file", fileOf(i))
}

// fileOf is the name of the file an installation's problem is about (its compose file, its
// .env), for the texts that name it.
//
// Args:
//
// i: the installation.
//
// Returns:
//
// The file's name, docker-compose.yml when the problem names none.
func fileOf(i detect.Install) string {
if i.ProblemFile == "" {
return "docker-compose.yml"
}
return filepath.Base(i.ProblemFile)
}

// current tells whether an installation found already runs the version the installer
// installs: taking it over then changes nothing, its next upgrade comes later. A Runner in
// database mode is never current, its move to a socket mode is an upgrade of its own.
//
// Args:
//
// i: the installation.
// latest: the version the installer installs (zero when unknown).
//
// Returns:
//
// True when it runs that version or a newer one.
func current(i detect.Install, latest releases.Version) bool {
if latest == (releases.Version{}) || i.Database != nil {
return false
}
v, err := releases.ParseVersion(i.Version)
return err == nil && v.Compare(latest) >= 0
}

// foundAdvice is what to do about the installations that cannot be taken over: the advice
Expand Down Expand Up @@ -190,11 +234,27 @@ func offerFound(ctx context.Context, f Flags, found []detect.Install, alongside
}
return adopt(ctx, adoptable[0], false)
}
// the version the installer installs : one found that runs it already is taken over
// without an upgrade, and said so (zero when it cannot be known : an upgrade then)
latest, _ := targetVersion(ctx, f)
allCurrent := len(adoptable) > 0
for _, i := range adoptable {
if !current(i, latest) {
allCurrent = false
}
}
for {
help := i18n.T("detect.found") + "\n\n" + foundList(found) + "\n\n"
if len(adoptable) > 0 {
title := i18n.T("detect.found")
if len(found) == 1 && current(found[0], latest) {
title = i18n.T("detect.foundCurrent", "version", found[0].Version)
}
help := title + "\n\n" + foundList(found) + "\n\n"
switch {
case allCurrent:
help += i18n.T("detect.foundHelpCurrent")
case len(adoptable) > 0:
help += i18n.T("detect.foundHelp")
} else {
default:
help += i18n.T("detect.foundHelpNone")
}
var opts []ui.Option
Expand All @@ -207,7 +267,11 @@ func offerFound(ctx context.Context, f Flags, found []detect.Install, alongside
if def == "stop" {
def = "adopt:" + strconv.Itoa(n)
}
opts = append(opts, ui.Option{Label: i18n.T("detect.adopt", "what", describe(i)), Value: "adopt:" + strconv.Itoa(n), Hint: i18n.T("detect.adoptHint")})
label, hint := i18n.T("detect.adopt", "what", describe(i)), i18n.T("detect.adoptHint")
if current(i, latest) {
label, hint = i18n.T("detect.adoptCurrent", "what", describe(i)), i18n.T("detect.adoptCurrentHint")
}
opts = append(opts, ui.Option{Label: label, Value: "adopt:" + strconv.Itoa(n), Hint: hint})
}
if alongside {
opts = append(opts, ui.Option{Label: i18n.T("detect.alongside"), Value: "alongside", Hint: i18n.T("detect.alongsideHint")})
Expand Down
40 changes: 40 additions & 0 deletions internal/cli/adopt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"context"
"errors"
"os"
"strings"
"testing"

"github.com/ansibleforms/installer/internal/detect"
"github.com/ansibleforms/installer/internal/releases"
"github.com/ansibleforms/installer/internal/state"
"github.com/ansibleforms/installer/internal/ui"
)
Expand Down Expand Up @@ -72,3 +74,41 @@ func TestOfferFoundNothingAdoptable(t *testing.T) {
t.Errorf("no reason given")
}
}

// one found at the version the installer installs is taken over without an upgrade, and said
// so; an older one, or a Runner in database mode, is upgraded
func TestCurrent(t *testing.T) {
latest := releases.Version{Major: 7, Minor: 3}
at := handMade
at.Version = "7.3.0"
if !current(at, latest) {
t.Errorf("7.3.0 is not current at 7.3.0")
}
if current(handMade, latest) {
t.Errorf("7.2.0 is current at 7.3.0")
}
if current(at, releases.Version{}) {
t.Errorf("current when the latest is unknown")
}
at.Database = &detect.DatabaseRunner{}
if current(at, latest) {
t.Errorf("a Runner in database mode is current")
}
}

// the compose file's problems name the file, and root is not told to run as root
func TestReasonNamesTheFile(t *testing.T) {
i := handMade
i.Problem, i.ProblemFile = detect.MissingComposeFile, "/opt/docker/.env"
if r := reason(i); !strings.Contains(r, ".env") || !strings.Contains(r, "/opt/docker") {
t.Errorf("reason: %s", r)
}
i.Problem, i.ProblemFile = detect.NoComposeFile, "/opt/docker/docker-compose.yaml"
r := reason(i)
if !strings.Contains(r, "docker-compose.yaml") {
t.Errorf("reason: %s", r)
}
if os.Geteuid() == 0 && strings.Contains(r, "as root or") {
t.Errorf("root told to run as root: %s", r)
}
}
46 changes: 46 additions & 0 deletions internal/compose/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ func Backup(ctx context.Context, st *state.State) (string, error) {
if st.Role == RoleRunner {
return "", errors.New(i18n.T("compose.runnerNoBackup"))
}
if err := repoComposeName(st.Dir); err != nil {
return "", err
}
dir := backupDir(st)
if err := os.MkdirAll(dir, 0o700); err != nil {
return "", err
Expand Down Expand Up @@ -94,6 +97,46 @@ func restore(ctx context.Context, st *state.State, dump string) error {
return system.FromFile(ctx, st.Dir, dump, name, args...)
}

// repoComposeName makes the folder's compose file docker-compose.yml, the name the docker
// repository uses and the upgrade writes. A project taken over that was started from
// compose.yaml, compose.yml or docker-compose.yaml has that file renamed, and the other
// compose files beside it, which compose would not read, moved aside (<name>.<time>.bak):
// otherwise compose would go on reading the old file after the upgrade wrote the new one.
//
// Args:
//
// dir: the installation folder.
//
// Errors:
//
// When a file cannot be renamed.
func repoComposeName(dir string) error {
var found []string
for _, name := range composeNames[:4] {
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
found = append(found, name)
}
}
// none (the upgrade says so when it reads it), or docker-compose.yml alone: it is the last
// name compose looks for, so the first one found only when no other is there
if len(found) == 0 || found[0] == "docker-compose.yml" {
return nil
}
stamp := time.Now().Format("20060102-150405")
for _, name := range found[1:] {
aside := name + "." + stamp + ".bak"
if err := os.Rename(filepath.Join(dir, name), filepath.Join(dir, aside)); err != nil {
return err
}
ui.Muted(i18n.T("compose.composeAside", "file", name, "aside", aside))
}
if err := os.Rename(filepath.Join(dir, found[0]), filepath.Join(dir, "docker-compose.yml")); err != nil {
return err
}
ui.Muted(i18n.T("compose.composeRenamed", "file", found[0]))
return nil
}

// ─── upgrade ──────────────────────────────────────────────────────────────────

// Undo is what a failed upgrade needs to go back: the version, compose file and dump before,
Expand Down Expand Up @@ -126,6 +169,9 @@ type Undo struct {
// When a step fails.
func Upgrade(ctx context.Context, st *state.State, to string, prep *Prep) (*Undo, error) {
undo := &Undo{Version: st.Version}
if err := repoComposeName(st.Dir); err != nil {
return nil, err
}
composeFile := filepath.Join(st.Dir, "docker-compose.yml")
old, err := os.ReadFile(composeFile)
if err != nil {
Expand Down
59 changes: 59 additions & 0 deletions internal/compose/ops_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package compose

import (
"os"
"path/filepath"
"strings"
"testing"
)

// a folder's files, by name and content
func folder(t *testing.T, files map[string]string) string {
t.Helper()
dir := t.TempDir()
for name, content := range files {
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
return dir
}

// a project taken over from docker-compose.yaml has it renamed docker-compose.yml, and the
// compose files compose would not read moved aside: the upgrade's file is then the one read
func TestRepoComposeName(t *testing.T) {
dir := folder(t, map[string]string{"docker-compose.yaml": "read", "docker-compose.yml": "stale"})
if err := repoComposeName(dir); err != nil {
t.Fatal(err)
}
if data, _ := os.ReadFile(filepath.Join(dir, "docker-compose.yml")); string(data) != "read" {
t.Errorf("docker-compose.yml holds %q, want the file compose read", data)
}
if _, err := os.Stat(filepath.Join(dir, "docker-compose.yaml")); err == nil {
t.Errorf("docker-compose.yaml left beside it")
}
entries, _ := os.ReadDir(dir)
aside := 0
for _, e := range entries {
if strings.HasPrefix(e.Name(), "docker-compose.yml.") && strings.HasSuffix(e.Name(), ".bak") {
aside++
}
}
if aside != 1 {
t.Errorf("%d files moved aside, want 1: %v", aside, entries)
}
}

// docker-compose.yml alone, or no compose file: nothing changes
func TestRepoComposeNameUnchanged(t *testing.T) {
dir := folder(t, map[string]string{"docker-compose.yml": "x"})
if err := repoComposeName(dir); err != nil {
t.Fatal(err)
}
if entries, _ := os.ReadDir(dir); len(entries) != 1 {
t.Errorf("files changed: %v", entries)
}
if err := repoComposeName(t.TempDir()); err != nil {
t.Errorf("no compose file: %v", err)
}
}
Loading
Loading