Compare commits

...

12 Commits

Author SHA1 Message Date
3wc
7c3b740e14 Update the server used to deploy the installer script
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-07 15:01:31 +01:00
2fbef41a3a
test: clean up properly
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-24 14:29:54 +02:00
6fb41e5300 fix: dont parse chaos version
All checks were successful
continuous-integration/drone/push Build is passing
See #547
2025-04-24 11:24:14 +00:00
1432f480c7 fix: -T/--tty disables TTY remote request
All checks were successful
continuous-integration/drone/push Build is passing
See #499
2025-04-24 08:57:53 +00:00
83af39771b
test: drop unused tagHash
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-24 10:57:34 +02:00
4d1333202e
test: flaky test when no RC is available
All checks were successful
continuous-integration/drone/push Build is passing
Fixes https://build.coopcloud.tech/toolshed/abra/2760/1/5
2025-04-24 10:33:49 +02:00
55c24f070c
feat: cancel git clone ops gracefully
All checks were successful
continuous-integration/drone/push Build is passing
See #528
2025-04-22 22:56:10 +02:00
229e8eb9da feat: --ssh/--force for recipe fetch
All checks were successful
continuous-integration/drone/push Build is passing
See toolshed/organising#546
2025-04-22 09:35:36 +00:00
b3ab95750e
fix: trim final newline on release note
All checks were successful
continuous-integration/drone/push Build is passing
Follow-up #544
2025-04-22 09:23:28 +02:00
de009921a2 fix: show release notes once
All checks were successful
continuous-integration/drone/push Build is passing
See #543
2025-04-22 05:47:03 +00:00
d081bbaefa
feat: auto select single server
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#513
2025-04-21 21:06:29 +02:00
515b5466ca
docs: add missing arg
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #540
2025-04-21 20:17:39 +02:00
17 changed files with 315 additions and 65 deletions

View File

@ -183,7 +183,7 @@ does not).`,
if err := internal.RunCmdRemote(
cl,
app,
requestTTY,
disableTTY,
app.Recipe.AbraShPath,
targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil {
log.Fatal(err)
@ -238,7 +238,7 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) {
var (
local bool
remoteUser string
requestTTY bool
disableTTY bool
)
func init() {
@ -259,11 +259,11 @@ func init() {
)
AppCmdCommand.Flags().BoolVarP(
&requestTTY,
&disableTTY,
"tty",
"T",
false,
"request remote TTY",
"disable remote TTY",
)
AppCmdCommand.Flags().BoolVarP(

View File

@ -33,7 +33,7 @@ var AppCpCommand = &cobra.Command{
abra app cp 1312.net myfile.txt app:/
# copy that file back to your current working directory locally
abra app cp 1312.net app:/myfile.txt`,
abra app cp 1312.net app:/myfile.txt ./`,
Args: cobra.ExactArgs(3),
ValidArgsFunction: func(
cmd *cobra.Command,

View File

@ -142,7 +142,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
appStats.AutoUpdate = autoUpdate
var newUpdates []string
if version != "unknown" {
if version != "unknown" && chaosVersion == "unknown" {
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatalf("unable to clone %s: %s", app.Name, err)
}

View File

@ -302,6 +302,12 @@ func ensureServerFlag() error {
return err
}
if len(servers) == 1 {
newAppServer = servers[0]
log.Infof("single server detected, choosing %s automatically", newAppServer)
return nil
}
if newAppServer == "" && !internal.NoInput {
prompt := &survey.Select{
Message: "Select app server:",

View File

@ -3,6 +3,7 @@ package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
@ -213,9 +214,7 @@ beforehand. See "abra app backup" for more.`,
return
}
if upgradeReleaseNotes != "" && chosenUpgrade != "" {
fmt.Print(upgradeReleaseNotes)
} else {
if upgradeReleaseNotes == "" {
upgradeWarnMessages = append(
upgradeWarnMessages,
fmt.Sprintf("no release notes available for %s", chosenUpgrade),
@ -337,6 +336,11 @@ func getReleaseNotes(
}
if note != "" {
// NOTE(d1): trim any final newline on the end of the note itself before
// we manually handle newlines (for multiple release notes and
// ensuring space between the warning messages)
note = strings.TrimSuffix(note, "\n")
*upgradeReleaseNotes += fmt.Sprintf("%s\n", note)
}
}

View File

@ -24,7 +24,7 @@ import (
func RunCmdRemote(
cl *dockerClient.Client,
app appPkg.App,
requestTTY bool,
disableTTY bool,
abraSh, serviceName, cmdName, cmdArgs, remoteUser string) error {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
@ -84,8 +84,10 @@ func RunCmdRemote(
}
execCreateOpts.Cmd = cmd
execCreateOpts.Tty = requestTTY
if !requestTTY {
execCreateOpts.Tty = true
if disableTTY {
execCreateOpts.Tty = false
log.Debugf("not requesting a remote TTY")
}

View File

@ -46,7 +46,7 @@ func DeployOverview(
app appPkg.App,
deployedVersion string,
toDeployVersion string,
info string,
releaseNotes string,
warnMessages []string,
) error {
deployConfig := "compose.yml"
@ -85,8 +85,8 @@ func DeployOverview(
fmt.Println(overview)
if info != "" {
fmt.Println(info)
if releaseNotes != "" {
fmt.Print(releaseNotes)
}
for _, msg := range warnMessages {

View File

@ -1,11 +1,15 @@
package recipe
import (
"os"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
gitCfg "github.com/go-git/go-git/v5/config"
"github.com/spf13/cobra"
)
@ -13,7 +17,16 @@ var RecipeFetchCommand = &cobra.Command{
Use: "fetch [recipe | --all] [flags]",
Aliases: []string{"f"},
Short: "Clone recipe(s) locally",
Long: `Using "--force/-f" Git syncs an existing recipe. It does not erase unstaged changes.`,
Args: cobra.RangeArgs(0, 1),
Example: ` # fetch from recipe catalogue
abra recipe fetch gitea
# fetch from remote recipe
abra recipe fetch git.foo.org/recipes/myrecipe
# fetch with ssh remote for hacking
abra recipe fetch gitea --ssh`,
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
@ -36,10 +49,39 @@ var RecipeFetchCommand = &cobra.Command{
ensureCtx := internal.GetEnsureContext()
if recipeName != "" {
r := internal.ValidateRecipe(args, cmd.Name())
if err := r.Ensure(ensureCtx); err != nil {
log.Fatal(err)
r := recipe.Get(recipeName)
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
if !force {
log.Warnf("%s is already fetched", r.Name)
return
}
}
r = internal.ValidateRecipe(args, cmd.Name())
if sshRemote {
if r.SSHURL == "" {
log.Warnf("unable to discover SSH remote for %s", r.Name)
return
}
repo, err := git.PlainOpen(r.Dir)
if err != nil {
log.Fatalf("unable to open %s: %s", r.Dir, err)
}
if err = repo.DeleteRemote("origin"); err != nil {
log.Fatalf("unable to remove default remote in %s: %s", r.Dir, err)
}
if _, err := repo.CreateRemote(&gitCfg.RemoteConfig{
Name: "origin",
URLs: []string{r.SSHURL},
}); err != nil {
log.Fatalf("unable to set SSH remote in %s: %s", r.Dir, err)
}
}
return
}
@ -61,6 +103,8 @@ var RecipeFetchCommand = &cobra.Command{
var (
fetchAllRecipes bool
sshRemote bool
force bool
)
func init() {
@ -71,4 +115,20 @@ func init() {
false,
"fetch all recipes",
)
RecipeFetchCommand.Flags().BoolVarP(
&sshRemote,
"ssh",
"s",
false,
"automatically set ssh remote",
)
RecipeFetchCommand.Flags().BoolVarP(
&force,
"force",
"f",
false,
"force re-fetch",
)
}

View File

@ -1,7 +1,10 @@
package git
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"coopcloud.tech/abra/pkg/log"
@ -22,12 +25,28 @@ func gitCloneIgnoreErr(err error) bool {
return false
}
// Clone runs a git clone which accounts for different default branches.
// Clone runs a git clone which accounts for different default branches. This
// function respects Ctrl+C (SIGINT) calls from the user, cancelling the
// context and deleting the (typically) half-baked clone of the repository.
// This avoids broken state for future clone / recipe ops.
func Clone(dir, url string) error {
ctx := context.Background()
ctx, cancelCtx := context.WithCancel(ctx)
sigIntCh := make(chan os.Signal, 1)
signal.Notify(sigIntCh, os.Interrupt)
defer func() {
signal.Stop(sigIntCh)
cancelCtx()
}()
errCh := make(chan error)
go func() {
if _, err := os.Stat(dir); os.IsNotExist(err) {
log.Debugf("git clone: %s", url)
_, err := git.PlainClone(dir, false, &git.CloneOptions{
_, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
@ -36,13 +55,17 @@ func Clone(dir, url string) error {
if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir)
return nil
errCh <- nil
}
if err := ctx.Err(); err != nil {
errCh <- fmt.Errorf("git clone %s: cancelled due to interrupt", dir)
}
if err != nil {
log.Debug("git clone: main branch failed, attempting master branch")
_, err := git.PlainClone(dir, false, &git.CloneOptions{
_, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
@ -51,11 +74,11 @@ func Clone(dir, url string) error {
if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir)
return nil
errCh <- nil
}
if err != nil {
return err
errCh <- err
}
}
@ -64,5 +87,20 @@ func Clone(dir, url string) error {
log.Debugf("git clone: %s already exists", dir)
}
errCh <- nil
}()
select {
case <-sigIntCh:
cancelCtx()
fmt.Println() // NOTE(d1): newline after ^C
if err := os.RemoveAll(dir); err != nil {
return fmt.Errorf("unable to clean up git clone of %s: %s", dir, err)
}
return fmt.Errorf("git clone %s: cancelled due to interrupt", dir)
case err := <-errCh:
return err
}
return nil
}

48
pkg/git/clone_test.go Normal file
View File

@ -0,0 +1,48 @@
package git
import (
"fmt"
"os"
"path"
"syscall"
"testing"
"coopcloud.tech/abra/pkg/config"
)
func TestClone(t *testing.T) {
dir := path.Join(config.RECIPES_DIR, "gitea")
os.RemoveAll(dir)
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea")
if err := Clone(dir, gitURL); err != nil {
t.Fatalf("unable to git clone gitea: %s", err)
}
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
t.Fatal("gitea repo was not cloned successfully")
}
}
func TestCancelGitClone(t *testing.T) {
dir := path.Join(config.RECIPES_DIR, "gitea")
os.RemoveAll(dir)
go func() {
p, err := os.FindProcess(os.Getpid())
if err != nil {
t.Fatalf("unable to find current process: %s", err)
}
p.Signal(syscall.SIGINT)
}()
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea")
if err := Clone(dir, gitURL); err == nil {
t.Fatal("cloning should have been interrupted")
}
if _, err := os.Stat(dir); err != nil && !os.IsNotExist(err) {
t.Fatal("recipe repo was not deleted")
}
}

View File

@ -3,5 +3,5 @@ STACK := abra_installer_script
default: deploy
deploy:
@DOCKER_CONTEXT=swarm.autonomic.zone docker stack rm $(STACK) && \
DOCKER_CONTEXT=swarm.autonomic.zone docker stack deploy -c compose.yml $(STACK)
@DOCKER_CONTEXT=swarm-0.coopcloud.tech docker stack rm $(STACK) && \
DOCKER_CONTEXT=swarm-0.coopcloud.tech docker stack deploy -c compose.yml $(STACK)

View File

@ -401,8 +401,6 @@ teardown(){
# bats test_tags=slow
@test "ignore env version on new deploy" {
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
assert_success

View File

@ -19,6 +19,7 @@ setup(){
}
teardown(){
_reset_recipe
_undeploy_app
}
@ -117,3 +118,22 @@ teardown(){
run $ABRA app ls --status
assert_success
}
# bats test_tags=slow
@test "list with chaos version" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
run $ABRA app ls --status
assert_success
assert_output --partial "+U"
run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success
assert_not_exists "$ABRA_DIR/servers/foo.com"
}

View File

@ -250,3 +250,10 @@ teardown(){
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "automatically select single server" {
# NOTE(d1): no --no-input required, single server available
run $ABRA app new "$TEST_RECIPE" --domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
}

View File

@ -25,13 +25,3 @@ teardown(){
run "$HOME/.local/bin/abra" -v
assert_output --partial 'beta'
}
# bats test_tags=slow
@test "install release candidate from script" {
run bash -c 'curl https://install.abra.coopcloud.tech | bash -s -- --rc'
assert_success
assert_exists "$HOME/.local/bin/abra"
run "$HOME/.local/bin/abra" -v
assert_output --partial '-rc'
}

View File

@ -5,6 +5,16 @@ setup() {
_common_setup
}
teardown(){
run rm -rf "$ABRA_DIR/recipes/matrix-synapse"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run rm -rf "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
assert_success
assert_not_exists "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
}
# bats test_tags=slow
@test "recipe fetch all" {
run rm -rf "$ABRA_DIR/recipes/matrix-synapse"
@ -35,3 +45,81 @@ setup() {
run $ABRA recipe fetch matrix-synapse --all
assert_failure
}
@test "do not refetch without --force" {
run $ABRA recipe fetch matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run $ABRA recipe fetch matrix-synapse
assert_output --partial "already fetched"
}
@test "refetch with --force" {
run $ABRA recipe fetch matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run $ABRA recipe fetch matrix-synapse --force
assert_success
refute_output --partial "already fetched"
}
@test "refetch with --force does not erase unstaged changes" {
run $ABRA recipe fetch matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run bash -c "echo foo >> $ABRA_DIR/recipes/matrix-synapse/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse/foo"
run $ABRA recipe fetch matrix-synapse --force
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
assert_exists "$ABRA_DIR/recipes/matrix-synapse/foo"
}
@test "fetch with --ssh" {
run $ABRA recipe fetch matrix-synapse --ssh
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run git -C "$ABRA_DIR/recipes/matrix-synapse" remote -v
assert_success
assert_output --partial "ssh://"
}
@test "re-fetch with --ssh/--force" {
run $ABRA recipe fetch matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run git -C "$ABRA_DIR/recipes/matrix-synapse" remote -v
assert_success
assert_output --partial "https://"
run $ABRA recipe fetch matrix-synapse --ssh --force
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run git -C "$ABRA_DIR/recipes/matrix-synapse" remote -v
assert_success
assert_output --partial "ssh://"
}
@test "fetch remote recipe" {
run $ABRA recipe fetch git.coopcloud.tech/coop-cloud/matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
}
@test "remote recipe do not refetch without --force" {
run $ABRA recipe fetch git.coopcloud.tech/coop-cloud/matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
run $ABRA recipe fetch git.coopcloud.tech/coop-cloud/matrix-synapse
assert_success
assert_output --partial "already fetched"
}

View File

@ -26,14 +26,3 @@ teardown(){
run "$HOME/.local/bin/abra" -v
assert_output --partial 'beta'
}
# bats test_tags=slow
@test "abra upgrade release candidate" {
run $ABRA upgrade --rc
assert_success
assert_output --partial 'Public interest infrastructure'
assert_exists "$HOME/.local/bin/abra"
run "$HOME/.local/bin/abra" -v
assert_output --partial '-rc'
}