forked from toolshed/abra
Compare commits
363 Commits
0.4.0-alph
...
prune
Author | SHA1 | Date | |
---|---|---|---|
0d8191bc3e | |||
20cdfe7a72 | |||
96a8cb7aff
|
|||
9b51d22c20 | |||
d789830ce4 | |||
e4b4084dfd
|
|||
ff58646cfc
|
|||
eec6469ba1
|
|||
e94f947d20
|
|||
cccbe4a2ec
|
|||
f53cfb6c36
|
|||
f55f01a25c
|
|||
ce5c1a9ebb
|
|||
5e3b039f93
|
|||
0e9d218bbc
|
|||
e1c635af86
|
|||
f6b139dfea
|
|||
3d2b8fa446
|
|||
2eebac6fc0
|
|||
f5e2710138
|
|||
986470784d
|
|||
e76ed771df | |||
f28af5e42f
|
|||
fdf4854b0c
|
|||
6b9512d09c
|
|||
21a86731d0
|
|||
91102e6607
|
|||
fadafda0b8
|
|||
c03cf76702
|
|||
ebb748b7e7
|
|||
2b3dbee24c
|
|||
a448cfdd0d
|
|||
5ee6eb53b2 | |||
7b2880d425 | |||
928d6f5d7f | |||
29fa607190
|
|||
7c541ffdfa
|
|||
7ccc4b4c08
|
|||
ef4df35995
|
|||
71a9155042
|
|||
2a88491d7c
|
|||
bf79552204
|
|||
0a7fa54759
|
|||
7c1a97be72 | |||
f20fbbc913 | |||
76717531bd | |||
6774893412 | |||
ebb86391af | |||
50db39424c | |||
ca1ea32c46 | |||
32851d4d99 | |||
c47aa49373
|
|||
cdee6b00c4
|
|||
a3e9383a4a
|
|||
b4cce7dcf4
|
|||
b089109c94
|
|||
27e0708ac7
|
|||
a93786c6be
|
|||
521570224b
|
|||
c72462e0b6
|
|||
54646650c7 | |||
903aac9d7a
|
|||
49865c6a97 | |||
a694c8c20e
|
|||
d4a42d8378
|
|||
e16ca45fa7
|
|||
32de2ee5de
|
|||
834d41ef50
|
|||
6fe5aed408
|
|||
03041b88d0
|
|||
9338afb492
|
|||
313ae0dbe2 | |||
0dc7ec8570 | |||
8a1a3aeb12
|
|||
910469cfa0 | |||
4f055096e9 | |||
6c93f980dc | |||
57f52bbf33 | |||
9f5620d881 | |||
44c4555aae | |||
025d1e0a8c | |||
f484021148 | |||
1403eac72c | |||
a6e23938eb | |||
cae0d9ef79 | |||
89fcb5b216
|
|||
56b3e9bb19 | |||
9aa4a98b0b
|
|||
5fbba0c934
|
|||
d772f4b2c6
|
|||
7513fbd57d | |||
9082761f86 | |||
a3bd6e14d0 | |||
49dd702d98
|
|||
e4cd5e3efe
|
|||
1db4602020 | |||
b50718050b | |||
9e39e1dc88 | |||
1a3a53cfc2
|
|||
5f53d591f8
|
|||
d7013518cc | |||
b204b289d1 | |||
3a0d9f7ed7
|
|||
e794c17fb4
|
|||
e788ac21f6 | |||
4e78b060e0
|
|||
4fada9c1b7
|
|||
08d26e1a39
|
|||
581b28a2b1
|
|||
e4d58849ce | |||
5e8b9d9bf7 | |||
11dd665794 | |||
ba163e9bf3 | |||
09048ee223 | |||
19a055b59b | |||
1b28a07e17 | |||
82866cd213 | |||
47f3d2638b
|
|||
a3b894320a
|
|||
9424a58c52 | |||
1751ba534e | |||
a21d431541 | |||
8fad34e430 | |||
a036de3c26
|
|||
4c2109e8ce | |||
7f745ff19f
|
|||
521d3d1259 | |||
14187449a5
|
|||
2037f4cc19
|
|||
05d492d30b | |||
9591e91ed6 | |||
f6f587e506
|
|||
4f28dbee87 | |||
ad1cc038e3 | |||
15dbd85d25 | |||
2a97955586 | |||
9e44d1dfba
|
|||
87ad8e2761 | |||
cfe703b15d | |||
96503fa9e9 | |||
07d49d8566
|
|||
5a7c25375a
|
|||
652143e76c
|
|||
8afce6eebf
|
|||
d3e6c9dc94
|
|||
4fd0ca3dd1 | |||
dc0b6c2c8c | |||
54f242baf7 | |||
07620c7d89 | |||
1cae4cce4e | |||
9347ade82c
|
|||
3fa18a8050
|
|||
4ac67662a2
|
|||
d1be4077c5
|
|||
5a88c34a7c
|
|||
2e452e3213
|
|||
9d16a8e10c
|
|||
8755a6c3b4 | |||
8cee8ae33a | |||
15b138e026 | |||
4a8ed36dea | |||
7d0c3cc496 | |||
3cf479ffd5 | |||
d402050a40 | |||
664edce09d
|
|||
e41caa891d
|
|||
42a6818ff4
|
|||
8f709c05bf
|
|||
a4ebf7befc
|
|||
8458e61d17
|
|||
b42d5bf113
|
|||
f684c6d6e4
|
|||
6593baf9f4 | |||
50123f3810
|
|||
d132e87f14
|
|||
37a1c3fb85 | |||
c8183aa6d1 | |||
4711de29ae
|
|||
b719aaba41
|
|||
074c51b672 | |||
1aa6be704a | |||
e8e3cb8598
|
|||
85fec6b107
|
|||
12dbb061a9 | |||
351bd7d4ba | |||
cdc7037c25
|
|||
682237c98e | |||
08d97be43a
|
|||
786dfde27e
|
|||
6e012b910e | |||
c153c5da2e | |||
0540e42168
|
|||
4bc95a5b52
|
|||
febc6e2874
|
|||
b2c990bf12
|
|||
3b8893502a
|
|||
e0a0378f73 | |||
0837045d44 | |||
cd8137a7d8
|
|||
ece4537a2d
|
|||
16fe1b68c6
|
|||
e37f235fd4 | |||
0423ce7e84
|
|||
d46ac22bd7
|
|||
cef5cd8611
|
|||
8b38dac9ab | |||
89fc875088 | |||
026a9ba2d7
|
|||
99f2b9c6dc | |||
578e91eeec
|
|||
49f79dbd45
|
|||
574d556bb9
|
|||
801aad64df
|
|||
b0a0829712
|
|||
6aae06c3ec | |||
d0c6fa5b45 | |||
c947354ee3 | |||
9b7e5752fb
|
|||
9bc51629d4 | |||
4ba15df9b7
|
|||
5721b357a2
|
|||
6140abbcac | |||
996255188b | |||
11d78234b2
|
|||
c214937e4a
|
|||
3a3f41988b
|
|||
f6690a80bd
|
|||
2337c4648b
|
|||
a1190f1352
|
|||
e421922f5b
|
|||
10d5705d1a
|
|||
a4f1634b24
|
|||
cbd924060f
|
|||
3c4bb6a55e
|
|||
a0d7a76f9d
|
|||
c71efb46ba
|
|||
ce69967ec5
|
|||
1a04439b1f | |||
979f417a63
|
|||
b27acb2f61
|
|||
622ecc4885
|
|||
ed5bbda811
|
|||
7b627ea518
|
|||
1ac66da83f
|
|||
061de96b62 | |||
6998298d32
|
|||
323f4467c8
|
|||
e8e41850b5
|
|||
0e23ec53d7
|
|||
b943a8b9b1
|
|||
acc665f054
|
|||
860f1d6376 | |||
2122f0e67c | |||
6aa23a76a1 | |||
338360096c
|
|||
7a8c7cd50f
|
|||
bafc8a8e34
|
|||
3d44d8c9fd
|
|||
b8b4616498
|
|||
da97117929 | |||
978297c464 | |||
11da4808fc | |||
4023e6a066
|
|||
f432bfdd23
|
|||
848e17578d | |||
1615130929
|
|||
7f315315f0
|
|||
6a50981120
|
|||
c67471e6ca
|
|||
f0fc1027e5
|
|||
c66695d55e
|
|||
262009701e
|
|||
b31cb6b866
|
|||
f39e186b66
|
|||
a8f35bdf2f
|
|||
6e1e02ac28
|
|||
16fc5ee54b
|
|||
37a1fcc4af
|
|||
a9b522719f
|
|||
ce70932a1c
|
|||
d61e104536
|
|||
d5f30a3ae4
|
|||
2555096510
|
|||
3797292b20
|
|||
6333815b71
|
|||
793a850fd5
|
|||
42c1450384
|
|||
a2377882f6
|
|||
e78b395662
|
|||
cdec834ca9
|
|||
b4b0b464bd
|
|||
d8a1b0ccc1
|
|||
3fbd381f55
|
|||
d3e127e5c8
|
|||
e9cfb076c6
|
|||
8ccf856110
|
|||
d0945aa09d
|
|||
123619219e
|
|||
a27410952e
|
|||
13e0392af6 | |||
99a6135f72 | |||
a6b52c1354
|
|||
fa51459191 | |||
c529988427
|
|||
231cc3c718
|
|||
3381b8936d
|
|||
823f869f1d
|
|||
ecbeacf10f
|
|||
3f838038d5
|
|||
91b4e021d0 | |||
598e87dca2 | |||
001511876d | |||
b295958c17 | |||
2fbdcfb958
|
|||
09ac74d205
|
|||
5da4afa0ec
|
|||
9d5e805748
|
|||
770ae5ed9b | |||
e056d8dc44
|
|||
c3442354e7
|
|||
6b2a0011af
|
|||
46fca7cfa7
|
|||
82d560a946
|
|||
fc5107865b
|
|||
53ed1fc545
|
|||
cc9e3d4e60
|
|||
0557284461 | |||
b5f23d3791 | |||
2b2dcc01b4
|
|||
0a208d049e
|
|||
141711ecd0
|
|||
cd46d71ce4 | |||
6fa090352d | |||
227c02cd09
|
|||
bfeda40e34
|
|||
5237c7ed50
|
|||
4e09f3b9a8
|
|||
dfb32cbb68
|
|||
bdd9b0a1aa
|
|||
b2d17a1829
|
|||
c905376472
|
|||
d316de218c
|
|||
123475bd36
|
|||
58e98f490d
|
|||
224b8865bf
|
|||
8fb9f42f13
|
|||
dc5e2a5b24
|
|||
40b4ef5ab2
|
|||
4a912ae3bc
|
|||
1150fcc595
|
|||
45224d1349
|
|||
7a40e2d616
|
|||
2277e4ef72
|
|||
c0c3d9fe76
|
|||
2493921ade
|
|||
22f9cf2be4
|
|||
a23124aede | |||
e670844b56 | |||
bc1729c5ca
|
|||
fa8611b115
|
|||
415df981ff
|
|||
57728e58e8
|
|||
c7062e0494
|
.dockerignore.drone.yml.gitignore.goreleaser.ymlAUTHORS.mdDockerfileLICENSEMakefileREADME.mdrenovate.json.gitignoreDockerfileREADME.mdapp.shautocomplete.shcatalogue.shcmd.shcommon.shinstall.shmakefilerecipe.shrecords.shserver.shtest_all.shtestfunctions.sh
cli
app
app.gobackup.gocheck.gocmd.gocmd_test.goconfig.gocp.godeploy.goerrors.golist.gologs.gonew.gops.goremove.gorestart.gorestore.gorollback.gorun.gosecret.goservices.goundeploy.goupgrade.goversion.govolume.go
catalogue
cli.gointernal
recipe
record
server
updater
cmd
go.modgo.sumpkg
app
autocomplete
catalogue
client
config
container
dns
formatter
git
jsontable
lint
recipe
runtime
secret
ssh
test
upstream
scripts
tests
integration
.abra
servers
manual
resources
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
*.swp
|
||||
*.swo
|
57
.drone.yml
57
.drone.yml
@ -3,45 +3,23 @@ kind: pipeline
|
||||
name: coopcloud.tech/abra
|
||||
steps:
|
||||
- name: make check
|
||||
image: golang:1.17
|
||||
image: golang:1.20
|
||||
commands:
|
||||
- make check
|
||||
|
||||
- name: make static
|
||||
image: golang:1.17
|
||||
ignore: true # until we decide we all want this check
|
||||
environment:
|
||||
STATIC_CHECK_URL: honnef.co/go/tools/cmd/staticcheck
|
||||
STATIC_CHECK_VERSION: v0.2.0
|
||||
commands:
|
||||
- go install $STATIC_CHECK_URL@$STATIC_CHECK_VERSION
|
||||
- make static
|
||||
|
||||
- name: make build
|
||||
image: golang:1.17
|
||||
image: golang:1.20
|
||||
commands:
|
||||
- make build
|
||||
|
||||
- name: make test
|
||||
image: golang:1.17
|
||||
commands:
|
||||
- make test
|
||||
|
||||
- name: notify on failure
|
||||
image: plugins/matrix
|
||||
settings:
|
||||
homeserver: https://matrix.autonomic.zone
|
||||
roomid: "IFazIpLtxiScqbHqoa:autonomic.zone"
|
||||
userid: "@autono-bot:autonomic.zone"
|
||||
accesstoken:
|
||||
from_secret: autono_bot_access_token
|
||||
depends_on:
|
||||
- make check
|
||||
- make build
|
||||
|
||||
- name: make test
|
||||
image: golang:1.20
|
||||
commands:
|
||||
- make test
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
depends_on:
|
||||
- make check
|
||||
|
||||
- name: fetch
|
||||
image: docker:git
|
||||
@ -55,7 +33,7 @@ steps:
|
||||
event: tag
|
||||
|
||||
- name: release
|
||||
image: golang:1.17
|
||||
image: golang:1.20
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: goreleaser_gitea_token
|
||||
@ -69,6 +47,23 @@ steps:
|
||||
when:
|
||||
event: tag
|
||||
|
||||
- name: publish image
|
||||
image: plugins/docker
|
||||
settings:
|
||||
auto_tag: true
|
||||
username: 3wordchant
|
||||
password:
|
||||
from_secret: git_coopcloud_tech_token_3wc
|
||||
repo: git.coopcloud.tech/coop-cloud/abra
|
||||
tags: dev
|
||||
registry: git.coopcloud.tech
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
depends_on:
|
||||
- make check
|
||||
|
||||
volumes:
|
||||
- name: deps
|
||||
temp: {}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
.e2e.env
|
||||
.envrc
|
||||
.vscode/
|
||||
/kadabra
|
||||
abra
|
||||
dist/
|
||||
tests/integration/.abra/catalogue
|
||||
|
@ -1,38 +1,76 @@
|
||||
---
|
||||
project_name: abra
|
||||
gitea_urls:
|
||||
api: https://git.coopcloud.tech/api/v1
|
||||
download: https://git.coopcloud.tech/
|
||||
skip_tls_verify: false
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go generate ./...
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
- id: abra
|
||||
binary: abra
|
||||
dir: cmd/abra
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
ldflags:
|
||||
- "-X 'main.Commit={{ .Commit }}'"
|
||||
- "-X 'main.Version={{ .Version }}'"
|
||||
|
||||
- id: kadabra
|
||||
binary: kadabra
|
||||
dir: cmd/kadabra
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
ldflags:
|
||||
- "-X 'main.Commit={{ .Commit }}'"
|
||||
- "-X 'main.Version={{ .Version }}'"
|
||||
|
||||
archives:
|
||||
- replacements:
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
format: binary
|
||||
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
changelog:
|
||||
sort: desc
|
||||
filters:
|
||||
exclude:
|
||||
- "^Merge"
|
||||
- "^Revert"
|
||||
- "^WIP:"
|
||||
- "^chore(deps):"
|
||||
- "^style:"
|
||||
- "^test:"
|
||||
- "^tests:"
|
||||
- "^Revert"
|
||||
|
13
AUTHORS.md
Normal file
13
AUTHORS.md
Normal file
@ -0,0 +1,13 @@
|
||||
# authors
|
||||
|
||||
> If you're looking at this and you hack on `abra` and you're not listed here,
|
||||
> please do add yourself! This is a community project, let's show some :heart:
|
||||
|
||||
- 3wordchant
|
||||
- cassowary
|
||||
- decentral1se
|
||||
- frando
|
||||
- kawaiipunk
|
||||
- knoflook
|
||||
- moritz
|
||||
- roxxers
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM golang:1.20-alpine AS build
|
||||
|
||||
ENV GOPRIVATE coopcloud.tech
|
||||
|
||||
RUN apk add --no-cache make git gcc musl-dev
|
||||
|
||||
COPY . /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN CGO_ENABLED=0 make build
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=build /app/abra /abra
|
||||
|
||||
ENTRYPOINT ["/abra"]
|
15
LICENSE
Normal file
15
LICENSE
Normal file
@ -0,0 +1,15 @@
|
||||
Abra: The Co-op Cloud utility belt
|
||||
Copyright (C) 2022 Co-op Cloud <helo@coopcloud.tech>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
16
Makefile
16
Makefile
@ -1,11 +1,13 @@
|
||||
ABRA := ./cmd/abra
|
||||
KADABRA := ./cmd/kadabra
|
||||
COMMIT := $(shell git rev-list -1 HEAD)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
|
||||
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
|
||||
|
||||
export GOPRIVATE=coopcloud.tech
|
||||
|
||||
all: format check static build test
|
||||
all: format check build test
|
||||
|
||||
run:
|
||||
@go run -ldflags=$(LDFLAGS) $(ABRA)
|
||||
@ -14,22 +16,22 @@ install:
|
||||
@go install -ldflags=$(LDFLAGS) $(ABRA)
|
||||
|
||||
build-dev:
|
||||
@go build -ldflags=$(LDFLAGS) $(ABRA)
|
||||
@go build -v -ldflags=$(LDFLAGS) $(ABRA)
|
||||
|
||||
build:
|
||||
@go build -ldflags=$(DIST_LDFLAGS) $(ABRA)
|
||||
@go build -v -ldflags=$(DIST_LDFLAGS) $(ABRA)
|
||||
@go build -v -ldflags=$(DIST_LDFLAGS) $(KADABRA)
|
||||
|
||||
clean:
|
||||
@rm '$(GOPATH)/bin/abra'
|
||||
@rm '$(GOPATH)/bin/kadabra'
|
||||
|
||||
format:
|
||||
@gofmt -s -w .
|
||||
|
||||
check:
|
||||
@test -z $$(gofmt -l .) || (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
|
||||
|
||||
static:
|
||||
@staticcheck $(ABRA)
|
||||
@test -z $$(gofmt -l .) || \
|
||||
(echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
|
||||
|
||||
test:
|
||||
@go test ./... -cover -v
|
||||
|
69
README.md
69
README.md
@ -1,73 +1,12 @@
|
||||
# abra
|
||||
|
||||
> https://coopcloud.tech
|
||||
# `abra`
|
||||
|
||||
[](https://build.coopcloud.tech/coop-cloud/abra)
|
||||
[](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra)
|
||||
|
||||
The Co-op Cloud utility belt 🎩🐇
|
||||
|
||||
`abra` is a command-line tool for managing your own [Co-op Cloud](https://coopcloud.tech). It can provision new servers, create apps, deploy them and a whole lot of other things. Please see [docs.coopcloud.tech](https://docs.coopcloud.tech) for more extensive documentation.
|
||||
<a href="https://github.com/egonelbre/gophers"><img align="right" width="150" src="https://github.com/egonelbre/gophers/raw/master/.thumb/sketch/adventure/poking-fire.png"/></a>
|
||||
|
||||
## Quick install
|
||||
`abra` is the flagship client & command-line tool for Co-op Cloud. It has been developed specifically for the purpose of making the day-to-day operations of [operators](https://docs.coopcloud.tech/operators/) and [maintainers](https://docs.coopcloud.tech/maintainers/) pleasant & convenient. It is libre software, written in [Go](https://go.dev) and maintained and extended by the community :heart:
|
||||
|
||||
```bash
|
||||
curl https://install.abra.autonomic.zone | bash
|
||||
```
|
||||
|
||||
Or using the latest release candidate (extra experimental!):
|
||||
|
||||
```bash
|
||||
curl https://install.abra.autonomic.zone | bash -s -- --rc
|
||||
```
|
||||
|
||||
Source for this script is in [scripts/installer/installer](./scripts/installer/installer).
|
||||
|
||||
## Hacking
|
||||
|
||||
### Getting started
|
||||
|
||||
Install [direnv](https://direnv.net), run `cp .envrc.sample .envrc`, then run `direnv allow` in this directory. This will set coopcloud repos as private due to [this bug.](https://git.coopcloud.tech/coop-cloud/coopcloud.tech/issues/20#issuecomment-8201). Or you can run `go env -w GOPRIVATE=coopcloud.tech` but I'm not sure how persistent this is.
|
||||
|
||||
Install [Go >= 1.16](https://golang.org/doc/install) and then:
|
||||
|
||||
- `make build` to build
|
||||
- `./abra` to run commands
|
||||
- `make test` will run tests
|
||||
- `make install` will install it to `$GOPATH/bin`
|
||||
- `go get <package>` and `go mod tidy` to add a new dependency
|
||||
|
||||
Our [Drone CI configuration](.drone.yml) runs a number of sanity on each pushed commit. See the [Makefile](./Makefile) for more handy targets.
|
||||
|
||||
Please use the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/) for your commits so we can automate our change log.
|
||||
|
||||
### Versioning
|
||||
|
||||
We use [goreleaser](https://goreleaser.com) to help us automate releases. We use [semver](https://semver.org) for versioning all releases of the tool. While we are still in the public alpha release phase, we will maintain a `0.y.z-alpha` format. Change logs are generated from our commit logs. We are still working this out and aim to refine our release praxis as we go.
|
||||
|
||||
For developers, while using this `-alpha` format, the `y` part is the "major" version part. So, if you make breaking changes, you increment that and _not_ the `x` part. So, if you're on `0.1.0-alpha`, then you'd go to `0.1.1-alpha` for a backwards compatible change and `0.2.0-alpha` for a backwards incompatible change.
|
||||
|
||||
### Making a new release
|
||||
|
||||
- Change `ABRA_VERSION` to match the new tag in [`scripts`](./scripts/installer/installer) (use [semver](https://semver.org))
|
||||
- Commit that change (e.g. `git commit -m 'chore: publish next tag x.y.z-alpha'`)
|
||||
- Make a new tag (e.g. `git tag -a x.y.z-alpha`)
|
||||
- Push the new tag (e.g. `git push && git push --tags`)
|
||||
- Wait until the build finishes on [build.coopcloud.tech](https://build.coopcloud.tech/coop-cloud/abra)
|
||||
- Deploy the new installer script (e.g. `cd ./scripts/installer && make`)
|
||||
- Check the release worked, (e.g. `abra upgrade; abra -v`)
|
||||
|
||||
### Fork maintenance
|
||||
|
||||
#### `godotenv`
|
||||
|
||||
We maintain a fork of [godotenv](https://github.com/Autonomic-Cooperative/godotenv) for two features:
|
||||
|
||||
1. multi-line env var support
|
||||
2. inline comment parsing
|
||||
|
||||
You can upgrade the version here by running `go get github.com/Autonomic-Cooperative/godotenv@<commit>` where `<commit>` is the latest commit you want to pin to. At time of writing, `go get github.com/Autonomic-Cooperative/godotenv@b031ea1211e7fd297af4c7747ffb562ebe00cd33` is the command you want to run to maintain the above functionality.
|
||||
|
||||
#### `docker/client`
|
||||
|
||||
A number of modules in [pkg/upstream](./pkg/upstream) are copy/pasta'd from the upstream [docker/docker/client](https://pkg.go.dev/github.com/docker/docker/client). We had to do this because upstream are not exposing their API as public.
|
||||
Please see [docs.coopcloud.tech/abra](https://docs.coopcloud.tech/abra) for help on install, upgrade, hacking, troubleshooting & more!
|
||||
|
@ -8,26 +8,30 @@ var AppCommand = cli.Command{
|
||||
Name: "app",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Manage apps",
|
||||
ArgsUsage: "<app>",
|
||||
Description: "This command provides functionality for managing the life cycle of your apps",
|
||||
ArgsUsage: "<domain>",
|
||||
Description: "Functionality for managing the life cycle of your apps",
|
||||
Subcommands: []cli.Command{
|
||||
appNewCommand,
|
||||
appConfigCommand,
|
||||
appRestartCommand,
|
||||
appDeployCommand,
|
||||
appUpgradeCommand,
|
||||
appUndeployCommand,
|
||||
appRemoveCommand,
|
||||
appBackupCommand,
|
||||
appCheckCommand,
|
||||
appListCommand,
|
||||
appPsCommand,
|
||||
appLogsCommand,
|
||||
appCmdCommand,
|
||||
appConfigCommand,
|
||||
appCpCommand,
|
||||
appRunCommand,
|
||||
appRollbackCommand,
|
||||
appSecretCommand,
|
||||
appVolumeCommand,
|
||||
appVersionCommand,
|
||||
appDeployCommand,
|
||||
appErrorsCommand,
|
||||
appListCommand,
|
||||
appLogsCommand,
|
||||
appNewCommand,
|
||||
appPsCommand,
|
||||
appRemoveCommand,
|
||||
appRestartCommand,
|
||||
appRestoreCommand,
|
||||
appRollbackCommand,
|
||||
appRunCommand,
|
||||
appSecretCommand,
|
||||
appServicesCommand,
|
||||
appUndeployCommand,
|
||||
appUpgradeCommand,
|
||||
appVersionCommand,
|
||||
appVolumeCommand,
|
||||
},
|
||||
}
|
||||
|
399
cli/app/backup.go
Normal file
399
cli/app/backup.go
Normal file
@ -0,0 +1,399 @@
|
||||
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"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"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/docker/docker/pkg/system"
|
||||
"github.com/klauspost/pgzip"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type backupConfig struct {
|
||||
preHookCmd string
|
||||
postHookCmd string
|
||||
backupPaths []string
|
||||
}
|
||||
|
||||
var appBackupCommand = cli.Command{
|
||||
Name: "backup",
|
||||
Aliases: []string{"bk"},
|
||||
Usage: "Run app backup",
|
||||
ArgsUsage: "<domain> [<service>]",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
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)
|
||||
conf := runtime.New()
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
recipe, err := recipe.Get(app.Recipe, conf)
|
||||
if 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
logrus.Infof("running backup for the %s service", serviceName)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// TimeStamp generates a file name friendly timestamp.
|
||||
func TimeStamp() string {
|
||||
ts := time.Now().UTC().Format(time.RFC3339)
|
||||
return strings.Replace(ts, ":", "-", -1)
|
||||
}
|
||||
|
||||
// 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 := 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())
|
||||
}
|
||||
|
||||
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)
|
||||
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())
|
||||
}
|
||||
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())
|
||||
}
|
||||
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 := system.TempFileSequential(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
|
||||
}
|
||||
break
|
||||
}
|
||||
if err = tw.WriteHeader(hdr); err != nil {
|
||||
break
|
||||
} else if _, err = io.Copy(tw, tr); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
err = rc.Close()
|
||||
} else {
|
||||
rc.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func openTarFile(pth string) (tr *tar.Reader, rc io.ReadCloser, err error) {
|
||||
var fin *os.File
|
||||
var n int
|
||||
buff := make([]byte, 1024)
|
||||
|
||||
if fin, err = os.Open(pth); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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 _, err = fin.Seek(0, 0); err != nil {
|
||||
fin.Close()
|
||||
return
|
||||
}
|
||||
|
||||
rc = fin
|
||||
tr = tar.NewReader(rc)
|
||||
|
||||
return tr, rc, nil
|
||||
}
|
@ -14,18 +14,17 @@ import (
|
||||
|
||||
var appCheckCommand = cli.Command{
|
||||
Name: "check",
|
||||
Aliases: []string{"c"},
|
||||
Aliases: []string{"chk"},
|
||||
Usage: "Check if app is configured correctly",
|
||||
ArgsUsage: "<service>",
|
||||
ArgsUsage: "<domain>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
envSamplePath := path.Join(config.RECIPES_DIR, app.Type, ".env.sample")
|
||||
envSamplePath := path.Join(config.RECIPES_DIR, app.Recipe, ".env.sample")
|
||||
if _, err := os.Stat(envSamplePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logrus.Fatalf("%s does not exist?", envSamplePath)
|
||||
|
245
cli/app/cmd.go
Normal file
245
cli/app/cmd.go
Normal file
@ -0,0 +1,245 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"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/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var appCmdCommand = cli.Command{
|
||||
Name: "command",
|
||||
Aliases: []string{"cmd"},
|
||||
Usage: "Run app commands",
|
||||
Description: `
|
||||
Run an app specific command.
|
||||
|
||||
These commands are bash functions, defined in the abra.sh of the recipe itself.
|
||||
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:
|
||||
|
||||
abra app cmd example.com app create_user -- me@example.com
|
||||
`,
|
||||
ArgsUsage: "<domain> [<service>] <command> [-- <args>]",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.LocalCmdFlag,
|
||||
internal.RemoteUserFlag,
|
||||
},
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Before: internal.SubCommandBefore,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if internal.LocalCmd && internal.RemoteUser != "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together"))
|
||||
}
|
||||
|
||||
hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd)
|
||||
|
||||
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
|
||||
if _, err := os.Stat(abraSh); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logrus.Fatalf("%s does not exist for %s?", abraSh, app.Name)
|
||||
}
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if internal.LocalCmd {
|
||||
cmdName := c.Args().Get(1)
|
||||
if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("--local detected, running %s on local work station", cmdName)
|
||||
|
||||
var exportEnv string
|
||||
for k, v := range app.Env {
|
||||
exportEnv = exportEnv + fmt.Sprintf("%s='%s'; ", k, v)
|
||||
}
|
||||
var sourceAndExec string
|
||||
if hasCmdArgs {
|
||||
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
|
||||
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName, parsedCmdArgs)
|
||||
} else {
|
||||
logrus.Debug("did not detect any command arguments")
|
||||
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName)
|
||||
}
|
||||
|
||||
shell := "/bin/bash"
|
||||
if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
|
||||
logrus.Debugf("%s does not exist locally, use /bin/sh as fallback", shell)
|
||||
shell = "/bin/sh"
|
||||
}
|
||||
cmd := exec.Command(shell, "-c", sourceAndExec)
|
||||
|
||||
if err := internal.RunCmd(cmd); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
targetServiceName := c.Args().Get(1)
|
||||
|
||||
cmdName := c.Args().Get(2)
|
||||
if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
serviceNames, err := config.GetAppServiceNames(app.Name)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
matchingServiceName := false
|
||||
for _, serviceName := range serviceNames {
|
||||
if serviceName == targetServiceName {
|
||||
matchingServiceName = true
|
||||
}
|
||||
}
|
||||
|
||||
if !matchingServiceName {
|
||||
logrus.Fatalf("no service %s for %s?", targetServiceName, app.Name)
|
||||
}
|
||||
|
||||
logrus.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
|
||||
|
||||
if hasCmdArgs {
|
||||
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
|
||||
} else {
|
||||
logrus.Debug("did not detect any command arguments")
|
||||
}
|
||||
|
||||
if err := runCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func parseCmdArgs(args []string, isLocal bool) (bool, string) {
|
||||
var (
|
||||
parsedCmdArgs string
|
||||
hasCmdArgs bool
|
||||
)
|
||||
|
||||
if isLocal {
|
||||
if len(args) > 2 {
|
||||
return true, fmt.Sprintf("%s ", strings.Join(args[2:], " "))
|
||||
}
|
||||
} else {
|
||||
if len(args) > 3 {
|
||||
return true, fmt.Sprintf("%s ", strings.Join(args[3:], " "))
|
||||
}
|
||||
}
|
||||
|
||||
return hasCmdArgs, parsedCmdArgs
|
||||
}
|
||||
|
||||
func ensureCommand(abraSh, recipeName, execCmd string) error {
|
||||
bytes, err := ioutil.ReadFile(abraSh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(string(bytes), execCmd) {
|
||||
return fmt.Errorf("%s doesn't have a %s function", recipeName, execCmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName, cmdName, cmdArgs string) error {
|
||||
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
|
||||
}
|
||||
|
||||
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server)
|
||||
|
||||
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
|
||||
content, err := archive.TarWithOptions(abraSh, toTarOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME: avoid instantiating a new CLI
|
||||
dcli, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shell := "/bin/bash"
|
||||
findShell := []string{"test", "-e", shell}
|
||||
execCreateOpts := types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
Cmd: findShell,
|
||||
Detach: false,
|
||||
Tty: false,
|
||||
}
|
||||
|
||||
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
|
||||
logrus.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name)
|
||||
shell = "/bin/sh"
|
||||
}
|
||||
|
||||
var cmd []string
|
||||
if cmdArgs != "" {
|
||||
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s %s", serviceName, app.Name, app.StackName(), cmdName, cmdArgs)}
|
||||
} else {
|
||||
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)}
|
||||
}
|
||||
|
||||
logrus.Debugf("running command: %s", strings.Join(cmd, " "))
|
||||
|
||||
if internal.RemoteUser != "" {
|
||||
logrus.Debugf("running command with user %s", internal.RemoteUser)
|
||||
execCreateOpts.User = internal.RemoteUser
|
||||
}
|
||||
|
||||
execCreateOpts.Cmd = cmd
|
||||
execCreateOpts.Tty = true
|
||||
|
||||
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
31
cli/app/cmd_test.go
Normal file
31
cli/app/cmd_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseCmdArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
input []string
|
||||
shouldParse bool
|
||||
expectedOutput string
|
||||
}{
|
||||
// `--` is not parsed when passed in from the command-line e.g. -- foo bar baz
|
||||
// so we need to eumlate that as missing when testing if bash args are passed in
|
||||
// see https://git.coopcloud.tech/coop-cloud/organising/issues/336 for more
|
||||
{[]string{"foo.com", "app", "test"}, false, ""},
|
||||
{[]string{"foo.com", "app", "test", "foo"}, true, "foo "},
|
||||
{[]string{"foo.com", "app", "test", "foo", "bar", "baz"}, true, "foo bar baz "},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
ok, parsed := parseCmdArgs(test.input, false)
|
||||
if ok != test.shouldParse {
|
||||
t.Fatalf("[%s] should not parse", strings.Join(test.input, " "))
|
||||
}
|
||||
if parsed != test.expectedOutput {
|
||||
t.Fatalf("%s does not match %s", parsed, test.expectedOutput)
|
||||
}
|
||||
}
|
||||
}
|
@ -15,11 +15,11 @@ import (
|
||||
|
||||
var appConfigCommand = cli.Command{
|
||||
Name: "config",
|
||||
Aliases: []string{"c"},
|
||||
Aliases: []string{"cfg"},
|
||||
Usage: "Edit app config",
|
||||
ArgsUsage: "<domain>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Action: func(c *cli.Context) error {
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"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/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
@ -22,7 +23,7 @@ import (
|
||||
var appCpCommand = cli.Command{
|
||||
Name: "cp",
|
||||
Aliases: []string{"c"},
|
||||
ArgsUsage: "<src> <dst>",
|
||||
ArgsUsage: "<domain> <src> <dst>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
@ -30,20 +31,24 @@ var appCpCommand = cli.Command{
|
||||
Before: internal.SubCommandBefore,
|
||||
Usage: "Copy files to/from a running app service",
|
||||
Description: `
|
||||
This command supports copying files to and from any app service file system.
|
||||
Copy files to and from any app service file system.
|
||||
|
||||
If you want to copy a myfile.txt to the root of the app service:
|
||||
|
||||
abra app cp <app> myfile.txt app:/
|
||||
abra app cp <domain> myfile.txt app:/
|
||||
|
||||
And if you want to copy that file back to your current working directory locally:
|
||||
|
||||
abra app cp <app> app:/myfile.txt .
|
||||
|
||||
abra app cp <domain> app:/myfile.txt .
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
src := c.Args().Get(1)
|
||||
dst := c.Args().Get(2)
|
||||
if src == "" {
|
||||
@ -89,42 +94,28 @@ And if you want to copy that file back to your current working directory locally
|
||||
logrus.Fatalf("%s does not exist locally?", dstPath)
|
||||
}
|
||||
}
|
||||
err := configureAndCp(c, app, srcPath, dstPath, service, isToContainer)
|
||||
if err != nil {
|
||||
|
||||
if err := configureAndCp(c, cl, app, srcPath, dstPath, service, isToContainer); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
return nil
|
||||
|
||||
return nil
|
||||
},
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
}
|
||||
|
||||
func configureAndCp(
|
||||
c *cli.Context,
|
||||
cl *dockerClient.Client,
|
||||
app config.App,
|
||||
srcPath string,
|
||||
dstPath string,
|
||||
service string,
|
||||
isToContainer bool) error {
|
||||
appFiles, err := config.LoadAppFiles("")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
appEnv, err := config.GetApp(appFiles, app.Name)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service))
|
||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service))
|
||||
|
||||
container, err := container.GetContainer(context.Background(), cl, filters, true)
|
||||
container, err := container.GetContainer(context.Background(), cl, filters, internal.NoInput)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -157,5 +148,6 @@ func configureAndCp(
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ var appDeployCommand = cli.Command{
|
||||
Name: "deploy",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Deploy an app",
|
||||
ArgsUsage: "<domain>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
@ -20,9 +21,8 @@ var appDeployCommand = cli.Command{
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `
|
||||
This command deploys an app. It does not support incrementing the version of a
|
||||
deployed app, for this you need to look at the "abra app upgrade <app>"
|
||||
command.
|
||||
Deploy an app. It does not support incrementing the version of a deployed app,
|
||||
for this you need to look at the "abra app upgrade <domain>" command.
|
||||
|
||||
You may pass "--force" to re-deploy the same version again. This can be useful
|
||||
if the container runtime has gotten into a weird state.
|
||||
|
@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -11,6 +12,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -22,8 +24,9 @@ import (
|
||||
var appErrorsCommand = cli.Command{
|
||||
Name: "errors",
|
||||
Usage: "List errors for a deployed app",
|
||||
ArgsUsage: "<domain>",
|
||||
Description: `
|
||||
This command lists errors for a deployed app.
|
||||
List errors for a deployed app.
|
||||
|
||||
This is a best-effort implementation and an attempt to gather a number of tips
|
||||
& tricks for finding errors together into one convenient command. When an app
|
||||
@ -40,21 +43,20 @@ Got any more ideas? Please let us know:
|
||||
|
||||
https://git.coopcloud.tech/coop-cloud/organising/issues/new/choose
|
||||
|
||||
This command is best accompanied by "abra app logs <app>" which may reveal
|
||||
This command is best accompanied by "abra app logs <domain>" which may reveal
|
||||
further information which can help you debug the cause of an app failure via
|
||||
the logs.
|
||||
|
||||
`,
|
||||
Aliases: []string{"e"},
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
internal.WatchFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
conf := runtime.New()
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
@ -71,14 +73,14 @@ the logs.
|
||||
}
|
||||
|
||||
if !internal.Watch {
|
||||
if err := checkErrors(c, cl, app); err != nil {
|
||||
if err := checkErrors(c, cl, app, conf); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
if err := checkErrors(c, cl, app); err != nil {
|
||||
if err := checkErrors(c, cl, app, conf); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
@ -88,15 +90,16 @@ the logs.
|
||||
},
|
||||
}
|
||||
|
||||
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error {
|
||||
recipe, err := recipe.Get(app.Type)
|
||||
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App, conf *runtime.Config) error {
|
||||
recipe, err := recipe.Get(app.Recipe, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, service := range recipe.Config.Services {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", service.Name)
|
||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
|
||||
|
||||
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
return err
|
||||
|
127
cli/app/list.go
127
cli/app/list.go
@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
@ -9,7 +10,6 @@ import (
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/ssh"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
@ -22,12 +22,12 @@ var statusFlag = &cli.BoolFlag{
|
||||
Destination: &status,
|
||||
}
|
||||
|
||||
var appType string
|
||||
var typeFlag = &cli.StringFlag{
|
||||
Name: "type, t",
|
||||
var recipeFilter string
|
||||
var recipeFlag = &cli.StringFlag{
|
||||
Name: "recipe, r",
|
||||
Value: "",
|
||||
Usage: "Show apps of a specific type",
|
||||
Destination: &appType,
|
||||
Usage: "Show apps of a specific recipe",
|
||||
Destination: &recipeFilter,
|
||||
}
|
||||
|
||||
var listAppServer string
|
||||
@ -39,22 +39,22 @@ var listAppServerFlag = &cli.StringFlag{
|
||||
}
|
||||
|
||||
type appStatus struct {
|
||||
server string
|
||||
recipe string
|
||||
appName string
|
||||
domain string
|
||||
status string
|
||||
version string
|
||||
upgrade string
|
||||
Server string `json:"server"`
|
||||
Recipe string `json:"recipe"`
|
||||
AppName string `json:"appName"`
|
||||
Domain string `json:"domain"`
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
Upgrade string `json:"upgrade"`
|
||||
}
|
||||
|
||||
type serverStatus struct {
|
||||
apps []appStatus
|
||||
appCount int
|
||||
versionCount int
|
||||
unversionedCount int
|
||||
latestCount int
|
||||
upgradeCount int
|
||||
Apps []appStatus `json:"apps"`
|
||||
AppCount int `json:"appCount"`
|
||||
VersionCount int `json:"versionCount"`
|
||||
UnversionedCount int `json:"unversionedCount"`
|
||||
LatestCount int `json:"latestCount"`
|
||||
UpgradeCount int `json:"upgradeCount"`
|
||||
}
|
||||
|
||||
var appListCommand = cli.Command{
|
||||
@ -62,19 +62,19 @@ var appListCommand = cli.Command{
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "List all managed apps",
|
||||
Description: `
|
||||
This command looks at your local file system listing of apps and servers (e.g.
|
||||
in ~/.abra/) to generate a report of all your apps.
|
||||
Read the local file system listing of apps and servers (e.g. ~/.abra/) to
|
||||
generate a report of all your apps.
|
||||
|
||||
By passing the "--status/-S" flag, you can query all your servers for the
|
||||
actual live deployment status. Depending on how many servers you manage, this
|
||||
can take some time.
|
||||
`,
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
internal.MachineReadableFlag,
|
||||
statusFlag,
|
||||
listAppServerFlag,
|
||||
typeFlag,
|
||||
recipeFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Action: func(c *cli.Context) error {
|
||||
@ -83,12 +83,12 @@ can take some time.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
apps, err := config.GetApps(appFiles)
|
||||
apps, err := config.GetApps(appFiles, recipeFilter)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
sort.Sort(config.ByServerAndType(apps))
|
||||
sort.Sort(config.ByServerAndRecipe(apps))
|
||||
|
||||
statuses := make(map[string]map[string]string)
|
||||
var catl recipe.RecipeCatalogue
|
||||
@ -96,19 +96,15 @@ can take some time.
|
||||
alreadySeen := make(map[string]bool)
|
||||
for _, app := range apps {
|
||||
if _, ok := alreadySeen[app.Server]; !ok {
|
||||
if err := ssh.EnsureHostKey(app.Server); err != nil {
|
||||
logrus.Fatal(fmt.Sprintf(internal.SSHFailMsg, app.Server))
|
||||
}
|
||||
alreadySeen[app.Server] = true
|
||||
}
|
||||
}
|
||||
|
||||
statuses, err = config.GetAppStatuses(appFiles)
|
||||
statuses, err = config.GetAppStatuses(apps, internal.MachineReadable)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
var err error
|
||||
catl, err = recipe.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -123,20 +119,20 @@ can take some time.
|
||||
var ok bool
|
||||
if stats, ok = allStats[app.Server]; !ok {
|
||||
stats = serverStatus{}
|
||||
if appType == "" {
|
||||
if recipeFilter == "" {
|
||||
// count server, no filtering
|
||||
totalServersCount++
|
||||
}
|
||||
}
|
||||
|
||||
if app.Type == appType || appType == "" {
|
||||
if appType != "" {
|
||||
if app.Recipe == recipeFilter || recipeFilter == "" {
|
||||
if recipeFilter != "" {
|
||||
// only count server if matches filter
|
||||
totalServersCount++
|
||||
}
|
||||
|
||||
appStats := appStatus{}
|
||||
stats.appCount++
|
||||
stats.AppCount++
|
||||
totalAppsCount++
|
||||
|
||||
if status {
|
||||
@ -151,17 +147,17 @@ can take some time.
|
||||
if statusMeta["status"] != "" {
|
||||
status = statusMeta["status"]
|
||||
}
|
||||
stats.versionCount++
|
||||
stats.VersionCount++
|
||||
} else {
|
||||
stats.unversionedCount++
|
||||
stats.UnversionedCount++
|
||||
}
|
||||
|
||||
appStats.status = status
|
||||
appStats.version = version
|
||||
appStats.Status = status
|
||||
appStats.Version = version
|
||||
|
||||
var newUpdates []string
|
||||
if version != "unknown" {
|
||||
updates, err := recipe.GetRecipeCatalogueVersions(app.Type, catl)
|
||||
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -185,29 +181,38 @@ can take some time.
|
||||
|
||||
if len(newUpdates) == 0 {
|
||||
if version == "unknown" {
|
||||
appStats.upgrade = "unknown"
|
||||
appStats.Upgrade = "unknown"
|
||||
} else {
|
||||
appStats.upgrade = "latest"
|
||||
stats.latestCount++
|
||||
appStats.Upgrade = "latest"
|
||||
stats.LatestCount++
|
||||
}
|
||||
} else {
|
||||
newUpdates = internal.ReverseStringList(newUpdates)
|
||||
appStats.upgrade = strings.Join(newUpdates, "\n")
|
||||
stats.upgradeCount++
|
||||
appStats.Upgrade = strings.Join(newUpdates, "\n")
|
||||
stats.UpgradeCount++
|
||||
}
|
||||
}
|
||||
|
||||
appStats.server = app.Server
|
||||
appStats.recipe = app.Type
|
||||
appStats.appName = app.Name
|
||||
appStats.domain = app.Domain
|
||||
appStats.Server = app.Server
|
||||
appStats.Recipe = app.Recipe
|
||||
appStats.AppName = app.Name
|
||||
appStats.Domain = app.Domain
|
||||
|
||||
stats.apps = append(stats.apps, appStats)
|
||||
stats.Apps = append(stats.Apps, appStats)
|
||||
}
|
||||
|
||||
allStats[app.Server] = stats
|
||||
}
|
||||
|
||||
if internal.MachineReadable {
|
||||
jsonstring, err := json.Marshal(allStats)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
} else {
|
||||
fmt.Println(string(jsonstring))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
alreadySeen := make(map[string]bool)
|
||||
for _, app := range apps {
|
||||
if _, ok := alreadySeen[app.Server]; ok {
|
||||
@ -216,17 +221,17 @@ can take some time.
|
||||
|
||||
serverStat := allStats[app.Server]
|
||||
|
||||
tableCol := []string{"recipe", "domain", "app name"}
|
||||
tableCol := []string{"recipe", "domain"}
|
||||
if status {
|
||||
tableCol = append(tableCol, []string{"status", "version", "upgrade"}...)
|
||||
}
|
||||
|
||||
table := formatter.CreateTable(tableCol)
|
||||
|
||||
for _, appStat := range serverStat.apps {
|
||||
tableRow := []string{appStat.recipe, appStat.domain, appStat.appName}
|
||||
for _, appStat := range serverStat.Apps {
|
||||
tableRow := []string{appStat.Recipe, appStat.Domain}
|
||||
if status {
|
||||
tableRow = append(tableRow, []string{appStat.status, appStat.version, appStat.upgrade}...)
|
||||
tableRow = append(tableRow, []string{appStat.Status, appStat.Version, appStat.Upgrade}...)
|
||||
}
|
||||
table.Append(tableRow)
|
||||
}
|
||||
@ -238,14 +243,14 @@ can take some time.
|
||||
fmt.Println(fmt.Sprintf(
|
||||
"server: %s | total apps: %v | versioned: %v | unversioned: %v | latest: %v | upgrade: %v",
|
||||
app.Server,
|
||||
serverStat.appCount,
|
||||
serverStat.versionCount,
|
||||
serverStat.unversionedCount,
|
||||
serverStat.latestCount,
|
||||
serverStat.upgradeCount,
|
||||
serverStat.AppCount,
|
||||
serverStat.VersionCount,
|
||||
serverStat.UnversionedCount,
|
||||
serverStat.LatestCount,
|
||||
serverStat.UpgradeCount,
|
||||
))
|
||||
} else {
|
||||
fmt.Println(fmt.Sprintf("server: %s | total apps: %v", app.Server, serverStat.appCount))
|
||||
fmt.Println(fmt.Sprintf("server: %s | total apps: %v", app.Server, serverStat.AppCount))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"coopcloud.tech/abra/pkg/service"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -20,18 +21,23 @@ import (
|
||||
)
|
||||
|
||||
var logOpts = types.ContainerLogsOptions{
|
||||
Details: false,
|
||||
Follow: true,
|
||||
ShowStderr: true,
|
||||
ShowStdout: true,
|
||||
Tail: "20",
|
||||
Since: "",
|
||||
Until: "",
|
||||
Timestamps: true,
|
||||
Follow: true,
|
||||
Tail: "20",
|
||||
Details: false,
|
||||
}
|
||||
|
||||
// stackLogs lists logs for all stack services
|
||||
func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", stackName)
|
||||
func stackLogs(c *cli.Context, app config.App, client *dockerClient.Client) {
|
||||
filters, err := app.Filters(true, false)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
serviceOpts := types.ServiceListOptions{Filters: filters}
|
||||
services, err := client.ServiceList(context.Background(), serviceOpts)
|
||||
if err != nil {
|
||||
@ -67,27 +73,29 @@ func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
|
||||
var appLogsCommand = cli.Command{
|
||||
Name: "logs",
|
||||
Aliases: []string{"l"},
|
||||
ArgsUsage: "[<service>]",
|
||||
ArgsUsage: "<domain> [<service>]",
|
||||
Usage: "Tail app logs",
|
||||
Flags: []cli.Flag{
|
||||
internal.StdErrOnlyFlag,
|
||||
internal.SinceLogsFlag,
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
app := internal.ValidateApp(c, runtime.WithEnsureRecipeExists(false))
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logOpts.Since = internal.SinceLogs
|
||||
|
||||
serviceName := c.Args().Get(1)
|
||||
if serviceName == "" {
|
||||
logrus.Debugf("tailing logs for all %s services", app.Type)
|
||||
stackLogs(c, app.StackName(), cl)
|
||||
logrus.Debugf("tailing logs for all %s services", app.Recipe)
|
||||
stackLogs(c, app, cl)
|
||||
} else {
|
||||
logrus.Debugf("tailing logs for %s", serviceName)
|
||||
if err := tailServiceLogs(c, cl, app, serviceName); err != nil {
|
||||
@ -102,6 +110,7 @@ var appLogsCommand = cli.Command{
|
||||
func tailServiceLogs(c *cli.Context, cl *dockerClient.Client, app config.App, serviceName string) error {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName))
|
||||
|
||||
chosenService, err := service.GetService(context.Background(), cl, filters, internal.NoInput)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
|
@ -7,11 +7,11 @@ import (
|
||||
)
|
||||
|
||||
var appNewDescription = `
|
||||
This command takes a recipe and uses it to create a new app. This new app
|
||||
configuration is stored in your ~/.abra directory under the appropriate server.
|
||||
Take a recipe and uses it to create a new app. This new app configuration is
|
||||
stored in your ~/.abra directory under the appropriate server.
|
||||
|
||||
This command does not deploy your app for you. You will need to run "abra app
|
||||
deploy <app>" to do so.
|
||||
deploy <domain>" to do so.
|
||||
|
||||
You can see what recipes are available (i.e. values for the <recipe> argument)
|
||||
by running "abra recipe ls".
|
||||
@ -36,12 +36,11 @@ var appNewCommand = cli.Command{
|
||||
internal.NoInputFlag,
|
||||
internal.NewAppServerFlag,
|
||||
internal.DomainFlag,
|
||||
internal.NewAppNameFlag,
|
||||
internal.PassFlag,
|
||||
internal.SecretsFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
ArgsUsage: "<recipe>",
|
||||
ArgsUsage: "[<recipe>]",
|
||||
Action: internal.NewAction,
|
||||
BashComplete: autocomplete.RecipeNameComplete,
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import (
|
||||
"github.com/buger/goterm"
|
||||
dockerFormatter "github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
@ -25,11 +24,11 @@ var appPsCommand = cli.Command{
|
||||
Name: "ps",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "Check app status",
|
||||
Description: "This command shows a more detailed status output of a specific deployed app.",
|
||||
ArgsUsage: "<domain>",
|
||||
Description: "Show a more detailed status output of a specific deployed app",
|
||||
Flags: []cli.Flag{
|
||||
internal.WatchFlag,
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
@ -67,8 +66,10 @@ var appPsCommand = cli.Command{
|
||||
|
||||
// showPSOutput renders ps output.
|
||||
func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", app.StackName())
|
||||
filters, err := app.Filters(true, true)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
|
@ -11,39 +11,47 @@ import (
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// Volumes stores the variable from VolumesFlag
|
||||
var Volumes bool
|
||||
|
||||
// VolumesFlag is used to specify if volumes should be deleted when deleting an app
|
||||
var VolumesFlag = &cli.BoolFlag{
|
||||
Name: "volumes",
|
||||
Destination: &Volumes,
|
||||
}
|
||||
|
||||
var appRemoveCommand = cli.Command{
|
||||
Name: "remove",
|
||||
Aliases: []string{"rm"},
|
||||
Usage: "Remove an already undeployed app",
|
||||
ArgsUsage: "<domain>",
|
||||
Usage: "Remove all app data, locally and remotely",
|
||||
Description: `
|
||||
This command removes everything related to an app which is already undeployed.
|
||||
|
||||
By default, it will prompt for confirmation before proceeding. All secrets,
|
||||
volumes and the local app env file will be deleted.
|
||||
|
||||
Only run this command when you are sure you want to completely remove the app
|
||||
and all associated app data. This is a destructive action, Be Careful!
|
||||
|
||||
If you would like to delete specific volumes or secrets, please use removal
|
||||
sub-commands under "app volume" and "app secret" instead.
|
||||
|
||||
Please note, if you delete the local app env file without removing volumes and
|
||||
secrets first, Abra will *not* be able to help you remove them afterwards.
|
||||
|
||||
To delete everything without prompt, use the "--force/-f" or the "--no-input/n"
|
||||
flag.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
VolumesFlag,
|
||||
internal.ForceFlag,
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
},
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Before: internal.SubCommandBefore,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
if !internal.Force {
|
||||
if !internal.Force && !internal.NoInput {
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("about to remove %s, are you sure?", app.Name),
|
||||
}
|
||||
msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?"
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)}
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -62,11 +70,14 @@ var appRemoveCommand = cli.Command{
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if isDeployed {
|
||||
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s \" or pass --force", app.Name, app.Name)
|
||||
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
|
||||
}
|
||||
|
||||
fs, err := app.Filters(false, false)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
fs := filters.NewArgs()
|
||||
fs.Add("name", app.StackName())
|
||||
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -81,22 +92,7 @@ var appRemoveCommand = cli.Command{
|
||||
}
|
||||
|
||||
if len(secrets) > 0 {
|
||||
var secretNamesToRemove []string
|
||||
|
||||
if !internal.Force {
|
||||
secretsPrompt := &survey.MultiSelect{
|
||||
Message: "which secrets do you want to remove?",
|
||||
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
|
||||
VimMode: true,
|
||||
Options: secretNames,
|
||||
Default: secretNames,
|
||||
}
|
||||
if err := survey.AskOne(secretsPrompt, &secretNamesToRemove); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range secretNamesToRemove {
|
||||
for _, name := range secretNames {
|
||||
err := cl.SecretRemove(context.Background(), secrets[name])
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -107,6 +103,11 @@ var appRemoveCommand = cli.Command{
|
||||
logrus.Info("no secrets to remove")
|
||||
}
|
||||
|
||||
fs, err = app.Filters(false, true)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
volumeListOKBody, err := cl.VolumeList(context.Background(), fs)
|
||||
volumeList := volumeListOKBody.Volumes
|
||||
if err != nil {
|
||||
@ -119,20 +120,7 @@ var appRemoveCommand = cli.Command{
|
||||
}
|
||||
|
||||
if len(vols) > 0 {
|
||||
if Volumes {
|
||||
var removeVols []string
|
||||
if !internal.Force {
|
||||
volumesPrompt := &survey.MultiSelect{
|
||||
Message: "which volumes do you want to remove?",
|
||||
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
|
||||
VimMode: true,
|
||||
Options: vols,
|
||||
Default: vols,
|
||||
}
|
||||
if err := survey.AskOne(volumesPrompt, &removeVols); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, vol := range removeVols {
|
||||
err := cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing
|
||||
if err != nil {
|
||||
@ -141,21 +129,15 @@ var appRemoveCommand = cli.Command{
|
||||
logrus.Info(fmt.Sprintf("volume %s removed", vol))
|
||||
}
|
||||
} else {
|
||||
logrus.Info("no volumes were removed")
|
||||
}
|
||||
} else {
|
||||
if Volumes {
|
||||
logrus.Info("no volumes to remove")
|
||||
}
|
||||
}
|
||||
|
||||
err = os.Remove(app.Path)
|
||||
if err != nil {
|
||||
if err = os.Remove(app.Path); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Info(fmt.Sprintf("file: %s removed", app.Path))
|
||||
|
||||
return nil
|
||||
},
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
}
|
||||
|
@ -18,10 +18,9 @@ var appRestartCommand = cli.Command{
|
||||
Name: "restart",
|
||||
Aliases: []string{"re"},
|
||||
Usage: "Restart an app",
|
||||
ArgsUsage: "<service>",
|
||||
ArgsUsage: "<domain>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `This command restarts a service within a deployed app.`,
|
||||
|
205
cli/app/restore.go
Normal file
205
cli/app/restore.go
Normal file
@ -0,0 +1,205 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"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/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type restoreConfig struct {
|
||||
preHookCmd string
|
||||
postHookCmd string
|
||||
}
|
||||
|
||||
var appRestoreCommand = cli.Command{
|
||||
Name: "restore",
|
||||
Aliases: []string{"rs"},
|
||||
Usage: "Run app restore",
|
||||
ArgsUsage: "<domain> <service> <file>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Description: `
|
||||
Run an app restore.
|
||||
|
||||
Pre/post hook commands are defined in the recipe configuration. Abra reads this
|
||||
configuration and run the comands in the context of the service before
|
||||
restoring the backup.
|
||||
|
||||
Unlike "abra app backup", restore must be run on a per-service basis. You can
|
||||
not restore all services in one go. Backup files produced by Abra are
|
||||
compressed archives which use absolute paths. This allows Abra to restore
|
||||
according to standard tar command logic.
|
||||
|
||||
Example:
|
||||
|
||||
abra app restore example.com app ~/.abra/backups/example_com_app_609341138.tar.gz
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
conf := runtime.New()
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
serviceName := c.Args().Get(1)
|
||||
if serviceName == "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing <service>?"))
|
||||
}
|
||||
|
||||
backupPath := c.Args().Get(2)
|
||||
if backupPath == "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing <file>?"))
|
||||
}
|
||||
|
||||
if _, err := os.Stat(backupPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logrus.Fatalf("%s doesn't exist?", backupPath)
|
||||
}
|
||||
}
|
||||
|
||||
recipe, err := recipe.Get(app.Recipe, conf)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
restoreConfigs := make(map[string]restoreConfig)
|
||||
for _, service := range recipe.Config.Services {
|
||||
if restoreEnabled, ok := service.Deploy.Labels["backupbot.restore"]; ok {
|
||||
if restoreEnabled == "true" {
|
||||
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name)
|
||||
rsConfig := restoreConfig{}
|
||||
|
||||
logrus.Debugf("restore config detected for %s", fullServiceName)
|
||||
|
||||
if preHookCmd, ok := service.Deploy.Labels["backupbot.restore.pre-hook"]; ok {
|
||||
logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd)
|
||||
rsConfig.preHookCmd = preHookCmd
|
||||
}
|
||||
|
||||
if postHookCmd, ok := service.Deploy.Labels["backupbot.restore.post-hook"]; ok {
|
||||
logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd)
|
||||
rsConfig.postHookCmd = postHookCmd
|
||||
}
|
||||
|
||||
restoreConfigs[service.Name] = rsConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rsConfig, ok := restoreConfigs[serviceName]
|
||||
if !ok {
|
||||
rsConfig = restoreConfig{}
|
||||
}
|
||||
|
||||
if err := runRestore(cl, app, backupPath, serviceName, rsConfig); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// runRestore does the actual restore logic.
|
||||
func runRestore(cl *dockerClient.Client, app config.App, backupPath, serviceName string, rsConfig restoreConfig) error {
|
||||
// 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 rsConfig.preHookCmd != "" {
|
||||
splitCmd := internal.SafeSplit(rsConfig.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 := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, rsConfig.preHookCmd)
|
||||
}
|
||||
|
||||
backupReader, err := os.Open(backupPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := archive.DecompressStream(backupReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// we use absolute paths so tar knows what to do. it will restore files
|
||||
// according to the paths set in the compresed archive
|
||||
restorePath := "/"
|
||||
|
||||
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, restorePath, content, copyOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("restored %s to %s", backupPath, fullServiceName)
|
||||
|
||||
if rsConfig.postHookCmd != "" {
|
||||
splitCmd := internal.SafeSplit(rsConfig.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, rsConfig.postHookCmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -8,6 +8,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/tagcmp"
|
||||
|
||||
@ -22,12 +23,13 @@ var appRollbackCommand = cli.Command{
|
||||
Name: "rollback",
|
||||
Aliases: []string{"rl"},
|
||||
Usage: "Roll an app back to a previous version",
|
||||
ArgsUsage: "<app>",
|
||||
ArgsUsage: "<domain>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
internal.ForceFlag,
|
||||
internal.ChaosFlag,
|
||||
internal.NoDomainChecksFlag,
|
||||
internal.DontWaitConvergeFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
@ -48,14 +50,15 @@ recipes.
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
stackName := app.StackName()
|
||||
conf := runtime.New()
|
||||
|
||||
if !internal.Chaos {
|
||||
if err := recipe.EnsureUpToDate(app.Type); err != nil {
|
||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
r, err := recipe.Get(app.Type)
|
||||
r, err := recipe.Get(app.Recipe, conf)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -85,13 +88,13 @@ recipes.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
versions, err := recipe.GetRecipeCatalogueVersions(app.Type, catl)
|
||||
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if len(versions) == 0 && !internal.Chaos {
|
||||
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Type)
|
||||
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Recipe)
|
||||
}
|
||||
|
||||
var availableDowngrades []string
|
||||
@ -125,7 +128,7 @@ recipes.
|
||||
|
||||
var chosenDowngrade string
|
||||
if !internal.Chaos {
|
||||
if internal.Force {
|
||||
if internal.Force || internal.NoInput {
|
||||
chosenDowngrade = availableDowngrades[0]
|
||||
logrus.Debugf("choosing %s as version to downgrade to (--force)", chosenDowngrade)
|
||||
} else {
|
||||
@ -140,7 +143,7 @@ recipes.
|
||||
}
|
||||
|
||||
if !internal.Chaos {
|
||||
if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil {
|
||||
if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -148,13 +151,13 @@ recipes.
|
||||
if internal.Chaos {
|
||||
logrus.Warn("chaos mode engaged")
|
||||
var err error
|
||||
chosenDowngrade, err = recipe.ChaosVersion(app.Type)
|
||||
chosenDowngrade, err = recipe.ChaosVersion(app.Recipe)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Type, "abra.sh")
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
|
||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -163,7 +166,7 @@ recipes.
|
||||
app.Env[k] = v
|
||||
}
|
||||
|
||||
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
|
||||
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -177,6 +180,10 @@ recipes.
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
config.ExposeAllEnv(stackName, compose, app.Env)
|
||||
config.SetRecipeLabel(compose, stackName, app.Recipe)
|
||||
config.SetChaosLabel(compose, stackName, internal.Chaos)
|
||||
config.SetUpdateLabel(compose, stackName, app.Env)
|
||||
|
||||
if !internal.Force {
|
||||
if err := internal.NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil {
|
||||
|
@ -19,14 +19,14 @@ import (
|
||||
|
||||
var user string
|
||||
var userFlag = &cli.StringFlag{
|
||||
Name: "user",
|
||||
Name: "user, u",
|
||||
Value: "",
|
||||
Destination: &user,
|
||||
}
|
||||
|
||||
var noTTY bool
|
||||
var noTTYFlag = &cli.BoolFlag{
|
||||
Name: "no-tty",
|
||||
Name: "no-tty, t",
|
||||
Destination: &noTTY,
|
||||
}
|
||||
|
||||
@ -35,12 +35,11 @@ var appRunCommand = cli.Command{
|
||||
Aliases: []string{"r"},
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
noTTYFlag,
|
||||
userFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
ArgsUsage: "<service> <args>...",
|
||||
ArgsUsage: "<domain> <service> <args>...",
|
||||
Usage: "Run a command in a service container",
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Action: func(c *cli.Context) error {
|
||||
@ -60,11 +59,11 @@ var appRunCommand = cli.Command{
|
||||
}
|
||||
|
||||
serviceName := c.Args().Get(1)
|
||||
stackAndServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
|
||||
stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", stackAndServiceName)
|
||||
|
||||
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
|
||||
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
@ -10,10 +10,11 @@ import (
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/secret"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -25,21 +26,33 @@ var allSecretsFlag = &cli.BoolFlag{
|
||||
Usage: "Generate all secrets",
|
||||
}
|
||||
|
||||
var rmAllSecrets bool
|
||||
var rmAllSecretsFlag = &cli.BoolFlag{
|
||||
Name: "all, a",
|
||||
Destination: &rmAllSecrets,
|
||||
Usage: "Remove all secrets",
|
||||
}
|
||||
|
||||
var appSecretGenerateCommand = cli.Command{
|
||||
Name: "generate",
|
||||
Aliases: []string{"g"},
|
||||
Usage: "Generate secrets",
|
||||
ArgsUsage: "<secret> <version>",
|
||||
ArgsUsage: "<domain> <secret> <version>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
allSecretsFlag, internal.PassFlag,
|
||||
allSecretsFlag,
|
||||
internal.PassFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if len(c.Args()) == 1 && !allSecrets {
|
||||
err := errors.New("missing arguments <secret>/<version> or '--all'")
|
||||
internal.ShowSubcommandHelpAndError(c, err)
|
||||
@ -62,21 +75,23 @@ var appSecretGenerateCommand = cli.Command{
|
||||
parsed := secret.ParseSecretEnvVarName(sec)
|
||||
if secretName == parsed {
|
||||
secretsToCreate[sec] = secretVersion
|
||||
matches = true
|
||||
}
|
||||
}
|
||||
|
||||
if !matches {
|
||||
logrus.Fatalf("%s doesn't exist in the env config?", secretName)
|
||||
}
|
||||
}
|
||||
|
||||
secretVals, err := secret.GenerateSecrets(secretsToCreate, app.StackName(), app.Server)
|
||||
secretVals, err := secret.GenerateSecrets(cl, secretsToCreate, app.StackName(), app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if internal.Pass {
|
||||
for name, data := range secretVals {
|
||||
if err := secret.PassInsertSecret(data, name, app.StackName(), app.Server); err != nil {
|
||||
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -105,11 +120,10 @@ var appSecretInsertCommand = cli.Command{
|
||||
Usage: "Insert secret",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
internal.PassFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
ArgsUsage: "<app> <secret-name> <version> <data>",
|
||||
ArgsUsage: "<domain> <secret-name> <version> <data>",
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Description: `
|
||||
This command inserts a secret into an app environment.
|
||||
@ -126,6 +140,11 @@ Example:
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if len(c.Args()) != 4 {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
|
||||
}
|
||||
@ -135,12 +154,14 @@ Example:
|
||||
data := c.Args().Get(3)
|
||||
|
||||
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
|
||||
if err := client.StoreSecret(secretName, data, app.Server); err != nil {
|
||||
if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Infof("%s successfully stored on server", secretName)
|
||||
|
||||
if internal.Pass {
|
||||
if err := secret.PassInsertSecret(data, name, app.StackName(), app.Server); err != nil {
|
||||
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -149,6 +170,25 @@ Example:
|
||||
},
|
||||
}
|
||||
|
||||
// secretRm removes a secret.
|
||||
func secretRm(cl *dockerClient.Client, app config.App, secretName, parsed string) error {
|
||||
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("deleted %s successfully from server", secretName)
|
||||
|
||||
if internal.PassRemove {
|
||||
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("deleted %s successfully from local pass store", secretName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var appSecretRmCommand = cli.Command{
|
||||
Name: "remove",
|
||||
Aliases: []string{"rm"},
|
||||
@ -156,27 +196,28 @@ var appSecretRmCommand = cli.Command{
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
allSecretsFlag, internal.PassFlag,
|
||||
rmAllSecretsFlag,
|
||||
internal.PassRemoveFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
ArgsUsage: "<app> <secret-name>",
|
||||
ArgsUsage: "<domain> [<secret-name>]",
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Description: `
|
||||
This command removes a secret from an app environment.
|
||||
This command removes app secrets.
|
||||
|
||||
Example:
|
||||
|
||||
abra app secret remove myapp db_pass
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
secrets := secret.ReadSecretEnvVars(app.Env)
|
||||
|
||||
if c.Args().Get(1) != "" && allSecrets {
|
||||
if c.Args().Get(1) != "" && rmAllSecrets {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together"))
|
||||
}
|
||||
|
||||
if c.Args().Get(1) == "" && !allSecrets {
|
||||
if c.Args().Get(1) == "" && !rmAllSecrets {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
|
||||
}
|
||||
|
||||
@ -185,47 +226,57 @@ Example:
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", app.StackName())
|
||||
filters, err := app.Filters(false, false)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
secretToRm := c.Args().Get(1)
|
||||
remoteSecretNames := make(map[string]bool)
|
||||
for _, cont := range secretList {
|
||||
secretName := cont.Spec.Annotations.Name
|
||||
parsed := secret.ParseGeneratedSecretName(secretName, app)
|
||||
if allSecrets {
|
||||
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
remoteSecretNames[cont.Spec.Annotations.Name] = true
|
||||
}
|
||||
logrus.Infof("deleted %s successfully from server", secretName)
|
||||
|
||||
if internal.Pass {
|
||||
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
|
||||
match := false
|
||||
secretToRm := c.Args().Get(1)
|
||||
for sec := range secrets {
|
||||
secretName := secret.ParseSecretEnvVarName(sec)
|
||||
|
||||
secVal, err := secret.ParseSecretEnvVarValue(secrets[sec])
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Infof("deleted %s successfully from local pass store", secretName)
|
||||
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, secVal.Version)
|
||||
if _, ok := remoteSecretNames[secretRemoteName]; ok {
|
||||
if secretToRm != "" {
|
||||
if secretName == secretToRm {
|
||||
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if parsed == secretToRm {
|
||||
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
|
||||
match = true
|
||||
|
||||
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Infof("deleted %s successfully from server", secretName)
|
||||
|
||||
if internal.Pass {
|
||||
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("deleted %s successfully from local pass store", secretName)
|
||||
}
|
||||
}
|
||||
if !match && secretToRm != "" {
|
||||
logrus.Fatalf("%s doesn't exist on server?", secretToRm)
|
||||
}
|
||||
|
||||
if !match {
|
||||
logrus.Fatal("no secrets to remove?")
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -237,7 +288,6 @@ var appSecretLsCommand = cli.Command{
|
||||
Aliases: []string{"ls"},
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Usage: "List all secrets",
|
||||
@ -253,8 +303,11 @@ var appSecretLsCommand = cli.Command{
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", app.StackName())
|
||||
filters, err := app.Filters(false, false)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -295,7 +348,7 @@ var appSecretCommand = cli.Command{
|
||||
Name: "secret",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Manage app secrets",
|
||||
ArgsUsage: "<command>",
|
||||
ArgsUsage: "<domain>",
|
||||
Subcommands: []cli.Command{
|
||||
appSecretGenerateCommand,
|
||||
appSecretInsertCommand,
|
||||
|
80
cli/app/services.go
Normal file
80
cli/app/services.go
Normal file
@ -0,0 +1,80 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/service"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var appServicesCommand = cli.Command{
|
||||
Name: "services",
|
||||
Aliases: []string{"sr"},
|
||||
Usage: "Display all services of an app",
|
||||
ArgsUsage: "<domain>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if !isDeployed {
|
||||
logrus.Fatalf("%s is not deployed?", app.Name)
|
||||
}
|
||||
|
||||
filters, err := app.Filters(true, true)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
tableCol := []string{"service name", "image"}
|
||||
table := formatter.CreateTable(tableCol)
|
||||
|
||||
for _, container := range containers {
|
||||
var containerNames []string
|
||||
for _, containerName := range container.Names {
|
||||
trimmed := strings.TrimPrefix(containerName, "/")
|
||||
containerNames = append(containerNames, trimmed)
|
||||
}
|
||||
|
||||
serviceShortName := service.ContainerToServiceName(container.Names, app.StackName())
|
||||
serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName)
|
||||
|
||||
tableRow := []string{
|
||||
serviceLongName,
|
||||
formatter.RemoveSha(container.Image),
|
||||
}
|
||||
table.Append(tableRow)
|
||||
}
|
||||
|
||||
table.Render()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
@ -2,33 +2,89 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var prune bool
|
||||
|
||||
var pruneFlag = &cli.BoolFlag{
|
||||
Name: "prune, p",
|
||||
Destination: &prune,
|
||||
Usage: "Prunes unused containers, networks, and dangling images for an app",
|
||||
}
|
||||
|
||||
func cleanup(c *cli.Context) error {
|
||||
for {
|
||||
if !prune {
|
||||
return nil
|
||||
}
|
||||
app := internal.ValidateApp(c)
|
||||
stackName := app.StackName()
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
pruneFilters := filters.NewArgs()
|
||||
stackSearch := fmt.Sprintf("%s*", stackName)
|
||||
pruneFilters.Add("label", stackSearch)
|
||||
cr, err := cl.ContainersPrune(ctx, pruneFilters)
|
||||
if err != nil {
|
||||
logrus.Errorf(err.Error())
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
logrus.Infof("Containers deleted: %s; Space reclaimed: %v", cr.ContainersDeleted, cr.SpaceReclaimed)
|
||||
|
||||
nr, err := cl.NetworksPrune(ctx, pruneFilters)
|
||||
if err != nil {
|
||||
logrus.Errorf(err.Error())
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
logrus.Infof("Networks deleted %s", nr.NetworksDeleted)
|
||||
|
||||
ir, err := cl.ImagesPrune(ctx, pruneFilters)
|
||||
if err != nil {
|
||||
logrus.Errorf(err.Error())
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
logrus.Infof("Images deleted: %s; Space reclaimed: %v", ir.ImagesDeleted, ir.SpaceReclaimed)
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var appUndeployCommand = cli.Command{
|
||||
Name: "undeploy",
|
||||
Aliases: []string{"un"},
|
||||
ArgsUsage: "<domain>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
pruneFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Usage: "Undeploy an app",
|
||||
Description: `
|
||||
This does not destroy any of the application data. However, you should remain
|
||||
vigilant, as your swarm installation will consider any previously attached
|
||||
volumes as eligiblef or pruning once undeployed.
|
||||
volumes as eligible for pruning once undeployed.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
stackName := app.StackName()
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -53,7 +109,7 @@ volumes as eligiblef or pruning once undeployed.
|
||||
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
cleanup(c)
|
||||
return nil
|
||||
},
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
@ -21,21 +22,22 @@ var appUpgradeCommand = cli.Command{
|
||||
Name: "upgrade",
|
||||
Aliases: []string{"up"},
|
||||
Usage: "Upgrade an app",
|
||||
ArgsUsage: "<app>",
|
||||
ArgsUsage: "<domain>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
internal.ForceFlag,
|
||||
internal.ChaosFlag,
|
||||
internal.NoDomainChecksFlag,
|
||||
internal.DontWaitConvergeFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `
|
||||
This command supports upgrading an app. You can use it to choose and roll out a
|
||||
new upgrade to an existing app.
|
||||
Upgrade an app. You can use it to choose and roll out a new upgrade to an
|
||||
existing app.
|
||||
|
||||
This command specifically supports incrementing the version of running apps, as
|
||||
opposed to "abra app deploy <app>" which will not change the version of a
|
||||
opposed to "abra app deploy <domain>" which will not change the version of a
|
||||
deployed app.
|
||||
|
||||
You may pass "--force/-f" to upgrade to the same version again. This can be
|
||||
@ -51,14 +53,20 @@ recipes.
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
stackName := app.StackName()
|
||||
conf := runtime.New()
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if !internal.Chaos {
|
||||
if err := recipe.EnsureUpToDate(app.Type); err != nil {
|
||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
r, err := recipe.Get(app.Type)
|
||||
r, err := recipe.Get(app.Recipe, conf)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -67,11 +75,6 @@ recipes.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("checking whether %s is already deployed", stackName)
|
||||
|
||||
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
|
||||
@ -88,17 +91,17 @@ recipes.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
versions, err := recipe.GetRecipeCatalogueVersions(app.Type, catl)
|
||||
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if len(versions) == 0 && !internal.Chaos {
|
||||
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Type)
|
||||
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Recipe)
|
||||
}
|
||||
|
||||
var availableUpgrades []string
|
||||
if deployedVersion == "uknown" {
|
||||
if deployedVersion == "unknown" {
|
||||
availableUpgrades = versions
|
||||
logrus.Warnf("failed to determine version of deployed %s", app.Name)
|
||||
}
|
||||
@ -128,7 +131,7 @@ recipes.
|
||||
|
||||
var chosenUpgrade string
|
||||
if len(availableUpgrades) > 0 && !internal.Chaos {
|
||||
if internal.Force {
|
||||
if internal.Force || internal.NoInput {
|
||||
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
|
||||
logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
|
||||
} else {
|
||||
@ -145,13 +148,13 @@ recipes.
|
||||
// if release notes written after git tag published, read them before we
|
||||
// check out the tag and then they'll appear to be missing. this covers
|
||||
// when we obviously will forget to write release notes before publishing
|
||||
releaseNotes, err := internal.GetReleaseNotes(app.Type, chosenUpgrade)
|
||||
releaseNotes, err := internal.GetReleaseNotes(app.Recipe, chosenUpgrade)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !internal.Chaos {
|
||||
if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil {
|
||||
if err := recipe.EnsureVersion(app.Recipe, chosenUpgrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -159,13 +162,13 @@ recipes.
|
||||
if internal.Chaos {
|
||||
logrus.Warn("chaos mode engaged")
|
||||
var err error
|
||||
chosenUpgrade, err = recipe.ChaosVersion(app.Type)
|
||||
chosenUpgrade, err = recipe.ChaosVersion(app.Recipe)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Type, "abra.sh")
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
|
||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -174,7 +177,7 @@ recipes.
|
||||
app.Env[k] = v
|
||||
}
|
||||
|
||||
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
|
||||
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -188,6 +191,10 @@ recipes.
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
config.ExposeAllEnv(stackName, compose, app.Env)
|
||||
config.SetRecipeLabel(compose, stackName, app.Recipe)
|
||||
config.SetChaosLabel(compose, stackName, internal.Chaos)
|
||||
config.SetUpdateLabel(compose, stackName, app.Env)
|
||||
|
||||
if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil {
|
||||
logrus.Fatal(err)
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -33,6 +34,7 @@ func getImagePath(image string) (string, error) {
|
||||
var appVersionCommand = cli.Command{
|
||||
Name: "version",
|
||||
Aliases: []string{"v"},
|
||||
ArgsUsage: "<domain>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
@ -40,13 +42,14 @@ var appVersionCommand = cli.Command{
|
||||
Before: internal.SubCommandBefore,
|
||||
Usage: "Show app versions",
|
||||
Description: `
|
||||
This command shows all information about versioning related to a deployed app.
|
||||
This includes the individual image names, tags and digests. But also the Co-op
|
||||
Cloud recipe version.
|
||||
Show all information about versioning related to a deployed app. This includes
|
||||
the individual image names, tags and digests. But also the Co-op Cloud recipe
|
||||
version.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
stackName := app.StackName()
|
||||
conf := runtime.New()
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
@ -68,7 +71,7 @@ Cloud recipe version.
|
||||
logrus.Fatalf("%s is not deployed?", app.Name)
|
||||
}
|
||||
|
||||
recipeMeta, err := recipe.GetRecipeMeta(app.Type)
|
||||
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe, conf)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -84,12 +87,12 @@ Cloud recipe version.
|
||||
logrus.Fatalf("could not retrieve deployed version (%s) from recipe catalogue?", deployedVersion)
|
||||
}
|
||||
|
||||
tableCol := []string{"version", "service", "image", "digest"}
|
||||
tableCol := []string{"version", "service", "image"}
|
||||
table := formatter.CreateTable(tableCol)
|
||||
table.SetAutoMergeCellsByColumnIndex([]int{0})
|
||||
|
||||
for serviceName, versionMeta := range versionsMeta {
|
||||
table.Append([]string{deployedVersion, serviceName, versionMeta.Image, versionMeta.Digest})
|
||||
table.Append([]string{deployedVersion, serviceName, versionMeta.Image})
|
||||
}
|
||||
|
||||
table.Render()
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
var appVolumeListCommand = cli.Command{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
ArgsUsage: "<domain>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
@ -25,18 +26,25 @@ var appVolumeListCommand = cli.Command{
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
volumeList, err := client.GetVolumes(context.Background(), app.Server, app.Name)
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
table := formatter.CreateTable([]string{"driver", "volume name"})
|
||||
filters, err := app.Filters(false, true)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
table := formatter.CreateTable([]string{"name", "created", "mounted"})
|
||||
var volTable [][]string
|
||||
for _, volume := range volumeList {
|
||||
volRow := []string{
|
||||
volume.Driver,
|
||||
volume.Name,
|
||||
}
|
||||
volRow := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
|
||||
volTable = append(volTable, volRow)
|
||||
}
|
||||
|
||||
@ -58,15 +66,15 @@ var appVolumeRemoveCommand = cli.Command{
|
||||
Description: `
|
||||
This command supports removing volumes associated with an app. The app in
|
||||
question must be undeployed before you try to remove volumes. See "abra app
|
||||
undeploy <app>" for more.
|
||||
undeploy <domain>" for more.
|
||||
|
||||
The command is interactive and will show a multiple select input which allows
|
||||
you to make a seclection. Use the "?" key to see more help on navigating this
|
||||
interface.
|
||||
|
||||
Passing "--force" will select all volumes for removal. Be careful.
|
||||
Passing "--force/-f" will select all volumes for removal. Be careful.
|
||||
`,
|
||||
ArgsUsage: "<app>",
|
||||
ArgsUsage: "<domain>",
|
||||
Aliases: []string{"rm"},
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
@ -77,14 +85,24 @@ Passing "--force" will select all volumes for removal. Be careful.
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
volumeList, err := client.GetVolumes(context.Background(), app.Server, app.Name)
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
filters, err := app.Filters(false, true)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
volumeNames := client.GetVolumeNames(volumeList)
|
||||
|
||||
var volumesToRemove []string
|
||||
if !internal.Force {
|
||||
if !internal.Force && !internal.NoInput {
|
||||
volumesPrompt := &survey.MultiSelect{
|
||||
Message: "which volumes do you want to remove?",
|
||||
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
|
||||
@ -95,11 +113,13 @@ Passing "--force" will select all volumes for removal. Be careful.
|
||||
if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if internal.Force || internal.NoInput {
|
||||
volumesToRemove = volumeNames
|
||||
}
|
||||
|
||||
err = client.RemoveVolumes(context.Background(), app.Server, volumesToRemove, internal.Force)
|
||||
err = client.RemoveVolumes(cl, context.Background(), app.Server, volumesToRemove, internal.Force)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -115,7 +135,7 @@ var appVolumeCommand = cli.Command{
|
||||
Name: "volume",
|
||||
Aliases: []string{"vl"},
|
||||
Usage: "Manage app volumes",
|
||||
ArgsUsage: "<command>",
|
||||
ArgsUsage: "<domain>",
|
||||
Subcommands: []cli.Command{
|
||||
appVolumeListCommand,
|
||||
appVolumeRemoveCommand,
|
||||
|
@ -8,54 +8,16 @@ import (
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/limit"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// CatalogueSkipList is all the repos that are not recipes.
|
||||
var CatalogueSkipList = map[string]bool{
|
||||
"abra": true,
|
||||
"abra-apps": true,
|
||||
"abra-aur": true,
|
||||
"abra-bash": true,
|
||||
"abra-capsul": true,
|
||||
"abra-gandi": true,
|
||||
"abra-hetzner": true,
|
||||
"apps": true,
|
||||
"aur-abra-git": true,
|
||||
"auto-apps-json": true,
|
||||
"auto-mirror": true,
|
||||
"backup-bot": true,
|
||||
"backup-bot-two": true,
|
||||
"beta.coopcloud.tech": true,
|
||||
"comrade-renovate-bot": true,
|
||||
"coopcloud.tech": true,
|
||||
"coturn": true,
|
||||
"docker-cp-deploy": true,
|
||||
"docker-dind-bats-kcov": true,
|
||||
"docs.coopcloud.tech": true,
|
||||
"drone-abra": true,
|
||||
"example": true,
|
||||
"gardening": true,
|
||||
"go-abra": true,
|
||||
"organising": true,
|
||||
"outline-with-patch": true,
|
||||
"pyabra": true,
|
||||
"radicle-seed-node": true,
|
||||
"recipes": true,
|
||||
"stack-ssh-deploy": true,
|
||||
"swarm-cronjob": true,
|
||||
"tagcmp": true,
|
||||
"traefik-cert-dumper": true,
|
||||
"tyop": true,
|
||||
}
|
||||
|
||||
var catalogueGenerateCommand = cli.Command{
|
||||
Name: "generate",
|
||||
Aliases: []string{"g"},
|
||||
@ -66,18 +28,17 @@ var catalogueGenerateCommand = cli.Command{
|
||||
internal.PublishFlag,
|
||||
internal.DryFlag,
|
||||
internal.SkipUpdatesFlag,
|
||||
internal.RegistryUsernameFlag,
|
||||
internal.RegistryPasswordFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `
|
||||
This command generates a new copy of the recipe catalogue which can be found on:
|
||||
Generate a new copy of the recipe catalogue which can be found on:
|
||||
|
||||
https://recipes.coopcloud.tech
|
||||
https://recipes.coopcloud.tech (website that humans read)
|
||||
https://recipes.coopcloud.tech/recipes.json (JSON that Abra reads)
|
||||
|
||||
It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository
|
||||
listing, parses README.md and git tags of those repositories to produce recipe
|
||||
metadata and produces a recipes JSON file.
|
||||
listing, parses README.md and git tags to produce recipe metadata which is
|
||||
loaded into the catalogue JSON file.
|
||||
|
||||
It is possible to generate new metadata for a single recipe by passing
|
||||
<recipe>. The existing local catalogue will be updated, not overwritten.
|
||||
@ -86,7 +47,7 @@ It is quite easy to get rate limited by Docker Hub when running this command.
|
||||
If you have a Hub account you can have Abra log you in to avoid this. Pass
|
||||
"--user" and "--pass".
|
||||
|
||||
Push your new release git.coopcloud.tech with "-p/--publish". This requires
|
||||
Push your new release to git.coopcloud.tech with "-p/--publish". This requires
|
||||
that you have permission to git push to these repositories and have your SSH
|
||||
keys configured on your account.
|
||||
`,
|
||||
@ -97,6 +58,10 @@ keys configured on your account.
|
||||
internal.ValidateRecipe(c)
|
||||
}
|
||||
|
||||
if err := catalogue.EnsureUpToDate(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
repos, err := recipe.ReadReposMetadata()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -114,7 +79,7 @@ keys configured on your account.
|
||||
|
||||
if !internal.SkipUpdates {
|
||||
logrus.Warn(logMsg)
|
||||
if err := updateRepositories(repos, recipeName); err != nil {
|
||||
if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -127,18 +92,14 @@ keys configured on your account.
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := CatalogueSkipList[recipeMeta.Name]; exists {
|
||||
if _, exists := catalogue.CatalogueSkipList[recipeMeta.Name]; exists {
|
||||
catlBar.Add(1)
|
||||
continue
|
||||
}
|
||||
|
||||
versions, err := recipe.GetRecipeVersions(
|
||||
recipeMeta.Name,
|
||||
internal.RegistryUsername,
|
||||
internal.RegistryPassword,
|
||||
)
|
||||
versions, err := recipe.GetRecipeVersions(recipeMeta.Name)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
logrus.Warn(err)
|
||||
}
|
||||
|
||||
features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name)
|
||||
@ -215,7 +176,7 @@ keys configured on your account.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, "recipes")
|
||||
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
|
||||
if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -236,7 +197,7 @@ keys configured on your account.
|
||||
}
|
||||
|
||||
if !internal.Dry && internal.Publish {
|
||||
url := fmt.Sprintf("%s/recipes/commit/%s", config.REPOS_BASE_URL, head.Hash())
|
||||
url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash())
|
||||
logrus.Infof("new changes published: %s", url)
|
||||
}
|
||||
|
||||
@ -260,62 +221,3 @@ var CatalogueCommand = cli.Command{
|
||||
catalogueGenerateCommand,
|
||||
},
|
||||
}
|
||||
|
||||
func updateRepositories(repos recipe.RepoCatalogue, recipeName string) error {
|
||||
var barLength int
|
||||
if recipeName != "" {
|
||||
barLength = 1
|
||||
} else {
|
||||
barLength = len(repos)
|
||||
}
|
||||
|
||||
cloneLimiter := limit.New(10)
|
||||
|
||||
retrieveBar := formatter.CreateProgressbar(barLength, "ensuring recipes are cloned & up-to-date...")
|
||||
ch := make(chan string, barLength)
|
||||
for _, repoMeta := range repos {
|
||||
go func(rm recipe.RepoMeta) {
|
||||
cloneLimiter.Begin()
|
||||
defer cloneLimiter.End()
|
||||
|
||||
if recipeName != "" && recipeName != rm.Name {
|
||||
ch <- rm.Name
|
||||
retrieveBar.Add(1)
|
||||
return
|
||||
}
|
||||
if _, exists := CatalogueSkipList[rm.Name]; exists {
|
||||
ch <- rm.Name
|
||||
retrieveBar.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
recipeDir := path.Join(config.RECIPES_DIR, rm.Name)
|
||||
|
||||
if err := gitPkg.Clone(recipeDir, rm.CloneURL); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
isClean, err := gitPkg.IsClean(recipeDir)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
logrus.Fatalf("%s has locally unstaged changes", rm.Name)
|
||||
}
|
||||
|
||||
if err := recipe.EnsureUpToDate(rm.Name); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
ch <- rm.Name
|
||||
retrieveBar.Add(1)
|
||||
}(repoMeta)
|
||||
}
|
||||
|
||||
for range repos {
|
||||
<-ch // wait for everything
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
55
cli/cli.go
55
cli/cli.go
@ -14,6 +14,7 @@ import (
|
||||
"coopcloud.tech/abra/cli/recipe"
|
||||
"coopcloud.tech/abra/cli/record"
|
||||
"coopcloud.tech/abra/cli/server"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/web"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -26,25 +27,17 @@ var AutoCompleteCommand = cli.Command{
|
||||
Aliases: []string{"ac"},
|
||||
Usage: "Configure shell autocompletion (recommended)",
|
||||
Description: `
|
||||
This command helps set up autocompletion in your shell by downloading the
|
||||
relevant autocompletion files and laying out what additional information must
|
||||
be loaded.
|
||||
Set up auto-completion in your shell by downloading the relevant files and
|
||||
laying out what additional information must be loaded. Supported shells are as
|
||||
follows: bash, fish, fizsh & zsh.
|
||||
|
||||
Example:
|
||||
|
||||
abra autocomplete bash
|
||||
|
||||
Supported shells are as follows:
|
||||
|
||||
fizsh
|
||||
zsh
|
||||
bash
|
||||
|
||||
`,
|
||||
ArgsUsage: "<shell>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
shellType := c.Args().First()
|
||||
@ -57,6 +50,7 @@ Supported shells are as follows:
|
||||
"bash": true,
|
||||
"zsh": true,
|
||||
"fizsh": true,
|
||||
"fish": true,
|
||||
}
|
||||
|
||||
if _, ok := supportedShells[shellType]; !ok {
|
||||
@ -87,19 +81,27 @@ Supported shells are as follows:
|
||||
switch shellType {
|
||||
case "bash":
|
||||
fmt.Println(fmt.Sprintf(`
|
||||
# Run the following commands to install autocompletion
|
||||
# Run the following commands to install auto-completion
|
||||
sudo mkdir /etc/bash_completion.d/
|
||||
sudo cp %s /etc/bash_completion.d/abra
|
||||
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
|
||||
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app names listed!
|
||||
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
|
||||
`, autocompletionFile))
|
||||
case "zsh":
|
||||
fmt.Println(fmt.Sprintf(`
|
||||
# Run the following commands to install autocompletion
|
||||
# Run the following commands to install auto-completion
|
||||
sudo mkdir /etc/zsh/completion.d/
|
||||
sudo cp %s /etc/zsh/completion.d/abra
|
||||
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
|
||||
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app names listed!
|
||||
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
|
||||
`, autocompletionFile))
|
||||
case "fish":
|
||||
fmt.Println(fmt.Sprintf(`
|
||||
# Run the following commands to install auto-completion
|
||||
sudo mkdir -p /etc/fish/completions
|
||||
sudo cp %s /etc/fish/completions/abra
|
||||
echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
|
||||
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
|
||||
`, autocompletionFile))
|
||||
}
|
||||
|
||||
@ -113,13 +115,11 @@ var UpgradeCommand = cli.Command{
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Upgrade Abra itself",
|
||||
Description: `
|
||||
This command allows you to upgrade Abra in-place with the latest stable or
|
||||
release candidate.
|
||||
Upgrade Abra in-place with the latest stable or release candidate.
|
||||
|
||||
If you would like to install the latest release candidate, please pass the
|
||||
"--rc" option. Please bear in mind that the latest release candidate may have
|
||||
some catastrophic bugs contained in it. In any case, thank you very much for
|
||||
the testing efforts!
|
||||
Pass "-r/--rc" to install the latest release candidate. Please bear in mind
|
||||
that it may contain catastrophic bugs. Thank you very much for the testing
|
||||
efforts!
|
||||
`,
|
||||
Flags: []cli.Flag{internal.RCFlag},
|
||||
Action: func(c *cli.Context) error {
|
||||
@ -162,16 +162,7 @@ func newAbraApp(version, commit string) *cli.App {
|
||||
UpgradeCommand,
|
||||
AutoCompleteCommand,
|
||||
},
|
||||
Authors: []cli.Author{
|
||||
// If you're looking at this and you hack on Abra and you're not listed
|
||||
// here, please do add yourself! This is a community project, let's show
|
||||
// some love
|
||||
{Name: "3wordchant"},
|
||||
{Name: "decentral1se"},
|
||||
{Name: "kawaiipunk"},
|
||||
{Name: "knoflook"},
|
||||
{Name: "roxxers"},
|
||||
},
|
||||
BashComplete: autocomplete.SubcommandComplete,
|
||||
}
|
||||
|
||||
app.EnableBashCompletion = true
|
||||
@ -182,6 +173,7 @@ func newAbraApp(version, commit string) *cli.App {
|
||||
path.Join(config.SERVERS_DIR),
|
||||
path.Join(config.RECIPES_DIR),
|
||||
path.Join(config.VENDOR_DIR),
|
||||
path.Join(config.BACKUP_DIR),
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
@ -197,6 +189,7 @@ func newAbraApp(version, commit string) *cli.App {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
|
35
cli/internal/backup.go
Normal file
35
cli/internal/backup.go
Normal file
@ -0,0 +1,35 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SafeSplit splits up a string into a list of commands safely.
|
||||
func SafeSplit(s string) []string {
|
||||
split := strings.Split(s, " ")
|
||||
|
||||
var result []string
|
||||
var inquote string
|
||||
var block string
|
||||
for _, i := range split {
|
||||
if inquote == "" {
|
||||
if strings.HasPrefix(i, "'") || strings.HasPrefix(i, "\"") {
|
||||
inquote = string(i[0])
|
||||
block = strings.TrimPrefix(i, inquote) + " "
|
||||
} else {
|
||||
result = append(result, i)
|
||||
}
|
||||
} else {
|
||||
if !strings.HasSuffix(i, inquote) {
|
||||
block += i + " "
|
||||
} else {
|
||||
block += strings.TrimSuffix(i, inquote)
|
||||
inquote = ""
|
||||
result = append(result, block)
|
||||
block = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
@ -13,7 +13,7 @@ var Secrets bool
|
||||
|
||||
// SecretsFlag turns on/off automatically generating secrets
|
||||
var SecretsFlag = &cli.BoolFlag{
|
||||
Name: "secrets, ss",
|
||||
Name: "secrets, S",
|
||||
Usage: "Automatically generate secrets",
|
||||
Destination: &Secrets,
|
||||
}
|
||||
@ -28,14 +28,14 @@ var PassFlag = &cli.BoolFlag{
|
||||
Destination: &Pass,
|
||||
}
|
||||
|
||||
// Context is temp
|
||||
var Context string
|
||||
// PassRemove stores the variable for PassRemoveFlag
|
||||
var PassRemove bool
|
||||
|
||||
// ContextFlag is temp
|
||||
var ContextFlag = &cli.StringFlag{
|
||||
Name: "context, c",
|
||||
Value: "",
|
||||
Destination: &Context,
|
||||
// PassRemoveFlag turns on/off removing generated secrets from pass
|
||||
var PassRemoveFlag = &cli.BoolFlag{
|
||||
Name: "pass, p",
|
||||
Usage: "Remove generated secrets from a local pass store",
|
||||
Destination: &PassRemove,
|
||||
}
|
||||
|
||||
// Force force functionality without asking.
|
||||
@ -53,7 +53,7 @@ var Chaos bool
|
||||
|
||||
// ChaosFlag turns on/off chaos functionality.
|
||||
var ChaosFlag = &cli.BoolFlag{
|
||||
Name: "chaos, ch",
|
||||
Name: "chaos, C",
|
||||
Usage: "Deploy uncommitted recipes changes. Use with care!",
|
||||
Destination: &Chaos,
|
||||
}
|
||||
@ -79,7 +79,7 @@ var NoInputFlag = &cli.BoolFlag{
|
||||
var DNSType string
|
||||
|
||||
var DNSTypeFlag = &cli.StringFlag{
|
||||
Name: "type, t",
|
||||
Name: "record-type, rt",
|
||||
Value: "",
|
||||
Usage: "Domain name record type (e.g. A)",
|
||||
Destination: &DNSType,
|
||||
@ -88,7 +88,7 @@ var DNSTypeFlag = &cli.StringFlag{
|
||||
var DNSName string
|
||||
|
||||
var DNSNameFlag = &cli.StringFlag{
|
||||
Name: "name, n",
|
||||
Name: "record-name, rn",
|
||||
Value: "",
|
||||
Usage: "Domain name record name (e.g. mysubdomain)",
|
||||
Destination: &DNSName,
|
||||
@ -97,7 +97,7 @@ var DNSNameFlag = &cli.StringFlag{
|
||||
var DNSValue string
|
||||
|
||||
var DNSValueFlag = &cli.StringFlag{
|
||||
Name: "value, v",
|
||||
Name: "record-value, rv",
|
||||
Value: "",
|
||||
Usage: "Domain name record value (e.g. 192.168.1.1)",
|
||||
Destination: &DNSValue,
|
||||
@ -105,7 +105,7 @@ var DNSValueFlag = &cli.StringFlag{
|
||||
|
||||
var DNSTTL string
|
||||
var DNSTTLFlag = &cli.StringFlag{
|
||||
Name: "ttl, T",
|
||||
Name: "record-ttl, rl",
|
||||
Value: "600s",
|
||||
Usage: "Domain name TTL value (seconds)",
|
||||
Destination: &DNSTTL,
|
||||
@ -114,7 +114,7 @@ var DNSTTLFlag = &cli.StringFlag{
|
||||
var DNSPriority int
|
||||
|
||||
var DNSPriorityFlag = &cli.IntFlag{
|
||||
Name: "priority, P",
|
||||
Name: "record-priority, rp",
|
||||
Value: 10,
|
||||
Usage: "Domain name priority value",
|
||||
Destination: &DNSPriority,
|
||||
@ -243,40 +243,50 @@ var DebugFlag = &cli.BoolFlag{
|
||||
Usage: "Show DEBUG messages",
|
||||
}
|
||||
|
||||
// MachineReadable stores the variable from MachineReadableFlag
|
||||
var MachineReadable bool
|
||||
|
||||
// MachineReadableFlag turns on/off machine readable output where supported
|
||||
var MachineReadableFlag = &cli.BoolFlag{
|
||||
Name: "machine, m",
|
||||
Destination: &MachineReadable,
|
||||
Usage: "Output in a machine-readable format (where supported)",
|
||||
}
|
||||
|
||||
// RC signifies the latest release candidate
|
||||
var RC bool
|
||||
|
||||
// RCFlag chooses the latest release candidate for install
|
||||
var RCFlag = &cli.BoolFlag{
|
||||
Name: "rc",
|
||||
Name: "rc, r",
|
||||
Destination: &RC,
|
||||
Usage: "Insatll the latest release candidate",
|
||||
}
|
||||
|
||||
var Major bool
|
||||
var MajorFlag = &cli.BoolFlag{
|
||||
Name: "major, ma, x",
|
||||
Name: "major, x",
|
||||
Usage: "Increase the major part of the version",
|
||||
Destination: &Major,
|
||||
}
|
||||
|
||||
var Minor bool
|
||||
var MinorFlag = &cli.BoolFlag{
|
||||
Name: "minor, mi, y",
|
||||
Name: "minor, y",
|
||||
Usage: "Increase the minor part of the version",
|
||||
Destination: &Minor,
|
||||
}
|
||||
|
||||
var Patch bool
|
||||
var PatchFlag = &cli.BoolFlag{
|
||||
Name: "patch, pa, z",
|
||||
Name: "patch, z",
|
||||
Usage: "Increase the patch part of the version",
|
||||
Destination: &Patch,
|
||||
}
|
||||
|
||||
var Dry bool
|
||||
var DryFlag = &cli.BoolFlag{
|
||||
Name: "dry-run, dr",
|
||||
Name: "dry-run, r",
|
||||
Usage: "Only reports changes that would be made",
|
||||
Destination: &Dry,
|
||||
}
|
||||
@ -290,7 +300,7 @@ var PublishFlag = &cli.BoolFlag{
|
||||
|
||||
var Domain string
|
||||
var DomainFlag = &cli.StringFlag{
|
||||
Name: "domain, dn",
|
||||
Name: "domain, D",
|
||||
Value: "",
|
||||
Usage: "Choose a domain name",
|
||||
Destination: &Domain,
|
||||
@ -304,17 +314,9 @@ var NewAppServerFlag = &cli.StringFlag{
|
||||
Destination: &NewAppServer,
|
||||
}
|
||||
|
||||
var NewAppName string
|
||||
var NewAppNameFlag = &cli.StringFlag{
|
||||
Name: "app-name, a",
|
||||
Value: "",
|
||||
Usage: "Choose an app name",
|
||||
Destination: &NewAppName,
|
||||
}
|
||||
|
||||
var NoDomainChecks bool
|
||||
var NoDomainChecksFlag = &cli.BoolFlag{
|
||||
Name: "no-domain-checks, nd",
|
||||
Name: "no-domain-checks, D",
|
||||
Usage: "Disable app domain sanity checks",
|
||||
Destination: &NoDomainChecks,
|
||||
}
|
||||
@ -326,9 +328,17 @@ var StdErrOnlyFlag = &cli.BoolFlag{
|
||||
Destination: &StdErrOnly,
|
||||
}
|
||||
|
||||
var SinceLogs string
|
||||
var SinceLogsFlag = &cli.StringFlag{
|
||||
Name: "since, S",
|
||||
Value: "",
|
||||
Usage: "tail logs since YYYY-MM-DDTHH:MM:SSZ",
|
||||
Destination: &SinceLogs,
|
||||
}
|
||||
|
||||
var DontWaitConverge bool
|
||||
var DontWaitConvergeFlag = &cli.BoolFlag{
|
||||
Name: "no-converge-checks, nc",
|
||||
Name: "no-converge-checks, c",
|
||||
Usage: "Don't wait for converge logic checks",
|
||||
Destination: &DontWaitConverge,
|
||||
}
|
||||
@ -354,24 +364,6 @@ var SkipUpdatesFlag = &cli.BoolFlag{
|
||||
Destination: &SkipUpdates,
|
||||
}
|
||||
|
||||
var RegistryUsername string
|
||||
var RegistryUsernameFlag = &cli.StringFlag{
|
||||
Name: "username, user",
|
||||
Value: "",
|
||||
Usage: "Registry username",
|
||||
EnvVar: "REGISTRY_USERNAME",
|
||||
Destination: &RegistryUsername,
|
||||
}
|
||||
|
||||
var RegistryPassword string
|
||||
var RegistryPasswordFlag = &cli.StringFlag{
|
||||
Name: "password, pass",
|
||||
Value: "",
|
||||
Usage: "Registry password",
|
||||
EnvVar: "REGISTRY_PASSWORD",
|
||||
Destination: &RegistryUsername,
|
||||
}
|
||||
|
||||
var AllTags bool
|
||||
var AllTagsFlag = &cli.BoolFlag{
|
||||
Name: "all-tags, a",
|
||||
@ -379,54 +371,20 @@ var AllTagsFlag = &cli.BoolFlag{
|
||||
Destination: &AllTags,
|
||||
}
|
||||
|
||||
// SSHFailMsg is a hopefully helpful SSH failure message
|
||||
var SSHFailMsg = `
|
||||
Woops, Abra is unable to connect to connect to %s.
|
||||
var LocalCmd bool
|
||||
var LocalCmdFlag = &cli.BoolFlag{
|
||||
Name: "local, l",
|
||||
Usage: "Run command locally",
|
||||
Destination: &LocalCmd,
|
||||
}
|
||||
|
||||
Here are a few tips for debugging your local SSH config. Abra uses plain 'ol
|
||||
SSH to make connections to servers, so if your SSH config is working, Abra is
|
||||
working.
|
||||
|
||||
In the first place, Abra will always try to read your Docker context connection
|
||||
string for SSH connection details. You can view your server context configs
|
||||
with the following command. Are they correct?
|
||||
|
||||
abra server ls
|
||||
|
||||
Is your ssh-agent running? You can start it by running the following command:
|
||||
|
||||
eval "$(ssh-agent)"
|
||||
|
||||
If your SSH private key loaded? You can check by running the following command:
|
||||
|
||||
ssh-add -L
|
||||
|
||||
If, you can add it with:
|
||||
|
||||
ssh-add ~/.ssh/<private-key-part>
|
||||
|
||||
If you are using a non-default public/private key, you can configure this in
|
||||
your ~/.ssh/config file which Abra will read in order to figure out connection
|
||||
details:
|
||||
|
||||
Host foo.coopcloud.tech
|
||||
Hostname foo.coopcloud.tech
|
||||
User bar
|
||||
Port 12345
|
||||
IdentityFile ~/.ssh/bar@foo.coopcloud.tech
|
||||
|
||||
If you're only using password authentication, you can use the following config:
|
||||
|
||||
Host foo.coopcloud.tech
|
||||
Hostname foo.coopcloud.tech
|
||||
User bar
|
||||
Port 12345
|
||||
PreferredAuthentications=password
|
||||
PubkeyAuthentication=no
|
||||
|
||||
Good luck!
|
||||
|
||||
`
|
||||
var RemoteUser string
|
||||
var RemoteUserFlag = &cli.StringFlag{
|
||||
Name: "user, u",
|
||||
Value: "",
|
||||
Usage: "User to run command within a service context",
|
||||
Destination: &RemoteUser,
|
||||
}
|
||||
|
||||
// SubCommandBefore wires up pre-action machinery (e.g. --debug handling).
|
||||
func SubCommandBefore(c *cli.Context) error {
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -24,14 +25,20 @@ import (
|
||||
// DeployAction is the main command-line action for this package
|
||||
func DeployAction(c *cli.Context) error {
|
||||
app := ValidateApp(c)
|
||||
conf := runtime.New()
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if !Chaos {
|
||||
if err := recipe.EnsureUpToDate(app.Type); err != nil {
|
||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
r, err := recipe.Get(app.Type)
|
||||
r, err := recipe.Get(app.Recipe, conf)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -40,11 +47,6 @@ func DeployAction(c *cli.Context) error {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("checking whether %s is already deployed", app.StackName())
|
||||
|
||||
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||
@ -66,24 +68,24 @@ func DeployAction(c *cli.Context) error {
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
versions, err := recipe.GetRecipeCatalogueVersions(app.Type, catl)
|
||||
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if len(versions) > 0 {
|
||||
version = versions[len(versions)-1]
|
||||
logrus.Debugf("choosing %s as version to deploy", version)
|
||||
if err := recipe.EnsureVersion(app.Type, version); err != nil {
|
||||
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
head, err := git.GetRecipeHead(app.Type)
|
||||
head, err := git.GetRecipeHead(app.Recipe)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
version = formatter.SmallSHA(head.String())
|
||||
logrus.Warn("no versions detected, using latest commit")
|
||||
if err := recipe.EnsureLatest(app.Type); err != nil {
|
||||
if err := recipe.EnsureLatest(app.Recipe, conf); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -91,13 +93,13 @@ func DeployAction(c *cli.Context) error {
|
||||
|
||||
if version == "unknown" && !Chaos {
|
||||
logrus.Debugf("choosing %s as version to deploy", version)
|
||||
if err := recipe.EnsureVersion(app.Type, version); err != nil {
|
||||
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if version != "unknown" && !Chaos {
|
||||
if err := recipe.EnsureVersion(app.Type, version); err != nil {
|
||||
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -105,13 +107,13 @@ func DeployAction(c *cli.Context) error {
|
||||
if Chaos {
|
||||
logrus.Warnf("chaos mode engaged")
|
||||
var err error
|
||||
version, err = recipe.ChaosVersion(app.Type)
|
||||
version, err = recipe.ChaosVersion(app.Recipe)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Type, "abra.sh")
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
|
||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -120,7 +122,7 @@ func DeployAction(c *cli.Context) error {
|
||||
app.Env[k] = v
|
||||
}
|
||||
|
||||
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
|
||||
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -134,21 +136,24 @@ func DeployAction(c *cli.Context) error {
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
config.ExposeAllEnv(app.StackName(), compose, app.Env)
|
||||
config.SetRecipeLabel(compose, app.StackName(), app.Recipe)
|
||||
config.SetChaosLabel(compose, app.StackName(), Chaos)
|
||||
config.SetUpdateLabel(compose, app.StackName(), app.Env)
|
||||
|
||||
if err := DeployOverview(app, version, "continue with deployment?"); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if !NoDomainChecks {
|
||||
domainName := app.Env["DOMAIN"]
|
||||
ipv4, err := dns.EnsureIPv4(domainName)
|
||||
if err != nil || ipv4 == "" {
|
||||
logrus.Fatalf("could not find an IP address assigned to %s?", domainName)
|
||||
}
|
||||
|
||||
domainName, ok := app.Env["DOMAIN"]
|
||||
if ok {
|
||||
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
logrus.Warn("skipping domain checks as no DOMAIN=... configured for app")
|
||||
}
|
||||
} else {
|
||||
logrus.Warn("skipping domain checks as requested")
|
||||
}
|
||||
@ -162,7 +167,7 @@ func DeployAction(c *cli.Context) error {
|
||||
|
||||
// DeployOverview shows a deployment overview
|
||||
func DeployOverview(app config.App, version, message string) error {
|
||||
tableCol := []string{"server", "compose", "domain", "app name", "version"}
|
||||
tableCol := []string{"server", "recipe", "config", "domain", "version"}
|
||||
table := formatter.CreateTable(tableCol)
|
||||
|
||||
deployConfig := "compose.yml"
|
||||
@ -175,7 +180,7 @@ func DeployOverview(app config.App, version, message string) error {
|
||||
server = "local"
|
||||
}
|
||||
|
||||
table.Append([]string{server, deployConfig, app.Domain, app.Name, version})
|
||||
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
|
||||
table.Render()
|
||||
|
||||
if NoInput {
|
||||
@ -200,7 +205,7 @@ func DeployOverview(app config.App, version, message string) error {
|
||||
|
||||
// NewVersionOverview shows an upgrade or downgrade overview
|
||||
func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
|
||||
tableCol := []string{"server", "compose", "domain", "app name", "current version", "to be deployed"}
|
||||
tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
|
||||
table := formatter.CreateTable(tableCol)
|
||||
|
||||
deployConfig := "compose.yml"
|
||||
@ -213,12 +218,12 @@ func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes
|
||||
server = "local"
|
||||
}
|
||||
|
||||
table.Append([]string{server, deployConfig, app.Domain, app.Name, currentVersion, newVersion})
|
||||
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
|
||||
table.Render()
|
||||
|
||||
if releaseNotes == "" {
|
||||
var err error
|
||||
releaseNotes, err = GetReleaseNotes(app.Type, newVersion)
|
||||
releaseNotes, err = GetReleaseNotes(app.Recipe, newVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -4,13 +4,17 @@ import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/jsontable"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"coopcloud.tech/abra/pkg/secret"
|
||||
"coopcloud.tech/abra/pkg/ssh"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -22,15 +26,15 @@ type AppSecrets map[string]string
|
||||
var RecipeName string
|
||||
|
||||
// createSecrets creates all secrets for a new app.
|
||||
func createSecrets(sanitisedAppName string) (AppSecrets, error) {
|
||||
appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", NewAppName))
|
||||
func createSecrets(cl *dockerClient.Client, sanitisedAppName string) (AppSecrets, error) {
|
||||
appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", Domain))
|
||||
appEnv, err := config.ReadEnv(appEnvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secretEnvVars := secret.ReadSecretEnvVars(appEnv)
|
||||
secrets, err := secret.GenerateSecrets(secretEnvVars, sanitisedAppName, NewAppServer)
|
||||
secrets, err := secret.GenerateSecrets(cl, secretEnvVars, sanitisedAppName, NewAppServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -38,7 +42,7 @@ func createSecrets(sanitisedAppName string) (AppSecrets, error) {
|
||||
if Pass {
|
||||
for secretName := range secrets {
|
||||
secretValue := secrets[secretName]
|
||||
if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, NewAppServer); err != nil {
|
||||
if err := secret.PassInsertSecret(secretValue, secretName, Domain, NewAppServer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@ -65,6 +69,31 @@ func ensureDomainFlag(recipe recipe.Recipe, server string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// promptForSecrets asks if we should generate secrets for a new app.
|
||||
func promptForSecrets(appName string) error {
|
||||
app, err := app.Get(appName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secretEnvVars := secret.ReadSecretEnvVars(app.Env)
|
||||
if len(secretEnvVars) == 0 {
|
||||
logrus.Debugf("%s has no secrets to generate, skipping...", app.Recipe)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !Secrets && !NoInput {
|
||||
prompt := &survey.Confirm{
|
||||
Message: "Generate app secrets?",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &Secrets); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureServerFlag checks if the server flag was used. if not, asks the user for it.
|
||||
func ensureServerFlag() error {
|
||||
servers, err := config.GetServers()
|
||||
@ -89,28 +118,9 @@ func ensureServerFlag() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureServerFlag checks if the AppName flag was used. if not, asks the user for it.
|
||||
func ensureAppNameFlag() error {
|
||||
if NewAppName == "" && !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "Specify app name:",
|
||||
Default: Domain,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &NewAppName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if NewAppName == "" {
|
||||
return fmt.Errorf("no app name provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewAction is the new app creation logic
|
||||
func NewAction(c *cli.Context) error {
|
||||
recipe := ValidateRecipeWithPrompt(c)
|
||||
recipe := ValidateRecipeWithPrompt(c, runtime.WithEnsureRecipeLatest(false))
|
||||
|
||||
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -124,48 +134,45 @@ func NewAction(c *cli.Context) error {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ensureAppNameFlag(); err != nil {
|
||||
sanitisedAppName := config.SanitiseAppName(Domain)
|
||||
logrus.Debugf("%s sanitised as %s for new app", Domain, sanitisedAppName)
|
||||
|
||||
if err := config.TemplateAppEnvSample(recipe.Name, Domain, NewAppServer, Domain); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
sanitisedAppName := config.SanitiseAppName(NewAppName)
|
||||
if len(sanitisedAppName) > 45 {
|
||||
logrus.Fatalf("%s cannot be longer than 45 characters", sanitisedAppName)
|
||||
}
|
||||
logrus.Debugf("%s sanitised as %s for new app", NewAppName, sanitisedAppName)
|
||||
|
||||
if err := config.TemplateAppEnvSample(recipe.Name, NewAppName, NewAppServer, Domain); err != nil {
|
||||
if err := promptForSecrets(Domain); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
cl, err := client.New(NewAppServer)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
var secrets AppSecrets
|
||||
var secretTable *jsontable.JSONTable
|
||||
if Secrets {
|
||||
if err := ssh.EnsureHostKey(NewAppServer); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
secrets, err := createSecrets(sanitisedAppName)
|
||||
secrets, err := createSecrets(cl, sanitisedAppName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
secretCols := []string{"Name", "Value"}
|
||||
secretTable := formatter.CreateTable(secretCols)
|
||||
secretTable = formatter.CreateTable(secretCols)
|
||||
for secret := range secrets {
|
||||
secretTable.Append([]string{secret, secrets[secret]})
|
||||
}
|
||||
|
||||
if len(secrets) > 0 {
|
||||
defer secretTable.Render()
|
||||
}
|
||||
}
|
||||
|
||||
if NewAppServer == "default" {
|
||||
NewAppServer = "local"
|
||||
}
|
||||
|
||||
tableCol := []string{"server", "type", "domain", "app name"}
|
||||
tableCol := []string{"server", "recipe", "domain"}
|
||||
table := formatter.CreateTable(tableCol)
|
||||
table.Append([]string{NewAppServer, recipe.Name, Domain, NewAppName})
|
||||
table.Append([]string{NewAppServer, recipe.Name, Domain})
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name))
|
||||
@ -173,11 +180,19 @@ func NewAction(c *cli.Context) error {
|
||||
table.Render()
|
||||
fmt.Println("")
|
||||
fmt.Println("You can configure this app by running the following:")
|
||||
fmt.Println(fmt.Sprintf("\n abra app config %s", NewAppName))
|
||||
fmt.Println(fmt.Sprintf("\n abra app config %s", Domain))
|
||||
fmt.Println("")
|
||||
fmt.Println("You can deploy this app by running the following:")
|
||||
fmt.Println(fmt.Sprintf("\n abra app deploy %s", NewAppName))
|
||||
fmt.Println(fmt.Sprintf("\n abra app deploy %s", Domain))
|
||||
fmt.Println("")
|
||||
|
||||
if len(secrets) > 0 {
|
||||
fmt.Println("Here are your generated secrets:")
|
||||
fmt.Println("")
|
||||
secretTable.Render()
|
||||
fmt.Println("")
|
||||
logrus.Warn("generated secrets are not shown again, please take note of them *now*")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// PromptBumpType prompts for version bump type
|
||||
func PromptBumpType(tagString string) error {
|
||||
func PromptBumpType(tagString, latestRelease string) error {
|
||||
if (!Major && !Minor && !Patch) && tagString == "" {
|
||||
fmt.Printf(`
|
||||
You need to make a decision about what kind of an update this new recipe
|
||||
@ -20,6 +20,8 @@ migration work or take care of some breaking changes? This can be signaled in
|
||||
the version you specify on the recipe deploy label and is called a semantic
|
||||
version.
|
||||
|
||||
The latest published version is %s.
|
||||
|
||||
Here is a semver cheat sheet (more on https://semver.org):
|
||||
|
||||
major: new features/bug fixes, backwards incompatible (e.g 1.0.0 -> 2.0.0).
|
||||
@ -34,7 +36,7 @@ Here is a semver cheat sheet (more on https://semver.org):
|
||||
should also Just Work and is mostly to do with minor bug fixes
|
||||
and/or security patches. "nothing to worry about".
|
||||
|
||||
`)
|
||||
`, latestRelease)
|
||||
|
||||
var chosenBumpType string
|
||||
prompt := &survey.Select{
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/ssh"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
@ -19,14 +19,15 @@ import (
|
||||
var AppName string
|
||||
|
||||
// ValidateRecipe ensures the recipe arg is valid.
|
||||
func ValidateRecipe(c *cli.Context) recipe.Recipe {
|
||||
func ValidateRecipe(c *cli.Context, opts ...runtime.Option) recipe.Recipe {
|
||||
recipeName := c.Args().First()
|
||||
conf := runtime.New(opts...)
|
||||
|
||||
if recipeName == "" {
|
||||
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
|
||||
}
|
||||
|
||||
chosenRecipe, err := recipe.Get(recipeName)
|
||||
chosenRecipe, err := recipe.Get(recipeName, conf)
|
||||
if err != nil {
|
||||
if c.Command.Name == "generate" {
|
||||
if strings.Contains(err.Error(), "missing a compose") {
|
||||
@ -34,6 +35,15 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
|
||||
}
|
||||
logrus.Warn(err)
|
||||
} else {
|
||||
if strings.Contains(err.Error(), "template_driver is not allowed") {
|
||||
logrus.Warnf("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName)
|
||||
}
|
||||
logrus.Fatalf("unable to validate recipe: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if conf.EnsureRecipeLatest {
|
||||
if err := recipe.EnsureLatest(recipeName, conf); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -45,8 +55,9 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
|
||||
|
||||
// ValidateRecipeWithPrompt ensures a recipe argument is present before
|
||||
// validating, asking for input if required.
|
||||
func ValidateRecipeWithPrompt(c *cli.Context) recipe.Recipe {
|
||||
func ValidateRecipeWithPrompt(c *cli.Context, opts ...runtime.Option) recipe.Recipe {
|
||||
recipeName := c.Args().First()
|
||||
conf := runtime.New(opts...)
|
||||
|
||||
if recipeName == "" && !NoInput {
|
||||
var recipes []string
|
||||
@ -94,19 +105,26 @@ func ValidateRecipeWithPrompt(c *cli.Context) recipe.Recipe {
|
||||
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
|
||||
}
|
||||
|
||||
chosenRecipe, err := recipe.Get(recipeName)
|
||||
chosenRecipe, err := recipe.Get(recipeName, conf)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if conf.EnsureRecipeLatest {
|
||||
if err := recipe.EnsureLatest(recipeName, conf); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("validated %s as recipe argument", recipeName)
|
||||
|
||||
return chosenRecipe
|
||||
}
|
||||
|
||||
// ValidateApp ensures the app name arg is valid.
|
||||
func ValidateApp(c *cli.Context) config.App {
|
||||
func ValidateApp(c *cli.Context, opts ...runtime.Option) config.App {
|
||||
appName := c.Args().First()
|
||||
conf := runtime.New(opts...)
|
||||
|
||||
if AppName != "" {
|
||||
appName = AppName
|
||||
@ -122,11 +140,7 @@ func ValidateApp(c *cli.Context) config.App {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := recipe.EnsureExists(app.Type); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ssh.EnsureHostKey(app.Server); err != nil {
|
||||
if err := recipe.EnsureExists(app.Recipe, conf); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
@ -136,7 +150,7 @@ func ValidateApp(c *cli.Context) config.App {
|
||||
}
|
||||
|
||||
// ValidateDomain ensures the domain name arg is valid.
|
||||
func ValidateDomain(c *cli.Context) (string, error) {
|
||||
func ValidateDomain(c *cli.Context) string {
|
||||
domainName := c.Args().First()
|
||||
|
||||
if domainName == "" && !NoInput {
|
||||
@ -145,7 +159,7 @@ func ValidateDomain(c *cli.Context) (string, error) {
|
||||
Default: "example.com",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &domainName); err != nil {
|
||||
return domainName, err
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,7 +169,7 @@ func ValidateDomain(c *cli.Context) (string, error) {
|
||||
|
||||
logrus.Debugf("validated %s as domain argument", domainName)
|
||||
|
||||
return domainName, nil
|
||||
return domainName
|
||||
}
|
||||
|
||||
// ValidateSubCmdFlags ensures flag order conforms to correct order
|
||||
@ -173,12 +187,12 @@ func ValidateSubCmdFlags(c *cli.Context) bool {
|
||||
}
|
||||
|
||||
// ValidateServer ensures the server name arg is valid.
|
||||
func ValidateServer(c *cli.Context) (string, error) {
|
||||
func ValidateServer(c *cli.Context) string {
|
||||
serverName := c.Args().First()
|
||||
|
||||
serverNames, err := config.ReadServerNames()
|
||||
if err != nil {
|
||||
return serverName, err
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if serverName == "" && !NoInput {
|
||||
@ -187,17 +201,28 @@ func ValidateServer(c *cli.Context) (string, error) {
|
||||
Options: serverNames,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &serverName); err != nil {
|
||||
return serverName, err
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
matched := false
|
||||
for _, name := range serverNames {
|
||||
if name == serverName {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
ShowSubcommandHelpAndError(c, errors.New("server doesn't exist?"))
|
||||
}
|
||||
|
||||
if serverName == "" {
|
||||
ShowSubcommandHelpAndError(c, errors.New("no server provided"))
|
||||
}
|
||||
|
||||
logrus.Debugf("validated %s as server argument", serverName)
|
||||
|
||||
return serverName, nil
|
||||
return serverName
|
||||
}
|
||||
|
||||
// EnsureDNSProvider ensures a DNS provider is chosen.
|
||||
|
40
cli/recipe/fetch.go
Normal file
40
cli/recipe/fetch.go
Normal file
@ -0,0 +1,40 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var recipeFetchCommand = cli.Command{
|
||||
Name: "fetch",
|
||||
Usage: "Fetch recipe local copies",
|
||||
Aliases: []string{"f"},
|
||||
ArgsUsage: "[<recipe>]",
|
||||
Description: "Fetchs all recipes without arguments.",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.RecipeNameComplete,
|
||||
Action: func(c *cli.Context) error {
|
||||
recipeName := c.Args().First()
|
||||
if recipeName != "" {
|
||||
internal.ValidateRecipe(c)
|
||||
return nil // ValidateRecipe ensures latest checkout
|
||||
}
|
||||
|
||||
repos, err := recipe.ReadReposMetadata()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
@ -19,7 +19,6 @@ var recipeLintCommand = cli.Command{
|
||||
ArgsUsage: "<recipe>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
internal.OnlyErrorFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
@ -31,13 +30,30 @@ var recipeLintCommand = cli.Command{
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
tableCol := []string{"ref", "rule", "satisfied", "severity", "resolve"}
|
||||
tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
|
||||
table := formatter.CreateTable(tableCol)
|
||||
|
||||
hasError := false
|
||||
bar := formatter.CreateProgressbar(-1, "running recipe lint rules...")
|
||||
for level := range lint.LintRules {
|
||||
for _, rule := range lint.LintRules[level] {
|
||||
if internal.OnlyErrors && rule.Level != "error" {
|
||||
logrus.Debugf("skipping %s, does not have level \"error\"", rule.Ref)
|
||||
continue
|
||||
}
|
||||
|
||||
skipped := false
|
||||
if rule.Skip(recipe) {
|
||||
skipped = true
|
||||
}
|
||||
|
||||
skippedOutput := "-"
|
||||
if skipped {
|
||||
skippedOutput = "yes"
|
||||
}
|
||||
|
||||
satisfied := false
|
||||
if !skipped {
|
||||
ok, err := rule.Function(recipe)
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
@ -47,22 +63,29 @@ var recipeLintCommand = cli.Command{
|
||||
hasError = true
|
||||
}
|
||||
|
||||
var result string
|
||||
if ok {
|
||||
result = "yes"
|
||||
} else {
|
||||
result = "NO"
|
||||
satisfied = true
|
||||
}
|
||||
}
|
||||
|
||||
if internal.OnlyErrors {
|
||||
if !ok && rule.Level == "error" {
|
||||
table.Append([]string{rule.Ref, rule.Description, result, rule.Level, rule.HowToResolve})
|
||||
bar.Add(1)
|
||||
satisfiedOutput := "yes"
|
||||
if !satisfied {
|
||||
satisfiedOutput = "NO"
|
||||
if skipped {
|
||||
satisfiedOutput = "-"
|
||||
}
|
||||
} else {
|
||||
table.Append([]string{rule.Ref, rule.Description, result, rule.Level, rule.HowToResolve})
|
||||
bar.Add(1)
|
||||
}
|
||||
|
||||
table.Append([]string{
|
||||
rule.Ref,
|
||||
rule.Description,
|
||||
rule.Level,
|
||||
satisfiedOutput,
|
||||
skippedOutput,
|
||||
rule.HowToResolve,
|
||||
})
|
||||
|
||||
bar.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ var recipeListCommand = cli.Command{
|
||||
Aliases: []string{"ls"},
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
internal.MachineReadableFlag,
|
||||
patternFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
@ -67,11 +67,15 @@ var recipeListCommand = cli.Command{
|
||||
}
|
||||
}
|
||||
|
||||
table.SetCaption(true, fmt.Sprintf("total recipes: %v", len))
|
||||
|
||||
if table.NumLines() > 0 {
|
||||
if internal.MachineReadable {
|
||||
table.SetCaption(false, "")
|
||||
table.JSONRender()
|
||||
} else {
|
||||
table.SetCaption(true, fmt.Sprintf("total recipes: %v", len))
|
||||
table.Render()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
|
@ -41,9 +41,9 @@ var recipeNewCommand = cli.Command{
|
||||
Usage: "Create a new recipe",
|
||||
ArgsUsage: "<recipe>",
|
||||
Description: `
|
||||
This command creates a new recipe.
|
||||
Create a new recipe.
|
||||
|
||||
Abra uses our built-in example repository which is available here:
|
||||
Abra uses the built-in example repository which is available here:
|
||||
|
||||
https://git.coopcloud.tech/coop-cloud/example
|
||||
|
||||
@ -111,7 +111,7 @@ In order to share your recipe, you can upload it the git repository to:
|
||||
|
||||
If you're not sure how to do that, come chat with us:
|
||||
|
||||
https://docs.coopcloud.tech/contact
|
||||
https://docs.coopcloud.tech/intro/contact
|
||||
|
||||
See "abra recipe -h" for additional recipe maintainer commands.
|
||||
|
||||
|
@ -13,7 +13,8 @@ var RecipeCommand = cli.Command{
|
||||
Description: `
|
||||
A recipe is a blueprint for an app. It is a bunch of config files which
|
||||
describe how to deploy and maintain an app. Recipes are maintained by the Co-op
|
||||
Cloud community and you can use Abra to read them and create apps for you.
|
||||
Cloud community and you can use Abra to read them, deploy them and create apps
|
||||
for you.
|
||||
|
||||
Anyone who uses a recipe can become a maintainer. Maintainers typically make
|
||||
sure the recipe is in good working order and the config upgraded in a timely
|
||||
@ -21,12 +22,13 @@ manner. Abra supports convenient automation for recipe maintainenace, see the
|
||||
"abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands.
|
||||
`,
|
||||
Subcommands: []cli.Command{
|
||||
recipeListCommand,
|
||||
recipeVersionCommand,
|
||||
recipeReleaseCommand,
|
||||
recipeNewCommand,
|
||||
recipeUpgradeCommand,
|
||||
recipeSyncCommand,
|
||||
recipeFetchCommand,
|
||||
recipeLintCommand,
|
||||
recipeListCommand,
|
||||
recipeNewCommand,
|
||||
recipeReleaseCommand,
|
||||
recipeSyncCommand,
|
||||
recipeUpgradeCommand,
|
||||
recipeVersionCommand,
|
||||
},
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/docker/distribution/reference"
|
||||
@ -27,17 +28,16 @@ var recipeReleaseCommand = cli.Command{
|
||||
Usage: "Release a new recipe version",
|
||||
ArgsUsage: "<recipe> [<version>]",
|
||||
Description: `
|
||||
This command is used to specify a new version of a recipe. These versions are
|
||||
then published on the Co-op Cloud recipe catalogue. These versions take the
|
||||
following form:
|
||||
Create a new version of a recipe. These versions are then published on the
|
||||
Co-op Cloud recipe catalogue. These versions take the following form:
|
||||
|
||||
a.b.c+x.y.z
|
||||
|
||||
Where the "a.b.c" part is a semantic version determined by the maintainer. And
|
||||
the "x.y.z" part is the image tag of the recipe "app" service (the main
|
||||
container which contains the software to be used).
|
||||
Where the "a.b.c" part is a semantic version determined by the maintainer. The
|
||||
"x.y.z" part is the image tag of the recipe "app" service (the main container
|
||||
which contains the software to be used, by naming convention).
|
||||
|
||||
We maintain a semantic versioning scheme ("a.b.c") alongside the libre app
|
||||
We maintain a semantic versioning scheme ("a.b.c") alongside the recipe
|
||||
versioning scheme ("x.y.z") in order to maximise the chances that the nature of
|
||||
recipe updates are properly communicated. I.e. developers of an app might
|
||||
publish a minor version but that might lead to changes in the recipe which are
|
||||
@ -59,7 +59,7 @@ your SSH keys configured on your account.
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.RecipeNameComplete,
|
||||
Action: func(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipeWithPrompt(c)
|
||||
recipe := internal.ValidateRecipeWithPrompt(c, runtime.WithEnsureRecipeLatest(false))
|
||||
|
||||
imagesTmp, err := getImageVersions(recipe)
|
||||
if err != nil {
|
||||
@ -322,12 +322,6 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
|
||||
}
|
||||
|
||||
var lastGitTag tagcmp.Tag
|
||||
if tagString == "" {
|
||||
if err := internal.PromptBumpType(tagString); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
parsed, err := tagcmp.Parse(tag)
|
||||
if err != nil {
|
||||
@ -368,13 +362,19 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
|
||||
newTag.Major = strconv.Itoa(now + 1)
|
||||
}
|
||||
|
||||
if tagString == "" {
|
||||
if err := internal.PromptBumpType(tagString, lastGitTag.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if internal.Major || internal.Minor || internal.Patch {
|
||||
newTag.Metadata = mainAppVersion
|
||||
tagString = newTag.String()
|
||||
}
|
||||
|
||||
if lastGitTag.String() == tagString {
|
||||
logrus.Fatalf("latest git tag (%s) and synced lable (%s) are the same?", lastGitTag, tagString)
|
||||
logrus.Fatalf("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString)
|
||||
}
|
||||
|
||||
if !internal.NoInput {
|
||||
@ -393,15 +393,15 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
|
||||
}
|
||||
|
||||
if err := commitRelease(recipe, tagString); err != nil {
|
||||
logrus.Fatal(err)
|
||||
logrus.Fatalf("failed to commit changes: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := tagRelease(tagString, repo); err != nil {
|
||||
logrus.Fatal(err)
|
||||
logrus.Fatalf("failed to tag release: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := pushRelease(recipe, tagString); err != nil {
|
||||
logrus.Fatal(err)
|
||||
logrus.Fatalf("failed to publish new release: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/go-git/go-git/v5"
|
||||
@ -31,8 +32,8 @@ var recipeSyncCommand = cli.Command{
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `
|
||||
This command will generate labels for the main recipe service (i.e. by
|
||||
convention, the service named 'app') which corresponds to the following format:
|
||||
Generate labels for the main recipe service (i.e. by convention, the service
|
||||
named "app") which corresponds to the following format:
|
||||
|
||||
coop-cloud.${STACK_NAME}.version=<version>
|
||||
|
||||
@ -41,7 +42,7 @@ auto-generate it for you. The <recipe> configuration will be updated on the
|
||||
local file system.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipeWithPrompt(c)
|
||||
recipe := internal.ValidateRecipeWithPrompt(c, runtime.WithEnsureRecipeLatest(false))
|
||||
|
||||
mainApp, err := internal.GetMainAppImage(recipe)
|
||||
if err != nil {
|
||||
@ -95,7 +96,8 @@ likely to change.
|
||||
}
|
||||
|
||||
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
|
||||
if err := internal.PromptBumpType(""); err != nil {
|
||||
latestRelease := tags[len(tags)-1]
|
||||
if err := internal.PromptBumpType("", latestRelease); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
@ -31,9 +31,9 @@ var recipeUpgradeCommand = cli.Command{
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Upgrade recipe image tags",
|
||||
Description: `
|
||||
This command reads and attempts to parse all image tags within the given
|
||||
<recipe> configuration and prompt with more recent tags to upgrade to. It will
|
||||
update the relevant compose file tags on the local file system.
|
||||
Parse all image tags within the given <recipe> configuration and prompt with
|
||||
more recent tags to upgrade to. It will update the relevant compose file tags
|
||||
on the local file system.
|
||||
|
||||
Some image tags cannot be parsed because they do not follow some sort of
|
||||
semver-like convention. In this case, all possible tags will be listed and it
|
||||
@ -46,7 +46,6 @@ interface.
|
||||
You may invoke this command in "wizard" mode and be prompted for input:
|
||||
|
||||
abra recipe upgrade
|
||||
|
||||
`,
|
||||
BashComplete: autocomplete.RecipeNameComplete,
|
||||
ArgsUsage: "<recipe>",
|
||||
@ -62,6 +61,10 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
||||
Action: func(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipeWithPrompt(c)
|
||||
|
||||
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
||||
if bumpType != 0 {
|
||||
// a bitwise check if the number is a power of 2
|
||||
@ -113,13 +116,13 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
image := reference.Path(img)
|
||||
regVersions, err := client.GetRegistryTags(image)
|
||||
regVersions, err := client.GetRegistryTags(img)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
|
||||
|
||||
image := reference.Path(img)
|
||||
logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
|
||||
image = formatter.StripTagMeta(image)
|
||||
|
||||
switch img.(type) {
|
||||
@ -142,7 +145,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
||||
|
||||
var compatible []tagcmp.Tag
|
||||
for _, regVersion := range regVersions {
|
||||
other, err := tagcmp.Parse(regVersion.Name)
|
||||
other, err := tagcmp.Parse(regVersion)
|
||||
if err != nil {
|
||||
continue // skip tags that cannot be parsed
|
||||
}
|
||||
@ -232,7 +235,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
||||
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
|
||||
compatibleStrings = []string{"skip"}
|
||||
for _, regVersion := range regVersions {
|
||||
compatibleStrings = append(compatibleStrings, regVersion.Name)
|
||||
compatibleStrings = append(compatibleStrings, regVersion)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -16,12 +17,11 @@ var recipeVersionCommand = cli.Command{
|
||||
ArgsUsage: "<recipe>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.RecipeNameComplete,
|
||||
Action: func(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipe(c)
|
||||
recipe := internal.ValidateRecipe(c, runtime.WithEnsureRecipeLatest(false))
|
||||
|
||||
catalogue, err := recipePkg.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
@ -33,13 +33,13 @@ var recipeVersionCommand = cli.Command{
|
||||
logrus.Fatalf("%s recipe doesn't exist?", recipe.Name)
|
||||
}
|
||||
|
||||
tableCol := []string{"Version", "Service", "Image", "Tag", "Digest"}
|
||||
tableCol := []string{"Version", "Service", "Image", "Tag"}
|
||||
table := formatter.CreateTable(tableCol)
|
||||
|
||||
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
|
||||
for tag, meta := range recipeMeta.Versions[i] {
|
||||
for service, serviceMeta := range meta {
|
||||
table.Append([]string{tag, service, serviceMeta.Image, serviceMeta.Tag, serviceMeta.Digest})
|
||||
table.Append([]string{tag, service, serviceMeta.Image, serviceMeta.Tag})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,13 +21,12 @@ var RecordListCommand = cli.Command{
|
||||
ArgsUsage: "<zone>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
internal.DNSProviderFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `
|
||||
This command lists all domain name records managed by a 3rd party provider for
|
||||
a specific zone.
|
||||
List all domain name records managed by a 3rd party provider for a specific
|
||||
zone.
|
||||
|
||||
You must specify a zone (e.g. example.com) under which your domain name records
|
||||
are listed. This zone must already be created on your provider account.
|
||||
|
@ -33,7 +33,7 @@ var RecordNewCommand = cli.Command{
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `
|
||||
This command creates a new domain name record for a specific zone.
|
||||
Create a new domain name record for a specific zone.
|
||||
|
||||
You must specify a zone (e.g. example.com) under which your domain name records
|
||||
are listed. This zone must already be created on your provider account.
|
||||
@ -45,7 +45,6 @@ Example:
|
||||
You may also invoke this command in "wizard" mode and be prompted for input:
|
||||
|
||||
abra record new
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
zone, err := internal.EnsureZoneArgument(c)
|
||||
|
@ -11,9 +11,9 @@ var RecordCommand = cli.Command{
|
||||
Aliases: []string{"rc"},
|
||||
ArgsUsage: "<record>",
|
||||
Description: `
|
||||
This command supports managing domain name records via 3rd party providers such
|
||||
as Gandi DNS. It supports listing, creating and removing all types of records
|
||||
that you might need for managing Co-op Cloud apps.
|
||||
Manage domain name records via 3rd party providers such as Gandi DNS. It
|
||||
supports listing, creating and removing all types of records that you might
|
||||
need for managing Co-op Cloud apps.
|
||||
|
||||
The following providers are supported:
|
||||
|
||||
@ -28,7 +28,6 @@ library documentation for more. It supports many existing providers and allows
|
||||
to implement new provider support easily.
|
||||
|
||||
https://pkg.go.dev/github.com/libdns/libdns
|
||||
|
||||
`,
|
||||
Subcommands: []cli.Command{
|
||||
RecordListCommand,
|
||||
|
@ -30,7 +30,7 @@ var RecordRemoveCommand = cli.Command{
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `
|
||||
This command removes a domain name record for a specific zone.
|
||||
Remove a domain name record for a specific zone.
|
||||
|
||||
It uses the type of record and name to match existing records and choose one
|
||||
for deletion. You must specify a zone (e.g. example.com) under which your
|
||||
|
@ -1,14 +1,9 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
@ -16,35 +11,11 @@ import (
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
"coopcloud.tech/abra/pkg/dns"
|
||||
"coopcloud.tech/abra/pkg/server"
|
||||
"coopcloud.tech/abra/pkg/ssh"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
sshPkg "coopcloud.tech/abra/pkg/ssh"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
dockerInstallMsg = `
|
||||
A docker installation cannot be found on %s. This is a required system
|
||||
dependency for running Co-op Cloud on your server. If you would like, Abra can
|
||||
attempt to install Docker for you using the upstream non-interactive
|
||||
installation script.
|
||||
|
||||
See the following documentation for more:
|
||||
|
||||
https://docs.docker.com/engine/install/debian/#install-using-the-convenience-script
|
||||
|
||||
N.B Docker doesn't recommend it for production environments but many use it for
|
||||
such purposes. Docker stable is now installed by default by this script. The
|
||||
source for this script can be seen here:
|
||||
|
||||
https://github.com/docker/docker-install
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
var local bool
|
||||
var localFlag = &cli.BoolFlag{
|
||||
Name: "local, l",
|
||||
@ -52,106 +23,36 @@ var localFlag = &cli.BoolFlag{
|
||||
Destination: &local,
|
||||
}
|
||||
|
||||
var provision bool
|
||||
var provisionFlag = &cli.BoolFlag{
|
||||
Name: "provision, p",
|
||||
Usage: "Provision server so it can deploy apps",
|
||||
Destination: &provision,
|
||||
}
|
||||
|
||||
var sshAuth string
|
||||
var sshAuthFlag = &cli.StringFlag{
|
||||
Name: "ssh-auth, sh",
|
||||
Value: "identity-file",
|
||||
Usage: "Select SSH authentication method (identity-file, password)",
|
||||
Destination: &sshAuth,
|
||||
}
|
||||
|
||||
var askSudoPass bool
|
||||
var askSudoPassFlag = &cli.BoolFlag{
|
||||
Name: "ask-sudo-pass, as",
|
||||
Usage: "Ask for sudo password",
|
||||
Destination: &askSudoPass,
|
||||
}
|
||||
|
||||
func cleanUp(domainName string) {
|
||||
logrus.Warnf("cleaning up context for %s", domainName)
|
||||
if domainName != "default" {
|
||||
logrus.Infof("cleaning up context for %s", domainName)
|
||||
if err := client.DeleteContext(domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Warnf("cleaning up server directory for %s", domainName)
|
||||
if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, domainName)); err != nil {
|
||||
logrus.Infof("attempting to clean up server directory for %s", domainName)
|
||||
|
||||
serverDir := filepath.Join(config.SERVERS_DIR, domainName)
|
||||
files, err := config.GetAllFilesInDirectory(serverDir)
|
||||
if err != nil {
|
||||
logrus.Fatalf("unable to list files in %s: %s", serverDir, err)
|
||||
}
|
||||
|
||||
if len(files) > 0 {
|
||||
logrus.Warnf("aborting clean up of %s because it is not empty", serverDir)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(serverDir); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func installDockerLocal(c *cli.Context) error {
|
||||
fmt.Println(fmt.Sprintf(dockerInstallMsg, "this local server"))
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("attempt install docker on local server?"),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
for _, exe := range []string{"wget", "bash"} {
|
||||
exists, err := ensureLocalExecutable(exe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("%s missing, please install it", exe)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", "wget -O- https://get.docker.com | bash")
|
||||
if err := internal.RunCmd(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newLocalServer(c *cli.Context, domainName string) error {
|
||||
if err := createServerDir(domainName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cl, err := newClient(c, domainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if provision {
|
||||
exists, err := ensureLocalExecutable("docker")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if err := installDockerLocal(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := initSwarmLocal(c, cl, domainName); err != nil {
|
||||
if !strings.Contains(err.Error(), "proxy already exists") {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Info("local server has been added")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newContext creates a new internal Docker context for a server. This is how
|
||||
// Docker manages SSH connection details. These are stored to disk in
|
||||
// ~/.docker. Abra can manage this completely for the user, so it's an
|
||||
// implementation detail.
|
||||
func newContext(c *cli.Context, domainName, username, port string) error {
|
||||
store := contextPkg.NewDefaultDockerContextStore()
|
||||
contexts, err := store.Store.List()
|
||||
@ -175,187 +76,7 @@ func newContext(c *cli.Context, domainName, username, port string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newClient(c *cli.Context, domainName string) (*dockerClient.Client, error) {
|
||||
cl, err := client.New(domainName)
|
||||
if err != nil {
|
||||
return &dockerClient.Client{}, err
|
||||
}
|
||||
return cl, nil
|
||||
}
|
||||
|
||||
func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *ssh.Client, domainName string) error {
|
||||
exists, err := ensureRemoteExecutable("docker", sshCl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
fmt.Println(fmt.Sprintf(dockerInstallMsg, domainName))
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("attempt install docker on %s?", domainName),
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
exes := []string{"wget", "bash"}
|
||||
if askSudoPass {
|
||||
exes = append(exes, "ssh-askpass")
|
||||
}
|
||||
|
||||
for _, exe := range exes {
|
||||
exists, err := ensureRemoteExecutable(exe, sshCl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("%s missing on remote, please install it", exe)
|
||||
}
|
||||
}
|
||||
|
||||
var sudoPass string
|
||||
if askSudoPass {
|
||||
cmd := "wget -O- https://get.docker.com | bash"
|
||||
|
||||
prompt := &survey.Password{
|
||||
Message: "sudo password?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &sudoPass); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("running %s on %s now with sudo password", cmd, domainName)
|
||||
|
||||
if sudoPass == "" {
|
||||
return fmt.Errorf("missing sudo password but requested --ask-sudo-pass?")
|
||||
}
|
||||
|
||||
logrus.Warn("installing docker, this could take some time...")
|
||||
|
||||
if err := ssh.RunSudoCmd(cmd, sudoPass, sshCl); err != nil {
|
||||
fmt.Print(fmt.Sprintf(`
|
||||
Abra was unable to bootstrap Docker, see below for logs:
|
||||
|
||||
|
||||
%s
|
||||
|
||||
If nothing works, you try running the Docker install script manually on your server:
|
||||
|
||||
wget -O- https://get.docker.com | bash
|
||||
|
||||
`, string(err.Error())))
|
||||
logrus.Fatal("Process exited with status 1")
|
||||
}
|
||||
|
||||
logrus.Infof("docker is installed on %s", domainName)
|
||||
|
||||
remoteUser := sshCl.SSHClient.Conn.User()
|
||||
logrus.Infof("adding %s to docker group", remoteUser)
|
||||
permsCmd := fmt.Sprintf("sudo usermod -aG docker %s", remoteUser)
|
||||
if err := ssh.RunSudoCmd(permsCmd, sudoPass, sshCl); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
cmd := "wget -O- https://get.docker.com | bash"
|
||||
|
||||
logrus.Debugf("running %s on %s now without sudo password", cmd, domainName)
|
||||
|
||||
logrus.Warn("installing docker, this could take some time...")
|
||||
|
||||
if out, err := sshCl.Exec(cmd); err != nil {
|
||||
fmt.Print(fmt.Sprintf(`
|
||||
Abra was unable to bootstrap Docker, see below for logs:
|
||||
|
||||
|
||||
%s
|
||||
|
||||
This could be due to a number of things but one of the most common is that your
|
||||
server user account does not have sudo access, and if it does, you need to pass
|
||||
"--ask-sudo-pass" in order to supply Abra with your password.
|
||||
|
||||
If nothing works, you try running the Docker install script manually on your server:
|
||||
|
||||
wget -O- https://get.docker.com | bash
|
||||
|
||||
`, string(out)))
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Infof("docker is installed on %s", domainName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string) error {
|
||||
initReq := swarm.InitRequest{ListenAddr: "0.0.0.0:2377"}
|
||||
if _, err := cl.SwarmInit(context.Background(), initReq); err != nil {
|
||||
if strings.Contains(err.Error(), "is already part of a swarm") ||
|
||||
strings.Contains(err.Error(), "must specify a listening address") {
|
||||
logrus.Infof("swarm mode already initialised on %s", domainName)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
logrus.Infof("initialised swarm mode on local server")
|
||||
}
|
||||
|
||||
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
|
||||
if _, err := cl.NetworkCreate(context.Background(), "proxy", netOpts); err != nil {
|
||||
if !strings.Contains(err.Error(), "proxy already exists") {
|
||||
return err
|
||||
}
|
||||
logrus.Info("swarm overlay network already created on local server")
|
||||
} else {
|
||||
logrus.Infof("swarm overlay network created on local server")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error {
|
||||
ipv4, err := dns.EnsureIPv4(domainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
initReq := swarm.InitRequest{
|
||||
ListenAddr: "0.0.0.0:2377",
|
||||
AdvertiseAddr: ipv4,
|
||||
}
|
||||
if _, err := cl.SwarmInit(context.Background(), initReq); err != nil {
|
||||
if strings.Contains(err.Error(), "is already part of a swarm") ||
|
||||
strings.Contains(err.Error(), "must specify a listening address") {
|
||||
logrus.Infof("swarm mode already initialised on %s", domainName)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
logrus.Infof("initialised swarm mode on %s", domainName)
|
||||
}
|
||||
|
||||
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
|
||||
if _, err := cl.NetworkCreate(context.Background(), "proxy", netOpts); err != nil {
|
||||
if !strings.Contains(err.Error(), "proxy already exists") {
|
||||
return err
|
||||
}
|
||||
logrus.Infof("swarm overlay network already created on %s", domainName)
|
||||
} else {
|
||||
logrus.Infof("swarm overlay network created on %s", domainName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createServerDir creates the ~/.abra/servers/... directory for a new server.
|
||||
func createServerDir(domainName string) error {
|
||||
if err := server.CreateServerDir(domainName); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
@ -363,6 +84,7 @@ func createServerDir(domainName string) error {
|
||||
}
|
||||
logrus.Debugf("server dir for %s already created", domainName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -371,153 +93,89 @@ var serverAddCommand = cli.Command{
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Add a server to your configuration",
|
||||
Description: `
|
||||
This command adds a new server to your configuration so that it can be managed
|
||||
by Abra. This can be useful when you already have a server provisioned and want
|
||||
to start running Abra commands against it.
|
||||
Add a new server to your configuration so that it can be managed by Abra.
|
||||
|
||||
This command can also provision your server ("--provision/-p") so that it is
|
||||
capable of hosting Co-op Cloud apps. Abra will default to expecting that you
|
||||
have a running ssh-agent and are using SSH keys to connect to your new server.
|
||||
Abra will also read your SSH config (matching "Host" as <domain>). SSH
|
||||
connection details precedence follows as such: command-line > SSH config >
|
||||
guessed defaults.
|
||||
Abra uses the SSH command-line to discover connection details for your server.
|
||||
It is advised to configure an entry per-host in your ~/.ssh/config for each
|
||||
server. For example:
|
||||
|
||||
If you have no SSH key configured for this host and are instead using password
|
||||
authentication, you may pass "--ssh-auth password" to have Abra ask you for the
|
||||
password. "--ask-sudo-pass" may be passed if you run your provisioning commands
|
||||
via sudo privilege escalation.
|
||||
Host example.com
|
||||
Hostname example.com
|
||||
User exampleUser
|
||||
Port 12345
|
||||
IdentityFile ~/.ssh/example@somewhere
|
||||
|
||||
Abra can then load SSH connection details from this configuratiion with:
|
||||
|
||||
abra server add example.com
|
||||
|
||||
If "--local" is passed, then Abra assumes that the current local server is
|
||||
intended as the target server. This is useful when you want to have your entire
|
||||
Co-op Cloud config located on the server itself, and not on your local
|
||||
developer machine.
|
||||
|
||||
Example:
|
||||
|
||||
abra server add --local
|
||||
|
||||
Otherwise, you may specify a remote server. The <domain> argument must be a
|
||||
publicy accessible domain name which points to your server. You should have SSH
|
||||
access to this server, Abra will assume port 22 and will use your current
|
||||
system username to make an initial connection. You can use the <user> and
|
||||
<port> arguments to adjust this.
|
||||
|
||||
Example:
|
||||
|
||||
abra server add --provision varia.zone glodemodem 12345
|
||||
|
||||
Abra will construct the following SSH connection and Docker context:
|
||||
|
||||
ssh://globemodem@varia.zone:12345
|
||||
|
||||
All communication between Abra and the server will use this SSH connection.
|
||||
|
||||
In this example, Abra will install Docker and initialise swarm mode.
|
||||
|
||||
You may omit flags to avoid performing this provisioning logic.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
localFlag,
|
||||
provisionFlag,
|
||||
sshAuthFlag,
|
||||
askSudoPassFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
ArgsUsage: "<domain> [<user>] [<port>]",
|
||||
ArgsUsage: "<domain>",
|
||||
Action: func(c *cli.Context) error {
|
||||
if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) {
|
||||
err := errors.New("cannot use <domain> and --local together")
|
||||
internal.ShowSubcommandHelpAndError(c, err)
|
||||
}
|
||||
|
||||
if sshAuth != "password" && sshAuth != "identity-file" {
|
||||
err := errors.New("--ssh-auth only accepts identity-file or password")
|
||||
internal.ShowSubcommandHelpAndError(c, err)
|
||||
var domainName string
|
||||
if local {
|
||||
domainName = "default"
|
||||
} else {
|
||||
domainName = internal.ValidateDomain(c)
|
||||
}
|
||||
|
||||
if local {
|
||||
if err := newLocalServer(c, "default"); err != nil {
|
||||
if err := createServerDir(domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Infof("attempting to create client for %s", domainName)
|
||||
if _, err := client.New(domainName); err != nil {
|
||||
cleanUp(domainName)
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Info("local server added")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
domainName, err := internal.ValidateDomain(c)
|
||||
if err != nil {
|
||||
if _, err := dns.EnsureIPv4(domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
username := c.Args().Get(1)
|
||||
if username == "" {
|
||||
systemUser, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
username = systemUser.Username
|
||||
}
|
||||
|
||||
port := c.Args().Get(2)
|
||||
if port == "" {
|
||||
port = "22"
|
||||
}
|
||||
|
||||
if err := createServerDir(domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := newContext(c, domainName, username, port); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
cl, err := newClient(c, domainName)
|
||||
hostConfig, err := sshPkg.GetHostConfig(domainName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if provision {
|
||||
logrus.Debugf("attempting to construct SSH client for %s", domainName)
|
||||
sshCl, err := ssh.New(domainName, sshAuth, username, port)
|
||||
if err != nil {
|
||||
if err := newContext(c, domainName, hostConfig.User, hostConfig.Port); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
defer sshCl.Close()
|
||||
logrus.Debugf("successfully created SSH client for %s", domainName)
|
||||
|
||||
if err := installDocker(c, cl, sshCl, domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := initSwarm(c, cl, domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := cl.Info(context.Background()); err != nil {
|
||||
logrus.Infof("attempting to create client for %s", domainName)
|
||||
if _, err := client.New(domainName); err != nil {
|
||||
cleanUp(domainName)
|
||||
logrus.Fatalf("couldn't make a remote docker connection to %s? use --provision/-p to attempt to install", domainName)
|
||||
logrus.Debugf("failed to construct client for %s, saw %s", domainName, err.Error())
|
||||
logrus.Fatal(sshPkg.Fatal(domainName, err))
|
||||
}
|
||||
|
||||
logrus.Infof("%s added", domainName)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// ensureLocalExecutable ensures that an executable is present on the local machine
|
||||
func ensureLocalExecutable(exe string) (bool, error) {
|
||||
out, err := exec.Command("which", exe).Output()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return string(out) != "", nil
|
||||
}
|
||||
|
||||
// ensureRemoteExecutable ensures that an executable is present on a remote machine
|
||||
func ensureRemoteExecutable(exe string, sshCl *ssh.Client) (bool, error) {
|
||||
out, err := sshCl.Exec(fmt.Sprintf("which %s", exe))
|
||||
if err != nil && string(out) != "" {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return string(out) != "", nil
|
||||
}
|
||||
|
@ -12,13 +12,22 @@ import (
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var problemsFilter bool
|
||||
|
||||
var problemsFilterFlag = &cli.BoolFlag{
|
||||
Name: "problems, p",
|
||||
Usage: "Show only servers with potential connection problems",
|
||||
Destination: &problemsFilter,
|
||||
}
|
||||
|
||||
var serverListCommand = cli.Command{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "List managed servers",
|
||||
Flags: []cli.Flag{
|
||||
problemsFilterFlag,
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
internal.MachineReadableFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Action: func(c *cli.Context) error {
|
||||
@ -30,8 +39,11 @@ var serverListCommand = cli.Command{
|
||||
|
||||
tableColumns := []string{"name", "host", "user", "port"}
|
||||
table := formatter.CreateTable(tableColumns)
|
||||
if internal.MachineReadable {
|
||||
defer table.JSONRender()
|
||||
} else {
|
||||
defer table.Render()
|
||||
|
||||
}
|
||||
serverNames, err := config.ReadServerNames()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -45,6 +57,7 @@ var serverListCommand = cli.Command{
|
||||
// No local context found, we can continue safely
|
||||
continue
|
||||
}
|
||||
|
||||
if ctx.Name == serverName {
|
||||
sp, err := ssh.ParseURL(endpoint)
|
||||
if err != nil {
|
||||
@ -53,6 +66,7 @@ var serverListCommand = cli.Command{
|
||||
row = []string{serverName, sp.Host, sp.User, sp.Port}
|
||||
}
|
||||
}
|
||||
|
||||
if len(row) == 0 {
|
||||
if serverName == "default" {
|
||||
row = []string{serverName, "local", "n/a", "n/a"}
|
||||
@ -60,8 +74,15 @@ var serverListCommand = cli.Command{
|
||||
row = []string{serverName, "unknown", "unknown", "unknown"}
|
||||
}
|
||||
}
|
||||
|
||||
if problemsFilter {
|
||||
if row[1] == "unknown" {
|
||||
table.Append(row)
|
||||
}
|
||||
} else {
|
||||
table.Append(row)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
|
@ -99,9 +99,10 @@ You can access this new VPS via SSH using the following command:
|
||||
ssh root@%s
|
||||
|
||||
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
|
||||
not list this server)! You will need to assign a domain name record ("abra
|
||||
record new") and add the server to your Abra configuration ("abra server add")
|
||||
to have a working server that you can deploy Co-op Cloud apps to.
|
||||
not list this server)! You will need to assign a domain name record (manually
|
||||
or by using "abra record new") and add the server to your Abra configuration
|
||||
("abra server add") to have a working server that you can deploy Co-op Cloud
|
||||
apps to.
|
||||
|
||||
When setting up domain name records, you probably want to set up the following
|
||||
2 A records. This supports deploying apps to your root domain (e.g.
|
||||
@ -110,7 +111,6 @@ bar.example.com).
|
||||
|
||||
@ 1800 IN A %s
|
||||
* 1800 IN A %s
|
||||
|
||||
`,
|
||||
internal.HetznerCloudName, ip, rootPassword,
|
||||
ip, ip, ip,
|
||||
@ -181,9 +181,10 @@ address. You can learn all about how to get SSH access to your new Capsul on:
|
||||
%s/about-ssh
|
||||
|
||||
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
|
||||
not list this server)! You will need to assign a domain name record ("abra
|
||||
record new") and add the server to your Abra configuration ("abra server add")
|
||||
to have a working server that you can deploy Co-op Cloud apps to.
|
||||
not list this server)! You will need to assign a domain name record (manually
|
||||
or by using "abra record new") and add the server to your Abra configuration
|
||||
("abra server add") to have a working server that you can deploy Co-op Cloud
|
||||
apps to.
|
||||
|
||||
When setting up domain name records, you probably want to set up the following
|
||||
2 A records. This supports deploying apps to your root domain (e.g.
|
||||
@ -192,7 +193,6 @@ bar.example.com).
|
||||
|
||||
@ 1800 IN A <your-capsul-ip>
|
||||
* 1800 IN A <your-capsul-ip>
|
||||
|
||||
`, internal.CapsulName, resp.ID, internal.CapsulInstanceURL))
|
||||
|
||||
return nil
|
||||
@ -203,7 +203,7 @@ var serverNewCommand = cli.Command{
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Create a new server using a 3rd party provider",
|
||||
Description: `
|
||||
This command creates a new server via a 3rd party provider.
|
||||
Create a new server via a 3rd party provider.
|
||||
|
||||
The following providers are supported:
|
||||
|
||||
@ -217,16 +217,11 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
||||
API tokens are read from the environment if specified, e.g.
|
||||
|
||||
export HCLOUD_TOKEN=...
|
||||
|
||||
Where "$provider_TOKEN" is the expected env var format.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
|
||||
internal.ServerProviderFlag,
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
|
||||
// Capsul
|
||||
internal.CapsulInstanceURLFlag,
|
||||
|
83
cli/server/prune.go
Normal file
83
cli/server/prune.go
Normal file
@ -0,0 +1,83 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var allFilter bool
|
||||
|
||||
var allFilterFlag = &cli.BoolFlag{
|
||||
Name: "all, a",
|
||||
Usage: "Remove all unused images not just dangling ones",
|
||||
Destination: &allFilter,
|
||||
}
|
||||
|
||||
var volunesFilter bool
|
||||
|
||||
var volumesFilterFlag = &cli.BoolFlag{
|
||||
Name: "volumes, v",
|
||||
Usage: "Prune volumes",
|
||||
Destination: &volunesFilter,
|
||||
}
|
||||
|
||||
var serverPruneCommand = cli.Command{
|
||||
Name: "prune",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "Prune a managed server; Runs a docker system prune",
|
||||
Description: "Prunes unused containers, networks, and dangling images",
|
||||
ArgsUsage: "[<server>]",
|
||||
Flags: []cli.Flag{
|
||||
allFilterFlag,
|
||||
volumesFilterFlag,
|
||||
internal.DebugFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Action: func(c *cli.Context) error {
|
||||
// Leaving filters empty for now
|
||||
var args filters.Args
|
||||
serverName := internal.ValidateServer(c)
|
||||
|
||||
cl, err := client.New(serverName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
cr, err := cl.ContainersPrune(ctx, args)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Infof("Containers deleted: %s; Space reclaimed: %v", cr.ContainersDeleted, cr.SpaceReclaimed)
|
||||
|
||||
nr, err := cl.NetworksPrune(ctx, args)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Infof("Networks deleted %s", nr.NetworksDeleted)
|
||||
|
||||
pruneFilters := filters.NewArgs()
|
||||
if allFilter {
|
||||
pruneFilters.Add("dangling", "false")
|
||||
}
|
||||
ir, err := cl.ImagesPrune(ctx, pruneFilters)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Infof("Images deleted: %s; Space reclaimed: %v", ir.ImagesDeleted, ir.SpaceReclaimed)
|
||||
|
||||
if volunesFilter {
|
||||
vr, err := cl.VolumesPrune(ctx, args)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Infof("Volumes deleted: %s; Space reclaimed: %v", vr.VolumesDeleted, vr.SpaceReclaimed)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
@ -104,7 +104,7 @@ var serverRemoveCommand = cli.Command{
|
||||
ArgsUsage: "[<server>]",
|
||||
Usage: "Remove a managed server",
|
||||
Description: `
|
||||
This command removes a server from Abra management.
|
||||
Remova a server from Abra management.
|
||||
|
||||
Depending on whether you used a 3rd party provider to create this server ("abra
|
||||
server new"), you can also destroy the virtual server as well. Pass
|
||||
@ -126,21 +126,24 @@ like tears in rain.
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Action: func(c *cli.Context) error {
|
||||
serverName := c.Args().Get(1)
|
||||
if serverName != "" {
|
||||
var err error
|
||||
serverName, err = internal.ValidateServer(c)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
serverName := internal.ValidateServer(c)
|
||||
|
||||
warnMsg := `Did not pass -s/--server for actual server deletion, prompting!
|
||||
|
||||
Abra doesn't currently know if it helped you create this server with one of the
|
||||
3rd party integrations (e.g. Capsul). You have a choice here to actually,
|
||||
really and finally destroy this server using those integrations. If you want to
|
||||
do this, choose Yes.
|
||||
|
||||
If you just want to remove the server config files & context, choose No.
|
||||
`
|
||||
|
||||
if !rmServer {
|
||||
logrus.Warn("did not pass -s/--server for actual server deletion, prompting")
|
||||
logrus.Warn(fmt.Sprintf(warnMsg))
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "prompt to actual server deletion?",
|
||||
Message: "delete actual live server?",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -164,10 +167,8 @@ like tears in rain.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if serverName != "" {
|
||||
if err := client.DeleteContext(serverName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -177,7 +178,6 @@ like tears in rain.
|
||||
}
|
||||
|
||||
logrus.Infof("server at %s has been lost in time, like tears in rain", serverName)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
|
@ -10,18 +10,18 @@ var ServerCommand = cli.Command{
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Manage servers",
|
||||
Description: `
|
||||
These commands support creating, managing and removing servers using 3rd party
|
||||
integrations.
|
||||
Create, manage and remove servers using 3rd party integrations.
|
||||
|
||||
Servers can be created from scratch using the "abra server new" command. If you
|
||||
already have a server, you can add it to your configuration using "abra server
|
||||
add". Abra can provision servers so that they are ready to deploy Co-op Cloud
|
||||
apps, see available flags on "server add" for more.
|
||||
recipes, see available flags on "abra server add" for more.
|
||||
`,
|
||||
Subcommands: []cli.Command{
|
||||
serverNewCommand,
|
||||
serverAddCommand,
|
||||
serverListCommand,
|
||||
serverRemoveCommand,
|
||||
serverPruneCommand,
|
||||
},
|
||||
}
|
||||
|
497
cli/updater/updater.go
Normal file
497
cli/updater/updater.go
Normal file
@ -0,0 +1,497 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/tagcmp"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const SERVER = "localhost"
|
||||
|
||||
var majorUpdate bool
|
||||
var majorFlag = &cli.BoolFlag{
|
||||
Name: "major, m",
|
||||
Usage: "Also check for major updates",
|
||||
Destination: &majorUpdate,
|
||||
}
|
||||
|
||||
var updateAll bool
|
||||
var allFlag = &cli.BoolFlag{
|
||||
Name: "all, a",
|
||||
Usage: "Update all deployed apps",
|
||||
Destination: &updateAll,
|
||||
}
|
||||
|
||||
// Notify checks for available upgrades
|
||||
var Notify = cli.Command{
|
||||
Name: "notify",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Check for available upgrades",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
majorFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `
|
||||
It reads the deployed app versions and looks for new versions in the recipe
|
||||
catalogue. If a new patch/minor version is available, a notification is
|
||||
printed. To include major versions use the --major flag.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
cl, err := client.New("default")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
stacks, err := stack.GetStacks(cl)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
for _, stackInfo := range stacks {
|
||||
stackName := stackInfo.Name
|
||||
recipeName, err := getLabel(cl, stackName, "recipe")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if recipeName != "" {
|
||||
_, err = getLatestUpgrade(cl, stackName, recipeName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// UpgradeApp upgrades apps.
|
||||
var UpgradeApp = cli.Command{
|
||||
Name: "upgrade",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Upgrade apps",
|
||||
ArgsUsage: "<stack-name> <recipe>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.ChaosFlag,
|
||||
majorFlag,
|
||||
allFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `
|
||||
Upgrade an app by specifying its stack name and recipe. By passing "--all"
|
||||
instead, every deployed app is upgraded. For each apps with enabled auto
|
||||
updates the deployed version is compared with the current recipe catalogue
|
||||
version. If a new patch/minor version is available, the app is upgraded. To
|
||||
include major versions use the "--major" flag. Don't do that, it will probably
|
||||
break things. Only apps that are not deployed with "--chaos" are upgraded, to
|
||||
update chaos deployments use the "--chaos" flag. Use it with care.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
cl, err := client.New("default")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
conf := runtime.New()
|
||||
|
||||
if !updateAll {
|
||||
stackName := c.Args().Get(0)
|
||||
recipeName := c.Args().Get(1)
|
||||
err = tryUpgrade(cl, stackName, recipeName, conf)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
stacks, err := stack.GetStacks(cl)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
for _, stackInfo := range stacks {
|
||||
stackName := stackInfo.Name
|
||||
recipeName, err := getLabel(cl, stackName, "recipe")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
err = tryUpgrade(cl, stackName, recipeName, conf)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// getLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}".
|
||||
func getLabel(cl *dockerclient.Client, stackName string, label string) (string, error) {
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
|
||||
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
|
||||
if labelValue, ok := service.Spec.Labels[labelKey]; ok {
|
||||
return labelValue, nil
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("no %s label found for %s", label, stackName)
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// getBoolLabel reads a boolean docker label.
|
||||
func getBoolLabel(cl *dockerclient.Client, stackName string, label string) (bool, error) {
|
||||
lableValue, err := getLabel(cl, stackName, label)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if lableValue != "" {
|
||||
value, err := strconv.ParseBool(lableValue)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
logrus.Debugf("Boolean label %s could not be found for %s, set default to false.", label, stackName)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// getEnv reads env variables from docker services.
|
||||
func getEnv(cl *dockerclient.Client, stackName string) (config.AppEnv, error) {
|
||||
envMap := make(map[string]string)
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
|
||||
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
envList := service.Spec.TaskTemplate.ContainerSpec.Env
|
||||
for _, envString := range envList {
|
||||
splitString := strings.SplitN(envString, "=", 2)
|
||||
if len(splitString) != 2 {
|
||||
logrus.Debugf("can't separate key from value: %s (this variable is probably unset)", envString)
|
||||
continue
|
||||
}
|
||||
k := splitString[0]
|
||||
v := splitString[1]
|
||||
logrus.Debugf("For %s read env %s with value: %s from docker service", stackName, k, v)
|
||||
envMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return envMap, nil
|
||||
}
|
||||
|
||||
// getLatestUpgrade returns the latest available version for an app respecting
|
||||
// the "--major" flag if it is newer than the currently deployed version.
|
||||
func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
|
||||
deployedVersion, err := getDeployedVersion(cl, stackName, recipeName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
availableUpgrades, err := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(availableUpgrades) == 0 {
|
||||
logrus.Debugf("no available upgrades for %s", stackName)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var chosenUpgrade string
|
||||
if len(availableUpgrades) > 0 {
|
||||
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
|
||||
logrus.Infof("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade)
|
||||
}
|
||||
|
||||
return chosenUpgrade, nil
|
||||
}
|
||||
|
||||
// getDeployedVersion returns the currently deployed version of an app.
|
||||
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
|
||||
logrus.Debugf("Retrieve deployed version whether %s is already deployed", stackName)
|
||||
|
||||
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !isDeployed {
|
||||
return "", fmt.Errorf("%s is not deployed?", stackName)
|
||||
}
|
||||
|
||||
if deployedVersion == "unknown" {
|
||||
return "", fmt.Errorf("failed to determine deployed version of %s", stackName)
|
||||
}
|
||||
|
||||
return deployedVersion, nil
|
||||
}
|
||||
|
||||
// getAvailableUpgrades returns all available versions of an app that are newer
|
||||
// than the deployed version. It only includes major upgrades if the "--major"
|
||||
// flag is set.
|
||||
func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string,
|
||||
deployedVersion string) ([]string, error) {
|
||||
catl, err := recipe.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
versions, err := recipe.GetRecipeCatalogueVersions(recipeName, catl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(versions) == 0 {
|
||||
return nil, fmt.Errorf("no published releases for %s in the recipe catalogue?", recipeName)
|
||||
}
|
||||
|
||||
var availableUpgrades []string
|
||||
for _, version := range versions {
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
versionDelta, err := parsedDeployedVersion.UpgradeDelta(parsedVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || majorUpdate) {
|
||||
availableUpgrades = append(availableUpgrades, version)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("Available updates for %s: %s", stackName, availableUpgrades)
|
||||
|
||||
return availableUpgrades, nil
|
||||
}
|
||||
|
||||
// processRecipeRepoVersion clones, pulls, checks out the version and lints the
|
||||
// recipe repository.
|
||||
func processRecipeRepoVersion(recipeName, version string, conf *runtime.Config) error {
|
||||
if err := recipe.EnsureExists(recipeName, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := recipe.EnsureUpToDate(recipeName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := recipe.EnsureVersion(recipeName, version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r, err := recipe.Get(recipeName, conf); err != nil {
|
||||
return err
|
||||
} else if err := lint.LintForErrors(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeAbraShEnv merges abra.sh env vars into the app env vars.
|
||||
func mergeAbraShEnv(recipeName string, env config.AppEnv) error {
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipeName, "abra.sh")
|
||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range abraShEnv {
|
||||
logrus.Debugf("read v:%s k: %s", v, k)
|
||||
env[k] = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDeployConfig merges and enriches the compose config for the deployment.
|
||||
func createDeployConfig(recipeName string, stackName string, env config.AppEnv) (*composetypes.Config, stack.Deploy, error) {
|
||||
env["STACK_NAME"] = stackName
|
||||
|
||||
deployOpts := stack.Deploy{
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
}
|
||||
|
||||
composeFiles, err := config.GetAppComposeFiles(recipeName, env)
|
||||
if err != nil {
|
||||
return nil, deployOpts, err
|
||||
}
|
||||
|
||||
deployOpts.Composefiles = composeFiles
|
||||
compose, err := config.GetAppComposeConfig(stackName, deployOpts, env)
|
||||
if err != nil {
|
||||
return nil, deployOpts, err
|
||||
}
|
||||
|
||||
config.ExposeAllEnv(stackName, compose, env)
|
||||
|
||||
// after the upgrade the deployment won't be in chaos state anymore
|
||||
config.SetChaosLabel(compose, stackName, false)
|
||||
config.SetRecipeLabel(compose, stackName, recipeName)
|
||||
config.SetUpdateLabel(compose, stackName, env)
|
||||
|
||||
return compose, deployOpts, nil
|
||||
}
|
||||
|
||||
// tryUpgrade performs the upgrade if all the requirements are fulfilled.
|
||||
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *runtime.Config) error {
|
||||
if recipeName == "" {
|
||||
logrus.Debugf("Don't update %s due to missing recipe name", stackName)
|
||||
return nil
|
||||
}
|
||||
|
||||
chaos, err := getBoolLabel(cl, stackName, "chaos")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if chaos && !internal.Chaos {
|
||||
logrus.Debugf("Don't update %s due to chaos deployment.", stackName)
|
||||
return nil
|
||||
}
|
||||
|
||||
updatesEnabled, err := getBoolLabel(cl, stackName, "autoupdate")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !updatesEnabled {
|
||||
logrus.Debugf("Don't update %s due to disabling auto updates or missing ENABLE_AUTOUPDATE env.", stackName)
|
||||
return nil
|
||||
}
|
||||
|
||||
upgradeVersion, err := getLatestUpgrade(cl, stackName, recipeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if upgradeVersion == "" {
|
||||
logrus.Debugf("Don't update %s due to no new version.", stackName)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = upgrade(cl, stackName, recipeName, upgradeVersion, conf)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// upgrade performs all necessary steps to upgrade an app.
|
||||
func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion string, conf *runtime.Config) error {
|
||||
env, err := getEnv(cl, stackName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app := config.App{
|
||||
Name: stackName,
|
||||
Recipe: recipeName,
|
||||
Server: SERVER,
|
||||
Env: env,
|
||||
}
|
||||
|
||||
if err = processRecipeRepoVersion(recipeName, upgradeVersion, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = mergeAbraShEnv(recipeName, app.Env); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
compose, deployOpts, err := createDeployConfig(recipeName, stackName, app.Env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("Upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
|
||||
|
||||
err = stack.RunDeploy(cl, deployOpts, compose, stackName, true)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func newAbraApp(version, commit string) *cli.App {
|
||||
app := &cli.App{
|
||||
Name: "kadabra",
|
||||
Usage: `The Co-op Cloud auto-updater
|
||||
____ ____ _ _
|
||||
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
|
||||
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
|
||||
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
|
||||
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|
||||
|_|
|
||||
`,
|
||||
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
|
||||
Commands: []cli.Command{
|
||||
Notify,
|
||||
UpgradeApp,
|
||||
},
|
||||
}
|
||||
|
||||
app.Before = func(c *cli.Context) error {
|
||||
logrus.Debugf("kadabra version %s, commit %s", version, commit)
|
||||
return nil
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// RunApp runs CLI abra app.
|
||||
func RunApp(version, commit string) {
|
||||
app := newAbraApp(version, commit)
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
@ -5,10 +5,10 @@ import (
|
||||
"coopcloud.tech/abra/cli"
|
||||
)
|
||||
|
||||
// Version is the current version of Abra
|
||||
// Version is the current version of Abra.
|
||||
var Version string
|
||||
|
||||
// Commit is the current git commit of Abra
|
||||
// Commit is the current git commit of Abra.
|
||||
var Commit string
|
||||
|
||||
func main() {
|
||||
|
23
cmd/kadabra/main.go
Normal file
23
cmd/kadabra/main.go
Normal file
@ -0,0 +1,23 @@
|
||||
// Package main provides the command-line entrypoint.
|
||||
package main
|
||||
|
||||
import (
|
||||
"coopcloud.tech/abra/cli/updater"
|
||||
)
|
||||
|
||||
// Version is the current version of Kadabra.
|
||||
var Version string
|
||||
|
||||
// Commit is the current git commit of Kadabra.
|
||||
var Commit string
|
||||
|
||||
func main() {
|
||||
if Version == "" {
|
||||
Version = "dev"
|
||||
}
|
||||
if Commit == "" {
|
||||
Commit = " "
|
||||
}
|
||||
|
||||
updater.RunApp(Version, Commit)
|
||||
}
|
49
go.mod
49
go.mod
@ -4,46 +4,47 @@ go 1.16
|
||||
|
||||
require (
|
||||
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7
|
||||
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
|
||||
github.com/docker/cli v20.10.12+incompatible
|
||||
github.com/docker/distribution v2.7.1+incompatible
|
||||
github.com/docker/docker v20.10.12+incompatible
|
||||
github.com/docker/go-units v0.4.0
|
||||
github.com/go-git/go-git/v5 v5.4.2
|
||||
github.com/hetznercloud/hcloud-go v1.33.1
|
||||
github.com/moby/sys/signal v0.6.0
|
||||
github.com/docker/cli v20.10.23+incompatible
|
||||
github.com/docker/distribution v2.8.1+incompatible
|
||||
github.com/docker/docker v20.10.23+incompatible
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/go-git/go-git/v5 v5.5.2
|
||||
github.com/hetznercloud/hcloud-go v1.40.0
|
||||
github.com/moby/sys/signal v0.7.0
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/schollz/progressbar/v3 v3.8.5
|
||||
github.com/schultz-is/passgen v1.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
gotest.tools/v3 v3.1.0
|
||||
github.com/schollz/progressbar/v3 v3.13.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
gotest.tools/v3 v3.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e
|
||||
github.com/Microsoft/hcsshim v0.8.21 // indirect
|
||||
github.com/buger/goterm v1.0.3
|
||||
github.com/containerd/containerd v1.5.5 // indirect
|
||||
github.com/buger/goterm v1.0.4
|
||||
github.com/containerd/containerd v1.5.9 // indirect
|
||||
github.com/containers/image v3.0.2+incompatible
|
||||
github.com/containers/storage v1.38.2 // indirect
|
||||
github.com/decentral1se/passgen v1.0.1
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
||||
github.com/gliderlabs/ssh v0.3.3
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0
|
||||
github.com/kevinburke/ssh_config v1.1.0
|
||||
github.com/hashicorp/go-retryablehttp v0.7.2
|
||||
github.com/klauspost/pgzip v1.2.5
|
||||
github.com/libdns/gandi v1.0.2
|
||||
github.com/libdns/libdns v0.2.1
|
||||
github.com/moby/sys/mount v0.2.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/runc v1.0.2 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/spf13/cobra v1.3.0 // indirect
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/urfave/cli v1.22.5
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
|
||||
github.com/urfave/cli v1.22.9
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
|
||||
golang.org/x/crypto v0.5.0 // indirect
|
||||
golang.org/x/sys v0.5.0
|
||||
)
|
||||
|
@ -1,13 +1,9 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -37,45 +33,6 @@ type deployedServiceSpec struct {
|
||||
// VersionSpec represents a deployed app and associated metadata.
|
||||
type VersionSpec map[string]deployedServiceSpec
|
||||
|
||||
// DeployedVersions lists metadata (e.g. versions) for deployed
|
||||
func DeployedVersions(ctx context.Context, cl *apiclient.Client, app config.App) (VersionSpec, bool, error) {
|
||||
services, err := stack.GetStackServices(ctx, cl, app.StackName())
|
||||
if err != nil {
|
||||
return VersionSpec{}, false, err
|
||||
}
|
||||
|
||||
appSpec := make(VersionSpec)
|
||||
for _, service := range services {
|
||||
serviceName := ParseServiceName(service.Spec.Name)
|
||||
label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), serviceName)
|
||||
if deployLabel, ok := service.Spec.Labels[label]; ok {
|
||||
version, _ := ParseVersionLabel(deployLabel)
|
||||
appSpec[serviceName] = deployedServiceSpec{Name: serviceName, Version: version}
|
||||
}
|
||||
}
|
||||
|
||||
deployed := len(services) > 0
|
||||
|
||||
if deployed {
|
||||
logrus.Debugf("detected %s as deployed versions of %s", appSpec, app.Name)
|
||||
} else {
|
||||
logrus.Debugf("detected %s as not deployed", app.Name)
|
||||
}
|
||||
|
||||
return appSpec, len(services) > 0, nil
|
||||
}
|
||||
|
||||
// ParseVersionLabel parses a $VERSION-$DIGEST app service label.
|
||||
func ParseVersionLabel(label string) (string, string) {
|
||||
// versions may look like v4.2-abcd or v4.2-alpine-abcd
|
||||
idx := strings.LastIndex(label, "-")
|
||||
version := label[:idx]
|
||||
digest := label[idx+1:]
|
||||
logrus.Debugf("parsed %s as version from %s", version, label)
|
||||
logrus.Debugf("parsed %s as digest from %s", digest, label)
|
||||
return version, digest
|
||||
}
|
||||
|
||||
// ParseServiceName parses a $STACK_NAME_$SERVICE_NAME service label.
|
||||
func ParseServiceName(label string) string {
|
||||
idx := strings.LastIndex(label, "_")
|
||||
|
@ -40,3 +40,24 @@ func RecipeNameComplete(c *cli.Context) {
|
||||
fmt.Println(name)
|
||||
}
|
||||
}
|
||||
|
||||
// SubcommandComplete completes subcommands.
|
||||
func SubcommandComplete(c *cli.Context) {
|
||||
if c.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
subcmds := []string{
|
||||
"app",
|
||||
"autocomplete",
|
||||
"catalogue",
|
||||
"recipe",
|
||||
"record",
|
||||
"server",
|
||||
"upgrade",
|
||||
}
|
||||
|
||||
for _, cmd := range subcmds {
|
||||
fmt.Println(cmd)
|
||||
}
|
||||
}
|
||||
|
122
pkg/catalogue/catalogue.go
Normal file
122
pkg/catalogue/catalogue.go
Normal file
@ -0,0 +1,122 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// CatalogueSkipList is all the repos that are not recipes.
|
||||
var CatalogueSkipList = map[string]bool{
|
||||
"abra": true,
|
||||
"abra-apps": true,
|
||||
"abra-aur": true,
|
||||
"abra-bash": true,
|
||||
"abra-capsul": true,
|
||||
"abra-gandi": true,
|
||||
"abra-hetzner": true,
|
||||
"apps": true,
|
||||
"aur-abra-git": true,
|
||||
"auto-recipes-catalogue-json": true,
|
||||
"auto-mirror": true,
|
||||
"backup-bot": true,
|
||||
"backup-bot-two": true,
|
||||
"beta.coopcloud.tech": true,
|
||||
"comrade-renovate-bot": true,
|
||||
"coopcloud.tech": true,
|
||||
"coturn": true,
|
||||
"docker-cp-deploy": true,
|
||||
"docker-dind-bats-kcov": true,
|
||||
"docs.coopcloud.tech": true,
|
||||
"drone-abra": true,
|
||||
"example": true,
|
||||
"gardening": true,
|
||||
"go-abra": true,
|
||||
"organising": true,
|
||||
"pyabra": true,
|
||||
"radicle-seed-node": true,
|
||||
"recipes-catalogue-json": true,
|
||||
"recipes-wishlist": true,
|
||||
"recipes.coopcloud.tech": true,
|
||||
"stack-ssh-deploy": true,
|
||||
"swarm-cronjob": true,
|
||||
"tagcmp": true,
|
||||
"traefik-cert-dumper": true,
|
||||
"tyop": true,
|
||||
}
|
||||
|
||||
// EnsureCatalogue ensures that the catalogue is cloned locally & present.
|
||||
func EnsureCatalogue() error {
|
||||
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
|
||||
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
|
||||
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
|
||||
if err := gitPkg.Clone(catalogueDir, url); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("cloned catalogue repository to %s", catalogueDir)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureUpToDate ensures that the local catalogue has no unstaged changes as
|
||||
// is up to date. This is useful to run before doing catalogue generation.
|
||||
func EnsureUpToDate() error {
|
||||
isClean, err := gitPkg.IsClean(config.CATALOGUE_DIR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
msg := "%s has locally unstaged changes? please commit/remove your changes before proceeding"
|
||||
return fmt.Errorf(msg, config.CATALOGUE_DIR)
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(config.CATALOGUE_DIR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remotes, err := repo.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(remotes) == 0 {
|
||||
msg := "cannot ensure %s is up-to-date, no git remotes configured"
|
||||
logrus.Debugf(msg, config.CATALOGUE_DIR)
|
||||
return nil
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
branch, err := gitPkg.CheckoutDefaultBranch(repo, config.CATALOGUE_DIR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := &git.PullOptions{
|
||||
Force: true,
|
||||
ReferenceName: branch,
|
||||
}
|
||||
|
||||
if err := worktree.Pull(opts); err != nil {
|
||||
if !strings.Contains(err.Error(), "already up-to-date") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("fetched latest git changes for %s", config.CATALOGUE_DIR)
|
||||
|
||||
return nil
|
||||
}
|
@ -2,24 +2,31 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
sshPkg "coopcloud.tech/abra/pkg/ssh"
|
||||
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// New initiates a new Docker client.
|
||||
func New(contextName string) (*client.Client, error) {
|
||||
// New initiates a new Docker client. New client connections are validated so
|
||||
// that we ensure connections via SSH to the daemon can succeed. It takes into
|
||||
// account that you may only want the local client and not communicate via SSH.
|
||||
// For this use-case, please pass "default" as the contextName.
|
||||
func New(serverName string) (*client.Client, error) {
|
||||
var clientOpts []client.Opt
|
||||
|
||||
if contextName != "default" {
|
||||
context, err := GetContext(contextName)
|
||||
if serverName != "default" {
|
||||
context, err := GetContext(serverName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("unknown server, run \"abra server add %s\"?", serverName)
|
||||
}
|
||||
|
||||
ctxEndpoint, err := contextPkg.GetContextEndpoint(context)
|
||||
@ -27,9 +34,12 @@ func New(contextName string) (*client.Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
helper := commandconnPkg.NewConnectionHelper(ctxEndpoint)
|
||||
helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
// No tls, no proxy
|
||||
Transport: &http.Transport{
|
||||
DialContext: helper.Dialer,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
@ -55,7 +65,20 @@ func New(contextName string) (*client.Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logrus.Debugf("created client for %s", contextName)
|
||||
logrus.Debugf("created client for %s", serverName)
|
||||
|
||||
info, err := cl.Info(context.Background())
|
||||
if err != nil {
|
||||
return cl, sshPkg.Fatal(serverName, err)
|
||||
}
|
||||
|
||||
if info.Swarm.LocalNodeState == "inactive" {
|
||||
if serverName != "default" {
|
||||
return cl, fmt.Errorf("swarm mode not enabled on %s?", serverName)
|
||||
} else {
|
||||
return cl, errors.New("swarm mode not enabled on local server?")
|
||||
}
|
||||
}
|
||||
|
||||
return cl, nil
|
||||
}
|
||||
|
@ -1,193 +1,28 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/web"
|
||||
"github.com/containers/image/docker"
|
||||
"github.com/containers/image/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type RawTag struct {
|
||||
Layer string
|
||||
Name string
|
||||
}
|
||||
// GetRegistryTags retrieves all tags of an image from a container registry.
|
||||
func GetRegistryTags(img reference.Named) ([]string, error) {
|
||||
var tags []string
|
||||
|
||||
type RawTags []RawTag
|
||||
ref, err := docker.ParseReference(fmt.Sprintf("//%s", img))
|
||||
if err != nil {
|
||||
return tags, fmt.Errorf("failed to parse image %s, saw: %s", img, err.Error())
|
||||
}
|
||||
|
||||
var registryURL = "https://registry.hub.docker.com/v1/repositories/%s/tags"
|
||||
|
||||
func GetRegistryTags(image string) (RawTags, error) {
|
||||
var tags RawTags
|
||||
|
||||
tagsUrl := fmt.Sprintf(registryURL, image)
|
||||
if err := web.ReadJSON(tagsUrl, &tags); err != nil {
|
||||
ctx := context.Background()
|
||||
tags, err = docker.GetRepositoryTags(ctx, &types.SystemContext{}, ref)
|
||||
if err != nil {
|
||||
return tags, err
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func basicAuth(username, password string) string {
|
||||
auth := username + ":" + password
|
||||
return base64.StdEncoding.EncodeToString([]byte(auth))
|
||||
}
|
||||
|
||||
// getRegv2Token retrieves a registry v2 authentication token.
|
||||
func getRegv2Token(cl *client.Client, image reference.Named, registryUsername, registryPassword string) (string, error) {
|
||||
img := reference.Path(image)
|
||||
tokenURL := "https://auth.docker.io/token"
|
||||
values := fmt.Sprintf("service=registry.docker.io&scope=repository:%s:pull", img)
|
||||
|
||||
fullURL := fmt.Sprintf("%s?%s", tokenURL, values)
|
||||
req, err := retryablehttp.NewRequest("GET", fullURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if registryUsername != "" && registryPassword != "" {
|
||||
logrus.Debugf("using registry log in credentials for token request")
|
||||
auth := basicAuth(registryUsername, registryPassword)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", auth))
|
||||
}
|
||||
|
||||
client := web.NewHTTPRetryClient()
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
_, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
tokenRes := struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
Expiry int `json:"expires_in"`
|
||||
Issued string `json:"issued_at"`
|
||||
Token string `json:"token"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(body, &tokenRes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenRes.Token, nil
|
||||
}
|
||||
|
||||
// GetTagDigest retrieves an image digest from a v2 registry
|
||||
func GetTagDigest(cl *client.Client, image reference.Named, registryUsername, registryPassword string) (string, error) {
|
||||
img := reference.Path(image)
|
||||
tag := image.(reference.NamedTagged).Tag()
|
||||
manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag)
|
||||
|
||||
req, err := retryablehttp.NewRequest("GET", manifestURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err := getRegv2Token(cl, image, registryUsername, registryPassword)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return "", fmt.Errorf("unable to retrieve registry token?")
|
||||
}
|
||||
|
||||
req.Header = http.Header{
|
||||
"Accept": []string{
|
||||
"application/vnd.docker.distribution.manifest.v2+json",
|
||||
"application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
},
|
||||
"Authorization": []string{fmt.Sprintf("Bearer %s", token)},
|
||||
}
|
||||
|
||||
client := web.NewHTTPRetryClient()
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
_, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
registryResT1 := struct {
|
||||
SchemaVersion int
|
||||
MediaType string
|
||||
Manifests []struct {
|
||||
MediaType string
|
||||
Size int
|
||||
Digest string
|
||||
Platform struct {
|
||||
Architecture string
|
||||
Os string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
|
||||
registryResT2 := struct {
|
||||
SchemaVersion int
|
||||
MediaType string
|
||||
Config struct {
|
||||
MediaType string
|
||||
Size int
|
||||
Digest string
|
||||
}
|
||||
Layers []struct {
|
||||
MediaType string
|
||||
Size int
|
||||
Digest string
|
||||
}
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(body, ®istryResT1); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var digest string
|
||||
for _, manifest := range registryResT1.Manifests {
|
||||
if string(manifest.Platform.Architecture) == "amd64" {
|
||||
digest = strings.Split(manifest.Digest, ":")[1][:7]
|
||||
}
|
||||
}
|
||||
|
||||
if digest == "" {
|
||||
if err := json.Unmarshal(body, ®istryResT2); err != nil {
|
||||
return "", err
|
||||
}
|
||||
digest = strings.Split(registryResT2.Config.Digest, ":")[1][:7]
|
||||
}
|
||||
|
||||
if digest == "" {
|
||||
return "", fmt.Errorf("Unable to retrieve amd64 digest for %s", image)
|
||||
}
|
||||
|
||||
return digest, nil
|
||||
}
|
||||
|
@ -4,20 +4,14 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func StoreSecret(secretName, secretValue, server string) error {
|
||||
cl, err := New(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
func StoreSecret(cl *client.Client, secretName, secretValue, server string) error {
|
||||
ann := swarm.Annotations{Name: secretName}
|
||||
spec := swarm.SecretSpec{Annotations: ann, Data: []byte(secretValue)}
|
||||
|
||||
// We don't bother with the secret IDs for now
|
||||
if _, err := cl.SecretCreate(ctx, spec); err != nil {
|
||||
if _, err := cl.SecretCreate(context.Background(), spec); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -5,23 +5,14 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func GetVolumes(ctx context.Context, server string, appName string) ([]*types.Volume, error) {
|
||||
|
||||
cl, err := New(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fs := filters.NewArgs()
|
||||
fs.Add("name", appName)
|
||||
|
||||
func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]*types.Volume, error) {
|
||||
volumeListOKBody, err := cl.VolumeList(ctx, fs)
|
||||
volumeList := volumeListOKBody.Volumes
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
return volumeList, err
|
||||
}
|
||||
|
||||
return volumeList, nil
|
||||
@ -29,23 +20,21 @@ func GetVolumes(ctx context.Context, server string, appName string) ([]*types.Vo
|
||||
|
||||
func GetVolumeNames(volumes []*types.Volume) []string {
|
||||
var volumeNames []string
|
||||
|
||||
for _, vol := range volumes {
|
||||
volumeNames = append(volumeNames, vol.Name)
|
||||
}
|
||||
|
||||
return volumeNames
|
||||
}
|
||||
|
||||
func RemoveVolumes(ctx context.Context, server string, volumeNames []string, force bool) error {
|
||||
cl, err := New(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func RemoveVolumes(cl *client.Client, ctx context.Context, server string, volumeNames []string, force bool) error {
|
||||
for _, volName := range volumeNames {
|
||||
err := cl.VolumeRemove(ctx, volName, force)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -2,17 +2,21 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/schollz/progressbar/v3"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -36,28 +40,75 @@ type AppFiles map[AppName]AppFile
|
||||
// App reprents an app with its env file read into memory
|
||||
type App struct {
|
||||
Name AppName
|
||||
Type string
|
||||
Recipe string
|
||||
Domain string
|
||||
Env AppEnv
|
||||
Server string
|
||||
Path string
|
||||
}
|
||||
|
||||
// StackName gets what the docker safe stack name is for the app. This should
|
||||
// not not shown to the user, use a.Name for that. Give the output of this
|
||||
// command to Docker only.
|
||||
// StackName gets whatever the docker safe (uses the right delimiting
|
||||
// character, e.g. "_") stack name is for the app. In general, you don't want
|
||||
// to use this to show anything to end-users, you want use a.Name instead.
|
||||
func (a App) StackName() string {
|
||||
if _, exists := a.Env["STACK_NAME"]; exists {
|
||||
return a.Env["STACK_NAME"]
|
||||
}
|
||||
|
||||
stackName := SanitiseAppName(a.Name)
|
||||
|
||||
if len(stackName) > 45 {
|
||||
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45])
|
||||
stackName = stackName[:45]
|
||||
}
|
||||
|
||||
a.Env["STACK_NAME"] = stackName
|
||||
|
||||
return stackName
|
||||
}
|
||||
|
||||
// SORTING TYPES
|
||||
// Filters retrieves exact app filters for querying the container runtime. Due
|
||||
// to upstream issues, filtering works different depending on what you're
|
||||
// querying. So, for example, secrets don't work with regex! The caller needs
|
||||
// to implement their own validation that the right secrets are matched. In
|
||||
// order to handle these cases, we provide the `appendServiceNames` /
|
||||
// `exactMatch` modifiers.
|
||||
func (a App) Filters(appendServiceNames, exactMatch bool) (filters.Args, error) {
|
||||
filters := filters.NewArgs()
|
||||
|
||||
composeFiles, err := GetAppComposeFiles(a.Recipe, a.Env)
|
||||
if err != nil {
|
||||
return filters, err
|
||||
}
|
||||
|
||||
opts := stack.Deploy{Composefiles: composeFiles}
|
||||
compose, err := GetAppComposeConfig(a.Recipe, opts, a.Env)
|
||||
if err != nil {
|
||||
return filters, err
|
||||
}
|
||||
|
||||
for _, service := range compose.Services {
|
||||
var filter string
|
||||
|
||||
if appendServiceNames {
|
||||
if exactMatch {
|
||||
filter = fmt.Sprintf("^%s_%s", a.StackName(), service.Name)
|
||||
} else {
|
||||
filter = fmt.Sprintf("%s_%s", a.StackName(), service.Name)
|
||||
}
|
||||
} else {
|
||||
if exactMatch {
|
||||
filter = fmt.Sprintf("^%s", a.StackName())
|
||||
} else {
|
||||
filter = fmt.Sprintf("%s", a.StackName())
|
||||
}
|
||||
}
|
||||
|
||||
filters.Add("name", filter)
|
||||
}
|
||||
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
// ByServer sort a slice of Apps
|
||||
type ByServer []App
|
||||
@ -68,25 +119,25 @@ func (a ByServer) Less(i, j int) bool {
|
||||
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
|
||||
}
|
||||
|
||||
// ByServerAndType sort a slice of Apps
|
||||
type ByServerAndType []App
|
||||
// ByServerAndRecipe sort a slice of Apps
|
||||
type ByServerAndRecipe []App
|
||||
|
||||
func (a ByServerAndType) Len() int { return len(a) }
|
||||
func (a ByServerAndType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByServerAndType) Less(i, j int) bool {
|
||||
func (a ByServerAndRecipe) Len() int { return len(a) }
|
||||
func (a ByServerAndRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByServerAndRecipe) Less(i, j int) bool {
|
||||
if a[i].Server == a[j].Server {
|
||||
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
|
||||
return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
|
||||
}
|
||||
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
|
||||
}
|
||||
|
||||
// ByType sort a slice of Apps
|
||||
type ByType []App
|
||||
// ByRecipe sort a slice of Apps
|
||||
type ByRecipe []App
|
||||
|
||||
func (a ByType) Len() int { return len(a) }
|
||||
func (a ByType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByType) Less(i, j int) bool {
|
||||
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
|
||||
func (a ByRecipe) Len() int { return len(a) }
|
||||
func (a ByRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByRecipe) Less(i, j int) bool {
|
||||
return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
|
||||
}
|
||||
|
||||
// ByName sort a slice of Apps
|
||||
@ -118,22 +169,25 @@ func readAppEnvFile(appFile AppFile, name AppName) (App, error) {
|
||||
func newApp(env AppEnv, name string, appFile AppFile) (App, error) {
|
||||
domain := env["DOMAIN"]
|
||||
|
||||
appType, exists := env["TYPE"]
|
||||
recipe, exists := env["RECIPE"]
|
||||
if !exists {
|
||||
return App{}, fmt.Errorf("%s is missing the TYPE env var", name)
|
||||
recipe, exists = env["TYPE"]
|
||||
if !exists {
|
||||
return App{}, fmt.Errorf("%s is missing the TYPE env var?", name)
|
||||
}
|
||||
}
|
||||
|
||||
return App{
|
||||
Name: name,
|
||||
Domain: domain,
|
||||
Type: appType,
|
||||
Recipe: recipe,
|
||||
Env: env,
|
||||
Server: appFile.Server,
|
||||
Path: appFile.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadAppFiles gets all app files for a given set of servers or all servers
|
||||
// LoadAppFiles gets all app files for a given set of servers or all servers.
|
||||
func LoadAppFiles(servers ...string) (AppFiles, error) {
|
||||
appFiles := make(AppFiles)
|
||||
if len(servers) == 1 {
|
||||
@ -142,7 +196,7 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
|
||||
var err error
|
||||
servers, err = GetAllFoldersInDirectory(SERVERS_DIR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return appFiles, err
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -151,10 +205,11 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
|
||||
|
||||
for _, server := range servers {
|
||||
serverDir := path.Join(SERVERS_DIR, server)
|
||||
files, err := getAllFilesInDirectory(serverDir)
|
||||
files, err := GetAllFilesInDirectory(serverDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server)
|
||||
return appFiles, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
appName := strings.TrimSuffix(file.Name(), ".env")
|
||||
appFilePath := path.Join(SERVERS_DIR, server, file.Name())
|
||||
@ -164,12 +219,13 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return appFiles, nil
|
||||
}
|
||||
|
||||
// GetApp loads an apps settings, reading it from file, in preparation to use it
|
||||
//
|
||||
// ONLY use when ready to use the env file to keep IO down
|
||||
// GetApp loads an apps settings, reading it from file, in preparation to use
|
||||
// it. It should only be used when ready to use the env file to keep IO
|
||||
// operations down.
|
||||
func GetApp(apps AppFiles, name AppName) (App, error) {
|
||||
appFile, exists := apps[name]
|
||||
if !exists {
|
||||
@ -184,8 +240,9 @@ func GetApp(apps AppFiles, name AppName) (App, error) {
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// GetApps returns a slice of Apps with their env files read from a given slice of AppFiles
|
||||
func GetApps(appFiles AppFiles) ([]App, error) {
|
||||
// GetApps returns a slice of Apps with their env files read from a given
|
||||
// slice of AppFiles.
|
||||
func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) {
|
||||
var apps []App
|
||||
|
||||
for name := range appFiles {
|
||||
@ -193,8 +250,15 @@ func GetApps(appFiles AppFiles) ([]App, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if recipeFilter != "" {
|
||||
if app.Recipe == recipeFilter {
|
||||
apps = append(apps, app)
|
||||
}
|
||||
} else {
|
||||
apps = append(apps, app)
|
||||
}
|
||||
}
|
||||
|
||||
return apps, nil
|
||||
}
|
||||
@ -213,13 +277,13 @@ func GetAppServiceNames(appName string) ([]string, error) {
|
||||
return serviceNames, err
|
||||
}
|
||||
|
||||
composeFiles, err := GetAppComposeFiles(app.Type, app.Env)
|
||||
composeFiles, err := GetAppComposeFiles(app.Recipe, app.Env)
|
||||
if err != nil {
|
||||
return serviceNames, err
|
||||
}
|
||||
|
||||
opts := stack.Deploy{Composefiles: composeFiles}
|
||||
compose, err := GetAppComposeConfig(app.Type, opts, app.Env)
|
||||
compose, err := GetAppComposeConfig(app.Recipe, opts, app.Env)
|
||||
if err != nil {
|
||||
return serviceNames, err
|
||||
}
|
||||
@ -240,7 +304,7 @@ func GetAppNames() ([]string, error) {
|
||||
return appNames, err
|
||||
}
|
||||
|
||||
apps, err := GetApps(appFiles)
|
||||
apps, err := GetApps(appFiles, "")
|
||||
if err != nil {
|
||||
return appNames, err
|
||||
}
|
||||
@ -252,7 +316,8 @@ func GetAppNames() ([]string, error) {
|
||||
return appNames, nil
|
||||
}
|
||||
|
||||
// TemplateAppEnvSample copies the example env file for the app into the users env files
|
||||
// TemplateAppEnvSample copies the example env file for the app into the users
|
||||
// env files.
|
||||
func TemplateAppEnvSample(recipeName, appName, server, domain string) error {
|
||||
envSamplePath := path.Join(RECIPES_DIR, recipeName, ".env.sample")
|
||||
envSample, err := ioutil.ReadFile(envSamplePath)
|
||||
@ -270,18 +335,15 @@ func TemplateAppEnvSample(recipeName, appName, server, domain string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(appEnvPath, os.O_RDWR, 0664)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tpl, err := template.ParseFiles(appEnvPath)
|
||||
read, err := ioutil.ReadFile(appEnvPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tpl.Execute(file, struct{ Name string }{recipeName}); err != nil {
|
||||
newContents := strings.Replace(string(read), recipeName+".example.com", domain, -1)
|
||||
|
||||
err = ioutil.WriteFile(appEnvPath, []byte(newContents), 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -290,35 +352,49 @@ func TemplateAppEnvSample(recipeName, appName, server, domain string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SanitiseAppName makes a app name usable with Docker by replacing illegal characters
|
||||
// SanitiseAppName makes a app name usable with Docker by replacing illegal
|
||||
// characters.
|
||||
func SanitiseAppName(name string) string {
|
||||
return strings.ReplaceAll(name, ".", "_")
|
||||
}
|
||||
|
||||
// GetAppStatuses queries servers to check the deployment status of given apps
|
||||
func GetAppStatuses(appFiles AppFiles) (map[string]map[string]string, error) {
|
||||
// GetAppStatuses queries servers to check the deployment status of given apps.
|
||||
func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]string, error) {
|
||||
statuses := make(map[string]map[string]string)
|
||||
|
||||
var unique []string
|
||||
servers := make(map[string]struct{})
|
||||
for _, appFile := range appFiles {
|
||||
if _, ok := servers[appFile.Server]; !ok {
|
||||
servers[appFile.Server] = struct{}{}
|
||||
unique = append(unique, appFile.Server)
|
||||
for _, app := range apps {
|
||||
if _, ok := servers[app.Server]; !ok {
|
||||
servers[app.Server] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
bar := formatter.CreateProgressbar(len(servers), "querying remote servers...")
|
||||
var bar *progressbar.ProgressBar
|
||||
if !MachineReadable {
|
||||
bar = formatter.CreateProgressbar(len(servers), "querying remote servers...")
|
||||
}
|
||||
|
||||
ch := make(chan stack.StackStatus, len(servers))
|
||||
for server := range servers {
|
||||
cl, err := client.New(server)
|
||||
if err != nil {
|
||||
return statuses, err
|
||||
}
|
||||
|
||||
go func(s string) {
|
||||
ch <- stack.GetAllDeployedServices(s)
|
||||
ch <- stack.GetAllDeployedServices(cl, s)
|
||||
if !MachineReadable {
|
||||
bar.Add(1)
|
||||
}
|
||||
}(server)
|
||||
}
|
||||
|
||||
for range servers {
|
||||
status := <-ch
|
||||
if status.Err != nil {
|
||||
return statuses, status.Err
|
||||
}
|
||||
|
||||
for _, service := range status.Services {
|
||||
result := make(map[string]string)
|
||||
name := service.Spec.Labels[convert.LabelNamespace]
|
||||
@ -381,3 +457,61 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv AppEnv) (*comp
|
||||
|
||||
return compose, nil
|
||||
}
|
||||
|
||||
// ExposeAllEnv exposes all env variables to the app container
|
||||
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv AppEnv) {
|
||||
for _, service := range compose.Services {
|
||||
if service.Name == "app" {
|
||||
logrus.Debugf("Add the following environment to the app service config of %s:", stackName)
|
||||
for k, v := range appEnv {
|
||||
_, exists := service.Environment[k]
|
||||
if !exists {
|
||||
value := v
|
||||
service.Environment[k] = &value
|
||||
logrus.Debugf("Add Key: %s Value: %s to %s", k, value, stackName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetRecipeLabel adds the label 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' to the app container
|
||||
// to signal which recipe is connected to the deployed app
|
||||
func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) {
|
||||
for _, service := range compose.Services {
|
||||
if service.Name == "app" {
|
||||
logrus.Debugf("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName)
|
||||
labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName)
|
||||
service.Deploy.Labels[labelKey] = recipe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetChaosLabel adds the label 'coop-cloud.${STACK_NAME}.chaos=true/false' to the app container
|
||||
// to signal if the app is deployed in chaos mode
|
||||
func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
|
||||
for _, service := range compose.Services {
|
||||
if service.Name == "app" {
|
||||
logrus.Debugf("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName)
|
||||
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", stackName)
|
||||
service.Deploy.Labels[labelKey] = strconv.FormatBool(chaos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the
|
||||
// auto update process for this app. The default if this variable is not set is to disable
|
||||
// the auto update process.
|
||||
func SetUpdateLabel(compose *composetypes.Config, stackName string, appEnv AppEnv) {
|
||||
for _, service := range compose.Services {
|
||||
if service.Name == "app" {
|
||||
enable_auto_update, exists := appEnv["ENABLE_AUTO_UPDATE"]
|
||||
if !exists {
|
||||
enable_auto_update = "false"
|
||||
}
|
||||
logrus.Debugf("set label 'coop-cloud.%s.autoupdate' to %s for %s", stackName, enable_auto_update, stackName)
|
||||
labelKey := fmt.Sprintf("coop-cloud.%s.autoupdate", stackName)
|
||||
service.Deploy.Labels[labelKey] = enable_auto_update
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,10 +16,13 @@ import (
|
||||
|
||||
var ABRA_DIR = os.ExpandEnv("$HOME/.abra")
|
||||
var SERVERS_DIR = path.Join(ABRA_DIR, "servers")
|
||||
var RECIPES_DIR = path.Join(ABRA_DIR, "apps")
|
||||
var RECIPES_DIR = path.Join(ABRA_DIR, "recipes")
|
||||
var VENDOR_DIR = path.Join(ABRA_DIR, "vendor")
|
||||
var BACKUP_DIR = path.Join(ABRA_DIR, "backups")
|
||||
var CATALOGUE_DIR = path.Join(ABRA_DIR, "catalogue")
|
||||
var RECIPES_JSON = path.Join(ABRA_DIR, "catalogue", "recipes.json")
|
||||
var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
|
||||
var CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
|
||||
var SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
|
||||
|
||||
// GetServers retrieves all servers.
|
||||
@ -63,8 +66,8 @@ func ReadServerNames() ([]string, error) {
|
||||
return serverNames, nil
|
||||
}
|
||||
|
||||
// getAllFilesInDirectory returns filenames of all files in directory
|
||||
func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
|
||||
// GetAllFilesInDirectory returns filenames of all files in directory
|
||||
func GetAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
|
||||
var realFiles []fs.FileInfo
|
||||
|
||||
files, err := ioutil.ReadDir(directory)
|
||||
|
@ -20,12 +20,12 @@ var serverName = "evil.corp"
|
||||
|
||||
var expectedAppEnv = AppEnv{
|
||||
"DOMAIN": "ecloud.evil.corp",
|
||||
"TYPE": "ecloud",
|
||||
"RECIPE": "ecloud",
|
||||
}
|
||||
|
||||
var expectedApp = App{
|
||||
Name: appName,
|
||||
Type: expectedAppEnv["TYPE"],
|
||||
Recipe: expectedAppEnv["RECIPE"],
|
||||
Domain: expectedAppEnv["DOMAIN"],
|
||||
Env: expectedAppEnv,
|
||||
Path: expectedAppFile.Path,
|
||||
@ -54,7 +54,7 @@ func TestGetAllFoldersInDirectory(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetAllFilesInDirectory(t *testing.T) {
|
||||
files, err := getAllFilesInDirectory(testFolder)
|
||||
files, err := GetAllFilesInDirectory(testFolder)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -74,11 +74,11 @@ func TestReadEnv(t *testing.T) {
|
||||
}
|
||||
if !reflect.DeepEqual(env, expectedAppEnv) {
|
||||
t.Fatalf(
|
||||
"did not get expected application settings. Expected: DOMAIN=%s TYPE=%s; Got: DOMAIN=%s TYPE=%s",
|
||||
"did not get expected application settings. Expected: DOMAIN=%s RECIPE=%s; Got: DOMAIN=%s RECIPE=%s",
|
||||
expectedAppEnv["DOMAIN"],
|
||||
expectedAppEnv["TYPE"],
|
||||
expectedAppEnv["RECIPE"],
|
||||
env["DOMAIN"],
|
||||
env["TYPE"],
|
||||
env["RECIPE"],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -13,10 +13,10 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetContainer retrieves a container. If prompt is true and the retrievd count
|
||||
// of containers does not match 1, then a prompt is presented to let the user
|
||||
// choose. A count of 0 is handled gracefully.
|
||||
func GetContainer(c context.Context, cl *client.Client, filters filters.Args, prompt bool) (types.Container, error) {
|
||||
// GetContainer retrieves a container. If noInput is false and the retrievd
|
||||
// count of containers does not match 1, then a prompt is presented to let the
|
||||
// user choose. A count of 0 is handled gracefully.
|
||||
func GetContainer(c context.Context, cl *client.Client, filters filters.Args, noInput bool) (types.Container, error) {
|
||||
containerOpts := types.ContainerListOptions{Filters: filters}
|
||||
containers, err := cl.ContainerList(c, containerOpts)
|
||||
if err != nil {
|
||||
@ -37,7 +37,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, pr
|
||||
containersRaw = append(containersRaw, fmt.Sprintf("%s (created %v)", trimmed, created))
|
||||
}
|
||||
|
||||
if !prompt {
|
||||
if noInput {
|
||||
err := fmt.Errorf("expected 1 container but found %v: %s", len(containers), strings.Join(containersRaw, " "))
|
||||
return types.Container{}, err
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@ -32,41 +31,20 @@ func NewToken(provider, providerTokenEnvVar string) (string, error) {
|
||||
|
||||
// EnsureIPv4 ensures that an ipv4 address is set for a domain name
|
||||
func EnsureIPv4(domainName string) (string, error) {
|
||||
var ipv4 string
|
||||
|
||||
// comrade librehosters DNS resolver -> https://www.privacy-handbuch.de/handbuch_93d.htm
|
||||
freifunkDNS := "5.1.66.255:53"
|
||||
|
||||
resolver := &net.Resolver{
|
||||
PreferGo: false,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{
|
||||
Timeout: time.Millisecond * time.Duration(10000),
|
||||
}
|
||||
return d.DialContext(ctx, "udp", freifunkDNS)
|
||||
},
|
||||
}
|
||||
|
||||
logrus.Debugf("created DNS resolver via %s", freifunkDNS)
|
||||
|
||||
ctx := context.Background()
|
||||
ips, err := resolver.LookupIPAddr(ctx, domainName)
|
||||
ipv4, err := net.ResolveIPAddr("ip", domainName)
|
||||
if err != nil {
|
||||
return ipv4, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
return ipv4, fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
|
||||
}
|
||||
|
||||
ipv4 = ips[0].IP.To4().String()
|
||||
logrus.Debugf("discovered the following ipv4 addr: %s", ipv4)
|
||||
|
||||
return ipv4, nil
|
||||
return ipv4.String(), nil
|
||||
}
|
||||
|
||||
// EnsureDomainsResolveSameIPv4 ensures that domains resolve to the same ipv4 address
|
||||
func EnsureDomainsResolveSameIPv4(domainName, server string) (string, error) {
|
||||
if server == "default" || server == "local" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var ipv4 string
|
||||
|
||||
domainIPv4, err := EnsureIPv4(domainName)
|
||||
|
@ -6,7 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/go-units"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
// "github.com/olekukonko/tablewriter"
|
||||
"coopcloud.tech/abra/pkg/jsontable"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -32,8 +33,8 @@ func HumanDuration(timestamp int64) string {
|
||||
}
|
||||
|
||||
// CreateTable prepares a table layout for output.
|
||||
func CreateTable(columns []string) *tablewriter.Table {
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
func CreateTable(columns []string) *jsontable.JSONTable {
|
||||
table := jsontable.NewJSONTable(os.Stdout)
|
||||
table.SetAutoWrapText(false)
|
||||
table.SetHeader(columns)
|
||||
return table
|
||||
|
@ -1,11 +1,34 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetCurrentBranch retrieves the current branch of a repository
|
||||
// Check if a branch exists in a repo. Use this and not repository.Branch(),
|
||||
// because the latter does not actually check for existing branches. See
|
||||
// https://github.com/gogit/gogit/issues/518 for more.
|
||||
func HasBranch(repository *git.Repository, name string) bool {
|
||||
var exist bool
|
||||
|
||||
if iter, err := repository.Branches(); err == nil {
|
||||
iterFunc := func(reference *plumbing.Reference) error {
|
||||
if name == reference.Name().Short() {
|
||||
exist = true
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_ = iter.ForEach(iterFunc)
|
||||
}
|
||||
|
||||
return exist
|
||||
}
|
||||
|
||||
// GetCurrentBranch retrieves the current branch of a repository.
|
||||
func GetCurrentBranch(repository *git.Repository) (string, error) {
|
||||
branchRefs, err := repository.Branches()
|
||||
if err != nil {
|
||||
@ -33,3 +56,45 @@ func GetCurrentBranch(repository *git.Repository) (string, error) {
|
||||
|
||||
return currentBranchName, nil
|
||||
}
|
||||
|
||||
// GetDefaultBranch retrieves the default branch of a repository.
|
||||
func GetDefaultBranch(repo *git.Repository, repoPath string) (plumbing.ReferenceName, error) {
|
||||
branch := "master"
|
||||
|
||||
if !HasBranch(repo, "master") {
|
||||
if !HasBranch(repo, "main") {
|
||||
return "", fmt.Errorf("failed to select default branch in %s", repoPath)
|
||||
}
|
||||
branch = "main"
|
||||
}
|
||||
|
||||
return plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", branch)), nil
|
||||
}
|
||||
|
||||
// CheckoutDefaultBranch checks out the default branch of a repository.
|
||||
func CheckoutDefaultBranch(repo *git.Repository, repoPath string) (plumbing.ReferenceName, error) {
|
||||
branch, err := GetDefaultBranch(repo, repoPath)
|
||||
if err != nil {
|
||||
return plumbing.ReferenceName(""), err
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return plumbing.ReferenceName(""), err
|
||||
}
|
||||
|
||||
checkOutOpts := &git.CheckoutOptions{
|
||||
Create: false,
|
||||
Force: true,
|
||||
Branch: branch,
|
||||
}
|
||||
|
||||
if err := worktree.Checkout(checkOutOpts); err != nil {
|
||||
logrus.Debugf("failed to check out %s in %s", branch, repoPath)
|
||||
return branch, err
|
||||
}
|
||||
|
||||
logrus.Debugf("successfully checked out %v in %s", branch, repoPath)
|
||||
|
||||
return branch, nil
|
||||
}
|
||||
|
@ -15,22 +15,32 @@ import (
|
||||
func Clone(dir, url string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
logrus.Debugf("%s does not exist, attempting to git clone from %s", dir, url)
|
||||
_, err := git.PlainClone(dir, false, &git.CloneOptions{URL: url, Tags: git.AllTags})
|
||||
|
||||
_, err := git.PlainClone(dir, false, &git.CloneOptions{
|
||||
URL: url,
|
||||
Tags: git.AllTags,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
|
||||
SingleBranch: true,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Debugf("cloning %s default branch failed, attempting from main branch", url)
|
||||
|
||||
_, err := git.PlainClone(dir, false, &git.CloneOptions{
|
||||
URL: url,
|
||||
Tags: git.AllTags,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
|
||||
SingleBranch: true,
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "authentication required") {
|
||||
name := filepath.Base(dir)
|
||||
return fmt.Errorf("unable to clone %s, does %s exist?", name, url)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("%s has been git cloned successfully", dir)
|
||||
} else {
|
||||
logrus.Debugf("%s already exists", dir)
|
||||
|
203
pkg/jsontable/jsontable.go
Normal file
203
pkg/jsontable/jsontable.go
Normal file
@ -0,0 +1,203 @@
|
||||
package jsontable
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
// A quick-and-dirty proxy/emulator of tablewriter to enable more easy machine readable output
|
||||
// - Does not strictly support types, just quoted or unquoted values
|
||||
// - Does not support nested values.
|
||||
// If a datalabel is set with SetDataLabel(true, "..."), that will be used as the key for teh data of the table,
|
||||
// otherwise if the caption is set with SetCaption(true, "..."), the data label will be set to the default of
|
||||
// "rows", otherwise the table will output as a JSON list.
|
||||
//
|
||||
// Proxys all actions through to the tablewriter except addrow and addbatch, which it does at render time
|
||||
//
|
||||
|
||||
type JSONTable struct {
|
||||
out io.Writer
|
||||
colsize int
|
||||
rows [][]string
|
||||
keys []string
|
||||
quoted []bool // hack to do output typing, quoted vs. unquoted
|
||||
hasDataLabel bool
|
||||
dataLabel string
|
||||
hasCaption bool
|
||||
caption string // the actual caption
|
||||
hasCaptionLabel bool
|
||||
captionLabel string // the key in the dictionary for the caption
|
||||
tbl *tablewriter.Table
|
||||
}
|
||||
|
||||
func writeChar(w io.Writer, c byte) {
|
||||
w.Write([]byte{c})
|
||||
|
||||
}
|
||||
|
||||
func NewJSONTable(writer io.Writer) *JSONTable {
|
||||
t := &JSONTable{
|
||||
out: writer,
|
||||
colsize: 0,
|
||||
rows: [][]string{},
|
||||
keys: []string{},
|
||||
quoted: []bool{},
|
||||
hasDataLabel: false,
|
||||
dataLabel: "rows",
|
||||
hasCaption: false,
|
||||
caption: "",
|
||||
hasCaptionLabel: false,
|
||||
captionLabel: "caption",
|
||||
tbl: tablewriter.NewWriter(writer),
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *JSONTable) NumLines() int {
|
||||
// JSON only but reflects a shared state.
|
||||
return len(t.rows)
|
||||
}
|
||||
|
||||
func (t *JSONTable) SetHeader(keys []string) {
|
||||
// Set the keys value which will assign each column to the keys.
|
||||
// Note that we'll ignore values that are beyond the length of the keys list
|
||||
t.colsize = len(keys)
|
||||
t.keys = []string{}
|
||||
for _, k := range keys {
|
||||
t.keys = append(t.keys, k)
|
||||
t.quoted = append(t.quoted, true)
|
||||
}
|
||||
t.tbl.SetHeader(keys)
|
||||
}
|
||||
|
||||
func (t *JSONTable) SetColumnQuoting(quoting []bool) {
|
||||
// Specify which columns are quoted or unquoted in output
|
||||
// JSON only
|
||||
for i := 0; i < t.colsize; i++ {
|
||||
t.quoted[i] = quoting[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (t *JSONTable) Append(row []string) {
|
||||
// We'll just append whatever to the rows list. If they fix the keys after appending rows, it'll work as
|
||||
// expected.
|
||||
// We should detect if the row is narrower than the key list tho.
|
||||
// JSON only (but we use the rows later when rendering a regular table)
|
||||
t.rows = append(t.rows, row)
|
||||
}
|
||||
|
||||
func (t *JSONTable) Render() {
|
||||
// Load the table with rows and render.
|
||||
// Proxy only
|
||||
for _, row := range t.rows {
|
||||
t.tbl.Append(row)
|
||||
}
|
||||
|
||||
t.tbl.Render()
|
||||
}
|
||||
|
||||
func (t *JSONTable) _JSONRenderInner() {
|
||||
// JSON only
|
||||
// Render the list of dictionaries to the writer.
|
||||
//// inner render loop
|
||||
writeChar(t.out, '[')
|
||||
for rowidx, row := range t.rows {
|
||||
if rowidx != 0 {
|
||||
writeChar(t.out, ',')
|
||||
}
|
||||
writeChar(t.out, '{')
|
||||
for keyidx, key := range t.keys {
|
||||
value := "nil"
|
||||
if keyidx < len(row) {
|
||||
value = row[keyidx]
|
||||
}
|
||||
if keyidx != 0 {
|
||||
writeChar(t.out, ',')
|
||||
}
|
||||
if t.quoted[keyidx] {
|
||||
fmt.Fprintf(t.out, "\"%s\":\"%s\"", key, value)
|
||||
} else {
|
||||
fmt.Fprintf(t.out, "\"%s\":%s", key, value)
|
||||
}
|
||||
}
|
||||
writeChar(t.out, '}')
|
||||
}
|
||||
writeChar(t.out, ']')
|
||||
|
||||
}
|
||||
|
||||
func (t *JSONTable) JSONRender() {
|
||||
// write JSON table to output
|
||||
// JSON only
|
||||
|
||||
if t.hasDataLabel || t.hasCaption {
|
||||
// dict mode
|
||||
writeChar(t.out, '{')
|
||||
|
||||
if t.hasCaption {
|
||||
fmt.Fprintf(t.out, "\"%s\":\"%s\",", t.captionLabel, t.caption)
|
||||
|
||||
}
|
||||
fmt.Fprintf(t.out, "\"%s\":", t.dataLabel)
|
||||
|
||||
}
|
||||
|
||||
// write list
|
||||
t._JSONRenderInner()
|
||||
|
||||
if t.hasDataLabel || t.hasCaption {
|
||||
// dict mode
|
||||
writeChar(t.out, '}')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *JSONTable) SetCaption(caption bool, captionText ...string) {
|
||||
t.hasCaption = caption
|
||||
if len(captionText) == 1 {
|
||||
t.caption = captionText[0]
|
||||
}
|
||||
t.tbl.SetCaption(caption, captionText...)
|
||||
}
|
||||
|
||||
func (t *JSONTable) SetCaptionLabel(captionLabel bool, captionLabelText ...string) {
|
||||
// JSON only
|
||||
t.hasCaptionLabel = captionLabel
|
||||
if len(captionLabelText) == 1 {
|
||||
t.captionLabel = captionLabelText[0]
|
||||
}
|
||||
}
|
||||
|
||||
func (t *JSONTable) SetDataLabel(dataLabel bool, dataLabelText ...string) {
|
||||
// JSON only
|
||||
t.hasDataLabel = dataLabel
|
||||
if len(dataLabelText) == 1 {
|
||||
t.dataLabel = dataLabelText[0]
|
||||
}
|
||||
}
|
||||
|
||||
func (t *JSONTable) AppendBulk(rows [][]string) {
|
||||
// JSON only but reflects shared state
|
||||
for _, row := range rows {
|
||||
t.Append(row)
|
||||
}
|
||||
}
|
||||
|
||||
// Stuff we should implement but we just proxy for now.
|
||||
func (t *JSONTable) SetAutoMergeCellsByColumnIndex(cols []int) {
|
||||
// FIXME
|
||||
t.tbl.SetAutoMergeCellsByColumnIndex(cols)
|
||||
}
|
||||
|
||||
func (t *JSONTable) SetAutoMergeCells(auto bool) {
|
||||
// FIXME
|
||||
t.tbl.SetAutoMergeCells(auto)
|
||||
}
|
||||
|
||||
// Stub functions
|
||||
func (t *JSONTable) SetAutoWrapText(auto bool) {
|
||||
t.tbl.SetAutoWrapText(auto)
|
||||
return
|
||||
}
|
83
pkg/jsontable/jsontable_test.go
Normal file
83
pkg/jsontable/jsontable_test.go
Normal file
@ -0,0 +1,83 @@
|
||||
package jsontable
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
var TestLine = []string{"1", "2"}
|
||||
var TestGroup = [][]string{{"1", "2", "3"}, {"a", "teohunteohu", "c", "d"}, {"☺", "☹"}}
|
||||
var TestKeys = []string{"key0", "key1", "key2"}
|
||||
|
||||
// test creation
|
||||
func TestNewTable(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
tbl := NewJSONTable(&b)
|
||||
if tbl.NumLines() != 0 {
|
||||
t.Fatalf("Something went weird when making table (should have 0 lines)")
|
||||
}
|
||||
}
|
||||
|
||||
// test adding things
|
||||
func TestTableAdd(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
tbl := NewJSONTable(&b)
|
||||
|
||||
tbl.Append(TestLine)
|
||||
if tbl.NumLines() != 1 {
|
||||
t.Fatalf("Appending a line does not result in a length of 1.")
|
||||
}
|
||||
|
||||
tbl.AppendBulk(TestGroup)
|
||||
numlines := tbl.NumLines()
|
||||
if numlines != (len(TestGroup) + 1) {
|
||||
t.Fatalf("Appending two lines does not result in a length of 4 (length is %d).", numlines)
|
||||
}
|
||||
}
|
||||
|
||||
// test JSON output is parsable
|
||||
func TestJsonParsable(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
tbl := NewJSONTable(&b)
|
||||
|
||||
tbl.AppendBulk(TestGroup)
|
||||
tbl.SetHeader(TestKeys)
|
||||
|
||||
tbl.JSONRender()
|
||||
|
||||
var son []map[string]interface{}
|
||||
|
||||
err := json.Unmarshal(b.Bytes(), &son)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Did not produce parsable JSON: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// test identical commands to a tablewriter and jsontable produce the same rendered output
|
||||
func TestTableWriter(t *testing.T) {
|
||||
var bjson bytes.Buffer
|
||||
var btable bytes.Buffer
|
||||
|
||||
tbl := NewJSONTable(&bjson)
|
||||
|
||||
tbl.AppendBulk(TestGroup)
|
||||
tbl.SetHeader(TestKeys)
|
||||
tbl.Render()
|
||||
|
||||
wtbl := tablewriter.NewWriter(&btable)
|
||||
|
||||
wtbl.AppendBulk(TestGroup)
|
||||
wtbl.SetHeader(TestKeys)
|
||||
wtbl.Render()
|
||||
|
||||
if bytes.Compare(bjson.Bytes(), btable.Bytes()) != 0 {
|
||||
t.Fatalf("JSON table and TableWriter produce non-identical outputs.\n%s\n%s", bjson.Bytes(), btable.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME test different output formats when captions etc. are added
|
@ -19,12 +19,40 @@ var Critical = "critical"
|
||||
|
||||
type LintFunction func(recipe.Recipe) (bool, error)
|
||||
|
||||
// SkipFunction determines whether the LintFunction is run or not. It should
|
||||
// not take the lint rule level into account because some rules are always an
|
||||
// error but may depend on some additional context of the recipe configuration.
|
||||
// This function aims to cover those additional cases.
|
||||
type SkipFunction func(recipe.Recipe) (bool, error)
|
||||
|
||||
// LintRule is a linting rule which helps a recipe maintainer avoid common
|
||||
// problems in their recipe configurations. We aim to highlight things that
|
||||
// might result in critical errors or hours lost in debugging obscure
|
||||
// Docker-isms. Humans make the final call on these rules, please raise an
|
||||
// issue if you disagree.
|
||||
type LintRule struct {
|
||||
Ref string
|
||||
Level string
|
||||
Description string
|
||||
HowToResolve string
|
||||
Function LintFunction
|
||||
Ref string // Reference of the linting rule
|
||||
Level string // Level of the warning
|
||||
Description string // Description of the issue
|
||||
HowToResolve string // Documentation for recipe maintainer
|
||||
Function LintFunction // Rule implementation
|
||||
SkipCondition SkipFunction // Whether or not to execute the lint rule
|
||||
}
|
||||
|
||||
// Skip implements the SkipFunction for the lint rule.
|
||||
func (l LintRule) Skip(recipe recipe.Recipe) bool {
|
||||
if l.SkipCondition != nil {
|
||||
ok, err := l.SkipCondition(recipe)
|
||||
if err != nil {
|
||||
logrus.Debugf("%s: skip condition: %s", l.Ref, err)
|
||||
}
|
||||
if ok {
|
||||
logrus.Debugf("skipping %s based on skip condition", l.Ref)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var LintRules = map[string][]LintRule{
|
||||
@ -78,6 +106,13 @@ var LintRules = map[string][]LintRule{
|
||||
HowToResolve: "fill out all the metadata",
|
||||
Function: LintMetadataFilledIn,
|
||||
},
|
||||
{
|
||||
Ref: "R013",
|
||||
Level: "warn",
|
||||
Description: "git.coopcloud.tech repo exists",
|
||||
HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...",
|
||||
Function: LintHasRecipeRepo,
|
||||
},
|
||||
},
|
||||
"error": {
|
||||
{
|
||||
@ -100,6 +135,7 @@ var LintRules = map[string][]LintRule{
|
||||
Description: "traefik routing enabled",
|
||||
HowToResolve: "include \"traefik.enable=true\" deploy label",
|
||||
Function: LintTraefikEnabled,
|
||||
SkipCondition: LintTraefikEnabledSkipCondition,
|
||||
},
|
||||
{
|
||||
Ref: "R011",
|
||||
@ -115,16 +151,12 @@ var LintRules = map[string][]LintRule{
|
||||
HowToResolve: "vendor config versions in an abra.sh",
|
||||
Function: LintAbraShVendors,
|
||||
},
|
||||
{
|
||||
Ref: "R013",
|
||||
Level: "error",
|
||||
Description: "git.coopcloud.tech repo exists",
|
||||
HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...",
|
||||
Function: LintHasRecipeRepo,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// LintForErrors lints specifically for errors and not other levels. This is
|
||||
// used in code paths such as "app deploy" to avoid nasty surprises but not for
|
||||
// the typical linting commands, which do handle other levels.
|
||||
func LintForErrors(recipe recipe.Recipe) error {
|
||||
logrus.Debugf("linting for critical errors in %s configs", recipe.Name)
|
||||
|
||||
@ -132,7 +164,12 @@ func LintForErrors(recipe recipe.Recipe) error {
|
||||
if level != "error" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, rule := range LintRules[level] {
|
||||
if rule.Skip(recipe) {
|
||||
continue
|
||||
}
|
||||
|
||||
ok, err := rule.Function(recipe)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -175,6 +212,24 @@ func LintAppService(recipe recipe.Recipe) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// LintTraefikEnabledSkipCondition signals a skip for this linting rule if it
|
||||
// confirms that there is no "DOMAIN=..." in the .env.sample configuration of
|
||||
// the recipe. This typically means that no domain is required to deploy and
|
||||
// therefore no matching traefik deploy label will be present.
|
||||
func LintTraefikEnabledSkipCondition(recipe recipe.Recipe) (bool, error) {
|
||||
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
|
||||
sampleEnv, err := config.ReadEnv(envSamplePath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Unable to discover .env.sample for %s", recipe.Name)
|
||||
}
|
||||
|
||||
if _, ok := sampleEnv["DOMAIN"]; !ok {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
|
||||
for _, service := range recipe.Config.Services {
|
||||
for label := range service.Deploy.Labels {
|
||||
|
@ -2,31 +2,36 @@ package recipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/compose"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/limit"
|
||||
"coopcloud.tech/abra/pkg/runtime"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/abra/pkg/web"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/go-git/go-git/v5"
|
||||
gitConfig "github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// RecipeCatalogueURL is the only current recipe catalogue available.
|
||||
const RecipeCatalogueURL = "https://apps.coopcloud.tech"
|
||||
const RecipeCatalogueURL = "https://recipes.coopcloud.tech/recipes.json"
|
||||
|
||||
// ReposMetadataURL is the recipe repository metadata
|
||||
const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos"
|
||||
@ -39,7 +44,6 @@ type service = string
|
||||
|
||||
// ServiceMeta represents meta info associated with a service.
|
||||
type ServiceMeta struct {
|
||||
Digest string `json:"digest"`
|
||||
Image string `json:"image"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
@ -203,8 +207,8 @@ func (r Recipe) Tags() ([]string, error) {
|
||||
}
|
||||
|
||||
// Get retrieves a recipe.
|
||||
func Get(recipeName string) (Recipe, error) {
|
||||
if err := EnsureExists(recipeName); err != nil {
|
||||
func Get(recipeName string, conf *runtime.Config) (Recipe, error) {
|
||||
if err := EnsureExists(recipeName, conf); err != nil {
|
||||
return Recipe{}, err
|
||||
}
|
||||
|
||||
@ -230,10 +234,14 @@ func Get(recipeName string) (Recipe, error) {
|
||||
return Recipe{}, err
|
||||
}
|
||||
|
||||
meta, err := GetRecipeMeta(recipeName)
|
||||
meta, err := GetRecipeMeta(recipeName, conf)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
meta = RecipeMeta{}
|
||||
} else {
|
||||
return Recipe{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return Recipe{
|
||||
Name: recipeName,
|
||||
@ -243,7 +251,11 @@ func Get(recipeName string) (Recipe, error) {
|
||||
}
|
||||
|
||||
// EnsureExists ensures that a recipe is locally cloned
|
||||
func EnsureExists(recipeName string) error {
|
||||
func EnsureExists(recipeName string, conf *runtime.Config) error {
|
||||
if !conf.EnsureRecipeExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
|
||||
|
||||
if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
|
||||
@ -303,8 +315,7 @@ func EnsureVersion(recipeName, version string) error {
|
||||
logrus.Debugf("read %s as tags for recipe %s", strings.Join(parsedTags, ", "), recipeName)
|
||||
|
||||
if tagRef.String() == "" {
|
||||
logrus.Warnf("no published release discovered for %s", recipeName)
|
||||
return nil
|
||||
return fmt.Errorf("no published release discovered for %s", recipeName)
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
@ -327,7 +338,7 @@ func EnsureVersion(recipeName, version string) error {
|
||||
}
|
||||
|
||||
// EnsureLatest makes sure the latest commit is checked out for a local recipe repository
|
||||
func EnsureLatest(recipeName string) error {
|
||||
func EnsureLatest(recipeName string, conf *runtime.Config) error {
|
||||
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
|
||||
|
||||
isClean, err := gitPkg.IsClean(recipeDir)
|
||||
@ -355,11 +366,21 @@ func EnsureLatest(recipeName string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
branch, err := gitPkg.GetCurrentBranch(repo)
|
||||
meta, err := GetRecipeMeta(recipeName, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var branch plumbing.ReferenceName
|
||||
if meta.DefaultBranch != "" {
|
||||
branch = plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", meta.DefaultBranch))
|
||||
} else {
|
||||
branch, err = gitPkg.GetDefaultBranch(repo, recipeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
checkOutOpts := &git.CheckoutOptions{
|
||||
Create: false,
|
||||
Force: true,
|
||||
@ -459,6 +480,9 @@ func GetRecipeFeaturesAndCategory(recipeName string) (Features, string, error) {
|
||||
"\n")
|
||||
|
||||
for _, val := range readmeLines {
|
||||
if strings.Contains(val, "**Status**") {
|
||||
feat.Status, _ = strconv.Atoi(strings.TrimSpace(strings.Split(strings.TrimPrefix(val, "* **Status**:"), ",")[0]))
|
||||
}
|
||||
if strings.Contains(val, "**Category**") {
|
||||
category = strings.TrimSpace(
|
||||
strings.TrimPrefix(val, "* **Category**:"),
|
||||
@ -564,21 +588,22 @@ func EnsureUpToDate(recipeName string) error {
|
||||
|
||||
isClean, err := gitPkg.IsClean(recipeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err)
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
return fmt.Errorf("%s has locally unstaged changes", recipeName)
|
||||
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
|
||||
return fmt.Errorf(msg, recipeName, recipeDir)
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("unable to open %s: %s", recipeDir, err)
|
||||
}
|
||||
|
||||
remotes, err := repo.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("unable to read remotes in %s: %s", recipeDir, err)
|
||||
}
|
||||
|
||||
if len(remotes) == 0 {
|
||||
@ -588,22 +613,35 @@ func EnsureUpToDate(recipeName string) error {
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("unable to open git work tree in %s: %s", recipeDir, err)
|
||||
}
|
||||
|
||||
branch, err := CheckoutDefaultBranch(repo, recipeName)
|
||||
branch, err := gitPkg.CheckoutDefaultBranch(repo, recipeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("unable to check out default branch in %s: %s", recipeDir, err)
|
||||
}
|
||||
|
||||
fetchOpts := &git.FetchOptions{
|
||||
Tags: git.AllTags,
|
||||
RefSpecs: []gitConfig.RefSpec{
|
||||
gitConfig.RefSpec(fmt.Sprintf("%s:%s", branch, branch)),
|
||||
},
|
||||
}
|
||||
if err := repo.Fetch(fetchOpts); err != nil {
|
||||
if !strings.Contains(err.Error(), "already up-to-date") {
|
||||
return fmt.Errorf("unable to fetch tags in %s: %s", recipeDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
opts := &git.PullOptions{
|
||||
Force: true,
|
||||
ReferenceName: branch,
|
||||
SingleBranch: true,
|
||||
}
|
||||
|
||||
if err := worktree.Pull(opts); err != nil {
|
||||
if !strings.Contains(err.Error(), "already up-to-date") {
|
||||
return err
|
||||
return fmt.Errorf("unable to git pull in %s: %s", recipeDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -612,49 +650,12 @@ func EnsureUpToDate(recipeName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDefaultBranch(repo *git.Repository, recipeName string) (plumbing.ReferenceName, error) {
|
||||
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
|
||||
|
||||
branch := "master"
|
||||
if _, err := repo.Branch("master"); err != nil {
|
||||
if _, err := repo.Branch("main"); err != nil {
|
||||
logrus.Debugf("failed to select branch in %s", recipeDir)
|
||||
return "", err
|
||||
}
|
||||
branch = "main"
|
||||
}
|
||||
|
||||
return plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", branch)), nil
|
||||
type CatalogueOfflineError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func CheckoutDefaultBranch(repo *git.Repository, recipeName string) (plumbing.ReferenceName, error) {
|
||||
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
|
||||
|
||||
branch, err := GetDefaultBranch(repo, recipeName)
|
||||
if err != nil {
|
||||
return plumbing.ReferenceName(""), err
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return plumbing.ReferenceName(""), err
|
||||
}
|
||||
|
||||
checkOutOpts := &git.CheckoutOptions{
|
||||
Create: false,
|
||||
Force: true,
|
||||
Branch: branch,
|
||||
}
|
||||
|
||||
if err := worktree.Checkout(checkOutOpts); err != nil {
|
||||
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
|
||||
logrus.Debugf("failed to check out %s in %s", branch, recipeDir)
|
||||
return branch, err
|
||||
}
|
||||
|
||||
logrus.Debugf("successfully checked out %v in %s", branch, recipeDir)
|
||||
|
||||
return branch, nil
|
||||
func (e *CatalogueOfflineError) Error() string {
|
||||
return fmt.Sprintf("catalogue offline: %s", e.msg)
|
||||
}
|
||||
|
||||
// recipeCatalogueFSIsLatest checks whether the recipe catalogue stored locally
|
||||
@ -663,7 +664,7 @@ func recipeCatalogueFSIsLatest() (bool, error) {
|
||||
httpClient := web.NewHTTPRetryClient()
|
||||
res, err := httpClient.Head(RecipeCatalogueURL)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, &CatalogueOfflineError{err.Error()}
|
||||
}
|
||||
|
||||
lastModified := res.Header["Last-Modified"][0]
|
||||
@ -689,7 +690,7 @@ func recipeCatalogueFSIsLatest() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
logrus.Debug("file system cached recipe catalogue is now up-to-date")
|
||||
logrus.Debug("file system cached recipe catalogue is up-to-date")
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@ -698,24 +699,29 @@ func recipeCatalogueFSIsLatest() (bool, error) {
|
||||
func ReadRecipeCatalogue() (RecipeCatalogue, error) {
|
||||
recipes := make(RecipeCatalogue)
|
||||
|
||||
if err := EnsureCatalogue(); err != nil {
|
||||
if err := catalogue.EnsureCatalogue(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recipeFSIsLatest, err := recipeCatalogueFSIsLatest()
|
||||
if err != nil {
|
||||
var offlineErr *CatalogueOfflineError
|
||||
if errors.As(err, &offlineErr) {
|
||||
logrus.Error(err)
|
||||
logrus.Error("unable to retrieve catalogue from internet, using local copy.")
|
||||
recipeFSIsLatest = true
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !recipeFSIsLatest {
|
||||
logrus.Debugf("reading recipe catalogue from web to get latest")
|
||||
if err := readRecipeCatalogueWeb(&recipes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return recipes, nil
|
||||
}
|
||||
|
||||
logrus.Debugf("reading recipe catalogue from file system cache to get latest")
|
||||
if err := readRecipeCatalogueFS(&recipes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -789,7 +795,7 @@ func VersionsOfService(recipe, serviceName string) ([]string, error) {
|
||||
}
|
||||
|
||||
// GetRecipeMeta retrieves the recipe metadata from the recipe catalogue.
|
||||
func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
|
||||
func GetRecipeMeta(recipeName string, conf *runtime.Config) (RecipeMeta, error) {
|
||||
catl, err := ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
return RecipeMeta{}, err
|
||||
@ -797,11 +803,10 @@ func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
|
||||
|
||||
recipeMeta, ok := catl[recipeName]
|
||||
if !ok {
|
||||
err := fmt.Errorf("recipe %s does not exist?", recipeName)
|
||||
return RecipeMeta{}, err
|
||||
return RecipeMeta{}, fmt.Errorf("recipe %s does not exist?", recipeName)
|
||||
}
|
||||
|
||||
if err := EnsureExists(recipeName); err != nil {
|
||||
if err := EnsureExists(recipeName, conf); err != nil {
|
||||
return RecipeMeta{}, err
|
||||
}
|
||||
|
||||
@ -923,8 +928,9 @@ func ReadReposMetadata() (RepoCatalogue, error) {
|
||||
}
|
||||
|
||||
// GetRecipeVersions retrieves all recipe versions.
|
||||
func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (RecipeVersions, error) {
|
||||
func GetRecipeVersions(recipeName string, opts ...runtime.Option) (RecipeVersions, error) {
|
||||
versions := RecipeVersions{}
|
||||
conf := runtime.New(opts...)
|
||||
|
||||
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
|
||||
|
||||
@ -937,7 +943,7 @@ func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (R
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
return versions, err
|
||||
}
|
||||
|
||||
gitTags, err := repo.Tags()
|
||||
@ -962,17 +968,11 @@ func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (R
|
||||
|
||||
logrus.Debugf("successfully checked out %s in %s", ref.Name(), recipeDir)
|
||||
|
||||
recipe, err := Get(recipeName)
|
||||
recipe, err := Get(recipeName, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cl, err := client.New("default") // only required for docker.io registry calls
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
queryCache := make(map[reference.Named]string)
|
||||
versionMeta := make(map[string]ServiceMeta)
|
||||
for _, service := range recipe.Config.Services {
|
||||
|
||||
@ -994,27 +994,9 @@ func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (R
|
||||
continue
|
||||
}
|
||||
|
||||
var exists bool
|
||||
var digest string
|
||||
if digest, exists = queryCache[img]; !exists {
|
||||
logrus.Debugf("looking up image: %s from %s", img, path)
|
||||
var err error
|
||||
digest, err = client.GetTagDigest(cl, img, registryUsername, registryPassword)
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
continue
|
||||
}
|
||||
logrus.Debugf("queried for image: %s, tag: %s, digest: %s", path, tag, digest)
|
||||
queryCache[img] = digest
|
||||
logrus.Debugf("cached image: %s, tag: %s, digest: %s", path, tag, digest)
|
||||
} else {
|
||||
logrus.Debugf("reading image: %s, tag: %s, digest: %s from cache", path, tag, digest)
|
||||
}
|
||||
|
||||
versionMeta[service.Name] = ServiceMeta{
|
||||
Digest: digest,
|
||||
Image: path,
|
||||
Tag: img.(reference.NamedTagged).Tag(),
|
||||
Tag: tag,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1025,7 +1007,7 @@ func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (R
|
||||
return versions, err
|
||||
}
|
||||
|
||||
_, err = CheckoutDefaultBranch(repo, recipeName)
|
||||
_, err = gitPkg.CheckoutDefaultBranch(repo, recipeDir)
|
||||
if err != nil {
|
||||
return versions, err
|
||||
}
|
||||
@ -1050,16 +1032,52 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
// EnsureCatalogue ensures that the catalogue is cloned locally & present.
|
||||
func EnsureCatalogue() error {
|
||||
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
|
||||
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
|
||||
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes")
|
||||
if err := gitPkg.Clone(catalogueDir, url); err != nil {
|
||||
return err
|
||||
// UpdateRepositories clones and updates all recipe repositories locally.
|
||||
func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
|
||||
var barLength int
|
||||
if recipeName != "" {
|
||||
barLength = 1
|
||||
} else {
|
||||
barLength = len(repos)
|
||||
}
|
||||
|
||||
logrus.Debugf("cloned catalogue repository to %s", catalogueDir)
|
||||
cloneLimiter := limit.New(10)
|
||||
|
||||
retrieveBar := formatter.CreateProgressbar(barLength, "ensuring recipes are cloned & up-to-date...")
|
||||
ch := make(chan string, barLength)
|
||||
for _, repoMeta := range repos {
|
||||
go func(rm RepoMeta) {
|
||||
cloneLimiter.Begin()
|
||||
defer cloneLimiter.End()
|
||||
|
||||
if recipeName != "" && recipeName != rm.Name {
|
||||
ch <- rm.Name
|
||||
retrieveBar.Add(1)
|
||||
return
|
||||
}
|
||||
if _, exists := catalogue.CatalogueSkipList[rm.Name]; exists {
|
||||
ch <- rm.Name
|
||||
retrieveBar.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
recipeDir := path.Join(config.RECIPES_DIR, rm.Name)
|
||||
|
||||
if err := gitPkg.Clone(recipeDir, rm.CloneURL); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := EnsureUpToDate(rm.Name); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
ch <- rm.Name
|
||||
retrieveBar.Add(1)
|
||||
}(repoMeta)
|
||||
}
|
||||
|
||||
for range repos {
|
||||
<-ch // wait for everything
|
||||
}
|
||||
|
||||
return nil
|
||||
|
58
pkg/runtime/runtime.go
Normal file
58
pkg/runtime/runtime.go
Normal file
@ -0,0 +1,58 @@
|
||||
package runtime
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
// Config is an internal configuration modifier. It can be instantiated on a
|
||||
// command call and can be changed on the fly to help make decisions further
|
||||
// down in the internals, e.g. whether or not to clone the recipe locally or
|
||||
// not.
|
||||
type Config struct {
|
||||
EnsureRecipeExists bool // ensure that the recipe is cloned locally
|
||||
EnsureRecipeLatest bool // ensure the local recipe has latest changes
|
||||
}
|
||||
|
||||
// Option modified a Config.
|
||||
type Option func(c *Config)
|
||||
|
||||
// New instantiates a new Config.
|
||||
func New(opts ...Option) *Config {
|
||||
conf := &Config{
|
||||
EnsureRecipeExists: true,
|
||||
}
|
||||
|
||||
for _, optFunc := range opts {
|
||||
optFunc(conf)
|
||||
}
|
||||
|
||||
return conf
|
||||
}
|
||||
|
||||
// WithEnsureRecipeExists determines whether or not we should be cloning the
|
||||
// local recipe or not. This can be useful for being more adaptable to offline
|
||||
// scenarios.
|
||||
func WithEnsureRecipeExists(ensureRecipeExists bool) Option {
|
||||
return func(c *Config) {
|
||||
if ensureRecipeExists {
|
||||
logrus.Debugf("runtime config: EnsureRecipeExists = %v, ensuring recipes are cloned", ensureRecipeExists)
|
||||
} else {
|
||||
logrus.Debugf("runtime config: EnsureRecipeExists = %v, not cloning recipes", ensureRecipeExists)
|
||||
}
|
||||
|
||||
c.EnsureRecipeExists = ensureRecipeExists
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnsureRecipeLatest determines whether we should update the local recipes
|
||||
// remotely via Git. This can be useful when e.g. ensuring we have the latest
|
||||
// changes before making new ones.
|
||||
func WithEnsureRecipeLatest(ensureRecipeLatest bool) Option {
|
||||
return func(c *Config) {
|
||||
if ensureRecipeLatest {
|
||||
logrus.Debugf("runtime config: EnsureRecipeLatest = %v, ensuring recipes have latest changes", ensureRecipeLatest)
|
||||
} else {
|
||||
logrus.Debugf("runtime config: EnsureRecipeLatest = %v, leaving recipes alone", ensureRecipeLatest)
|
||||
}
|
||||
|
||||
c.EnsureRecipeLatest = ensureRecipeLatest
|
||||
}
|
||||
}
|
@ -8,10 +8,12 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"github.com/schultz-is/passgen"
|
||||
"github.com/decentral1se/passgen"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -116,27 +118,36 @@ func ParseSecretEnvVarValue(secret string) (secretValue, error) {
|
||||
}
|
||||
|
||||
// GenerateSecrets generates secrets locally and sends them to a remote server for storage.
|
||||
func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (map[string]string, error) {
|
||||
func GenerateSecrets(cl *dockerClient.Client, secretEnvVars map[string]string, appName, server string) (map[string]string, error) {
|
||||
secrets := make(map[string]string)
|
||||
|
||||
var mutex sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
ch := make(chan error, len(secretEnvVars))
|
||||
for secretEnvVar := range secretEnvVars {
|
||||
wg.Add(1)
|
||||
|
||||
go func(s string) {
|
||||
defer wg.Done()
|
||||
|
||||
secretName := ParseSecretEnvVarName(s)
|
||||
secretValue, err := ParseSecretEnvVarValue(secretEnvVars[s])
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secretValue.Version)
|
||||
logrus.Debugf("attempting to generate and store %s on %s", secretRemoteName, server)
|
||||
|
||||
if secretValue.Length > 0 {
|
||||
passwords, err := GeneratePasswords(1, uint(secretValue.Length))
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
if err := client.StoreSecret(secretRemoteName, passwords[0], server); err != nil {
|
||||
|
||||
if err := client.StoreSecret(cl, secretRemoteName, passwords[0], server); err != nil {
|
||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
|
||||
ch <- nil
|
||||
@ -145,6 +156,9 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
secrets[secretName] = passwords[0]
|
||||
} else {
|
||||
passphrases, err := GeneratePassphrases(1)
|
||||
@ -152,7 +166,8 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
if err := client.StoreSecret(secretRemoteName, passphrases[0], server); err != nil {
|
||||
|
||||
if err := client.StoreSecret(cl, secretRemoteName, passphrases[0], server); err != nil {
|
||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
|
||||
ch <- nil
|
||||
@ -161,12 +176,17 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
secrets[secretName] = passphrases[0]
|
||||
}
|
||||
ch <- nil
|
||||
}(secretEnvVar)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for range secretEnvVars {
|
||||
err := <-ch
|
||||
if err != nil {
|
||||
|
559
pkg/ssh/ssh.go
559
pkg/ssh/ssh.go
@ -1,37 +1,13 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/context"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
dockerSSHPkg "github.com/docker/cli/cli/connhelper/ssh"
|
||||
sshPkg "github.com/gliderlabs/ssh"
|
||||
"github.com/kevinburke/ssh_config"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
"golang.org/x/crypto/ssh/knownhosts"
|
||||
)
|
||||
|
||||
var KnownHostsPath = filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")
|
||||
|
||||
type Client struct {
|
||||
SSHClient *ssh.Client
|
||||
}
|
||||
|
||||
// HostConfig is a SSH host config.
|
||||
type HostConfig struct {
|
||||
Host string
|
||||
@ -40,509 +16,70 @@ type HostConfig struct {
|
||||
User string
|
||||
}
|
||||
|
||||
// Exec cmd on the remote host and return stderr and stdout
|
||||
func (c *Client) Exec(cmd string) ([]byte, error) {
|
||||
session, err := c.SSHClient.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
return session.CombinedOutput(cmd)
|
||||
}
|
||||
|
||||
// Close the underlying SSH connection
|
||||
func (c *Client) Close() error {
|
||||
return c.SSHClient.Close()
|
||||
}
|
||||
|
||||
// New creates a new SSH client connection.
|
||||
func New(domainName, sshAuth, username, port string) (*Client, error) {
|
||||
var client *Client
|
||||
|
||||
ctxConnDetails, err := GetContextConnDetails(domainName)
|
||||
if err != nil {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
if sshAuth == "identity-file" {
|
||||
var err error
|
||||
client, err = connectWithAgentTimeout(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
5*time.Second,
|
||||
// String presents a human friendly output for the HostConfig.
|
||||
func (h HostConfig) String() string {
|
||||
return fmt.Sprintf(
|
||||
"{host: %s, username: %s, port: %s, identityfile: %s}",
|
||||
h.Host,
|
||||
h.User,
|
||||
h.Port,
|
||||
h.IdentityFile,
|
||||
)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
} else {
|
||||
password := ""
|
||||
prompt := &survey.Password{
|
||||
Message: "SSH password?",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &password); err != nil {
|
||||
return client, err
|
||||
}
|
||||
|
||||
var err error
|
||||
client, err = connectWithPasswordTimeout(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
password,
|
||||
5*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// sudoWriter supports sudo command handling
|
||||
type sudoWriter struct {
|
||||
b bytes.Buffer
|
||||
pw string
|
||||
stdin io.Writer
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
// Write satisfies the write interface for sudoWriter
|
||||
func (w *sudoWriter) Write(p []byte) (int, error) {
|
||||
if strings.Contains(string(p), "sudo_password") {
|
||||
w.stdin.Write([]byte(w.pw + "\n"))
|
||||
w.pw = ""
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
|
||||
return w.b.Write(p)
|
||||
}
|
||||
|
||||
// RunSudoCmd runs SSH commands and streams output
|
||||
func RunSudoCmd(cmd, passwd string, cl *Client) error {
|
||||
session, err := cl.SSHClient.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
sudoCmd := fmt.Sprintf("SSH_ASKPASS=/usr/bin/ssh-askpass; sudo -p sudo_password -S %s", cmd)
|
||||
|
||||
w := &sudoWriter{pw: passwd}
|
||||
w.stdin, err = session.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.Stdout = w
|
||||
session.Stderr = w
|
||||
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 0,
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
|
||||
err = session.RequestPty("xterm", 80, 40, modes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := session.Run(sudoCmd); err != nil {
|
||||
return fmt.Errorf("%s", string(w.b.Bytes()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureKnowHostsFiles ensures that ~/.ssh/known_hosts is created
|
||||
func EnsureKnowHostsFiles() error {
|
||||
if _, err := os.Stat(KnownHostsPath); os.IsNotExist(err) {
|
||||
logrus.Debugf("missing %s, creating now", KnownHostsPath)
|
||||
file, err := os.OpenFile(KnownHostsPath, os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetHostKey checks if a host key is registered in the ~/.ssh/known_hosts file
|
||||
func GetHostKey(hostname string) (bool, sshPkg.PublicKey, error) {
|
||||
var hostKey sshPkg.PublicKey
|
||||
|
||||
ctxConnDetails, err := GetContextConnDetails(hostname)
|
||||
if err != nil {
|
||||
return false, hostKey, err
|
||||
}
|
||||
|
||||
if err := EnsureKnowHostsFiles(); err != nil {
|
||||
return false, hostKey, err
|
||||
}
|
||||
|
||||
file, err := os.Open(KnownHostsPath)
|
||||
if err != nil {
|
||||
return false, hostKey, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Split(scanner.Text(), " ")
|
||||
if len(fields) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
hostnameAndPort := fmt.Sprintf("%s:%s", ctxConnDetails.Host, ctxConnDetails.Port)
|
||||
hashed := knownhosts.Normalize(hostnameAndPort)
|
||||
|
||||
if strings.Contains(fields[0], hashed) {
|
||||
var err error
|
||||
hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes())
|
||||
if err != nil {
|
||||
return false, hostKey, fmt.Errorf("error parsing server SSH host key %q: %v", fields[2], err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hostKey != nil {
|
||||
logrus.Debugf("server SSH host key present in ~/.ssh/known_hosts for %s", hostname)
|
||||
return true, hostKey, nil
|
||||
}
|
||||
|
||||
return false, hostKey, nil
|
||||
}
|
||||
|
||||
// InsertHostKey adds a new host key to the ~/.ssh/known_hosts file
|
||||
func InsertHostKey(hostname string, remote net.Addr, pubKey ssh.PublicKey) error {
|
||||
file, err := os.OpenFile(KnownHostsPath, os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hashedHostname := knownhosts.Normalize(hostname)
|
||||
lineHostname := knownhosts.Line([]string{hashedHostname}, pubKey)
|
||||
_, err = file.WriteString(fmt.Sprintf("%s\n", lineHostname))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashedRemote := knownhosts.Normalize(remote.String())
|
||||
lineRemote := knownhosts.Line([]string{hashedRemote}, pubKey)
|
||||
_, err = file.WriteString(fmt.Sprintf("%s\n", lineRemote))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("SSH host key generated: %s", lineHostname)
|
||||
logrus.Debugf("SSH host key generated: %s", lineRemote)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HostKeyAddCallback ensures server ssh host keys are handled
|
||||
func HostKeyAddCallback(hostnameAndPort string, remote net.Addr, pubKey ssh.PublicKey) error {
|
||||
exists, _, err := GetHostKey(hostnameAndPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
hostname := strings.Split(hostnameAndPort, ":")[0]
|
||||
logrus.Debugf("server SSH host key found for %s", hostname)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !exists {
|
||||
hostname := strings.Split(hostnameAndPort, ":")[0]
|
||||
parsedPubKey := FingerprintSHA256(pubKey)
|
||||
|
||||
fmt.Printf(fmt.Sprintf(`
|
||||
You are attempting to make an SSH connection to a server but there is no entry
|
||||
in your ~/.ssh/known_hosts file which confirms that you have already validated
|
||||
that this is indeed the server you want to connect to. Please take a moment to
|
||||
validate the following SSH host key, it is important.
|
||||
|
||||
Host: %s
|
||||
Fingerprint: %s
|
||||
|
||||
If this is confusing to you, you can read the article below and learn how to
|
||||
validate this fingerprint safely. Thanks to the comrades at cyberia.club for
|
||||
writing this extensive guide <3
|
||||
|
||||
https://sequentialread.com/understanding-the-secure-shell-protocol-ssh/
|
||||
|
||||
`, hostname, parsedPubKey))
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "are you sure you trust this host key?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
logrus.Debugf("attempting to insert server SSH host key for %s, %s", hostnameAndPort, remote)
|
||||
|
||||
if err := InsertHostKey(hostnameAndPort, remote, pubKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("successfully added server SSH host key for %s", hostname)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// connect makes the SSH connection
|
||||
func connect(username, host, port string, authMethod ssh.AuthMethod, timeout time.Duration) (*Client, error) {
|
||||
config := &ssh.ClientConfig{
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{authMethod},
|
||||
HostKeyCallback: HostKeyAddCallback, // the main reason why we fork
|
||||
}
|
||||
|
||||
hostnameAndPort := fmt.Sprintf("%s:%s", host, port)
|
||||
|
||||
logrus.Debugf("tcp dialing %s", hostnameAndPort)
|
||||
|
||||
var conn net.Conn
|
||||
var err error
|
||||
conn, err = net.DialTimeout("tcp", hostnameAndPort, timeout)
|
||||
if err != nil {
|
||||
logrus.Debugf("tcp dialing %s failed, trying via ~/.ssh/config", hostnameAndPort)
|
||||
hostConfig, err := GetHostConfig(host, username, port, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err = net.DialTimeout("tcp", fmt.Sprintf("%s:%s", hostConfig.Host, hostConfig.Port), timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sshConn, chans, reqs, err := ssh.NewClientConn(conn, hostnameAndPort, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := ssh.NewClient(sshConn, chans, reqs)
|
||||
c := &Client{SSHClient: client}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func connectWithAgentTimeout(host, username, port string, timeout time.Duration) (*Client, error) {
|
||||
logrus.Debugf("using ssh-agent to make an SSH connection for %s", host)
|
||||
|
||||
sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agentCl := agent.NewClient(sshAgent)
|
||||
authMethod := ssh.PublicKeysCallback(agentCl.Signers)
|
||||
|
||||
loadedKeys, err := agentCl.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var convertedKeys []string
|
||||
for _, key := range loadedKeys {
|
||||
convertedKeys = append(convertedKeys, key.String())
|
||||
}
|
||||
|
||||
if len(convertedKeys) > 0 {
|
||||
logrus.Debugf("ssh-agent has these keys loaded: %s", strings.Join(convertedKeys, ","))
|
||||
} else {
|
||||
logrus.Debug("ssh-agent has no keys loaded")
|
||||
}
|
||||
|
||||
return connect(username, host, port, authMethod, timeout)
|
||||
}
|
||||
|
||||
func connectWithPasswordTimeout(host, username, port, pass string, timeout time.Duration) (*Client, error) {
|
||||
authMethod := ssh.Password(pass)
|
||||
|
||||
return connect(username, host, port, authMethod, timeout)
|
||||
}
|
||||
|
||||
// EnsureHostKey ensures that a host key trusted and added to the ~/.ssh/known_hosts file
|
||||
func EnsureHostKey(hostname string) error {
|
||||
if hostname == "default" || hostname == "local" {
|
||||
logrus.Debugf("not checking server SSH host key against local/default target")
|
||||
return nil
|
||||
}
|
||||
|
||||
exists, _, err := GetHostKey(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctxConnDetails, err := GetContextConnDetails(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = connectWithAgentTimeout(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
5*time.Second,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FingerprintSHA256 generates the SHA256 fingerprint for a server SSH host key
|
||||
func FingerprintSHA256(key ssh.PublicKey) string {
|
||||
hash := sha256.Sum256(key.Marshal())
|
||||
b64hash := base64.StdEncoding.EncodeToString(hash[:])
|
||||
trimmed := strings.TrimRight(b64hash, "=")
|
||||
return fmt.Sprintf("SHA256:%s", trimmed)
|
||||
}
|
||||
|
||||
// GetContextConnDetails retrieves SSH connection details from a docker context endpoint
|
||||
func GetContextConnDetails(serverName string) (*dockerSSHPkg.Spec, error) {
|
||||
dockerContextStore := context.NewDefaultDockerContextStore()
|
||||
contexts, err := dockerContextStore.Store.List()
|
||||
if err != nil {
|
||||
return &dockerSSHPkg.Spec{}, err
|
||||
}
|
||||
|
||||
if strings.Contains(serverName, ":") {
|
||||
serverName = strings.Split(serverName, ":")[0]
|
||||
}
|
||||
|
||||
for _, ctx := range contexts {
|
||||
endpoint, err := context.GetContextEndpoint(ctx)
|
||||
if err != nil && strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, we can continue safely
|
||||
continue
|
||||
}
|
||||
if ctx.Name == serverName {
|
||||
ctxConnDetails, err := dockerSSHPkg.ParseURL(endpoint)
|
||||
if err != nil {
|
||||
return &dockerSSHPkg.Spec{}, err
|
||||
}
|
||||
logrus.Debugf("found context connection details %v for %s", ctxConnDetails, serverName)
|
||||
return ctxConnDetails, nil
|
||||
}
|
||||
}
|
||||
|
||||
hostConfig, err := GetHostConfig(serverName, "", "", false)
|
||||
if err != nil {
|
||||
return &dockerSSHPkg.Spec{}, err
|
||||
}
|
||||
|
||||
logrus.Debugf("couldn't find a docker context matching %s", serverName)
|
||||
logrus.Debugf("searching ~/.ssh/config for a Host entry for %s", serverName)
|
||||
|
||||
connDetails := &dockerSSHPkg.Spec{
|
||||
Host: hostConfig.Host,
|
||||
User: hostConfig.User,
|
||||
Port: hostConfig.Port,
|
||||
}
|
||||
|
||||
logrus.Debugf("using %v from ~/.ssh/config for connection details", connDetails)
|
||||
|
||||
return connDetails, nil
|
||||
}
|
||||
|
||||
// GetHostConfig retrieves a ~/.ssh/config config for a host.
|
||||
func GetHostConfig(hostname, username, port string, override bool) (HostConfig, error) {
|
||||
// GetHostConfig retrieves a ~/.ssh/config config for a host using /usr/bin/ssh
|
||||
// directly. We therefore maintain consistent interop with this standard
|
||||
// tooling. This is useful because SSH confuses a lot of people and having to
|
||||
// learn how two tools (`ssh` and `abra`) handle SSH connection details instead
|
||||
// of one (just `ssh`) is Not Cool. Here's to less bug reports on this topic!
|
||||
func GetHostConfig(hostname string) (HostConfig, error) {
|
||||
var hostConfig HostConfig
|
||||
|
||||
if hostname == "" || override {
|
||||
if sshHost := ssh_config.Get(hostname, "Hostname"); sshHost != "" {
|
||||
hostname = sshHost
|
||||
}
|
||||
}
|
||||
|
||||
if username == "" || override {
|
||||
if sshUser := ssh_config.Get(hostname, "User"); sshUser != "" {
|
||||
username = sshUser
|
||||
} else {
|
||||
systemUser, err := user.Current()
|
||||
out, err := exec.Command("ssh", "-G", hostname).Output()
|
||||
if err != nil {
|
||||
return hostConfig, err
|
||||
}
|
||||
username = systemUser.Username
|
||||
}
|
||||
}
|
||||
|
||||
if port == "" || override {
|
||||
if sshPort := ssh_config.Get(hostname, "Port"); sshPort != "" {
|
||||
// skip override probably correct port with dummy default value from
|
||||
// ssh_config which is 22. only when the original port number is empty
|
||||
// should we try this default. this might not cover all cases
|
||||
// unfortunately.
|
||||
if port != "" && sshPort != "22" {
|
||||
port = sshPort
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
entries := strings.Split(line, " ")
|
||||
for idx, entry := range entries {
|
||||
if entry == "hostname" {
|
||||
hostConfig.Host = entries[idx+1]
|
||||
}
|
||||
if entry == "user" {
|
||||
hostConfig.User = entries[idx+1]
|
||||
}
|
||||
if entry == "port" {
|
||||
hostConfig.Port = entries[idx+1]
|
||||
}
|
||||
if entry == "identityfile" {
|
||||
if hostConfig.IdentityFile == "" {
|
||||
hostConfig.IdentityFile = entries[idx+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if idf := ssh_config.Get(hostname, "IdentityFile"); idf != "" && idf != "~/.ssh/identity" {
|
||||
var err error
|
||||
idf, err = identityFileAbsPath(idf)
|
||||
if err != nil {
|
||||
return hostConfig, err
|
||||
}
|
||||
hostConfig.IdentityFile = idf
|
||||
} else {
|
||||
hostConfig.IdentityFile = ""
|
||||
}
|
||||
|
||||
hostConfig.Host = hostname
|
||||
hostConfig.Port = port
|
||||
hostConfig.User = username
|
||||
|
||||
logrus.Debugf("constructed SSH config %s for %s", hostConfig, hostname)
|
||||
logrus.Debugf("retrieved ssh config for %s: %s", hostname, hostConfig.String())
|
||||
|
||||
return hostConfig, nil
|
||||
}
|
||||
|
||||
func identityFileAbsPath(relPath string) (string, error) {
|
||||
var err error
|
||||
var absPath string
|
||||
|
||||
if strings.HasPrefix(relPath, "~/") {
|
||||
systemUser, err := user.Current()
|
||||
if err != nil {
|
||||
return absPath, err
|
||||
}
|
||||
absPath = filepath.Join(systemUser.HomeDir, relPath[2:])
|
||||
// Fatal is a error output wrapper which aims to make SSH failures easier to
|
||||
// parse through re-wording.
|
||||
func Fatal(hostname string, err error) error {
|
||||
out := err.Error()
|
||||
if strings.Contains(out, "Host key verification failed.") {
|
||||
return fmt.Errorf("SSH host key verification failed for %s", hostname)
|
||||
} else if strings.Contains(out, "Could not resolve hostname") {
|
||||
return fmt.Errorf("could not resolve hostname for %s", hostname)
|
||||
} else if strings.Contains(out, "Connection timed out") {
|
||||
return fmt.Errorf("connection timed out for %s", hostname)
|
||||
} else if strings.Contains(out, "Permission denied") {
|
||||
return fmt.Errorf("ssh auth: permission denied for %s", hostname)
|
||||
} else if strings.Contains(out, "Network is unreachable") {
|
||||
return fmt.Errorf("unable to connect to %s, network is unreachable?", hostname)
|
||||
} else {
|
||||
absPath, err = filepath.Abs(relPath)
|
||||
if err != nil {
|
||||
return absPath, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("resolved %s to %s to read the ssh identity file", relPath, absPath)
|
||||
|
||||
return absPath, nil
|
||||
}
|
||||
|
38
pkg/test/test.go
Normal file
38
pkg/test/test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// RmServerAppRecipe deletes the test server / app / recipe.
|
||||
func RmServerAppRecipe() {
|
||||
testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com")
|
||||
if err := os.Remove(testAppLink); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test")
|
||||
if err := os.Remove(testRecipeLink); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// MkServerAppRecipe symlinks the test server / app / recipe.
|
||||
func MkServerAppRecipe() {
|
||||
RmServerAppRecipe()
|
||||
|
||||
testAppDir := os.ExpandEnv("$PWD/../../tests/resources/testapp")
|
||||
testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com")
|
||||
if err := os.Symlink(testAppDir, testAppLink); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
testRecipeDir := os.ExpandEnv("$PWD/../../tests/resources/testrecipe")
|
||||
testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test")
|
||||
if err := os.Symlink(testRecipeDir, testRecipeLink); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
@ -2,18 +2,15 @@ package commandconn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
sshPkg "coopcloud.tech/abra/pkg/ssh"
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/cli/cli/connhelper/ssh"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
dCliContextStore "github.com/docker/cli/cli/context/store"
|
||||
dClient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetConnectionHelper returns Docker-specific connection helper for the given URL.
|
||||
@ -36,25 +33,6 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne
|
||||
return nil, errors.Wrap(err, "ssh host connection is not valid")
|
||||
}
|
||||
|
||||
if err := sshPkg.EnsureHostKey(ctxConnDetails.Host); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostConfig, err := sshPkg.GetHostConfig(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hostConfig.IdentityFile != "" {
|
||||
msg := "discovered %s as identity file for %s, using for ssh connection"
|
||||
logrus.Debugf(msg, hostConfig.IdentityFile, ctxConnDetails.Host)
|
||||
sshFlags = append(sshFlags, fmt.Sprintf("-o IdentityFile=%s", hostConfig.IdentityFile))
|
||||
}
|
||||
|
||||
return &connhelper.ConnectionHelper{
|
||||
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return New(ctx, "ssh", append(sshFlags, ctxConnDetails.Args("docker", "system", "dial-stdio")...)...)
|
||||
@ -67,13 +45,13 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func NewConnectionHelper(daemonURL string) *connhelper.ConnectionHelper {
|
||||
func NewConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) {
|
||||
helper, err := GetConnectionHelper(daemonURL)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return helper
|
||||
return helper, nil
|
||||
}
|
||||
|
||||
func getDockerEndpoint(host string) (docker.Endpoint, error) {
|
||||
|
@ -420,6 +420,12 @@ func convertServiceSecrets(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// NOTE(d1): strip # length=... modifiers
|
||||
if strings.Contains(obj.Name, "#") {
|
||||
vals := strings.Split(obj.Name, "#")
|
||||
obj.Name = strings.TrimSpace(vals[0])
|
||||
}
|
||||
|
||||
file := swarm.SecretReferenceFileTarget(obj.File)
|
||||
refs = append(refs, &swarm.SecretReference{
|
||||
File: &file,
|
||||
|
@ -35,16 +35,21 @@ func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Confi
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recipeName, exists := appEnv["RECIPE"]
|
||||
if !exists {
|
||||
recipeName, _ = appEnv["TYPE"]
|
||||
}
|
||||
|
||||
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
||||
if len(unsupportedProperties) > 0 {
|
||||
logrus.Warnf("%s: ignoring unsupported options: %s",
|
||||
appEnv["TYPE"], strings.Join(unsupportedProperties, ", "))
|
||||
recipeName, strings.Join(unsupportedProperties, ", "))
|
||||
}
|
||||
|
||||
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
|
||||
if len(deprecatedProperties) > 0 {
|
||||
logrus.Warnf("%s: ignoring deprecated options: %s",
|
||||
appEnv["TYPE"], propertyWarnings(deprecatedProperties))
|
||||
recipeName, propertyWarnings(deprecatedProperties))
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
@ -8,9 +8,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
abraClient "coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
"github.com/docker/cli/cli/command/stack/formatter"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@ -18,7 +18,7 @@ import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -57,20 +57,10 @@ func GetStackServices(ctx context.Context, dockerclient client.APIClient, namesp
|
||||
}
|
||||
|
||||
// GetDeployedServicesByLabel filters services by label
|
||||
func GetDeployedServicesByLabel(contextName string, label string) StackStatus {
|
||||
cl, err := abraClient.New(contextName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, bail out gracefully
|
||||
return StackStatus{[]swarm.Service{}, nil}
|
||||
}
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
func GetDeployedServicesByLabel(cl *dockerClient.Client, contextName string, label string) StackStatus {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("label", label)
|
||||
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filters})
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
@ -78,18 +68,8 @@ func GetDeployedServicesByLabel(contextName string, label string) StackStatus {
|
||||
return StackStatus{services, nil}
|
||||
}
|
||||
|
||||
func GetAllDeployedServices(contextName string) StackStatus {
|
||||
cl, err := abraClient.New(contextName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, bail out gracefully
|
||||
return StackStatus{[]swarm.Service{}, nil}
|
||||
}
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: getAllStacksFilter()})
|
||||
func GetAllDeployedServices(cl *dockerClient.Client, contextName string) StackStatus {
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: getAllStacksFilter()})
|
||||
if err != nil {
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
@ -98,7 +78,7 @@ func GetAllDeployedServices(contextName string) StackStatus {
|
||||
}
|
||||
|
||||
// GetDeployedServicesByName filters services by name
|
||||
func GetDeployedServicesByName(ctx context.Context, cl *dockerclient.Client, stackName, serviceName string) StackStatus {
|
||||
func GetDeployedServicesByName(ctx context.Context, cl *dockerClient.Client, stackName, serviceName string) StackStatus {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("%s_%s", stackName, serviceName))
|
||||
|
||||
@ -111,7 +91,7 @@ func GetDeployedServicesByName(ctx context.Context, cl *dockerclient.Client, sta
|
||||
}
|
||||
|
||||
// IsDeployed chekcks whether an appp is deployed or not.
|
||||
func IsDeployed(ctx context.Context, cl *dockerclient.Client, stackName string) (bool, string, error) {
|
||||
func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string) (bool, string, error) {
|
||||
version := "unknown"
|
||||
isDeployed := false
|
||||
|
||||
@ -142,7 +122,7 @@ func IsDeployed(ctx context.Context, cl *dockerclient.Client, stackName string)
|
||||
}
|
||||
|
||||
// pruneServices removes services that are no longer referenced in the source
|
||||
func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, services map[string]struct{}) {
|
||||
func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, services map[string]struct{}) {
|
||||
oldServices, err := GetStackServices(ctx, cl, namespace.Name())
|
||||
if err != nil {
|
||||
logrus.Infof("Failed to list services: %s\n", err)
|
||||
@ -158,9 +138,7 @@ func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace conve
|
||||
}
|
||||
|
||||
// RunDeploy is the swarm implementation of docker stack deploy
|
||||
func RunDeploy(cl *dockerclient.Client, opts Deploy, cfg *composetypes.Config, appName string, dontWait bool) error {
|
||||
ctx := context.Background()
|
||||
|
||||
func RunDeploy(cl *dockerClient.Client, opts Deploy, cfg *composetypes.Config, appName string, dontWait bool) error {
|
||||
if err := validateResolveImageFlag(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -170,7 +148,7 @@ func RunDeploy(cl *dockerclient.Client, opts Deploy, cfg *composetypes.Config, a
|
||||
opts.ResolveImage = ResolveImageNever
|
||||
}
|
||||
|
||||
return deployCompose(ctx, cl, opts, cfg, appName, dontWait)
|
||||
return deployCompose(context.Background(), cl, opts, cfg, appName, dontWait)
|
||||
}
|
||||
|
||||
// validateResolveImageFlag validates the opts.resolveImage command line option
|
||||
@ -183,7 +161,7 @@ func validateResolveImageFlag(opts *Deploy) error {
|
||||
}
|
||||
}
|
||||
|
||||
func deployCompose(ctx context.Context, cl *dockerclient.Client, opts Deploy, config *composetypes.Config, appName string, dontWait bool) error {
|
||||
func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, config *composetypes.Config, appName string, dontWait bool) error {
|
||||
namespace := convert.NewNamespace(opts.Namespace)
|
||||
|
||||
if opts.Prune {
|
||||
@ -241,7 +219,7 @@ func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) ma
|
||||
return serviceNetworks
|
||||
}
|
||||
|
||||
func validateExternalNetworks(ctx context.Context, client dockerclient.NetworkAPIClient, externalNetworks []string) error {
|
||||
func validateExternalNetworks(ctx context.Context, client dockerClient.NetworkAPIClient, externalNetworks []string) error {
|
||||
for _, networkName := range externalNetworks {
|
||||
if !container.NetworkMode(networkName).IsUserDefined() {
|
||||
// Networks that are not user defined always exist on all nodes as
|
||||
@ -250,7 +228,7 @@ func validateExternalNetworks(ctx context.Context, client dockerclient.NetworkAP
|
||||
}
|
||||
network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{})
|
||||
switch {
|
||||
case dockerclient.IsErrNotFound(err):
|
||||
case dockerClient.IsErrNotFound(err):
|
||||
return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName)
|
||||
case err != nil:
|
||||
return err
|
||||
@ -261,7 +239,7 @@ func validateExternalNetworks(ctx context.Context, client dockerclient.NetworkAP
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSecrets(ctx context.Context, cl *dockerclient.Client, secrets []swarm.SecretSpec) error {
|
||||
func createSecrets(ctx context.Context, cl *dockerClient.Client, secrets []swarm.SecretSpec) error {
|
||||
for _, secretSpec := range secrets {
|
||||
secret, _, err := cl.SecretInspectWithRaw(ctx, secretSpec.Name)
|
||||
switch {
|
||||
@ -270,7 +248,7 @@ func createSecrets(ctx context.Context, cl *dockerclient.Client, secrets []swarm
|
||||
if err := cl.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to update secret %s", secretSpec.Name)
|
||||
}
|
||||
case dockerclient.IsErrNotFound(err):
|
||||
case dockerClient.IsErrNotFound(err):
|
||||
// secret does not exist, then we create a new one.
|
||||
logrus.Infof("Creating secret %s\n", secretSpec.Name)
|
||||
if _, err := cl.SecretCreate(ctx, secretSpec); err != nil {
|
||||
@ -283,7 +261,7 @@ func createSecrets(ctx context.Context, cl *dockerclient.Client, secrets []swarm
|
||||
return nil
|
||||
}
|
||||
|
||||
func createConfigs(ctx context.Context, cl *dockerclient.Client, configs []swarm.ConfigSpec) error {
|
||||
func createConfigs(ctx context.Context, cl *dockerClient.Client, configs []swarm.ConfigSpec) error {
|
||||
for _, configSpec := range configs {
|
||||
config, _, err := cl.ConfigInspectWithRaw(ctx, configSpec.Name)
|
||||
switch {
|
||||
@ -292,7 +270,7 @@ func createConfigs(ctx context.Context, cl *dockerclient.Client, configs []swarm
|
||||
if err := cl.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to update config %s", configSpec.Name)
|
||||
}
|
||||
case dockerclient.IsErrNotFound(err):
|
||||
case dockerClient.IsErrNotFound(err):
|
||||
// config does not exist, then we create a new one.
|
||||
logrus.Infof("Creating config %s\n", configSpec.Name)
|
||||
if _, err := cl.ConfigCreate(ctx, configSpec); err != nil {
|
||||
@ -305,7 +283,7 @@ func createConfigs(ctx context.Context, cl *dockerclient.Client, configs []swarm
|
||||
return nil
|
||||
}
|
||||
|
||||
func createNetworks(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, networks map[string]types.NetworkCreate) error {
|
||||
func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, networks map[string]types.NetworkCreate) error {
|
||||
existingNetworks, err := getStackNetworks(ctx, cl, namespace.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
@ -335,7 +313,7 @@ func createNetworks(ctx context.Context, cl *dockerclient.Client, namespace conv
|
||||
|
||||
func deployServices(
|
||||
ctx context.Context,
|
||||
cl *dockerclient.Client,
|
||||
cl *dockerClient.Client,
|
||||
services map[string]swarm.ServiceSpec,
|
||||
namespace convert.Namespace,
|
||||
sendAuth bool,
|
||||
@ -469,7 +447,7 @@ func getStackConfigs(ctx context.Context, dockerclient client.APIClient, namespa
|
||||
|
||||
// https://github.com/docker/cli/blob/master/cli/command/service/helpers.go
|
||||
// https://github.com/docker/cli/blob/master/cli/command/service/progress/progress.go
|
||||
func WaitOnService(ctx context.Context, cl *dockerclient.Client, serviceID, appName string) error {
|
||||
func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appName string) error {
|
||||
errChan := make(chan error, 1)
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
@ -507,3 +485,37 @@ If a service is failing to even start, try smoke out the error with:
|
||||
`, appName, timeout, appName, appName, appName))
|
||||
}
|
||||
}
|
||||
|
||||
// Copypasta from https://github.com/docker/cli/blob/master/cli/command/stack/swarm/list.go
|
||||
// GetStacks lists the swarm stacks.
|
||||
func GetStacks(cl *dockerClient.Client) ([]*formatter.Stack, error) {
|
||||
services, err := cl.ServiceList(
|
||||
context.Background(),
|
||||
types.ServiceListOptions{Filters: getAllStacksFilter()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*formatter.Stack)
|
||||
for _, service := range services {
|
||||
labels := service.Spec.Labels
|
||||
name, ok := labels[convert.LabelNamespace]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("cannot get label %s for service %s",
|
||||
convert.LabelNamespace, service.ID)
|
||||
}
|
||||
ztack, ok := m[name]
|
||||
if !ok {
|
||||
m[name] = &formatter.Stack{
|
||||
Name: name,
|
||||
Services: 1,
|
||||
}
|
||||
} else {
|
||||
ztack.Services++
|
||||
}
|
||||
}
|
||||
var stacks []*formatter.Stack
|
||||
for _, stack := range m {
|
||||
stacks = append(stacks, stack)
|
||||
}
|
||||
return stacks, nil
|
||||
}
|
||||
|
@ -1,3 +1,13 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"packageRules": [{
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}],
|
||||
"postUpdateOptions": [
|
||||
"gomodTidy"
|
||||
],
|
||||
"ignoreDeps": [
|
||||
"github.com/urfave/cli"
|
||||
]
|
||||
}
|
||||
|
7
scripts/autocomplete/fish
Normal file
7
scripts/autocomplete/fish
Normal file
@ -0,0 +1,7 @@
|
||||
function complete_abra_args
|
||||
set -l cmd (commandline -poc) --generate-bash-completion
|
||||
$cmd
|
||||
end
|
||||
complete -c abra -f -n "not __fish_seen_subcommand_from -h --help -v --version complete_abra_args" -a "(complete_abra_args)"
|
||||
complete -c abra -f -s h -l help -d 'show help'
|
||||
complete -c abra -f -s v -l version -d 'print the version'
|
@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ABRA_VERSION="0.3.0-alpha"
|
||||
ABRA_VERSION="0.6.0-beta"
|
||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
|
||||
RC_VERSION="0.4.0-alpha-rc6"
|
||||
RC_VERSION="0.7.0-rc2-beta"
|
||||
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
|
||||
|
||||
for arg in "$@"; do
|
||||
@ -44,8 +44,17 @@ function install_abra_release {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m)
|
||||
ARCH=$(uname -m)
|
||||
if [[ $ARCH =~ "aarch64" ]]; then
|
||||
ARCH="arm64"
|
||||
elif [[ $ARCH =~ "armv5l" ]]; then
|
||||
ARCH="armv5"
|
||||
elif [[ $ARCH =~ "armv6l" ]]; then
|
||||
ARCH="armv6"
|
||||
elif [[ $ARCH =~ "armv7l" ]]; then
|
||||
ARCH="armv7"
|
||||
fi
|
||||
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$ARCH
|
||||
FILENAME="abra_"$ABRA_VERSION"_"$PLATFORM""
|
||||
sed_command_rel='s/.*"assets":\[\{[^]]*"name":"'$FILENAME'"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p'
|
||||
sed_command_checksums='s/.*"assets":\[\{[^\]*"name":"checksums.txt"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p'
|
||||
|
5
scripts/release/upx.sh
Executable file
5
scripts/release/upx.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
upx ./dist/abra_*/abra
|
@ -1 +0,0 @@
|
||||
TYPE=gitea
|
@ -1 +0,0 @@
|
||||
TYPE=wordpress
|
@ -1 +0,0 @@
|
||||
TYPE=wordpress
|
1
tests/integration/.gitignore
vendored
Normal file
1
tests/integration/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
logs
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user