Compare commits

...

118 Commits

Author SHA1 Message Date
f 7f0a74d3c3
fix: source autocompletion on the current terminal
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-07-11 12:02:38 -03:00
f e99114e695
fix: setup should be run once 2024-07-11 12:02:22 -03:00
f b1208f9db5
fix: sometimes the completion directories already exist 2024-07-11 12:01:21 -03:00
decentral1se b8e1a3b75f
test: remote recipe tests
continuous-integration/drone/push Build is passing Details
See #432
2024-07-10 16:03:28 +02:00
decentral1se ff90b43929
fix: use struct data for HEAD retrieval
continuous-integration/drone/push Build is passing Details
See ce7dda1eae
2024-07-10 15:51:11 +02:00
p4u1 c5724d56f8 fix(config): Removes config file name from abra dir
continuous-integration/drone/push Build is passing Details
2024-07-10 13:42:24 +00:00
decentral1se ce7dda1eae
fix: use recipe struct data
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
Follow up for #432
2024-07-10 15:40:45 +02:00
decentral1se d38f3ab7f5
test: speed up test
continuous-integration/drone/push Build is passing Details
2024-07-10 13:27:58 +02:00
decentral1se 4be8c8daed
test: fix outputs [ci skip]
See https://build.coopcloud.tech/coop-cloud/abra/2035/1/5
2024-07-10 13:20:39 +02:00
decentral1se 3c9405a4ed
refactor!: --problems/p goes away
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
Follow up for #413
2024-07-10 13:06:46 +02:00
p4u1 f6b7510da6 feat: introduce remote recipes
continuous-integration/drone/push Build is passing Details
Reviewed-on: #432
2024-07-10 10:25:06 +00:00
p4u1 7596982282 feat: update new version in env file
continuous-integration/drone/pr Build is passing Details
2024-07-10 12:12:43 +02:00
p4u1 4085eb6654 feat: define recipe version inside app env file 2024-07-10 12:11:46 +02:00
p4u1 790dbca362 feat!: remove all catalogue reads from app commands 2024-07-10 12:06:57 +02:00
p4u1 d7a870b887 feat: remote recipes 2024-07-10 12:06:44 +02:00
decentral1se 1a3ec7a107
fix: pass recipe name for listing cmds
continuous-integration/drone/push Build is passing Details
2024-07-09 17:23:06 +02:00
decentral1se 7f910b4e5b
test: recipe test fixups
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-07-09 11:34:20 +02:00
decentral1se b82ac3bd63
refactor: make IsChaos an actual bool 2024-07-09 11:34:01 +02:00
decentral1se 00d60f7114
fix: ensure force upgrade/rollback works 2024-07-09 11:33:33 +02:00
decentral1se 71d93cbbea
refactor: debug logging and errors for version issues 2024-07-09 11:33:07 +02:00
decentral1se 2fb5493ab5
feat: support chaos commits on deploy
See coop-cloud/organising#517
2024-07-09 11:31:52 +02:00
decentral1se 0ff8e49cfd
docs: pass on sub-command help 2024-07-09 09:43:18 +02:00
decentral1se addbda9145
test: fixups for the changepocalypse
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-07-09 09:41:49 +02:00
decentral1se c33ca1c6bc
fix!: chaos consistency (deploy/undeploy/rollback/upgrade)
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
See coop-cloud/organising#559

--chaos for rollback/upgrade goes away.
2024-07-08 17:23:49 +02:00
decentral1se 4580df72cb
fix: use recipe name
continuous-integration/drone/push Build is passing Details
2024-07-08 14:58:57 +02:00
decentral1se f003430a8d
fix: use recipe name, not app name
continuous-integration/drone/push Build is passing Details
2024-07-08 14:54:15 +02:00
decentral1se 5426464092
refactor!: drop version, show versions in ps
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
See coop-cloud/organising#526
See coop-cloud/organising#502
2024-07-08 14:41:46 +02:00
decentral1se 72c021c727
fix: remove old commands from deploy fail help
continuous-integration/drone/push Build is passing Details
2024-07-08 14:29:51 +02:00
decentral1se f2e076b35f
fix: set default logger on kadabra 2024-07-08 14:26:27 +02:00
decentral1se 4ccb4198d6
fix: "recipe version" handles non-catalogue recipes 2024-07-08 14:26:26 +02:00
decentral1se a9f7579ca9
fix: remove old logrus calls 2024-07-08 14:21:17 +02:00
p4u1 9cd1fe658b refactor(recipe): create a recipe struct that gets used everywhere #430
continuous-integration/drone/push Build is passing Details
Reviewed-on: #430
2024-07-08 12:18:58 +00:00
p4u1 41c16db670 test: fix test failure
continuous-integration/drone/pr Build is passing Details
2024-07-08 14:10:17 +02:00
p4u1 87ecc05962 refactor(recipe): remove direct usage of config.RECIPE_DIR
continuous-integration/drone/pr Build is failing Details
2024-07-08 13:48:02 +02:00
p4u1 f14d49cc64 refactor(recipe): rename Recipe2 -> Recipe 2024-07-08 13:19:40 +02:00
p4u1 f638b6a16b refator(recipe): remove old struct 2024-07-08 13:16:47 +02:00
p4u1 5617a9ba07 refactor(recipe): remove remaining usage of old recipe struct 2024-07-08 13:15:20 +02:00
p4u1 c1b03bcbd7 refactor(recipe): load load compoes config where its used 2024-07-08 12:31:39 +02:00
p4u1 99da8d4e57 refactor(recipe): move GetComposeFiles to new struct 2024-07-08 12:06:58 +02:00
p4u1 ca1db33e97 refactor(recipe): remove Dir method on old struct 2024-07-08 11:48:53 +02:00
p4u1 eb62e0ecc3 refactor(recipe): move Tags method to new struct 2024-07-08 11:45:47 +02:00
p4u1 6f90fc3025 refactor(recipe): don't use README.md path directly 2024-07-08 11:43:18 +02:00
p4u1 c861c09cce refactor(recipe): use method or variable for .env.sample 2024-07-08 11:41:26 +02:00
p4u1 2f41b6d8b4 refactor(recipe): store sample env path in new struct 2024-07-08 11:31:55 +02:00
p4u1 73e9b818b4 refactor(recipe): move SampleEnv method to new struct 2024-07-08 11:02:43 +02:00
p4u1 f268e5893b refactor(recipe): move functions that operate on the git repo to new file 2024-07-08 11:00:50 +02:00
p4u1 47013c63d6 refactor(recipe): use template for ssh url 2024-07-08 10:56:08 +02:00
p4u1 4cf6155fb8 refactor(recipe): introduce Dir var 2024-07-08 10:56:08 +02:00
p4u1 01f3f4be17 refactor(recipe): use new recipe.Ensure method 2024-07-08 10:55:55 +02:00
p4u1 eee2ecda06 refactor(recipe): add offline and chaos options to Ensure method 2024-07-08 10:55:55 +02:00
p4u1 950f85e2b4 refactor(recipe): introduce new recipe struct and move some methods 2024-07-08 10:55:43 +02:00
decentral1se 9ef64778f5
chore: go deps update
continuous-integration/drone/push Build is passing Details
2024-07-08 01:52:17 +02:00
decentral1se 735f521bc0
refactor(errors)!: remove WIP/broken command
continuous-integration/drone/push Build is passing Details
2024-07-08 01:33:06 +02:00
decentral1se 96a25425a4
refactor(ps)!: remove -w, "watch ..." does it better
continuous-integration/drone/push Build is passing Details
2024-07-08 01:10:58 +02:00
decentral1se 1a8dca9804
fix(deploy): only output when actually waiting
continuous-integration/drone/push Build is passing Details
2024-07-08 01:01:14 +02:00
decentral1se 465827d5ee
log: no additional newlines 2024-07-08 01:01:14 +02:00
decentral1se cde06f4f00
log: output caller on debug, use stdout as default 2024-07-08 01:01:13 +02:00
decentral1se 050a479df7
refactor: "service name" -> "service" 2024-07-08 00:38:54 +02:00
decentral1se ef108d63e1
refactor: use central logger
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-07-08 00:01:28 +02:00
decentral1se cf8ff410cc
feat: central log config
See coop-cloud/organising#422
2024-07-08 00:01:27 +02:00
decentral1se 6ec678208f
chore: formatting 2024-07-07 22:40:06 +02:00
decentral1se a001be3021
docs: better "app ps" description 2024-07-07 22:39:57 +02:00
decentral1se 6712bd446f
chore: add upstream link
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-07-07 21:52:45 +02:00
decentral1se 1097daa69f
fix: "abra app restart" docs + --all-services
See coop-cloud/organising#605
2024-07-07 21:52:24 +02:00
decentral1se beaa233421
test: only publish image on main merge
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-07-07 12:21:51 +02:00
decentral1se f871f9beee
test: reduce duplication
continuous-integration/drone/push Build is passing Details
2024-07-07 12:13:07 +02:00
decentral1se 0f8f0f908f
test: ensure catalogue
continuous-integration/drone/push Build is passing Details
2024-07-07 12:03:43 +02:00
decentral1se c5211fbd7e
test: fix imports 2024-07-07 12:03:37 +02:00
p4u1 0076b31253 new package envfile and move GetComposeFiles to recipe package
continuous-integration/drone/pr Build is failing Details
2024-07-06 16:37:16 +02:00
p4u1 37aff723c0 move GetComposeFiles 2024-07-06 16:37:16 +02:00
p4u1 f18c642226 refactor: move app files from config to app package 2024-07-06 16:37:16 +02:00
p4u1 ac695ae28e feat: introduce abra config file and load abra dir from it (!419)
continuous-integration/drone/push Build is passing Details
This is the first step to introduce a configuration file for abra. The config file must be named `abra.yaml` or Γ bra.yml`. abra look for the config file in the current directory and when not found traverses the directory tree up until it is found or the home/root directory is reached.

For now there is only one setting that is made configurable: `abraDir`. The new logic for setting the abra dir is the following:
1. lookup `$ABRA_DIR` env
2. look for config file and take value from there
3. `$HOME/.abra` as fallback

See coop-cloud/organising#303.

Reviewed-on: #419
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2024-07-06 14:36:31 +00:00
decentral1se ac87898005
test: run versioned script [ci skip] 2024-07-03 10:02:04 +02:00
decentral1se 32ae2499b6
test: add CI integration script [ci skip] 2024-07-03 09:57:22 +02:00
decentral1se 1136ec5dcd
build: remove old release scripts 2024-07-03 09:57:06 +02:00
decentral1se 6a2db1abaa
test: run int suite on remote server via cron
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-07-02 17:18:05 +02:00
decentral1se 9554ad40c8
refactor: use adapted upstream detach=false logic [ci skip]
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
See coop-cloud/organising#607.
2024-07-02 14:52:12 +02:00
decentral1se 2014cd6622
test: less fragile integration suite [ci skip]
See coop-cloud/organising#584
See coop-cloud/organising#595
2024-07-02 12:16:58 +02:00
decentral1se a9ce2106c6
test: skip test for now
continuous-integration/drone/push Build is passing Details
Also, don't build image if tests fail.
2024-06-28 06:12:32 +02:00
decentral1se 34de38928a
test: include catalogue
continuous-integration/drone/push Build is failing Details
2024-06-26 23:46:35 +02:00
decentral1se f58522d822
fix: dont always download the catalogue
continuous-integration/drone/pr Build is failing Details
continuous-integration/drone/push Build is failing Details
See coop-cloud/organising#592
2024-06-25 16:48:41 +02:00
decentral1se 712ebfb701
test: update and fix cleanup for "server add"
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-06-25 16:24:44 +02:00
decentral1se 1fe601cd16
fix: custom timeout only for "server add" 2024-06-25 16:13:57 +02:00
decentral1se 7b7e1bfa97
refactor!: server add/rm has better UI/UX
Less confusing logging messages, clear "is created" / "already exists"
output. Move the majority of logging to debug output to not confuse the
situation. Some code cleanups also in there.
2024-06-25 09:48:53 +02:00
decentral1se 1a12bef53e
docs: better "server add" help output 2024-06-25 09:24:01 +02:00
decentral1se d787f71215
fix: more accurate dns errors
continuous-integration/drone/push Build is passing Details
2024-06-25 00:27:48 +02:00
decentral1se 9bf44c15ed
fix: clean up if failed to create context 2024-06-25 00:27:34 +02:00
decentral1se 349cacc1f2
docs: explain -D for "server add" 2024-06-25 00:27:16 +02:00
decentral1se 938534f5ac feat: support non-TLD resolving server domains
continuous-integration/drone/push Build is passing Details
See coop-cloud/organising#566
2024-06-24 22:07:16 +00:00
p4u1 6cd331ebd6 secret: allow inserting secret from file and add trim flag
continuous-integration/drone/push Build is passing Details
2024-06-22 16:49:59 +00:00
decentral1se 40517171f7
test: separate test for git name/email
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
See #405
2024-06-22 18:46:28 +02:00
p4u1 b2485cc122 feat: add git-user and git-email flags to recipe new
continuous-integration/drone/push Build is passing Details
2024-06-22 16:38:32 +00:00
p4u1 9ec99c7712 test: return/echo from git helper functions
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-06-22 17:04:33 +02:00
decentral1se aa3910f8df
refactor!: drop all SSH opts / config handling
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
See coop-cloud/organising#601
See coop-cloud/organising#482
2024-06-21 17:16:41 +02:00
decentral1se 43990b6fae
test: use more plumbung for git output
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-06-21 17:10:12 +02:00
decentral1se 91ea2c01a5
fix: fix old app version deploy wrt. compose files
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
See coop-cloud/organising#617
2024-06-21 16:14:40 +02:00
decentral1se 316fdd3643
fix: abra app new checks out latest version
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
See coop-cloud/organising#618
2024-06-21 15:51:34 +02:00
decentral1se e07ae8cccd
chore: make format/check
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-06-19 19:17:22 +02:00
decentral1se 300a4ead01
fix: stop using deprecated APIs
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details
2024-06-19 19:14:52 +02:00
decentral1se f209b6f564
chore: go get -u -t 2024-06-19 19:14:44 +02:00
decentral1se 791183adfe
build: new deps target 2024-06-19 19:14:31 +02:00
Moritz e6b35e8524 fix(upgrade): make upgrade --chaos working again
continuous-integration/drone/push Build is passing Details
2024-05-22 10:21:31 +02:00
Moritz 8a0274cac0 fix(recipe): output correct formatted json for recipe version
continuous-integration/drone/push Build is passing Details
2024-05-21 16:59:59 +02:00
Moritz e609924af0 feat(upgrade): add --releasenotes: show release notes and skip upgrading
continuous-integration/drone/push Build is passing Details
2024-05-21 13:49:36 +02:00
Moritz 70e2943301 fix(upgrade): only show release notes relevant for the upgrade 2024-05-21 13:49:11 +02:00
Moritz 0590c1824d checkout deployed version
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-05-14 00:07:58 +02:00
Moritz 459abecfa5 only show container that should be deployed
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-05-13 23:26:02 +02:00
Moritz 183ad8f576 machine readable ps output
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-05-13 22:08:03 +02:00
decentral1se 03f94da2d8
docs: add fauno [ci skip] 2024-05-01 01:20:25 +02:00
f 766f69b0fd
feat: strip debug symbols
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
to produce smaller binaries
2024-04-30 14:05:03 -03:00
decentral1se 004cd70aed
fix: use unique rule number & wording [ci skip] 2024-04-06 23:52:56 +02:00
decentral1se a4de446f58
test: more verbose failure msg, use contains [ci skip] 2024-04-06 23:48:22 +02:00
Rich M d21c35965d fix: add warning for long secret names (!359)
continuous-integration/drone/push Build is passing Details
A start of a fix for coop-cloud/organising#463
Putting some code out to start a discussion.  I've added a linting rule for recipes to establish a general principal but I want to put some validation into cli/app/new.go as that's the point we have both the recipe and the domain and can say for sure whether or not the secret names lengths cause a problem but that will have to wait for a bit.  Let me know if I've missed the mark somewhere

Reviewed-on: #359
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: Rich M <r.p.makepeace@gmail.com>
Co-committed-by: Rich M <r.p.makepeace@gmail.com>
2024-04-06 21:41:37 +00:00
Mayel de Borniol 63ea58ffaa add relevant command to error message
continuous-integration/drone/push Build is passing Details
2024-04-01 18:51:53 +01:00
decentral1se 2ecace3e90
fix: add missing packages on final layer
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
Closes coop-cloud/organising#598
2024-04-01 13:57:51 +02:00
p4u1 d5ac3958a4 feat: add retries to app volume remove
continuous-integration/drone/push Build is passing Details
2024-03-27 05:38:24 +00:00
3wc 72c20e0039 fix: make installer work again
continuous-integration/drone/push Build is passing Details
2024-03-26 21:07:38 -03:00
decentral1se 575f9905f1
Revert "Revert "feat: backup revolution""
continuous-integration/drone/push Build is passing Details
This reverts commit 2c515ce70a.
2024-03-12 10:34:40 +01:00
158 changed files with 5372 additions and 5545 deletions

View File

@ -10,10 +10,10 @@ steps:
- name: make test
image: golang:1.21
environment:
ABRA_DIR: "/root/.abra"
CATL_URL: https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git
commands:
- make build-abra
- ./abra help # show version, initialise $ABRA_DIR
- mkdir -p $HOME/.abra
- git clone $CATL_URL $HOME/.abra/catalogue
- make test
depends_on:
- make check
@ -54,11 +54,36 @@ steps:
tags: dev
registry: git.coopcloud.tech
when:
event:
exclude:
- pull_request
branch:
- main
depends_on:
- make check
- make test
- name: integration test
image: appleboy/drone-ssh
settings:
host:
- int.coopcloud.tech
username: abra
key:
from_secret: abra_int_private_key
port: 22
command_timeout: 60m
script_stop: true
envs: [ DRONE_SOURCE_BRANCH ]
request_pty: true
script:
- |
wget https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
chmod +x run-ci-int
sh run-ci-int
when:
event:
- cron:
cron:
# @daily https://docs.drone.io/cron/
- integration
volumes:
- name: deps

View File

@ -29,6 +29,8 @@ builds:
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"
- "-s"
- "-w"
- id: kadabra
binary: kadabra
@ -50,6 +52,8 @@ builds:
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"
- "-s"
- "-w"
checksum:
name_template: "checksums.txt"

View File

@ -7,6 +7,7 @@
- cassowary
- codegod100
- decentral1se
- fauno
- frando
- kawaiipunk
- knoflook

View File

@ -1,23 +1,29 @@
# Build image
FROM golang:1.21-alpine AS build
ENV GOPRIVATE coopcloud.tech
RUN apk add --no-cache \
ca-certificates \
gcc \
git \
make \
musl-dev
RUN update-ca-certificates
COPY . /app
WORKDIR /app
RUN CGO_ENABLED=0 make build
FROM scratch
# Release image ("slim")
FROM alpine:3.19.1
RUN apk add --no-cache \
ca-certificates \
git \
openssh
RUN update-ca-certificates
COPY --from=build /app/abra /abra

View File

@ -53,3 +53,6 @@ test:
loc:
@find . -name "*.go" | xargs wc -l
deps:
@go get -t -u ./...

View File

@ -5,11 +5,10 @@ import (
)
var AppCommand = cli.Command{
Name: "app",
Aliases: []string{"a"},
Usage: "Manage apps",
ArgsUsage: "<domain>",
Description: "Functionality for managing the life cycle of your apps",
Name: "app",
Aliases: []string{"a"},
Usage: "Manage apps",
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
appBackupCommand,
appCheckCommand,
@ -17,7 +16,6 @@ var AppCommand = cli.Command{
appConfigCommand,
appCpCommand,
appDeployCommand,
appErrorsCommand,
appListCommand,
appLogsCommand,
appNewCommand,
@ -31,7 +29,6 @@ var AppCommand = cli.Command{
appServicesCommand,
appUndeployCommand,
appUpgradeCommand,
appVersionCommand,
appVolumeCommand,
},
}

View File

@ -1,414 +1,279 @@
package app
import (
"archive/tar"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/klauspost/pgzip"
"github.com/sirupsen/logrus"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
)
type backupConfig struct {
preHookCmd string
postHookCmd string
backupPaths []string
var snapshot string
var snapshotFlag = &cli.StringFlag{
Name: "snapshot, s",
Usage: "Lists specific snapshot",
Destination: &snapshot,
}
var appBackupCommand = cli.Command{
Name: "backup",
Aliases: []string{"bk"},
Usage: "Run app backup",
ArgsUsage: "<domain> [<service>]",
var includePath string
var includePathFlag = &cli.StringFlag{
Name: "path, p",
Usage: "Include path",
Destination: &includePath,
}
var resticRepo string
var resticRepoFlag = &cli.StringFlag{
Name: "repo, r",
Usage: "Restic repository",
Destination: &resticRepo,
}
var appBackupListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.ChaosFlag,
snapshotFlag,
includePathFlag,
},
Before: internal.SubCommandBefore,
Usage: "List all backups",
BashComplete: autocomplete.AppNameComplete,
Description: `
Run an app backup.
A backup command and pre/post hook commands are defined in the recipe
configuration. Abra reads this configuration and run the comands in the context
of the deployed services. Pass <service> if you only want to back up a single
service. All backups are placed in the ~/.abra/backups directory.
A single backup file is produced for all backup paths specified for a service.
If we have the following backup configuration:
- "backupbot.backup.path=/var/lib/foo,/var/lib/bar"
And we run "abra app backup example.com app", Abra will produce a file that
looks like:
~/.abra/backups/example_com_app_609341138.tar.gz
This file is a compressed archive which contains all backup paths. To see paths, run:
tar -tf ~/.abra/backups/example_com_app_609341138.tar.gz
(Make sure to change the name of the backup file)
This single file can be used to restore your app. See "abra app restore" for more.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
recipe, err := recipePkg.Get(app.Recipe, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
backupConfigs := make(map[string]backupConfig)
for _, service := range recipe.Config.Services {
if backupsEnabled, ok := service.Deploy.Labels["backupbot.backup"]; ok {
if backupsEnabled == "true" {
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name)
bkConfig := backupConfig{}
logrus.Debugf("backup config detected for %s", fullServiceName)
if paths, ok := service.Deploy.Labels["backupbot.backup.path"]; ok {
logrus.Debugf("detected backup paths for %s: %s", fullServiceName, paths)
bkConfig.backupPaths = strings.Split(paths, ",")
}
if preHookCmd, ok := service.Deploy.Labels["backupbot.backup.pre-hook"]; ok {
logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd)
bkConfig.preHookCmd = preHookCmd
}
if postHookCmd, ok := service.Deploy.Labels["backupbot.backup.post-hook"]; ok {
logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd)
bkConfig.postHookCmd = postHookCmd
}
backupConfigs[service.Name] = bkConfig
}
}
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
serviceName := c.Args().Get(1)
if serviceName != "" {
backupConfig, ok := backupConfigs[serviceName]
if !ok {
logrus.Fatalf("no backup config for %s? does %s exist?", serviceName, serviceName)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
logrus.Infof("running backup for the %s service", serviceName)
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if includePath != "" {
log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
}
if err := runBackup(cl, app, serviceName, backupConfig); err != nil {
logrus.Fatal(err)
}
} else {
if len(backupConfigs) == 0 {
logrus.Fatalf("no backup configs discovered for %s?", app.Name)
}
for serviceName, backupConfig := range backupConfigs {
logrus.Infof("running backup for the %s service", serviceName)
if err := runBackup(cl, app, serviceName, backupConfig); err != nil {
logrus.Fatal(err)
}
}
if err := internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
return nil
},
}
// TimeStamp generates a file name friendly timestamp.
func TimeStamp() string {
ts := time.Now().UTC().Format(time.RFC3339)
return strings.Replace(ts, ":", "-", -1)
}
var appBackupDownloadCommand = cli.Command{
Name: "download",
Aliases: []string{"d"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
snapshotFlag,
includePathFlag,
},
Before: internal.SubCommandBefore,
Usage: "Download a backup",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
// runBackup does the actual backup logic.
func runBackup(cl *dockerClient.Client, app config.App, serviceName string, bkConfig backupConfig) error {
if len(bkConfig.backupPaths) == 0 {
return fmt.Errorf("backup paths are empty for %s?", serviceName)
}
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
if err != nil {
return err
}
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
if bkConfig.preHookCmd != "" {
splitCmd := internal.SafeSplit(bkConfig.preHookCmd)
logrus.Debugf("split pre-hook command for %s into %s", fullServiceName, splitCmd)
preHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil {
return fmt.Errorf("failed to run %s on %s: %s", bkConfig.preHookCmd, targetContainer.ID, err.Error())
if !internal.Chaos {
if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
}
if !internal.Offline {
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
}
logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, bkConfig.preHookCmd)
}
var tempBackupPaths []string
for _, remoteBackupPath := range bkConfig.backupPaths {
sanitisedPath := strings.ReplaceAll(remoteBackupPath, "/", "_")
localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s%s_%s.tar.gz", fullServiceName, sanitisedPath, TimeStamp()))
logrus.Debugf("temporarily backing up %s:%s to %s", fullServiceName, remoteBackupPath, localBackupPath)
logrus.Infof("backing up %s:%s", fullServiceName, remoteBackupPath)
content, _, err := cl.CopyFromContainer(context.Background(), targetContainer.ID, remoteBackupPath)
cl, err := client.New(app.Server)
if err != nil {
logrus.Debugf("failed to copy %s from container: %s", remoteBackupPath, err.Error())
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
log.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if includePath != "" {
log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
}
if err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
remoteBackupDir := "/tmp/backup.tar.gz"
currentWorkingDir := "."
if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil {
log.Fatal(err)
}
fmt.Println("backup successfully downloaded to current working directory")
return nil
},
}
var appBackupCreateCommand = cli.Command{
Name: "create",
Aliases: []string{"c"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
resticRepoFlag,
},
Before: internal.SubCommandBefore,
Usage: "Create a new backup",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
}
if !internal.Chaos {
if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
}
return fmt.Errorf("failed to copy %s from container: %s", remoteBackupPath, err.Error())
}
defer content.Close()
_, srcBase := archive.SplitPathDirEntry(remoteBackupPath)
preArchive := archive.RebaseArchiveEntries(content, srcBase, remoteBackupPath)
if err := copyToFile(localBackupPath, preArchive); err != nil {
logrus.Debugf("failed to create tar archive (%s): %s", localBackupPath, err.Error())
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
if !internal.Offline {
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
return fmt.Errorf("failed to create tar archive (%s): %s", localBackupPath, err.Error())
}
tempBackupPaths = append(tempBackupPaths, localBackupPath)
}
logrus.Infof("compressing and merging archives...")
if err := mergeArchives(tempBackupPaths, fullServiceName); err != nil {
logrus.Debugf("failed to merge archive files: %s", err.Error())
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
}
return fmt.Errorf("failed to merge archive files: %s", err.Error())
}
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
}
if bkConfig.postHookCmd != "" {
splitCmd := internal.SafeSplit(bkConfig.postHookCmd)
logrus.Debugf("split post-hook command for %s into %s", fullServiceName, splitCmd)
postHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &postHookExecOpts); err != nil {
return err
}
logrus.Infof("succesfully ran %s post-hook command: %s", fullServiceName, bkConfig.postHookCmd)
}
return nil
}
func copyToFile(outfile string, r io.Reader) error {
tmpFile, err := os.CreateTemp(filepath.Dir(outfile), ".tar_temp")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
_, err = io.Copy(tmpFile, r)
tmpFile.Close()
if err != nil {
os.Remove(tmpPath)
return err
}
if err = os.Rename(tmpPath, outfile); err != nil {
os.Remove(tmpPath)
return err
}
return nil
}
func cleanupTempArchives(tarPaths []string) error {
for _, tarPath := range tarPaths {
if err := os.RemoveAll(tarPath); err != nil {
return err
}
logrus.Debugf("remove temporary archive file %s", tarPath)
}
return nil
}
func mergeArchives(tarPaths []string, serviceName string) error {
var out io.Writer
var cout *pgzip.Writer
localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s_%s.tar.gz", serviceName, TimeStamp()))
fout, err := os.Create(localBackupPath)
if err != nil {
return fmt.Errorf("Failed to open %s: %s", localBackupPath, err)
}
defer fout.Close()
out = fout
cout = pgzip.NewWriter(out)
out = cout
tw := tar.NewWriter(out)
for _, tarPath := range tarPaths {
if err := addTar(tw, tarPath); err != nil {
return fmt.Errorf("failed to merge %s: %v", tarPath, err)
}
}
if err := tw.Close(); err != nil {
return fmt.Errorf("failed to close tar writer %v", err)
}
if cout != nil {
if err := cout.Flush(); err != nil {
return fmt.Errorf("failed to flush: %s", err)
} else if err = cout.Close(); err != nil {
return fmt.Errorf("failed to close compressed writer: %s", err)
}
}
logrus.Infof("backed up %s to %s", serviceName, localBackupPath)
return nil
}
func addTar(tw *tar.Writer, pth string) (err error) {
var tr *tar.Reader
var rc io.ReadCloser
var hdr *tar.Header
if tr, rc, err = openTarFile(pth); err != nil {
return
}
for {
if hdr, err = tr.Next(); err != nil {
if err == io.EOF {
err = nil
if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
break
}
if err = tw.WriteHeader(hdr); err != nil {
break
} else if _, err = io.Copy(tw, tr); err != nil {
break
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
}
if err == nil {
err = rc.Close()
} else {
rc.Close()
}
return
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if resticRepo != "" {
log.Debugf("including RESTIC_REPO=%s in backupbot exec invocation", resticRepo)
execEnv = append(execEnv, fmt.Sprintf("RESTIC_REPO=%s", resticRepo))
}
if err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
return nil
},
}
func openTarFile(pth string) (tr *tar.Reader, rc io.ReadCloser, err error) {
var fin *os.File
var n int
buff := make([]byte, 1024)
var appBackupSnapshotsCommand = cli.Command{
Name: "snapshots",
Aliases: []string{"s"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
snapshotFlag,
},
Before: internal.SubCommandBefore,
Usage: "List backup snapshots",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if fin, err = os.Open(pth); err != nil {
return
}
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
}
if n, err = fin.Read(buff); err != nil {
fin.Close()
return
} else if n == 0 {
fin.Close()
err = fmt.Errorf("%s is empty", pth)
return
}
if !internal.Chaos {
if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
}
if _, err = fin.Seek(0, 0); err != nil {
fin.Close()
return
}
if !internal.Offline {
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
rc = fin
tr = tar.NewReader(rc)
if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
}
return tr, rc, nil
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if err := internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
return nil
},
}
var appBackupCommand = cli.Command{
Name: "backup",
Aliases: []string{"b"},
Usage: "Manage app backups",
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
appBackupListCommand,
appBackupSnapshotsCommand,
appBackupDownloadCommand,
appBackupCreateCommand,
},
}

View File

@ -2,12 +2,10 @@ package app
import (
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
)
@ -38,32 +36,16 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
tableCol := []string{"recipe env sample", "app env"}
table := formatter.CreateTable(tableCol)
envVars, err := config.CheckEnv(app)
envVars, err := appPkg.CheckEnv(app)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
for _, envVar := range envVars {

View File

@ -5,18 +5,15 @@ import (
"fmt"
"os"
"os/exec"
"path"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
)
@ -31,10 +28,11 @@ They can be run within the context of a service (e.g. app) or locally on your
work station by passing "--local". Arguments can be passed into these functions
using the "-- <args>" syntax.
Example:
**WARNING**: options must be passed directly after the sub-command "cmd".
abra app cmd example.com app create_user -- me@example.com
`,
EXAMPLE: