forked from toolshed/abra
Compare commits
343 Commits
upgrade-cl
...
abra-app-m
| Author | SHA1 | Date | |
|---|---|---|---|
| 126acee958 | |||
|
5cf6048ecb
|
|||
|
3e2797c433
|
|||
| df89e8143a | |||
| b4ddd3e77c | |||
|
81c28e3006
|
|||
|
34d2e3b092
|
|||
|
1894c2f5fc
|
|||
|
e0bd03bec3
|
|||
|
77ff146991
|
|||
|
6fad1a1dcc
|
|||
|
a90e239547
|
|||
|
9ee094fcd7
|
|||
|
1aa7016789
|
|||
| 60b3af1fa4 | |||
| 5f4b5e0fad | |||
| feadfca0d6 | |||
| 73d4ee1c98 | |||
|
f46c18c8d7
|
|||
| f5a843bd90 | |||
|
fac372dc73
|
|||
|
8a3be01c3e
|
|||
|
4193d63d23
|
|||
|
38f308910a
|
|||
|
4aaa7400b8
|
|||
| 091611b984 | |||
| 2cfc40dc28 | |||
| 6849e3554d | |||
|
452de7fdc2
|
|||
|
952d768ab0
|
|||
| 2c91d2040e | |||
| eff4435971 | |||
| 032fe99086 | |||
| 7add56df00 | |||
| 0ab05cece2 | |||
| c63f6db61e | |||
| 56a68dfa91 | |||
| 157d131b37 | |||
| 3fae036db2 | |||
| ce9d0934b6 | |||
|
a32e30374f
|
|||
|
cf46569f04
|
|||
|
022606c13c
|
|||
|
8cfda5229f
|
|||
|
855a4c37c4
|
|||
| 7c3b740e14 | |||
|
2fbef41a3a
|
|||
| 6fb41e5300 | |||
| 1432f480c7 | |||
|
83af39771b
|
|||
|
4d1333202e
|
|||
|
55c24f070c
|
|||
| 229e8eb9da | |||
|
b3ab95750e
|
|||
| de009921a2 | |||
|
d081bbaefa
|
|||
|
515b5466ca
|
|||
|
6965799bdc
|
|||
|
f75c9a6259
|
|||
| a43a092ba7 | |||
| fa084a61d2 | |||
|
895a7fe7d6
|
|||
|
742a726778
|
|||
|
2b9a185aff
|
|||
|
b7c1e87c0b
|
|||
|
cdfb8a08bb
|
|||
|
8943cea13f
|
|||
|
6d64e0edd3
|
|||
| 47045ca8f1 | |||
| d0f982456e | |||
| 80ad6c6681 | |||
|
cb63cfe9c2
|
|||
|
d1e49d17ce
|
|||
|
1574aa0631
|
|||
|
1723025fbf
|
|||
|
a2b678caf6
|
|||
| 0a371ec360 | |||
| e58a716fe1 | |||
| d09a19a385 | |||
| cee808ff06 | |||
| 4326d1d259 | |||
| b976872f77 | |||
| 7b6ea76437 | |||
| 9069758969 | |||
| 15d6b1a2a5 | |||
|
8a7fe4ca07
|
|||
| 64ad60663f | |||
| cb3f46b46e | |||
|
41e514ae9a
|
|||
|
086b4828ff
|
|||
|
ed263854d4
|
|||
|
eb6fe4ba6e
|
|||
| 993172d31b | |||
| c70b6e72a7 | |||
| 22e4dd7fca | |||
|
b6009057a8
|
|||
|
b978f04910
|
|||
|
3ac29d54d9
|
|||
|
877c17fab5
|
|||
|
f01fd26ce3
|
|||
|
273c165a41
|
|||
|
c88fc66c99
|
|||
|
9b271a6963
|
|||
|
8af87aa382
|
|||
|
ac0b9cd052
|
|||
|
4923984e84
|
|||
|
2bc77de751
|
|||
|
b3a2402cec
|
|||
|
a773fd4256
|
|||
|
b1a0d54bd3
|
|||
|
3869d6bce9
|
|||
|
0ff07ab224
|
|||
|
936c1b0626
|
|||
|
b576cba227
|
|||
|
d087f3debf
|
|||
|
e57a6d87a3
|
|||
|
74b64099de
|
|||
|
354712ca46
|
|||
|
81cdc843ec
|
|||
|
d2931e3af0
|
|||
|
b9f2d1f568
|
|||
| a379b31a19 | |||
|
17e15dba77
|
|||
|
1194f3b228
|
|||
|
2dc8034c16
|
|||
|
c5ddeb2d8a
|
|||
|
0a63f9ce27
|
|||
|
3a71dc47f8
|
|||
|
f07c64f7b8
|
|||
|
dd03c40e10
|
|||
|
48198d55bd
|
|||
|
c0931b96d8
|
|||
|
64ea0f9684
|
|||
|
b0cd8ccbb9
|
|||
|
5975be6870
|
|||
|
bfed51a69c
|
|||
|
5d0faf5e13
|
|||
|
cd6af9708c
|
|||
|
ef95bce1e4
|
|||
|
a159583874
|
|||
|
e3b0500875
|
|||
|
994310a4ff
|
|||
| 74108b0dd9 | |||
|
3727c7fa78
|
|||
|
9a4414fd13
|
|||
|
9f189680f3
|
|||
|
356e527f1f
|
|||
|
7ec61c6d03
|
|||
|
fab93a559a
|
|||
|
8ac31330be
|
|||
|
03000c25e0
|
|||
|
3f32dbb1a3
|
|||
|
27f68b1dc5
|
|||
| a0da5299fe | |||
|
866c5c4536
|
|||
|
dc4c6784cb
|
|||
| 97959ef5da | |||
| b6573720ec | |||
|
4e8995cc0e
|
|||
|
efb3fd8759
|
|||
|
008582c3d9
|
|||
|
8fa20e2c7f
|
|||
|
aa1dc795ef
|
|||
|
18df498295
|
|||
|
671e1ca276
|
|||
|
0df2b15c33
|
|||
|
3f29084664
|
|||
|
0bb25a00ec
|
|||
| 28c7676413 | |||
| 730fef09a3 | |||
| 8d076a308a | |||
| 9510c04aeb | |||
|
d9e60afd71
|
|||
|
31fa9b1a7a
|
|||
| f664599836 | |||
| bba1640913 | |||
| 7b54c2b5b9 | |||
| 8ee1947fe9 | |||
|
b313b0a145
|
|||
|
1f9b863be0
|
|||
|
3b3ce85ef9
|
|||
|
1f8662cd95
|
|||
|
375e17a4a0
|
|||
|
04aec8232f
|
|||
|
2a5985e44e
|
|||
|
c65be64e7d
|
|||
|
fd8652e26d
|
|||
|
518c5795f4
|
|||
|
827edcb0da
|
|||
|
05489a129c
|
|||
|
c02e11eb0a
|
|||
|
8b8e158664
|
|||
|
e5a6dea10c
|
|||
|
1132b09b5b
|
|||
|
b2436174b0
|
|||
|
ea10019068
|
|||
|
9b0b3c2e4c
|
|||
|
8084bff104
|
|||
|
d22e2c38ce
|
|||
|
e945087f79
|
|||
|
7734dd555d
|
|||
|
aedf5e5ff7
|
|||
|
95c598d030
|
|||
|
56068362e8
|
|||
|
cf14731b46
|
|||
|
486cfa68d8
|
|||
|
1718903834
|
|||
|
eb9894e5bb
|
|||
|
a2116774e8
|
|||
|
d2efdf8bf5
|
|||
|
b15c05929c
|
|||
|
f167a91868
|
|||
|
8cded8752a
|
|||
|
d1876e2fae
|
|||
|
e42a1bca29
|
|||
|
b5493ba059
|
|||
|
a41a36b8fd
|
|||
|
de006782b6
|
|||
|
f28cffe6d8
|
|||
|
d3ede0f0f6
|
|||
|
ae4653f5e3
|
|||
| 7f0a74d3c3 | |||
| e99114e695 | |||
| b1208f9db5 | |||
|
b8e1a3b75f
|
|||
|
ff90b43929
|
|||
| c5724d56f8 | |||
|
ce7dda1eae
|
|||
|
d38f3ab7f5
|
|||
|
4be8c8daed
|
|||
|
3c9405a4ed
|
|||
| f6b7510da6 | |||
| 7596982282 | |||
| 4085eb6654 | |||
| 790dbca362 | |||
| d7a870b887 | |||
|
1a3ec7a107
|
|||
|
7f910b4e5b
|
|||
|
b82ac3bd63
|
|||
|
00d60f7114
|
|||
|
71d93cbbea
|
|||
|
2fb5493ab5
|
|||
|
0ff8e49cfd
|
|||
|
addbda9145
|
|||
|
c33ca1c6bc
|
|||
|
4580df72cb
|
|||
|
f003430a8d
|
|||
|
5426464092
|
|||
|
72c021c727
|
|||
|
f2e076b35f
|
|||
|
4ccb4198d6
|
|||
|
a9f7579ca9
|
|||
| 9cd1fe658b | |||
| 41c16db670 | |||
| 87ecc05962 | |||
| f14d49cc64 | |||
| f638b6a16b | |||
| 5617a9ba07 | |||
| c1b03bcbd7 | |||
| 99da8d4e57 | |||
| ca1db33e97 | |||
| eb62e0ecc3 | |||
| 6f90fc3025 | |||
| c861c09cce | |||
| 2f41b6d8b4 | |||
| 73e9b818b4 | |||
| f268e5893b | |||
| 47013c63d6 | |||
| 4cf6155fb8 | |||
| 01f3f4be17 | |||
| eee2ecda06 | |||
| 950f85e2b4 | |||
|
9ef64778f5
|
|||
|
735f521bc0
|
|||
|
96a25425a4
|
|||
|
1a8dca9804
|
|||
|
465827d5ee
|
|||
|
cde06f4f00
|
|||
|
050a479df7
|
|||
|
ef108d63e1
|
|||
|
cf8ff410cc
|
|||
|
6ec678208f
|
|||
|
a001be3021
|
|||
|
6712bd446f
|
|||
|
1097daa69f
|
|||
|
beaa233421
|
|||
|
f871f9beee
|
|||
|
0f8f0f908f
|
|||
|
c5211fbd7e
|
|||
| 0076b31253 | |||
| 37aff723c0 | |||
| f18c642226 | |||
| ac695ae28e | |||
|
ac87898005
|
|||
|
32ae2499b6
|
|||
|
1136ec5dcd
|
|||
|
6a2db1abaa
|
|||
|
9554ad40c8
|
|||
|
2014cd6622
|
|||
|
a9ce2106c6
|
|||
|
34de38928a
|
|||
|
f58522d822
|
|||
|
712ebfb701
|
|||
|
1fe601cd16
|
|||
|
7b7e1bfa97
|
|||
|
1a12bef53e
|
|||
|
d787f71215
|
|||
|
9bf44c15ed
|
|||
|
349cacc1f2
|
|||
| 938534f5ac | |||
| 6cd331ebd6 | |||
|
40517171f7
|
|||
| b2485cc122 | |||
| 9ec99c7712 | |||
|
aa3910f8df
|
|||
|
43990b6fae
|
|||
|
91ea2c01a5
|
|||
|
316fdd3643
|
|||
|
e07ae8cccd
|
|||
|
300a4ead01
|
|||
|
f209b6f564
|
|||
|
791183adfe
|
|||
| e6b35e8524 | |||
| 8a0274cac0 | |||
| e609924af0 | |||
| 70e2943301 | |||
| 0590c1824d | |||
| 459abecfa5 | |||
| 183ad8f576 | |||
|
03f94da2d8
|
|||
| 766f69b0fd | |||
|
004cd70aed
|
|||
|
a4de446f58
|
|||
| d21c35965d | |||
| 63ea58ffaa | |||
|
2ecace3e90
|
|||
| d5ac3958a4 | |||
| 72c20e0039 | |||
|
575f9905f1
|
|||
|
e3a0af5840
|
|||
|
9a3a39a185
|
|||
|
cea56dddde
|
|||
|
2c515ce70a
|
77
.drone.yml
77
.drone.yml
@ -3,17 +3,17 @@ kind: pipeline
|
|||||||
name: coopcloud.tech/abra
|
name: coopcloud.tech/abra
|
||||||
steps:
|
steps:
|
||||||
- name: make check
|
- name: make check
|
||||||
image: golang:1.21
|
image: golang:1.24
|
||||||
commands:
|
commands:
|
||||||
- make check
|
- make check
|
||||||
|
|
||||||
- name: make test
|
- name: make test
|
||||||
image: golang:1.21
|
image: golang:1.24
|
||||||
environment:
|
environment:
|
||||||
ABRA_DIR: "/root/.abra"
|
CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git
|
||||||
commands:
|
commands:
|
||||||
- make build-abra
|
- mkdir -p $HOME/.abra
|
||||||
- ./abra help # show version, initialise $ABRA_DIR
|
- git clone $CATL_URL $HOME/.abra/catalogue
|
||||||
- make test
|
- make test
|
||||||
depends_on:
|
depends_on:
|
||||||
- make check
|
- make check
|
||||||
@ -29,7 +29,7 @@ steps:
|
|||||||
event: tag
|
event: tag
|
||||||
|
|
||||||
- name: release
|
- name: release
|
||||||
image: goreleaser/goreleaser:v1.18.2
|
image: goreleaser/goreleaser:v2.5.1
|
||||||
environment:
|
environment:
|
||||||
GITEA_TOKEN:
|
GITEA_TOKEN:
|
||||||
from_secret: goreleaser_gitea_token
|
from_secret: goreleaser_gitea_token
|
||||||
@ -47,19 +47,72 @@ steps:
|
|||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
auto_tag: true
|
auto_tag: true
|
||||||
username: 3wordchant
|
username: abra-bot
|
||||||
password:
|
password:
|
||||||
from_secret: git_coopcloud_tech_token_3wc
|
from_secret: git_coopcloud_tech_token_abra_bot
|
||||||
repo: git.coopcloud.tech/coop-cloud/abra
|
repo: git.coopcloud.tech/toolshed/abra
|
||||||
tags: dev
|
tags: dev
|
||||||
registry: git.coopcloud.tech
|
registry: git.coopcloud.tech
|
||||||
when:
|
when:
|
||||||
event:
|
branch:
|
||||||
exclude:
|
- main
|
||||||
- pull_request
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- make check
|
- make check
|
||||||
|
- make test
|
||||||
|
|
||||||
|
- name: on-demand integration test
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
settings:
|
||||||
|
host:
|
||||||
|
- int.coopcloud.tech
|
||||||
|
username: abra
|
||||||
|
key:
|
||||||
|
from_secret: abra_int_private_key
|
||||||
|
port: 22
|
||||||
|
command_timeout: 60m
|
||||||
|
script_stop: true
|
||||||
|
request_pty: true
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
wget https://git.coopcloud.tech/toolshed/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
|
||||||
|
chmod +x run-ci-int
|
||||||
|
sh run-ci-int
|
||||||
|
when:
|
||||||
|
ref:
|
||||||
|
- refs/heads/int-*
|
||||||
|
depends_on:
|
||||||
|
- make check
|
||||||
|
- make test
|
||||||
|
|
||||||
|
- name: nightly integration test
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
settings:
|
||||||
|
host:
|
||||||
|
- int.coopcloud.tech
|
||||||
|
username: abra
|
||||||
|
key:
|
||||||
|
from_secret: abra_int_private_key
|
||||||
|
port: 22
|
||||||
|
command_timeout: 60m
|
||||||
|
script_stop: true
|
||||||
|
request_pty: true
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
wget https://git.coopcloud.tech/toolshed/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
|
||||||
|
chmod +x run-ci-int
|
||||||
|
sh run-ci-int
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- cron:
|
||||||
|
cron:
|
||||||
|
# @daily https://docs.drone.io/cron/
|
||||||
|
- integration
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: deps
|
- name: deps
|
||||||
temp: {}
|
temp: {}
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
action:
|
||||||
|
exclude:
|
||||||
|
- synchronized
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
go env -w GOPRIVATE=coopcloud.tech
|
# integration test suite
|
||||||
|
|
||||||
# export PASSWORD_STORE_DIR=$(pwd)/../../autonomic/passwords/passwords/
|
|
||||||
|
|
||||||
# export ABRA_DIR="$HOME/.abra_test"
|
# export ABRA_DIR="$HOME/.abra_test"
|
||||||
# export ABRA_TEST_DOMAIN=test.example.com
|
# export TEST_SERVER=test.example.com
|
||||||
# export ABRA_SKIP_TEARDOWN=1 # for faster feedback when developing tests
|
# export ABRA_CI=1
|
||||||
|
|
||||||
|
# release automation
|
||||||
|
# export GITEA_TOKEN=
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Do not use this issue tracker"
|
|
||||||
about: "Do not use this issue tracker"
|
|
||||||
title: "Do not use this issue tracker"
|
|
||||||
labels: []
|
|
||||||
---
|
|
||||||
|
|
||||||
Please report your issue on [`coop-cloud/organising`](https://git.coopcloud.tech/coop-cloud/organising)
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,8 +2,7 @@
|
|||||||
.e2e.env
|
.e2e.env
|
||||||
.envrc
|
.envrc
|
||||||
.vscode/
|
.vscode/
|
||||||
|
/abra
|
||||||
/kadabra
|
/kadabra
|
||||||
abra
|
|
||||||
dist/
|
dist/
|
||||||
tests/integration/.bats
|
tests/integration/.bats
|
||||||
vendor/
|
|
||||||
|
|||||||
@ -29,6 +29,8 @@ builds:
|
|||||||
ldflags:
|
ldflags:
|
||||||
- "-X 'main.Commit={{ .Commit }}'"
|
- "-X 'main.Commit={{ .Commit }}'"
|
||||||
- "-X 'main.Version={{ .Version }}'"
|
- "-X 'main.Version={{ .Version }}'"
|
||||||
|
- "-s"
|
||||||
|
- "-w"
|
||||||
|
|
||||||
- id: kadabra
|
- id: kadabra
|
||||||
binary: kadabra
|
binary: kadabra
|
||||||
@ -47,15 +49,13 @@ builds:
|
|||||||
- 5
|
- 5
|
||||||
- 6
|
- 6
|
||||||
- 7
|
- 7
|
||||||
|
gcflags:
|
||||||
|
- "all=-l -B"
|
||||||
ldflags:
|
ldflags:
|
||||||
- "-X 'main.Commit={{ .Commit }}'"
|
- "-X 'main.Commit={{ .Commit }}'"
|
||||||
- "-X 'main.Version={{ .Version }}'"
|
- "-X 'main.Version={{ .Version }}'"
|
||||||
|
- "-s"
|
||||||
archives:
|
- "-w"
|
||||||
- replacements:
|
|
||||||
386: i386
|
|
||||||
amd64: x86_64
|
|
||||||
format: binary
|
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: "checksums.txt"
|
name_template: "checksums.txt"
|
||||||
|
|||||||
@ -4,9 +4,11 @@
|
|||||||
> please do add yourself! This is a community project, let's show some 💞
|
> please do add yourself! This is a community project, let's show some 💞
|
||||||
|
|
||||||
- 3wordchant
|
- 3wordchant
|
||||||
|
- ammaratef45
|
||||||
- cassowary
|
- cassowary
|
||||||
- codegod100
|
- codegod100
|
||||||
- decentral1se
|
- decentral1se
|
||||||
|
- fauno
|
||||||
- frando
|
- frando
|
||||||
- kawaiipunk
|
- kawaiipunk
|
||||||
- knoflook
|
- knoflook
|
||||||
@ -16,3 +18,5 @@
|
|||||||
- roxxers
|
- roxxers
|
||||||
- vera
|
- vera
|
||||||
- yksflip
|
- yksflip
|
||||||
|
- basebuilder
|
||||||
|
- mayel
|
||||||
|
|||||||
18
Dockerfile
18
Dockerfile
@ -1,23 +1,29 @@
|
|||||||
FROM golang:1.21-alpine AS build
|
# Build image
|
||||||
|
FROM golang:1.24-alpine AS build
|
||||||
|
|
||||||
ENV GOPRIVATE coopcloud.tech
|
ENV GOPRIVATE=coopcloud.tech
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
ca-certificates \
|
|
||||||
gcc \
|
gcc \
|
||||||
git \
|
git \
|
||||||
make \
|
make \
|
||||||
musl-dev
|
musl-dev
|
||||||
|
|
||||||
RUN update-ca-certificates
|
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 make build
|
RUN CGO_ENABLED=0 make build
|
||||||
|
|
||||||
FROM scratch
|
# Release image ("slim")
|
||||||
|
FROM alpine:3.19.1
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
git \
|
||||||
|
openssh
|
||||||
|
|
||||||
|
RUN update-ca-certificates
|
||||||
|
|
||||||
COPY --from=build /app/abra /abra
|
COPY --from=build /app/abra /abra
|
||||||
|
|
||||||
|
|||||||
24
Makefile
24
Makefile
@ -2,9 +2,10 @@ ABRA := ./cmd/abra
|
|||||||
KADABRA := ./cmd/kadabra
|
KADABRA := ./cmd/kadabra
|
||||||
COMMIT := $(shell git rev-list -1 HEAD)
|
COMMIT := $(shell git rev-list -1 HEAD)
|
||||||
GOPATH := $(shell go env GOPATH)
|
GOPATH := $(shell go env GOPATH)
|
||||||
GOVERSION := 1.21
|
GOVERSION := 1.24
|
||||||
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
|
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
|
||||||
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
|
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
|
||||||
|
GCFLAGS := "all=-l -B"
|
||||||
|
|
||||||
export GOPRIVATE=coopcloud.tech
|
export GOPRIVATE=coopcloud.tech
|
||||||
|
|
||||||
@ -12,22 +13,24 @@ export GOPRIVATE=coopcloud.tech
|
|||||||
all: format check build-abra test
|
all: format check build-abra test
|
||||||
|
|
||||||
run-abra:
|
run-abra:
|
||||||
@go run -ldflags=$(LDFLAGS) $(ABRA)
|
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
|
||||||
|
|
||||||
run-kadabra:
|
run-kadabra:
|
||||||
@go run -ldflags=$(LDFLAGS) $(KADABRA)
|
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(KADABRA)
|
||||||
|
|
||||||
install-abra:
|
install-abra:
|
||||||
@go install -ldflags=$(LDFLAGS) $(ABRA)
|
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
|
||||||
|
|
||||||
install-kadabra:
|
install-kadabra:
|
||||||
@go install -ldflags=$(LDFLAGS) $(KADABRA)
|
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(KADABRA)
|
||||||
|
|
||||||
|
install: install-abra install-kadabra
|
||||||
|
|
||||||
build-abra:
|
build-abra:
|
||||||
@go build -v -ldflags=$(DIST_LDFLAGS) $(ABRA)
|
@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(ABRA)
|
||||||
|
|
||||||
build-kadabra:
|
build-kadabra:
|
||||||
@go build -v -ldflags=$(DIST_LDFLAGS) $(KADABRA)
|
@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(KADABRA)
|
||||||
|
|
||||||
build: build-abra build-kadabra
|
build: build-abra build-kadabra
|
||||||
|
|
||||||
@ -42,10 +45,10 @@ clean:
|
|||||||
@rm '$(GOPATH)/bin/kadabra'
|
@rm '$(GOPATH)/bin/kadabra'
|
||||||
|
|
||||||
format:
|
format:
|
||||||
@gofmt -s -w .
|
@gofmt -s -w $$(find . -type f -name '*.go' | grep -v "/vendor/")
|
||||||
|
|
||||||
check:
|
check:
|
||||||
@test -z $$(gofmt -l .) || \
|
@test -z $$(gofmt -l $$(find . -type f -name '*.go' | grep -v "/vendor/")) || \
|
||||||
(echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
|
(echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@ -53,3 +56,6 @@ test:
|
|||||||
|
|
||||||
loc:
|
loc:
|
||||||
@find . -name "*.go" | xargs wc -l
|
@find . -name "*.go" | xargs wc -l
|
||||||
|
|
||||||
|
deps:
|
||||||
|
@go get -t -u ./...
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# `abra`
|
# `abra`
|
||||||
|
|
||||||
[](https://build.coopcloud.tech/coop-cloud/abra)
|
[](https://build.coopcloud.tech/toolshed/abra)
|
||||||
[](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra)
|
[](https://goreportcard.com/report/git.coopcloud.tech/toolshed/abra)
|
||||||
[](https://pkg.go.dev/coopcloud.tech/abra)
|
[](https://pkg.go.dev/coopcloud.tech/abra)
|
||||||
|
|
||||||
The Co-op Cloud utility belt 🎩🐇
|
The Co-op Cloud utility belt 🎩🐇
|
||||||
|
|||||||
@ -1,37 +1,12 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/urfave/cli"
|
"github.com/leonelquinteros/gotext"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AppCommand = cli.Command{
|
var AppCommand = &cobra.Command{
|
||||||
Name: "app",
|
Use: "app [cmd] [args] [flags]",
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
Usage: "Manage apps",
|
Short: gotext.Get("Manage apps"),
|
||||||
ArgsUsage: "<domain>",
|
|
||||||
Description: "Functionality for managing the life cycle of your apps",
|
|
||||||
Subcommands: []cli.Command{
|
|
||||||
appBackupCommand,
|
|
||||||
appCheckCommand,
|
|
||||||
appCmdCommand,
|
|
||||||
appConfigCommand,
|
|
||||||
appCpCommand,
|
|
||||||
appDeployCommand,
|
|
||||||
appErrorsCommand,
|
|
||||||
appListCommand,
|
|
||||||
appLogsCommand,
|
|
||||||
appNewCommand,
|
|
||||||
appPsCommand,
|
|
||||||
appRemoveCommand,
|
|
||||||
appRestartCommand,
|
|
||||||
appRestoreCommand,
|
|
||||||
appRollbackCommand,
|
|
||||||
appRunCommand,
|
|
||||||
appSecretCommand,
|
|
||||||
appServicesCommand,
|
|
||||||
appUndeployCommand,
|
|
||||||
appUpgradeCommand,
|
|
||||||
appVersionCommand,
|
|
||||||
appVolumeCommand,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,291 +6,302 @@ import (
|
|||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var snapshot string
|
var AppBackupListCommand = &cobra.Command{
|
||||||
var snapshotFlag = &cli.StringFlag{
|
Use: "list <domain> [flags]",
|
||||||
Name: "snapshot, s",
|
|
||||||
Usage: "Lists specific snapshot",
|
|
||||||
Destination: &snapshot,
|
|
||||||
}
|
|
||||||
|
|
||||||
var includePath string
|
|
||||||
var includePathFlag = &cli.StringFlag{
|
|
||||||
Name: "path, p",
|
|
||||||
Usage: "Include path",
|
|
||||||
Destination: &includePath,
|
|
||||||
}
|
|
||||||
|
|
||||||
var resticRepo string
|
|
||||||
var resticRepoFlag = &cli.StringFlag{
|
|
||||||
Name: "repo, r",
|
|
||||||
Usage: "Restic repository",
|
|
||||||
Destination: &resticRepo,
|
|
||||||
}
|
|
||||||
|
|
||||||
var appBackupListCommand = cli.Command{
|
|
||||||
Name: "list",
|
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
Flags: []cli.Flag{
|
Short: "List the contents of a snapshot",
|
||||||
internal.DebugFlag,
|
Args: cobra.ExactArgs(1),
|
||||||
internal.OfflineFlag,
|
ValidArgsFunction: func(
|
||||||
snapshotFlag,
|
cmd *cobra.Command,
|
||||||
includePathFlag,
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Usage: "List all backups",
|
app := internal.ValidateApp(args)
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Chaos {
|
|
||||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
|
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
execEnv := []string{
|
||||||
|
fmt.Sprintf("SERVICE=%s", app.Domain),
|
||||||
|
"MACHINE_LOGS=true",
|
||||||
}
|
}
|
||||||
|
|
||||||
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
|
|
||||||
if snapshot != "" {
|
if snapshot != "" {
|
||||||
logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
|
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
|
||||||
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
|
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
|
||||||
}
|
}
|
||||||
if includePath != "" {
|
|
||||||
logrus.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
|
if showAllPaths {
|
||||||
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
|
log.Debugf("including SHOW_ALL=%v in backupbot exec invocation", showAllPaths)
|
||||||
|
execEnv = append(execEnv, fmt.Sprintf("SHOW_ALL=%v", showAllPaths))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil {
|
if timestamps {
|
||||||
logrus.Fatal(err)
|
log.Debugf("including TIMESTAMPS=%v in backupbot exec invocation", timestamps)
|
||||||
|
execEnv = append(execEnv, fmt.Sprintf("TIMESTAMPS=%v", timestamps))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if _, err = internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var appBackupDownloadCommand = cli.Command{
|
var AppBackupDownloadCommand = &cobra.Command{
|
||||||
Name: "download",
|
Use: "download <domain> [flags]",
|
||||||
Aliases: []string{"d"},
|
Aliases: []string{"d"},
|
||||||
Flags: []cli.Flag{
|
Short: "Download a snapshot",
|
||||||
internal.DebugFlag,
|
Long: `Downloads a backup.tar.gz to the current working directory.
|
||||||
internal.OfflineFlag,
|
|
||||||
snapshotFlag,
|
"--volumes/-v" includes data contained in volumes alongide paths specified in
|
||||||
includePathFlag,
|
"backupbot.backup.path" labels.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Usage: "Download a backup",
|
app := internal.ValidateApp(args)
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Chaos {
|
|
||||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
|
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
execEnv := []string{
|
||||||
|
fmt.Sprintf("SERVICE=%s", app.Domain),
|
||||||
|
"MACHINE_LOGS=true",
|
||||||
}
|
}
|
||||||
|
|
||||||
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
|
|
||||||
if snapshot != "" {
|
if snapshot != "" {
|
||||||
logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
|
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
|
||||||
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
|
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
|
||||||
}
|
}
|
||||||
|
|
||||||
if includePath != "" {
|
if includePath != "" {
|
||||||
logrus.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
|
log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
|
||||||
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
|
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil {
|
if includeSecrets {
|
||||||
logrus.Fatal(err)
|
log.Debugf("including SECRETS=%v in backupbot exec invocation", includeSecrets)
|
||||||
|
execEnv = append(execEnv, fmt.Sprintf("SECRETS=%v", includeSecrets))
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeVolumes {
|
||||||
|
log.Debugf("including VOLUMES=%v in backupbot exec invocation", includeVolumes)
|
||||||
|
execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%v", includeVolumes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteBackupDir := "/tmp/backup.tar.gz"
|
remoteBackupDir := "/tmp/backup.tar.gz"
|
||||||
currentWorkingDir := "."
|
currentWorkingDir := "."
|
||||||
if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil {
|
if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("backup successfully downloaded to current working directory")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var appBackupCreateCommand = cli.Command{
|
var AppBackupCreateCommand = &cobra.Command{
|
||||||
Name: "create",
|
Use: "create <domain> [flags]",
|
||||||
Aliases: []string{"c"},
|
Aliases: []string{"c"},
|
||||||
Flags: []cli.Flag{
|
Short: "Create a new snapshot",
|
||||||
internal.DebugFlag,
|
Args: cobra.ExactArgs(1),
|
||||||
internal.OfflineFlag,
|
ValidArgsFunction: func(
|
||||||
resticRepoFlag,
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Usage: "Create a new backup",
|
app := internal.ValidateApp(args)
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Chaos {
|
|
||||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
|
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
|
execEnv := []string{
|
||||||
if resticRepo != "" {
|
fmt.Sprintf("SERVICE=%s", app.Domain),
|
||||||
logrus.Debugf("including RESTIC_REPO=%s in backupbot exec invocation", resticRepo)
|
"MACHINE_LOGS=true",
|
||||||
execEnv = append(execEnv, fmt.Sprintf("RESTIC_REPO=%s", resticRepo))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil {
|
if retries != "" {
|
||||||
logrus.Fatal(err)
|
log.Debugf("including RETRIES=%s in backupbot exec invocation", retries)
|
||||||
|
execEnv = append(execEnv, fmt.Sprintf("RETRIES=%s", retries))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if _, err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var appBackupSnapshotsCommand = cli.Command{
|
var AppBackupSnapshotsCommand = &cobra.Command{
|
||||||
Name: "snapshots",
|
Use: "snapshots <domain> [flags]",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
Flags: []cli.Flag{
|
Short: "List all snapshots",
|
||||||
internal.DebugFlag,
|
Args: cobra.ExactArgs(1),
|
||||||
internal.OfflineFlag,
|
ValidArgsFunction: func(
|
||||||
snapshotFlag,
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Usage: "List backup snapshots",
|
app := internal.ValidateApp(args)
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Chaos {
|
|
||||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
|
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
|
execEnv := []string{
|
||||||
if snapshot != "" {
|
fmt.Sprintf("SERVICE=%s", app.Domain),
|
||||||
logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
|
"MACHINE_LOGS=true",
|
||||||
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil {
|
if _, err = internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var appBackupCommand = cli.Command{
|
var AppBackupCommand = &cobra.Command{
|
||||||
Name: "backup",
|
Use: "backup [cmd] [args] [flags]",
|
||||||
Aliases: []string{"b"},
|
Aliases: []string{"b"},
|
||||||
Usage: "Manage app backups",
|
Short: "Manage app backups",
|
||||||
ArgsUsage: "<domain>",
|
}
|
||||||
Subcommands: []cli.Command{
|
|
||||||
appBackupListCommand,
|
var (
|
||||||
appBackupSnapshotsCommand,
|
snapshot string
|
||||||
appBackupDownloadCommand,
|
retries string
|
||||||
appBackupCreateCommand,
|
includePath string
|
||||||
},
|
showAllPaths bool
|
||||||
|
timestamps bool
|
||||||
|
includeSecrets bool
|
||||||
|
includeVolumes bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppBackupListCommand.Flags().StringVarP(
|
||||||
|
&snapshot,
|
||||||
|
"snapshot",
|
||||||
|
"s",
|
||||||
|
"",
|
||||||
|
"list specific snapshot",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppBackupListCommand.Flags().BoolVarP(
|
||||||
|
&showAllPaths,
|
||||||
|
"all",
|
||||||
|
"a",
|
||||||
|
false,
|
||||||
|
"show all paths",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppBackupListCommand.Flags().BoolVarP(
|
||||||
|
×tamps,
|
||||||
|
"timestamps",
|
||||||
|
"t",
|
||||||
|
false,
|
||||||
|
"include timestamps",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppBackupDownloadCommand.Flags().StringVarP(
|
||||||
|
&snapshot,
|
||||||
|
"snapshot",
|
||||||
|
"s",
|
||||||
|
"",
|
||||||
|
"list specific snapshot",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppBackupDownloadCommand.Flags().StringVarP(
|
||||||
|
&includePath,
|
||||||
|
"path",
|
||||||
|
"p",
|
||||||
|
"",
|
||||||
|
"volumes path",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppBackupDownloadCommand.Flags().BoolVarP(
|
||||||
|
&includeSecrets,
|
||||||
|
"secrets",
|
||||||
|
"S",
|
||||||
|
false,
|
||||||
|
"include secrets",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppBackupDownloadCommand.Flags().BoolVarP(
|
||||||
|
&includeVolumes,
|
||||||
|
"volumes",
|
||||||
|
"v",
|
||||||
|
false,
|
||||||
|
"include volumes",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppBackupDownloadCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppBackupCreateCommand.Flags().StringVarP(
|
||||||
|
&retries,
|
||||||
|
"retries",
|
||||||
|
"r",
|
||||||
|
"1",
|
||||||
|
"number of retry attempts",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppBackupCreateCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
106
cli/app/check.go
106
cli/app/check.go
@ -1,23 +1,22 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appCheckCommand = cli.Command{
|
var AppCheckCommand = &cobra.Command{
|
||||||
Name: "check",
|
Use: "check <domain> [flags]",
|
||||||
Aliases: []string{"chk"},
|
Aliases: []string{"chk"},
|
||||||
Usage: "Ensure an app is well configured",
|
Short: "Ensure an app is well configured",
|
||||||
Description: `
|
Long: `Compare env vars in both the app ".env" and recipe ".env.sample" file.
|
||||||
This command compares env vars in both the app ".env" and recipe ".env.sample"
|
|
||||||
file.
|
|
||||||
|
|
||||||
The goal is to ensure that recipe ".env.sample" env vars are defined in your
|
The goal is to ensure that recipe ".env.sample" env vars are defined in your
|
||||||
app ".env" file. Only env var definitions in the ".env.sample" which are
|
app ".env" file. Only env var definitions in the ".env.sample" which are
|
||||||
@ -27,55 +26,66 @@ these env vars, then "check" will complain.
|
|||||||
Recipe maintainers may or may not provide defaults for env vars within their
|
Recipe maintainers may or may not provide defaults for env vars within their
|
||||||
recipes regardless of commenting or not (e.g. through the use of
|
recipes regardless of commenting or not (e.g. through the use of
|
||||||
${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
|
${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
|
||||||
ArgsUsage: "<domain>",
|
Args: cobra.ExactArgs(1),
|
||||||
Flags: []cli.Flag{
|
ValidArgsFunction: func(
|
||||||
internal.DebugFlag,
|
cmd *cobra.Command,
|
||||||
internal.ChaosFlag,
|
args []string,
|
||||||
internal.OfflineFlag,
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
app := internal.ValidateApp(args)
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Chaos {
|
table, err := formatter.CreateTable()
|
||||||
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tableCol := []string{"recipe env sample", "app env"}
|
|
||||||
table := formatter.CreateTable(tableCol)
|
|
||||||
|
|
||||||
envVars, err := config.CheckEnv(app)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.
|
||||||
|
Headers(
|
||||||
|
fmt.Sprintf("%s .env.sample", app.Recipe.Name),
|
||||||
|
fmt.Sprintf("%s.env", app.Name),
|
||||||
|
).
|
||||||
|
StyleFunc(func(row, col int) lipgloss.Style {
|
||||||
|
switch {
|
||||||
|
case col == 1:
|
||||||
|
return lipgloss.NewStyle().Padding(0, 1, 0, 1).Align(lipgloss.Center)
|
||||||
|
default:
|
||||||
|
return lipgloss.NewStyle().Padding(0, 1, 0, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
envVars, err := appPkg.CheckEnv(app)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, envVar := range envVars {
|
for _, envVar := range envVars {
|
||||||
if envVar.Present {
|
if envVar.Present {
|
||||||
table.Append([]string{envVar.Name, "✅"})
|
val := []string{envVar.Name, "✅"}
|
||||||
|
table.Row(val...)
|
||||||
} else {
|
} else {
|
||||||
table.Append([]string{envVar.Name, "❌"})
|
val := []string{envVar.Name, "❌"}
|
||||||
|
table.Row(val...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table.Render()
|
if err := formatter.PrintTable(table); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
return nil
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppCheckCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
373
cli/app/cmd.go
373
cli/app/cmd.go
@ -5,106 +5,117 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/app"
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"github.com/spf13/cobra"
|
||||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appCmdCommand = cli.Command{
|
var AppCmdCommand = &cobra.Command{
|
||||||
Name: "command",
|
Use: "command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]",
|
||||||
Aliases: []string{"cmd"},
|
Aliases: []string{"cmd"},
|
||||||
Usage: "Run app commands",
|
Short: "Run app commands",
|
||||||
Description: `Run an app specific command.
|
Long: `Run an app specific command.
|
||||||
|
|
||||||
These commands are bash functions, defined in the abra.sh of the recipe itself.
|
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
|
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
|
work station by passing "--local/-l".
|
||||||
using the "-- <args>" syntax.
|
|
||||||
|
|
||||||
Example:
|
N.B. If using the "--" style to pass arguments, flags (e.g. "--local/-l") must
|
||||||
|
be passed *before* the "--". It is possible to pass arguments without the "--"
|
||||||
|
as long as no dashes are present (i.e. "foo" works without "--", "-foo"
|
||||||
|
does not).`,
|
||||||
|
Example: ` # pass <cmd> args/flags without "--"
|
||||||
|
abra app cmd 1312.net app my_cmd_arg foo --user bar
|
||||||
|
|
||||||
abra app cmd example.com app create_user -- me@example.com
|
# pass <cmd> args/flags with "--"
|
||||||
`,
|
abra app cmd 1312.net app my_cmd_args --user bar -- foo -vvv
|
||||||
ArgsUsage: "<domain> [<service>] <command> [-- <args>]",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
|
||||||
internal.LocalCmdFlag,
|
|
||||||
internal.RemoteUserFlag,
|
|
||||||
internal.TtyFlag,
|
|
||||||
internal.OfflineFlag,
|
|
||||||
internal.ChaosFlag,
|
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
Subcommands: []cli.Command{appCmdListCommand},
|
|
||||||
BashComplete: func(ctx *cli.Context) {
|
|
||||||
args := ctx.Args()
|
|
||||||
switch len(args) {
|
|
||||||
case 0:
|
|
||||||
autocomplete.AppNameComplete(ctx)
|
|
||||||
case 1:
|
|
||||||
autocomplete.ServiceNameComplete(args.Get(0))
|
|
||||||
case 2:
|
|
||||||
cmdNameComplete(args.Get(0))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
# drop the [service] arg if using "--local/-l"
|
||||||
logrus.Fatal(err)
|
abra app cmd 1312.net my_cmd --local`,
|
||||||
}
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if local {
|
||||||
if !internal.Chaos {
|
if !(len(args) >= 2) {
|
||||||
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
|
return errors.New("requires at least 2 arguments with --local/-l")
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Offline {
|
if slices.Contains(os.Args, "--") {
|
||||||
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
|
if cmd.ArgsLenAtDash() > 2 {
|
||||||
logrus.Fatal(err)
|
return errors.New("accepts at most 2 args with --local/-l")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
|
// NOTE(d1): it is unclear how to correctly validate this case
|
||||||
logrus.Fatal(err)
|
//
|
||||||
|
// abra app cmd 1312.net app test_cmd_args foo --local
|
||||||
|
// FATAL <recipe> doesn't have a app function
|
||||||
|
//
|
||||||
|
// "app" should not be there, but there is no reliable way to detect arg
|
||||||
|
// count when the user can pass an arbitrary amount of recipe command
|
||||||
|
// arguments
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(len(args) >= 3) {
|
||||||
|
return errors.New("requires at least 3 arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch l := len(args); l {
|
||||||
|
case 0:
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
case 1:
|
||||||
|
if !local {
|
||||||
|
return autocomplete.ServiceNameComplete(args[0])
|
||||||
}
|
}
|
||||||
|
return autocomplete.CommandNameComplete(args[0])
|
||||||
|
case 2:
|
||||||
|
if !local {
|
||||||
|
return autocomplete.CommandNameComplete(args[0])
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
app := internal.ValidateApp(args)
|
||||||
|
|
||||||
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.LocalCmd && internal.RemoteUser != "" {
|
if local && remoteUser != "" {
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together"))
|
log.Fatal("cannot use --local & --user together")
|
||||||
}
|
}
|
||||||
|
|
||||||
hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd)
|
hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local)
|
||||||
|
|
||||||
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
|
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
|
||||||
if _, err := os.Stat(abraSh); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
logrus.Fatalf("%s does not exist for %s?", abraSh, app.Name)
|
log.Fatalf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name)
|
||||||
}
|
}
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.LocalCmd {
|
if local {
|
||||||
if !(len(c.Args()) >= 2) {
|
cmdName := args[1]
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
|
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdName := c.Args().Get(1)
|
log.Debugf("--local detected, running %s on local work station", cmdName)
|
||||||
if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("--local detected, running %s on local work station", cmdName)
|
|
||||||
|
|
||||||
var exportEnv string
|
var exportEnv string
|
||||||
for k, v := range app.Env {
|
for k, v := range app.Env {
|
||||||
@ -113,70 +124,95 @@ Example:
|
|||||||
|
|
||||||
var sourceAndExec string
|
var sourceAndExec string
|
||||||
if hasCmdArgs {
|
if hasCmdArgs {
|
||||||
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
|
log.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)
|
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName, parsedCmdArgs)
|
||||||
} else {
|
} else {
|
||||||
logrus.Debug("did not detect any command arguments")
|
log.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)
|
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName)
|
||||||
}
|
}
|
||||||
|
|
||||||
shell := "/bin/bash"
|
shell := "/bin/bash"
|
||||||
if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
|
if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
|
||||||
logrus.Debugf("%s does not exist locally, use /bin/sh as fallback", shell)
|
log.Debugf("%s does not exist locally, use /bin/sh as fallback", shell)
|
||||||
shell = "/bin/sh"
|
shell = "/bin/sh"
|
||||||
}
|
}
|
||||||
cmd := exec.Command(shell, "-c", sourceAndExec)
|
cmd := exec.Command(shell, "-c", sourceAndExec)
|
||||||
|
|
||||||
if err := internal.RunCmd(cmd); err != nil {
|
if err := internal.RunCmd(cmd); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !(len(c.Args()) >= 3) {
|
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
targetServiceName := c.Args().Get(1)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cmdName := c.Args().Get(2)
|
cmdName := args[2]
|
||||||
if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
|
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceNames, err := config.GetAppServiceNames(app.Name)
|
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
matchingServiceName := false
|
matchingServiceName := false
|
||||||
for _, serviceName := range serviceNames {
|
targetServiceName := args[1]
|
||||||
if serviceName == targetServiceName {
|
for _, serviceName := range serviceNames {
|
||||||
matchingServiceName = true
|
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := internal.RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if !matchingServiceName {
|
||||||
|
log.Fatalf("no service %s for %s?", targetServiceName, app.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
|
||||||
|
|
||||||
|
if hasCmdArgs {
|
||||||
|
log.Debugf("parsed following command arguments: %s", parsedCmdArgs)
|
||||||
|
} else {
|
||||||
|
log.Debug("did not detect any command arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
cl, err := client.New(app.Server)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := internal.RunCmdRemote(
|
||||||
|
cl,
|
||||||
|
app,
|
||||||
|
disableTTY,
|
||||||
|
app.Recipe.AbraShPath,
|
||||||
|
targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var AppCmdListCommand = &cobra.Command{
|
||||||
|
Use: "list <domain> [flags]",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Short: "List all available commands",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
app := internal.ValidateApp(args)
|
||||||
|
|
||||||
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(cmdNames)
|
||||||
|
|
||||||
|
for _, cmdName := range cmdNames {
|
||||||
|
fmt.Println(cmdName)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,75 +235,42 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) {
|
|||||||
return hasCmdArgs, parsedCmdArgs
|
return hasCmdArgs, parsedCmdArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdNameComplete(appName string) {
|
var (
|
||||||
app, err := app.Get(appName)
|
local bool
|
||||||
if err != nil {
|
remoteUser string
|
||||||
return
|
disableTTY bool
|
||||||
}
|
)
|
||||||
cmdNames, _ := getShCmdNames(app)
|
|
||||||
if err != nil {
|
func init() {
|
||||||
return
|
AppCmdCommand.Flags().BoolVarP(
|
||||||
}
|
&local,
|
||||||
for _, n := range cmdNames {
|
"local",
|
||||||
fmt.Println(n)
|
"l",
|
||||||
}
|
false,
|
||||||
}
|
"run command locally",
|
||||||
|
)
|
||||||
var appCmdListCommand = cli.Command{
|
|
||||||
Name: "list",
|
AppCmdCommand.Flags().StringVarP(
|
||||||
Aliases: []string{"ls"},
|
&remoteUser,
|
||||||
Usage: "List all available commands",
|
"user",
|
||||||
ArgsUsage: "<domain>",
|
"u",
|
||||||
Flags: []cli.Flag{
|
"",
|
||||||
internal.DebugFlag,
|
"request remote user",
|
||||||
internal.OfflineFlag,
|
)
|
||||||
internal.ChaosFlag,
|
|
||||||
},
|
AppCmdCommand.Flags().BoolVarP(
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
&disableTTY,
|
||||||
Before: internal.SubCommandBefore,
|
"tty",
|
||||||
Action: func(c *cli.Context) error {
|
"T",
|
||||||
app := internal.ValidateApp(c)
|
false,
|
||||||
|
"disable remote TTY",
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
)
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
AppCmdCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
if !internal.Chaos {
|
"chaos",
|
||||||
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
|
"C",
|
||||||
logrus.Fatal(err)
|
false,
|
||||||
}
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdNames, err := getShCmdNames(app)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cmdName := range cmdNames {
|
|
||||||
fmt.Println(cmdName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func getShCmdNames(app config.App) ([]string, error) {
|
|
||||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
|
|
||||||
cmdNames, err := config.ReadAbraShCmdNames(abraShPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(cmdNames)
|
|
||||||
return cmdNames, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ func TestParseCmdArgs(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
// `--` is not parsed when passed in from the command-line e.g. -- foo bar baz
|
// `--` 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
|
// 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
|
// see https://git.coopcloud.tech/toolshed/organising/issues/336 for more
|
||||||
{[]string{"foo.com", "app", "test"}, false, ""},
|
{[]string{"foo.com", "app", "test"}, false, ""},
|
||||||
{[]string{"foo.com", "app", "test", "foo"}, true, "foo "},
|
{[]string{"foo.com", "app", "test", "foo"}, true, "foo "},
|
||||||
{[]string{"foo.com", "app", "test", "foo", "bar", "baz"}, true, "foo bar baz "},
|
{[]string{"foo.com", "app", "test", "foo", "bar", "baz"}, true, "foo bar baz "},
|
||||||
|
|||||||
@ -1,64 +1,57 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appConfigCommand = cli.Command{
|
var AppConfigCommand = &cobra.Command{
|
||||||
Name: "config",
|
Use: "config <domain> [flags]",
|
||||||
Aliases: []string{"cfg"},
|
Aliases: []string{"cfg"},
|
||||||
Usage: "Edit app config",
|
Short: "Edit app config",
|
||||||
ArgsUsage: "<domain>",
|
Example: " abra config 1312.net",
|
||||||
Flags: []cli.Flag{
|
Args: cobra.ExactArgs(1),
|
||||||
internal.DebugFlag,
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
files, err := appPkg.LoadAppFiles("")
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
appName := c.Args().First()
|
|
||||||
|
|
||||||
if appName == "" {
|
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("no app provided"))
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := config.LoadAppFiles("")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appName := args[0]
|
||||||
appFile, exists := files[appName]
|
appFile, exists := files[appName]
|
||||||
if !exists {
|
if !exists {
|
||||||
logrus.Fatalf("cannot find app with name %s", appName)
|
log.Fatalf("cannot find app with name %s", appName)
|
||||||
}
|
}
|
||||||
|
|
||||||
ed, ok := os.LookupEnv("EDITOR")
|
ed, ok := os.LookupEnv("EDITOR")
|
||||||
if !ok {
|
if !ok {
|
||||||
edPrompt := &survey.Select{
|
edPrompt := &survey.Select{
|
||||||
Message: "Which editor do you wish to use?",
|
Message: "which editor do you wish to use?",
|
||||||
Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"},
|
Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"},
|
||||||
}
|
}
|
||||||
if err := survey.AskOne(edPrompt, &ed); err != nil {
|
if err := survey.AskOne(edPrompt, &ed); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(ed, appFile.Path)
|
c := exec.Command(ed, appFile.Path)
|
||||||
cmd.Stdin = os.Stdin
|
c.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
c.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
c.Stderr = os.Stderr
|
||||||
if err := cmd.Run(); err != nil {
|
if err := c.Run(); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,65 +15,61 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
containerPkg "coopcloud.tech/abra/pkg/container"
|
containerPkg "coopcloud.tech/abra/pkg/container"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/upstream/container"
|
"coopcloud.tech/abra/pkg/upstream/container"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/docker/api/types"
|
containertypes "github.com/docker/docker/api/types/container"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/docker/pkg/archive"
|
"github.com/docker/docker/pkg/archive"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appCpCommand = cli.Command{
|
var AppCpCommand = &cobra.Command{
|
||||||
Name: "cp",
|
Use: "cp <domain> <src> <dst> [flags]",
|
||||||
Aliases: []string{"c"},
|
Aliases: []string{"c"},
|
||||||
ArgsUsage: "<domain> <src> <dst>",
|
Short: "Copy files to/from a deployed app service",
|
||||||
Flags: []cli.Flag{
|
Example: ` # copy myfile.txt to the root of the app service
|
||||||
internal.DebugFlag,
|
abra app cp 1312.net myfile.txt app:/
|
||||||
internal.NoInputFlag,
|
|
||||||
|
# copy that file back to your current working directory locally
|
||||||
|
abra app cp 1312.net app:/myfile.txt ./`,
|
||||||
|
Args: cobra.ExactArgs(3),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch l := len(args); l {
|
||||||
|
case 0:
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Usage: "Copy files to/from a deployed app service",
|
app := internal.ValidateApp(args)
|
||||||
Description: `
|
|
||||||
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:
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
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 <domain> app:/myfile.txt .
|
|
||||||
`,
|
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
src := c.Args().Get(1)
|
|
||||||
dst := c.Args().Get(2)
|
|
||||||
if src == "" {
|
|
||||||
logrus.Fatal("missing <src> argument")
|
|
||||||
}
|
|
||||||
if dst == "" {
|
|
||||||
logrus.Fatal("missing <dest> argument")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
src := args[1]
|
||||||
|
dst := args[2]
|
||||||
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
|
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
|
container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
|
log.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
|
||||||
|
|
||||||
if toContainer {
|
if toContainer {
|
||||||
err = CopyToContainer(cl, container.ID, srcPath, dstPath)
|
err = CopyToContainer(cl, container.ID, srcPath, dstPath)
|
||||||
@ -81,10 +77,8 @@ And if you want to copy that file back to your current working directory locally
|
|||||||
err = CopyFromContainer(cl, container.ID, srcPath, dstPath)
|
err = CopyFromContainer(cl, container.ID, srcPath, dstPath)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +134,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{
|
if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
AttachStdin: true,
|
AttachStdin: true,
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
@ -167,8 +161,8 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("copy %s from local to %s on container", srcPath, dstPath)
|
log.Debugf("copy %s from local to %s on container", srcPath, dstPath)
|
||||||
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
copyOpts := containertypes.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||||
if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
|
if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -179,7 +173,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{
|
if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
AttachStdin: true,
|
AttachStdin: true,
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
@ -377,3 +371,13 @@ func moveFile(sourcePath, destPath string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppCpCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -3,252 +3,352 @@ package app
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
"coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
|
"coopcloud.tech/abra/pkg/config"
|
||||||
|
"coopcloud.tech/abra/pkg/envfile"
|
||||||
"coopcloud.tech/abra/pkg/secret"
|
"coopcloud.tech/abra/pkg/secret"
|
||||||
|
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
"coopcloud.tech/abra/pkg/dns"
|
"coopcloud.tech/abra/pkg/dns"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/git"
|
|
||||||
"coopcloud.tech/abra/pkg/lint"
|
"coopcloud.tech/abra/pkg/lint"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"github.com/sirupsen/logrus"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/urfave/cli"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var appDeployCommand = cli.Command{
|
var AppDeployCommand = &cobra.Command{
|
||||||
Name: "deploy",
|
Use: "deploy <domain> [version] [flags]",
|
||||||
Aliases: []string{"d"},
|
Aliases: []string{"d"},
|
||||||
Usage: "Deploy an app",
|
Short: "Deploy an app",
|
||||||
ArgsUsage: "<domain> [<version>]",
|
Long: `Deploy an app.
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
This command supports chaos operations. Use "--chaos/-C" to deploy your recipe
|
||||||
internal.NoInputFlag,
|
checkout as-is. Recipe commit hashes are also supported as values for
|
||||||
internal.ForceFlag,
|
"[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`,
|
||||||
internal.ChaosFlag,
|
Example: ` # standard deployment
|
||||||
internal.NoDomainChecksFlag,
|
abra app deploy 1312.net
|
||||||
internal.DontWaitConvergeFlag,
|
|
||||||
internal.OfflineFlag,
|
# chaos deployment
|
||||||
|
abra app deploy 1312.net --chaos
|
||||||
|
|
||||||
|
# deploy specific version
|
||||||
|
abra app deploy 1312.net 2.0.0+1.2.3
|
||||||
|
|
||||||
|
# deploy a specific git hash
|
||||||
|
abra app deploy 1312.net 886db76d`,
|
||||||
|
Args: cobra.RangeArgs(1, 2),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string,
|
||||||
|
) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch l := len(args); l {
|
||||||
|
case 0:
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
case 1:
|
||||||
|
app, err := appPkg.Get(args[0])
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
|
return []string{errMsg}, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Description: `
|
var (
|
||||||
Deploy an app. It does not support incrementing the version of a deployed app,
|
deployWarnMessages []string
|
||||||
for this you need to look at the "abra app upgrade <domain>" command.
|
toDeployVersion string
|
||||||
|
)
|
||||||
|
|
||||||
You may pass "--force" to re-deploy the same version again. This can be useful
|
app := internal.ValidateApp(args)
|
||||||
if the container runtime has gotten into a weird state.
|
|
||||||
|
|
||||||
Chaos mode ("--chaos") will deploy your local checkout of a recipe as-is,
|
if err := validateArgsAndFlags(args); err != nil {
|
||||||
including unstaged changes and can be useful for live hacking and testing new
|
log.Fatal(err)
|
||||||
recipes.
|
|
||||||
`,
|
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
stackName := app.StackName()
|
|
||||||
|
|
||||||
specificVersion := c.Args().Get(1)
|
|
||||||
if specificVersion != "" && internal.Chaos {
|
|
||||||
logrus.Fatal("cannot use <version> and --chaos together")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Chaos {
|
|
||||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := recipe.Get(app.Recipe, internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := lint.LintForErrors(r); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("checking whether %s is already deployed", stackName)
|
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
|
log.Debugf("checking whether %s is already deployed", app.StackName())
|
||||||
|
|
||||||
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secStats, err := secret.PollSecretsStatus(cl, app)
|
if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) {
|
||||||
|
log.Fatalf("%s is already deployed", app.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
toDeployVersion, err = getDeployVersion(args, deployMeta, app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(fmt.Errorf("get deploy version: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, secStat := range secStats {
|
if !internal.Chaos {
|
||||||
if !secStat.CreatedOnRemote {
|
_, err = app.Recipe.EnsureVersion(toDeployVersion)
|
||||||
logrus.Fatalf("unable to deploy, secrets not generated (%s)?", secStat.LocalName)
|
if err != nil {
|
||||||
|
log.Fatalf("ensure recipe: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isDeployed {
|
if err := lint.LintForErrors(app.Recipe); err != nil {
|
||||||
if internal.Force || internal.Chaos {
|
if internal.Chaos {
|
||||||
logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name)
|
log.Warn(err)
|
||||||
} else {
|
} else {
|
||||||
logrus.Fatalf("%s is already deployed", app.Name)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
version := deployedVersion
|
if err := validateSecrets(cl, app); err != nil {
|
||||||
if specificVersion != "" {
|
log.Fatal(err)
|
||||||
version = specificVersion
|
|
||||||
logrus.Debugf("choosing %s as version to deploy", version)
|
|
||||||
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Chaos && specificVersion == "" {
|
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
|
||||||
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(versions) == 0 && !internal.Chaos {
|
|
||||||
logrus.Warn("no published versions in catalogue, trying local recipe repository")
|
|
||||||
recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Warn(err)
|
|
||||||
}
|
|
||||||
for _, recipeVersion := range recipeVersions {
|
|
||||||
for version := range recipeVersion {
|
|
||||||
versions = append(versions, version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(versions) > 0 && !internal.Chaos {
|
|
||||||
version = versions[len(versions)-1]
|
|
||||||
logrus.Debugf("choosing %s as version to deploy", version)
|
|
||||||
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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 internal.Chaos {
|
|
||||||
logrus.Warnf("chaos mode engaged")
|
|
||||||
var err error
|
|
||||||
version, err = recipe.ChaosVersion(app.Recipe)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
|
|
||||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
for k, v := range abraShEnv {
|
for k, v := range abraShEnv {
|
||||||
app.Env[k] = v
|
app.Env[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stackName := app.StackName()
|
||||||
deployOpts := stack.Deploy{
|
deployOpts := stack.Deploy{
|
||||||
Composefiles: composeFiles,
|
Composefiles: composeFiles,
|
||||||
Namespace: stackName,
|
Namespace: stackName,
|
||||||
Prune: false,
|
Prune: false,
|
||||||
ResolveImage: stack.ResolveImageAlways,
|
ResolveImage: stack.ResolveImageAlways,
|
||||||
|
Detach: false,
|
||||||
}
|
}
|
||||||
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.ExposeAllEnv(stackName, compose, app.Env)
|
appPkg.ExposeAllEnv(stackName, compose, app.Env)
|
||||||
config.SetRecipeLabel(compose, stackName, app.Recipe)
|
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
|
||||||
config.SetChaosLabel(compose, stackName, internal.Chaos)
|
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
|
||||||
config.SetChaosVersionLabel(compose, stackName, version)
|
if internal.Chaos {
|
||||||
config.SetUpdateLabel(compose, stackName, app.Env)
|
appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
|
||||||
|
}
|
||||||
|
appPkg.SetUpdateLabel(compose, stackName, app.Env)
|
||||||
|
appPkg.SetVersionLabel(compose, stackName, toDeployVersion)
|
||||||
|
|
||||||
envVars, err := config.CheckEnv(app)
|
envVars, err := appPkg.CheckEnv(app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, envVar := range envVars {
|
for _, envVar := range envVars {
|
||||||
if !envVar.Present {
|
if !envVar.Present {
|
||||||
logrus.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
|
deployWarnMessages = append(deployWarnMessages,
|
||||||
|
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := internal.DeployOverview(app, version, "continue with deployment?"); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.NoDomainChecks {
|
if !internal.NoDomainChecks {
|
||||||
domainName, ok := app.Env["DOMAIN"]
|
if domainName, ok := app.Env["DOMAIN"]; ok {
|
||||||
if ok {
|
|
||||||
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
|
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logrus.Warn("skipping domain checks as no DOMAIN=... configured for app")
|
log.Debug("skipping domain checks, no DOMAIN=... configured")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logrus.Warn("skipping domain checks as requested")
|
log.Debug("skipping domain checks")
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName)
|
deployedVersion := config.NO_VERSION_DEFAULT
|
||||||
|
if deployMeta.IsDeployed {
|
||||||
|
deployedVersion = deployMeta.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := internal.DeployOverview(
|
||||||
|
app,
|
||||||
|
deployedVersion,
|
||||||
|
toDeployVersion,
|
||||||
|
"",
|
||||||
|
deployWarnMessages,
|
||||||
|
); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
|
|
||||||
|
|
||||||
if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil {
|
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
|
||||||
logrus.Fatal(err)
|
|
||||||
|
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := app.Filters(true, false, serviceNames...)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stack.RunDeploy(
|
||||||
|
cl,
|
||||||
|
deployOpts,
|
||||||
|
compose,
|
||||||
|
app.Name,
|
||||||
|
app.Server,
|
||||||
|
internal.DontWaitConverge,
|
||||||
|
f,
|
||||||
|
); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
|
postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
|
||||||
if ok && !internal.DontWaitConverge {
|
if ok && !internal.DontWaitConverge {
|
||||||
logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
|
log.Debugf("run the following post-deploy commands: %s", postDeployCmds)
|
||||||
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
|
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
|
||||||
logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
|
log.Fatalf("attempting to run post deploy commands, saw: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
if err := app.WriteRecipeVersion(toDeployVersion, false); err != nil {
|
||||||
|
log.Fatalf("writing recipe version failed: %s", err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLatestVersionOrCommit(app app.App) (string, error) {
|
||||||
|
versions, err := app.Recipe.Tags()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(versions) > 0 && !internal.Chaos {
|
||||||
|
return versions[len(versions)-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
head, err := app.Recipe.Head()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter.SmallSHA(head.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateArgsAndFlags ensures compatible args/flags.
|
||||||
|
func validateArgsAndFlags(args []string) error {
|
||||||
|
if len(args) == 2 && args[1] != "" && internal.Chaos {
|
||||||
|
return fmt.Errorf("cannot use [version] and --chaos together")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSecrets(cl *dockerClient.Client, app app.App) error {
|
||||||
|
secStats, err := secret.PollSecretsStatus(cl, app)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, secStat := range secStats {
|
||||||
|
if !secStat.CreatedOnRemote {
|
||||||
|
return fmt.Errorf("secret not generated: %s", secStat.LocalName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app app.App) (string, error) {
|
||||||
|
// Chaos mode overrides everything
|
||||||
|
if internal.Chaos {
|
||||||
|
v, err := app.Recipe.ChaosVersion()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
log.Debugf("version: taking chaos version: %s", v)
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the deploy version is set with a cli argument
|
||||||
|
if len(cliArgs) == 2 && cliArgs[1] != "" {
|
||||||
|
log.Debugf("version: taking version from cli arg: %s", cliArgs[1])
|
||||||
|
return cliArgs[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the recipe has a version in the .env file
|
||||||
|
if app.Recipe.EnvVersion != "" && !internal.IgnoreEnvVersion {
|
||||||
|
if strings.HasSuffix(app.Recipe.EnvVersionRaw, "+U") {
|
||||||
|
return "", fmt.Errorf("version: can not redeploy chaos version %s", app.Recipe.EnvVersionRaw)
|
||||||
|
}
|
||||||
|
log.Debugf("version: taking version from .env file: %s", app.Recipe.EnvVersion)
|
||||||
|
return app.Recipe.EnvVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take deployed version
|
||||||
|
if deployMeta.IsDeployed {
|
||||||
|
log.Debugf("version: taking deployed version: %s", deployMeta.Version)
|
||||||
|
return deployMeta.Version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := getLatestVersionOrCommit(app)
|
||||||
|
log.Debugf("version: taking new recipe version: %s", v)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppDeployCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppDeployCommand.Flags().BoolVarP(
|
||||||
|
&internal.Force,
|
||||||
|
"force",
|
||||||
|
"f",
|
||||||
|
false,
|
||||||
|
"perform action without further prompt",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppDeployCommand.Flags().BoolVarP(
|
||||||
|
&internal.NoDomainChecks,
|
||||||
|
"no-domain-checks",
|
||||||
|
"D",
|
||||||
|
false,
|
||||||
|
"disable public DNS checks",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppDeployCommand.Flags().BoolVarP(
|
||||||
|
&internal.DontWaitConverge,
|
||||||
|
"no-converge-checks",
|
||||||
|
"c",
|
||||||
|
false,
|
||||||
|
"disable converge logic checks",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
43
cli/app/env.go
Normal file
43
cli/app/env.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppEnvCommand = &cobra.Command{
|
||||||
|
Use: "env <domain> [flags]",
|
||||||
|
Aliases: []string{"e"},
|
||||||
|
Short: "Show app .env values",
|
||||||
|
Example: " abra app env 1312.net",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
app := internal.ValidateApp(args)
|
||||||
|
|
||||||
|
var envKeys []string
|
||||||
|
for k := range app.Env {
|
||||||
|
envKeys = append(envKeys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(envKeys)
|
||||||
|
|
||||||
|
var rows [][]string
|
||||||
|
for _, k := range envKeys {
|
||||||
|
rows = append(rows, []string{k, app.Env[k]})
|
||||||
|
}
|
||||||
|
|
||||||
|
overview := formatter.CreateOverview("ENV OVERVIEW", rows)
|
||||||
|
fmt.Println(overview)
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,142 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
|
||||||
"coopcloud.tech/abra/pkg/client"
|
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
|
||||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
var appErrorsCommand = cli.Command{
|
|
||||||
Name: "errors",
|
|
||||||
Usage: "List errors for a deployed app",
|
|
||||||
ArgsUsage: "<domain>",
|
|
||||||
Description: `
|
|
||||||
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
|
|
||||||
is failing to deploy or having issues, it could be a lot of things.
|
|
||||||
|
|
||||||
This command currently takes into account:
|
|
||||||
|
|
||||||
Is the service deployed?
|
|
||||||
Is the service killed by an OOM error?
|
|
||||||
Is the service reporting an error (like in "ps --no-trunc" output)
|
|
||||||
Is the service healthcheck failing? what are the healthcheck logs?
|
|
||||||
|
|
||||||
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 <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.WatchFlag,
|
|
||||||
internal.OfflineFlag,
|
|
||||||
},
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Watch {
|
|
||||||
if err := checkErrors(c, cl, app); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
if err := checkErrors(c, cl, app); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error {
|
|
||||||
recipe, err := recipe.Get(app.Recipe, internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, service := range recipe.Config.Services {
|
|
||||||
filters := filters.NewArgs()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(containers) == 0 {
|
|
||||||
logrus.Warnf("%s is not up, something seems wrong", service.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
container := containers[0]
|
|
||||||
containerState, err := cl.ContainerInspect(context.Background(), container.ID)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if containerState.State.OOMKilled {
|
|
||||||
logrus.Warnf("%s has been killed due to an out of memory error", service.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if containerState.State.Error != "" {
|
|
||||||
logrus.Warnf("%s reports this error: %s", service.Name, containerState.State.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if containerState.State.Health != nil {
|
|
||||||
if containerState.State.Health.Status != "healthy" {
|
|
||||||
logrus.Warnf("%s healthcheck status is %s", service.Name, containerState.State.Health.Status)
|
|
||||||
logrus.Warnf("%s healthcheck has failed %s times", service.Name, strconv.Itoa(containerState.State.Health.FailingStreak))
|
|
||||||
for _, log := range containerState.State.Health.Log {
|
|
||||||
logrus.Warnf("%s healthcheck logs: %s", service.Name, strings.TrimSpace(log.Output))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServiceName(names []string) string {
|
|
||||||
containerName := strings.Join(names, " ")
|
|
||||||
trimmed := strings.TrimPrefix(containerName, "/")
|
|
||||||
return strings.Split(trimmed, ".")[0]
|
|
||||||
}
|
|
||||||
139
cli/app/labels.go
Normal file
139
cli/app/labels.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
|
"coopcloud.tech/abra/pkg/client"
|
||||||
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||||
|
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/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppLabelsCommand = &cobra.Command{
|
||||||
|
Use: "labels <domain> [flags]",
|
||||||
|
Aliases: []string{"lb"},
|
||||||
|
Short: "Show deployment labels",
|
||||||
|
Long: "Both local recipe and live deployment labels are shown.",
|
||||||
|
Example: " abra app labels 1312.net",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
app := internal.ValidateApp(args)
|
||||||
|
|
||||||
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cl, err := client.New(app.Server)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteLabels, err := getLabels(cl, app.StackName())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := [][]string{
|
||||||
|
{"DEPLOYED LABELS", "---"},
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteLabelKeys := make([]string, 0, len(remoteLabels))
|
||||||
|
for k := range remoteLabels {
|
||||||
|
remoteLabelKeys = append(remoteLabelKeys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(remoteLabelKeys)
|
||||||
|
|
||||||
|
for _, k := range remoteLabelKeys {
|
||||||
|
rows = append(rows, []string{
|
||||||
|
k,
|
||||||
|
remoteLabels[k],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(remoteLabelKeys) == 0 {
|
||||||
|
rows = append(rows, []string{"unknown"})
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, []string{"RECIPE LABELS", "---"})
|
||||||
|
|
||||||
|
config, err := app.Recipe.GetComposeConfig(app.Env)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var localLabelKeys []string
|
||||||
|
var appServiceConfig composetypes.ServiceConfig
|
||||||
|
for _, service := range config.Services {
|
||||||
|
if service.Name == "app" {
|
||||||
|
appServiceConfig = service
|
||||||
|
|
||||||
|
for k := range service.Deploy.Labels {
|
||||||
|
localLabelKeys = append(localLabelKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(localLabelKeys)
|
||||||
|
|
||||||
|
for _, k := range localLabelKeys {
|
||||||
|
rows = append(rows, []string{
|
||||||
|
k,
|
||||||
|
appServiceConfig.Deploy.Labels[k],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
overview := formatter.CreateOverview("LABELS OVERVIEW", rows)
|
||||||
|
fmt.Println(overview)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLabels reads docker labels from running services in the format of "coop-cloud.${STACK_NAME}.${LABEL}".
|
||||||
|
func getLabels(cl *dockerClient.Client, stackName string) (map[string]string, error) {
|
||||||
|
labels := 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 labels, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, service := range services {
|
||||||
|
if service.Spec.Name != fmt.Sprintf("%s_app", stackName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range service.Spec.Labels {
|
||||||
|
labels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppLabelsCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
}
|
||||||
229
cli/app/list.go
229
cli/app/list.go
@ -8,37 +8,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/tagcmp"
|
"coopcloud.tech/tagcmp"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var status bool
|
|
||||||
var statusFlag = &cli.BoolFlag{
|
|
||||||
Name: "status, S",
|
|
||||||
Usage: "Show app deployment status",
|
|
||||||
Destination: &status,
|
|
||||||
}
|
|
||||||
|
|
||||||
var recipeFilter string
|
|
||||||
var recipeFlag = &cli.StringFlag{
|
|
||||||
Name: "recipe, r",
|
|
||||||
Value: "",
|
|
||||||
Usage: "Show apps of a specific recipe",
|
|
||||||
Destination: &recipeFilter,
|
|
||||||
}
|
|
||||||
|
|
||||||
var listAppServer string
|
|
||||||
var listAppServerFlag = &cli.StringFlag{
|
|
||||||
Name: "server, s",
|
|
||||||
Value: "",
|
|
||||||
Usage: "Show apps of a specific server",
|
|
||||||
Destination: &listAppServer,
|
|
||||||
}
|
|
||||||
|
|
||||||
type appStatus struct {
|
type appStatus struct {
|
||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
Recipe string `json:"recipe"`
|
Recipe string `json:"recipe"`
|
||||||
@ -61,42 +38,36 @@ type serverStatus struct {
|
|||||||
UpgradeCount int `json:"upgradeCount"`
|
UpgradeCount int `json:"upgradeCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var appListCommand = cli.Command{
|
var AppListCommand = &cobra.Command{
|
||||||
Name: "list",
|
Use: "list [flags]",
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
Usage: "List all managed apps",
|
Short: "List all managed apps",
|
||||||
Description: `
|
Long: `Generate a report of all managed 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
|
Use "--status/-S" flag to query all servers for the live deployment status.`,
|
||||||
actual live deployment status. Depending on how many servers you manage, this
|
Example: ` # list apps of all servers without live status
|
||||||
can take some time.
|
abra app ls
|
||||||
`,
|
|
||||||
Flags: []cli.Flag{
|
# list apps of a specific server with live status
|
||||||
internal.DebugFlag,
|
abra app ls -s 1312.net -S
|
||||||
internal.MachineReadableFlag,
|
|
||||||
statusFlag,
|
# list apps of all servers which match a specific recipe
|
||||||
listAppServerFlag,
|
abra app ls -r gitea`,
|
||||||
recipeFlag,
|
Args: cobra.NoArgs,
|
||||||
internal.OfflineFlag,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
},
|
appFiles, err := appPkg.LoadAppFiles(listAppServer)
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
appFiles, err := config.LoadAppFiles(listAppServer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
apps, err := config.GetApps(appFiles, recipeFilter)
|
apps, err := appPkg.GetApps(appFiles, recipeFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(config.ByServerAndRecipe(apps))
|
sort.Sort(appPkg.ByServerAndRecipe(apps))
|
||||||
|
|
||||||
statuses := make(map[string]map[string]string)
|
statuses := make(map[string]map[string]string)
|
||||||
var catl recipe.RecipeCatalogue
|
|
||||||
if status {
|
if status {
|
||||||
alreadySeen := make(map[string]bool)
|
alreadySeen := make(map[string]bool)
|
||||||
for _, app := range apps {
|
for _, app := range apps {
|
||||||
@ -105,14 +76,9 @@ can take some time.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses, err = config.GetAppStatuses(apps, internal.MachineReadable)
|
statuses, err = appPkg.GetAppStatuses(apps, internal.MachineReadable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
|
||||||
|
|
||||||
catl, err = recipe.ReadRecipeCatalogue(internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +96,7 @@ can take some time.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.Recipe == recipeFilter || recipeFilter == "" {
|
if app.Recipe.Name == recipeFilter || recipeFilter == "" {
|
||||||
if recipeFilter != "" {
|
if recipeFilter != "" {
|
||||||
// only count server if matches filter
|
// only count server if matches filter
|
||||||
totalServersCount++
|
totalServersCount++
|
||||||
@ -176,21 +142,25 @@ can take some time.
|
|||||||
appStats.AutoUpdate = autoUpdate
|
appStats.AutoUpdate = autoUpdate
|
||||||
|
|
||||||
var newUpdates []string
|
var newUpdates []string
|
||||||
if version != "unknown" {
|
if version != "unknown" && chaos == "false" {
|
||||||
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
|
if err := app.Recipe.EnsureExists(); err != nil {
|
||||||
|
log.Fatalf("unable to clone %s: %s", app.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updates, err := app.Recipe.Tags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatalf("unable to retrieve tags for %s: %s", app.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedVersion, err := tagcmp.Parse(version)
|
parsedVersion, err := tagcmp.Parse(version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, update := range updates {
|
for _, update := range updates {
|
||||||
parsedUpdate, err := tagcmp.Parse(update)
|
parsedUpdate, err := tagcmp.Parse(update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
|
if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
|
||||||
@ -207,14 +177,14 @@ can take some time.
|
|||||||
stats.LatestCount++
|
stats.LatestCount++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newUpdates = internal.ReverseStringList(newUpdates)
|
newUpdates = internal.SortVersionsDesc(newUpdates)
|
||||||
appStats.Upgrade = strings.Join(newUpdates, "\n")
|
appStats.Upgrade = strings.Join(newUpdates, "\n")
|
||||||
stats.UpgradeCount++
|
stats.UpgradeCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appStats.Server = app.Server
|
appStats.Server = app.Server
|
||||||
appStats.Recipe = app.Recipe
|
appStats.Recipe = app.Recipe.Name
|
||||||
appStats.AppName = app.Name
|
appStats.AppName = app.Name
|
||||||
appStats.Domain = app.Domain
|
appStats.Domain = app.Domain
|
||||||
|
|
||||||
@ -226,11 +196,12 @@ can take some time.
|
|||||||
if internal.MachineReadable {
|
if internal.MachineReadable {
|
||||||
jsonstring, err := json.Marshal(allStats)
|
jsonstring, err := json.Marshal(allStats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(string(jsonstring))
|
fmt.Println(string(jsonstring))
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
alreadySeen := make(map[string]bool)
|
alreadySeen := make(map[string]bool)
|
||||||
@ -241,60 +212,118 @@ can take some time.
|
|||||||
|
|
||||||
serverStat := allStats[app.Server]
|
serverStat := allStats[app.Server]
|
||||||
|
|
||||||
tableCol := []string{"recipe", "domain"}
|
headers := []string{"RECIPE", "DOMAIN", "SERVER"}
|
||||||
if status {
|
if status {
|
||||||
tableCol = append(tableCol, []string{"status", "chaos", "version", "upgrade", "autoupdate"}...)
|
headers = append(headers, []string{
|
||||||
|
"STATUS",
|
||||||
|
"CHAOS",
|
||||||
|
"VERSION",
|
||||||
|
"UPGRADE",
|
||||||
|
"AUTOUPDATE"}...,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
table := formatter.CreateTable(tableCol)
|
table, err := formatter.CreateTable()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Headers(headers...)
|
||||||
|
|
||||||
|
var rows [][]string
|
||||||
for _, appStat := range serverStat.Apps {
|
for _, appStat := range serverStat.Apps {
|
||||||
tableRow := []string{appStat.Recipe, appStat.Domain}
|
row := []string{appStat.Recipe, appStat.Domain, appStat.Server}
|
||||||
if status {
|
if status {
|
||||||
chaosStatus := appStat.Chaos
|
chaosStatus := appStat.Chaos
|
||||||
if chaosStatus != "unknown" {
|
if chaosStatus != "unknown" {
|
||||||
chaosEnabled, err := strconv.ParseBool(chaosStatus)
|
chaosEnabled, err := strconv.ParseBool(chaosStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
if chaosEnabled && appStat.ChaosVersion != "unknown" {
|
if chaosEnabled && appStat.ChaosVersion != "unknown" {
|
||||||
chaosStatus = appStat.ChaosVersion
|
chaosStatus = appStat.ChaosVersion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tableRow = append(tableRow, []string{appStat.Status, chaosStatus, appStat.Version, appStat.Upgrade, appStat.AutoUpdate}...)
|
|
||||||
|
row = append(row, []string{
|
||||||
|
appStat.Status,
|
||||||
|
chaosStatus,
|
||||||
|
appStat.Version,
|
||||||
|
appStat.Upgrade,
|
||||||
|
appStat.AutoUpdate}...,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
table.Append(tableRow)
|
|
||||||
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
if table.NumLines() > 0 {
|
table.Rows(rows...)
|
||||||
table.Render()
|
|
||||||
|
|
||||||
if status {
|
if len(rows) > 0 {
|
||||||
fmt.Println(fmt.Sprintf(
|
if err := formatter.PrintTable(table); err != nil {
|
||||||
"server: %s | total apps: %v | versioned: %v | unversioned: %v | latest: %v | upgrade: %v",
|
log.Fatal(err)
|
||||||
app.Server,
|
|
||||||
serverStat.AppCount,
|
|
||||||
serverStat.VersionCount,
|
|
||||||
serverStat.UnversionedCount,
|
|
||||||
serverStat.LatestCount,
|
|
||||||
serverStat.UpgradeCount,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
fmt.Println(fmt.Sprintf("server: %s | total apps: %v", app.Server, serverStat.AppCount))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(allStats) > 1 && table.NumLines() > 0 {
|
if len(allStats) > 1 && len(rows) > 0 {
|
||||||
fmt.Println() // newline separator for multiple servers
|
fmt.Println() // newline separator for multiple servers
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
alreadySeen[app.Server] = true
|
alreadySeen[app.Server] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(allStats) > 1 {
|
|
||||||
fmt.Println(fmt.Sprintf("total servers: %v | total apps: %v ", totalServersCount, totalAppsCount))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
status bool
|
||||||
|
recipeFilter string
|
||||||
|
listAppServer string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppListCommand.Flags().BoolVarP(
|
||||||
|
&status,
|
||||||
|
"status",
|
||||||
|
"S",
|
||||||
|
false,
|
||||||
|
"show app deployment status",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppListCommand.Flags().StringVarP(
|
||||||
|
&recipeFilter,
|
||||||
|
"recipe",
|
||||||
|
"r",
|
||||||
|
"",
|
||||||
|
"show apps of a specific recipe",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppListCommand.RegisterFlagCompletionFunc(
|
||||||
|
"recipe",
|
||||||
|
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.RecipeNameComplete()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
AppListCommand.Flags().BoolVarP(
|
||||||
|
&internal.MachineReadable,
|
||||||
|
"machine",
|
||||||
|
"m",
|
||||||
|
false,
|
||||||
|
"print machine-readable output",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppListCommand.Flags().StringVarP(
|
||||||
|
&listAppServer,
|
||||||
|
"server",
|
||||||
|
"s",
|
||||||
|
"",
|
||||||
|
"show apps of a specific server",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppListCommand.RegisterFlagCompletionFunc(
|
||||||
|
"server",
|
||||||
|
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.ServerNameComplete()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
176
cli/app/logs.go
176
cli/app/logs.go
@ -2,138 +2,106 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"fmt"
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/logs"
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/spf13/cobra"
|
||||||
"github.com/docker/docker/api/types/filters"
|
|
||||||
"github.com/docker/docker/api/types/swarm"
|
|
||||||
dockerClient "github.com/docker/docker/client"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appLogsCommand = cli.Command{
|
var AppLogsCommand = &cobra.Command{
|
||||||
Name: "logs",
|
Use: "logs <domain> [service] [flags]",
|
||||||
Aliases: []string{"l"},
|
Aliases: []string{"l"},
|
||||||
ArgsUsage: "<domain> [<service>]",
|
Short: "Tail app logs",
|
||||||
Usage: "Tail app logs",
|
Args: cobra.RangeArgs(1, 2),
|
||||||
Flags: []cli.Flag{
|
ValidArgsFunction: func(
|
||||||
internal.StdErrOnlyFlag,
|
cmd *cobra.Command,
|
||||||
internal.SinceLogsFlag,
|
args []string,
|
||||||
internal.DebugFlag,
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch l := len(args); l {
|
||||||
|
case 0:
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
case 1:
|
||||||
|
app, err := appPkg.Get(args[0])
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
|
return []string{errMsg}, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
return autocomplete.ServiceNameComplete(app.Name)
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
app := internal.ValidateApp(args)
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
stackName := app.StackName()
|
stackName := app.StackName()
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
if err := app.Recipe.EnsureExists(); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, stackName)
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDeployed {
|
if !deployMeta.IsDeployed {
|
||||||
logrus.Fatalf("%s is not deployed?", app.Name)
|
log.Fatalf("%s is not deployed?", app.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceName := c.Args().Get(1)
|
var serviceNames []string
|
||||||
serviceNames := []string{}
|
if len(args) == 2 {
|
||||||
if serviceName != "" {
|
serviceNames = []string{args[1]}
|
||||||
serviceNames = []string{serviceName}
|
|
||||||
}
|
}
|
||||||
err = tailLogs(cl, app, serviceNames)
|
|
||||||
|
f, err := app.Filters(true, false, serviceNames...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
opts := logs.TailOpts{
|
||||||
|
AppName: app.Name,
|
||||||
|
Services: serviceNames,
|
||||||
|
StdErr: stdErr,
|
||||||
|
Since: sinceLogs,
|
||||||
|
Filters: f,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logs.TailLogs(cl, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// tailLogs prints logs for the given app with optional service names to be
|
var (
|
||||||
// filtered on. It also checks if the latest task is not runnning and then
|
stdErr bool
|
||||||
// prints the past tasks.
|
sinceLogs string
|
||||||
func tailLogs(cl *dockerClient.Client, app config.App, serviceNames []string) error {
|
)
|
||||||
f, err := app.Filters(true, false, serviceNames...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: f})
|
func init() {
|
||||||
if err != nil {
|
AppLogsCommand.Flags().BoolVarP(
|
||||||
return err
|
&stdErr,
|
||||||
}
|
"stderr",
|
||||||
|
"s",
|
||||||
|
false,
|
||||||
|
"only tail stderr",
|
||||||
|
)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
AppLogsCommand.Flags().StringVarP(
|
||||||
for _, service := range services {
|
&sinceLogs,
|
||||||
filters := filters.NewArgs()
|
"since",
|
||||||
filters.Add("name", service.Spec.Name)
|
"S",
|
||||||
tasks, err := cl.TaskList(context.Background(), types.TaskListOptions{Filters: f})
|
"",
|
||||||
if err != nil {
|
"tail logs since YYYY-MM-DDTHH:MM:SSZ",
|
||||||
return err
|
)
|
||||||
}
|
|
||||||
if len(tasks) > 0 {
|
|
||||||
// Need to sort the tasks by the CreatedAt field in the inverse order.
|
|
||||||
// Otherwise they are in the reversed order and not sorted properly.
|
|
||||||
slices.SortFunc[[]swarm.Task](tasks, func(t1, t2 swarm.Task) int {
|
|
||||||
return int(t2.Meta.CreatedAt.Unix() - t1.Meta.CreatedAt.Unix())
|
|
||||||
})
|
|
||||||
lastTask := tasks[0].Status
|
|
||||||
if lastTask.State != swarm.TaskStateRunning {
|
|
||||||
for _, task := range tasks {
|
|
||||||
logrus.Errorf("[%s] %s State %s: %s", service.Spec.Name, task.Meta.CreatedAt.Format(time.RFC3339), task.Status.State, task.Status.Err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect the logs in a go routine, so the logs from all services are
|
|
||||||
// collected in parallel.
|
|
||||||
wg.Add(1)
|
|
||||||
go func(serviceID string) {
|
|
||||||
logs, err := cl.ServiceLogs(context.Background(), serviceID, types.ContainerLogsOptions{
|
|
||||||
ShowStderr: true,
|
|
||||||
ShowStdout: !internal.StdErrOnly,
|
|
||||||
Since: internal.SinceLogs,
|
|
||||||
Until: "",
|
|
||||||
Timestamps: true,
|
|
||||||
Follow: true,
|
|
||||||
Tail: "20",
|
|
||||||
Details: false,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
defer logs.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(os.Stdout, logs)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}(service.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all log streams to be closed.
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
313
cli/app/move.go
Normal file
313
cli/app/move.go
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
"coopcloud.tech/abra/pkg/app"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
|
"coopcloud.tech/abra/pkg/client"
|
||||||
|
containerPkg "coopcloud.tech/abra/pkg/container"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
"coopcloud.tech/abra/pkg/secret"
|
||||||
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
containertypes "github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppMoveCommand = &cobra.Command{
|
||||||
|
Use: "move <domain> <server> [flags]",
|
||||||
|
Short: "Moves an app to a different server",
|
||||||
|
Long: `Move an app to a differnt server.
|
||||||
|
|
||||||
|
This will copy secrets and volumes from the old server to the new one. It will also undeploy the app from old server but not deploy it on the new. You will have to do that your self, after the move finished.
|
||||||
|
|
||||||
|
Use "--dry-run/-r" to see which secrets and volumes will be moved.`,
|
||||||
|
Example: ` # moving an app
|
||||||
|
abra app move nextcloud.example.com myserver.com`,
|
||||||
|
Args: cobra.RangeArgs(1, 2),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string,
|
||||||
|
) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch l := len(args); l {
|
||||||
|
case 0:
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
case 1:
|
||||||
|
return autocomplete.ServerNameComplete()
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
app := internal.ValidateApp(args)
|
||||||
|
if len(args) <= 1 {
|
||||||
|
log.Fatal("no server provided")
|
||||||
|
}
|
||||||
|
newServer := args[1]
|
||||||
|
|
||||||
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cl, err := client.New(app.Server)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resources, err := getAppResources(cl, app)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal.MoveOverview(app, newServer, resources.SecretNames(), resources.VolumeNames())
|
||||||
|
if err := internal.PromptProcced(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: wait timeout will be removed, until it actually is just set it to a high value.
|
||||||
|
stack.WaitTimeout = 500
|
||||||
|
rmOpts := stack.Remove{
|
||||||
|
Namespaces: []string{app.StackName()},
|
||||||
|
Detach: false,
|
||||||
|
}
|
||||||
|
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cl2, err := client.New(newServer)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range resources.SecretList {
|
||||||
|
sname := strings.Split(strings.TrimPrefix(s.Spec.Name, app.StackName()+"_"), "_")
|
||||||
|
secretName := strings.Join(sname[:len(sname)-1], "_")
|
||||||
|
data := resources.Secrets[secretName]
|
||||||
|
if err := client.StoreSecret(cl2, s.Spec.Name, data); err != nil {
|
||||||
|
log.Infof("creating secret: %s", s.Spec.Name)
|
||||||
|
log.Errorf("failed to store secret on new server: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range resources.Volumes {
|
||||||
|
log.Infof("moving volume: %s", v.Name)
|
||||||
|
|
||||||
|
// Need to create the volume before copying the data, because when
|
||||||
|
// docker creates a new volume it set the folder permissions to
|
||||||
|
// root, which might be wrong. This ensures we always have the
|
||||||
|
// correct folder permissions inside the volume.
|
||||||
|
log.Debug("creating volume: %s", v.Name)
|
||||||
|
_, err := cl2.VolumeCreate(context.Background(), volume.CreateOptions{
|
||||||
|
Name: v.Name,
|
||||||
|
Driver: v.Driver,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create volume: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := fmt.Sprintf("%s.tar.gz", v.Name)
|
||||||
|
log.Debug("creating %s", fileName)
|
||||||
|
cmd := exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo tar --same-owner -czhpf %s -C /var/lib/docker/volumes %s", fileName, v.Name))
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
log.Errorf("failed to tar volume: %s", err)
|
||||||
|
fmt.Println(string(out))
|
||||||
|
}
|
||||||
|
log.Debug("copying %s to local machine", fileName)
|
||||||
|
cmd = exec.Command("scp", fmt.Sprintf("%s:%s", app.Server, fileName), fileName)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
log.Errorf("failed to copy tar to local machine: %s", err)
|
||||||
|
fmt.Println(string(out))
|
||||||
|
}
|
||||||
|
log.Debug("copying %s to %s", fileName, newServer)
|
||||||
|
cmd = exec.Command("scp", fileName, fmt.Sprintf("%s:%s", newServer, fileName))
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
log.Errorf("failed to copy tar to new server: %s", err)
|
||||||
|
fmt.Println(string(out))
|
||||||
|
}
|
||||||
|
log.Debug("extracting %s on %s", fileName, newServer)
|
||||||
|
cmd = exec.Command("ssh", newServer, "-tt", fmt.Sprintf("sudo tar --same-owner -xzpf %s -C /var/lib/docker/volumes", fileName))
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
log.Errorf("failed to extract tar: %s", err)
|
||||||
|
fmt.Println(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove tar files
|
||||||
|
cmd = exec.Command("ssh", newServer, "-tt", fmt.Sprintf("sudo rm %s", fileName))
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
log.Errorf("failed to remove tar from new server: %s", err)
|
||||||
|
fmt.Println(string(out))
|
||||||
|
}
|
||||||
|
cmd = exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo rm %s", fileName))
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
log.Errorf("failed to remove tar from old server: %s", err)
|
||||||
|
fmt.Println(string(out))
|
||||||
|
}
|
||||||
|
cmd = exec.Command("rm", fileName)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
log.Errorf("failed to remove tar on local machine: %s", err)
|
||||||
|
fmt.Println(string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("moving app config to new server")
|
||||||
|
if err := copyFile(app.Path, strings.ReplaceAll(app.Path, app.Server, newServer)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Remove(app.Path); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("% was succefully moved to %s", app.Name, newServer)
|
||||||
|
fmt.Println("Run the following command to deploy the app", app.Name, newServer)
|
||||||
|
fmt.Println(" abra app deploy --no-domain-checks", app.Domain)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("And don't forget to update you DNS record. And don't panic, as it might take a bit for the dust to settle. Traefik for example might fail to obtain the lets encrypt certificate for a while.", app.Domain)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("If anything goes wrong, you can always move the app config file to the original server and deploy it there again. There was no data removed on the old server")
|
||||||
|
return
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppResources struct {
|
||||||
|
Secrets map[string]string
|
||||||
|
SecretList []swarm.Secret
|
||||||
|
Volumes map[string]containertypes.MountPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppResources) SecretNames() []string {
|
||||||
|
secrets := []string{}
|
||||||
|
for name := range a.Secrets {
|
||||||
|
secrets = append(secrets, name)
|
||||||
|
}
|
||||||
|
return secrets
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppResources) VolumeNames() []string {
|
||||||
|
volumes := []string{}
|
||||||
|
for name := range a.Volumes {
|
||||||
|
volumes = append(volumes, name)
|
||||||
|
}
|
||||||
|
return volumes
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAppResources(cl *dockerclient.Client, app app.App) (*AppResources, error) {
|
||||||
|
filter, err := app.Filters(false, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filter})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretConfigs, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := stack.Deploy{Composefiles: composeFiles, Namespace: app.StackName()}
|
||||||
|
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resources := &AppResources{
|
||||||
|
Secrets: make(map[string]string),
|
||||||
|
SecretList: secretList,
|
||||||
|
Volumes: make(map[string]containertypes.MountPoint),
|
||||||
|
}
|
||||||
|
for _, s := range services {
|
||||||
|
secretNames := map[string]string{}
|
||||||
|
for _, serviceCompose := range compose.Services {
|
||||||
|
if app.StackName()+"_"+serviceCompose.Name != s.Spec.Name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, secret := range serviceCompose.Secrets {
|
||||||
|
for _, s := range secretList {
|
||||||
|
if s.Spec.Name == app.StackName()+"_"+secret.Source+"_"+secretConfigs[secret.Source].Version {
|
||||||
|
secretNames[secret.Source] = s.ID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f := filters.NewArgs()
|
||||||
|
f.Add("name", s.Spec.Name)
|
||||||
|
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, f, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, m := range targetContainer.Mounts {
|
||||||
|
if m.Type == mount.TypeVolume {
|
||||||
|
resources.Volumes[m.Name] = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for secretName, secretID := range secretNames {
|
||||||
|
if _, ok := resources.Secrets[secretName]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Debugf("extracting secret %s", secretName)
|
||||||
|
|
||||||
|
out, err := exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo cat /var/lib/docker/containers/%s/mounts/secrets/%s", targetContainer.ID, secretID)).Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(string(out))
|
||||||
|
fmt.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resources.Secrets[secretName] = string(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src string, dst string) error {
|
||||||
|
// Read all content of src to data, may cause OOM for a large file.
|
||||||
|
data, err := os.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Write data to dst
|
||||||
|
err = os.WriteFile(dst, data, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppMoveCommand.Flags().BoolVarP(
|
||||||
|
&internal.Dry,
|
||||||
|
"dry-run",
|
||||||
|
"r",
|
||||||
|
false,
|
||||||
|
"report changes that would be made",
|
||||||
|
)
|
||||||
|
}
|
||||||
335
cli/app/new.go
335
cli/app/new.go
@ -2,33 +2,36 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
"coopcloud.tech/abra/pkg/app"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/jsontable"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
|
||||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||||
"coopcloud.tech/abra/pkg/secret"
|
"coopcloud.tech/abra/pkg/secret"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
"github.com/charmbracelet/lipgloss/table"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appNewDescription = `
|
var appNewDescription = `Creates a new app from a default recipe.
|
||||||
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 new app configuration is stored in your $ABRA_DIR directory under the
|
||||||
|
appropriate server.
|
||||||
|
|
||||||
This command does not deploy your app for you. You will need to run "abra app
|
This command does not deploy your app for you. You will need to run "abra app
|
||||||
deploy <domain>" to do so.
|
deploy <domain>" to do so.
|
||||||
|
|
||||||
You can see what recipes are available (i.e. values for the <recipe> argument)
|
You can see what recipes are available (i.e. values for the [recipe] argument)
|
||||||
by running "abra recipe ls".
|
by running "abra recipe ls".
|
||||||
|
|
||||||
|
Recipe commit hashes are supported values for "[version]".
|
||||||
|
|
||||||
Passing the "--secrets/-S" flag will automatically generate secrets for your
|
Passing the "--secrets/-S" flag will automatically generate secrets for your
|
||||||
app and store them encrypted at rest on the chosen target server. These
|
app and store them encrypted at rest on the chosen target server. These
|
||||||
generated secrets are only visible at generation time, so please take care to
|
generated secrets are only visible at generation time, so please take care to
|
||||||
@ -36,145 +39,188 @@ store them somewhere safe.
|
|||||||
|
|
||||||
You can use the "--pass/-P" to store these generated passwords locally in a
|
You can use the "--pass/-P" to store these generated passwords locally in a
|
||||||
pass store (see passwordstore.org for more). The pass command must be available
|
pass store (see passwordstore.org for more). The pass command must be available
|
||||||
on your $PATH.
|
on your $PATH.`
|
||||||
`
|
|
||||||
|
|
||||||
var appNewCommand = cli.Command{
|
var AppNewCommand = &cobra.Command{
|
||||||
Name: "new",
|
Use: "new [recipe] [version] [flags]",
|
||||||
Aliases: []string{"n"},
|
Aliases: []string{"n"},
|
||||||
Usage: "Create a new app",
|
Short: "Create a new app",
|
||||||
Description: appNewDescription,
|
Long: appNewDescription,
|
||||||
Flags: []cli.Flag{
|
Args: cobra.RangeArgs(0, 2),
|
||||||
internal.DebugFlag,
|
ValidArgsFunction: func(
|
||||||
internal.NoInputFlag,
|
cmd *cobra.Command,
|
||||||
internal.NewAppServerFlag,
|
args []string,
|
||||||
internal.DomainFlag,
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
internal.PassFlag,
|
switch l := len(args); l {
|
||||||
internal.SecretsFlag,
|
|
||||||
internal.OfflineFlag,
|
|
||||||
internal.ChaosFlag,
|
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
ArgsUsage: "[<recipe>] [<version>]",
|
|
||||||
BashComplete: func(ctx *cli.Context) {
|
|
||||||
args := ctx.Args()
|
|
||||||
switch len(args) {
|
|
||||||
case 0:
|
case 0:
|
||||||
autocomplete.RecipeNameComplete(ctx)
|
return autocomplete.RecipeNameComplete()
|
||||||
case 1:
|
case 1:
|
||||||
autocomplete.RecipeVersionComplete(ctx.Args().Get(0))
|
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||||
|
return autocomplete.RecipeVersionComplete(recipe.Name)
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
recipe := internal.ValidateRecipe(c)
|
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||||
|
|
||||||
if !internal.Chaos {
|
if len(args) == 2 && internal.Chaos {
|
||||||
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
|
log.Fatal("cannot use [version] and --chaos together")
|
||||||
logrus.Fatal(err)
|
}
|
||||||
|
|
||||||
|
var recipeVersion string
|
||||||
|
if len(args) == 2 {
|
||||||
|
recipeVersion = args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
chaosVersion := config.CHAOS_DEFAULT
|
||||||
|
if internal.Chaos {
|
||||||
|
var err error
|
||||||
|
chaosVersion, err = recipe.ChaosVersion()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
|
recipeVersion = chaosVersion
|
||||||
logrus.Fatal(err)
|
} else {
|
||||||
|
if err := recipe.EnsureIsClean(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var recipeVersions recipePkg.RecipeVersions
|
||||||
|
if recipeVersion == "" {
|
||||||
|
var err error
|
||||||
|
recipeVersions, _, err = recipe.GetRecipeVersions()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.Args().Get(1) == "" {
|
|
||||||
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
|
if len(recipeVersions) > 0 {
|
||||||
logrus.Fatal(err)
|
latest := recipeVersions[len(recipeVersions)-1]
|
||||||
|
for tag := range latest {
|
||||||
|
recipeVersion = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := recipePkg.EnsureVersion(recipe.Name, c.Args().Get(1)); err != nil {
|
if err := recipe.EnsureLatest(); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipeVersion == "" {
|
||||||
|
head, err := recipe.Head()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to retrieve latest commit for %s: %s", recipe.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
recipeVersion = formatter.SmallSHA(head.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ensureServerFlag(); err != nil {
|
if err := ensureServerFlag(); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil {
|
if err := ensureDomainFlag(recipe, newAppServer); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sanitisedAppName := config.SanitiseAppName(internal.Domain)
|
sanitisedAppName := appPkg.SanitiseAppName(appDomain)
|
||||||
logrus.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName)
|
log.Debugf("%s sanitised as %s for new app", appDomain, sanitisedAppName)
|
||||||
|
|
||||||
if err := config.TemplateAppEnvSample(
|
if err := appPkg.TemplateAppEnvSample(
|
||||||
recipe.Name,
|
recipe,
|
||||||
internal.Domain,
|
appDomain,
|
||||||
internal.NewAppServer,
|
newAppServer,
|
||||||
internal.Domain,
|
appDomain,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var secrets AppSecrets
|
var appSecrets AppSecrets
|
||||||
var secretTable *jsontable.JSONTable
|
var secretsTable *table.Table
|
||||||
if internal.Secrets {
|
if generateSecrets {
|
||||||
sampleEnv, err := recipe.SampleEnv()
|
sampleEnv, err := recipe.SampleEnv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
composeFiles, err := config.GetComposeFiles(recipe.Name, sampleEnv)
|
composeFiles, err := recipe.GetComposeFiles(sampleEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
|
secretsConfig, err := secret.ReadSecretsConfig(
|
||||||
secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, config.StackName(internal.Domain))
|
recipe.SampleEnvPath,
|
||||||
|
composeFiles,
|
||||||
|
appPkg.StackName(appDomain),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
|
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(internal.NewAppServer)
|
cl, err := client.New(newAppServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
|
appSecrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secretCols := []string{"Name", "Value"}
|
secretsTable, err = formatter.CreateTable()
|
||||||
secretTable = formatter.CreateTable(secretCols)
|
if err != nil {
|
||||||
for name, val := range secrets {
|
log.Fatal(err)
|
||||||
secretTable.Append([]string{name, val})
|
}
|
||||||
|
|
||||||
|
headers := []string{"NAME", "VALUE"}
|
||||||
|
secretsTable.Headers(headers...)
|
||||||
|
|
||||||
|
for name, val := range appSecrets {
|
||||||
|
secretsTable.Row(name, val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.NewAppServer == "default" {
|
if newAppServer == "default" {
|
||||||
internal.NewAppServer = "local"
|
newAppServer = "local"
|
||||||
}
|
}
|
||||||
|
|
||||||
tableCol := []string{"server", "recipe", "domain"}
|
log.Infof("%s created (version: %s)", appDomain, recipeVersion)
|
||||||
table := formatter.CreateTable(tableCol)
|
|
||||||
table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain})
|
|
||||||
|
|
||||||
fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name))
|
if len(appSecrets) > 0 {
|
||||||
fmt.Println("")
|
rows := [][]string{}
|
||||||
table.Render()
|
for k, v := range appSecrets {
|
||||||
fmt.Println("")
|
rows = append(rows, []string{k, v})
|
||||||
fmt.Println("You can configure this app by running the following:")
|
}
|
||||||
fmt.Println(fmt.Sprintf("\n abra app config %s", internal.Domain))
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("You can deploy this app by running the following:")
|
|
||||||
fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain))
|
|
||||||
|
|
||||||
if len(secrets) > 0 {
|
overview := formatter.CreateOverview("SECRETS OVERVIEW", rows)
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("Here are your generated secrets:")
|
fmt.Println(overview)
|
||||||
fmt.Println("")
|
|
||||||
secretTable.Render()
|
log.Warnf(
|
||||||
logrus.Warn("generated secrets are not shown again, please take note of them NOW")
|
"secrets are %s shown again, please save them %s",
|
||||||
|
formatter.BoldUnderlineStyle.Render("NOT"),
|
||||||
|
formatter.BoldUnderlineStyle.Render("NOW"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
app, err := app.Get(appDomain)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
|
||||||
|
log.Fatalf("writing recipe version failed: %s", err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,19 +229,25 @@ type AppSecrets map[string]string
|
|||||||
|
|
||||||
// createSecrets creates all secrets for a new app.
|
// createSecrets creates all secrets for a new app.
|
||||||
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
|
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
|
||||||
secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer)
|
// NOTE(d1): trim to match app.StackName() implementation
|
||||||
|
if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH {
|
||||||
|
log.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH])
|
||||||
|
sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := secret.GenerateSecrets(cl, secretsConfig, newAppServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.Pass {
|
if saveInPass {
|
||||||
for secretName := range secrets {
|
for secretName := range secrets {
|
||||||
secretValue := secrets[secretName]
|
secretValue := secrets[secretName]
|
||||||
if err := secret.PassInsertSecret(
|
if err := secret.PassInsertSecret(
|
||||||
secretValue,
|
secretValue,
|
||||||
secretName,
|
secretName,
|
||||||
internal.Domain,
|
appDomain,
|
||||||
internal.NewAppServer,
|
newAppServer,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -206,18 +258,18 @@ func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
|
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
|
||||||
func ensureDomainFlag(recipe recipe.Recipe, server string) error {
|
func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
|
||||||
if internal.Domain == "" && !internal.NoInput {
|
if appDomain == "" && !internal.NoInput {
|
||||||
prompt := &survey.Input{
|
prompt := &survey.Input{
|
||||||
Message: "Specify app domain",
|
Message: "Specify app domain",
|
||||||
Default: fmt.Sprintf("%s.%s", recipe.Name, server),
|
Default: fmt.Sprintf("%s.%s", recipe.Name, server),
|
||||||
}
|
}
|
||||||
if err := survey.AskOne(prompt, &internal.Domain); err != nil {
|
if err := survey.AskOne(prompt, &appDomain); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.Domain == "" {
|
if appDomain == "" {
|
||||||
return fmt.Errorf("no domain provided")
|
return fmt.Errorf("no domain provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,15 +279,15 @@ func ensureDomainFlag(recipe recipe.Recipe, server string) error {
|
|||||||
// promptForSecrets asks if we should generate secrets for a new app.
|
// promptForSecrets asks if we should generate secrets for a new app.
|
||||||
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
|
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
|
||||||
if len(secretsConfig) == 0 {
|
if len(secretsConfig) == 0 {
|
||||||
logrus.Debugf("%s has no secrets to generate, skipping...", recipeName)
|
log.Debugf("%s has no secrets to generate, skipping...", recipeName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Secrets && !internal.NoInput {
|
if !generateSecrets && !internal.NoInput {
|
||||||
prompt := &survey.Confirm{
|
prompt := &survey.Confirm{
|
||||||
Message: "Generate app secrets?",
|
Message: "Generate app secrets?",
|
||||||
}
|
}
|
||||||
if err := survey.AskOne(prompt, &internal.Secrets); err != nil {
|
if err := survey.AskOne(prompt, &generateSecrets); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,19 +302,82 @@ func ensureServerFlag() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.NewAppServer == "" && !internal.NoInput {
|
if len(servers) == 1 {
|
||||||
|
newAppServer = servers[0]
|
||||||
|
log.Infof("single server detected, choosing %s automatically", newAppServer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if newAppServer == "" && !internal.NoInput {
|
||||||
prompt := &survey.Select{
|
prompt := &survey.Select{
|
||||||
Message: "Select app server:",
|
Message: "Select app server:",
|
||||||
Options: servers,
|
Options: servers,
|
||||||
}
|
}
|
||||||
if err := survey.AskOne(prompt, &internal.NewAppServer); err != nil {
|
if err := survey.AskOne(prompt, &newAppServer); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.NewAppServer == "" {
|
if newAppServer == "" {
|
||||||
return fmt.Errorf("no server provided")
|
return fmt.Errorf("no server provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
newAppServer string
|
||||||
|
appDomain string
|
||||||
|
saveInPass bool
|
||||||
|
generateSecrets bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppNewCommand.Flags().StringVarP(
|
||||||
|
&newAppServer,
|
||||||
|
"server",
|
||||||
|
"s",
|
||||||
|
"",
|
||||||
|
"specify server for new app",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppNewCommand.RegisterFlagCompletionFunc(
|
||||||
|
"server",
|
||||||
|
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.ServerNameComplete()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
AppNewCommand.Flags().StringVarP(
|
||||||
|
&appDomain,
|
||||||
|
"domain",
|
||||||
|
"D",
|
||||||
|
"",
|
||||||
|
"domain name for app",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppNewCommand.Flags().BoolVarP(
|
||||||
|
&saveInPass,
|
||||||
|
"pass",
|
||||||
|
"p",
|
||||||
|
false,
|
||||||
|
"store secrets in a local pass store",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppNewCommand.Flags().BoolVarP(
|
||||||
|
&generateSecrets,
|
||||||
|
"secrets",
|
||||||
|
"S",
|
||||||
|
false,
|
||||||
|
"automatically generate secrets",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppNewCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
219
cli/app/ps.go
219
cli/app/ps.go
@ -2,100 +2,209 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/service"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
abraService "coopcloud.tech/abra/pkg/service"
|
||||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"github.com/buger/goterm"
|
|
||||||
dockerFormatter "github.com/docker/cli/cli/command/formatter"
|
dockerFormatter "github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/docker/docker/api/types"
|
containerTypes "github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appPsCommand = cli.Command{
|
var AppPsCommand = &cobra.Command{
|
||||||
Name: "ps",
|
Use: "ps <domain> [flags]",
|
||||||
Aliases: []string{"p"},
|
Aliases: []string{"p"},
|
||||||
Usage: "Check app status",
|
Short: "Check app deployment status",
|
||||||
ArgsUsage: "<domain>",
|
Args: cobra.ExactArgs(1),
|
||||||
Description: "Show a more detailed status output of a specific deployed app",
|
ValidArgsFunction: func(
|
||||||
Flags: []cli.Flag{
|
cmd *cobra.Command,
|
||||||
internal.WatchFlag,
|
args []string,
|
||||||
internal.DebugFlag,
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
app := internal.ValidateApp(args)
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDeployed {
|
if !deployMeta.IsDeployed {
|
||||||
logrus.Fatalf("%s is not deployed?", app.Name)
|
log.Fatalf("%s is not deployed?", app.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Watch {
|
chaosVersion := config.CHAOS_DEFAULT
|
||||||
showPSOutput(c, app, cl)
|
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
|
||||||
return nil
|
if statusMeta, ok := statuses[app.StackName()]; ok {
|
||||||
|
if isChaos, exists := statusMeta["chaos"]; exists && isChaos == "true" {
|
||||||
|
if cVersion, exists := statusMeta["chaosVersion"]; exists {
|
||||||
|
chaosVersion = cVersion
|
||||||
|
if strings.HasSuffix(chaosVersion, config.DIRTY_DEFAULT) {
|
||||||
|
chaosVersion = formatter.BoldDirtyDefault(chaosVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
goterm.Clear()
|
showPSOutput(app, cl, deployMeta.Version, chaosVersion)
|
||||||
for {
|
|
||||||
goterm.MoveCursor(1, 1)
|
|
||||||
showPSOutput(c, app, cl)
|
|
||||||
goterm.Flush()
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// showPSOutput renders ps output.
|
// showPSOutput renders ps output.
|
||||||
func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
|
func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chaosVersion string) {
|
||||||
filters, err := app.Filters(true, true)
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
|
deployOpts := stack.Deploy{
|
||||||
|
Composefiles: composeFiles,
|
||||||
|
Namespace: app.StackName(),
|
||||||
|
Prune: false,
|
||||||
|
ResolveImage: stack.ResolveImageAlways,
|
||||||
|
}
|
||||||
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tableCol := []string{"service name", "image", "created", "status", "state", "ports"}
|
services := compose.Services
|
||||||
table := formatter.CreateTable(tableCol)
|
sort.Slice(services, func(i, j int) bool {
|
||||||
|
return services[i].Name < services[j].Name
|
||||||
|
})
|
||||||
|
|
||||||
for _, container := range containers {
|
var rows [][]string
|
||||||
var containerNames []string
|
allContainerStats := make(map[string]map[string]string)
|
||||||
for _, containerName := range container.Names {
|
for _, service := range services {
|
||||||
trimmed := strings.TrimPrefix(containerName, "/")
|
filters := filters.NewArgs()
|
||||||
containerNames = append(containerNames, trimmed)
|
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
|
||||||
|
|
||||||
|
containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tableRow := []string{
|
var containerStats map[string]string
|
||||||
service.ContainerToServiceName(container.Names, app.StackName()),
|
if len(containers) == 0 {
|
||||||
formatter.RemoveSha(container.Image),
|
containerStats = map[string]string{
|
||||||
formatter.HumanDuration(container.Created),
|
"version": deployedVersion,
|
||||||
container.Status,
|
"chaos": chaosVersion,
|
||||||
container.State,
|
"service": service.Name,
|
||||||
dockerFormatter.DisplayablePorts(container.Ports),
|
"image": "unknown",
|
||||||
|
"created": "unknown",
|
||||||
|
"status": "unknown",
|
||||||
|
"state": "unknown",
|
||||||
|
"ports": "unknown",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container := containers[0]
|
||||||
|
containerStats = map[string]string{
|
||||||
|
"version": deployedVersion,
|
||||||
|
"chaos": chaosVersion,
|
||||||
|
"service": abraService.ContainerToServiceName(container.Names, app.StackName()),
|
||||||
|
"image": formatter.RemoveSha(container.Image),
|
||||||
|
"created": formatter.HumanDuration(container.Created),
|
||||||
|
"status": container.Status,
|
||||||
|
"state": container.State,
|
||||||
|
"ports": dockerFormatter.DisplayablePorts(container.Ports),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
table.Append(tableRow)
|
|
||||||
|
allContainerStats[containerStats["service"]] = containerStats
|
||||||
|
|
||||||
|
// NOTE(d1): don't clobber these variables for --machine output
|
||||||
|
dVersion := deployedVersion
|
||||||
|
cVersion := chaosVersion
|
||||||
|
|
||||||
|
if containerStats["service"] != "app" {
|
||||||
|
// NOTE(d1): don't repeat info which only relevant for the "app" service
|
||||||
|
dVersion = ""
|
||||||
|
cVersion = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
row := []string{
|
||||||
|
containerStats["service"],
|
||||||
|
containerStats["status"],
|
||||||
|
containerStats["image"],
|
||||||
|
dVersion,
|
||||||
|
cVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
table.Render()
|
if internal.MachineReadable {
|
||||||
|
rendered, err := json.Marshal(allContainerStats)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("unable to convert to JSON: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(rendered))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
table, err := formatter.CreateTable()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := []string{
|
||||||
|
"SERVICE",
|
||||||
|
"STATUS",
|
||||||
|
"IMAGE",
|
||||||
|
"VERSION",
|
||||||
|
"CHAOS",
|
||||||
|
}
|
||||||
|
|
||||||
|
table.
|
||||||
|
Headers(headers...).
|
||||||
|
Rows(rows...)
|
||||||
|
|
||||||
|
if err := formatter.PrintTable(table); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppPsCommand.Flags().BoolVarP(
|
||||||
|
&internal.MachineReadable,
|
||||||
|
"machine",
|
||||||
|
"m",
|
||||||
|
false,
|
||||||
|
"print machine-readable output",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppPsCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,28 +3,23 @@ package app
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/volume"
|
"github.com/spf13/cobra"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appRemoveCommand = cli.Command{
|
var AppRemoveCommand = &cobra.Command{
|
||||||
Name: "remove",
|
Use: "remove <domain> [flags]",
|
||||||
Aliases: []string{"rm"},
|
Aliases: []string{"rm"},
|
||||||
ArgsUsage: "<domain>",
|
Short: "Remove all app data, locally and remotely",
|
||||||
Usage: "Remove all app data, locally and remotely",
|
Long: `Remove everything related to an app which is already undeployed.
|
||||||
Description: `
|
|
||||||
This command removes everything related to an app which is already undeployed.
|
|
||||||
|
|
||||||
By default, it will prompt for confirmation before proceeding. All secrets,
|
By default, it will prompt for confirmation before proceeding. All secrets,
|
||||||
volumes and the local app env file will be deleted.
|
volumes and the local app env file will be deleted.
|
||||||
@ -39,52 +34,69 @@ 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.
|
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"
|
To delete everything without prompt, use the "--force/-f" or the "--no-input/n"
|
||||||
flag.
|
flag.`,
|
||||||
`,
|
Example: " abra app remove 1312.net",
|
||||||
Flags: []cli.Flag{
|
Args: cobra.ExactArgs(1),
|
||||||
internal.ForceFlag,
|
ValidArgsFunction: func(
|
||||||
internal.DebugFlag,
|
cmd *cobra.Command,
|
||||||
internal.NoInputFlag,
|
args []string,
|
||||||
internal.OfflineFlag,
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Before: internal.SubCommandBefore,
|
app := internal.ValidateApp(args)
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if !internal.Force && !internal.NoInput {
|
if !internal.Force && !internal.NoInput {
|
||||||
|
log.Warnf("ALERTA ALERTA: deleting %s data and config (local/remote)", app.Name)
|
||||||
|
|
||||||
response := false
|
response := false
|
||||||
msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?"
|
prompt := &survey.Confirm{Message: "are you sure?"}
|
||||||
prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)}
|
|
||||||
if err := survey.AskOne(prompt, &response); err != nil {
|
if err := survey.AskOne(prompt, &response); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !response {
|
if !response {
|
||||||
logrus.Fatal("aborting as requested")
|
log.Fatal("aborting as requested")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
if isDeployed {
|
if deployMeta.IsDeployed {
|
||||||
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
|
log.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fs, err := app.Filters(false, false)
|
fs, err := app.Filters(false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configs, err := client.GetConfigs(cl, context.Background(), app.Server, fs)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
configNames := client.GetConfigNames(configs)
|
||||||
|
|
||||||
|
if len(configNames) > 0 {
|
||||||
|
if err := client.RemoveConfigs(cl, context.Background(), configNames, internal.Force); err != nil {
|
||||||
|
log.Fatalf("removing configs failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%d config(s) removed successfully", len(configNames))
|
||||||
|
} else {
|
||||||
|
log.Info("no configs to remove")
|
||||||
}
|
}
|
||||||
|
|
||||||
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
|
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets := make(map[string]string)
|
secrets := make(map[string]string)
|
||||||
@ -99,69 +111,50 @@ flag.
|
|||||||
for _, name := range secretNames {
|
for _, name := range secretNames {
|
||||||
err := cl.SecretRemove(context.Background(), secrets[name])
|
err := cl.SecretRemove(context.Background(), secrets[name])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
logrus.Info(fmt.Sprintf("secret: %s removed", name))
|
log.Info(fmt.Sprintf("secret: %s removed", name))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logrus.Info("no secrets to remove")
|
log.Info("no secrets to remove")
|
||||||
}
|
}
|
||||||
|
|
||||||
fs, err = app.Filters(false, true)
|
fs, err = app.Filters(false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
volumeListOptions := volume.ListOptions{fs}
|
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, fs)
|
||||||
volumeListOKBody, err := cl.VolumeList(context.Background(), volumeListOptions)
|
|
||||||
volumeList := volumeListOKBody.Volumes
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
volumeNames := client.GetVolumeNames(volumeList)
|
||||||
|
|
||||||
var vols []string
|
if len(volumeNames) > 0 {
|
||||||
for _, vol := range volumeList {
|
err := client.RemoveVolumes(cl, context.Background(), volumeNames, internal.Force, 5)
|
||||||
vols = append(vols, vol.Name)
|
if err != nil {
|
||||||
}
|
log.Fatalf("removing volumes failed: %s", err)
|
||||||
|
|
||||||
if len(vols) > 0 {
|
|
||||||
for _, vol := range vols {
|
|
||||||
err = retryFunc(5, func() error {
|
|
||||||
return cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("removing volumes failed: %s", err)
|
|
||||||
}
|
|
||||||
logrus.Info(fmt.Sprintf("volume %s removed", vol))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Infof("%d volume(s) removed successfully", len(volumeNames))
|
||||||
} else {
|
} else {
|
||||||
logrus.Info("no volumes to remove")
|
log.Info("no volumes to remove")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = os.Remove(app.Path); err != nil {
|
if err = os.Remove(app.Path); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Info(fmt.Sprintf("file: %s removed", app.Path))
|
log.Info(fmt.Sprintf("file: %s removed", app.Path))
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// retryFunc retries the given function for the given retries. After the nth
|
func init() {
|
||||||
// retry it waits (n + 1)^2 seconds before the next retry (starting with n=0).
|
AppRemoveCommand.Flags().BoolVarP(
|
||||||
// It returns an error if the function still failed after the last retry.
|
&internal.Force,
|
||||||
func retryFunc(retries int, fn func() error) error {
|
"force",
|
||||||
for i := 0; i < retries; i++ {
|
"f",
|
||||||
err := fn()
|
false,
|
||||||
if err == nil {
|
"perform action without further prompt",
|
||||||
return nil
|
)
|
||||||
}
|
|
||||||
if i+1 < retries {
|
|
||||||
sleep := time.Duration(i+1) * time.Duration(i+1)
|
|
||||||
logrus.Infof("%s: waiting %d seconds before next retry", err, sleep)
|
|
||||||
time.Sleep(sleep * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%d retries failed", retries)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,79 +2,164 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
"coopcloud.tech/abra/pkg/ui"
|
||||||
upstream "coopcloud.tech/abra/pkg/upstream/service"
|
upstream "coopcloud.tech/abra/pkg/upstream/service"
|
||||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/urfave/cli"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var appRestartCommand = cli.Command{
|
var AppRestartCommand = &cobra.Command{
|
||||||
Name: "restart",
|
Use: "restart <domain> [[service] | --all-services] [flags]",
|
||||||
Aliases: []string{"re"},
|
Aliases: []string{"re"},
|
||||||
Usage: "Restart an app",
|
Short: "Restart an app",
|
||||||
ArgsUsage: "<domain>",
|
Long: `This command restarts services within a deployed app.
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
|
||||||
internal.OfflineFlag,
|
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
Description: `This command restarts a service within a deployed app.`,
|
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
serviceNameShort := c.Args().Get(1)
|
Run "abra app ps <domain>" to see a list of service names.
|
||||||
if serviceNameShort == "" {
|
|
||||||
err := errors.New("missing service?")
|
Pass "--all-services/-a" to restart all services.`,
|
||||||
internal.ShowSubcommandHelpAndError(c, err)
|
Example: ` # restart a single app service
|
||||||
|
abra app restart 1312.net app
|
||||||
|
|
||||||
|
# restart all app services
|
||||||
|
abra app restart 1312.net -a`,
|
||||||
|
Args: cobra.RangeArgs(1, 2),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch l := len(args); l {
|
||||||
|
case 0:
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
case 1:
|
||||||
|
if !allServices {
|
||||||
|
return autocomplete.ServiceNameComplete(args[0])
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
app := internal.ValidateApp(args)
|
||||||
|
|
||||||
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceName string
|
||||||
|
if len(args) == 2 {
|
||||||
|
serviceName = args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if serviceName == "" && !allServices {
|
||||||
|
log.Fatal("missing [service]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if serviceName != "" && allServices {
|
||||||
|
log.Fatal("cannot use [service] and --all-services/-a together")
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceNames []string
|
||||||
|
if allServices {
|
||||||
|
var err error
|
||||||
|
serviceNames, err = appPkg.GetAppServiceNames(app.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
serviceNames = append(serviceNames, serviceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDeployed {
|
if !deployMeta.IsDeployed {
|
||||||
logrus.Fatalf("%s is not deployed?", app.Name)
|
log.Fatalf("%s is not deployed?", app.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceName := fmt.Sprintf("%s_%s", app.StackName(), serviceNameShort)
|
for _, serviceName := range serviceNames {
|
||||||
|
stackServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
|
||||||
|
|
||||||
logrus.Debugf("attempting to scale %s to 0 (restart logic)", serviceName)
|
service, _, err := cl.ServiceInspectWithRaw(
|
||||||
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 0); err != nil {
|
context.Background(),
|
||||||
logrus.Fatal(err)
|
stackServiceName,
|
||||||
|
types.ServiceInspectOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("attempting to scale %s to 0", stackServiceName)
|
||||||
|
|
||||||
|
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 0); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := app.Filters(true, false, serviceName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitOpts := stack.WaitOpts{
|
||||||
|
Services: []ui.ServiceMeta{{Name: stackServiceName, ID: service.ID}},
|
||||||
|
AppName: app.Name,
|
||||||
|
ServerName: app.Server,
|
||||||
|
Filters: f,
|
||||||
|
NoLog: true,
|
||||||
|
Quiet: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("%s has been scaled to 0", stackServiceName)
|
||||||
|
log.Debugf("attempting to scale %s to 1", stackServiceName)
|
||||||
|
|
||||||
|
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 1); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("%s has been scaled to 1", stackServiceName)
|
||||||
|
log.Infof("%s service successfully restarted", serviceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("%s has been scaled to 0 (restart logic)", serviceName)
|
|
||||||
|
|
||||||
logrus.Debugf("attempting to scale %s to 1 (restart logic)", serviceName)
|
|
||||||
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 1); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("%s has been scaled to 1 (restart logic)", serviceName)
|
|
||||||
|
|
||||||
logrus.Infof("%s service successfully restarted", serviceNameShort)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var allServices bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppRestartCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
AppRestartCommand.Flags().BoolVarP(
|
||||||
|
&allServices,
|
||||||
|
"all-services",
|
||||||
|
"a",
|
||||||
|
false,
|
||||||
|
"restart all services",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -2,81 +2,134 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var targetPath string
|
var AppRestoreCommand = &cobra.Command{
|
||||||
var targetPathFlag = &cli.StringFlag{
|
Use: "restore <domain> [flags]",
|
||||||
Name: "target, t",
|
Aliases: []string{"rs"},
|
||||||
Usage: "Target path",
|
Short: "Restore a snapshot",
|
||||||
Destination: &targetPath,
|
Long: `Snapshots are restored while apps are deployed.
|
||||||
}
|
|
||||||
|
|
||||||
var appRestoreCommand = cli.Command{
|
Some restore scenarios may require service / app restarts.`,
|
||||||
Name: "restore",
|
Args: cobra.ExactArgs(1),
|
||||||
Aliases: []string{"rs"},
|
ValidArgsFunction: func(
|
||||||
Usage: "Restore an app backup",
|
cmd *cobra.Command,
|
||||||
ArgsUsage: "<domain> <service>",
|
args []string,
|
||||||
Flags: []cli.Flag{
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
internal.DebugFlag,
|
return autocomplete.AppNameComplete()
|
||||||
internal.OfflineFlag,
|
|
||||||
targetPathFlag,
|
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
app := internal.ValidateApp(args)
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Chaos {
|
|
||||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
|
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
execEnv := []string{
|
||||||
|
fmt.Sprintf("SERVICE=%s", app.Domain),
|
||||||
|
"MACHINE_LOGS=true",
|
||||||
}
|
}
|
||||||
|
|
||||||
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
|
|
||||||
if snapshot != "" {
|
if snapshot != "" {
|
||||||
logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
|
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
|
||||||
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
|
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetPath != "" {
|
if targetPath != "" {
|
||||||
logrus.Debugf("including TARGET=%s in backupbot exec invocation", targetPath)
|
log.Debugf("including TARGET=%s in backupbot exec invocation", targetPath)
|
||||||
execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath))
|
execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil {
|
if internal.NoInput {
|
||||||
logrus.Fatal(err)
|
log.Debugf("including NONINTERACTIVE=%v in backupbot exec invocation", internal.NoInput)
|
||||||
|
execEnv = append(execEnv, fmt.Sprintf("NONINTERACTIVE=%v", internal.NoInput))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if len(volumes) > 0 {
|
||||||
|
allVolumes := strings.Join(volumes, ",")
|
||||||
|
log.Debugf("including VOLUMES=%s in backupbot exec invocation", allVolumes)
|
||||||
|
execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%s", allVolumes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(services) > 0 {
|
||||||
|
allServices := strings.Join(services, ",")
|
||||||
|
log.Debugf("including CONTAINER=%s in backupbot exec invocation", allServices)
|
||||||
|
execEnv = append(execEnv, fmt.Sprintf("CONTAINER=%s", allServices))
|
||||||
|
}
|
||||||
|
|
||||||
|
if hooks {
|
||||||
|
log.Debugf("including NO_COMMANDS=%v in backupbot exec invocation", false)
|
||||||
|
execEnv = append(execEnv, fmt.Sprintf("NO_COMMANDS=%v", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
targetPath string
|
||||||
|
hooks bool
|
||||||
|
services []string
|
||||||
|
volumes []string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppRestoreCommand.Flags().StringVarP(
|
||||||
|
&targetPath,
|
||||||
|
"target",
|
||||||
|
"t",
|
||||||
|
"/",
|
||||||
|
"target path",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppRestoreCommand.Flags().StringArrayVarP(
|
||||||
|
&services,
|
||||||
|
"services",
|
||||||
|
"s",
|
||||||
|
[]string{},
|
||||||
|
"restore specific services",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppRestoreCommand.Flags().StringArrayVarP(
|
||||||
|
&volumes,
|
||||||
|
"volumes",
|
||||||
|
"v",
|
||||||
|
[]string{},
|
||||||
|
"restore specific volumes",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppRestoreCommand.Flags().BoolVarP(
|
||||||
|
&hooks,
|
||||||
|
"hooks",
|
||||||
|
"H",
|
||||||
|
false,
|
||||||
|
"enable pre/post-hook command execution",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppRestoreCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,240 +1,343 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/pkg/app"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
|
"coopcloud.tech/abra/pkg/envfile"
|
||||||
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/lint"
|
"coopcloud.tech/abra/pkg/lint"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
|
||||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"coopcloud.tech/tagcmp"
|
"coopcloud.tech/tagcmp"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appRollbackCommand = cli.Command{
|
var AppRollbackCommand = &cobra.Command{
|
||||||
Name: "rollback",
|
Use: "rollback <domain> [version] [flags]",
|
||||||
Aliases: []string{"rl"},
|
Aliases: []string{"rl"},
|
||||||
Usage: "Roll an app back to a previous version",
|
Short: "Roll an app back to a previous version",
|
||||||
ArgsUsage: "<domain> [<version>]",
|
Long: `This command rolls an app back to a previous version.
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
Unlike "abra app deploy", chaos operations are not supported here. Only recipe
|
||||||
internal.NoInputFlag,
|
versions are supported values for "[version]".
|
||||||
internal.ForceFlag,
|
|
||||||
internal.ChaosFlag,
|
It is possible to "--force/-f" an downgrade if you want to re-deploy a specific
|
||||||
internal.NoDomainChecksFlag,
|
version.
|
||||||
internal.DontWaitConvergeFlag,
|
|
||||||
internal.OfflineFlag,
|
Only the deployed version is consulted when trying to determine what downgrades
|
||||||
|
are available. The live deployment version is the "source of truth" in this
|
||||||
|
case. The stored .env version is not consulted.
|
||||||
|
|
||||||
|
A downgrade can be destructive, please ensure you have a copy of your app data
|
||||||
|
beforehand. See "abra app backup" for more.`,
|
||||||
|
Example: ` # standard rollback
|
||||||
|
abra app rollback 1312.net
|
||||||
|
|
||||||
|
# rollback to specific version
|
||||||
|
abra app rollback 1312.net 2.0.0+1.2.3`,
|
||||||
|
Args: cobra.RangeArgs(1, 2),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch l := len(args); l {
|
||||||
|
case 0:
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
case 1:
|
||||||
|
app, err := appPkg.Get(args[0])
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
|
return []string{errMsg}, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Description: `
|
var (
|
||||||
This command rolls an app back to a previous version if one exists.
|
downgradeWarnMessages []string
|
||||||
|
chosenDowngrade string
|
||||||
|
availableDowngrades []string
|
||||||
|
)
|
||||||
|
|
||||||
You may pass "--force/-f" to downgrade to the same version again. This can be
|
app := internal.ValidateApp(args)
|
||||||
useful if the container runtime has gotten into a weird state.
|
|
||||||
|
|
||||||
This action could be destructive, please ensure you have a copy of your app
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
data beforehand.
|
log.Fatal(err)
|
||||||
|
|
||||||
Chaos mode ("--chaos") will deploy your local checkout of a recipe as-is,
|
|
||||||
including unstaged changes and can be useful for live hacking and testing new
|
|
||||||
recipes.
|
|
||||||
`,
|
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
stackName := app.StackName()
|
|
||||||
|
|
||||||
specificVersion := c.Args().Get(1)
|
|
||||||
if specificVersion != "" && internal.Chaos {
|
|
||||||
logrus.Fatal("cannot use <version> and --chaos together")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Chaos {
|
|
||||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := recipe.Get(app.Recipe, internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := lint.LintForErrors(r); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("checking whether %s is already deployed", stackName)
|
deployMeta, err := ensureDeployed(cl, app)
|
||||||
|
|
||||||
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDeployed {
|
if err := lint.LintForErrors(app.Recipe); err != nil {
|
||||||
logrus.Fatalf("%s is not deployed?", app.Name)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
versions, err := app.Recipe.Tags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
|
// NOTE(d1): we've no idea what the live deployment version is, so every
|
||||||
if err != nil {
|
// possible downgrade can be shown. it's up to the user to make the choice
|
||||||
logrus.Fatal(err)
|
if deployMeta.Version == config.UNKNOWN_DEFAULT {
|
||||||
}
|
|
||||||
|
|
||||||
if len(versions) == 0 && !internal.Chaos {
|
|
||||||
logrus.Warn("no published versions in catalogue, trying local recipe repository")
|
|
||||||
recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Warn(err)
|
|
||||||
}
|
|
||||||
for _, recipeVersion := range recipeVersions {
|
|
||||||
for version := range recipeVersion {
|
|
||||||
versions = append(versions, version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var availableDowngrades []string
|
|
||||||
if deployedVersion == "unknown" {
|
|
||||||
availableDowngrades = versions
|
availableDowngrades = versions
|
||||||
logrus.Warnf("failed to determine deployed version of %s", app.Name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if specificVersion != "" {
|
if len(args) == 2 && args[1] != "" {
|
||||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
chosenDowngrade = args[1]
|
||||||
|
|
||||||
|
if err := validateDowngradeVersionArg(chosenDowngrade, app, deployMeta); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
availableDowngrades = append(availableDowngrades, chosenDowngrade)
|
||||||
|
}
|
||||||
|
|
||||||
|
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenDowngrade == "" {
|
||||||
|
downgradeAvailable, err := ensureDowngradesAvailable(versions, &availableDowngrades, deployMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
|
||||||
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) || parsedSpecificVersion.Equals(parsedDeployedVersion) {
|
|
||||||
logrus.Fatalf("%s is not a downgrade for %s?", deployedVersion, specificVersion)
|
|
||||||
}
|
|
||||||
availableDowngrades = append(availableDowngrades, specificVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
if deployedVersion != "unknown" && !internal.Chaos && specificVersion == "" {
|
|
||||||
for _, version := range versions {
|
|
||||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
parsedVersion, err := tagcmp.Parse(version)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
if parsedVersion.IsLessThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
|
|
||||||
availableDowngrades = append(availableDowngrades, version)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(availableDowngrades) == 0 && !internal.Force {
|
if !downgradeAvailable {
|
||||||
logrus.Info("no available downgrades, you're on oldest ✌️")
|
log.Info("no available downgrades")
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var chosenDowngrade string
|
if internal.Force || internal.NoInput || chosenDowngrade != "" {
|
||||||
if len(availableDowngrades) > 0 && !internal.Chaos {
|
if len(availableDowngrades) > 0 {
|
||||||
if internal.Force || internal.NoInput || specificVersion != "" {
|
|
||||||
chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
|
chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
|
||||||
logrus.Debugf("choosing %s as version to downgrade to (--force/--no-input)", chosenDowngrade)
|
}
|
||||||
} else {
|
} else {
|
||||||
prompt := &survey.Select{
|
if err := chooseDowngrade(availableDowngrades, deployMeta, &chosenDowngrade); err != nil {
|
||||||
Message: fmt.Sprintf("Please select a downgrade (current version: %s):", deployedVersion),
|
log.Fatal(err)
|
||||||
Options: internal.ReverseStringList(availableDowngrades),
|
|
||||||
}
|
|
||||||
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Chaos {
|
if internal.Force &&
|
||||||
if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil {
|
chosenDowngrade == "" &&
|
||||||
logrus.Fatal(err)
|
deployMeta.Version != config.UNKNOWN_DEFAULT {
|
||||||
}
|
chosenDowngrade = deployMeta.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.Chaos {
|
if chosenDowngrade == "" {
|
||||||
logrus.Warn("chaos mode engaged")
|
log.Fatal("unknown deployed version, unable to downgrade")
|
||||||
var err error
|
|
||||||
chosenDowngrade, err = recipe.ChaosVersion(app.Recipe)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
|
log.Debugf("choosing %s as version to rollback", chosenDowngrade)
|
||||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
|
||||||
|
if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
for k, v := range abraShEnv {
|
for k, v := range abraShEnv {
|
||||||
app.Env[k] = v
|
app.Env[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stackName := app.StackName()
|
||||||
deployOpts := stack.Deploy{
|
deployOpts := stack.Deploy{
|
||||||
Composefiles: composeFiles,
|
Composefiles: composeFiles,
|
||||||
Namespace: stackName,
|
Namespace: stackName,
|
||||||
Prune: false,
|
Prune: false,
|
||||||
ResolveImage: stack.ResolveImageAlways,
|
ResolveImage: stack.ResolveImageAlways,
|
||||||
|
Detach: false,
|
||||||
}
|
}
|
||||||
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
|
||||||
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
config.ExposeAllEnv(stackName, compose, app.Env)
|
|
||||||
config.SetRecipeLabel(compose, stackName, app.Recipe)
|
appPkg.ExposeAllEnv(stackName, compose, app.Env)
|
||||||
config.SetChaosLabel(compose, stackName, internal.Chaos)
|
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
|
||||||
config.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
|
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
|
||||||
config.SetUpdateLabel(compose, stackName, app.Env)
|
if internal.Chaos {
|
||||||
|
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
|
||||||
|
}
|
||||||
|
appPkg.SetUpdateLabel(compose, stackName, app.Env)
|
||||||
|
|
||||||
// NOTE(d1): no release notes implemeneted for rolling back
|
// NOTE(d1): no release notes implemeneted for rolling back
|
||||||
if err := internal.NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil {
|
if err := internal.DeployOverview(
|
||||||
logrus.Fatal(err)
|
app,
|
||||||
|
deployMeta.Version,
|
||||||
|
chosenDowngrade,
|
||||||
|
"",
|
||||||
|
downgradeWarnMessages,
|
||||||
|
); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
|
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
|
||||||
logrus.Fatal(err)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
|
||||||
|
|
||||||
|
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := app.Filters(true, false, serviceNames...)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stack.RunDeploy(
|
||||||
|
cl,
|
||||||
|
deployOpts,
|
||||||
|
compose,
|
||||||
|
stackName,
|
||||||
|
app.Server,
|
||||||
|
internal.DontWaitConverge,
|
||||||
|
f,
|
||||||
|
); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil {
|
||||||
|
log.Fatalf("writing recipe version failed: %s", err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chooseDowngrade prompts the user to choose an downgrade interactively.
|
||||||
|
func chooseDowngrade(
|
||||||
|
availableDowngrades []string,
|
||||||
|
deployMeta stack.DeployMeta,
|
||||||
|
chosenDowngrade *string,
|
||||||
|
) error {
|
||||||
|
msg := fmt.Sprintf("please select a downgrade (version: %s):", deployMeta.Version)
|
||||||
|
|
||||||
|
if deployMeta.IsChaos {
|
||||||
|
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
|
||||||
|
|
||||||
|
msg = fmt.Sprintf(
|
||||||
|
"please select a downgrade (version: %s, chaos: %s):",
|
||||||
|
deployMeta.Version,
|
||||||
|
chaosVersion,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := &survey.Select{
|
||||||
|
Message: msg,
|
||||||
|
Options: internal.SortVersionsDesc(availableDowngrades),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := survey.AskOne(prompt, chosenDowngrade); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateDownpgradeVersionArg validates the specific version.
|
||||||
|
func validateDowngradeVersionArg(
|
||||||
|
specificVersion string,
|
||||||
|
app app.App,
|
||||||
|
deployMeta stack.DeployMeta,
|
||||||
|
) error {
|
||||||
|
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("current deployment '%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) &&
|
||||||
|
!parsedSpecificVersion.Equals(parsedDeployedVersion) {
|
||||||
|
return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
|
||||||
|
return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDowngradesAvailable ensures that there are available downgrades.
|
||||||
|
func ensureDowngradesAvailable(
|
||||||
|
versions []string,
|
||||||
|
availableDowngrades *[]string,
|
||||||
|
deployMeta stack.DeployMeta,
|
||||||
|
) (bool, error) {
|
||||||
|
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, version := range versions {
|
||||||
|
parsedVersion, err := tagcmp.Parse(version)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedVersion.IsLessThan(parsedDeployedVersion) &&
|
||||||
|
!(parsedVersion.Equals(parsedDeployedVersion)) {
|
||||||
|
*availableDowngrades = append(*availableDowngrades, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*availableDowngrades) == 0 && !internal.Force {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppRollbackCommand.Flags().BoolVarP(
|
||||||
|
&internal.Force,
|
||||||
|
"force",
|
||||||
|
"f",
|
||||||
|
false,
|
||||||
|
"perform action without further prompt",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppRollbackCommand.Flags().BoolVarP(
|
||||||
|
&internal.NoDomainChecks,
|
||||||
|
"no-domain-checks",
|
||||||
|
"D",
|
||||||
|
false,
|
||||||
|
"disable public DNS checks",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppRollbackCommand.Flags().BoolVarP(
|
||||||
|
&internal.DontWaitConverge, "no-converge-checks",
|
||||||
|
"c",
|
||||||
|
false,
|
||||||
|
"disable converge logic checks",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
114
cli/app/run.go
114
cli/app/run.go
@ -2,99 +2,113 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
containerPkg "coopcloud.tech/abra/pkg/container"
|
containerPkg "coopcloud.tech/abra/pkg/container"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/upstream/container"
|
"coopcloud.tech/abra/pkg/upstream/container"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/docker/api/types"
|
containertypes "github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var user string
|
var AppRunCommand = &cobra.Command{
|
||||||
var userFlag = &cli.StringFlag{
|
Use: "run <domain> <service> <cmd> [[args] [flags] | [flags] -- [args]]",
|
||||||
Name: "user, u",
|
|
||||||
Value: "",
|
|
||||||
Destination: &user,
|
|
||||||
}
|
|
||||||
|
|
||||||
var noTTY bool
|
|
||||||
var noTTYFlag = &cli.BoolFlag{
|
|
||||||
Name: "no-tty, t",
|
|
||||||
Destination: &noTTY,
|
|
||||||
}
|
|
||||||
|
|
||||||
var appRunCommand = cli.Command{
|
|
||||||
Name: "run",
|
|
||||||
Aliases: []string{"r"},
|
Aliases: []string{"r"},
|
||||||
Flags: []cli.Flag{
|
Short: "Run a command inside a service container",
|
||||||
internal.DebugFlag,
|
Example: ` # run <cmd> with args/flags
|
||||||
noTTYFlag,
|
abra app run 1312.net app -- ls -lha
|
||||||
userFlag,
|
|
||||||
|
# run <cmd> without args/flags
|
||||||
|
abra app run 1312.net app bash --user nobody
|
||||||
|
|
||||||
|
# run <cmd> with both kinds of args/flags
|
||||||
|
abra app run 1312.net app --user nobody -- ls -lha`,
|
||||||
|
Args: cobra.MinimumNArgs(3),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch l := len(args); l {
|
||||||
|
case 0:
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
case 1:
|
||||||
|
return autocomplete.ServiceNameComplete(args[0])
|
||||||
|
case 2:
|
||||||
|
return autocomplete.CommandNameComplete(args[0])
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
ArgsUsage: "<domain> <service> <args>...",
|
app := internal.ValidateApp(args)
|
||||||
Usage: "Run a command in a service container",
|
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if len(c.Args()) < 2 {
|
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.Args()) < 3 {
|
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
|
|
||||||
}
|
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceName := c.Args().Get(1)
|
serviceName := args[1]
|
||||||
stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
|
stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
|
||||||
|
|
||||||
filters := filters.NewArgs()
|
filters := filters.NewArgs()
|
||||||
filters.Add("name", stackAndServiceName)
|
filters.Add("name", stackAndServiceName)
|
||||||
|
|
||||||
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
|
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := c.Args()[2:]
|
userCmd := args[2:]
|
||||||
execCreateOpts := types.ExecConfig{
|
execCreateOpts := containertypes.ExecOptions{
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
AttachStdin: true,
|
AttachStdin: true,
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
Cmd: cmd,
|
Cmd: userCmd,
|
||||||
Detach: false,
|
Detach: false,
|
||||||
Tty: true,
|
Tty: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if user != "" {
|
if runAsUser != "" {
|
||||||
execCreateOpts.User = user
|
execCreateOpts.User = runAsUser
|
||||||
}
|
}
|
||||||
if noTTY {
|
if noTTY {
|
||||||
execCreateOpts.Tty = false
|
execCreateOpts.Tty = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: avoid instantiating a new CLI
|
|
||||||
dcli, err := command.NewDockerCli()
|
dcli, err := command.NewDockerCli()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
|
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
noTTY bool
|
||||||
|
runAsUser string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppRunCommand.Flags().BoolVarP(&noTTY,
|
||||||
|
"no-tty",
|
||||||
|
"t",
|
||||||
|
false,
|
||||||
|
"do not request a TTY",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppRunCommand.Flags().StringVarP(
|
||||||
|
&runAsUser,
|
||||||
|
"user",
|
||||||
|
"u",
|
||||||
|
"",
|
||||||
|
"run command as user",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -2,106 +2,77 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/secret"
|
"coopcloud.tech/abra/pkg/secret"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var AppSecretGenerateCommand = &cobra.Command{
|
||||||
allSecrets bool
|
Use: "generate <domain> [[secret] [version] | --all] [flags]",
|
||||||
allSecretsFlag = &cli.BoolFlag{
|
Aliases: []string{"g"},
|
||||||
Name: "all, a",
|
Short: "Generate secrets",
|
||||||
Destination: &allSecrets,
|
Args: cobra.RangeArgs(1, 3),
|
||||||
Usage: "Generate all secrets",
|
ValidArgsFunction: func(
|
||||||
}
|
cmd *cobra.Command,
|
||||||
)
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
var (
|
switch l := len(args); l {
|
||||||
rmAllSecrets bool
|
case 0:
|
||||||
rmAllSecretsFlag = &cli.BoolFlag{
|
return autocomplete.AppNameComplete()
|
||||||
Name: "all, a",
|
case 1:
|
||||||
Destination: &rmAllSecrets,
|
app, err := appPkg.Get(args[0])
|
||||||
Usage: "Remove all secrets",
|
if err != nil {
|
||||||
}
|
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
)
|
return []string{errMsg}, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
var appSecretGenerateCommand = cli.Command{
|
return autocomplete.SecretComplete(app.Recipe.Name)
|
||||||
Name: "generate",
|
default:
|
||||||
Aliases: []string{"g"},
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
Usage: "Generate secrets",
|
}
|
||||||
ArgsUsage: "<domain> <secret> <version>",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
|
||||||
allSecretsFlag,
|
|
||||||
internal.PassFlag,
|
|
||||||
internal.MachineReadableFlag,
|
|
||||||
internal.OfflineFlag,
|
|
||||||
internal.ChaosFlag,
|
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
app := internal.ValidateApp(args)
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Chaos {
|
if len(args) <= 2 && !generateAllSecrets {
|
||||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
log.Fatal("missing arguments [secret]/[version] or '--all'")
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.Args()) == 1 && !allSecrets {
|
if len(args) > 2 && generateAllSecrets {
|
||||||
err := errors.New("missing arguments <secret>/<version> or '--all'")
|
log.Fatal("cannot use '[secret] [version]' and '--all' together")
|
||||||
internal.ShowSubcommandHelpAndError(c, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Args().Get(1) != "" && allSecrets {
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
err := errors.New("cannot use '<secret> <version>' and '--all' together")
|
|
||||||
internal.ShowSubcommandHelpAndError(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !allSecrets {
|
if !generateAllSecrets {
|
||||||
secretName := c.Args().Get(1)
|
secretName := args[1]
|
||||||
secretVersion := c.Args().Get(2)
|
secretVersion := args[2]
|
||||||
s, ok := secrets[secretName]
|
s, ok := secrets[secretName]
|
||||||
if !ok {
|
if !ok {
|
||||||
logrus.Fatalf("%s doesn't exist in the env config?", secretName)
|
log.Fatalf("%s doesn't exist in the env config?", secretName)
|
||||||
}
|
}
|
||||||
s.Version = secretVersion
|
s.Version = secretVersion
|
||||||
secrets = map[string]secret.Secret{
|
secrets = map[string]secret.Secret{
|
||||||
@ -111,195 +82,250 @@ var appSecretGenerateCommand = cli.Command{
|
|||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server)
|
secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.Pass {
|
if storeInPass {
|
||||||
for name, data := range secretVals {
|
for name, data := range secretVals {
|
||||||
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
|
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(secretVals) == 0 {
|
if len(secretVals) == 0 {
|
||||||
logrus.Warn("no secrets generated")
|
log.Warn("no secrets generated")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableCol := []string{"name", "value"}
|
headers := []string{"NAME", "VALUE"}
|
||||||
table := formatter.CreateTable(tableCol)
|
table, err := formatter.CreateTable()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Headers(headers...)
|
||||||
|
|
||||||
|
var rows [][]string
|
||||||
for name, val := range secretVals {
|
for name, val := range secretVals {
|
||||||
table.Append([]string{name, val})
|
row := []string{name, val}
|
||||||
|
rows = append(rows, row)
|
||||||
|
table.Row(row...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.MachineReadable {
|
if internal.MachineReadable {
|
||||||
table.JSONRender()
|
out, err := formatter.ToJSON(headers, rows)
|
||||||
} else {
|
if err != nil {
|
||||||
table.Render()
|
log.Fatal("unable to render to JSON: %s", err)
|
||||||
|
}
|
||||||
|
fmt.Println(out)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
logrus.Warn("generated secrets are not shown again, please take note of them NOW")
|
|
||||||
|
|
||||||
return nil
|
if err := formatter.PrintTable(table); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf(
|
||||||
|
"generated secrets %s shown again, please take note of them %s",
|
||||||
|
formatter.BoldStyle.Render("NOT"),
|
||||||
|
formatter.BoldStyle.Render("NOW"),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var appSecretInsertCommand = cli.Command{
|
var AppSecretInsertCommand = &cobra.Command{
|
||||||
Name: "insert",
|
Use: "insert <domain> <secret> <version> <data> [flags]",
|
||||||
Aliases: []string{"i"},
|
Aliases: []string{"i"},
|
||||||
Usage: "Insert secret",
|
Short: "Insert secret",
|
||||||
Flags: []cli.Flag{
|
Long: `This command inserts a secret into an app environment.
|
||||||
internal.DebugFlag,
|
|
||||||
internal.PassFlag,
|
Arbitrary secret insertion is not supported. Secrets that are inserted must
|
||||||
},
|
match those configured in the recipe beforehand.
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
ArgsUsage: "<domain> <secret-name> <version> <data>",
|
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Description: `
|
|
||||||
This command inserts a secret into an app environment.
|
|
||||||
|
|
||||||
This can be useful when you want to manually generate secrets for an app
|
This can be useful when you want to manually generate secrets for an app
|
||||||
environment. Typically, you can let Abra generate them for you on app creation
|
environment. Typically, you can let Abra generate them for you on app creation
|
||||||
(see "abra app new --secrets" for more).
|
(see "abra app new --secrets/-S" for more).`,
|
||||||
|
Example: ` # insert regular secret
|
||||||
|
abra app secret insert 1312.net my_secret v1 mySuperSecret
|
||||||
|
|
||||||
Example:
|
# insert secret as file
|
||||||
|
abra app secret insert 1312.net my_secret v1 secret.txt -f`,
|
||||||
|
Args: cobra.MinimumNArgs(4),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch l := len(args); l {
|
||||||
|
case 0:
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
case 1:
|
||||||
|
app, err := appPkg.Get(args[0])
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
|
return []string{errMsg}, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
return autocomplete.SecretComplete(app.Recipe.Name)
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
app := internal.ValidateApp(args)
|
||||||
|
|
||||||
abra app secret insert myapp db_pass v1 mySecretPassword
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
`,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if len(c.Args()) != 4 {
|
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
name := c.Args().Get(1)
|
name := args[1]
|
||||||
version := c.Args().Get(2)
|
version := args[2]
|
||||||
data := c.Args().Get(3)
|
data := args[3]
|
||||||
|
|
||||||
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("%s successfully stored on server", secretName)
|
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
if internal.Pass {
|
var isRecipeSecret bool
|
||||||
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
|
for secretName := range secrets {
|
||||||
logrus.Fatal(err)
|
if secretName == name {
|
||||||
|
isRecipeSecret = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !isRecipeSecret {
|
||||||
|
log.Fatalf("no secret %s available for recipe %s?", name, app.Recipe.Name)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
if insertFromFile {
|
||||||
|
raw, err := os.ReadFile(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("reading secret from file: %s", err)
|
||||||
|
}
|
||||||
|
data = string(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
if trimInput {
|
||||||
|
data = strings.TrimSpace(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
|
||||||
|
if err := client.StoreSecret(cl, secretName, data); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%s successfully stored on server", secretName)
|
||||||
|
|
||||||
|
if storeInPass {
|
||||||
|
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// secretRm removes a secret.
|
// secretRm removes a secret.
|
||||||
func secretRm(cl *dockerClient.Client, app config.App, secretName, parsed string) error {
|
func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string) error {
|
||||||
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
|
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("deleted %s successfully from server", secretName)
|
log.Infof("deleted %s successfully from server", secretName)
|
||||||
|
|
||||||
if internal.PassRemove {
|
if removeFromPass {
|
||||||
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
|
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("deleted %s successfully from local pass store", secretName)
|
log.Infof("deleted %s successfully from local pass store", secretName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var appSecretRmCommand = cli.Command{
|
var AppSecretRmCommand = &cobra.Command{
|
||||||
Name: "remove",
|
Use: "remove <domain> [[secret] | --all] [flags]",
|
||||||
Aliases: []string{"rm"},
|
Aliases: []string{"rm"},
|
||||||
Usage: "Remove a secret",
|
Short: "Remove a secret",
|
||||||
Flags: []cli.Flag{
|
Long: `This command removes a secret from an app environment.
|
||||||
internal.DebugFlag,
|
|
||||||
internal.NoInputFlag,
|
|
||||||
rmAllSecretsFlag,
|
|
||||||
internal.PassRemoveFlag,
|
|
||||||
internal.OfflineFlag,
|
|
||||||
internal.ChaosFlag,
|
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
ArgsUsage: "<domain> [<secret-name>]",
|
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Description: `
|
|
||||||
This command removes app secrets.
|
|
||||||
|
|
||||||
Example:
|
Arbitrary secret removal is not supported. Secrets that are removed must
|
||||||
|
match those configured in the recipe beforehand.`,
|
||||||
abra app secret remove myapp db_pass
|
Example: " abra app secret rm 1312.net oauth_key",
|
||||||
`,
|
Args: cobra.RangeArgs(1, 2),
|
||||||
Action: func(c *cli.Context) error {
|
ValidArgsFunction: func(
|
||||||
app := internal.ValidateApp(c)
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
logrus.Fatal(err)
|
switch l := len(args); l {
|
||||||
}
|
case 0:
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
if !internal.Chaos {
|
case 1:
|
||||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
if !rmAllSecrets {
|
||||||
logrus.Fatal(err)
|
app, err := appPkg.Get(args[0])
|
||||||
}
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
if !internal.Offline {
|
return []string{errMsg}, cobra.ShellCompDirectiveError
|
||||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
return autocomplete.SecretComplete(app.Recipe.Name)
|
||||||
}
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
app := internal.ValidateApp(args)
|
||||||
|
|
||||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Args().Get(1) != "" && rmAllSecrets {
|
if len(args) == 2 && rmAllSecrets {
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together"))
|
log.Fatal("cannot use [secret] and --all/-a together")
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Args().Get(1) == "" && !rmAllSecrets {
|
if len(args) != 2 && !rmAllSecrets {
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
|
log.Fatal("no secret(s) specified?")
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
filters, err := app.Filters(false, false)
|
filters, err := app.Filters(false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
|
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteSecretNames := make(map[string]bool)
|
remoteSecretNames := make(map[string]bool)
|
||||||
@ -307,122 +333,230 @@ Example:
|
|||||||
remoteSecretNames[cont.Spec.Annotations.Name] = true
|
remoteSecretNames[cont.Spec.Annotations.Name] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var secretToRm string
|
||||||
|
if len(args) == 2 {
|
||||||
|
secretToRm = args[1]
|
||||||
|
}
|
||||||
|
|
||||||
match := false
|
match := false
|
||||||
secretToRm := c.Args().Get(1)
|
|
||||||
for secretName, val := range secrets {
|
for secretName, val := range secrets {
|
||||||
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
|
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
|
||||||
if _, ok := remoteSecretNames[secretRemoteName]; ok {
|
if _, ok := remoteSecretNames[secretRemoteName]; ok {
|
||||||
if secretToRm != "" {
|
if secretToRm != "" {
|
||||||
if secretName == secretToRm {
|
if secretName == secretToRm {
|
||||||
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
|
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match = true
|
match = true
|
||||||
|
|
||||||
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
|
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !match && secretToRm != "" {
|
if !match && secretToRm != "" {
|
||||||
logrus.Fatalf("%s doesn't exist on server?", secretToRm)
|
log.Fatalf("%s doesn't exist on server?", secretToRm)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !match {
|
if !match {
|
||||||
logrus.Fatal("no secrets to remove?")
|
log.Fatal("no secrets to remove?")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var appSecretLsCommand = cli.Command{
|
var AppSecretLsCommand = &cobra.Command{
|
||||||
Name: "list",
|
Use: "list <domain>",
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
Flags: []cli.Flag{
|
Short: "List all secrets",
|
||||||
internal.DebugFlag,
|
Args: cobra.MinimumNArgs(1),
|
||||||
internal.OfflineFlag,
|
ValidArgsFunction: func(
|
||||||
internal.ChaosFlag,
|
cmd *cobra.Command,
|
||||||
internal.MachineReadableFlag,
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Usage: "List all secrets",
|
app := internal.ValidateApp(args)
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Chaos {
|
|
||||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
|
headers := []string{"NAME", "VERSION", "GENERATED NAME", "CREATED ON SERVER"}
|
||||||
table := formatter.CreateTable(tableCol)
|
table, err := formatter.CreateTable()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Headers(headers...)
|
||||||
|
|
||||||
secStats, err := secret.PollSecretsStatus(cl, app)
|
secStats, err := secret.PollSecretsStatus(cl, app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rows [][]string
|
||||||
for _, secStat := range secStats {
|
for _, secStat := range secStats {
|
||||||
tableRow := []string{
|
row := []string{
|
||||||
secStat.LocalName,
|
secStat.LocalName,
|
||||||
secStat.Version,
|
secStat.Version,
|
||||||
secStat.RemoteName,
|
secStat.RemoteName,
|
||||||
strconv.FormatBool(secStat.CreatedOnRemote),
|
strconv.FormatBool(secStat.CreatedOnRemote),
|
||||||
}
|
}
|
||||||
table.Append(tableRow)
|
|
||||||
|
rows = append(rows, row)
|
||||||
|
table.Row(row...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if table.NumLines() > 0 {
|
if len(rows) > 0 {
|
||||||
if internal.MachineReadable {
|
if internal.MachineReadable {
|
||||||
table.JSONRender()
|
out, err := formatter.ToJSON(headers, rows)
|
||||||
} else {
|
if err != nil {
|
||||||
table.Render()
|
log.Fatal("unable to render to JSON: %s", err)
|
||||||
|
}
|
||||||
|
fmt.Println(out)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logrus.Warnf("no secrets stored for %s", app.Name)
|
if err := formatter.PrintTable(table); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
log.Warnf("no secrets stored for %s", app.Name)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var appSecretCommand = cli.Command{
|
var AppSecretCommand = &cobra.Command{
|
||||||
Name: "secret",
|
Use: "secret [cmd] [args] [flags]",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
Usage: "Manage app secrets",
|
Short: "Manage app secrets",
|
||||||
ArgsUsage: "<domain>",
|
}
|
||||||
Subcommands: []cli.Command{
|
|
||||||
appSecretGenerateCommand,
|
var (
|
||||||
appSecretInsertCommand,
|
storeInPass bool
|
||||||
appSecretRmCommand,
|
insertFromFile bool
|
||||||
appSecretLsCommand,
|
trimInput bool
|
||||||
},
|
rmAllSecrets bool
|
||||||
|
generateAllSecrets bool
|
||||||
|
removeFromPass bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppSecretGenerateCommand.Flags().BoolVarP(
|
||||||
|
&internal.MachineReadable,
|
||||||
|
"machine",
|
||||||
|
"m",
|
||||||
|
false,
|
||||||
|
"print machine-readable output",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretGenerateCommand.Flags().BoolVarP(
|
||||||
|
&storeInPass,
|
||||||
|
"pass",
|
||||||
|
"p",
|
||||||
|
false,
|
||||||
|
"store generated secrets in a local pass store",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretGenerateCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretGenerateCommand.Flags().BoolVarP(
|
||||||
|
&generateAllSecrets,
|
||||||
|
"all",
|
||||||
|
"a",
|
||||||
|
false,
|
||||||
|
"generate all secrets",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretInsertCommand.Flags().BoolVarP(
|
||||||
|
&storeInPass,
|
||||||
|
"pass",
|
||||||
|
"p",
|
||||||
|
false,
|
||||||
|
"store generated secrets in a local pass store",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretInsertCommand.Flags().BoolVarP(
|
||||||
|
&insertFromFile,
|
||||||
|
"file",
|
||||||
|
"f",
|
||||||
|
false,
|
||||||
|
"treat input as a file",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretInsertCommand.Flags().BoolVarP(
|
||||||
|
&trimInput,
|
||||||
|
"trim",
|
||||||
|
"t",
|
||||||
|
false,
|
||||||
|
"trim input",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretInsertCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretRmCommand.Flags().BoolVarP(
|
||||||
|
&rmAllSecrets,
|
||||||
|
"all",
|
||||||
|
"a",
|
||||||
|
false,
|
||||||
|
"remove all secrets",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretRmCommand.Flags().BoolVarP(
|
||||||
|
&removeFromPass,
|
||||||
|
"pass",
|
||||||
|
"p",
|
||||||
|
false,
|
||||||
|
"remove generated secrets from a local pass store",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretRmCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretLsCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppSecretLsCommand.Flags().BoolVarP(
|
||||||
|
&internal.MachineReadable,
|
||||||
|
"machine",
|
||||||
|
"m",
|
||||||
|
false,
|
||||||
|
"print machine-readable output",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,53 +9,64 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/service"
|
"coopcloud.tech/abra/pkg/service"
|
||||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"github.com/docker/docker/api/types"
|
containerTypes "github.com/docker/docker/api/types/container"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appServicesCommand = cli.Command{
|
var AppServicesCommand = &cobra.Command{
|
||||||
Name: "services",
|
Use: "services <domain> [flags]",
|
||||||
Aliases: []string{"sr"},
|
Aliases: []string{"sr"},
|
||||||
Usage: "Display all services of an app",
|
Short: "Display all services of an app",
|
||||||
ArgsUsage: "<domain>",
|
Args: cobra.ExactArgs(1),
|
||||||
Flags: []cli.Flag{
|
ValidArgsFunction: func(
|
||||||
internal.DebugFlag,
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
app := internal.ValidateApp(args)
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDeployed {
|
if !deployMeta.IsDeployed {
|
||||||
logrus.Fatalf("%s is not deployed?", app.Name)
|
log.Fatalf("%s is not deployed?", app.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
filters, err := app.Filters(true, true)
|
filters, err := app.Filters(true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
|
containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableCol := []string{"service name", "image"}
|
table, err := formatter.CreateTable()
|
||||||
table := formatter.CreateTable(tableCol)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)"}
|
||||||
|
table.Headers(headers...)
|
||||||
|
|
||||||
|
var rows [][]string
|
||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
var containerNames []string
|
var containerNames []string
|
||||||
for _, containerName := range container.Names {
|
for _, containerName := range container.Names {
|
||||||
@ -66,15 +77,20 @@ var appServicesCommand = cli.Command{
|
|||||||
serviceShortName := service.ContainerToServiceName(container.Names, app.StackName())
|
serviceShortName := service.ContainerToServiceName(container.Names, app.StackName())
|
||||||
serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName)
|
serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName)
|
||||||
|
|
||||||
tableRow := []string{
|
row := []string{
|
||||||
|
serviceShortName,
|
||||||
serviceLongName,
|
serviceLongName,
|
||||||
formatter.RemoveSha(container.Image),
|
|
||||||
}
|
}
|
||||||
table.Append(tableRow)
|
|
||||||
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
table.Render()
|
table.Rows(rows...)
|
||||||
|
|
||||||
return nil
|
if len(rows) > 0 {
|
||||||
|
if err := formatter.PrintTable(table); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,52 +3,118 @@ package app
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var prune bool
|
var AppUndeployCommand = &cobra.Command{
|
||||||
|
Use: "undeploy <domain> [flags]",
|
||||||
|
Aliases: []string{"un"},
|
||||||
|
Short: "Undeploy an app",
|
||||||
|
Long: `This does not destroy any application data.
|
||||||
|
|
||||||
var pruneFlag = &cli.BoolFlag{
|
However, you should remain vigilant, as your swarm installation will consider
|
||||||
Name: "prune, p",
|
any previously attached volumes as eligible for pruning once undeployed.
|
||||||
Destination: &prune,
|
|
||||||
Usage: "Prunes unused containers, networks, and dangling images for an app",
|
Passing "--prune/-p" does not remove those volumes.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
app := internal.ValidateApp(args)
|
||||||
|
stackName := app.StackName()
|
||||||
|
|
||||||
|
if err := app.Recipe.EnsureExists(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cl, err := client.New(app.Server)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("checking whether %s is already deployed", stackName)
|
||||||
|
|
||||||
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !deployMeta.IsDeployed {
|
||||||
|
log.Fatalf("%s is not deployed?", app.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := internal.DeployOverview(
|
||||||
|
app,
|
||||||
|
deployMeta.Version,
|
||||||
|
config.NO_DOMAIN_DEFAULT,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := stack.Deploy{Composefiles: composeFiles, Namespace: stackName}
|
||||||
|
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("initialising undeploy")
|
||||||
|
|
||||||
|
rmOpts := stack.Remove{
|
||||||
|
Namespaces: []string{stackName},
|
||||||
|
Detach: false,
|
||||||
|
}
|
||||||
|
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prune {
|
||||||
|
if err := pruneApp(cl, app); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("undeploy succeeded 🟢")
|
||||||
|
|
||||||
|
if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
|
||||||
|
log.Fatalf("writing recipe version failed: %s", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// pruneApp runs the equivalent of a "docker system prune" but only filtering
|
// pruneApp runs the equivalent of a "docker system prune" but only filtering
|
||||||
// against resources connected with the app deployment. It is not a system wide
|
// against resources connected with the app deployment. It is not a system wide
|
||||||
// prune. Volumes are not pruned to avoid unwated data loss.
|
// prune. Volumes are not pruned to avoid unwated data loss.
|
||||||
func pruneApp(c *cli.Context, cl *dockerClient.Client, app config.App) error {
|
func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
|
||||||
stackName := app.StackName()
|
stackName := app.StackName()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for {
|
|
||||||
logrus.Debugf("polling for %s stack, waiting to be undeployed...", stackName)
|
|
||||||
|
|
||||||
services, err := stack.GetStackServices(ctx, cl, stackName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(services) == 0 {
|
|
||||||
logrus.Debugf("%s undeployed, moving on with pruning logic", stackName)
|
|
||||||
time.Sleep(time.Second) // give runtime more time to tear down related state
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneFilters := filters.NewArgs()
|
pruneFilters := filters.NewArgs()
|
||||||
stackSearch := fmt.Sprintf("%s*", stackName)
|
stackSearch := fmt.Sprintf("%s*", stackName)
|
||||||
pruneFilters.Add("label", stackSearch)
|
pruneFilters.Add("label", stackSearch)
|
||||||
@ -58,14 +124,14 @@ func pruneApp(c *cli.Context, cl *dockerClient.Client, app config.App) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
|
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
|
||||||
logrus.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
|
log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
|
||||||
|
|
||||||
nr, err := cl.NetworksPrune(ctx, pruneFilters)
|
nr, err := cl.NetworksPrune(ctx, pruneFilters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("networks pruned: %d", len(nr.NetworksDeleted))
|
log.Infof("networks pruned: %d", len(nr.NetworksDeleted))
|
||||||
|
|
||||||
ir, err := cl.ImagesPrune(ctx, pruneFilters)
|
ir, err := cl.ImagesPrune(ctx, pruneFilters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -73,66 +139,21 @@ func pruneApp(c *cli.Context, cl *dockerClient.Client, app config.App) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
|
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
|
||||||
logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
|
log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var appUndeployCommand = cli.Command{
|
var (
|
||||||
Name: "undeploy",
|
prune bool
|
||||||
Aliases: []string{"un"},
|
)
|
||||||
ArgsUsage: "<domain>",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
|
||||||
internal.NoInputFlag,
|
|
||||||
pruneFlag,
|
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
Usage: "Undeploy an app",
|
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Description: `
|
|
||||||
This does not destroy any of the application data.
|
|
||||||
|
|
||||||
However, you should remain vigilant, as your swarm installation will consider
|
func init() {
|
||||||
any previously attached volumes as eligible for pruning once undeployed.
|
AppUndeployCommand.Flags().BoolVarP(
|
||||||
|
&prune,
|
||||||
Passing "-p/--prune" does not remove those volumes.
|
"prune",
|
||||||
`,
|
"p",
|
||||||
Action: func(c *cli.Context) error {
|
false,
|
||||||
app := internal.ValidateApp(c)
|
"prune unused containers, networks, and dangling images",
|
||||||
stackName := app.StackName()
|
)
|
||||||
|
|
||||||
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)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isDeployed {
|
|
||||||
logrus.Fatalf("%s is not deployed?", app.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := internal.DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
|
|
||||||
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prune {
|
|
||||||
if err := pruneApp(c, cl, app); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,294 +3,459 @@ package app
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
"coopcloud.tech/abra/pkg/app"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
|
"coopcloud.tech/abra/pkg/envfile"
|
||||||
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/lint"
|
"coopcloud.tech/abra/pkg/lint"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
|
||||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"coopcloud.tech/tagcmp"
|
"coopcloud.tech/tagcmp"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/sirupsen/logrus"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/urfave/cli"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var appUpgradeCommand = cli.Command{
|
var AppUpgradeCommand = &cobra.Command{
|
||||||
Name: "upgrade",
|
Use: "upgrade <domain> [version] [flags]",
|
||||||
Aliases: []string{"up"},
|
Aliases: []string{"up"},
|
||||||
Usage: "Upgrade an app",
|
Short: "Upgrade an app",
|
||||||
ArgsUsage: "<domain> [<version>]",
|
Long: `Upgrade an app.
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
Unlike "abra app deploy", chaos operations are not supported here. Only recipe
|
||||||
internal.NoInputFlag,
|
versions are supported values for "[version]".
|
||||||
internal.ForceFlag,
|
|
||||||
internal.ChaosFlag,
|
It is possible to "--force/-f" an upgrade if you want to re-deploy a specific
|
||||||
internal.NoDomainChecksFlag,
|
version.
|
||||||
internal.DontWaitConvergeFlag,
|
|
||||||
internal.OfflineFlag,
|
Only the deployed version is consulted when trying to determine what upgrades
|
||||||
|
are available. The live deployment version is the "source of truth" in this
|
||||||
|
case. The stored .env version is not consulted.
|
||||||
|
|
||||||
|
An upgrade can be destructive, please ensure you have a copy of your app data
|
||||||
|
beforehand. See "abra app backup" for more.`,
|
||||||
|
Args: cobra.RangeArgs(1, 2),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string,
|
||||||
|
) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch l := len(args); l {
|
||||||
|
case 0:
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
|
case 1:
|
||||||
|
app, err := appPkg.Get(args[0])
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
|
return []string{errMsg}, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Description: `
|
var (
|
||||||
Upgrade an app. You can use it to choose and roll out a new upgrade to an
|
upgradeWarnMessages []string
|
||||||
existing app.
|
chosenUpgrade string
|
||||||
|
availableUpgrades []string
|
||||||
|
upgradeReleaseNotes string
|
||||||
|
)
|
||||||
|
|
||||||
This command specifically supports incrementing the version of running apps, as
|
app := internal.ValidateApp(args)
|
||||||
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
|
if err := app.Recipe.Ensure(recipe.EnsureContext{
|
||||||
useful if the container runtime has gotten into a weird state.
|
Chaos: internal.Chaos,
|
||||||
|
Offline: internal.Offline,
|
||||||
This action could be destructive, please ensure you have a copy of your app
|
// Ignore the env version for now, to make sure we are at the latest commit.
|
||||||
data beforehand.
|
// This enables us to get release notes, that were added after a release.
|
||||||
|
IgnoreEnvVersion: true,
|
||||||
Chaos mode ("--chaos") will deploy your local checkout of a recipe as-is,
|
}); err != nil {
|
||||||
including unstaged changes and can be useful for live hacking and testing new
|
log.Fatal(err)
|
||||||
recipes.
|
|
||||||
`,
|
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
stackName := app.StackName()
|
|
||||||
|
|
||||||
specificVersion := c.Args().Get(1)
|
|
||||||
if specificVersion != "" && internal.Chaos {
|
|
||||||
logrus.Fatal("cannot use <version> and --chaos together")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Chaos {
|
|
||||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !internal.Offline {
|
|
||||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recipe, err := recipePkg.Get(app.Recipe, internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := lint.LintForErrors(recipe); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("checking whether %s is already deployed", stackName)
|
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
|
deployMeta, err := ensureDeployed(cl, app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDeployed {
|
if err := lint.LintForErrors(app.Recipe); err != nil {
|
||||||
logrus.Fatalf("%s is not deployed?", app.Name)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
|
versions, err := app.Recipe.Tags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
versions, err := recipePkg.GetRecipeCatalogueVersions(app.Recipe, catl)
|
// NOTE(d1): we've no idea what the live deployment version is, so every
|
||||||
if err != nil {
|
// possible upgrade can be shown. it's up to the user to make the choice
|
||||||
logrus.Fatal(err)
|
if deployMeta.Version == config.UNKNOWN_DEFAULT {
|
||||||
}
|
|
||||||
|
|
||||||
if len(versions) == 0 && !internal.Chaos {
|
|
||||||
logrus.Warn("no published versions in catalogue, trying local recipe repository")
|
|
||||||
recipeVersions, err := recipePkg.GetRecipeVersions(app.Recipe, internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Warn(err)
|
|
||||||
}
|
|
||||||
for _, recipeVersion := range recipeVersions {
|
|
||||||
for version := range recipeVersion {
|
|
||||||
versions = append(versions, version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var availableUpgrades []string
|
|
||||||
if deployedVersion == "unknown" {
|
|
||||||
availableUpgrades = versions
|
availableUpgrades = versions
|
||||||
logrus.Warnf("failed to determine deployed version of %s", app.Name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if specificVersion != "" {
|
if len(args) == 2 && args[1] != "" {
|
||||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
chosenUpgrade = args[1]
|
||||||
|
|
||||||
|
if err := validateUpgradeVersionArg(chosenUpgrade, app, deployMeta); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
availableUpgrades = append(availableUpgrades, chosenUpgrade)
|
||||||
|
}
|
||||||
|
|
||||||
|
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenUpgrade == "" {
|
||||||
|
upgradeAvailable, err := ensureUpgradesAvailable(app, versions, &availableUpgrades, deployMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
|
||||||
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) || parsedSpecificVersion.Equals(parsedDeployedVersion) {
|
|
||||||
logrus.Fatalf("%s is not an upgrade for %s?", deployedVersion, specificVersion)
|
|
||||||
}
|
|
||||||
availableUpgrades = append(availableUpgrades, specificVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if deployedVersion != "unknown" && !internal.Chaos && specificVersion == "" {
|
|
||||||
for _, version := range versions {
|
|
||||||
parsedVersion, err := tagcmp.Parse(version)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
|
|
||||||
availableUpgrades = append(availableUpgrades, version)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(availableUpgrades) == 0 && !internal.Force {
|
if !upgradeAvailable {
|
||||||
logrus.Infof("no available upgrades, you're on latest (%s) ✌️", deployedVersion)
|
log.Info("no available upgrades")
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var chosenUpgrade string
|
if internal.Force || internal.NoInput || chosenUpgrade != "" {
|
||||||
if len(availableUpgrades) > 0 && !internal.Chaos {
|
if len(availableUpgrades) > 0 {
|
||||||
if internal.Force || internal.NoInput || specificVersion != "" {
|
|
||||||
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
|
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
|
||||||
logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
|
}
|
||||||
} else {
|
} else {
|
||||||
prompt := &survey.Select{
|
if err := chooseUpgrade(availableUpgrades, deployMeta, &chosenUpgrade); err != nil {
|
||||||
Message: fmt.Sprintf("Please select an upgrade (current version: %s):", deployedVersion),
|
log.Fatal(err)
|
||||||
Options: internal.ReverseStringList(availableUpgrades),
|
|
||||||
}
|
|
||||||
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.Force && chosenUpgrade == "" {
|
if internal.Force &&
|
||||||
logrus.Warnf("%s is already upgraded to latest but continuing (--force/--chaos)", app.Name)
|
chosenUpgrade == "" &&
|
||||||
chosenUpgrade = deployedVersion
|
deployMeta.Version != config.UNKNOWN_DEFAULT {
|
||||||
|
chosenUpgrade = deployMeta.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
// if release notes written after git tag published, read them before we
|
if chosenUpgrade == "" {
|
||||||
// check out the tag and then they'll appear to be missing. this covers
|
log.Fatal("unknown deployed version, unable to upgrade")
|
||||||
// when we obviously will forget to write release notes before publishing
|
|
||||||
var releaseNotes string
|
|
||||||
for _, version := range versions {
|
|
||||||
parsedVersion, err := tagcmp.Parse(version)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parsedVersion.Equals(parsedDeployedVersion)) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
|
|
||||||
note, err := internal.GetReleaseNotes(app.Recipe, version)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if note != "" {
|
|
||||||
releaseNotes += fmt.Sprintf("%s\n", note)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Chaos {
|
log.Debugf("choosing %s as version to upgrade", chosenUpgrade)
|
||||||
if err := recipePkg.EnsureVersion(app.Recipe, chosenUpgrade); err != nil {
|
|
||||||
logrus.Fatal(err)
|
// Get the release notes before checking out the new version in the
|
||||||
}
|
// recipe. This enables us to get release notes, that were added after
|
||||||
|
// a release.
|
||||||
|
if err := getReleaseNotes(app, versions, chosenUpgrade, deployMeta, &upgradeReleaseNotes); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.Chaos {
|
if _, err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil {
|
||||||
logrus.Warn("chaos mode engaged")
|
log.Fatal(err)
|
||||||
var err error
|
|
||||||
chosenUpgrade, err = recipePkg.ChaosVersion(app.Recipe)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
|
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
|
||||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
for k, v := range abraShEnv {
|
for k, v := range abraShEnv {
|
||||||
app.Env[k] = v
|
app.Env[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stackName := app.StackName()
|
||||||
deployOpts := stack.Deploy{
|
deployOpts := stack.Deploy{
|
||||||
Composefiles: composeFiles,
|
Composefiles: composeFiles,
|
||||||
Namespace: stackName,
|
Namespace: stackName,
|
||||||
Prune: false,
|
Prune: false,
|
||||||
ResolveImage: stack.ResolveImageAlways,
|
ResolveImage: stack.ResolveImageAlways,
|
||||||
|
Detach: false,
|
||||||
}
|
}
|
||||||
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
|
||||||
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.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
|
|
||||||
config.SetUpdateLabel(compose, stackName, app.Env)
|
|
||||||
|
|
||||||
envVars, err := config.CheckEnv(app)
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appPkg.ExposeAllEnv(stackName, compose, app.Env)
|
||||||
|
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
|
||||||
|
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
|
||||||
|
if internal.Chaos {
|
||||||
|
appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
|
||||||
|
}
|
||||||
|
appPkg.SetUpdateLabel(compose, stackName, app.Env)
|
||||||
|
|
||||||
|
envVars, err := appPkg.CheckEnv(app)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, envVar := range envVars {
|
for _, envVar := range envVars {
|
||||||
if !envVar.Present {
|
if !envVar.Present {
|
||||||
logrus.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
|
upgradeWarnMessages = append(upgradeWarnMessages,
|
||||||
|
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil {
|
if showReleaseNotes {
|
||||||
logrus.Fatal(err)
|
fmt.Print(upgradeReleaseNotes)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName)
|
if upgradeReleaseNotes == "" {
|
||||||
|
upgradeWarnMessages = append(
|
||||||
|
upgradeWarnMessages,
|
||||||
|
fmt.Sprintf("no release notes available for %s", chosenUpgrade),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := internal.DeployOverview(
|
||||||
|
app,
|
||||||
|
deployMeta.Version,
|
||||||
|
chosenUpgrade,
|
||||||
|
upgradeReleaseNotes,
|
||||||
|
upgradeWarnMessages,
|
||||||
|
); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
|
|
||||||
|
|
||||||
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
|
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
|
||||||
logrus.Fatal(err)
|
|
||||||
|
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := app.Filters(true, false, serviceNames...)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stack.RunDeploy(
|
||||||
|
cl,
|
||||||
|
deployOpts,
|
||||||
|
compose,
|
||||||
|
stackName,
|
||||||
|
app.Server,
|
||||||
|
internal.DontWaitConverge,
|
||||||
|
f,
|
||||||
|
); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
|
postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
|
||||||
if ok && !internal.DontWaitConverge {
|
if ok && !internal.DontWaitConverge {
|
||||||
logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
|
log.Debugf("run the following post-deploy commands: %s", postDeployCmds)
|
||||||
|
|
||||||
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
|
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
|
||||||
logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
|
log.Fatalf("attempting to run post deploy commands, saw: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if err := app.WriteRecipeVersion(chosenUpgrade, false); err != nil {
|
||||||
|
log.Fatalf("writing recipe version failed: %s", err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chooseUpgrade prompts the user to choose an upgrade interactively.
|
||||||
|
func chooseUpgrade(
|
||||||
|
availableUpgrades []string,
|
||||||
|
deployMeta stack.DeployMeta,
|
||||||
|
chosenUpgrade *string,
|
||||||
|
) error {
|
||||||
|
msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version)
|
||||||
|
|
||||||
|
if deployMeta.IsChaos {
|
||||||
|
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
|
||||||
|
|
||||||
|
msg = fmt.Sprintf(
|
||||||
|
"please select an upgrade (version: %s, chaos: %s):",
|
||||||
|
deployMeta.Version,
|
||||||
|
chaosVersion,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := &survey.Select{
|
||||||
|
Message: msg,
|
||||||
|
Options: internal.SortVersionsDesc(availableUpgrades),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := survey.AskOne(prompt, chosenUpgrade); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReleaseNotes(
|
||||||
|
app app.App,
|
||||||
|
versions []string,
|
||||||
|
chosenUpgrade string,
|
||||||
|
deployMeta stack.DeployMeta,
|
||||||
|
upgradeReleaseNotes *string,
|
||||||
|
) error {
|
||||||
|
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing chosen upgrade version failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing deployment version failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, version := range internal.SortVersionsDesc(versions) {
|
||||||
|
parsedVersion, err := tagcmp.Parse(version)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing recipe version failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
|
||||||
|
parsedVersion.IsLessThan(parsedChosenUpgrade) {
|
||||||
|
note, err := app.Recipe.GetReleaseNotes(version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if note != "" {
|
||||||
|
// NOTE(d1): trim any final newline on the end of the note itself before
|
||||||
|
// we manually handle newlines (for multiple release notes and
|
||||||
|
// ensuring space between the warning messages)
|
||||||
|
note = strings.TrimSuffix(note, "\n")
|
||||||
|
|
||||||
|
*upgradeReleaseNotes += fmt.Sprintf("%s\n", note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureUpgradesAvailable ensures that there are available upgrades.
|
||||||
|
func ensureUpgradesAvailable(
|
||||||
|
app app.App,
|
||||||
|
versions []string,
|
||||||
|
availableUpgrades *[]string,
|
||||||
|
deployMeta stack.DeployMeta,
|
||||||
|
) (bool, error) {
|
||||||
|
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("parsing deployed version failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, version := range versions {
|
||||||
|
parsedVersion, err := tagcmp.Parse(version)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("parsing recipe version failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
|
||||||
|
!(parsedVersion.Equals(parsedDeployedVersion)) {
|
||||||
|
*availableUpgrades = append(*availableUpgrades, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*availableUpgrades) == 0 && !internal.Force {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateUpgradeVersionArg validates the specific version.
|
||||||
|
func validateUpgradeVersionArg(
|
||||||
|
specificVersion string,
|
||||||
|
app app.App,
|
||||||
|
deployMeta stack.DeployMeta,
|
||||||
|
) error {
|
||||||
|
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("'%s' is not a known version", deployMeta.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) &&
|
||||||
|
!parsedSpecificVersion.Equals(parsedDeployedVersion) {
|
||||||
|
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
|
||||||
|
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDeployed ensures the app is deployed and if so, returns deployment
|
||||||
|
// meta info.
|
||||||
|
func ensureDeployed(cl *dockerClient.Client, app app.App) (stack.DeployMeta, error) {
|
||||||
|
log.Debugf("checking whether %s is already deployed", app.StackName())
|
||||||
|
|
||||||
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||||
|
if err != nil {
|
||||||
|
return stack.DeployMeta{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !deployMeta.IsDeployed {
|
||||||
|
return stack.DeployMeta{}, fmt.Errorf("%s is not deployed?", app.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deployMeta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var showReleaseNotes bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppUpgradeCommand.Flags().BoolVarP(
|
||||||
|
&internal.Force,
|
||||||
|
"force",
|
||||||
|
"f",
|
||||||
|
false,
|
||||||
|
"perform action without further prompt",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppUpgradeCommand.Flags().BoolVarP(
|
||||||
|
&internal.NoDomainChecks,
|
||||||
|
"no-domain-checks",
|
||||||
|
"D",
|
||||||
|
false,
|
||||||
|
"disable public DNS checks",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppUpgradeCommand.Flags().BoolVarP(
|
||||||
|
&internal.DontWaitConverge, "no-converge-checks",
|
||||||
|
"c",
|
||||||
|
false,
|
||||||
|
"disable converge logic checks",
|
||||||
|
)
|
||||||
|
|
||||||
|
AppUpgradeCommand.Flags().BoolVarP(
|
||||||
|
&showReleaseNotes,
|
||||||
|
"releasenotes",
|
||||||
|
"r",
|
||||||
|
false,
|
||||||
|
"only show release notes",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,117 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
|
||||||
"coopcloud.tech/abra/pkg/client"
|
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
"github.com/docker/distribution/reference"
|
|
||||||
"github.com/olekukonko/tablewriter"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
func sortServiceByName(versions [][]string) func(i, j int) bool {
|
|
||||||
return func(i, j int) bool {
|
|
||||||
// NOTE(d1): corresponds to the `tableCol` definition below
|
|
||||||
if versions[i][1] == "app" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return versions[i][1] < versions[j][1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getImagePath returns the image name
|
|
||||||
func getImagePath(image string) (string, error) {
|
|
||||||
img, err := reference.ParseNormalizedNamed(image)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
path := reference.Path(img)
|
|
||||||
|
|
||||||
path = formatter.StripTagMeta(path)
|
|
||||||
|
|
||||||
logrus.Debugf("parsed %s from %s", path, image)
|
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var appVersionCommand = cli.Command{
|
|
||||||
Name: "version",
|
|
||||||
Aliases: []string{"v"},
|
|
||||||
ArgsUsage: "<domain>",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
|
||||||
internal.NoInputFlag,
|
|
||||||
internal.OfflineFlag,
|
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
Usage: "Show version info of a deployed app",
|
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("checking whether %s is already deployed", stackName)
|
|
||||||
|
|
||||||
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isDeployed {
|
|
||||||
logrus.Fatalf("%s is not deployed?", app.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if deployedVersion == "unknown" {
|
|
||||||
logrus.Fatalf("failed to determine version of deployed %s", app.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe, internal.Offline)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
versionsMeta := make(map[string]recipe.ServiceMeta)
|
|
||||||
for _, recipeVersion := range recipeMeta.Versions {
|
|
||||||
if currentVersion, exists := recipeVersion[deployedVersion]; exists {
|
|
||||||
versionsMeta = currentVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(versionsMeta) == 0 {
|
|
||||||
logrus.Fatalf("could not retrieve deployed version (%s) from recipe catalogue?", deployedVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
tableCol := []string{"version", "service", "image", "tag"}
|
|
||||||
table := formatter.CreateTable(tableCol)
|
|
||||||
|
|
||||||
var versions [][]string
|
|
||||||
for serviceName, versionMeta := range versionsMeta {
|
|
||||||
versions = append(versions, []string{deployedVersion, serviceName, versionMeta.Image, versionMeta.Tag})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(versions, sortServiceByName(versions))
|
|
||||||
|
|
||||||
for _, version := range versions {
|
|
||||||
table.Append(version)
|
|
||||||
}
|
|
||||||
|
|
||||||
table.SetAutoMergeCellsByColumnIndex([]int{0})
|
|
||||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
|
||||||
table.Render()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@ -2,116 +2,158 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appVolumeListCommand = cli.Command{
|
var AppVolumeListCommand = &cobra.Command{
|
||||||
Name: "list",
|
Use: "list <domain> [flags]",
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
ArgsUsage: "<domain>",
|
Short: "List volumes associated with an app",
|
||||||
Flags: []cli.Flag{
|
Args: cobra.ExactArgs(1),
|
||||||
internal.DebugFlag,
|
ValidArgsFunction: func(
|
||||||
internal.NoInputFlag,
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Usage: "List volumes associated with an app",
|
app := internal.ValidateApp(args)
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
filters, err := app.Filters(false, true)
|
filters, err := app.Filters(false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
|
volumes, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
table := formatter.CreateTable([]string{"name", "created", "mounted"})
|
headers := []string{"NAME", "ON SERVER"}
|
||||||
var volTable [][]string
|
|
||||||
for _, volume := range volumeList {
|
table, err := formatter.CreateTable()
|
||||||
volRow := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
|
if err != nil {
|
||||||
volTable = append(volTable, volRow)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
table.AppendBulk(volTable)
|
table.Headers(headers...)
|
||||||
|
|
||||||
if table.NumLines() > 0 {
|
var rows [][]string
|
||||||
table.Render()
|
for _, volume := range volumes {
|
||||||
} else {
|
row := []string{volume.Name, volume.Mountpoint}
|
||||||
logrus.Warnf("no volumes created for %s", app.Name)
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
table.Rows(rows...)
|
||||||
|
|
||||||
|
if len(rows) > 0 {
|
||||||
|
if err := formatter.PrintTable(table); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("no volumes created for %s", app.Name)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var appVolumeRemoveCommand = cli.Command{
|
var AppVolumeRemoveCommand = &cobra.Command{
|
||||||
Name: "remove",
|
Use: "remove <domain> [volume] [flags]",
|
||||||
Usage: "Remove volume(s) associated with an app",
|
Short: "Remove volume(s) associated with an app",
|
||||||
Description: `
|
Long: `Remove volumes associated with an app.
|
||||||
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
|
The app in question must be undeployed before you try to remove volumes. See
|
||||||
undeploy <domain>" for more.
|
"abra app undeploy <domain>" for more.
|
||||||
|
|
||||||
The command is interactive and will show a multiple select input which allows
|
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
|
you to make a seclection. Use the "?" key to see more help on navigating this
|
||||||
interface.
|
interface.
|
||||||
|
|
||||||
Passing "--force/-f" will select all volumes for removal. Be careful.
|
Passing "--force/-f" will select all volumes for removal. Be careful.`,
|
||||||
`,
|
Example: ` # delete volumes interactively
|
||||||
ArgsUsage: "<domain>",
|
abra app volume rm 1312.net
|
||||||
Aliases: []string{"rm"},
|
|
||||||
Flags: []cli.Flag{
|
# delete specific volume
|
||||||
internal.DebugFlag,
|
abra app volume rm 1312.net my_volume`,
|
||||||
internal.NoInputFlag,
|
Aliases: []string{"rm"},
|
||||||
internal.ForceFlag,
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.AppNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.AppNameComplete,
|
app := internal.ValidateApp(args)
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
app := internal.ValidateApp(c)
|
var volumeToDelete string
|
||||||
|
if len(args) == 2 {
|
||||||
|
volumeToDelete = args[1]
|
||||||
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isDeployed {
|
if deployMeta.IsDeployed {
|
||||||
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
|
log.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
filters, err := app.Filters(false, true)
|
filters, err := app.Filters(false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
|
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
volumeNames := client.GetVolumeNames(volumeList)
|
volumeNames := client.GetVolumeNames(volumeList)
|
||||||
|
|
||||||
|
if volumeToDelete != "" {
|
||||||
|
var exactMatch bool
|
||||||
|
|
||||||
|
fullVolumeToDeleteName := fmt.Sprintf("%s_%s", app.StackName(), volumeToDelete)
|
||||||
|
for _, volName := range volumeNames {
|
||||||
|
if volName == fullVolumeToDeleteName {
|
||||||
|
exactMatch = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exactMatch {
|
||||||
|
log.Fatalf("unable to remove volume: no volume with name '%s'?", volumeToDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.RemoveVolumes(cl, context.Background(), []string{fullVolumeToDeleteName}, internal.Force, 5)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("removing volume %s failed: %s", volumeToDelete, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("volume %s removed successfully", volumeToDelete)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var volumesToRemove []string
|
var volumesToRemove []string
|
||||||
if !internal.Force && !internal.NoInput {
|
if !internal.Force && !internal.NoInput {
|
||||||
volumesPrompt := &survey.MultiSelect{
|
volumesPrompt := &survey.MultiSelect{
|
||||||
@ -122,7 +164,7 @@ Passing "--force/-f" will select all volumes for removal. Be careful.
|
|||||||
Default: volumeNames,
|
Default: volumeNames,
|
||||||
}
|
}
|
||||||
if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil {
|
if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,27 +173,30 @@ Passing "--force/-f" will select all volumes for removal. Be careful.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(volumesToRemove) > 0 {
|
if len(volumesToRemove) > 0 {
|
||||||
err = client.RemoveVolumes(cl, context.Background(), app.Server, volumesToRemove, internal.Force)
|
err := client.RemoveVolumes(cl, context.Background(), volumesToRemove, internal.Force, 5)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatalf("removing volumes failed: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Info("volumes removed successfully")
|
log.Infof("%d volumes removed successfully", len(volumesToRemove))
|
||||||
} else {
|
} else {
|
||||||
logrus.Info("no volumes removed")
|
log.Info("no volumes removed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var appVolumeCommand = cli.Command{
|
var AppVolumeCommand = &cobra.Command{
|
||||||
Name: "volume",
|
Use: "volume [cmd] [args] [flags]",
|
||||||
Aliases: []string{"vl"},
|
Aliases: []string{"vl"},
|
||||||
Usage: "Manage app volumes",
|
Short: "Manage app volumes",
|
||||||
ArgsUsage: "<domain>",
|
}
|
||||||
Subcommands: []cli.Command{
|
|
||||||
appVolumeListCommand,
|
func init() {
|
||||||
appVolumeRemoveCommand,
|
AppVolumeRemoveCommand.Flags().BoolVarP(
|
||||||
},
|
&internal.Force,
|
||||||
|
"force",
|
||||||
|
"f",
|
||||||
|
false,
|
||||||
|
"perform action without further prompt",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
@ -12,100 +13,102 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var catalogueGenerateCommand = cli.Command{
|
var CatalogueGenerateCommand = &cobra.Command{
|
||||||
Name: "generate",
|
Use: "generate [recipe] [flags]",
|
||||||
Aliases: []string{"g"},
|
Aliases: []string{"g"},
|
||||||
Usage: "Generate the recipe catalogue",
|
Short: "Generate the recipe catalogue",
|
||||||
Flags: []cli.Flag{
|
Long: `Generate a new copy of the recipe catalogue.
|
||||||
internal.DebugFlag,
|
|
||||||
internal.NoInputFlag,
|
|
||||||
internal.PublishFlag,
|
|
||||||
internal.DryFlag,
|
|
||||||
internal.SkipUpdatesFlag,
|
|
||||||
internal.ChaosFlag,
|
|
||||||
internal.OfflineFlag,
|
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
Description: `
|
|
||||||
Generate a new copy of the recipe catalogue which can be found on:
|
|
||||||
|
|
||||||
https://recipes.coopcloud.tech (website that humans read)
|
N.B. this command **will** wipe local unstaged changes from your local recipes
|
||||||
https://recipes.coopcloud.tech/recipes.json (JSON that Abra reads)
|
if present. "--chaos/-C" on this command refers to the catalogue repository
|
||||||
|
("$ABRA_DIR/catalogue") and not the recipes. Please take care not to lose your
|
||||||
It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository
|
changes.
|
||||||
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
|
It is possible to generate new metadata for a single recipe by passing
|
||||||
<recipe>. The existing local catalogue will be updated, not overwritten.
|
[recipe]. The existing local catalogue will be updated, not overwritten.
|
||||||
|
|
||||||
It is quite easy to get rate limited by Docker Hub when running this command.
|
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
|
If you have a Hub account you can "docker login" and Abra will automatically
|
||||||
"--user" and "--pass".
|
use those details.
|
||||||
|
|
||||||
Push your new release to git.coopcloud.tech with "-p/--publish". This requires
|
Push your new release to git.coopcloud.tech with "--publish/-p". This requires
|
||||||
that you have permission to git push to these repositories and have your SSH
|
that you have permission to git push to these repositories and have your SSH
|
||||||
keys configured on your account.
|
keys configured on your account.`,
|
||||||
`,
|
Args: cobra.RangeArgs(0, 1),
|
||||||
ArgsUsage: "[<recipe>]",
|
ValidArgsFunction: func(
|
||||||
BashComplete: autocomplete.RecipeNameComplete,
|
cmd *cobra.Command,
|
||||||
Action: func(c *cli.Context) error {
|
args []string,
|
||||||
recipeName := c.Args().First()
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.RecipeNameComplete()
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var recipeName string
|
||||||
|
if len(args) > 0 {
|
||||||
|
recipeName = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
if recipeName != "" {
|
if recipeName != "" {
|
||||||
internal.ValidateRecipe(c)
|
internal.ValidateRecipe(args, cmd.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := catalogue.EnsureCatalogue(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Chaos {
|
if !internal.Chaos {
|
||||||
if err := catalogue.EnsureIsClean(); err != nil {
|
if err := catalogue.EnsureIsClean(); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repos, err := recipe.ReadReposMetadata()
|
repos, err := recipe.ReadReposMetadata(internal.Debug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var barLength int
|
barLength := len(repos)
|
||||||
var logMsg string
|
|
||||||
if recipeName != "" {
|
if recipeName != "" {
|
||||||
barLength = 1
|
barLength = 1
|
||||||
logMsg = fmt.Sprintf("ensuring %v recipe is cloned & up-to-date", barLength)
|
|
||||||
} else {
|
|
||||||
barLength = len(repos)
|
|
||||||
logMsg = fmt.Sprintf("ensuring %v recipes are cloned & up-to-date, this could take some time...", barLength)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.SkipUpdates {
|
if !skipUpdates {
|
||||||
logrus.Warn(logMsg)
|
if err := recipe.UpdateRepositories(repos, recipeName, internal.Debug); err != nil {
|
||||||
if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
|
log.Fatal(err)
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var warnings []string
|
||||||
catl := make(recipe.RecipeCatalogue)
|
catl := make(recipe.RecipeCatalogue)
|
||||||
catlBar := formatter.CreateProgressbar(barLength, "generating catalogue metadata...")
|
catlBar := formatter.CreateProgressbar(barLength, "collecting catalogue metadata")
|
||||||
for _, recipeMeta := range repos {
|
for _, recipeMeta := range repos {
|
||||||
if recipeName != "" && recipeName != recipeMeta.Name {
|
if recipeName != "" && recipeName != recipeMeta.Name {
|
||||||
catlBar.Add(1)
|
if !internal.Debug {
|
||||||
|
catlBar.Add(1)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
versions, err := recipe.GetRecipeVersions(recipeMeta.Name, internal.Offline)
|
r := recipe.Get(recipeMeta.Name)
|
||||||
|
versions, warnMsgs, err := r.GetRecipeVersions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warn(err)
|
warnings = append(warnings, err.Error())
|
||||||
|
}
|
||||||
|
if len(warnMsgs) > 0 {
|
||||||
|
warnings = append(warnings, warnMsgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name)
|
features, category, warnMsgs, err := recipe.GetRecipeFeaturesAndCategory(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warn(err)
|
warnings = append(warnings, err.Error())
|
||||||
|
}
|
||||||
|
if len(warnMsgs) > 0 {
|
||||||
|
warnings = append(warnings, warnMsgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
catl[recipeMeta.Name] = recipe.RecipeMeta{
|
catl[recipeMeta.Name] = recipe.RecipeMeta{
|
||||||
@ -121,103 +124,152 @@ keys configured on your account.
|
|||||||
Features: features,
|
Features: features,
|
||||||
}
|
}
|
||||||
|
|
||||||
catlBar.Add(1)
|
if !internal.Debug {
|
||||||
|
catlBar.Add(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := catlBar.Close(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var uniqueWarnings []string
|
||||||
|
for _, w := range warnings {
|
||||||
|
if !slices.Contains(uniqueWarnings, w) {
|
||||||
|
uniqueWarnings = append(uniqueWarnings, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, warnMsg := range uniqueWarnings {
|
||||||
|
log.Warn(warnMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
recipesJSON, err := json.MarshalIndent(catl, "", " ")
|
recipesJSON, err := json.MarshalIndent(catl, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if recipeName == "" {
|
if recipeName == "" {
|
||||||
if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil {
|
if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
catlFS, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
catlFS, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
catlFS[recipeName] = catl[recipeName]
|
catlFS[recipeName] = catl[recipeName]
|
||||||
|
|
||||||
updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ")
|
updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(config.RECIPES_JSON, updatedRecipesJSON, 0764); err != nil {
|
if err := ioutil.WriteFile(config.RECIPES_JSON, updatedRecipesJSON, 0764); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON)
|
log.Infof("generated recipe catalogue: %s", config.RECIPES_JSON)
|
||||||
|
|
||||||
cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
|
cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
|
||||||
if internal.Publish {
|
if publishChanges {
|
||||||
|
|
||||||
isClean, err := gitPkg.IsClean(cataloguePath)
|
isClean, err := gitPkg.IsClean(cataloguePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isClean {
|
if isClean {
|
||||||
if !internal.Dry {
|
if !internal.Dry {
|
||||||
logrus.Fatalf("no changes discovered in %s, nothing to publish?", cataloguePath)
|
log.Fatalf("no changes discovered in %s, nothing to publish?", cataloguePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := "chore: publish new catalogue release changes"
|
msg := "chore: publish new catalogue release changes"
|
||||||
if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil {
|
if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := git.PlainOpen(cataloguePath)
|
repo, err := git.PlainOpen(cataloguePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
|
sshURL := fmt.Sprintf(config.TOOLSHED_SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
|
||||||
if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
|
if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gitPkg.Push(cataloguePath, "origin-ssh", false, internal.Dry); err != nil {
|
if err := gitPkg.Push(cataloguePath, "origin-ssh", false, internal.Dry); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := git.PlainOpen(cataloguePath)
|
repo, err := git.PlainOpen(cataloguePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
head, err := repo.Head()
|
head, err := repo.Head()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Dry && internal.Publish {
|
if !internal.Dry && publishChanges {
|
||||||
url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, 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)
|
log.Infof("new changes published: %s", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.Dry {
|
if internal.Dry {
|
||||||
logrus.Info("dry run: no changes published")
|
log.Info("dry run: no changes published")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
|
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
|
||||||
var CatalogueCommand = cli.Command{
|
var CatalogueCommand = &cobra.Command{
|
||||||
Name: "catalogue",
|
Use: "catalogue [cmd] [args] [flags]",
|
||||||
Usage: "Manage the recipe catalogue",
|
Short: "Manage the recipe catalogue",
|
||||||
Aliases: []string{"c"},
|
Aliases: []string{"c"},
|
||||||
ArgsUsage: "<recipe>",
|
}
|
||||||
Description: "This command helps recipe packagers interact with the recipe catalogue",
|
|
||||||
Subcommands: []cli.Command{
|
var (
|
||||||
catalogueGenerateCommand,
|
publishChanges bool
|
||||||
},
|
skipUpdates bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
CatalogueGenerateCommand.Flags().BoolVarP(
|
||||||
|
&publishChanges,
|
||||||
|
"publish",
|
||||||
|
"p",
|
||||||
|
false,
|
||||||
|
"publish changes to git.coopcloud.tech",
|
||||||
|
)
|
||||||
|
|
||||||
|
CatalogueGenerateCommand.Flags().BoolVarP(
|
||||||
|
&internal.Dry,
|
||||||
|
"dry-run",
|
||||||
|
"r",
|
||||||
|
false,
|
||||||
|
"report changes that would be made",
|
||||||
|
)
|
||||||
|
|
||||||
|
CatalogueGenerateCommand.Flags().BoolVarP(
|
||||||
|
&skipUpdates,
|
||||||
|
"skip-updates",
|
||||||
|
"s",
|
||||||
|
false,
|
||||||
|
"skip updating recipe repositories",
|
||||||
|
)
|
||||||
|
|
||||||
|
CatalogueGenerateCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
206
cli/cli.go
206
cli/cli.go
@ -1,206 +0,0 @@
|
|||||||
// Package cli provides the interface for the command-line.
|
|
||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/app"
|
|
||||||
"coopcloud.tech/abra/cli/catalogue"
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
|
||||||
"coopcloud.tech/abra/cli/recipe"
|
|
||||||
"coopcloud.tech/abra/cli/server"
|
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
|
||||||
cataloguePkg "coopcloud.tech/abra/pkg/catalogue"
|
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
"coopcloud.tech/abra/pkg/web"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AutoCompleteCommand helps people set up auto-complete in their shells
|
|
||||||
var AutoCompleteCommand = cli.Command{
|
|
||||||
Name: "autocomplete",
|
|
||||||
Aliases: []string{"ac"},
|
|
||||||
Usage: "Configure shell autocompletion (recommended)",
|
|
||||||
Description: `
|
|
||||||
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
|
|
||||||
`,
|
|
||||||
ArgsUsage: "<shell>",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
|
||||||
},
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
shellType := c.Args().First()
|
|
||||||
|
|
||||||
if shellType == "" {
|
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("no shell provided"))
|
|
||||||
}
|
|
||||||
|
|
||||||
supportedShells := map[string]bool{
|
|
||||||
"bash": true,
|
|
||||||
"zsh": true,
|
|
||||||
"fizsh": true,
|
|
||||||
"fish": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := supportedShells[shellType]; !ok {
|
|
||||||
logrus.Fatalf("%s is not a supported shell right now, sorry", shellType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if shellType == "fizsh" {
|
|
||||||
shellType = "zsh" // handled the same on the autocompletion side
|
|
||||||
}
|
|
||||||
|
|
||||||
autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion")
|
|
||||||
if err := os.Mkdir(autocompletionDir, 0764); err != nil {
|
|
||||||
if !os.IsExist(err) {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
logrus.Debugf("%s already created", autocompletionDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType)
|
|
||||||
if _, err := os.Stat(autocompletionFile); err != nil && os.IsNotExist(err) {
|
|
||||||
url := fmt.Sprintf("https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/autocomplete/%s", shellType)
|
|
||||||
logrus.Infof("fetching %s", url)
|
|
||||||
if err := web.GetFile(autocompletionFile, url); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch shellType {
|
|
||||||
case "bash":
|
|
||||||
fmt.Println(fmt.Sprintf(`
|
|
||||||
# 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
|
|
||||||
# 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 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
|
|
||||||
# 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpgradeCommand upgrades abra in-place.
|
|
||||||
var UpgradeCommand = cli.Command{
|
|
||||||
Name: "upgrade",
|
|
||||||
Aliases: []string{"u"},
|
|
||||||
Usage: "Upgrade Abra itself",
|
|
||||||
Description: `
|
|
||||||
Upgrade Abra in-place with the latest stable or release candidate.
|
|
||||||
|
|
||||||
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 {
|
|
||||||
mainURL := "https://install.abra.coopcloud.tech"
|
|
||||||
cmd := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
|
|
||||||
|
|
||||||
if internal.RC {
|
|
||||||
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
|
|
||||||
cmd = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("attempting to run %s", cmd)
|
|
||||||
|
|
||||||
if err := internal.RunCmd(cmd); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAbraApp(version, commit string) *cli.App {
|
|
||||||
app := &cli.App{
|
|
||||||
Name: "abra",
|
|
||||||
Usage: `The Co-op Cloud command-line utility belt 🎩🐇
|
|
||||||
____ ____ _ _
|
|
||||||
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
|
|
||||||
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
|
|
||||||
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
|
|
||||||
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|
|
||||||
|_|
|
|
||||||
`,
|
|
||||||
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
|
|
||||||
Commands: []cli.Command{
|
|
||||||
app.AppCommand,
|
|
||||||
server.ServerCommand,
|
|
||||||
recipe.RecipeCommand,
|
|
||||||
catalogue.CatalogueCommand,
|
|
||||||
UpgradeCommand,
|
|
||||||
AutoCompleteCommand,
|
|
||||||
},
|
|
||||||
BashComplete: autocomplete.SubcommandComplete,
|
|
||||||
}
|
|
||||||
|
|
||||||
app.EnableBashCompletion = true
|
|
||||||
|
|
||||||
app.Before = func(c *cli.Context) error {
|
|
||||||
paths := []string{
|
|
||||||
config.ABRA_DIR,
|
|
||||||
config.SERVERS_DIR,
|
|
||||||
config.RECIPES_DIR,
|
|
||||||
config.VENDOR_DIR,
|
|
||||||
config.BACKUP_DIR,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range paths {
|
|
||||||
if err := os.Mkdir(path, 0764); err != nil {
|
|
||||||
if !os.IsExist(err) {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cataloguePkg.EnsureCatalogue(); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("abra 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
62
cli/complete.go
Normal file
62
cli/complete.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AutocompleteCommand = &cobra.Command{
|
||||||
|
Use: "autocomplete [bash|zsh|fish|powershell]",
|
||||||
|
Short: "Generate autocompletion script",
|
||||||
|
Long: `To load completions:
|
||||||
|
|
||||||
|
Bash:
|
||||||
|
# Load autocompletion for the current Bash session
|
||||||
|
$ source <(abra autocomplete bash)
|
||||||
|
|
||||||
|
# To load autocompletion for each session, execute once:
|
||||||
|
# Linux:
|
||||||
|
$ abra autocomplete bash | sudo tee /etc/bash_completion.d/abra
|
||||||
|
# macOS:
|
||||||
|
$ abra autocomplete bash | sudo tee $(brew --prefix)/etc/bash_completion.d/abra
|
||||||
|
|
||||||
|
Zsh:
|
||||||
|
# If shell autocompletion is not already enabled in your environment,
|
||||||
|
# you will need to enable it. You can execute the following once:
|
||||||
|
|
||||||
|
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||||
|
|
||||||
|
# To load autocompletions for each session, execute once:
|
||||||
|
$ abra autocomplete zsh > "${fpath[1]}/_abra"
|
||||||
|
|
||||||
|
# You will need to start a new shell for this setup to take effect.
|
||||||
|
|
||||||
|
fish:
|
||||||
|
$ abra autocomplete fish | source
|
||||||
|
|
||||||
|
# To load autocompletions for each session, execute once:
|
||||||
|
$ abra autocomplete fish > ~/.config/fish/completions/abra.fish
|
||||||
|
|
||||||
|
PowerShell:
|
||||||
|
PS> abra autocomplete powershell | Out-String | Invoke-Expression
|
||||||
|
|
||||||
|
# To load autocompletions for every new session, run:
|
||||||
|
PS> abra autocomplete powershell > abra.ps1
|
||||||
|
# and source this file from your PowerShell profile.`,
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
||||||
|
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
switch args[0] {
|
||||||
|
case "bash":
|
||||||
|
cmd.Root().GenBashCompletion(os.Stdout)
|
||||||
|
case "zsh":
|
||||||
|
cmd.Root().GenZshCompletion(os.Stdout)
|
||||||
|
case "fish":
|
||||||
|
cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||||
|
case "powershell":
|
||||||
|
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -2,16 +2,19 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
containerPkg "coopcloud.tech/abra/pkg/container"
|
containerPkg "coopcloud.tech/abra/pkg/container"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/service"
|
"coopcloud.tech/abra/pkg/service"
|
||||||
"coopcloud.tech/abra/pkg/upstream/container"
|
"coopcloud.tech/abra/pkg/upstream/container"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
containertypes "github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RetrieveBackupBotContainer gets the deployed backupbot container.
|
// RetrieveBackupBotContainer gets the deployed backupbot container.
|
||||||
@ -19,10 +22,10 @@ func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
chosenService, err := service.GetServiceByLabel(ctx, cl, config.BackupbotLabel, NoInput)
|
chosenService, err := service.GetServiceByLabel(ctx, cl, config.BackupbotLabel, NoInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.Container{}, err
|
return types.Container{}, fmt.Errorf("no backupbot discovered, is it deployed?")
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("retrieved %s as backup enabled service", chosenService.Spec.Name)
|
log.Debugf("retrieved %s as backup enabled service", chosenService.Spec.Name)
|
||||||
|
|
||||||
filters := filters.NewArgs()
|
filters := filters.NewArgs()
|
||||||
filters.Add("name", chosenService.Spec.Name)
|
filters.Add("name", chosenService.Spec.Name)
|
||||||
@ -40,8 +43,12 @@ func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RunBackupCmdRemote runs a backup related command on a remote backupbot container.
|
// RunBackupCmdRemote runs a backup related command on a remote backupbot container.
|
||||||
func RunBackupCmdRemote(cl *dockerClient.Client, backupCmd string, containerID string, execEnv []string) error {
|
func RunBackupCmdRemote(
|
||||||
execBackupListOpts := types.ExecConfig{
|
cl *dockerClient.Client,
|
||||||
|
backupCmd string,
|
||||||
|
containerID string,
|
||||||
|
execEnv []string) (io.Writer, error) {
|
||||||
|
execBackupListOpts := containertypes.ExecOptions{
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
AttachStdin: true,
|
AttachStdin: true,
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
@ -51,17 +58,18 @@ func RunBackupCmdRemote(cl *dockerClient.Client, backupCmd string, containerID s
|
|||||||
Tty: true,
|
Tty: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts)
|
log.Debugf("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts)
|
||||||
|
|
||||||
// FIXME: avoid instantiating a new CLI
|
// FIXME: avoid instantiating a new CLI
|
||||||
dcli, err := command.NewDockerCli()
|
dcli, err := command.NewDockerCli()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := container.RunExec(dcli, cl, containerID, &execBackupListOpts); err != nil {
|
out, err := container.RunExec(dcli, cl, containerID, &execBackupListOpts)
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,251 +1,20 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
var (
|
||||||
"os"
|
// NOTE(d1): global
|
||||||
|
Debug bool
|
||||||
|
NoInput bool
|
||||||
|
Offline bool
|
||||||
|
IgnoreEnvVersion bool
|
||||||
|
|
||||||
logrusStack "github.com/Gurpartap/logrus-stack"
|
// NOTE(d1): sub-command specific
|
||||||
"github.com/sirupsen/logrus"
|
Chaos bool
|
||||||
"github.com/urfave/cli"
|
DontWaitConverge bool
|
||||||
|
Dry bool
|
||||||
|
Force bool
|
||||||
|
MachineReadable bool
|
||||||
|
Major bool
|
||||||
|
Minor bool
|
||||||
|
NoDomainChecks bool
|
||||||
|
Patch bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// Secrets stores the variable from SecretsFlag
|
|
||||||
var Secrets bool
|
|
||||||
|
|
||||||
// SecretsFlag turns on/off automatically generating secrets
|
|
||||||
var SecretsFlag = &cli.BoolFlag{
|
|
||||||
Name: "secrets, S",
|
|
||||||
Usage: "Automatically generate secrets",
|
|
||||||
Destination: &Secrets,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass stores the variable from PassFlag
|
|
||||||
var Pass bool
|
|
||||||
|
|
||||||
// PassFlag turns on/off storing generated secrets in pass
|
|
||||||
var PassFlag = &cli.BoolFlag{
|
|
||||||
Name: "pass, p",
|
|
||||||
Usage: "Store the generated secrets in a local pass store",
|
|
||||||
Destination: &Pass,
|
|
||||||
}
|
|
||||||
|
|
||||||
// PassRemove stores the variable for PassRemoveFlag
|
|
||||||
var PassRemove bool
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
var Force bool
|
|
||||||
|
|
||||||
// ForceFlag turns on/off force functionality.
|
|
||||||
var ForceFlag = &cli.BoolFlag{
|
|
||||||
Name: "force, f",
|
|
||||||
Usage: "Perform action without further prompt. Use with care!",
|
|
||||||
Destination: &Force,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chaos engages chaos mode.
|
|
||||||
var Chaos bool
|
|
||||||
|
|
||||||
// ChaosFlag turns on/off chaos functionality.
|
|
||||||
var ChaosFlag = &cli.BoolFlag{
|
|
||||||
Name: "chaos, C",
|
|
||||||
Usage: "Proceed with uncommitted recipes changes. Use with care!",
|
|
||||||
Destination: &Chaos,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable tty to run commands from script
|
|
||||||
var Tty bool
|
|
||||||
|
|
||||||
// TtyFlag turns on/off tty mode.
|
|
||||||
var TtyFlag = &cli.BoolFlag{
|
|
||||||
Name: "tty, T",
|
|
||||||
Usage: "Disables TTY mode to run this command from a script.",
|
|
||||||
Destination: &Tty,
|
|
||||||
}
|
|
||||||
|
|
||||||
var NoInput bool
|
|
||||||
var NoInputFlag = &cli.BoolFlag{
|
|
||||||
Name: "no-input, n",
|
|
||||||
Usage: "Toggle non-interactive mode",
|
|
||||||
Destination: &NoInput,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug stores the variable from DebugFlag.
|
|
||||||
var Debug bool
|
|
||||||
|
|
||||||
// DebugFlag turns on/off verbose logging down to the DEBUG level.
|
|
||||||
var DebugFlag = &cli.BoolFlag{
|
|
||||||
Name: "debug, d",
|
|
||||||
Destination: &Debug,
|
|
||||||
Usage: "Show DEBUG messages",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offline stores the variable from OfflineFlag.
|
|
||||||
var Offline bool
|
|
||||||
|
|
||||||
// DebugFlag turns on/off offline mode.
|
|
||||||
var OfflineFlag = &cli.BoolFlag{
|
|
||||||
Name: "offline, o",
|
|
||||||
Destination: &Offline,
|
|
||||||
Usage: "Prefer offline & filesystem access when possible",
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, r",
|
|
||||||
Destination: &RC,
|
|
||||||
Usage: "Install the latest release candidate",
|
|
||||||
}
|
|
||||||
|
|
||||||
var Major bool
|
|
||||||
var MajorFlag = &cli.BoolFlag{
|
|
||||||
Name: "major, x",
|
|
||||||
Usage: "Increase the major part of the version",
|
|
||||||
Destination: &Major,
|
|
||||||
}
|
|
||||||
|
|
||||||
var Minor bool
|
|
||||||
var MinorFlag = &cli.BoolFlag{
|
|
||||||
Name: "minor, y",
|
|
||||||
Usage: "Increase the minor part of the version",
|
|
||||||
Destination: &Minor,
|
|
||||||
}
|
|
||||||
|
|
||||||
var Patch bool
|
|
||||||
var PatchFlag = &cli.BoolFlag{
|
|
||||||
Name: "patch, z",
|
|
||||||
Usage: "Increase the patch part of the version",
|
|
||||||
Destination: &Patch,
|
|
||||||
}
|
|
||||||
|
|
||||||
var Dry bool
|
|
||||||
var DryFlag = &cli.BoolFlag{
|
|
||||||
Name: "dry-run, r",
|
|
||||||
Usage: "Only reports changes that would be made",
|
|
||||||
Destination: &Dry,
|
|
||||||
}
|
|
||||||
|
|
||||||
var Publish bool
|
|
||||||
var PublishFlag = &cli.BoolFlag{
|
|
||||||
Name: "publish, p",
|
|
||||||
Usage: "Publish changes to git.coopcloud.tech",
|
|
||||||
Destination: &Publish,
|
|
||||||
}
|
|
||||||
|
|
||||||
var Domain string
|
|
||||||
var DomainFlag = &cli.StringFlag{
|
|
||||||
Name: "domain, D",
|
|
||||||
Value: "",
|
|
||||||
Usage: "Choose a domain name",
|
|
||||||
Destination: &Domain,
|
|
||||||
}
|
|
||||||
|
|
||||||
var NewAppServer string
|
|
||||||
var NewAppServerFlag = &cli.StringFlag{
|
|
||||||
Name: "server, s",
|
|
||||||
Value: "",
|
|
||||||
Usage: "Show apps of a specific server",
|
|
||||||
Destination: &NewAppServer,
|
|
||||||
}
|
|
||||||
|
|
||||||
var NoDomainChecks bool
|
|
||||||
var NoDomainChecksFlag = &cli.BoolFlag{
|
|
||||||
Name: "no-domain-checks, D",
|
|
||||||
Usage: "Disable app domain sanity checks",
|
|
||||||
Destination: &NoDomainChecks,
|
|
||||||
}
|
|
||||||
|
|
||||||
var StdErrOnly bool
|
|
||||||
var StdErrOnlyFlag = &cli.BoolFlag{
|
|
||||||
Name: "stderr, s",
|
|
||||||
Usage: "Only tail stderr",
|
|
||||||
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, c",
|
|
||||||
Usage: "Don't wait for converge logic checks",
|
|
||||||
Destination: &DontWaitConverge,
|
|
||||||
}
|
|
||||||
|
|
||||||
var Watch bool
|
|
||||||
var WatchFlag = &cli.BoolFlag{
|
|
||||||
Name: "watch, w",
|
|
||||||
Usage: "Watch status by polling repeatedly",
|
|
||||||
Destination: &Watch,
|
|
||||||
}
|
|
||||||
|
|
||||||
var OnlyErrors bool
|
|
||||||
var OnlyErrorFlag = &cli.BoolFlag{
|
|
||||||
Name: "errors, e",
|
|
||||||
Usage: "Only show errors",
|
|
||||||
Destination: &OnlyErrors,
|
|
||||||
}
|
|
||||||
|
|
||||||
var SkipUpdates bool
|
|
||||||
var SkipUpdatesFlag = &cli.BoolFlag{
|
|
||||||
Name: "skip-updates, s",
|
|
||||||
Usage: "Skip updating recipe repositories",
|
|
||||||
Destination: &SkipUpdates,
|
|
||||||
}
|
|
||||||
|
|
||||||
var AllTags bool
|
|
||||||
var AllTagsFlag = &cli.BoolFlag{
|
|
||||||
Name: "all-tags, a",
|
|
||||||
Usage: "List all tags, not just upgrades",
|
|
||||||
Destination: &AllTags,
|
|
||||||
}
|
|
||||||
|
|
||||||
var LocalCmd bool
|
|
||||||
var LocalCmdFlag = &cli.BoolFlag{
|
|
||||||
Name: "local, l",
|
|
||||||
Usage: "Run command locally",
|
|
||||||
Destination: &LocalCmd,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
if Debug {
|
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
|
||||||
logrus.SetFormatter(&logrus.TextFormatter{})
|
|
||||||
logrus.SetOutput(os.Stderr)
|
|
||||||
logrus.AddHook(logrusStack.StandardHook())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -8,20 +8,24 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/config"
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
containerPkg "coopcloud.tech/abra/pkg/container"
|
containerPkg "coopcloud.tech/abra/pkg/container"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/upstream/container"
|
"coopcloud.tech/abra/pkg/upstream/container"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/docker/api/types"
|
containertypes "github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/pkg/archive"
|
"github.com/docker/docker/pkg/archive"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunCmdRemote executes an abra.sh command in the target service
|
// RunCmdRemote executes an abra.sh command in the target service
|
||||||
func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName, cmdName, cmdArgs string) error {
|
func RunCmdRemote(
|
||||||
|
cl *dockerClient.Client,
|
||||||
|
app appPkg.App,
|
||||||
|
disableTTY bool,
|
||||||
|
abraSh, serviceName, cmdName, cmdArgs, remoteUser string) error {
|
||||||
filters := filters.NewArgs()
|
filters := filters.NewArgs()
|
||||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
|
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
|
||||||
|
|
||||||
@ -30,7 +34,7 @@ func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server)
|
log.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server)
|
||||||
|
|
||||||
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
|
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
|
||||||
content, err := archive.TarWithOptions(abraSh, toTarOpts)
|
content, err := archive.TarWithOptions(abraSh, toTarOpts)
|
||||||
@ -38,7 +42,7 @@ func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
copyOpts := containertypes.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||||
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil {
|
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -51,7 +55,7 @@ func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName,
|
|||||||
|
|
||||||
shell := "/bin/bash"
|
shell := "/bin/bash"
|
||||||
findShell := []string{"test", "-e", shell}
|
findShell := []string{"test", "-e", shell}
|
||||||
execCreateOpts := types.ExecConfig{
|
execCreateOpts := containertypes.ExecOptions{
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
AttachStdin: true,
|
AttachStdin: true,
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
@ -61,7 +65,7 @@ func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
|
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)
|
log.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name)
|
||||||
shell = "/bin/sh"
|
shell = "/bin/sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,17 +76,19 @@ func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName,
|
|||||||
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)}
|
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, " "))
|
log.Debugf("running command: %s", strings.Join(cmd, " "))
|
||||||
|
|
||||||
if RemoteUser != "" {
|
if remoteUser != "" {
|
||||||
logrus.Debugf("running command with user %s", RemoteUser)
|
log.Debugf("running command with user %s", remoteUser)
|
||||||
execCreateOpts.User = RemoteUser
|
execCreateOpts.User = remoteUser
|
||||||
}
|
}
|
||||||
|
|
||||||
execCreateOpts.Cmd = cmd
|
execCreateOpts.Cmd = cmd
|
||||||
|
|
||||||
execCreateOpts.Tty = true
|
execCreateOpts.Tty = true
|
||||||
if Tty {
|
if disableTTY {
|
||||||
execCreateOpts.Tty = false
|
execCreateOpts.Tty = false
|
||||||
|
log.Debugf("not requesting a remote TTY")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
|
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
|
||||||
|
|||||||
@ -1,27 +1,58 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
"coopcloud.tech/tagcmp"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewVersionOverview shows an upgrade or downgrade overview
|
var borderStyle = lipgloss.NewStyle().
|
||||||
func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
|
BorderStyle(lipgloss.ThickBorder()).
|
||||||
tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
|
Padding(0, 1, 0, 1).
|
||||||
table := formatter.CreateTable(tableCol)
|
MaxWidth(79).
|
||||||
|
BorderForeground(lipgloss.Color("63"))
|
||||||
|
|
||||||
|
var headerStyle = lipgloss.NewStyle().
|
||||||
|
Underline(true).
|
||||||
|
Bold(true).
|
||||||
|
PaddingBottom(1)
|
||||||
|
|
||||||
|
var leftStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
var rightStyle = lipgloss.NewStyle()
|
||||||
|
|
||||||
|
// horizontal is a JoinHorizontal helper function.
|
||||||
|
func horizontal(left, mid, right string) string {
|
||||||
|
return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatComposeFiles(composeFiles string) string {
|
||||||
|
return strings.ReplaceAll(composeFiles, ":", "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeployOverview shows a deployment overview
|
||||||
|
func DeployOverview(
|
||||||
|
app appPkg.App,
|
||||||
|
deployedVersion string,
|
||||||
|
toDeployVersion string,
|
||||||
|
releaseNotes string,
|
||||||
|
warnMessages []string,
|
||||||
|
) error {
|
||||||
deployConfig := "compose.yml"
|
deployConfig := "compose.yml"
|
||||||
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
||||||
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
|
deployConfig = formatComposeFiles(composeFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
server := app.Server
|
server := app.Server
|
||||||
@ -29,14 +60,38 @@ func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes
|
|||||||
server = "local"
|
server = "local"
|
||||||
}
|
}
|
||||||
|
|
||||||
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
|
domain := app.Domain
|
||||||
table.Render()
|
if domain == "" {
|
||||||
|
domain = config.NO_DOMAIN_DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
if releaseNotes != "" && newVersion != "" {
|
envVersion := app.Recipe.EnvVersionRaw
|
||||||
fmt.Println()
|
if envVersion == "" {
|
||||||
|
envVersion = config.NO_VERSION_DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := [][]string{
|
||||||
|
{"DOMAIN", domain},
|
||||||
|
{"RECIPE", app.Recipe.Name},
|
||||||
|
{"SERVER", server},
|
||||||
|
{"CONFIG", deployConfig},
|
||||||
|
{"", ""},
|
||||||
|
{"CURRENT DEPLOYMENT", formatter.BoldDirtyDefault(deployedVersion)},
|
||||||
|
{"ENV VERSION", formatter.BoldDirtyDefault(envVersion)},
|
||||||
|
{"NEW DEPLOYMENT", formatter.BoldDirtyDefault(toDeployVersion)},
|
||||||
|
}
|
||||||
|
|
||||||
|
deployType := getDeployType(deployedVersion, toDeployVersion)
|
||||||
|
overview := formatter.CreateOverview(fmt.Sprintf("%s OVERVIEW", deployType), rows)
|
||||||
|
|
||||||
|
fmt.Println(overview)
|
||||||
|
|
||||||
|
if releaseNotes != "" {
|
||||||
fmt.Print(releaseNotes)
|
fmt.Print(releaseNotes)
|
||||||
} else {
|
}
|
||||||
logrus.Warnf("no release notes available for %s", newVersion)
|
|
||||||
|
for _, msg := range warnMessages {
|
||||||
|
log.Warn(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if NoInput {
|
if NoInput {
|
||||||
@ -44,49 +99,108 @@ func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes
|
|||||||
}
|
}
|
||||||
|
|
||||||
response := false
|
response := false
|
||||||
prompt := &survey.Confirm{
|
prompt := &survey.Confirm{Message: "proceed?"}
|
||||||
Message: "continue with deployment?",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := survey.AskOne(prompt, &response); err != nil {
|
if err := survey.AskOne(prompt, &response); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !response {
|
if !response {
|
||||||
logrus.Fatal("exiting as requested")
|
log.Fatal("deployment cancelled")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetReleaseNotes prints release notes for a recipe version
|
func getDeployType(currentVersion, newVersion string) string {
|
||||||
func GetReleaseNotes(recipeName, version string) (string, error) {
|
if newVersion == config.NO_DOMAIN_DEFAULT {
|
||||||
if version == "" {
|
return "UNDEPLOY"
|
||||||
return "", nil
|
}
|
||||||
|
if strings.Contains(newVersion, "+U") {
|
||||||
|
return "CHAOS DEPLOY"
|
||||||
|
}
|
||||||
|
if strings.Contains(currentVersion, "+U") {
|
||||||
|
return "UNCHAOS DEPLOY"
|
||||||
|
}
|
||||||
|
if currentVersion == newVersion {
|
||||||
|
return "REDEPLOY"
|
||||||
|
}
|
||||||
|
if currentVersion == config.NO_VERSION_DEFAULT {
|
||||||
|
return "NEW DEPLOY"
|
||||||
|
}
|
||||||
|
currentParsed, err := tagcmp.Parse(currentVersion)
|
||||||
|
if err != nil {
|
||||||
|
return "DEPLOY"
|
||||||
|
}
|
||||||
|
newParsed, err := tagcmp.Parse(newVersion)
|
||||||
|
if err != nil {
|
||||||
|
return "DEPLOY"
|
||||||
|
}
|
||||||
|
if currentParsed.IsLessThan(newParsed) {
|
||||||
|
return "UPGRADE"
|
||||||
|
}
|
||||||
|
return "DOWNGRADE"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveOverview shows a overview before moving an app to a different server
|
||||||
|
func MoveOverview(
|
||||||
|
app appPkg.App,
|
||||||
|
newServer string,
|
||||||
|
secrets []string,
|
||||||
|
volumes []string,
|
||||||
|
) {
|
||||||
|
server := app.Server
|
||||||
|
if app.Server == "default" {
|
||||||
|
server = "local"
|
||||||
}
|
}
|
||||||
|
|
||||||
fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version)
|
domain := app.Domain
|
||||||
|
if domain == "" {
|
||||||
if _, err := os.Stat(fpath); !os.IsNotExist(err) {
|
domain = config.NO_DOMAIN_DEFAULT
|
||||||
releaseNotes, err := ioutil.ReadFile(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
withTitle := fmt.Sprintf("%s release notes:\n%s", version, string(releaseNotes))
|
|
||||||
return withTitle, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", nil
|
rows := [][]string{
|
||||||
|
{"DOMAIN", domain},
|
||||||
|
{"RECIPE", app.Recipe.Name},
|
||||||
|
{"OLD SERVER", server},
|
||||||
|
{"New SERVER", newServer},
|
||||||
|
{"SECRETS", strings.Join(secrets, "\n")},
|
||||||
|
{"VOLUMES", strings.Join(volumes, "\n")},
|
||||||
|
}
|
||||||
|
|
||||||
|
overview := formatter.CreateOverview("MOVE OVERVIEW", rows)
|
||||||
|
|
||||||
|
fmt.Println(overview)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PromptProcced() error {
|
||||||
|
if NoInput {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if Dry {
|
||||||
|
return fmt.Errorf("dry run")
|
||||||
|
}
|
||||||
|
|
||||||
|
response := false
|
||||||
|
prompt := &survey.Confirm{Message: "proceed?"}
|
||||||
|
if err := survey.AskOne(prompt, &response); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response {
|
||||||
|
return errors.New("cancelled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostCmds parses a string of commands and executes them inside of the respective services
|
// PostCmds parses a string of commands and executes them inside of the respective services
|
||||||
// the commands string must have the following format:
|
// the commands string must have the following format:
|
||||||
// "<service> <command> <arguments>|<service> <command> <arguments>|... "
|
// "<service> <command> <arguments>|<service> <command> <arguments>|... "
|
||||||
func PostCmds(cl *dockerClient.Client, app config.App, commands string) error {
|
func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
|
||||||
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
|
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
|
||||||
if _, err := os.Stat(abraSh); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name))
|
return fmt.Errorf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -94,7 +208,7 @@ func PostCmds(cl *dockerClient.Client, app config.App, commands string) error {
|
|||||||
for _, command := range strings.Split(commands, "|") {
|
for _, command := range strings.Split(commands, "|") {
|
||||||
commandParts := strings.Split(command, " ")
|
commandParts := strings.Split(command, " ")
|
||||||
if len(commandParts) < 2 {
|
if len(commandParts) < 2 {
|
||||||
return fmt.Errorf(fmt.Sprintf("not enough arguments: %s", command))
|
return fmt.Errorf("not enough arguments: %s", command)
|
||||||
}
|
}
|
||||||
targetServiceName := commandParts[0]
|
targetServiceName := commandParts[0]
|
||||||
cmdName := commandParts[1]
|
cmdName := commandParts[1]
|
||||||
@ -102,13 +216,13 @@ func PostCmds(cl *dockerClient.Client, app config.App, commands string) error {
|
|||||||
if len(commandParts) > 2 {
|
if len(commandParts) > 2 {
|
||||||
parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " "))
|
parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " "))
|
||||||
}
|
}
|
||||||
logrus.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName)
|
log.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName)
|
||||||
|
|
||||||
if err := EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
|
if err := EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceNames, err := config.GetAppServiceNames(app.Name)
|
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -121,53 +235,38 @@ func PostCmds(cl *dockerClient.Client, app config.App, commands string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !matchingServiceName {
|
if !matchingServiceName {
|
||||||
return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name))
|
return fmt.Errorf("no service %s for %s?", targetServiceName, app.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
|
log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
|
||||||
|
|
||||||
Tty = true
|
requestTTY := true
|
||||||
if err := RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
|
if err := RunCmdRemote(
|
||||||
|
cl,
|
||||||
|
app,
|
||||||
|
requestTTY,
|
||||||
|
app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeployOverview shows a deployment overview
|
// SortVersionsDesc sorts versions in descending order.
|
||||||
func DeployOverview(app config.App, version, message string) error {
|
func SortVersionsDesc(versions []string) []string {
|
||||||
tableCol := []string{"server", "recipe", "config", "domain", "version"}
|
var tags []tagcmp.Tag
|
||||||
table := formatter.CreateTable(tableCol)
|
|
||||||
|
|
||||||
deployConfig := "compose.yml"
|
for _, v := range versions {
|
||||||
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
parsed, _ := tagcmp.Parse(v) // skips unsupported tags
|
||||||
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
|
tags = append(tags, parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
server := app.Server
|
sort.Sort(tagcmp.ByTagDesc(tags))
|
||||||
if app.Server == "default" {
|
|
||||||
server = "local"
|
var desc []string
|
||||||
|
for _, t := range tags {
|
||||||
|
desc = append(desc, t.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
|
return desc
|
||||||
table.Render()
|
|
||||||
|
|
||||||
if NoInput {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
response := false
|
|
||||||
prompt := &survey.Confirm{
|
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := survey.AskOne(prompt, &response); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !response {
|
|
||||||
logrus.Fatal("exiting as requested")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
17
cli/internal/deploy_test.go
Normal file
17
cli/internal/deploy_test.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSortVersionsDesc(t *testing.T) {
|
||||||
|
versions := SortVersionsDesc([]string{
|
||||||
|
"0.2.3+1.2.2",
|
||||||
|
"1.0.0+2.2.2",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "1.0.0+2.2.2", versions[0])
|
||||||
|
assert.Equal(t, "0.2.3+1.2.2", versions[1])
|
||||||
|
}
|
||||||
11
cli/internal/ensure.go
Normal file
11
cli/internal/ensure.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import "coopcloud.tech/abra/pkg/recipe"
|
||||||
|
|
||||||
|
func GetEnsureContext() recipe.EnsureContext {
|
||||||
|
return recipe.EnsureContext{
|
||||||
|
Chaos,
|
||||||
|
Offline,
|
||||||
|
IgnoreEnvVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ShowSubcommandHelpAndError exits the program on error, logs the error to the
|
|
||||||
// terminal, and shows the help command.
|
|
||||||
func ShowSubcommandHelpAndError(c *cli.Context, err interface{}) {
|
|
||||||
if err2 := cli.ShowSubcommandHelp(c); err2 != nil {
|
|
||||||
logrus.Error(err2)
|
|
||||||
}
|
|
||||||
logrus.Error(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
// ReverseStringList reverses a list of a strings. Roll on Go generics.
|
|
||||||
func ReverseStringList(strings []string) []string {
|
|
||||||
for i, j := 0, len(strings)-1; i < j; i, j = i+1, j-1 {
|
|
||||||
strings[i], strings[j] = strings[j], strings[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings
|
|
||||||
}
|
|
||||||
@ -4,10 +4,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PromptBumpType prompts for version bump type
|
// PromptBumpType prompts for version bump type
|
||||||
@ -65,7 +65,7 @@ func GetBumpType() string {
|
|||||||
} else if Patch {
|
} else if Patch {
|
||||||
bumpType = "patch"
|
bumpType = "patch"
|
||||||
} else {
|
} else {
|
||||||
logrus.Fatal("no version bump type specififed?")
|
log.Fatal("no version bump type specififed?")
|
||||||
}
|
}
|
||||||
|
|
||||||
return bumpType
|
return bumpType
|
||||||
@ -80,7 +80,7 @@ func SetBumpType(bumpType string) {
|
|||||||
} else if bumpType == "patch" {
|
} else if bumpType == "patch" {
|
||||||
Patch = true
|
Patch = true
|
||||||
} else {
|
} else {
|
||||||
logrus.Fatal("no version bump type specififed?")
|
log.Fatal("no version bump type specififed?")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +88,11 @@ func SetBumpType(bumpType string) {
|
|||||||
func GetMainAppImage(recipe recipe.Recipe) (string, error) {
|
func GetMainAppImage(recipe recipe.Recipe) (string, error) {
|
||||||
var path string
|
var path string
|
||||||
|
|
||||||
for _, service := range recipe.Config.Services {
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, service := range config.Services {
|
||||||
if service.Name == "app" {
|
if service.Name == "app" {
|
||||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -1,103 +1,118 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/app"
|
"coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidateRecipe ensures the recipe arg is valid.
|
// ValidateRecipe ensures the recipe arg is valid.
|
||||||
func ValidateRecipe(c *cli.Context) recipe.Recipe {
|
func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
|
||||||
recipeName := c.Args().First()
|
var recipeName string
|
||||||
|
if len(args) > 0 {
|
||||||
|
recipeName = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
if recipeName == "" && !NoInput {
|
var recipes []string
|
||||||
var recipes []string
|
|
||||||
|
|
||||||
catl, err := recipe.ReadRecipeCatalogue(Offline)
|
catl, err := recipe.ReadRecipeCatalogue(Offline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
knownRecipes := make(map[string]bool)
|
knownRecipes := make(map[string]bool)
|
||||||
for name := range catl {
|
for name := range catl {
|
||||||
knownRecipes[name] = true
|
knownRecipes[name] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
localRecipes, err := recipe.GetRecipesLocal()
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
localRecipes, err := recipe.GetRecipesLocal()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("can't read local recipes: %s", err)
|
||||||
|
} else {
|
||||||
for _, recipeLocal := range localRecipes {
|
for _, recipeLocal := range localRecipes {
|
||||||
if _, ok := knownRecipes[recipeLocal]; !ok {
|
if _, ok := knownRecipes[recipeLocal]; !ok {
|
||||||
knownRecipes[recipeLocal] = true
|
knownRecipes[recipeLocal] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for recipeName := range knownRecipes {
|
for recipeName := range knownRecipes {
|
||||||
recipes = append(recipes, recipeName)
|
recipes = append(recipes, recipeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if recipeName == "" && !NoInput {
|
||||||
prompt := &survey.Select{
|
prompt := &survey.Select{
|
||||||
Message: "Select recipe",
|
Message: "Select recipe",
|
||||||
Options: recipes,
|
Options: recipes,
|
||||||
}
|
}
|
||||||
if err := survey.AskOne(prompt, &recipeName); err != nil {
|
if err := survey.AskOne(prompt, &recipeName); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if recipeName == "" {
|
if recipeName == "" {
|
||||||
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
|
log.Fatal("no recipe name provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
chosenRecipe, err := recipe.Get(recipeName, Offline)
|
if _, ok := knownRecipes[recipeName]; !ok {
|
||||||
if err != nil {
|
if !strings.Contains(recipeName, "/") {
|
||||||
if c.Command.Name == "generate" {
|
log.Fatalf("no recipe '%s' exists?", recipeName)
|
||||||
if strings.Contains(err.Error(), "missing a compose") {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("validated %s as recipe argument", recipeName)
|
chosenRecipe := recipe.Get(recipeName)
|
||||||
|
if err := chosenRecipe.EnsureExists(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = chosenRecipe.GetComposeConfig(nil)
|
||||||
|
if err != nil {
|
||||||
|
if cmdName == "generate" {
|
||||||
|
if strings.Contains(err.Error(), "missing a compose") {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Warn(err)
|
||||||
|
} else {
|
||||||
|
if strings.Contains(err.Error(), "template_driver is not allowed") {
|
||||||
|
log.Warnf("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName)
|
||||||
|
}
|
||||||
|
log.Fatalf("unable to validate recipe: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("validated %s as recipe argument", recipeName)
|
||||||
|
|
||||||
return chosenRecipe
|
return chosenRecipe
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateApp ensures the app name arg is valid.
|
// ValidateApp ensures the app name arg is valid.
|
||||||
func ValidateApp(c *cli.Context) config.App {
|
func ValidateApp(args []string) app.App {
|
||||||
appName := c.Args().First()
|
if len(args) == 0 {
|
||||||
|
log.Fatal("no app provided")
|
||||||
if appName == "" {
|
|
||||||
ShowSubcommandHelpAndError(c, errors.New("no app provided"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appName := args[0]
|
||||||
|
|
||||||
app, err := app.Get(appName)
|
app, err := app.Get(appName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("validated %s as app argument", appName)
|
log.Debugf("validated %s as app argument", appName)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateDomain ensures the domain name arg is valid.
|
// ValidateDomain ensures the domain name arg is valid.
|
||||||
func ValidateDomain(c *cli.Context) string {
|
func ValidateDomain(args []string) string {
|
||||||
domainName := c.Args().First()
|
var domainName string
|
||||||
|
if len(args) > 0 {
|
||||||
|
domainName = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
if domainName == "" && !NoInput {
|
if domainName == "" && !NoInput {
|
||||||
prompt := &survey.Input{
|
prompt := &survey.Input{
|
||||||
@ -105,40 +120,29 @@ func ValidateDomain(c *cli.Context) string {
|
|||||||
Default: "example.com",
|
Default: "example.com",
|
||||||
}
|
}
|
||||||
if err := survey.AskOne(prompt, &domainName); err != nil {
|
if err := survey.AskOne(prompt, &domainName); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if domainName == "" {
|
if domainName == "" {
|
||||||
ShowSubcommandHelpAndError(c, errors.New("no domain provided"))
|
log.Fatal("no domain provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("validated %s as domain argument", domainName)
|
log.Debugf("validated %s as domain argument", domainName)
|
||||||
|
|
||||||
return domainName
|
return domainName
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateSubCmdFlags ensures flag order conforms to correct order
|
|
||||||
func ValidateSubCmdFlags(c *cli.Context) bool {
|
|
||||||
for argIdx, arg := range c.Args() {
|
|
||||||
if !strings.HasPrefix(arg, "--") {
|
|
||||||
for _, flag := range c.Args()[argIdx:] {
|
|
||||||
if strings.HasPrefix(flag, "--") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateServer ensures the server name arg is valid.
|
// ValidateServer ensures the server name arg is valid.
|
||||||
func ValidateServer(c *cli.Context) string {
|
func ValidateServer(args []string) string {
|
||||||
serverName := c.Args().First()
|
var serverName string
|
||||||
|
if len(args) > 0 {
|
||||||
|
serverName = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
serverNames, err := config.ReadServerNames()
|
serverNames, err := config.ReadServerNames()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if serverName == "" && !NoInput {
|
if serverName == "" && !NoInput {
|
||||||
@ -147,7 +151,7 @@ func ValidateServer(c *cli.Context) string {
|
|||||||
Options: serverNames,
|
Options: serverNames,
|
||||||
}
|
}
|
||||||
if err := survey.AskOne(prompt, &serverName); err != nil {
|
if err := survey.AskOne(prompt, &serverName); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,14 +163,14 @@ func ValidateServer(c *cli.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if serverName == "" {
|
if serverName == "" {
|
||||||
ShowSubcommandHelpAndError(c, errors.New("no server provided"))
|
log.Fatal("no server provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matched {
|
if !matched {
|
||||||
ShowSubcommandHelpAndError(c, errors.New("server doesn't exist?"))
|
log.Fatal("server doesn't exist?")
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("validated %s as server argument", serverName)
|
log.Debugf("validated %s as server argument", serverName)
|
||||||
|
|
||||||
return serverName
|
return serverName
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,40 +1,29 @@
|
|||||||
package recipe
|
package recipe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||||
"github.com/sirupsen/logrus"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/urfave/cli"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var recipeDiffCommand = cli.Command{
|
var RecipeDiffCommand = &cobra.Command{
|
||||||
Name: "diff",
|
Use: "diff <recipe> [flags]",
|
||||||
Usage: "Show unstaged changes in recipe config",
|
Aliases: []string{"d"},
|
||||||
Description: "Due to limitations in our underlying Git dependency, this command requires /usr/bin/git.",
|
Short: "Show unstaged changes in recipe config",
|
||||||
Aliases: []string{"d"},
|
Long: "This command requires /usr/bin/git.",
|
||||||
ArgsUsage: "<recipe>",
|
Args: cobra.MinimumNArgs(1),
|
||||||
Flags: []cli.Flag{
|
ValidArgsFunction: func(
|
||||||
internal.DebugFlag,
|
cmd *cobra.Command,
|
||||||
internal.NoInputFlag,
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.RecipeNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.RecipeNameComplete,
|
r := internal.ValidateRecipe(args, cmd.Name())
|
||||||
Action: func(c *cli.Context) error {
|
if err := gitPkg.DiffUnstaged(r.Dir); err != nil {
|
||||||
recipeName := c.Args().First()
|
log.Fatal(err)
|
||||||
|
|
||||||
if recipeName != "" {
|
|
||||||
internal.ValidateRecipe(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
|
|
||||||
if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,50 +1,134 @@
|
|||||||
package recipe
|
package recipe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/urfave/cli"
|
gitCfg "github.com/go-git/go-git/v5/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var recipeFetchCommand = cli.Command{
|
var RecipeFetchCommand = &cobra.Command{
|
||||||
Name: "fetch",
|
Use: "fetch [recipe | --all] [flags]",
|
||||||
Usage: "Fetch recipe(s)",
|
Aliases: []string{"f"},
|
||||||
Aliases: []string{"f"},
|
Short: "Clone recipe(s) locally",
|
||||||
ArgsUsage: "[<recipe>]",
|
Long: `Using "--force/-f" Git syncs an existing recipe. It does not erase unstaged changes.`,
|
||||||
Description: "Retrieves all recipes if no <recipe> argument is passed",
|
Args: cobra.RangeArgs(0, 1),
|
||||||
Flags: []cli.Flag{
|
Example: ` # fetch from recipe catalogue
|
||||||
internal.DebugFlag,
|
abra recipe fetch gitea
|
||||||
internal.NoInputFlag,
|
|
||||||
internal.OfflineFlag,
|
# fetch from remote recipe
|
||||||
|
abra recipe fetch git.foo.org/recipes/myrecipe
|
||||||
|
|
||||||
|
# fetch with ssh remote for hacking
|
||||||
|
abra recipe fetch gitea --ssh`,
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.RecipeNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.RecipeNameComplete,
|
var recipeName string
|
||||||
Action: func(c *cli.Context) error {
|
if len(args) > 0 {
|
||||||
recipeName := c.Args().First()
|
recipeName = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipeName == "" && !fetchAllRecipes {
|
||||||
|
log.Fatal("missing [recipe] or --all/-a")
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipeName != "" && fetchAllRecipes {
|
||||||
|
log.Fatal("cannot use [recipe] and --all/-a together")
|
||||||
|
}
|
||||||
|
|
||||||
if recipeName != "" {
|
if recipeName != "" {
|
||||||
internal.ValidateRecipe(c)
|
r := recipe.Get(recipeName)
|
||||||
if err := recipe.Ensure(recipeName); err != nil {
|
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
|
||||||
logrus.Fatal(err)
|
if !force {
|
||||||
|
log.Warnf("%s is already fetched", r.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
r = internal.ValidateRecipe(args, cmd.Name())
|
||||||
|
|
||||||
|
if sshRemote {
|
||||||
|
if r.SSHURL == "" {
|
||||||
|
log.Warnf("unable to discover SSH remote for %s", r.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := git.PlainOpen(r.Dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("unable to open %s: %s", r.Dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = repo.DeleteRemote("origin"); err != nil {
|
||||||
|
log.Fatalf("unable to remove default remote in %s: %s", r.Dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := repo.CreateRemote(&gitCfg.RemoteConfig{
|
||||||
|
Name: "origin",
|
||||||
|
URLs: []string{r.SSHURL},
|
||||||
|
}); err != nil {
|
||||||
|
log.Fatalf("unable to set SSH remote in %s: %s", r.Dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...")
|
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...")
|
||||||
|
ensureCtx := internal.GetEnsureContext()
|
||||||
for recipeName := range catalogue {
|
for recipeName := range catalogue {
|
||||||
if err := recipe.Ensure(recipeName); err != nil {
|
r := recipe.Get(recipeName)
|
||||||
logrus.Error(err)
|
if err := r.Ensure(ensureCtx); err != nil {
|
||||||
|
log.Error(err)
|
||||||
}
|
}
|
||||||
catlBar.Add(1)
|
catlBar.Add(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
fetchAllRecipes bool
|
||||||
|
sshRemote bool
|
||||||
|
force bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RecipeFetchCommand.Flags().BoolVarP(
|
||||||
|
&fetchAllRecipes,
|
||||||
|
"all",
|
||||||
|
"a",
|
||||||
|
false,
|
||||||
|
"fetch all recipes",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeFetchCommand.Flags().BoolVarP(
|
||||||
|
&sshRemote,
|
||||||
|
"ssh",
|
||||||
|
"s",
|
||||||
|
false,
|
||||||
|
"automatically set ssh remote",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeFetchCommand.Flags().BoolVarP(
|
||||||
|
&force,
|
||||||
|
"force",
|
||||||
|
"f",
|
||||||
|
false,
|
||||||
|
"force re-fetch",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,63 +1,55 @@
|
|||||||
package recipe
|
package recipe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/lint"
|
"coopcloud.tech/abra/pkg/lint"
|
||||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var recipeLintCommand = cli.Command{
|
var RecipeLintCommand = &cobra.Command{
|
||||||
Name: "lint",
|
Use: "lint <recipe> [flags]",
|
||||||
Usage: "Lint a recipe",
|
Short: "Lint a recipe",
|
||||||
Aliases: []string{"l"},
|
Aliases: []string{"l"},
|
||||||
ArgsUsage: "<recipe>",
|
Args: cobra.MinimumNArgs(1),
|
||||||
Flags: []cli.Flag{
|
ValidArgsFunction: func(
|
||||||
internal.DebugFlag,
|
cmd *cobra.Command,
|
||||||
internal.OnlyErrorFlag,
|
args []string,
|
||||||
internal.OfflineFlag,
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
internal.NoInputFlag,
|
return autocomplete.RecipeNameComplete()
|
||||||
internal.ChaosFlag,
|
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.RecipeNameComplete,
|
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
recipe := internal.ValidateRecipe(c)
|
|
||||||
|
|
||||||
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
|
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Chaos {
|
headers := []string{
|
||||||
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
|
"ref",
|
||||||
logrus.Fatal(err)
|
"rule",
|
||||||
}
|
"severity",
|
||||||
|
"satisfied",
|
||||||
if !internal.Offline {
|
"skipped",
|
||||||
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
|
"resolve",
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
|
table, err := formatter.CreateTable()
|
||||||
table := formatter.CreateTable(tableCol)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Headers(headers...)
|
||||||
|
|
||||||
hasError := false
|
hasError := false
|
||||||
bar := formatter.CreateProgressbar(-1, "running recipe lint rules...")
|
var rows [][]string
|
||||||
|
var warnMessages []string
|
||||||
for level := range lint.LintRules {
|
for level := range lint.LintRules {
|
||||||
for _, rule := range lint.LintRules[level] {
|
for _, rule := range lint.LintRules[level] {
|
||||||
if internal.OnlyErrors && rule.Level != "error" {
|
if onlyError && rule.Level != "error" {
|
||||||
logrus.Debugf("skipping %s, does not have level \"error\"", rule.Ref)
|
log.Debugf("skipping %s, does not have level \"error\"", rule.Ref)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +67,7 @@ var recipeLintCommand = cli.Command{
|
|||||||
if !skipped {
|
if !skipped {
|
||||||
ok, err := rule.Function(recipe)
|
ok, err := rule.Function(recipe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warn(err)
|
warnMessages = append(warnMessages, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok && rule.Level == "error" {
|
if !ok && rule.Level == "error" {
|
||||||
@ -95,28 +87,54 @@ var recipeLintCommand = cli.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table.Append([]string{
|
row := []string{
|
||||||
rule.Ref,
|
rule.Ref,
|
||||||
rule.Description,
|
rule.Description,
|
||||||
rule.Level,
|
rule.Level,
|
||||||
satisfiedOutput,
|
satisfiedOutput,
|
||||||
skippedOutput,
|
skippedOutput,
|
||||||
rule.HowToResolve,
|
rule.HowToResolve,
|
||||||
})
|
}
|
||||||
|
|
||||||
bar.Add(1)
|
rows = append(rows, row)
|
||||||
|
table.Row(row...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if table.NumLines() > 0 {
|
if len(rows) > 0 {
|
||||||
fmt.Println()
|
if err := formatter.PrintTable(table); err != nil {
|
||||||
table.Render()
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasError {
|
for _, warnMsg := range warnMessages {
|
||||||
logrus.Warn("watch out, some critical errors are present in your recipe config")
|
log.Warn(warnMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if hasError {
|
||||||
|
log.Warnf("critical errors present in %s config", recipe.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
onlyError bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RecipeLintCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeLintCommand.Flags().BoolVarP(
|
||||||
|
&onlyError,
|
||||||
|
"error",
|
||||||
|
"e",
|
||||||
|
false,
|
||||||
|
"only show errors",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -8,45 +8,46 @@ import (
|
|||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var pattern string
|
var RecipeListCommand = &cobra.Command{
|
||||||
var patternFlag = &cli.StringFlag{
|
Use: "list",
|
||||||
Name: "pattern, p",
|
Short: "List recipes",
|
||||||
Value: "",
|
|
||||||
Usage: "Simple string to filter recipes",
|
|
||||||
Destination: &pattern,
|
|
||||||
}
|
|
||||||
|
|
||||||
var recipeListCommand = cli.Command{
|
|
||||||
Name: "list",
|
|
||||||
Usage: "List available recipes",
|
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
Flags: []cli.Flag{
|
Args: cobra.NoArgs,
|
||||||
internal.DebugFlag,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
internal.MachineReadableFlag,
|
|
||||||
patternFlag,
|
|
||||||
internal.OfflineFlag,
|
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err.Error())
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
recipes := catl.Flatten()
|
recipes := catl.Flatten()
|
||||||
sort.Sort(recipe.ByRecipeName(recipes))
|
sort.Sort(recipe.ByRecipeName(recipes))
|
||||||
|
|
||||||
tableCol := []string{"name", "category", "status", "healthcheck", "backups", "email", "tests", "SSO"}
|
table, err := formatter.CreateTable()
|
||||||
table := formatter.CreateTable(tableCol)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
len := 0
|
headers := []string{
|
||||||
|
"name",
|
||||||
|
"category",
|
||||||
|
"status",
|
||||||
|
"healthcheck",
|
||||||
|
"backups",
|
||||||
|
"email",
|
||||||
|
"tests",
|
||||||
|
"SSO",
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Headers(headers...)
|
||||||
|
|
||||||
|
var rows [][]string
|
||||||
for _, recipe := range recipes {
|
for _, recipe := range recipes {
|
||||||
tableRow := []string{
|
row := []string{
|
||||||
recipe.Name,
|
recipe.Name,
|
||||||
recipe.Category,
|
recipe.Category,
|
||||||
strconv.Itoa(recipe.Features.Status),
|
strconv.Itoa(recipe.Features.Status),
|
||||||
@ -59,25 +60,50 @@ var recipeListCommand = cli.Command{
|
|||||||
|
|
||||||
if pattern != "" {
|
if pattern != "" {
|
||||||
if strings.Contains(recipe.Name, pattern) {
|
if strings.Contains(recipe.Name, pattern) {
|
||||||
table.Append(tableRow)
|
table.Row(row...)
|
||||||
len++
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
table.Append(tableRow)
|
table.Row(row...)
|
||||||
len++
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if table.NumLines() > 0 {
|
if len(rows) > 0 {
|
||||||
if internal.MachineReadable {
|
if internal.MachineReadable {
|
||||||
table.SetCaption(false, "")
|
out, err := formatter.ToJSON(headers, rows)
|
||||||
table.JSONRender()
|
if err != nil {
|
||||||
} else {
|
log.Fatal("unable to render to JSON: %s", err)
|
||||||
table.SetCaption(true, fmt.Sprintf("total recipes: %v", len))
|
}
|
||||||
table.Render()
|
fmt.Println(out)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := formatter.PrintTable(table); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
pattern string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RecipeListCommand.Flags().BoolVarP(
|
||||||
|
&internal.MachineReadable,
|
||||||
|
"machine",
|
||||||
|
"m",
|
||||||
|
false,
|
||||||
|
"print machine-readable output",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeListCommand.Flags().StringVarP(
|
||||||
|
&pattern,
|
||||||
|
"pattern",
|
||||||
|
"p",
|
||||||
|
"",
|
||||||
|
"filter by recipe",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -2,18 +2,17 @@ package recipe
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
"coopcloud.tech/abra/pkg/git"
|
"coopcloud.tech/abra/pkg/git"
|
||||||
"github.com/sirupsen/logrus"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/urfave/cli"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// recipeMetadata is the recipe metadata for the README.md
|
// recipeMetadata is the recipe metadata for the README.md
|
||||||
@ -30,97 +29,61 @@ type recipeMetadata struct {
|
|||||||
SSO string
|
SSO string
|
||||||
}
|
}
|
||||||
|
|
||||||
var recipeNewCommand = cli.Command{
|
var RecipeNewCommand = &cobra.Command{
|
||||||
Name: "new",
|
Use: "new <recipe> [flags]",
|
||||||
Aliases: []string{"n"},
|
Aliases: []string{"n"},
|
||||||
Flags: []cli.Flag{
|
Short: "Create a new recipe",
|
||||||
internal.DebugFlag,
|
Long: `A community managed recipe template is used.`,
|
||||||
internal.NoInputFlag,
|
Args: cobra.ExactArgs(1),
|
||||||
internal.OfflineFlag,
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.RecipeNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
Usage: "Create a new recipe",
|
recipeName := args[0]
|
||||||
ArgsUsage: "<recipe>",
|
|
||||||
Description: `
|
|
||||||
Create a new recipe.
|
|
||||||
|
|
||||||
Abra uses the built-in example repository which is available here:
|
r := recipe.Get(recipeName)
|
||||||
|
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
|
||||||
https://git.coopcloud.tech/coop-cloud/example
|
log.Fatalf("%s recipe directory already exists?", r.Dir)
|
||||||
|
|
||||||
Files within the example repository make use of the Golang templating system
|
|
||||||
which Abra uses to inject values into the generated recipe folder (e.g. name of
|
|
||||||
recipe and domain in the sample environment config).
|
|
||||||
`,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
recipeName := c.Args().First()
|
|
||||||
|
|
||||||
if recipeName == "" {
|
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
|
|
||||||
}
|
|
||||||
|
|
||||||
directory := path.Join(config.RECIPES_DIR, recipeName)
|
|
||||||
if _, err := os.Stat(directory); !os.IsNotExist(err) {
|
|
||||||
logrus.Fatalf("%s recipe directory already exists?", directory)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
|
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
|
||||||
if err := git.Clone(directory, url); err != nil {
|
if err := git.Clone(r.Dir, url); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gitRepo := path.Join(config.RECIPES_DIR, recipeName, ".git")
|
gitRepo := path.Join(r.Dir, ".git")
|
||||||
if err := os.RemoveAll(gitRepo); err != nil {
|
if err := os.RemoveAll(gitRepo); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
logrus.Debugf("removed example git repo in %s", gitRepo)
|
log.Debugf("removed .git repo in %s", gitRepo)
|
||||||
|
|
||||||
meta := newRecipeMeta(recipeName)
|
meta := newRecipeMeta(recipeName)
|
||||||
|
|
||||||
toParse := []string{
|
for _, path := range []string{r.ReadmePath, r.SampleEnvPath} {
|
||||||
path.Join(config.RECIPES_DIR, recipeName, "README.md"),
|
|
||||||
path.Join(config.RECIPES_DIR, recipeName, ".env.sample"),
|
|
||||||
}
|
|
||||||
for _, path := range toParse {
|
|
||||||
tpl, err := template.ParseFiles(path)
|
tpl, err := template.ParseFiles(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var templated bytes.Buffer
|
var templated bytes.Buffer
|
||||||
if err := tpl.Execute(&templated, meta); err != nil {
|
if err := tpl.Execute(&templated, meta); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(path, templated.Bytes(), 0644); err != nil {
|
if err := os.WriteFile(path, templated.Bytes(), 0o644); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newGitRepo := path.Join(config.RECIPES_DIR, recipeName)
|
if err := git.Init(r.Dir, true, gitName, gitEmail); err != nil {
|
||||||
if err := git.Init(newGitRepo, true); err != nil {
|
log.Fatal(err)
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print(fmt.Sprintf(`
|
log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir))
|
||||||
Your new %s recipe has been created in %s.
|
log.Info("happy hacking 🎉")
|
||||||
|
|
||||||
In order to share your recipe, you can upload it the git repository to:
|
|
||||||
|
|
||||||
https://git.coopcloud.tech/coop-cloud/%s
|
|
||||||
|
|
||||||
If you're not sure how to do that, come chat with us:
|
|
||||||
|
|
||||||
https://docs.coopcloud.tech/intro/contact
|
|
||||||
|
|
||||||
See "abra recipe -h" for additional recipe maintainer commands.
|
|
||||||
|
|
||||||
Happy Hacking!
|
|
||||||
|
|
||||||
`, recipeName, path.Join(config.RECIPES_DIR, recipeName), recipeName))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,3 +102,26 @@ func newRecipeMeta(recipeName string) recipeMetadata {
|
|||||||
SSO: "No",
|
SSO: "No",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
gitName string
|
||||||
|
gitEmail string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RecipeNewCommand.Flags().StringVarP(
|
||||||
|
&gitName,
|
||||||
|
"git-name",
|
||||||
|
"N",
|
||||||
|
"",
|
||||||
|
"Git (user) name to do commits with",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeNewCommand.Flags().StringVarP(
|
||||||
|
&gitEmail,
|
||||||
|
"git-email",
|
||||||
|
"e",
|
||||||
|
"",
|
||||||
|
"Git email name to do commits with",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,36 +1,19 @@
|
|||||||
package recipe
|
package recipe
|
||||||
|
|
||||||
import (
|
import "github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RecipeCommand defines all recipe related sub-commands.
|
// RecipeCommand defines all recipe related sub-commands.
|
||||||
var RecipeCommand = cli.Command{
|
var RecipeCommand = &cobra.Command{
|
||||||
Name: "recipe",
|
Use: "recipe [cmd] [args] [flags]",
|
||||||
Aliases: []string{"r"},
|
Aliases: []string{"r"},
|
||||||
Usage: "Manage recipes",
|
Short: "Manage recipes",
|
||||||
ArgsUsage: "<recipe>",
|
Long: `A recipe is a blueprint for an app.
|
||||||
Description: `
|
|
||||||
A recipe is a blueprint for an app. It is a bunch of config files which
|
It is a bunch of config files which describe how to deploy and maintain an app.
|
||||||
describe how to deploy and maintain an app. Recipes are maintained by the Co-op
|
Recipes are maintained by the Co-op Cloud community and you can use Abra to
|
||||||
Cloud community and you can use Abra to read them, deploy them and create apps
|
read them, deploy them and create apps for you.
|
||||||
for you.
|
|
||||||
|
|
||||||
Anyone who uses a recipe can become a maintainer. Maintainers typically make
|
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
|
sure the recipe is in good working order and the config upgraded in a timely
|
||||||
manner. Abra supports convenient automation for recipe maintainenace, see the
|
manner.`,
|
||||||
"abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands.
|
|
||||||
`,
|
|
||||||
Subcommands: []cli.Command{
|
|
||||||
recipeFetchCommand,
|
|
||||||
recipeLintCommand,
|
|
||||||
recipeListCommand,
|
|
||||||
recipeNewCommand,
|
|
||||||
recipeReleaseCommand,
|
|
||||||
recipeSyncCommand,
|
|
||||||
recipeUpgradeCommand,
|
|
||||||
recipeVersionCommand,
|
|
||||||
recipeResetCommand,
|
|
||||||
recipeDiffCommand,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,27 +10,25 @@ import (
|
|||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
|
||||||
"coopcloud.tech/tagcmp"
|
"coopcloud.tech/tagcmp"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var recipeReleaseCommand = cli.Command{
|
var RecipeReleaseCommand = &cobra.Command{
|
||||||
Name: "release",
|
Use: "release <recipe> [version] [flags]",
|
||||||
Aliases: []string{"rl"},
|
Aliases: []string{"rl"},
|
||||||
Usage: "Release a new recipe version",
|
Short: "Release a new recipe version",
|
||||||
ArgsUsage: "<recipe> [<version>]",
|
Long: `Create a new version of a recipe.
|
||||||
Description: `
|
|
||||||
Create a new version of a recipe. These versions are then published on the
|
These versions are then published on the Co-op Cloud recipe catalogue. These
|
||||||
Co-op Cloud recipe catalogue. These versions take the following form:
|
versions take the following form:
|
||||||
|
|
||||||
a.b.c+x.y.z
|
a.b.c+x.y.z
|
||||||
|
|
||||||
@ -44,99 +42,104 @@ 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
|
publish a minor version but that might lead to changes in the recipe which are
|
||||||
major and therefore require intervention while doing the upgrade work.
|
major and therefore require intervention while doing the upgrade work.
|
||||||
|
|
||||||
Publish your new release to git.coopcloud.tech with "-p/--publish". This
|
Publish your new release to git.coopcloud.tech with "--publish/-p". This
|
||||||
requires that you have permission to git push to these repositories and have
|
requires that you have permission to git push to these repositories and have
|
||||||
your SSH keys configured on your account.
|
your SSH keys configured on your account.`,
|
||||||
`,
|
Args: cobra.RangeArgs(1, 2),
|
||||||
Flags: []cli.Flag{
|
ValidArgsFunction: func(
|
||||||
internal.DebugFlag,
|
cmd *cobra.Command,
|
||||||
internal.NoInputFlag,
|
args []string,
|
||||||
internal.DryFlag,
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
internal.MajorFlag,
|
switch l := len(args); l {
|
||||||
internal.MinorFlag,
|
case 0:
|
||||||
internal.PatchFlag,
|
return autocomplete.RecipeNameComplete()
|
||||||
internal.PublishFlag,
|
case 1:
|
||||||
internal.OfflineFlag,
|
return autocomplete.RecipeVersionComplete(args[0])
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.RecipeNameComplete,
|
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
recipe := internal.ValidateRecipe(c)
|
|
||||||
|
|
||||||
imagesTmp, err := getImageVersions(recipe)
|
imagesTmp, err := getImageVersions(recipe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mainApp, err := internal.GetMainAppImage(recipe)
|
mainApp, err := internal.GetMainAppImage(recipe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mainAppVersion := imagesTmp[mainApp]
|
mainAppVersion := imagesTmp[mainApp]
|
||||||
if mainAppVersion == "" {
|
if mainAppVersion == "" {
|
||||||
logrus.Fatalf("main app service version for %s is empty?", recipe.Name)
|
log.Fatalf("main app service version for %s is empty?", recipe.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagString string
|
||||||
|
if len(args) == 2 {
|
||||||
|
tagString = args[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
tagString := c.Args().Get(1)
|
|
||||||
if tagString != "" {
|
if tagString != "" {
|
||||||
if _, err := tagcmp.Parse(tagString); err != nil {
|
if _, err := tagcmp.Parse(tagString); err != nil {
|
||||||
logrus.Fatalf("cannot parse %s, invalid tag specified?", tagString)
|
log.Fatalf("cannot parse %s, invalid tag specified?", tagString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
|
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
|
||||||
logrus.Fatal("cannot specify tag and bump type at the same time")
|
log.Fatal("cannot specify tag and bump type at the same time")
|
||||||
}
|
}
|
||||||
|
|
||||||
if tagString != "" {
|
if tagString != "" {
|
||||||
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
|
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tags, err := recipe.Tags()
|
tags, err := recipe.Tags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
|
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
|
||||||
var err error
|
var err error
|
||||||
tagString, err = getLabelVersion(recipe, false)
|
tagString, err = getLabelVersion(recipe, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isClean, err := gitPkg.IsClean(recipe.Dir())
|
isClean, err := gitPkg.IsClean(recipe.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isClean {
|
if !isClean {
|
||||||
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
|
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
|
||||||
if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil {
|
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
logrus.Warnf("previous git tags detected, assuming this is a new semver release")
|
log.Warnf("previous git tags detected, assuming this is a new semver release")
|
||||||
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
|
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logrus.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name)
|
log.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name)
|
||||||
|
|
||||||
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
|
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
|
||||||
if cleanUpErr := cleanUpTag(tagString, recipe.Name); err != nil {
|
if cleanUpErr := cleanUpTag(recipe, tagString); err != nil {
|
||||||
logrus.Fatal(cleanUpErr)
|
log.Fatal(cleanUpErr)
|
||||||
}
|
}
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,8 +147,12 @@ your SSH keys configured on your account.
|
|||||||
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
|
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
|
||||||
services := make(map[string]string)
|
services := make(map[string]string)
|
||||||
|
|
||||||
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
missingTag := false
|
missingTag := false
|
||||||
for _, service := range recipe.Config.Services {
|
for _, service := range config.Services {
|
||||||
if service.Image == "" {
|
if service.Image == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -184,8 +191,7 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
|
|||||||
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
|
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
directory := path.Join(config.RECIPES_DIR, recipe.Name)
|
repo, err := git.PlainOpen(recipe.Dir)
|
||||||
repo, err := git.PlainOpen(directory)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -210,19 +216,19 @@ func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := addReleaseNotes(recipe, tagString); err != nil {
|
if err := addReleaseNotes(recipe, tagString); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := commitRelease(recipe, tagString); err != nil {
|
if err := commitRelease(recipe, tagString); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tagRelease(tagString, repo); err != nil {
|
if err := tagRelease(tagString, repo); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pushRelease(recipe, tagString); err != nil {
|
if err := pushRelease(recipe, tagString); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -246,8 +252,14 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
|
|||||||
// addReleaseNotes checks if the release/next release note exists and moves the
|
// addReleaseNotes checks if the release/next release note exists and moves the
|
||||||
// file to release/<tag>.
|
// file to release/<tag>.
|
||||||
func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
||||||
repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
|
releaseDir := path.Join(recipe.Dir, "release")
|
||||||
tagReleaseNotePath := path.Join(repoPath, "release", tag)
|
if _, err := os.Stat(releaseDir); errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.Mkdir(releaseDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tagReleaseNotePath := path.Join(releaseDir, tag)
|
||||||
if _, err := os.Stat(tagReleaseNotePath); err == nil {
|
if _, err := os.Stat(tagReleaseNotePath); err == nil {
|
||||||
// Release note for current tag already exist exists.
|
// Release note for current tag already exist exists.
|
||||||
return nil
|
return nil
|
||||||
@ -255,49 +267,55 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
nextReleaseNotePath := path.Join(repoPath, "release", "next")
|
var addNextAsReleaseNotes bool
|
||||||
|
|
||||||
|
nextReleaseNotePath := path.Join(releaseDir, "next")
|
||||||
if _, err := os.Stat(nextReleaseNotePath); err == nil {
|
if _, err := os.Stat(nextReleaseNotePath); err == nil {
|
||||||
// release/next note exists. Move it to release/<tag>
|
// release/next note exists. Move it to release/<tag>
|
||||||
if internal.Dry {
|
if internal.Dry {
|
||||||
logrus.Debugf("dry run: move release note from 'next' to %s", tag)
|
log.Debugf("dry run: move release note from 'next' to %s", tag)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.NoInput {
|
if !internal.NoInput {
|
||||||
prompt := &survey.Input{
|
prompt := &survey.Confirm{
|
||||||
Message: "Use release note in release/next?",
|
Message: "Use release note in release/next?",
|
||||||
}
|
}
|
||||||
var addReleaseNote bool
|
|
||||||
if err := survey.AskOne(prompt, &addReleaseNote); err != nil {
|
if err := survey.AskOne(prompt, &addNextAsReleaseNotes); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !addReleaseNote {
|
|
||||||
|
if !addNextAsReleaseNotes {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := os.Rename(nextReleaseNotePath, tagReleaseNotePath)
|
|
||||||
if err != nil {
|
if err := os.Rename(nextReleaseNotePath, tagReleaseNotePath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = gitPkg.Add(repoPath, path.Join("release", "next"), internal.Dry)
|
|
||||||
if err != nil {
|
if err := gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry)
|
|
||||||
if err != nil {
|
if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if !errors.Is(err, os.ErrNotExist) {
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// No release note exists for the current release.
|
// NOTE(d1): No release note exists for the current release. Or, we've
|
||||||
if internal.NoInput {
|
// already used release/next as the release note
|
||||||
|
if internal.NoInput || addNextAsReleaseNotes {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt := &survey.Input{
|
prompt := &survey.Input{
|
||||||
Message: "Release Note (leave empty for no release note)",
|
Message: "Release Note (leave empty for no release note)",
|
||||||
}
|
}
|
||||||
|
|
||||||
var releaseNote string
|
var releaseNote string
|
||||||
if err := survey.AskOne(prompt, &releaseNote); err != nil {
|
if err := survey.AskOne(prompt, &releaseNote); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -307,12 +325,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644)
|
if err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry)
|
|
||||||
if err != nil {
|
if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,24 +338,23 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
|||||||
|
|
||||||
func commitRelease(recipe recipe.Recipe, tag string) error {
|
func commitRelease(recipe recipe.Recipe, tag string) error {
|
||||||
if internal.Dry {
|
if internal.Dry {
|
||||||
logrus.Debugf("dry run: no changes committed")
|
log.Debugf("dry run: no changes committed")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
isClean, err := gitPkg.IsClean(recipe.Dir())
|
isClean, err := gitPkg.IsClean(recipe.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if isClean {
|
if isClean {
|
||||||
if !internal.Dry {
|
if !internal.Dry {
|
||||||
return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir())
|
return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := fmt.Sprintf("chore: publish %s release", tag)
|
msg := fmt.Sprintf("chore: publish %s release", tag)
|
||||||
repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
|
if err := gitPkg.Commit(recipe.Dir, msg, internal.Dry); err != nil {
|
||||||
if err := gitPkg.Commit(repoPath, msg, internal.Dry); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,7 +363,7 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
|
|||||||
|
|
||||||
func tagRelease(tagString string, repo *git.Repository) error {
|
func tagRelease(tagString string, repo *git.Repository) error {
|
||||||
if internal.Dry {
|
if internal.Dry {
|
||||||
logrus.Debugf("dry run: no git tag created (%s)", tagString)
|
log.Debugf("dry run: no git tag created (%s)", tagString)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,43 +383,42 @@ func tagRelease(tagString string, repo *git.Repository) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hash := formatter.SmallSHA(head.Hash().String())
|
hash := formatter.SmallSHA(head.Hash().String())
|
||||||
logrus.Debugf(fmt.Sprintf("created tag %s at %s", tagString, hash))
|
log.Debugf(fmt.Sprintf("created tag %s at %s", tagString, hash))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pushRelease(recipe recipe.Recipe, tagString string) error {
|
func pushRelease(recipe recipe.Recipe, tagString string) error {
|
||||||
if internal.Dry {
|
if internal.Dry {
|
||||||
logrus.Info("dry run: no changes published")
|
log.Info("dry run: no changes published")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Publish && !internal.NoInput {
|
if !publish && !internal.NoInput {
|
||||||
prompt := &survey.Confirm{
|
prompt := &survey.Confirm{
|
||||||
Message: "publish new release?",
|
Message: "publish new release?",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := survey.AskOne(prompt, &internal.Publish); err != nil {
|
if err := survey.AskOne(prompt, &publish); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.Publish {
|
if publish {
|
||||||
if err := recipe.Push(internal.Dry); err != nil {
|
if err := recipe.Push(internal.Dry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString)
|
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString)
|
||||||
logrus.Infof("new release published: %s", url)
|
log.Infof("new release published: %s", url)
|
||||||
} else {
|
} else {
|
||||||
logrus.Info("no -p/--publish passed, not publishing")
|
log.Info("no -p/--publish passed, not publishing")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
|
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
|
||||||
directory := path.Join(config.RECIPES_DIR, recipe.Name)
|
repo, err := git.PlainOpen(recipe.Dir)
|
||||||
repo, err := git.PlainOpen(directory)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -468,7 +483,7 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
|
|||||||
}
|
}
|
||||||
|
|
||||||
if lastGitTag.String() == tagString {
|
if lastGitTag.String() == tagString {
|
||||||
logrus.Fatalf("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString)
|
log.Fatalf("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.NoInput {
|
if !internal.NoInput {
|
||||||
@ -478,37 +493,36 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
|
|||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
if err := survey.AskOne(prompt, &ok); err != nil {
|
if err := survey.AskOne(prompt, &ok); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
logrus.Fatal("exiting as requested")
|
log.Fatal("exiting as requested")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := addReleaseNotes(recipe, tagString); err != nil {
|
if err := addReleaseNotes(recipe, tagString); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := commitRelease(recipe, tagString); err != nil {
|
if err := commitRelease(recipe, tagString); err != nil {
|
||||||
logrus.Fatalf("failed to commit changes: %s", err.Error())
|
log.Fatalf("failed to commit changes: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tagRelease(tagString, repo); err != nil {
|
if err := tagRelease(tagString, repo); err != nil {
|
||||||
logrus.Fatalf("failed to tag release: %s", err.Error())
|
log.Fatalf("failed to tag release: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pushRelease(recipe, tagString); err != nil {
|
if err := pushRelease(recipe, tagString); err != nil {
|
||||||
logrus.Fatalf("failed to publish new release: %s", err.Error())
|
log.Fatalf("failed to publish new release: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanUpTag removes a freshly created tag
|
// cleanUpTag removes a freshly created tag
|
||||||
func cleanUpTag(tag, recipeName string) error {
|
func cleanUpTag(recipe recipe.Recipe, tag string) error {
|
||||||
directory := path.Join(config.RECIPES_DIR, recipeName)
|
repo, err := git.PlainOpen(recipe.Dir)
|
||||||
repo, err := git.PlainOpen(directory)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -519,22 +533,22 @@ func cleanUpTag(tag, recipeName string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("removed freshly created tag %s", tag)
|
log.Debugf("removed freshly created tag %s", tag)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
|
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
|
||||||
initTag, err := recipePkg.GetVersionLabelLocal(recipe)
|
initTag, err := recipe.GetVersionLabelLocal()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if initTag == "" {
|
if initTag == "" {
|
||||||
logrus.Fatalf("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name)
|
log.Fatalf("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Warnf("discovered %s as currently synced recipe label", initTag)
|
log.Warnf("discovered %s as currently synced recipe label", initTag)
|
||||||
|
|
||||||
if prompt && !internal.NoInput {
|
if prompt && !internal.NoInput {
|
||||||
var response bool
|
var response bool
|
||||||
@ -550,3 +564,50 @@ func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
|
|||||||
|
|
||||||
return initTag, nil
|
return initTag, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
publish bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RecipeReleaseCommand.Flags().BoolVarP(
|
||||||
|
&internal.Dry,
|
||||||
|
"dry-run",
|
||||||
|
"r",
|
||||||
|
false,
|
||||||
|
"report changes that would be made",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeReleaseCommand.Flags().BoolVarP(
|
||||||
|
&internal.Major,
|
||||||
|
"major",
|
||||||
|
"x",
|
||||||
|
false,
|
||||||
|
"increase the major part of the version",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeReleaseCommand.Flags().BoolVarP(
|
||||||
|
&internal.Minor,
|
||||||
|
"minor",
|
||||||
|
"y",
|
||||||
|
false,
|
||||||
|
"increase the minor part of the version",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeReleaseCommand.Flags().BoolVarP(
|
||||||
|
&internal.Patch,
|
||||||
|
"patch",
|
||||||
|
"z",
|
||||||
|
false,
|
||||||
|
"increase the patch part of the version",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeReleaseCommand.Flags().BoolVarP(
|
||||||
|
&publish,
|
||||||
|
"publish",
|
||||||
|
"p",
|
||||||
|
false,
|
||||||
|
"publish changes to git.coopcloud.tech",
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@ -1,56 +1,46 @@
|
|||||||
package recipe
|
package recipe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var recipeResetCommand = cli.Command{
|
var RecipeResetCommand = &cobra.Command{
|
||||||
Name: "reset",
|
Use: "reset <recipe> [flags]",
|
||||||
Usage: "Remove all unstaged changes from recipe config",
|
Aliases: []string{"rs"},
|
||||||
Description: "WARNING, this will delete your changes. Be Careful.",
|
Short: "Remove all unstaged changes from recipe config",
|
||||||
Aliases: []string{"rs"},
|
Long: "WARNING: this will delete your changes. Be Careful.",
|
||||||
ArgsUsage: "<recipe>",
|
Args: cobra.ExactArgs(1),
|
||||||
Flags: []cli.Flag{
|
ValidArgsFunction: func(
|
||||||
internal.DebugFlag,
|
cmd *cobra.Command,
|
||||||
internal.NoInputFlag,
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.RecipeNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.RecipeNameComplete,
|
r := internal.ValidateRecipe(args, cmd.Name())
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
recipeName := c.Args().First()
|
|
||||||
|
|
||||||
if recipeName != "" {
|
repo, err := git.PlainOpen(r.Dir)
|
||||||
internal.ValidateRecipe(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
repoPath := path.Join(config.RECIPES_DIR, recipeName)
|
|
||||||
repo, err := git.PlainOpen(repoPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ref, err := repo.Head()
|
ref, err := repo.Head()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
worktree, err := repo.Worktree()
|
worktree, err := repo.Worktree()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &git.ResetOptions{Commit: ref.Hash(), Mode: git.HardReset}
|
opts := &git.ResetOptions{Commit: ref.Hash(), Mode: git.HardReset}
|
||||||
if err := worktree.Reset(opts); err != nil {
|
if err := worktree.Reset(opts); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,71 +2,75 @@ package recipe
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/tagcmp"
|
"coopcloud.tech/tagcmp"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var recipeSyncCommand = cli.Command{
|
var RecipeSyncCommand = &cobra.Command{
|
||||||
Name: "sync",
|
Use: "sync <recipe> [version] [flags]",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
Usage: "Sync recipe version label",
|
Short: "Sync recipe version label",
|
||||||
ArgsUsage: "<recipe> [<version>]",
|
Long: `Generate labels for the main recipe service.
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
By convention, the service named "app" using the following format:
|
||||||
internal.NoInputFlag,
|
|
||||||
internal.DryFlag,
|
|
||||||
internal.MajorFlag,
|
|
||||||
internal.MinorFlag,
|
|
||||||
internal.PatchFlag,
|
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
Description: `
|
|
||||||
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>
|
coop-cloud.${STACK_NAME}.version=<version>
|
||||||
|
|
||||||
Where <version> can be specifed on the command-line or Abra can attempt to
|
Where [version] can be specifed on the command-line or Abra can attempt to
|
||||||
auto-generate it for you. The <recipe> configuration will be updated on the
|
auto-generate it for you. The <recipe> configuration will be updated on the
|
||||||
local file system.
|
local file system.`,
|
||||||
`,
|
Args: cobra.RangeArgs(1, 2),
|
||||||
BashComplete: autocomplete.RecipeNameComplete,
|
ValidArgsFunction: func(
|
||||||
Action: func(c *cli.Context) error {
|
cmd *cobra.Command,
|
||||||
recipe := internal.ValidateRecipe(c)
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch l := len(args); l {
|
||||||
|
case 0:
|
||||||
|
return autocomplete.RecipeNameComplete()
|
||||||
|
case 1:
|
||||||
|
return autocomplete.RecipeVersionComplete(args[0])
|
||||||
|
default:
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||||
|
|
||||||
mainApp, err := internal.GetMainAppImage(recipe)
|
mainApp, err := internal.GetMainAppImage(recipe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
imagesTmp, err := getImageVersions(recipe)
|
imagesTmp, err := getImageVersions(recipe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mainAppVersion := imagesTmp[mainApp]
|
mainAppVersion := imagesTmp[mainApp]
|
||||||
|
|
||||||
tags, err := recipe.Tags()
|
tags, err := recipe.Tags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextTag string
|
||||||
|
if len(args) == 2 {
|
||||||
|
nextTag = args[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTag := c.Args().Get(1)
|
|
||||||
if len(tags) == 0 && nextTag == "" {
|
if len(tags) == 0 && nextTag == "" {
|
||||||
logrus.Warnf("no git tags found for %s", recipe.Name)
|
log.Warnf("no git tags found for %s", recipe.Name)
|
||||||
if internal.NoInput {
|
if internal.NoInput {
|
||||||
logrus.Fatalf("unable to continue, input required for initial version")
|
log.Fatalf("unable to continue, input required for initial version")
|
||||||
}
|
}
|
||||||
fmt.Println(fmt.Sprintf(`
|
fmt.Println(fmt.Sprintf(`
|
||||||
The following options are two types of initial semantic version that you can
|
The following options are two types of initial semantic version that you can
|
||||||
@ -93,7 +97,7 @@ likely to change.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
|
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
|
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
|
||||||
@ -102,27 +106,26 @@ likely to change.
|
|||||||
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
|
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
|
||||||
latestRelease := tags[len(tags)-1]
|
latestRelease := tags[len(tags)-1]
|
||||||
if err := internal.PromptBumpType("", latestRelease); err != nil {
|
if err := internal.PromptBumpType("", latestRelease); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if nextTag == "" {
|
if nextTag == "" {
|
||||||
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
|
repo, err := git.PlainOpen(recipe.Dir)
|
||||||
repo, err := git.PlainOpen(recipeDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastGitTag tagcmp.Tag
|
var lastGitTag tagcmp.Tag
|
||||||
iter, err := repo.Tags()
|
iter, err := repo.Tags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := iter.ForEach(func(ref *plumbing.Reference) error {
|
if err := iter.ForEach(func(ref *plumbing.Reference) error {
|
||||||
obj, err := repo.TagObject(ref.Hash())
|
obj, err := repo.TagObject(ref.Hash())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal("Tag at commit ", ref.Hash(), " is unannotated or otherwise broken. Please fix it.")
|
log.Fatal("Tag at commit ", ref.Hash(), " is unannotated or otherwise broken. Please fix it.")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +142,7 @@ likely to change.
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// bumpType is used to decide what part of the tag should be incremented
|
// bumpType is used to decide what part of the tag should be incremented
|
||||||
@ -147,7 +150,7 @@ likely to change.
|
|||||||
if bumpType != 0 {
|
if bumpType != 0 {
|
||||||
// a bitwise check if the number is a power of 2
|
// a bitwise check if the number is a power of 2
|
||||||
if (bumpType & (bumpType - 1)) != 0 {
|
if (bumpType & (bumpType - 1)) != 0 {
|
||||||
logrus.Fatal("you can only use one version flag: --major, --minor or --patch")
|
log.Fatal("you can only use one version flag: --major, --minor or --patch")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,14 +159,14 @@ likely to change.
|
|||||||
if internal.Patch {
|
if internal.Patch {
|
||||||
now, err := strconv.Atoi(newTag.Patch)
|
now, err := strconv.Atoi(newTag.Patch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newTag.Patch = strconv.Itoa(now + 1)
|
newTag.Patch = strconv.Itoa(now + 1)
|
||||||
} else if internal.Minor {
|
} else if internal.Minor {
|
||||||
now, err := strconv.Atoi(newTag.Minor)
|
now, err := strconv.Atoi(newTag.Minor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newTag.Patch = "0"
|
newTag.Patch = "0"
|
||||||
@ -171,7 +174,7 @@ likely to change.
|
|||||||
} else if internal.Major {
|
} else if internal.Major {
|
||||||
now, err := strconv.Atoi(newTag.Major)
|
now, err := strconv.Atoi(newTag.Major)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newTag.Patch = "0"
|
newTag.Patch = "0"
|
||||||
@ -181,35 +184,67 @@ likely to change.
|
|||||||
}
|
}
|
||||||
|
|
||||||
newTag.Metadata = mainAppVersion
|
newTag.Metadata = mainAppVersion
|
||||||
logrus.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name)
|
log.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name)
|
||||||
nextTag = newTag.String()
|
nextTag = newTag.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tagcmp.Parse(nextTag); err != nil {
|
if _, err := tagcmp.Parse(nextTag); err != nil {
|
||||||
logrus.Fatalf("invalid version %s specified", nextTag)
|
log.Fatalf("invalid version %s specified", nextTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
mainService := "app"
|
mainService := "app"
|
||||||
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
|
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
|
||||||
if !internal.Dry {
|
if !internal.Dry {
|
||||||
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
|
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logrus.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name)
|
log.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
isClean, err := gitPkg.IsClean(recipe.Dir())
|
isClean, err := gitPkg.IsClean(recipe.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
if !isClean {
|
if !isClean {
|
||||||
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
|
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
|
||||||
if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil {
|
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RecipeSyncCommand.Flags().BoolVarP(
|
||||||
|
&internal.Dry,
|
||||||
|
"dry-run",
|
||||||
|
"r",
|
||||||
|
false,
|
||||||
|
"report changes that would be made",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeSyncCommand.Flags().BoolVarP(
|
||||||
|
&internal.Major,
|
||||||
|
"major",
|
||||||
|
"x",
|
||||||
|
false,
|
||||||
|
"increase the major part of the version",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeSyncCommand.Flags().BoolVarP(
|
||||||
|
&internal.Minor,
|
||||||
|
"minor",
|
||||||
|
"y",
|
||||||
|
false,
|
||||||
|
"increase the minor part of the version",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeSyncCommand.Flags().BoolVarP(
|
||||||
|
&internal.Patch,
|
||||||
|
"patch",
|
||||||
|
"z",
|
||||||
|
false,
|
||||||
|
"increase the patch part of the version",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -12,15 +12,14 @@ import (
|
|||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||||
"coopcloud.tech/tagcmp"
|
"coopcloud.tech/tagcmp"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type imgPin struct {
|
type imgPin struct {
|
||||||
@ -28,8 +27,8 @@ type imgPin struct {
|
|||||||
version tagcmp.Tag
|
version tagcmp.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
// anUpgrade represents a single service upgrade (as within a recipe), and the list of tags that it can be upgraded to,
|
// anUpgrade represents a single service upgrade (as within a recipe), and the
|
||||||
// for serialization purposes.
|
// list of tags that it can be upgraded to, for serialization purposes.
|
||||||
type anUpgrade struct {
|
type anUpgrade struct {
|
||||||
Service string `json:"service"`
|
Service string `json:"service"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
@ -37,14 +36,13 @@ type anUpgrade struct {
|
|||||||
UpgradeTags []string `json:"upgrades"`
|
UpgradeTags []string `json:"upgrades"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var recipeUpgradeCommand = cli.Command{
|
var RecipeUpgradeCommand = &cobra.Command{
|
||||||
Name: "upgrade",
|
Use: "upgrade <recipe> [flags]",
|
||||||
Aliases: []string{"u"},
|
Aliases: []string{"u"},
|
||||||
Usage: "Upgrade recipe image tags",
|
Short: "Upgrade recipe image tags",
|
||||||
Description: `
|
Long: `Upgrade a given <recipe> configuration.
|
||||||
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
|
It will update the relevant compose file tags on the local file system.
|
||||||
on the local file system.
|
|
||||||
|
|
||||||
Some image tags cannot be parsed because they do not follow some sort of
|
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
|
semver-like convention. In this case, all possible tags will be listed and it
|
||||||
@ -54,46 +52,26 @@ The command is interactive and will show a select input which allows you to
|
|||||||
make a seclection. Use the "?" key to see more help on navigating this
|
make a seclection. Use the "?" key to see more help on navigating this
|
||||||
interface.
|
interface.
|
||||||
|
|
||||||
You may invoke this command in "wizard" mode and be prompted for input:
|
You may invoke this command in "wizard" mode and be prompted for input.`,
|
||||||
|
Args: cobra.RangeArgs(0, 1),
|
||||||
abra recipe upgrade
|
ValidArgsFunction: func(
|
||||||
`,
|
cmd *cobra.Command,
|
||||||
ArgsUsage: "<recipe>",
|
args []string,
|
||||||
Flags: []cli.Flag{
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
internal.DebugFlag,
|
return autocomplete.RecipeNameComplete()
|
||||||
internal.NoInputFlag,
|
|
||||||
internal.PatchFlag,
|
|
||||||
internal.MinorFlag,
|
|
||||||
internal.MajorFlag,
|
|
||||||
internal.MachineReadableFlag,
|
|
||||||
internal.AllTagsFlag,
|
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.RecipeNameComplete,
|
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
recipe := internal.ValidateRecipe(c)
|
|
||||||
|
|
||||||
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
|
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
||||||
if bumpType != 0 {
|
if bumpType != 0 {
|
||||||
// a bitwise check if the number is a power of 2
|
// a bitwise check if the number is a power of 2
|
||||||
if (bumpType & (bumpType - 1)) != 0 {
|
if (bumpType & (bumpType - 1)) != 0 {
|
||||||
logrus.Fatal("you can only use one of: --major, --minor, --patch.")
|
log.Fatal("you can only use one of: --major, --minor, --patch.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,26 +84,25 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
|||||||
|
|
||||||
// check for versions file and load pinned versions
|
// check for versions file and load pinned versions
|
||||||
versionsPresent := false
|
versionsPresent := false
|
||||||
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
|
versionsPath := path.Join(recipe.Dir, "versions")
|
||||||
versionsPath := path.Join(recipeDir, "versions")
|
servicePins := make(map[string]imgPin)
|
||||||
var servicePins = make(map[string]imgPin)
|
|
||||||
if _, err := os.Stat(versionsPath); err == nil {
|
if _, err := os.Stat(versionsPath); err == nil {
|
||||||
logrus.Debugf("found versions file for %s", recipe.Name)
|
log.Debugf("found versions file for %s", recipe.Name)
|
||||||
file, err := os.Open(versionsPath)
|
file, err := os.Open(versionsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
splitLine := strings.Split(line, " ")
|
splitLine := strings.Split(line, " ")
|
||||||
if splitLine[0] != "pin" || len(splitLine) != 3 {
|
if splitLine[0] != "pin" || len(splitLine) != 3 {
|
||||||
logrus.Fatalf("malformed version pin specification: %s", line)
|
log.Fatalf("malformed version pin specification: %s", line)
|
||||||
}
|
}
|
||||||
pinSlice := strings.Split(splitLine[2], ":")
|
pinSlice := strings.Split(splitLine[2], ":")
|
||||||
pinTag, err := tagcmp.Parse(pinSlice[1])
|
pinTag, err := tagcmp.Parse(pinSlice[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
pin := imgPin{
|
pin := imgPin{
|
||||||
image: pinSlice[0],
|
image: pinSlice[0],
|
||||||
@ -134,45 +111,50 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
|||||||
servicePins[splitLine[1]] = pin
|
servicePins[splitLine[1]] = pin
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
logrus.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
versionsPresent = true
|
versionsPresent = true
|
||||||
} else {
|
} else {
|
||||||
logrus.Debugf("did not find versions file for %s", recipe.Name)
|
log.Debugf("did not find versions file for %s", recipe.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, service := range recipe.Config.Services {
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, service := range config.Services {
|
||||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
regVersions, err := client.GetRegistryTags(img)
|
regVersions, err := client.GetRegistryTags(img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
image := reference.Path(img)
|
image := reference.Path(img)
|
||||||
logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
|
log.Debugf("retrieved %s from remote registry for %s", regVersions, image)
|
||||||
image = formatter.StripTagMeta(image)
|
image = formatter.StripTagMeta(image)
|
||||||
|
|
||||||
switch img.(type) {
|
switch img.(type) {
|
||||||
case reference.NamedTagged:
|
case reference.NamedTagged:
|
||||||
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
|
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
|
||||||
logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
|
log.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
logrus.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name)
|
log.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
|
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name)
|
log.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("parsed %s for %s", tag, service.Name)
|
log.Debugf("parsed %s for %s", tag, service.Name)
|
||||||
|
|
||||||
var compatible []tagcmp.Tag
|
var compatible []tagcmp.Tag
|
||||||
for _, regVersion := range regVersions {
|
for _, regVersion := range regVersions {
|
||||||
@ -186,18 +168,18 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name)
|
log.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name)
|
||||||
|
|
||||||
sort.Sort(tagcmp.ByTagDesc(compatible))
|
sort.Sort(tagcmp.ByTagDesc(compatible))
|
||||||
|
|
||||||
if len(compatible) == 0 && !internal.AllTags {
|
if len(compatible) == 0 && !allTags {
|
||||||
logrus.Info(fmt.Sprintf("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
|
log.Info(fmt.Sprintf("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
|
||||||
continue // skip on to the next tag and don't update any compose files
|
continue // skip on to the next tag and don't update any compose files
|
||||||
}
|
}
|
||||||
|
|
||||||
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline)
|
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
compatibleStrings := []string{"skip"}
|
compatibleStrings := []string{"skip"}
|
||||||
@ -213,7 +195,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name)
|
log.Debugf("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name)
|
||||||
|
|
||||||
var upgradeTag string
|
var upgradeTag string
|
||||||
_, ok := servicePins[service.Name]
|
_, ok := servicePins[service.Name]
|
||||||
@ -230,13 +212,13 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if contains {
|
if contains {
|
||||||
logrus.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
|
log.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
|
||||||
} else {
|
} else {
|
||||||
logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
|
log.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logrus.Fatalf("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
|
log.Fatalf("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -244,7 +226,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
|||||||
for _, upTag := range compatible {
|
for _, upTag := range compatible {
|
||||||
upElement, err := tag.UpgradeDelta(upTag)
|
upElement, err := tag.UpgradeDelta(upTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
delta := upElement.UpgradeType()
|
delta := upElement.UpgradeType()
|
||||||
if delta <= bumpType {
|
if delta <= bumpType {
|
||||||
@ -253,15 +235,15 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if upgradeTag == "" {
|
if upgradeTag == "" {
|
||||||
logrus.Warnf("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image)
|
log.Warnf("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
|
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
|
||||||
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags {
|
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || allTags {
|
||||||
tag := img.(reference.NamedTagged).Tag()
|
tag := img.(reference.NamedTagged).Tag()
|
||||||
if !internal.AllTags {
|
if !allTags {
|
||||||
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag))
|
log.Warn(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag))
|
||||||
}
|
}
|
||||||
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
|
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
|
||||||
compatibleStrings = []string{"skip"}
|
compatibleStrings = []string{"skip"}
|
||||||
@ -299,7 +281,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
|||||||
Options: compatibleStrings,
|
Options: compatibleStrings,
|
||||||
}
|
}
|
||||||
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
|
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -307,14 +289,14 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
|||||||
if upgradeTag != "skip" {
|
if upgradeTag != "skip" {
|
||||||
ok, err := recipe.UpdateTag(image, upgradeTag)
|
ok, err := recipe.UpdateTag(image, upgradeTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
|
log.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !internal.NoInput {
|
if !internal.NoInput {
|
||||||
logrus.Warnf("not upgrading %s, skipping as requested", image)
|
log.Warnf("not upgrading %s, skipping as requested", image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -323,33 +305,77 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
|||||||
if internal.MachineReadable {
|
if internal.MachineReadable {
|
||||||
jsonstring, err := json.Marshal(upgradeList)
|
jsonstring, err := json.Marshal(upgradeList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(string(jsonstring))
|
fmt.Println(string(jsonstring))
|
||||||
|
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, upgrade := range upgradeList {
|
for _, upgrade := range upgradeList {
|
||||||
logrus.Infof("can upgrade service: %s, image: %s, tag: %s ::\n", upgrade.Service, upgrade.Image, upgrade.Tag)
|
log.Infof("can upgrade service: %s, image: %s, tag: %s ::", upgrade.Service, upgrade.Image, upgrade.Tag)
|
||||||
for _, utag := range upgrade.UpgradeTags {
|
for _, utag := range upgrade.UpgradeTags {
|
||||||
logrus.Infof(" %s\n", utag)
|
log.Infof(" %s", utag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isClean, err := gitPkg.IsClean(recipeDir)
|
isClean, err := gitPkg.IsClean(recipe.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
if !isClean {
|
if !isClean {
|
||||||
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
|
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
|
||||||
if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
|
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
allTags bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RecipeUpgradeCommand.Flags().BoolVarP(
|
||||||
|
&internal.Major,
|
||||||
|
"major",
|
||||||
|
"x",
|
||||||
|
false,
|
||||||
|
"increase the major part of the version",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeUpgradeCommand.Flags().BoolVarP(
|
||||||
|
&internal.Minor,
|
||||||
|
"minor",
|
||||||
|
"y",
|
||||||
|
false,
|
||||||
|
"increase the minor part of the version",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeUpgradeCommand.Flags().BoolVarP(
|
||||||
|
&internal.Patch,
|
||||||
|
"patch",
|
||||||
|
"z",
|
||||||
|
false,
|
||||||
|
"increase the patch part of the version",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeUpgradeCommand.Flags().BoolVarP(
|
||||||
|
&internal.MachineReadable,
|
||||||
|
"machine",
|
||||||
|
"m",
|
||||||
|
false,
|
||||||
|
"print machine-readable output",
|
||||||
|
)
|
||||||
|
|
||||||
|
RecipeUpgradeCommand.Flags().BoolVarP(
|
||||||
|
&allTags,
|
||||||
|
"all-tags",
|
||||||
|
"a",
|
||||||
|
false,
|
||||||
|
"list all tags, not just upgrades",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -7,79 +7,129 @@ import (
|
|||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||||
"github.com/olekukonko/tablewriter"
|
"github.com/spf13/cobra"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func sortServiceByName(versions [][]string) func(i, j int) bool {
|
var RecipeVersionCommand = &cobra.Command{
|
||||||
return func(i, j int) bool {
|
Use: "versions <recipe> [flags]",
|
||||||
// NOTE(d1): corresponds to the `tableCol` definition below
|
Aliases: []string{"v"},
|
||||||
if versions[i][1] == "app" {
|
Short: "List recipe versions",
|
||||||
return true
|
Args: cobra.ExactArgs(1),
|
||||||
}
|
ValidArgsFunction: func(
|
||||||
return versions[i][1] < versions[j][1]
|
cmd *cobra.Command,
|
||||||
}
|
args []string,
|
||||||
}
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.RecipeNameComplete()
|
||||||
var recipeVersionCommand = cli.Command{
|
|
||||||
Name: "versions",
|
|
||||||
Aliases: []string{"v"},
|
|
||||||
Usage: "List recipe versions",
|
|
||||||
ArgsUsage: "<recipe>",
|
|
||||||
Description: "Versions are read from the recipe catalogue.",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
|
||||||
internal.OfflineFlag,
|
|
||||||
internal.NoInputFlag,
|
|
||||||
internal.MachineReadableFlag,
|
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.RecipeNameComplete,
|
var warnMessages []string
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
recipe := internal.ValidateRecipe(c)
|
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||||
|
|
||||||
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
|
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
recipeMeta, ok := catl[recipe.Name]
|
recipeMeta, ok := catl[recipe.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
logrus.Fatalf("%s is not published on the catalogue?", recipe.Name)
|
warnMessages = append(warnMessages, "retrieved versions from local recipe repository")
|
||||||
|
|
||||||
|
recipeVersions, warnMsg, err := recipe.GetRecipeVersions()
|
||||||
|
if err != nil {
|
||||||
|
warnMessages = append(warnMessages, err.Error())
|
||||||
|
}
|
||||||
|
if len(warnMsg) > 0 {
|
||||||
|
warnMessages = append(warnMessages, warnMsg...)
|
||||||
|
}
|
||||||
|
|
||||||
|
recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(recipeMeta.Versions) == 0 {
|
if len(recipeMeta.Versions) == 0 {
|
||||||
logrus.Fatalf("%s has no catalogue published versions?", recipe.Name)
|
log.Fatalf("%s has no published versions?", recipe.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
|
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
|
||||||
tableCols := []string{"version", "service", "image", "tag"}
|
table, err := formatter.CreateTable()
|
||||||
table := formatter.CreateTable(tableCols)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Headers("SERVICE", "IMAGE", "TAG", "VERSION")
|
||||||
|
|
||||||
for version, meta := range recipeMeta.Versions[i] {
|
for version, meta := range recipeMeta.Versions[i] {
|
||||||
var versions [][]string
|
var allRows [][]string
|
||||||
|
var rows [][]string
|
||||||
|
|
||||||
for service, serviceMeta := range meta {
|
for service, serviceMeta := range meta {
|
||||||
versions = append(versions, []string{version, service, serviceMeta.Image, serviceMeta.Tag})
|
recipeVersion := version
|
||||||
|
if service != "app" {
|
||||||
|
recipeVersion = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, []string{
|
||||||
|
service,
|
||||||
|
serviceMeta.Image,
|
||||||
|
serviceMeta.Tag,
|
||||||
|
recipeVersion,
|
||||||
|
})
|
||||||
|
|
||||||
|
allRows = append(allRows, []string{
|
||||||
|
version,
|
||||||
|
service,
|
||||||
|
serviceMeta.Image,
|
||||||
|
serviceMeta.Tag,
|
||||||
|
recipeVersion,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(versions, sortServiceByName(versions))
|
sort.Slice(rows, sortServiceByName(rows))
|
||||||
|
|
||||||
for _, version := range versions {
|
table.Rows(rows...)
|
||||||
table.Append(version)
|
|
||||||
|
if !internal.MachineReadable {
|
||||||
|
if err := formatter.PrintTable(table); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.MachineReadable {
|
if internal.MachineReadable {
|
||||||
table.JSONRender()
|
sort.Slice(allRows, sortServiceByName(allRows))
|
||||||
} else {
|
headers := []string{"VERSION", "SERVICE", "NAME", "TAG"}
|
||||||
table.SetAutoMergeCellsByColumnIndex([]int{0})
|
out, err := formatter.ToJSON(headers, allRows)
|
||||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
if err != nil {
|
||||||
table.Render()
|
log.Fatal("unable to render to JSON: %s", err)
|
||||||
fmt.Println()
|
}
|
||||||
|
fmt.Println(out)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if !internal.MachineReadable {
|
||||||
|
for _, warnMsg := range warnMessages {
|
||||||
|
log.Warn(warnMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sortServiceByName(versions [][]string) func(i, j int) bool {
|
||||||
|
return func(i, j int) bool {
|
||||||
|
return versions[i][0] < versions[j][0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RecipeVersionCommand.Flags().BoolVarP(
|
||||||
|
&internal.MachineReadable,
|
||||||
|
"machine",
|
||||||
|
"m",
|
||||||
|
false,
|
||||||
|
"print machine-readable output",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
221
cli/run.go
Normal file
221
cli/run.go
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/cli/app"
|
||||||
|
"coopcloud.tech/abra/cli/catalogue"
|
||||||
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
"coopcloud.tech/abra/cli/recipe"
|
||||||
|
"coopcloud.tech/abra/cli/server"
|
||||||
|
"coopcloud.tech/abra/pkg/config"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
charmLog "github.com/charmbracelet/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/cobra/doc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(version, commit string) {
|
||||||
|
rootCmd := &cobra.Command{
|
||||||
|
Use: "abra [cmd] [args] [flags]",
|
||||||
|
Short: "The Co-op Cloud command-line utility belt 🎩🐇",
|
||||||
|
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
|
||||||
|
ValidArgs: []string{
|
||||||
|
"app",
|
||||||
|
"autocomplete",
|
||||||
|
"catalogue",
|
||||||
|
"man",
|
||||||
|
"recipe",
|
||||||
|
"server",
|
||||||
|
"upgrade",
|
||||||
|
},
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
dirs := []map[string]os.FileMode{
|
||||||
|
{config.ABRA_DIR: 0764},
|
||||||
|
{config.SERVERS_DIR: 0700},
|
||||||
|
{config.RECIPES_DIR: 0764},
|
||||||
|
{config.LOGS_DIR: 0764},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
for path, perm := range dir {
|
||||||
|
if err := os.Mkdir(path, perm); err != nil {
|
||||||
|
if !os.IsExist(err) {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Logger.SetStyles(charmLog.DefaultStyles())
|
||||||
|
charmLog.SetDefault(log.Logger)
|
||||||
|
|
||||||
|
if internal.MachineReadable {
|
||||||
|
log.SetOutput(os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if internal.Debug {
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
log.SetOutput(os.Stderr)
|
||||||
|
log.SetReportCaller(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("abra version %s, commit %s", version, commit)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCmd.CompletionOptions.DisableDefaultCmd = true
|
||||||
|
|
||||||
|
manCommand := &cobra.Command{
|
||||||
|
Use: "man [flags]",
|
||||||
|
Aliases: []string{"m"},
|
||||||
|
Short: "Generate manpage",
|
||||||
|
Example: ` # generate the man pages into /usr/local/share/man/man1
|
||||||
|
abra_path=$(which abra) # pass abra absolute path to sudo below
|
||||||
|
sudo $abra_path man
|
||||||
|
sudo mandb
|
||||||
|
|
||||||
|
# read the man pages
|
||||||
|
man abra
|
||||||
|
man abra-app-deploy`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
header := &doc.GenManHeader{
|
||||||
|
Title: "ABRA",
|
||||||
|
Section: "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
manDir := "/usr/local/share/man/man1"
|
||||||
|
if _, err := os.Stat(manDir); os.IsNotExist(err) {
|
||||||
|
log.Fatalf("unable to proceed, '%s' does not exist?")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := doc.GenManTree(rootCmd, header, manDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("don't forget to run 'sudo mandb'")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(
|
||||||
|
&internal.Debug,
|
||||||
|
"debug",
|
||||||
|
"d",
|
||||||
|
false,
|
||||||
|
"show debug messages",
|
||||||
|
)
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(
|
||||||
|
&internal.NoInput,
|
||||||
|
"no-input",
|
||||||
|
"n",
|
||||||
|
false,
|
||||||
|
"toggle non-interactive mode",
|
||||||
|
)
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(
|
||||||
|
&internal.Offline,
|
||||||
|
"offline",
|
||||||
|
"o",
|
||||||
|
false,
|
||||||
|
"prefer offline & filesystem access",
|
||||||
|
)
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(
|
||||||
|
&internal.IgnoreEnvVersion,
|
||||||
|
"ignore-env-version",
|
||||||
|
"i",
|
||||||
|
false,
|
||||||
|
"ignore .env version checkout",
|
||||||
|
)
|
||||||
|
|
||||||
|
catalogue.CatalogueCommand.AddCommand(
|
||||||
|
catalogue.CatalogueGenerateCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
server.ServerCommand.AddCommand(
|
||||||
|
server.ServerAddCommand,
|
||||||
|
server.ServerListCommand,
|
||||||
|
server.ServerPruneCommand,
|
||||||
|
server.ServerRemoveCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
recipe.RecipeCommand.AddCommand(
|
||||||
|
recipe.RecipeDiffCommand,
|
||||||
|
recipe.RecipeFetchCommand,
|
||||||
|
recipe.RecipeLintCommand,
|
||||||
|
recipe.RecipeListCommand,
|
||||||
|
recipe.RecipeNewCommand,
|
||||||
|
recipe.RecipeReleaseCommand,
|
||||||
|
recipe.RecipeResetCommand,
|
||||||
|
recipe.RecipeSyncCommand,
|
||||||
|
recipe.RecipeUpgradeCommand,
|
||||||
|
recipe.RecipeVersionCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(
|
||||||
|
UpgradeCommand,
|
||||||
|
AutocompleteCommand,
|
||||||
|
manCommand,
|
||||||
|
app.AppCommand,
|
||||||
|
catalogue.CatalogueCommand,
|
||||||
|
server.ServerCommand,
|
||||||
|
recipe.RecipeCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.AppCmdCommand.AddCommand(
|
||||||
|
app.AppCmdListCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.AppSecretCommand.AddCommand(
|
||||||
|
app.AppSecretGenerateCommand,
|
||||||
|
app.AppSecretInsertCommand,
|
||||||
|
app.AppSecretRmCommand,
|
||||||
|
app.AppSecretLsCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.AppVolumeCommand.AddCommand(
|
||||||
|
app.AppVolumeListCommand,
|
||||||
|
app.AppVolumeRemoveCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.AppBackupCommand.AddCommand(
|
||||||
|
app.AppBackupListCommand,
|
||||||
|
app.AppBackupDownloadCommand,
|
||||||
|
app.AppBackupCreateCommand,
|
||||||
|
app.AppBackupSnapshotsCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.AppCommand.AddCommand(
|
||||||
|
app.AppBackupCommand,
|
||||||
|
app.AppCheckCommand,
|
||||||
|
app.AppCmdCommand,
|
||||||
|
app.AppConfigCommand,
|
||||||
|
app.AppCpCommand,
|
||||||
|
app.AppDeployCommand,
|
||||||
|
app.AppListCommand,
|
||||||
|
app.AppLogsCommand,
|
||||||
|
app.AppNewCommand,
|
||||||
|
app.AppPsCommand,
|
||||||
|
app.AppRemoveCommand,
|
||||||
|
app.AppRestartCommand,
|
||||||
|
app.AppRestoreCommand,
|
||||||
|
app.AppRollbackCommand,
|
||||||
|
app.AppMoveCommand,
|
||||||
|
app.AppRunCommand,
|
||||||
|
app.AppSecretCommand,
|
||||||
|
app.AppServicesCommand,
|
||||||
|
app.AppUndeployCommand,
|
||||||
|
app.AppUpgradeCommand,
|
||||||
|
app.AppVolumeCommand,
|
||||||
|
app.AppLabelsCommand,
|
||||||
|
app.AppEnvCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,51 +1,148 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||||
"coopcloud.tech/abra/pkg/dns"
|
"coopcloud.tech/abra/pkg/dns"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/server"
|
"coopcloud.tech/abra/pkg/server"
|
||||||
sshPkg "coopcloud.tech/abra/pkg/ssh"
|
sshPkg "coopcloud.tech/abra/pkg/ssh"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var local bool
|
var ServerAddCommand = &cobra.Command{
|
||||||
var localFlag = &cli.BoolFlag{
|
Use: "add [[server] | --local] [flags]",
|
||||||
Name: "local, l",
|
Aliases: []string{"a"},
|
||||||
Usage: "Use local server",
|
Short: "Add a new server",
|
||||||
Destination: &local,
|
Long: `Add a new server to your configuration so that it can be managed by Abra.
|
||||||
|
|
||||||
|
Abra relies on the standard SSH command-line and ~/.ssh/config for client
|
||||||
|
connection details. You must configure an entry per-host in your ~/.ssh/config
|
||||||
|
for each server:
|
||||||
|
|
||||||
|
Host 1312.net 1312
|
||||||
|
Hostname 1312.net
|
||||||
|
User antifa
|
||||||
|
Port 12345
|
||||||
|
IdentityFile ~/.ssh/antifa@somewhere
|
||||||
|
|
||||||
|
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. The domain is then set to "default".`,
|
||||||
|
Example: " abra server add 1312.net",
|
||||||
|
Args: cobra.RangeArgs(0, 1),
|
||||||
|
ValidArgsFunction: func(
|
||||||
|
cmd *cobra.Command,
|
||||||
|
args []string,
|
||||||
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if !local {
|
||||||
|
return autocomplete.ServerNameComplete()
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if len(args) > 0 && local {
|
||||||
|
log.Fatal("cannot use [server] and --local together")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 && !local {
|
||||||
|
log.Fatal("missing argument or --local/-l flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
name := "default"
|
||||||
|
if !local {
|
||||||
|
name = internal.ValidateDomain(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(d1): reasonable 5 second timeout for connections which can't
|
||||||
|
// succeed. The connection is attempted twice, so this results in 10
|
||||||
|
// seconds.
|
||||||
|
timeout := client.WithTimeout(5)
|
||||||
|
|
||||||
|
if local {
|
||||||
|
created, err := createServerDir(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("attempting to create client for %s", name)
|
||||||
|
|
||||||
|
if _, err := client.New(name, timeout); err != nil {
|
||||||
|
cleanUp(name)
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if created {
|
||||||
|
log.Info("local server successfully added")
|
||||||
|
} else {
|
||||||
|
log.Warn("local server already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := createServerDir(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
created, err := newContext(name)
|
||||||
|
if err != nil {
|
||||||
|
cleanUp(name)
|
||||||
|
log.Fatalf("unable to create local context: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("attempting to create client for %s", name)
|
||||||
|
|
||||||
|
if _, err := client.New(name, timeout); err != nil {
|
||||||
|
cleanUp(name)
|
||||||
|
log.Fatalf("ssh %s error: %s", name, sshPkg.Fatal(name, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if created {
|
||||||
|
log.Infof("%s successfully added", name)
|
||||||
|
|
||||||
|
if _, err := dns.EnsureIPv4(name); err != nil {
|
||||||
|
log.Warnf("unable to resolve IPv4 for %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("%s already exists", name)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanUp(domainName string) {
|
// cleanUp cleans up the partially created context/client details for a failed
|
||||||
if domainName != "default" {
|
// "server add" attempt.
|
||||||
logrus.Infof("cleaning up context for %s", domainName)
|
func cleanUp(name string) {
|
||||||
if err := client.DeleteContext(domainName); err != nil {
|
if name != "default" {
|
||||||
logrus.Fatal(err)
|
log.Debugf("serverAdd: cleanUp: cleaning up context for %s", name)
|
||||||
|
if err := client.DeleteContext(name); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("attempting to clean up server directory for %s", domainName)
|
serverDir := filepath.Join(config.SERVERS_DIR, name)
|
||||||
|
|
||||||
serverDir := filepath.Join(config.SERVERS_DIR, domainName)
|
|
||||||
files, err := config.GetAllFilesInDirectory(serverDir)
|
files, err := config.GetAllFilesInDirectory(serverDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("unable to list files in %s: %s", serverDir, err)
|
log.Fatalf("serverAdd: cleanUp: unable to list files in %s: %s", serverDir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(files) > 0 {
|
if len(files) > 0 {
|
||||||
logrus.Warnf("aborting clean up of %s because it is not empty", serverDir)
|
log.Debugf("serverAdd: cleanUp: %s is not empty, aborting cleanup", serverDir)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.RemoveAll(serverDir); err != nil {
|
if err := os.RemoveAll(serverDir); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatalf("serverAdd: cleanUp: failed to remove %s: %s", serverDir, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,129 +150,54 @@ func cleanUp(domainName string) {
|
|||||||
// Docker manages SSH connection details. These are stored to disk in
|
// Docker manages SSH connection details. These are stored to disk in
|
||||||
// ~/.docker. Abra can manage this completely for the user, so it's an
|
// ~/.docker. Abra can manage this completely for the user, so it's an
|
||||||
// implementation detail.
|
// implementation detail.
|
||||||
func newContext(c *cli.Context, domainName, username, port string) error {
|
func newContext(name string) (bool, error) {
|
||||||
store := contextPkg.NewDefaultDockerContextStore()
|
store := contextPkg.NewDefaultDockerContextStore()
|
||||||
contexts, err := store.Store.List()
|
contexts, err := store.Store.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, context := range contexts {
|
for _, context := range contexts {
|
||||||
if context.Name == domainName {
|
if context.Name == name {
|
||||||
logrus.Debugf("context for %s already exists", domainName)
|
log.Debugf("context for %s already exists", name)
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("creating context with domain %s, username %s and port %s", domainName, username, port)
|
log.Debugf("creating context with domain %s", name)
|
||||||
|
|
||||||
if err := client.CreateContext(domainName, username, port); err != nil {
|
if err := client.CreateContext(name); err != nil {
|
||||||
return err
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createServerDir creates the ~/.abra/servers/... directory for a new server.
|
// createServerDir creates the ~/.abra/servers/... directory for a new server.
|
||||||
func createServerDir(domainName string) error {
|
func createServerDir(name string) (bool, error) {
|
||||||
if err := server.CreateServerDir(domainName); err != nil {
|
if err := server.CreateServerDir(name); err != nil {
|
||||||
if !os.IsExist(err) {
|
if !os.IsExist(err) {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
logrus.Debugf("server dir for %s already created", domainName)
|
|
||||||
|
log.Debugf("server dir for %s already created", name)
|
||||||
|
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverAddCommand = cli.Command{
|
var (
|
||||||
Name: "add",
|
local bool
|
||||||
Aliases: []string{"a"},
|
)
|
||||||
Usage: "Add a server to your configuration",
|
|
||||||
Description: `
|
|
||||||
Add a new server to your configuration so that it can be managed by Abra.
|
|
||||||
|
|
||||||
Abra uses the SSH command-line to discover connection details for your server.
|
func init() {
|
||||||
It is advised to configure an entry per-host in your ~/.ssh/config for each
|
ServerAddCommand.Flags().BoolVarP(
|
||||||
server. For example:
|
&local,
|
||||||
|
"local",
|
||||||
Host example.com
|
"l",
|
||||||
Hostname example.com
|
false,
|
||||||
User exampleUser
|
"use local server",
|
||||||
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.
|
|
||||||
`,
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
|
||||||
internal.NoInputFlag,
|
|
||||||
localFlag,
|
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
var domainName string
|
|
||||||
if local {
|
|
||||||
domainName = "default"
|
|
||||||
} else {
|
|
||||||
domainName = internal.ValidateDomain(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if local {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := dns.EnsureIPv4(domainName); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := createServerDir(domainName); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostConfig, err := sshPkg.GetHostConfig(domainName)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := newContext(c, domainName, hostConfig.User, hostConfig.Port); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Infof("attempting to create client for %s", domainName)
|
|
||||||
if _, err := client.New(domainName); err != nil {
|
|
||||||
cleanUp(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
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,110 +1,103 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
"coopcloud.tech/abra/pkg/context"
|
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/docker/cli/cli/connhelper/ssh"
|
"github.com/docker/cli/cli/connhelper/ssh"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var problemsFilter bool
|
var ServerListCommand = &cobra.Command{
|
||||||
|
Use: "list [flags]",
|
||||||
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"},
|
Aliases: []string{"ls"},
|
||||||
Usage: "List managed servers",
|
Short: "List managed servers",
|
||||||
Flags: []cli.Flag{
|
Args: cobra.NoArgs,
|
||||||
problemsFilterFlag,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
internal.DebugFlag,
|
dockerContextStore := contextPkg.NewDefaultDockerContextStore()
|
||||||
internal.MachineReadableFlag,
|
|
||||||
internal.OfflineFlag,
|
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
dockerContextStore := context.NewDefaultDockerContextStore()
|
|
||||||
contexts, err := dockerContextStore.Store.List()
|
contexts, err := dockerContextStore.Store.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableColumns := []string{"name", "host", "user", "port"}
|
table, err := formatter.CreateTable()
|
||||||
table := formatter.CreateTable(tableColumns)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := []string{"NAME", "HOST"}
|
||||||
|
table.Headers(headers...)
|
||||||
|
|
||||||
serverNames, err := config.ReadServerNames()
|
serverNames, err := config.ReadServerNames()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rows [][]string
|
||||||
for _, serverName := range serverNames {
|
for _, serverName := range serverNames {
|
||||||
var row []string
|
var row []string
|
||||||
for _, ctx := range contexts {
|
for _, dockerCtx := range contexts {
|
||||||
endpoint, err := context.GetContextEndpoint(ctx)
|
endpoint, err := contextPkg.GetContextEndpoint(dockerCtx)
|
||||||
if err != nil && strings.Contains(err.Error(), "does not exist") {
|
if err != nil && strings.Contains(err.Error(), "does not exist") {
|
||||||
// No local context found, we can continue safely
|
// No local context found, we can continue safely
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Name == serverName {
|
if dockerCtx.Name == serverName {
|
||||||
sp, err := ssh.ParseURL(endpoint)
|
sp, err := ssh.ParseURL(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sp.Host == "" {
|
if sp.Host == "" {
|
||||||
sp.Host = "unknown"
|
sp.Host = "unknown"
|
||||||
}
|
}
|
||||||
if sp.User == "" {
|
|
||||||
sp.User = "unknown"
|
|
||||||
}
|
|
||||||
if sp.Port == "" {
|
|
||||||
sp.Port = "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
row = []string{serverName, sp.Host, sp.User, sp.Port}
|
row = []string{serverName, sp.Host}
|
||||||
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(row) == 0 {
|
if len(row) == 0 {
|
||||||
if serverName == "default" {
|
if serverName == "default" {
|
||||||
row = []string{serverName, "local", "n/a", "n/a"}
|
row = []string{serverName, "local"}
|
||||||
} else {
|
} else {
|
||||||
row = []string{serverName, "unknown", "unknown", "unknown"}
|
row = []string{serverName, "unknown"}
|
||||||
}
|
}
|
||||||
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
if problemsFilter {
|
table.Row(row...)
|
||||||
for _, val := range row {
|
|
||||||
if val == "unknown" {
|
|
||||||
table.Append(row)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
table.Append(row)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if internal.MachineReadable {
|
if internal.MachineReadable {
|
||||||
table.JSONRender()
|
out, err := formatter.ToJSON(headers, rows)
|
||||||
} else {
|
if err != nil {
|
||||||
if problemsFilter && table.NumLines() == 0 {
|
log.Fatal("unable to render to JSON: %s", err)
|
||||||
logrus.Info("all servers wired up correctly 👏")
|
|
||||||
} else {
|
|
||||||
table.Render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println(out)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if err := formatter.PrintTable(table); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ServerListCommand.Flags().BoolVarP(
|
||||||
|
&internal.MachineReadable,
|
||||||
|
"machine",
|
||||||
|
"m",
|
||||||
|
false,
|
||||||
|
"print machine-readable output",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,103 +1,102 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var allFilter bool
|
var ServerPruneCommand = &cobra.Command{
|
||||||
|
Use: "prune <server> [flags]",
|
||||||
var allFilterFlag = &cli.BoolFlag{
|
|
||||||
Name: "all, a",
|
|
||||||
Usage: "Remove all unused images not just dangling ones",
|
|
||||||
Destination: &allFilter,
|
|
||||||
}
|
|
||||||
|
|
||||||
var volumesFilter bool
|
|
||||||
|
|
||||||
var volumesFilterFlag = &cli.BoolFlag{
|
|
||||||
Name: "volumes, v",
|
|
||||||
Usage: "Prune volumes. This will remove app data, Be Careful!",
|
|
||||||
Destination: &volumesFilter,
|
|
||||||
}
|
|
||||||
|
|
||||||
var serverPruneCommand = cli.Command{
|
|
||||||
Name: "prune",
|
|
||||||
Aliases: []string{"p"},
|
Aliases: []string{"p"},
|
||||||
Usage: "Prune a managed server; Runs a docker system prune",
|
Short: "Prune resources on a server",
|
||||||
Description: `
|
Long: `Prunes unused containers, networks, and dangling images.
|
||||||
Prunes unused containers, networks, and dangling images.
|
|
||||||
|
|
||||||
If passing "-v/--volumes" then volumes not connected to a deployed app will
|
Use "--volumes/-v" to remove volumes that are not associated with a deployed
|
||||||
also be removed. This can result in unwanted data loss if not used carefully.
|
app. This can result in unwanted data loss if not used carefully.`,
|
||||||
`,
|
Args: cobra.ExactArgs(1),
|
||||||
ArgsUsage: "[<server>]",
|
ValidArgsFunction: func(
|
||||||
Flags: []cli.Flag{
|
cmd *cobra.Command,
|
||||||
allFilterFlag,
|
args []string,
|
||||||
volumesFilterFlag,
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
internal.DebugFlag,
|
return autocomplete.ServerNameComplete()
|
||||||
internal.OfflineFlag,
|
|
||||||
internal.NoInputFlag,
|
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.ServerNameComplete,
|
serverName := internal.ValidateServer(args)
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
serverName := internal.ValidateServer(c)
|
|
||||||
|
|
||||||
cl, err := client.New(serverName)
|
cl, err := client.New(serverName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var args filters.Args
|
var filterArgs filters.Args
|
||||||
|
|
||||||
ctx := context.Background()
|
cr, err := cl.ContainersPrune(cmd.Context(), filterArgs)
|
||||||
cr, err := cl.ContainersPrune(ctx, args)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
|
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
|
||||||
logrus.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
|
log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
|
||||||
|
|
||||||
nr, err := cl.NetworksPrune(ctx, args)
|
nr, err := cl.NetworksPrune(cmd.Context(), filterArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("networks pruned: %d", len(nr.NetworksDeleted))
|
log.Infof("networks pruned: %d", len(nr.NetworksDeleted))
|
||||||
|
|
||||||
pruneFilters := filters.NewArgs()
|
pruneFilters := filters.NewArgs()
|
||||||
if allFilter {
|
if allFilter {
|
||||||
logrus.Debugf("removing all images, not only dangling ones")
|
log.Debugf("removing all images, not only dangling ones")
|
||||||
pruneFilters.Add("dangling", "false")
|
pruneFilters.Add("dangling", "false")
|
||||||
}
|
}
|
||||||
|
|
||||||
ir, err := cl.ImagesPrune(ctx, pruneFilters)
|
ir, err := cl.ImagesPrune(cmd.Context(), pruneFilters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
|
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
|
||||||
logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
|
log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
|
||||||
|
|
||||||
if volumesFilter {
|
if volumesFilter {
|
||||||
vr, err := cl.VolumesPrune(ctx, args)
|
vr, err := cl.VolumesPrune(cmd.Context(), filterArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
volSpaceReclaimed := formatter.ByteCountSI(vr.SpaceReclaimed)
|
volSpaceReclaimed := formatter.ByteCountSI(vr.SpaceReclaimed)
|
||||||
logrus.Infof("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed)
|
log.Infof("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
allFilter bool
|
||||||
|
volumesFilter bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ServerPruneCommand.Flags().BoolVarP(
|
||||||
|
&allFilter,
|
||||||
|
"all",
|
||||||
|
"a",
|
||||||
|
false,
|
||||||
|
"remove all unused images",
|
||||||
|
)
|
||||||
|
|
||||||
|
ServerPruneCommand.Flags().BoolVarP(
|
||||||
|
&volumesFilter,
|
||||||
|
"volumes",
|
||||||
|
"v",
|
||||||
|
false,
|
||||||
|
"remove volumes",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -8,41 +8,39 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
"github.com/sirupsen/logrus"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/urfave/cli"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var serverRemoveCommand = cli.Command{
|
var ServerRemoveCommand = &cobra.Command{
|
||||||
Name: "remove",
|
Use: "remove <server> [flags]",
|
||||||
Aliases: []string{"rm"},
|
Aliases: []string{"rm"},
|
||||||
ArgsUsage: "<server>",
|
Short: "Remove a managed server",
|
||||||
Usage: "Remove a managed server",
|
Long: `Remove a managed server.
|
||||||
Description: `Remove a managed server.
|
|
||||||
|
|
||||||
Abra will remove the internal bookkeeping (~/.abra/servers/...) and underlying
|
Abra will remove the internal bookkeeping ($ABRA_DIR/servers/...) and
|
||||||
client connection context. This server will then be lost in time, like tears in
|
underlying client connection context. This server will then be lost in time,
|
||||||
rain.
|
like tears in rain.`,
|
||||||
`,
|
Args: cobra.ExactArgs(1),
|
||||||
Flags: []cli.Flag{
|
ValidArgsFunction: func(
|
||||||
internal.DebugFlag,
|
cmd *cobra.Command,
|
||||||
internal.NoInputFlag,
|
args []string,
|
||||||
internal.OfflineFlag,
|
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return autocomplete.ServerNameComplete()
|
||||||
},
|
},
|
||||||
Before: internal.SubCommandBefore,
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
BashComplete: autocomplete.ServerNameComplete,
|
serverName := internal.ValidateServer(args)
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
serverName := internal.ValidateServer(c)
|
|
||||||
|
|
||||||
if err := client.DeleteContext(serverName); err != nil {
|
if err := client.DeleteContext(serverName); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, serverName)); err != nil {
|
if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, serverName)); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("server at %s has been lost in time, like tears in rain", serverName)
|
log.Infof("%s is now lost in time, like tears in rain", serverName)
|
||||||
|
|
||||||
return nil
|
return
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,10 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import "github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServerCommand defines the `abra server` command and its subcommands
|
// ServerCommand defines the `abra server` command and its subcommands
|
||||||
var ServerCommand = cli.Command{
|
var ServerCommand = &cobra.Command{
|
||||||
Name: "server",
|
Use: "server [cmd] [args] [flags]",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
Usage: "Manage servers",
|
Short: "Manage servers",
|
||||||
Subcommands: []cli.Command{
|
|
||||||
serverAddCommand,
|
|
||||||
serverListCommand,
|
|
||||||
serverRemoveCommand,
|
|
||||||
serverPruneCommand,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,143 +8,128 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/envfile"
|
||||||
"coopcloud.tech/abra/pkg/lint"
|
"coopcloud.tech/abra/pkg/lint"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"coopcloud.tech/tagcmp"
|
"coopcloud.tech/tagcmp"
|
||||||
|
charmLog "github.com/charmbracelet/log"
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
dockerclient "github.com/docker/docker/client"
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const SERVER = "localhost"
|
const SERVER = "localhost"
|
||||||
|
|
||||||
var majorUpdate bool
|
// NotifyCommand checks for available upgrades.
|
||||||
var majorFlag = &cli.BoolFlag{
|
var NotifyCommand = &cobra.Command{
|
||||||
Name: "major, m",
|
Use: "notify [flags]",
|
||||||
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"},
|
Aliases: []string{"n"},
|
||||||
Usage: "Check for available upgrades",
|
Short: "Check for available upgrades",
|
||||||
Flags: []cli.Flag{
|
Long: `Notify on new versions for deployed apps.
|
||||||
internal.DebugFlag,
|
|
||||||
majorFlag,
|
If a new patch/minor version is available, a notification is printed.
|
||||||
internal.OfflineFlag,
|
|
||||||
},
|
Use "--major/-m" to include new major versions.`,
|
||||||
Before: internal.SubCommandBefore,
|
Args: cobra.NoArgs,
|
||||||
Description: `
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
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")
|
cl, err := client.New("default")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stacks, err := stack.GetStacks(cl)
|
stacks, err := stack.GetStacks(cl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, stackInfo := range stacks {
|
for _, stackInfo := range stacks {
|
||||||
stackName := stackInfo.Name
|
stackName := stackInfo.Name
|
||||||
recipeName, err := getLabel(cl, stackName, "recipe")
|
recipeName, err := getLabel(cl, stackName, "recipe")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if recipeName != "" {
|
if recipeName != "" {
|
||||||
_, err = getLatestUpgrade(cl, stackName, recipeName)
|
_, err = getLatestUpgrade(cl, stackName, recipeName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpgradeApp upgrades apps.
|
// UpgradeCommand upgrades apps.
|
||||||
var UpgradeApp = cli.Command{
|
var UpgradeCommand = &cobra.Command{
|
||||||
Name: "upgrade",
|
Use: "upgrade [[stack] [recipe] | --all] [flags]",
|
||||||
Aliases: []string{"u"},
|
Aliases: []string{"u"},
|
||||||
Usage: "Upgrade apps",
|
Short: "Upgrade apps",
|
||||||
ArgsUsage: "<stack-name> <recipe>",
|
Long: `Upgrade an app by specifying stack name and recipe.
|
||||||
Flags: []cli.Flag{
|
|
||||||
internal.DebugFlag,
|
Use "--all" to upgrade every deployed app.
|
||||||
internal.ChaosFlag,
|
|
||||||
majorFlag,
|
For each app with auto updates enabled, the deployed version is compared with
|
||||||
allFlag,
|
the current recipe catalogue version. If a new patch/minor version is
|
||||||
internal.OfflineFlag,
|
available, the app is upgraded.
|
||||||
},
|
|
||||||
Before: internal.SubCommandBefore,
|
To include major versions use the "--major/-m" flag. You probably don't want
|
||||||
Description: `
|
that as it will break things. Only apps that are not deployed with "--chaos/-C"
|
||||||
Upgrade an app by specifying its stack name and recipe. By passing "--all"
|
are upgraded, to update chaos deployments use the "--chaos/-C" flag. Use it
|
||||||
instead, every deployed app is upgraded. For each apps with enabled auto
|
with care.`,
|
||||||
updates the deployed version is compared with the current recipe catalogue
|
Args: cobra.RangeArgs(0, 2),
|
||||||
version. If a new patch/minor version is available, the app is upgraded. To
|
// TODO(d1): complete stack/recipe
|
||||||
include major versions use the "--major" flag. Don't do that, it will probably
|
// ValidArgsFunction: func(
|
||||||
break things. Only apps that are not deployed with "--chaos" are upgraded, to
|
// cmd *cobra.Command,
|
||||||
update chaos deployments use the "--chaos" flag. Use it with care.
|
// args []string,
|
||||||
`,
|
// toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
Action: func(c *cli.Context) error {
|
// },
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
cl, err := client.New("default")
|
cl, err := client.New("default")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updateAll && len(args) != 2 {
|
||||||
|
log.Fatal("missing arguments or --all/-a flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !updateAll {
|
if !updateAll {
|
||||||
stackName := c.Args().Get(0)
|
stackName := args[0]
|
||||||
recipeName := c.Args().Get(1)
|
recipeName := args[1]
|
||||||
|
|
||||||
err = tryUpgrade(cl, stackName, recipeName)
|
err = tryUpgrade(cl, stackName, recipeName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stacks, err := stack.GetStacks(cl)
|
stacks, err := stack.GetStacks(cl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, stackInfo := range stacks {
|
for _, stackInfo := range stacks {
|
||||||
stackName := stackInfo.Name
|
stackName := stackInfo.Name
|
||||||
recipeName, err := getLabel(cl, stackName, "recipe")
|
recipeName, err := getLabel(cl, stackName, "recipe")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tryUpgrade(cl, stackName, recipeName)
|
err = tryUpgrade(cl, stackName, recipeName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +150,7 @@ func getLabel(cl *dockerclient.Client, stackName string, label string) (string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("no %s label found for %s", label, stackName)
|
log.Debugf("no %s label found for %s", label, stackName)
|
||||||
|
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
@ -186,13 +171,13 @@ func getBoolLabel(cl *dockerclient.Client, stackName string, label string) (bool
|
|||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("Boolean label %s could not be found for %s, set default to false.", label, stackName)
|
log.Debugf("boolean label %s could not be found for %s, set default to false.", label, stackName)
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEnv reads env variables from docker services.
|
// getEnv reads env variables from docker services.
|
||||||
func getEnv(cl *dockerclient.Client, stackName string) (config.AppEnv, error) {
|
func getEnv(cl *dockerclient.Client, stackName string) (envfile.AppEnv, error) {
|
||||||
envMap := make(map[string]string)
|
envMap := make(map[string]string)
|
||||||
filter := filters.NewArgs()
|
filter := filters.NewArgs()
|
||||||
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
|
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
|
||||||
@ -207,12 +192,12 @@ func getEnv(cl *dockerclient.Client, stackName string) (config.AppEnv, error) {
|
|||||||
for _, envString := range envList {
|
for _, envString := range envList {
|
||||||
splitString := strings.SplitN(envString, "=", 2)
|
splitString := strings.SplitN(envString, "=", 2)
|
||||||
if len(splitString) != 2 {
|
if len(splitString) != 2 {
|
||||||
logrus.Debugf("can't separate key from value: %s (this variable is probably unset)", envString)
|
log.Debugf("can't separate key from value: %s (this variable is probably unset)", envString)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
k := splitString[0]
|
k := splitString[0]
|
||||||
v := splitString[1]
|
v := splitString[1]
|
||||||
logrus.Debugf("For %s read env %s with value: %s from docker service", stackName, k, v)
|
log.Debugf("for %s read env %s with value: %s from docker service", stackName, k, v)
|
||||||
envMap[k] = v
|
envMap[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,14 +219,14 @@ func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(availableUpgrades) == 0 {
|
if len(availableUpgrades) == 0 {
|
||||||
logrus.Debugf("no available upgrades for %s", stackName)
|
log.Debugf("no available upgrades for %s", stackName)
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var chosenUpgrade string
|
var chosenUpgrade string
|
||||||
if len(availableUpgrades) > 0 {
|
if len(availableUpgrades) > 0 {
|
||||||
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
|
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
|
||||||
logrus.Infof("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade)
|
log.Infof("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade)
|
||||||
}
|
}
|
||||||
|
|
||||||
return chosenUpgrade, nil
|
return chosenUpgrade, nil
|
||||||
@ -249,22 +234,22 @@ func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName stri
|
|||||||
|
|
||||||
// getDeployedVersion returns the currently deployed version of an app.
|
// getDeployedVersion returns the currently deployed version of an app.
|
||||||
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
|
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
|
||||||
logrus.Debugf("Retrieve deployed version whether %s is already deployed", stackName)
|
log.Debugf("retrieve deployed version whether %s is already deployed", stackName)
|
||||||
|
|
||||||
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDeployed {
|
if !deployMeta.IsDeployed {
|
||||||
return "", fmt.Errorf("%s is not deployed?", stackName)
|
return "", fmt.Errorf("%s is not deployed?", stackName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if deployedVersion == "unknown" {
|
if deployMeta.Version == "unknown" {
|
||||||
return "", fmt.Errorf("failed to determine deployed version of %s", stackName)
|
return "", fmt.Errorf("failed to determine deployed version of %s", stackName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return deployedVersion, nil
|
return deployMeta.Version, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getAvailableUpgrades returns all available versions of an app that are newer
|
// getAvailableUpgrades returns all available versions of an app that are newer
|
||||||
@ -283,7 +268,7 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(versions) == 0 {
|
if len(versions) == 0 {
|
||||||
logrus.Warnf("no published releases for %s in the recipe catalogue?", recipeName)
|
log.Warnf("no published releases for %s in the recipe catalogue?", recipeName)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,34 +289,32 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || majorUpdate) {
|
if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || includeMajorUpdates) {
|
||||||
availableUpgrades = append(availableUpgrades, version)
|
availableUpgrades = append(availableUpgrades, version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("Available updates for %s: %s", stackName, availableUpgrades)
|
log.Debugf("available updates for %s: %s", stackName, availableUpgrades)
|
||||||
|
|
||||||
return availableUpgrades, nil
|
return availableUpgrades, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// processRecipeRepoVersion clones, pulls, checks out the version and lints the
|
// processRecipeRepoVersion clones, pulls, checks out the version and lints the
|
||||||
// recipe repository.
|
// recipe repository.
|
||||||
func processRecipeRepoVersion(recipeName, version string) error {
|
func processRecipeRepoVersion(r recipe.Recipe, version string) error {
|
||||||
if err := recipe.EnsureExists(recipeName); err != nil {
|
if err := r.EnsureExists(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := recipe.EnsureUpToDate(recipeName); err != nil {
|
if err := r.EnsureUpToDate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := recipe.EnsureVersion(recipeName, version); err != nil {
|
if _, err := r.EnsureVersion(version); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if r, err := recipe.Get(recipeName, internal.Offline); err != nil {
|
if err := lint.LintForErrors(r); err != nil {
|
||||||
return err
|
|
||||||
} else if err := lint.LintForErrors(r); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,15 +322,14 @@ func processRecipeRepoVersion(recipeName, version string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mergeAbraShEnv merges abra.sh env vars into the app env vars.
|
// mergeAbraShEnv merges abra.sh env vars into the app env vars.
|
||||||
func mergeAbraShEnv(recipeName string, env config.AppEnv) error {
|
func mergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error {
|
||||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipeName, "abra.sh")
|
abraShEnv, err := envfile.ReadAbraShEnvVars(recipe.AbraShPath)
|
||||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range abraShEnv {
|
for k, v := range abraShEnv {
|
||||||
logrus.Debugf("read v:%s k: %s", v, k)
|
log.Debugf("read v:%s k: %s", v, k)
|
||||||
env[k] = v
|
env[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,32 +337,33 @@ func mergeAbraShEnv(recipeName string, env config.AppEnv) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createDeployConfig merges and enriches the compose config for the deployment.
|
// createDeployConfig merges and enriches the compose config for the deployment.
|
||||||
func createDeployConfig(recipeName string, stackName string, env config.AppEnv) (*composetypes.Config, stack.Deploy, error) {
|
func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (*composetypes.Config, stack.Deploy, error) {
|
||||||
env["STACK_NAME"] = stackName
|
env["STACK_NAME"] = stackName
|
||||||
|
|
||||||
deployOpts := stack.Deploy{
|
deployOpts := stack.Deploy{
|
||||||
Namespace: stackName,
|
Namespace: stackName,
|
||||||
Prune: false,
|
Prune: false,
|
||||||
ResolveImage: stack.ResolveImageAlways,
|
ResolveImage: stack.ResolveImageAlways,
|
||||||
|
Detach: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
composeFiles, err := config.GetComposeFiles(recipeName, env)
|
composeFiles, err := r.GetComposeFiles(env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, deployOpts, err
|
return nil, deployOpts, err
|
||||||
}
|
}
|
||||||
|
|
||||||
deployOpts.Composefiles = composeFiles
|
deployOpts.Composefiles = composeFiles
|
||||||
compose, err := config.GetAppComposeConfig(stackName, deployOpts, env)
|
compose, err := appPkg.GetAppComposeConfig(stackName, deployOpts, env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, deployOpts, err
|
return nil, deployOpts, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config.ExposeAllEnv(stackName, compose, env)
|
appPkg.ExposeAllEnv(stackName, compose, env)
|
||||||
|
|
||||||
// after the upgrade the deployment won't be in chaos state anymore
|
// after the upgrade the deployment won't be in chaos state anymore
|
||||||
config.SetChaosLabel(compose, stackName, false)
|
appPkg.SetChaosLabel(compose, stackName, false)
|
||||||
config.SetRecipeLabel(compose, stackName, recipeName)
|
appPkg.SetRecipeLabel(compose, stackName, r.Name)
|
||||||
config.SetUpdateLabel(compose, stackName, env)
|
appPkg.SetUpdateLabel(compose, stackName, env)
|
||||||
|
|
||||||
return compose, deployOpts, nil
|
return compose, deployOpts, nil
|
||||||
}
|
}
|
||||||
@ -388,7 +371,7 @@ func createDeployConfig(recipeName string, stackName string, env config.AppEnv)
|
|||||||
// tryUpgrade performs the upgrade if all the requirements are fulfilled.
|
// tryUpgrade performs the upgrade if all the requirements are fulfilled.
|
||||||
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
|
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
|
||||||
if recipeName == "" {
|
if recipeName == "" {
|
||||||
logrus.Debugf("don't update %s due to missing recipe name", stackName)
|
log.Debugf("don't update %s due to missing recipe name", stackName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,7 +381,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if chaos && !internal.Chaos {
|
if chaos && !internal.Chaos {
|
||||||
logrus.Debugf("don't update %s due to chaos deployment", stackName)
|
log.Debugf("don't update %s due to chaos deployment", stackName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,7 +391,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !updatesEnabled {
|
if !updatesEnabled {
|
||||||
logrus.Debugf("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName)
|
log.Debugf("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,7 +401,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if upgradeVersion == "" {
|
if upgradeVersion == "" {
|
||||||
logrus.Debugf("don't update %s due to no new version", stackName)
|
log.Debugf("don't update %s due to no new version", stackName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,71 +411,140 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// upgrade performs all necessary steps to upgrade an app.
|
// upgrade performs all necessary steps to upgrade an app.
|
||||||
func upgrade(cl *dockerclient.Client, stackName, recipeName,
|
func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion string) error {
|
||||||
upgradeVersion string) error {
|
|
||||||
env, err := getEnv(cl, stackName)
|
env, err := getEnv(cl, stackName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
app := config.App{
|
app := appPkg.App{
|
||||||
Name: stackName,
|
Name: stackName,
|
||||||
Recipe: recipeName,
|
Recipe: recipe.Get(recipeName),
|
||||||
Server: SERVER,
|
Server: SERVER,
|
||||||
Env: env,
|
Env: env,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = processRecipeRepoVersion(recipeName, upgradeVersion); err != nil {
|
r := recipe.Get(recipeName)
|
||||||
|
|
||||||
|
if err = processRecipeRepoVersion(r, upgradeVersion); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = mergeAbraShEnv(recipeName, app.Env); err != nil {
|
if err = mergeAbraShEnv(app.Recipe, app.Env); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
compose, deployOpts, err := createDeployConfig(recipeName, stackName, app.Env)
|
compose, deployOpts, err := createDeployConfig(r, stackName, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
|
log.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
|
||||||
|
|
||||||
err = stack.RunDeploy(cl, deployOpts, compose, stackName, true)
|
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := app.Filters(true, false, serviceNames...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = stack.RunDeploy(
|
||||||
|
cl,
|
||||||
|
deployOpts,
|
||||||
|
compose,
|
||||||
|
stackName,
|
||||||
|
app.Server,
|
||||||
|
true,
|
||||||
|
f,
|
||||||
|
)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAbraApp(version, commit string) *cli.App {
|
func newKadabraApp(version, commit string) *cobra.Command {
|
||||||
app := &cli.App{
|
rootCmd := &cobra.Command{
|
||||||
Name: "kadabra",
|
Use: "kadabra [cmd] [flags]",
|
||||||
Usage: `The Co-op Cloud auto-updater
|
|
||||||
____ ____ _ _
|
|
||||||
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
|
|
||||||
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
|
|
||||||
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
|
|
||||||
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|
|
||||||
|_|
|
|
||||||
`,
|
|
||||||
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
|
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
|
||||||
Commands: []cli.Command{
|
Short: "The Co-op Cloud auto-updater 🤖 🚀",
|
||||||
Notify,
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
UpgradeApp,
|
log.Logger.SetStyles(charmLog.DefaultStyles())
|
||||||
|
charmLog.SetDefault(log.Logger)
|
||||||
|
|
||||||
|
if internal.Debug {
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
log.SetOutput(os.Stderr)
|
||||||
|
log.SetReportCaller(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("kadabra version %s, commit %s", version, commit)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Before = func(c *cli.Context) error {
|
rootCmd.PersistentFlags().BoolVarP(
|
||||||
logrus.Debugf("kadabra version %s, commit %s", version, commit)
|
&internal.Debug, "debug", "d", false,
|
||||||
return nil
|
"show debug messages",
|
||||||
}
|
)
|
||||||
|
|
||||||
return app
|
rootCmd.PersistentFlags().BoolVarP(
|
||||||
|
&internal.NoInput, "no-input", "n", false,
|
||||||
|
"toggle non-interactive mode",
|
||||||
|
)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(
|
||||||
|
NotifyCommand,
|
||||||
|
UpgradeCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
return rootCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunApp runs CLI abra app.
|
// RunApp runs CLI abra app.
|
||||||
func RunApp(version, commit string) {
|
func RunApp(version, commit string) {
|
||||||
app := newAbraApp(version, commit)
|
app := newKadabraApp(version, commit)
|
||||||
|
|
||||||
if err := app.Run(os.Args); err != nil {
|
if err := app.Execute(); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
includeMajorUpdates bool
|
||||||
|
updateAll bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
NotifyCommand.Flags().BoolVarP(
|
||||||
|
&includeMajorUpdates,
|
||||||
|
"major",
|
||||||
|
"m",
|
||||||
|
false,
|
||||||
|
"check for major updates",
|
||||||
|
)
|
||||||
|
|
||||||
|
UpgradeCommand.Flags().BoolVarP(
|
||||||
|
&internal.Chaos,
|
||||||
|
"chaos",
|
||||||
|
"C",
|
||||||
|
false,
|
||||||
|
"ignore uncommitted recipes changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
UpgradeCommand.Flags().BoolVarP(
|
||||||
|
&includeMajorUpdates,
|
||||||
|
"major",
|
||||||
|
"m",
|
||||||
|
false,
|
||||||
|
"check for major updates",
|
||||||
|
)
|
||||||
|
|
||||||
|
UpgradeCommand.Flags().BoolVarP(
|
||||||
|
&updateAll,
|
||||||
|
"all",
|
||||||
|
"a",
|
||||||
|
false,
|
||||||
|
"update all deployed apps",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
56
cli/upgrade.go
Normal file
56
cli/upgrade.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Package cli provides the interface for the command-line.
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpgradeCommand upgrades abra in-place.
|
||||||
|
var UpgradeCommand = &cobra.Command{
|
||||||
|
Use: "upgrade [flags]",
|
||||||
|
Aliases: []string{"u"},
|
||||||
|
Short: "Upgrade abra",
|
||||||
|
Long: `Upgrade abra in-place with the latest stable or release candidate.
|
||||||
|
|
||||||
|
By default, the latest stable release is downloaded.
|
||||||
|
|
||||||
|
Use "--rc/-r" to install the latest release candidate. Please bear in mind that
|
||||||
|
it may contain absolutely catastrophic deal-breaker bugs. Thank you very much
|
||||||
|
for the testing efforts 💗`,
|
||||||
|
Example: " abra upgrade --rc",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
mainURL := "https://install.abra.coopcloud.tech"
|
||||||
|
c := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
|
||||||
|
|
||||||
|
if releaseCandidate {
|
||||||
|
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
|
||||||
|
c = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("attempting to run %s", c)
|
||||||
|
|
||||||
|
if err := internal.RunCmd(c); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
releaseCandidate bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
UpgradeCommand.Flags().BoolVarP(
|
||||||
|
&releaseCandidate,
|
||||||
|
"rc",
|
||||||
|
"r",
|
||||||
|
false,
|
||||||
|
"install release candidate (may contain bugs)",
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -19,5 +19,5 @@ func main() {
|
|||||||
Commit = " "
|
Commit = " "
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.RunApp(Version, Commit)
|
cli.Run(Version, Commit)
|
||||||
}
|
}
|
||||||
|
|||||||
197
go.mod
197
go.mod
@ -1,120 +1,163 @@
|
|||||||
module coopcloud.tech/abra
|
module coopcloud.tech/abra
|
||||||
|
|
||||||
go 1.21
|
go 1.24.0
|
||||||
|
|
||||||
|
toolchain go1.24.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
|
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
|
||||||
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
|
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||||
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
|
github.com/charmbracelet/bubbletea v1.3.6
|
||||||
github.com/docker/cli v24.0.7+incompatible
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/docker/distribution v2.8.3+incompatible
|
github.com/charmbracelet/log v0.4.2
|
||||||
github.com/docker/docker v24.0.7+incompatible
|
github.com/distribution/reference v0.6.0
|
||||||
|
github.com/docker/cli v28.3.3+incompatible
|
||||||
|
github.com/docker/docker v28.3.3+incompatible
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
github.com/go-git/go-git/v5 v5.10.0
|
github.com/go-git/go-git/v5 v5.16.2
|
||||||
github.com/google/go-cmp v0.5.9
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/moby/sys/signal v0.7.0
|
github.com/leonelquinteros/gotext v1.7.2
|
||||||
github.com/moby/term v0.5.0
|
github.com/moby/sys/signal v0.7.1
|
||||||
github.com/olekukonko/tablewriter v0.0.5
|
github.com/moby/term v0.5.2
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/schollz/progressbar/v3 v3.14.1
|
github.com/schollz/progressbar/v3 v3.18.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
golang.org/x/term v0.34.0
|
||||||
gotest.tools/v3 v3.5.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
gotest.tools/v3 v3.5.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
|
||||||
github.com/BurntSushi/toml v1.0.0 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||||
github.com/Microsoft/hcsshim v0.9.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
|
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||||
|
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/distribution/reference v0.5.0 // indirect
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-metrics v0.0.1 // indirect
|
github.com/docker/go-metrics v0.0.1 // indirect
|
||||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
github.com/klauspost/compress v1.14.2 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/miekg/pkcs11 v1.0.3 // indirect
|
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/go-archive v0.1.0 // indirect
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
|
github.com/moby/sys/mountinfo v0.7.2 // indirect
|
||||||
|
github.com/moby/sys/user v0.4.0 // indirect
|
||||||
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/runc v1.1.0 // indirect
|
github.com/opencontainers/runc v1.1.13 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||||
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
|
github.com/pjbgf/sha1cd v0.4.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.42.0 // indirect
|
github.com/prometheus/common v0.65.0 // indirect
|
||||||
github.com/prometheus/procfs v0.10.1 // indirect
|
github.com/prometheus/procfs v0.17.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.4 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.7 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/mod v0.12.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
golang.org/x/net v0.17.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||||
golang.org/x/sync v0.3.0 // indirect
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
golang.org/x/term v0.14.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 // indirect
|
||||||
golang.org/x/text v0.13.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
|
||||||
golang.org/x/tools v0.13.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||||
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||||
|
golang.org/x/net v0.43.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||||
|
google.golang.org/grpc v1.74.2 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.7 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // 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/image v3.0.2+incompatible
|
||||||
github.com/containers/storage v1.38.2 // indirect
|
github.com/containers/storage v1.38.2 // indirect
|
||||||
github.com/decentral1se/passgen v1.0.1
|
github.com/decentral1se/passgen v1.0.1
|
||||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/gorilla/mux v1.8.0 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.5
|
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||||
github.com/klauspost/pgzip v1.2.6
|
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||||
github.com/moby/patternmatcher v0.5.0 // indirect
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 // indirect
|
github.com/prometheus/client_golang v1.23.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.16.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/sergi/go-diff v1.2.0 // indirect
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/spf13/cobra v1.3.0 // indirect
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/stretchr/testify v1.8.4
|
|
||||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||||
github.com/urfave/cli v1.22.9
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
|
golang.org/x/sys v0.35.0
|
||||||
golang.org/x/sys v0.14.0
|
|
||||||
)
|
)
|
||||||
|
|||||||
12
locales/default.pot
Normal file
12
locales/default.pot
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Language: \n"
|
||||||
|
"X-Generator: xgotext\n"
|
||||||
|
|
||||||
|
#: app.go:11
|
||||||
|
msgid "Manage apps"
|
||||||
|
msgstr ""
|
||||||
20
locales/es/LC_MESSAGES/default.po
Normal file
20
locales/es/LC_MESSAGES/default.po
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2025-08-04 14:15+0000\n"
|
||||||
|
"PO-Revision-Date: 2025-08-04 14:15+0000\n"
|
||||||
|
"Last-Translator: 3wordchant <3wc.coopcloud@doesthisthing.work>\n"
|
||||||
|
"Language-Team: Spanish <https://translate.coopcloud.tech/projects/"
|
||||||
|
"co-op-cloud/abra/es/>\n"
|
||||||
|
"Language: es\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: ENCODING\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
|
"X-Generator: Weblate 5.12.2\n"
|
||||||
|
|
||||||
|
#: app.go:11
|
||||||
|
msgid "Manage apps"
|
||||||
|
msgstr "Gestionar aplicaciones"
|
||||||
683
pkg/app/app.go
683
pkg/app/app.go
@ -1,42 +1,687 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
"github.com/sirupsen/logrus"
|
"coopcloud.tech/abra/pkg/envfile"
|
||||||
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
|
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||||
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/schollz/progressbar/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get retrieves an app
|
// Get retrieves an app
|
||||||
func Get(appName string) (config.App, error) {
|
func Get(appName string) (App, error) {
|
||||||
files, err := config.LoadAppFiles("")
|
files, err := LoadAppFiles("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.App{}, err
|
return App{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := config.GetApp(files, appName)
|
app, err := GetApp(files, appName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.App{}, err
|
return App{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("retrieved %s for %s", app, appName)
|
log.Debugf("loaded app %s: %s", appName, app)
|
||||||
|
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deployedServiceSpec represents a deployed service of an app.
|
// GetApp loads an apps settings, reading it from file, in preparation to use
|
||||||
type deployedServiceSpec struct {
|
// it. It should only be used when ready to use the env file to keep IO
|
||||||
Name string
|
// operations down.
|
||||||
Version string
|
func GetApp(apps AppFiles, name AppName) (App, error) {
|
||||||
|
appFile, exists := apps[name]
|
||||||
|
if !exists {
|
||||||
|
return App{}, fmt.Errorf("cannot find app with name %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := ReadAppEnvFile(appFile, name)
|
||||||
|
if err != nil {
|
||||||
|
return App{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VersionSpec represents a deployed app and associated metadata.
|
// GetApps returns a slice of Apps with their env files read from a given
|
||||||
type VersionSpec map[string]deployedServiceSpec
|
// slice of AppFiles.
|
||||||
|
func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) {
|
||||||
|
var apps []App
|
||||||
|
|
||||||
// ParseServiceName parses a $STACK_NAME_$SERVICE_NAME service label.
|
for name := range appFiles {
|
||||||
func ParseServiceName(label string) string {
|
app, err := GetApp(appFiles, name)
|
||||||
idx := strings.LastIndex(label, "_")
|
if err != nil {
|
||||||
serviceName := label[idx+1:]
|
return nil, err
|
||||||
logrus.Debugf("parsed %s as service name from %s", serviceName, label)
|
}
|
||||||
return serviceName
|
|
||||||
|
if recipeFilter != "" {
|
||||||
|
if app.Recipe.Name == recipeFilter {
|
||||||
|
apps = append(apps, app)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apps = append(apps, app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// App reprents an app with its env file read into memory
|
||||||
|
type App struct {
|
||||||
|
Name AppName
|
||||||
|
Recipe recipe.Recipe
|
||||||
|
Domain string
|
||||||
|
Env envfile.AppEnv
|
||||||
|
Server string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// String outputs a human-friendly string representation.
|
||||||
|
func (a App) String() string {
|
||||||
|
out := fmt.Sprintf("{name: %s, ", a.Name)
|
||||||
|
out += fmt.Sprintf("recipe: %s, ", a.Recipe)
|
||||||
|
out += fmt.Sprintf("domain: %s, ", a.Domain)
|
||||||
|
out += fmt.Sprintf("env %s, ", a.Env)
|
||||||
|
out += fmt.Sprintf("server %s, ", a.Server)
|
||||||
|
out += fmt.Sprintf("path %s}", a.Path)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type aliases to make code hints easier to understand
|
||||||
|
|
||||||
|
// AppName is AppName
|
||||||
|
type AppName = string
|
||||||
|
|
||||||
|
// AppFile represents app env files on disk without reading the contents
|
||||||
|
type AppFile struct {
|
||||||
|
Path string
|
||||||
|
Server string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppFiles is a slice of appfiles
|
||||||
|
type AppFiles map[AppName]AppFile
|
||||||
|
|
||||||
|
// See documentation of config.StackName
|
||||||
|
func (a App) StackName() string {
|
||||||
|
if _, exists := a.Env["STACK_NAME"]; exists {
|
||||||
|
return a.Env["STACK_NAME"]
|
||||||
|
}
|
||||||
|
|
||||||
|
stackName := StackName(a.Name)
|
||||||
|
|
||||||
|
a.Env["STACK_NAME"] = stackName
|
||||||
|
|
||||||
|
return stackName
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 StackName(appName string) string {
|
||||||
|
stackName := SanitiseAppName(appName)
|
||||||
|
|
||||||
|
if len(stackName) > config.MAX_SANITISED_APP_NAME_LENGTH {
|
||||||
|
log.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:config.MAX_SANITISED_APP_NAME_LENGTH])
|
||||||
|
stackName = stackName[:config.MAX_SANITISED_APP_NAME_LENGTH]
|
||||||
|
}
|
||||||
|
|
||||||
|
return stackName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters retrieves app filters for querying the container runtime. By default
|
||||||
|
// it filters on all services in the app. It is also possible to pass an
|
||||||
|
// otional list of service names, which get filtered instead.
|
||||||
|
//
|
||||||
|
// 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, services ...string) (filters.Args, error) {
|
||||||
|
filters := filters.NewArgs()
|
||||||
|
if len(services) > 0 {
|
||||||
|
for _, serviceName := range services {
|
||||||
|
filters.Add("name", ServiceFilter(a.StackName(), serviceName, exactMatch))
|
||||||
|
}
|
||||||
|
return filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// When not appending the service name, just add one filter for the whole
|
||||||
|
// stack.
|
||||||
|
if !appendServiceNames {
|
||||||
|
f := fmt.Sprintf("%s", a.StackName())
|
||||||
|
if exactMatch {
|
||||||
|
f = fmt.Sprintf("^%s", f)
|
||||||
|
}
|
||||||
|
filters.Add("name", f)
|
||||||
|
return filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
composeFiles, err := a.Recipe.GetComposeFiles(a.Env)
|
||||||
|
if err != nil {
|
||||||
|
return filters, err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := stack.Deploy{Composefiles: composeFiles}
|
||||||
|
compose, err := GetAppComposeConfig(a.Recipe.Name, opts, a.Env)
|
||||||
|
if err != nil {
|
||||||
|
return filters, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, service := range compose.Services {
|
||||||
|
f := ServiceFilter(a.StackName(), service.Name, exactMatch)
|
||||||
|
filters.Add("name", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceFilter creates a filter string for filtering a service in the docker
|
||||||
|
// container runtime. When exact match is true, it uses regex to match the
|
||||||
|
// string exactly.
|
||||||
|
func ServiceFilter(stack, service string, exact bool) string {
|
||||||
|
if exact {
|
||||||
|
return fmt.Sprintf("^%s_%s", stack, service)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s_%s", stack, service)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByServer sort a slice of Apps
|
||||||
|
type ByServer []App
|
||||||
|
|
||||||
|
func (a ByServer) Len() int { return len(a) }
|
||||||
|
func (a ByServer) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a ByServer) Less(i, j int) bool {
|
||||||
|
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByServerAndRecipe sort a slice of Apps
|
||||||
|
type ByServerAndRecipe []App
|
||||||
|
|
||||||
|
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].Recipe.Name) < strings.ToLower(a[j].Recipe.Name)
|
||||||
|
}
|
||||||
|
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByRecipe sort a slice of Apps
|
||||||
|
type ByRecipe []App
|
||||||
|
|
||||||
|
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.Name) < strings.ToLower(a[j].Recipe.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByName sort a slice of Apps
|
||||||
|
type ByName []App
|
||||||
|
|
||||||
|
func (a ByName) Len() int { return len(a) }
|
||||||
|
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a ByName) Less(i, j int) bool {
|
||||||
|
return strings.ToLower(a[i].Name) < strings.ToLower(a[j].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
|
||||||
|
env, err := envfile.ReadEnv(appFile.Path)
|
||||||
|
if err != nil {
|
||||||
|
return App{}, fmt.Errorf("env file for %s couldn't be read: %s", name, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := NewApp(env, name, appFile)
|
||||||
|
if err != nil {
|
||||||
|
return App{}, fmt.Errorf("env file for %s has issues: %s", name, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp creates new App object
|
||||||
|
func NewApp(env envfile.AppEnv, name string, appFile AppFile) (App, error) {
|
||||||
|
domain := env["DOMAIN"]
|
||||||
|
|
||||||
|
recipeName, exists := env["RECIPE"]
|
||||||
|
if !exists {
|
||||||
|
recipeName, exists = env["TYPE"]
|
||||||
|
if !exists {
|
||||||
|
return App{}, fmt.Errorf("%s is missing the TYPE env var?", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return App{
|
||||||
|
Name: name,
|
||||||
|
Domain: domain,
|
||||||
|
Recipe: recipe.Get(recipeName),
|
||||||
|
Env: env,
|
||||||
|
Server: appFile.Server,
|
||||||
|
Path: appFile.Path,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if servers[0] == "" {
|
||||||
|
// Empty servers flag, one string will always be passed
|
||||||
|
var err error
|
||||||
|
servers, err = config.GetAllFoldersInDirectory(config.SERVERS_DIR)
|
||||||
|
if err != nil {
|
||||||
|
return appFiles, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", "))
|
||||||
|
|
||||||
|
for _, server := range servers {
|
||||||
|
serverDir := path.Join(config.SERVERS_DIR, server)
|
||||||
|
files, err := config.GetAllFilesInDirectory(serverDir)
|
||||||
|
if err != nil {
|
||||||
|
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(config.SERVERS_DIR, server, file.Name())
|
||||||
|
appFiles[appName] = AppFile{
|
||||||
|
Path: appFilePath,
|
||||||
|
Server: server,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return appFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppServiceNames retrieves a list of app service names.
|
||||||
|
func GetAppServiceNames(appName string) ([]string, error) {
|
||||||
|
var serviceNames []string
|
||||||
|
|
||||||
|
appFiles, err := LoadAppFiles("")
|
||||||
|
if err != nil {
|
||||||
|
return serviceNames, err
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := GetApp(appFiles, appName)
|
||||||
|
if err != nil {
|
||||||
|
return serviceNames, err
|
||||||
|
}
|
||||||
|
|
||||||
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
|
if err != nil {
|
||||||
|
return serviceNames, err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := stack.Deploy{Composefiles: composeFiles}
|
||||||
|
compose, err := GetAppComposeConfig(app.Recipe.Name, opts, app.Env)
|
||||||
|
if err != nil {
|
||||||
|
return serviceNames, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, service := range compose.Services {
|
||||||
|
serviceNames = append(serviceNames, service.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceNames, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppNames retrieves a list of app names.
|
||||||
|
func GetAppNames() ([]string, error) {
|
||||||
|
var appNames []string
|
||||||
|
|
||||||
|
appFiles, err := LoadAppFiles("")
|
||||||
|
if err != nil {
|
||||||
|
return appNames, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, err := GetApps(appFiles, "")
|
||||||
|
if err != nil {
|
||||||
|
return appNames, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, app := range apps {
|
||||||
|
appNames = append(appNames, app.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return appNames, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateAppEnvSample copies the example env file for the app into the users
|
||||||
|
// env files.
|
||||||
|
func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error {
|
||||||
|
envSample, err := os.ReadFile(r.SampleEnvPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
|
||||||
|
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("%s already exists?", appEnvPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(appEnvPath, envSample, 0o664)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
read, err := os.ReadFile(appEnvPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newContents := strings.Replace(string(read), r.Name+".example.com", domain, -1)
|
||||||
|
|
||||||
|
err = os.WriteFile(appEnvPath, []byte(newContents), 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("copied & templated %s to %s", r.SampleEnvPath, appEnvPath)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(apps []App, MachineReadable bool) (map[string]map[string]string, error) {
|
||||||
|
statuses := make(map[string]map[string]string)
|
||||||
|
|
||||||
|
servers := make(map[string]struct{})
|
||||||
|
for _, app := range apps {
|
||||||
|
if _, ok := servers[app.Server]; !ok {
|
||||||
|
servers[app.Server] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
log.Warn(err)
|
||||||
|
ch <- stack.StackStatus{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(s string) {
|
||||||
|
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]
|
||||||
|
|
||||||
|
if _, ok := statuses[name]; !ok {
|
||||||
|
result["status"] = "deployed"
|
||||||
|
}
|
||||||
|
|
||||||
|
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name)
|
||||||
|
chaos, ok := service.Spec.Labels[labelKey]
|
||||||
|
if ok {
|
||||||
|
result["chaos"] = chaos
|
||||||
|
}
|
||||||
|
|
||||||
|
labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name)
|
||||||
|
if chaosVersion, ok := service.Spec.Labels[labelKey]; ok {
|
||||||
|
result["chaosVersion"] = chaosVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name)
|
||||||
|
if autoUpdate, ok := service.Spec.Labels[labelKey]; ok {
|
||||||
|
result["autoUpdate"] = autoUpdate
|
||||||
|
} else {
|
||||||
|
result["autoUpdate"] = "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
|
||||||
|
if version, ok := service.Spec.Labels[labelKey]; ok {
|
||||||
|
result["version"] = version
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses[name] = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("retrieved app statuses: %s", statuses)
|
||||||
|
|
||||||
|
return statuses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppComposeConfig retrieves a compose specification for a recipe. This
|
||||||
|
// specification is the result of a merge of all the compose.**.yml files in
|
||||||
|
// the recipe repository.
|
||||||
|
func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv) (*composetypes.Config, error) {
|
||||||
|
compose, err := loader.LoadComposefile(opts, appEnv)
|
||||||
|
if err != nil {
|
||||||
|
return &composetypes.Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("retrieved %s for %s", compose.Filename, recipe)
|
||||||
|
|
||||||
|
return compose, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExposeAllEnv exposes all env variables to the app container
|
||||||
|
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) {
|
||||||
|
for _, service := range compose.Services {
|
||||||
|
if service.Name == "app" {
|
||||||
|
log.Debugf("adding env vars to %s service config", stackName)
|
||||||
|
for k, v := range appEnv {
|
||||||
|
_, exists := service.Environment[k]
|
||||||
|
if !exists {
|
||||||
|
value := v
|
||||||
|
service.Environment[k] = &value
|
||||||
|
log.Debugf("%s: %s: %s", stackName, k, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckEnv(app App) ([]envfile.EnvVar, error) {
|
||||||
|
var envVars []envfile.EnvVar
|
||||||
|
|
||||||
|
envSample, err := app.Recipe.SampleEnv()
|
||||||
|
if err != nil {
|
||||||
|
return envVars, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys []string
|
||||||
|
for key := range envSample {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
if _, ok := app.Env[key]; ok {
|
||||||
|
envVars = append(envVars, envfile.EnvVar{Name: key, Present: true})
|
||||||
|
} else {
|
||||||
|
envVars = append(envVars, envfile.EnvVar{Name: key, Present: false})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return envVars, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAbraShCmdNames reads the names of commands.
|
||||||
|
func ReadAbraShCmdNames(abraSh string) ([]string, error) {
|
||||||
|
var cmdNames []string
|
||||||
|
|
||||||
|
file, err := os.Open(abraSh)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return cmdNames, nil
|
||||||
|
}
|
||||||
|
return cmdNames, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
cmdNameRegex, err := regexp.Compile(`(\w+)(\(\).*\{)`)
|
||||||
|
if err != nil {
|
||||||
|
return cmdNames, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
matches := cmdNameRegex.FindStringSubmatch(line)
|
||||||
|
if len(matches) > 0 {
|
||||||
|
cmdNames = append(cmdNames, matches[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cmdNames) > 0 {
|
||||||
|
log.Debugf("read %s from %s", strings.Join(cmdNames, " "), abraSh)
|
||||||
|
} else {
|
||||||
|
log.Debugf("read 0 command names from %s", abraSh)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmdNames, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wipe removes the version from the app .env file.
|
||||||
|
func (a App) WipeRecipeVersion() error {
|
||||||
|
file, err := os.Open(a.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var (
|
||||||
|
lines []string
|
||||||
|
scanner = bufio.NewScanner(file)
|
||||||
|
)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
|
||||||
|
lines = append(lines, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "#") {
|
||||||
|
lines = append(lines, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
splitted := strings.Split(line, ":")
|
||||||
|
lines = append(lines, splitted[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("version wiped from %s.env", a.Domain)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteRecipeVersion writes the recipe version to the app .env file.
|
||||||
|
func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||||
|
file, err := os.Open(a.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var (
|
||||||
|
dirtyVersion string
|
||||||
|
skipped bool
|
||||||
|
lines []string
|
||||||
|
scanner = bufio.NewScanner(file)
|
||||||
|
)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
|
||||||
|
lines = append(lines, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "#") {
|
||||||
|
lines = append(lines, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(line, version) && !a.Recipe.Dirty && !strings.HasSuffix(line, config.DIRTY_DEFAULT) {
|
||||||
|
skipped = true
|
||||||
|
lines = append(lines, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
splitted := strings.Split(line, ":")
|
||||||
|
|
||||||
|
line = fmt.Sprintf("%s:%s", splitted[0], version)
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Recipe.Dirty && dirtyVersion != "" {
|
||||||
|
version = dirtyVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dryRun {
|
||||||
|
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Debugf("skipping writing version %s because dry run", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !skipped {
|
||||||
|
log.Debugf("version %s saved to %s.env", version, a.Domain)
|
||||||
|
} else {
|
||||||
|
log.Debugf("skipping version %s write as already exists in %s.env", version, a.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package config_test
|
package app_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -6,46 +6,49 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
|
"coopcloud.tech/abra/pkg/envfile"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
|
testPkg "coopcloud.tech/abra/pkg/test"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewApp(t *testing.T) {
|
func TestNewApp(t *testing.T) {
|
||||||
app, err := config.NewApp(ExpectedAppEnv, AppName, ExpectedAppFile)
|
app, err := appPkg.NewApp(testPkg.ExpectedAppEnv, testPkg.AppName, testPkg.ExpectedAppFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(app, ExpectedApp) {
|
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
|
||||||
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, ExpectedApp)
|
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadAppEnvFile(t *testing.T) {
|
func TestReadAppEnvFile(t *testing.T) {
|
||||||
app, err := config.ReadAppEnvFile(ExpectedAppFile, AppName)
|
app, err := appPkg.ReadAppEnvFile(testPkg.ExpectedAppFile, testPkg.AppName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(app, ExpectedApp) {
|
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
|
||||||
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, ExpectedApp)
|
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetApp(t *testing.T) {
|
func TestGetApp(t *testing.T) {
|
||||||
app, err := config.GetApp(ExpectedAppFiles, AppName)
|
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(app, ExpectedApp) {
|
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
|
||||||
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, ExpectedApp)
|
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetComposeFiles(t *testing.T) {
|
func TestGetComposeFiles(t *testing.T) {
|
||||||
offline := true
|
r := recipe.Get("abra-test-recipe")
|
||||||
r, err := recipe.Get("abra-test-recipe", offline)
|
err := r.EnsureExists()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -57,32 +60,32 @@ func TestGetComposeFiles(t *testing.T) {
|
|||||||
{
|
{
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
[]string{
|
[]string{
|
||||||
fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name),
|
fmt.Sprintf("%s/compose.yml", r.Dir),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
map[string]string{"COMPOSE_FILE": "compose.yml"},
|
map[string]string{"COMPOSE_FILE": "compose.yml"},
|
||||||
[]string{
|
[]string{
|
||||||
fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name),
|
fmt.Sprintf("%s/compose.yml", r.Dir),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
map[string]string{"COMPOSE_FILE": "compose.extra_secret.yml"},
|
map[string]string{"COMPOSE_FILE": "compose.extra_secret.yml"},
|
||||||
[]string{
|
[]string{
|
||||||
fmt.Sprintf("%s/%s/compose.extra_secret.yml", config.RECIPES_DIR, r.Name),
|
fmt.Sprintf("%s/compose.extra_secret.yml", r.Dir),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
map[string]string{"COMPOSE_FILE": "compose.yml:compose.extra_secret.yml"},
|
map[string]string{"COMPOSE_FILE": "compose.yml:compose.extra_secret.yml"},
|
||||||
[]string{
|
[]string{
|
||||||
fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name),
|
fmt.Sprintf("%s/compose.yml", r.Dir),
|
||||||
fmt.Sprintf("%s/%s/compose.extra_secret.yml", config.RECIPES_DIR, r.Name),
|
fmt.Sprintf("%s/compose.extra_secret.yml", r.Dir),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
composeFiles, err := config.GetComposeFiles(r.Name, test.appEnv)
|
composeFiles, err := r.GetComposeFiles(test.appEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -91,8 +94,8 @@ func TestGetComposeFiles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetComposeFilesError(t *testing.T) {
|
func TestGetComposeFilesError(t *testing.T) {
|
||||||
offline := true
|
r := recipe.Get("abra-test-recipe")
|
||||||
r, err := recipe.Get("abra-test-recipe", offline)
|
err := r.EnsureExists()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -103,7 +106,7 @@ func TestGetComposeFilesError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
_, err := config.GetComposeFiles(r.Name, test.appEnv)
|
_, err := r.GetComposeFiles(test.appEnv)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("should have failed: %v", test.appEnv)
|
t.Fatalf("should have failed: %v", test.appEnv)
|
||||||
}
|
}
|
||||||
@ -112,16 +115,16 @@ func TestGetComposeFilesError(t *testing.T) {
|
|||||||
|
|
||||||
func TestFilters(t *testing.T) {
|
func TestFilters(t *testing.T) {
|
||||||
oldDir := config.RECIPES_DIR
|
oldDir := config.RECIPES_DIR
|
||||||
config.RECIPES_DIR = "./testdir"
|
config.RECIPES_DIR = "./testdata"
|
||||||
defer func() {
|
defer func() {
|
||||||
config.RECIPES_DIR = oldDir
|
config.RECIPES_DIR = oldDir
|
||||||
}()
|
}()
|
||||||
|
|
||||||
app, err := config.NewApp(config.AppEnv{
|
app, err := appPkg.NewApp(envfile.AppEnv{
|
||||||
"DOMAIN": "test.example.com",
|
"DOMAIN": "test.example.com",
|
||||||
"RECIPE": "test-recipe",
|
"RECIPE": "test-recipe",
|
||||||
}, "test_example_com", config.AppFile{
|
}, "test_example_com", appPkg.AppFile{
|
||||||
Path: "./testdir/filtertest.end",
|
Path: "./testdata/filtertest.end",
|
||||||
Server: "local",
|
Server: "local",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -195,3 +198,29 @@ func compareFilter(t *testing.T, f1 filters.Args, f2 map[string]map[string]bool)
|
|||||||
t.Errorf("filters mismatch (-want +got):\n%s", diff)
|
t.Errorf("filters mismatch (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWriteRecipeVersionOverwrite(t *testing.T) {
|
||||||
|
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer t.Cleanup(func() {
|
||||||
|
if err := app.WipeRecipeVersion(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "", app.Recipe.EnvVersion)
|
||||||
|
|
||||||
|
if err := app.WriteRecipeVersion("foo", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err = appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "foo", app.Recipe.EnvVersion)
|
||||||
|
}
|
||||||
98
pkg/app/compose.go
Normal file
98
pkg/app/compose.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/pkg/envfile"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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" {
|
||||||
|
log.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" {
|
||||||
|
log.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChaosVersionLabel adds the label 'coop-cloud.${STACK_NAME}.chaos-version=$(GIT_COMMIT)' to the app container
|
||||||
|
func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) {
|
||||||
|
for _, service := range compose.Services {
|
||||||
|
if service.Name == "app" {
|
||||||
|
log.Debugf("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName)
|
||||||
|
labelKey := fmt.Sprintf("coop-cloud.%s.chaos-version", stackName)
|
||||||
|
service.Deploy.Labels[labelKey] = chaosVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetVersionLabel(compose *composetypes.Config, stackName string, version string) {
|
||||||
|
for _, service := range compose.Services {
|
||||||
|
if service.Name == "app" {
|
||||||
|
log.Debugf("set label 'coop-cloud.%s.version' to %v for %s", stackName, version, stackName)
|
||||||
|
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
|
||||||
|
service.Deploy.Labels[labelKey] = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 envfile.AppEnv) {
|
||||||
|
for _, service := range compose.Services {
|
||||||
|
if service.Name == "app" {
|
||||||
|
enable_auto_update, exists := appEnv["ENABLE_AUTO_UPDATE"]
|
||||||
|
if !exists {
|
||||||
|
enable_auto_update = "false"
|
||||||
|
}
|
||||||
|
log.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
|
||||||
|
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
|
||||||
|
for _, service := range compose.Services {
|
||||||
|
if service.Name == "app" {
|
||||||
|
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
|
||||||
|
log.Debugf("get label '%s'", labelKey)
|
||||||
|
if labelValue, ok := service.Deploy.Labels[labelKey]; ok {
|
||||||
|
return labelValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("no %s label found for %s", label, stackName)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeoutFromLabel reads the timeout value from docker label "coop-cloud.${STACK_NAME}.TIMEOUT" and returns 50 as default value
|
||||||
|
func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) {
|
||||||
|
timeout := 50 // Default Timeout
|
||||||
|
var err error = nil
|
||||||
|
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
|
||||||
|
log.Debugf("timeout label: %s", timeoutLabel)
|
||||||
|
timeout, err = strconv.Atoi(timeoutLabel)
|
||||||
|
}
|
||||||
|
return timeout, err
|
||||||
|
}
|
||||||
@ -2,101 +2,123 @@ package autocomplete
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/app"
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/spf13/cobra"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AppNameComplete copletes app names.
|
// AppNameComplete copletes app names.
|
||||||
func AppNameComplete(c *cli.Context) {
|
func AppNameComplete() ([]string, cobra.ShellCompDirective) {
|
||||||
appNames, err := config.GetAppNames()
|
appFiles, err := app.LoadAppFiles("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warn(err)
|
err := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
|
return []string{err}, cobra.ShellCompDirectiveError
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.NArg() > 0 {
|
var appNames []string
|
||||||
return
|
for appName := range appFiles {
|
||||||
|
appNames = append(appNames, appName)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range appNames {
|
return appNames, cobra.ShellCompDirectiveDefault
|
||||||
fmt.Println(a)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServiceNameComplete(appName string) {
|
func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
|
||||||
serviceNames, err := config.GetAppServiceNames(appName)
|
serviceNames, err := app.GetAppServiceNames(appName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
err := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
}
|
return []string{err}, cobra.ShellCompDirectiveError
|
||||||
for _, s := range serviceNames {
|
|
||||||
fmt.Println(s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return serviceNames, cobra.ShellCompDirectiveDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecipeNameComplete completes recipe names.
|
// RecipeNameComplete completes recipe names.
|
||||||
func RecipeNameComplete(c *cli.Context) {
|
func RecipeNameComplete() ([]string, cobra.ShellCompDirective) {
|
||||||
catl, err := recipe.ReadRecipeCatalogue(false)
|
catl, err := recipe.ReadRecipeCatalogue(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warn(err)
|
err := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
}
|
return []string{err}, cobra.ShellCompDirectiveError
|
||||||
|
|
||||||
if c.NArg() > 0 {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var recipeNames []string
|
||||||
for name := range catl {
|
for name := range catl {
|
||||||
fmt.Println(name)
|
recipeNames = append(recipeNames, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return recipeNames, cobra.ShellCompDirectiveDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecipeVersionComplete completes versions for the recipe.
|
// RecipeVersionComplete completes versions for the recipe.
|
||||||
func RecipeVersionComplete(recipeName string) {
|
func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
|
||||||
catl, err := recipe.ReadRecipeCatalogue(false)
|
catl, err := recipe.ReadRecipeCatalogue(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warn(err)
|
err := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
|
return []string{err}, cobra.ShellCompDirectiveError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var recipeVersions []string
|
||||||
for _, v := range catl[recipeName].Versions {
|
for _, v := range catl[recipeName].Versions {
|
||||||
for v2 := range v {
|
for v2 := range v {
|
||||||
fmt.Println(v2)
|
recipeVersions = append(recipeVersions, v2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return recipeVersions, cobra.ShellCompDirectiveDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerNameComplete completes server names.
|
// ServerNameComplete completes server names.
|
||||||
func ServerNameComplete(c *cli.Context) {
|
func ServerNameComplete() ([]string, cobra.ShellCompDirective) {
|
||||||
files, err := config.LoadAppFiles("")
|
files, err := app.LoadAppFiles("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
err := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
}
|
return []string{err}, cobra.ShellCompDirectiveError
|
||||||
|
|
||||||
if c.NArg() > 0 {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var serverNames []string
|
||||||
for _, appFile := range files {
|
for _, appFile := range files {
|
||||||
fmt.Println(appFile.Server)
|
serverNames = append(serverNames, appFile.Server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return serverNames, cobra.ShellCompDirectiveDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubcommandComplete completes sub-commands.
|
// CommandNameComplete completes recipe commands.
|
||||||
func SubcommandComplete(c *cli.Context) {
|
func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
|
||||||
if c.NArg() > 0 {
|
app, err := app.Get(appName)
|
||||||
return
|
if err != nil {
|
||||||
|
err := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
|
return []string{err}, cobra.ShellCompDirectiveError
|
||||||
}
|
}
|
||||||
|
|
||||||
subcmds := []string{
|
cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
|
||||||
"app",
|
if err != nil {
|
||||||
"autocomplete",
|
err := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
"catalogue",
|
return []string{err}, cobra.ShellCompDirectiveError
|
||||||
"recipe",
|
|
||||||
"server",
|
|
||||||
"upgrade",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cmd := range subcmds {
|
sort.Strings(cmdNames)
|
||||||
fmt.Println(cmd)
|
|
||||||
}
|
return cmdNames, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretsComplete completes recipe secrets.
|
||||||
|
func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
r := recipe.Get(recipeName)
|
||||||
|
|
||||||
|
config, err := r.GetComposeConfig(nil)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Sprintf("autocomplete failed: %s", err)
|
||||||
|
return []string{err}, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretNames []string
|
||||||
|
for name := range config.Secrets {
|
||||||
|
secretNames = append(secretNames, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return secretNames, cobra.ShellCompDirectiveDefault
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,21 +8,20 @@ import (
|
|||||||
|
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// EnsureCatalogue ensures that the catalogue is cloned locally & present.
|
// EnsureCatalogue ensures that the catalogue is cloned locally & present.
|
||||||
func EnsureCatalogue() error {
|
func EnsureCatalogue() error {
|
||||||
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
|
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
|
||||||
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
|
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
|
||||||
logrus.Warnf("local recipe catalogue is missing, retrieving now")
|
log.Debugf("catalogue is missing, retrieving now")
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
|
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
|
||||||
if err := gitPkg.Clone(catalogueDir, url); err != nil {
|
if err := gitPkg.Clone(catalogueDir, url); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("cloned catalogue repository to %s", catalogueDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -57,7 +56,7 @@ func EnsureUpToDate() error {
|
|||||||
|
|
||||||
if len(remotes) == 0 {
|
if len(remotes) == 0 {
|
||||||
msg := "cannot ensure %s is up-to-date, no git remotes configured"
|
msg := "cannot ensure %s is up-to-date, no git remotes configured"
|
||||||
logrus.Debugf(msg, config.CATALOGUE_DIR)
|
log.Debugf(msg, config.CATALOGUE_DIR)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +81,7 @@ func EnsureUpToDate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("fetched latest git changes for %s", config.CATALOGUE_DIR)
|
log.Debugf("fetched latest git changes for %s", config.CATALOGUE_DIR)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,17 +10,32 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
sshPkg "coopcloud.tech/abra/pkg/ssh"
|
sshPkg "coopcloud.tech/abra/pkg/ssh"
|
||||||
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
|
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Conf is a Docker client configuration.
|
||||||
|
type Conf struct {
|
||||||
|
Timeout int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opt is a Docker client option.
|
||||||
|
type Opt func(c *Conf)
|
||||||
|
|
||||||
|
// WithTimeout specifies a timeout for a Docker client.
|
||||||
|
func WithTimeout(timeout int) Opt {
|
||||||
|
return func(c *Conf) {
|
||||||
|
c.Timeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// New initiates a new Docker client. New client connections are validated so
|
// 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
|
// 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.
|
// account that you may only want the local client and not communicate via SSH.
|
||||||
// For this use-case, please pass "default" as the contextName.
|
// For this use-case, please pass "default" as the contextName.
|
||||||
func New(serverName string) (*client.Client, error) {
|
func New(serverName string, opts ...Opt) (*client.Client, error) {
|
||||||
var clientOpts []client.Opt
|
var clientOpts []client.Opt
|
||||||
|
|
||||||
if serverName != "default" {
|
if serverName != "default" {
|
||||||
@ -34,7 +49,12 @@ func New(serverName string) (*client.Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint)
|
conf := &Conf{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint, conf.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -65,7 +85,7 @@ func New(serverName string) (*client.Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("created client for %s", serverName)
|
log.Debugf("created client for %s", serverName)
|
||||||
|
|
||||||
info, err := cl.Info(context.Background())
|
info, err := cl.Info(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -75,9 +95,9 @@ func New(serverName string) (*client.Client, error) {
|
|||||||
if info.Swarm.LocalNodeState == "inactive" {
|
if info.Swarm.LocalNodeState == "inactive" {
|
||||||
if serverName != "default" {
|
if serverName != "default" {
|
||||||
return cl, fmt.Errorf("swarm mode not enabled on %s?", serverName)
|
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, errors.New("swarm mode not enabled on local server?")
|
||||||
}
|
}
|
||||||
|
|
||||||
return cl, nil
|
return cl, nil
|
||||||
|
|||||||
38
pkg/client/configs.go
Normal file
38
pkg/client/configs.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetConfigs(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]swarm.Config, error) {
|
||||||
|
configList, err := cl.ConfigList(ctx, swarm.ConfigListOptions{Filters: fs})
|
||||||
|
if err != nil {
|
||||||
|
return configList, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return configList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfigNames(configs []swarm.Config) []string {
|
||||||
|
var confNames []string
|
||||||
|
|
||||||
|
for _, conf := range configs {
|
||||||
|
confNames = append(confNames, conf.Spec.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return confNames
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveConfigs(cl *client.Client, ctx context.Context, configNames []string, force bool) error {
|
||||||
|
for _, confName := range configNames {
|
||||||
|
if err := cl.ConfigRemove(context.Background(), confName); err != nil {
|
||||||
|
return fmt.Errorf("conf %s: %s", confName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -5,28 +5,25 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/context"
|
"coopcloud.tech/abra/pkg/context"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
|
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
|
||||||
dConfig "github.com/docker/cli/cli/config"
|
dConfig "github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/context/docker"
|
"github.com/docker/cli/cli/context/docker"
|
||||||
contextStore "github.com/docker/cli/cli/context/store"
|
contextStore "github.com/docker/cli/cli/context/store"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Context = contextStore.Metadata
|
type Context = contextStore.Metadata
|
||||||
|
|
||||||
func CreateContext(contextName string, user string, port string) error {
|
// CreateContext creates a new Docker context.
|
||||||
host := contextName
|
func CreateContext(contextName string) error {
|
||||||
if user != "" {
|
host := fmt.Sprintf("ssh://%s", contextName)
|
||||||
host = fmt.Sprintf("%s@%s", user, host)
|
|
||||||
}
|
|
||||||
if port != "" {
|
|
||||||
host = fmt.Sprintf("%s:%s", host, port)
|
|
||||||
}
|
|
||||||
host = fmt.Sprintf("ssh://%s", host)
|
|
||||||
if err := createContext(contextName, host); err != nil {
|
if err := createContext(contextName, host); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logrus.Debugf("created the %s context", contextName)
|
|
||||||
|
log.Debugf("created the %s context", contextName)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/containers/image/docker"
|
"github.com/containers/image/docker"
|
||||||
"github.com/containers/image/types"
|
"github.com/containers/image/types"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetRegistryTags retrieves all tags of an image from a container registry.
|
// GetRegistryTags retrieves all tags of an image from a container registry.
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StoreSecret(cl *client.Client, secretName, secretValue, server string) error {
|
func StoreSecret(cl *client.Client, secretName, secretValue string) error {
|
||||||
ann := swarm.Annotations{Name: secretName}
|
ann := swarm.Annotations{Name: secretName}
|
||||||
spec := swarm.SecretSpec{Annotations: ann, Data: []byte(secretValue)}
|
spec := swarm.SecretSpec{Annotations: ann, Data: []byte(secretValue)}
|
||||||
|
|
||||||
|
|||||||
@ -2,15 +2,17 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/volume"
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]*volume.Volume, error) {
|
func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]*volume.Volume, error) {
|
||||||
volumeListOptions := volume.ListOptions{fs}
|
volumeListOKBody, err := cl.VolumeList(ctx, volume.ListOptions{Filters: fs})
|
||||||
volumeListOKBody, err := cl.VolumeList(ctx, volumeListOptions)
|
|
||||||
volumeList := volumeListOKBody.Volumes
|
volumeList := volumeListOKBody.Volumes
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return volumeList, err
|
return volumeList, err
|
||||||
@ -29,13 +31,32 @@ func GetVolumeNames(volumes []*volume.Volume) []string {
|
|||||||
return volumeNames
|
return volumeNames
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveVolumes(cl *client.Client, ctx context.Context, server string, volumeNames []string, force bool) error {
|
func RemoveVolumes(cl *client.Client, ctx context.Context, volumeNames []string, force bool, retries int) error {
|
||||||
for _, volName := range volumeNames {
|
for _, volName := range volumeNames {
|
||||||
err := cl.VolumeRemove(ctx, volName, force)
|
err := retryFunc(5, func() error {
|
||||||
|
return cl.VolumeRemove(context.Background(), volName, force)
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("volume %s: %s", volName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// retryFunc retries the given function for the given retries. After the nth
|
||||||
|
// retry it waits (n + 1)^2 seconds before the next retry (starting with n=0).
|
||||||
|
// It returns an error if the function still failed after the last retry.
|
||||||
|
func retryFunc(retries int, fn func() error) error {
|
||||||
|
for i := 0; i < retries; i++ {
|
||||||
|
err := fn()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if i+1 < retries {
|
||||||
|
sleep := time.Duration(i+1) * time.Duration(i+1)
|
||||||
|
log.Infof("%s: waiting %d seconds before next retry", err, sleep)
|
||||||
|
time.Sleep(sleep * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%d retries failed", retries)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package app
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -1,158 +0,0 @@
|
|||||||
package compose
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
|
||||||
"github.com/docker/distribution/reference"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UpdateTag updates an image tag in-place on file system local compose files.
|
|
||||||
func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
|
|
||||||
composeFiles, err := filepath.Glob(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("considering %s config(s) for tag update", strings.Join(composeFiles, ", "))
|
|
||||||
|
|
||||||
for _, composeFile := range composeFiles {
|
|
||||||
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
|
||||||
|
|
||||||
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
|
|
||||||
sampleEnv, err := config.ReadEnv(envSamplePath)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
compose, err := loader.LoadComposefile(opts, sampleEnv)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, service := range compose.Services {
|
|
||||||
if service.Image == "" {
|
|
||||||
continue // may be a compose.$optional.yml file
|
|
||||||
}
|
|
||||||
|
|
||||||
img, _ := reference.ParseNormalizedNamed(service.Image)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var composeTag string
|
|
||||||
switch img.(type) {
|
|
||||||
case reference.NamedTagged:
|
|
||||||
composeTag = img.(reference.NamedTagged).Tag()
|
|
||||||
default:
|
|
||||||
logrus.Debugf("unable to parse %s, skipping", img)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
composeImage := formatter.StripTagMeta(reference.Path(img))
|
|
||||||
|
|
||||||
logrus.Debugf("parsed %s from %s", composeTag, service.Image)
|
|
||||||
|
|
||||||
if image == composeImage {
|
|
||||||
bytes, err := ioutil.ReadFile(composeFile)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
old := fmt.Sprintf("%s:%s", composeImage, composeTag)
|
|
||||||
new := fmt.Sprintf("%s:%s", composeImage, tag)
|
|
||||||
replacedBytes := strings.Replace(string(bytes), old, new, -1)
|
|
||||||
|
|
||||||
logrus.Debugf("updating %s to %s in %s", old, new, compose.Filename)
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateLabel updates a label in-place on file system local compose files.
|
|
||||||
func UpdateLabel(pattern, serviceName, label, recipeName string) error {
|
|
||||||
composeFiles, err := filepath.Glob(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("considering %s config(s) for label update", strings.Join(composeFiles, ", "))
|
|
||||||
|
|
||||||
for _, composeFile := range composeFiles {
|
|
||||||
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
|
||||||
|
|
||||||
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
|
|
||||||
sampleEnv, err := config.ReadEnv(envSamplePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
compose, err := loader.LoadComposefile(opts, sampleEnv)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceExists := false
|
|
||||||
var service composetypes.ServiceConfig
|
|
||||||
for _, s := range compose.Services {
|
|
||||||
if s.Name == serviceName {
|
|
||||||
service = s
|
|
||||||
serviceExists = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !serviceExists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
discovered := false
|
|
||||||
for oldLabel, value := range service.Deploy.Labels {
|
|
||||||
if strings.HasPrefix(oldLabel, "coop-cloud") && strings.Contains(oldLabel, "version") {
|
|
||||||
discovered = true
|
|
||||||
|
|
||||||
bytes, err := ioutil.ReadFile(composeFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
|
|
||||||
replacedBytes := strings.Replace(string(bytes), old, label, -1)
|
|
||||||
|
|
||||||
if old == label {
|
|
||||||
logrus.Warnf("%s is already set, nothing to do?", label)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("updating %s to %s in %s", old, label, compose.Filename)
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Infof("synced label %s to service %s", label, serviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !discovered {
|
|
||||||
logrus.Warn("no existing label found, automagic insertion not supported yet")
|
|
||||||
logrus.Fatalf("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
126
pkg/config/abra.go
Normal file
126
pkg/config/abra.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadAbraConfig returns the abra configuration. It tries to find a abra
|
||||||
|
// configuration file (see findAbraConfig for lookup logic). When no
|
||||||
|
// configuration was found it returns the default config.
|
||||||
|
func LoadAbraConfig() Abra {
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
configFile := findAbraConfig(wd)
|
||||||
|
if configFile == "" {
|
||||||
|
log.Debugf("no config file found")
|
||||||
|
return Abra{}
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
// Do nothing, when an error occurs
|
||||||
|
log.Debugf("error reading config file: %s", err)
|
||||||
|
return Abra{}
|
||||||
|
}
|
||||||
|
|
||||||
|
config := Abra{}
|
||||||
|
err = yaml.Unmarshal(data, &config)
|
||||||
|
if err != nil {
|
||||||
|
// Do nothing, when an error occurs
|
||||||
|
log.Debugf("error loading config file: %s", err)
|
||||||
|
return Abra{}
|
||||||
|
}
|
||||||
|
log.Debugf("config file loaded from: %s", configFile)
|
||||||
|
config.configPath = filepath.Dir(configFile)
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// findAbraConfig recursively looks for a abra.y(a)ml file in the given directory.
|
||||||
|
// When the file was not found it calls the function again with the parent
|
||||||
|
// directory until the home directory is hit. When no abra config was found it
|
||||||
|
// returns an empty string.
|
||||||
|
func findAbraConfig(dir string) string {
|
||||||
|
dir, err := filepath.Abs(dir)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if dir == os.ExpandEnv("$HOME") || dir == "/" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
p := path.Join(dir, "abra.yaml")
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
p = path.Join(dir, "abra.yml")
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return findAbraConfig(filepath.Dir(dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abra defines the configuration file for abra.
|
||||||
|
type Abra struct {
|
||||||
|
configPath string
|
||||||
|
AbraDir string `yaml:"abraDir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAbraDir returns the abra dir. It has the following logic:
|
||||||
|
// 1. check if $ABRA_DIR is set
|
||||||
|
// 2. check if abraDir was set in a config file
|
||||||
|
// 3. use $HOME/.abra when above two options failed
|
||||||
|
func (a Abra) GetAbraDir() string {
|
||||||
|
if dir, exists := os.LookupEnv("ABRA_DIR"); exists && dir != "" {
|
||||||
|
log.Debug("read abra dir from $ABRA_DIR")
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
if a.AbraDir != "" {
|
||||||
|
log.Debug("read abra dir from config file")
|
||||||
|
if path.IsAbs(a.AbraDir) {
|
||||||
|
return a.AbraDir
|
||||||
|
}
|
||||||
|
// Make the path absolute
|
||||||
|
return path.Join(a.configPath, a.AbraDir)
|
||||||
|
}
|
||||||
|
log.Debug("using default abra dir")
|
||||||
|
return os.ExpandEnv("$HOME/.abra")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Abra) GetServersDir() string { return path.Join(a.GetAbraDir(), "servers") }
|
||||||
|
func (a Abra) GetRecipesDir() string { return path.Join(a.GetAbraDir(), "recipes") }
|
||||||
|
func (a Abra) GetLogsDir() string { return path.Join(a.GetAbraDir(), "logs") }
|
||||||
|
func (a Abra) GetVendorDir() string { return path.Join(a.GetAbraDir(), "vendor") }
|
||||||
|
func (a Abra) GetBackupDir() string { return path.Join(a.GetAbraDir(), "backups") }
|
||||||
|
func (a Abra) GetCatalogueDir() string { return path.Join(a.GetAbraDir(), "catalogue") }
|
||||||
|
|
||||||
|
var config = LoadAbraConfig()
|
||||||
|
|
||||||
|
var (
|
||||||
|
ABRA_DIR = config.GetAbraDir()
|
||||||
|
SERVERS_DIR = config.GetServersDir()
|
||||||
|
RECIPES_DIR = config.GetRecipesDir()
|
||||||
|
LOGS_DIR = config.GetLogsDir()
|
||||||
|
VENDOR_DIR = config.GetVendorDir()
|
||||||
|
BACKUP_DIR = config.GetBackupDir()
|
||||||
|
CATALOGUE_DIR = config.GetCatalogueDir()
|
||||||
|
RECIPES_JSON = path.Join(config.GetCatalogueDir(), "recipes.json")
|
||||||
|
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
|
||||||
|
CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
|
||||||
|
TOOLSHED_SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/toolshed/%s.git"
|
||||||
|
RECIPES_SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
|
||||||
|
|
||||||
|
// NOTE(d1): please note, this was done purely out of laziness on our part
|
||||||
|
// AFAICR. it's easy to punt the value into the label because that is what is
|
||||||
|
// expects. it's not particularly useful in terms of UI/UX but hey, nobody
|
||||||
|
// complained yet!
|
||||||
|
CHAOS_DEFAULT = "false"
|
||||||
|
|
||||||
|
DIRTY_DEFAULT = "+U"
|
||||||
|
|
||||||
|
NO_DOMAIN_DEFAULT = "N/A"
|
||||||
|
NO_VERSION_DEFAULT = "N/A"
|
||||||
|
|
||||||
|
UNKNOWN_DEFAULT = "unknown"
|
||||||
|
)
|
||||||
133
pkg/config/abra_test.go
Normal file
133
pkg/config/abra_test.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindAbraConfig(t *testing.T) {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
Dir string
|
||||||
|
Config string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Dir: "testdata/abraconfig1",
|
||||||
|
Config: filepath.Join(wd, "testdata/abraconfig1/abra.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dir: "testdata/abraconfig1/subdir",
|
||||||
|
Config: filepath.Join(wd, "testdata/abraconfig1/abra.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dir: "testdata/abraconfig2",
|
||||||
|
Config: filepath.Join(wd, "testdata/abraconfig2/abra.yml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dir: "testdata/abraconfig2/subdir",
|
||||||
|
Config: filepath.Join(wd, "testdata/abraconfig2/abra.yml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Dir: "testdata",
|
||||||
|
Config: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.Dir, func(t *testing.T) {
|
||||||
|
config := findAbraConfig(tc.Dir)
|
||||||
|
if config != tc.Config {
|
||||||
|
t.Errorf("\nwant: %s\ngot: %s", tc.Config, config)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadAbraConfigGetAbraDir(t *testing.T) {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("ABRA_DIR", "")
|
||||||
|
|
||||||
|
t.Run("default", func(t *testing.T) {
|
||||||
|
cfg := LoadAbraConfig()
|
||||||
|
wantAbraDir := os.ExpandEnv("$HOME/.abra")
|
||||||
|
if cfg.GetAbraDir() != wantAbraDir {
|
||||||
|
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("from config file", func(t *testing.T) {
|
||||||
|
t.Cleanup(func() { os.Chdir(wd) })
|
||||||
|
err = os.Chdir(filepath.Join(wd, "testdata/abraconfig1"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := LoadAbraConfig()
|
||||||
|
wantAbraDir := filepath.Join(wd, "testdata/abraconfig1/foobar")
|
||||||
|
if cfg.GetAbraDir() != wantAbraDir {
|
||||||
|
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("default when config file is empty", func(t *testing.T) {
|
||||||
|
t.Cleanup(func() { os.Chdir(wd) })
|
||||||
|
err := os.Chdir(filepath.Join(wd, "testdata/abraconfig2"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := LoadAbraConfig()
|
||||||
|
wantAbraDir := os.ExpandEnv("$HOME/.abra")
|
||||||
|
if cfg.GetAbraDir() != wantAbraDir {
|
||||||
|
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("from env variable", func(t *testing.T) {
|
||||||
|
t.Setenv("ABRA_DIR", "foo")
|
||||||
|
cfg := LoadAbraConfig()
|
||||||
|
wantAbraDir := "foo"
|
||||||
|
if cfg.GetAbraDir() != wantAbraDir {
|
||||||
|
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadAbraConfigServersDir(t *testing.T) {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("ABRA_DIR", "")
|
||||||
|
|
||||||
|
t.Run("default", func(t *testing.T) {
|
||||||
|
cfg := LoadAbraConfig()
|
||||||
|
wantServersDir := os.ExpandEnv("$HOME/.abra/servers")
|
||||||
|
if cfg.GetServersDir() != wantServersDir {
|
||||||
|
t.Errorf("\nwant: %s\ngot: %s", wantServersDir, cfg.GetServersDir())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("from config file", func(t *testing.T) {
|
||||||
|
t.Cleanup(func() { os.Chdir(wd) })
|
||||||
|
err = os.Chdir(filepath.Join(wd, "testdata/abraconfig1"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := LoadAbraConfig()
|
||||||
|
log.Println(cfg)
|
||||||
|
wantServersDir := filepath.Join(wd, "testdata/abraconfig1/foobar/servers")
|
||||||
|
if cfg.GetServersDir() != wantServersDir {
|
||||||
|
t.Errorf("\nwant: %s\ngot: %s", wantServersDir, cfg.GetServersDir())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,627 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Type aliases to make code hints easier to understand
|
|
||||||
|
|
||||||
// AppEnv is a map of the values in an apps env config
|
|
||||||
type AppEnv = map[string]string
|
|
||||||
|
|
||||||
// AppModifiers is a map of modifiers in an apps env config
|
|
||||||
type AppModifiers = map[string]map[string]string
|
|
||||||
|
|
||||||
// AppName is AppName
|
|
||||||
type AppName = string
|
|
||||||
|
|
||||||
// AppFile represents app env files on disk without reading the contents
|
|
||||||
type AppFile struct {
|
|
||||||
Path string
|
|
||||||
Server string
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppFiles is a slice of appfiles
|
|
||||||
type AppFiles map[AppName]AppFile
|
|
||||||
|
|
||||||
// App reprents an app with its env file read into memory
|
|
||||||
type App struct {
|
|
||||||
Name AppName
|
|
||||||
Recipe string
|
|
||||||
Domain string
|
|
||||||
Env AppEnv
|
|
||||||
Server string
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
// See documentation of config.StackName
|
|
||||||
func (a App) StackName() string {
|
|
||||||
if _, exists := a.Env["STACK_NAME"]; exists {
|
|
||||||
return a.Env["STACK_NAME"]
|
|
||||||
}
|
|
||||||
|
|
||||||
stackName := StackName(a.Name)
|
|
||||||
|
|
||||||
a.Env["STACK_NAME"] = stackName
|
|
||||||
|
|
||||||
return stackName
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 StackName(appName string) string {
|
|
||||||
stackName := SanitiseAppName(appName)
|
|
||||||
|
|
||||||
if len(stackName) > 45 {
|
|
||||||
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45])
|
|
||||||
stackName = stackName[:45]
|
|
||||||
}
|
|
||||||
|
|
||||||
return stackName
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filters retrieves app filters for querying the container runtime. By default
|
|
||||||
// it filters on all services in the app. It is also possible to pass an
|
|
||||||
// otional list of service names, which get filtered instead.
|
|
||||||
//
|
|
||||||
// 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, services ...string) (filters.Args, error) {
|
|
||||||
filters := filters.NewArgs()
|
|
||||||
if len(services) > 0 {
|
|
||||||
for _, serviceName := range services {
|
|
||||||
filters.Add("name", ServiceFilter(a.StackName(), serviceName, exactMatch))
|
|
||||||
}
|
|
||||||
return filters, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// When not appending the service name, just add one filter for the whole
|
|
||||||
// stack.
|
|
||||||
if !appendServiceNames {
|
|
||||||
f := fmt.Sprintf("%s", a.StackName())
|
|
||||||
if exactMatch {
|
|
||||||
f = fmt.Sprintf("^%s", f)
|
|
||||||
}
|
|
||||||
filters.Add("name", f)
|
|
||||||
return filters, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
composeFiles, err := GetComposeFiles(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 {
|
|
||||||
f := ServiceFilter(a.StackName(), service.Name, exactMatch)
|
|
||||||
filters.Add("name", f)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filters, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceFilter creates a filter string for filtering a service in the docker
|
|
||||||
// container runtime. When exact match is true, it uses regex to match the
|
|
||||||
// string exactly.
|
|
||||||
func ServiceFilter(stack, service string, exact bool) string {
|
|
||||||
if exact {
|
|
||||||
return fmt.Sprintf("^%s_%s", stack, service)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s_%s", stack, service)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByServer sort a slice of Apps
|
|
||||||
type ByServer []App
|
|
||||||
|
|
||||||
func (a ByServer) Len() int { return len(a) }
|
|
||||||
func (a ByServer) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
||||||
func (a ByServer) Less(i, j int) bool {
|
|
||||||
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByServerAndRecipe sort a slice of Apps
|
|
||||||
type ByServerAndRecipe []App
|
|
||||||
|
|
||||||
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].Recipe) < strings.ToLower(a[j].Recipe)
|
|
||||||
}
|
|
||||||
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByRecipe sort a slice of Apps
|
|
||||||
type ByRecipe []App
|
|
||||||
|
|
||||||
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
|
|
||||||
type ByName []App
|
|
||||||
|
|
||||||
func (a ByName) Len() int { return len(a) }
|
|
||||||
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
||||||
func (a ByName) Less(i, j int) bool {
|
|
||||||
return strings.ToLower(a[i].Name) < strings.ToLower(a[j].Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
|
|
||||||
env, err := ReadEnv(appFile.Path)
|
|
||||||
if err != nil {
|
|
||||||
return App{}, fmt.Errorf("env file for %s couldn't be read: %s", name, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("read env %s from %s", env, appFile.Path)
|
|
||||||
|
|
||||||
app, err := NewApp(env, name, appFile)
|
|
||||||
if err != nil {
|
|
||||||
return App{}, fmt.Errorf("env file for %s has issues: %s", name, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return app, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewApp creates new App object
|
|
||||||
func NewApp(env AppEnv, name string, appFile AppFile) (App, error) {
|
|
||||||
domain := env["DOMAIN"]
|
|
||||||
|
|
||||||
recipe, exists := env["RECIPE"]
|
|
||||||
if !exists {
|
|
||||||
recipe, exists = env["TYPE"]
|
|
||||||
if !exists {
|
|
||||||
return App{}, fmt.Errorf("%s is missing the TYPE env var?", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return App{
|
|
||||||
Name: name,
|
|
||||||
Domain: domain,
|
|
||||||
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.
|
|
||||||
func LoadAppFiles(servers ...string) (AppFiles, error) {
|
|
||||||
appFiles := make(AppFiles)
|
|
||||||
if len(servers) == 1 {
|
|
||||||
if servers[0] == "" {
|
|
||||||
// Empty servers flag, one string will always be passed
|
|
||||||
var err error
|
|
||||||
servers, err = GetAllFoldersInDirectory(SERVERS_DIR)
|
|
||||||
if err != nil {
|
|
||||||
return appFiles, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", "))
|
|
||||||
|
|
||||||
for _, server := range servers {
|
|
||||||
serverDir := path.Join(SERVERS_DIR, server)
|
|
||||||
files, err := GetAllFilesInDirectory(serverDir)
|
|
||||||
if err != nil {
|
|
||||||
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())
|
|
||||||
appFiles[appName] = AppFile{
|
|
||||||
Path: appFilePath,
|
|
||||||
Server: server,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return appFiles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
return App{}, fmt.Errorf("cannot find app with name %s", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
app, err := ReadAppEnvFile(appFile, name)
|
|
||||||
if err != nil {
|
|
||||||
return App{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return app, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
app, err := GetApp(appFiles, name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if recipeFilter != "" {
|
|
||||||
if app.Recipe == recipeFilter {
|
|
||||||
apps = append(apps, app)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
apps = append(apps, app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppServiceNames retrieves a list of app service names.
|
|
||||||
func GetAppServiceNames(appName string) ([]string, error) {
|
|
||||||
var serviceNames []string
|
|
||||||
|
|
||||||
appFiles, err := LoadAppFiles("")
|
|
||||||
if err != nil {
|
|
||||||
return serviceNames, err
|
|
||||||
}
|
|
||||||
|
|
||||||
app, err := GetApp(appFiles, appName)
|
|
||||||
if err != nil {
|
|
||||||
return serviceNames, err
|
|
||||||
}
|
|
||||||
|
|
||||||
composeFiles, err := GetComposeFiles(app.Recipe, app.Env)
|
|
||||||
if err != nil {
|
|
||||||
return serviceNames, err
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := stack.Deploy{Composefiles: composeFiles}
|
|
||||||
compose, err := GetAppComposeConfig(app.Recipe, opts, app.Env)
|
|
||||||
if err != nil {
|
|
||||||
return serviceNames, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, service := range compose.Services {
|
|
||||||
serviceNames = append(serviceNames, service.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return serviceNames, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppNames retrieves a list of app names.
|
|
||||||
func GetAppNames() ([]string, error) {
|
|
||||||
var appNames []string
|
|
||||||
|
|
||||||
appFiles, err := LoadAppFiles("")
|
|
||||||
if err != nil {
|
|
||||||
return appNames, err
|
|
||||||
}
|
|
||||||
|
|
||||||
apps, err := GetApps(appFiles, "")
|
|
||||||
if err != nil {
|
|
||||||
return appNames, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, app := range apps {
|
|
||||||
appNames = append(appNames, app.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return appNames, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
appEnvPath := path.Join(ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
|
|
||||||
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("%s already exists?", appEnvPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ioutil.WriteFile(appEnvPath, envSample, 0o664)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
read, err := ioutil.ReadFile(appEnvPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
newContents := strings.Replace(string(read), recipeName+".example.com", domain, -1)
|
|
||||||
|
|
||||||
err = ioutil.WriteFile(appEnvPath, []byte(newContents), 0)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("copied & templated %s to %s", envSamplePath, appEnvPath)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(apps []App, MachineReadable bool) (map[string]map[string]string, error) {
|
|
||||||
statuses := make(map[string]map[string]string)
|
|
||||||
|
|
||||||
servers := make(map[string]struct{})
|
|
||||||
for _, app := range apps {
|
|
||||||
if _, ok := servers[app.Server]; !ok {
|
|
||||||
servers[app.Server] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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]
|
|
||||||
|
|
||||||
if _, ok := statuses[name]; !ok {
|
|
||||||
result["status"] = "deployed"
|
|
||||||
}
|
|
||||||
|
|
||||||
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name)
|
|
||||||
chaos, ok := service.Spec.Labels[labelKey]
|
|
||||||
if ok {
|
|
||||||
result["chaos"] = chaos
|
|
||||||
}
|
|
||||||
|
|
||||||
labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name)
|
|
||||||
if chaosVersion, ok := service.Spec.Labels[labelKey]; ok {
|
|
||||||
result["chaosVersion"] = chaosVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name)
|
|
||||||
if autoUpdate, ok := service.Spec.Labels[labelKey]; ok {
|
|
||||||
result["autoUpdate"] = autoUpdate
|
|
||||||
} else {
|
|
||||||
result["autoUpdate"] = "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
|
|
||||||
if version, ok := service.Spec.Labels[labelKey]; ok {
|
|
||||||
result["version"] = version
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
statuses[name] = result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("retrieved app statuses: %s", statuses)
|
|
||||||
|
|
||||||
return statuses, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensurePathExists ensures that a path exists.
|
|
||||||
func ensurePathExists(path string) error {
|
|
||||||
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetComposeFiles gets the list of compose files for an app (or recipe if you
|
|
||||||
// don't already have an app) which should be merged into a composetypes.Config
|
|
||||||
// while respecting the COMPOSE_FILE env var.
|
|
||||||
func GetComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
|
|
||||||
var composeFiles []string
|
|
||||||
|
|
||||||
composeFileEnvVar, ok := appEnv["COMPOSE_FILE"]
|
|
||||||
if !ok {
|
|
||||||
path := fmt.Sprintf("%s/%s/compose.yml", RECIPES_DIR, recipe)
|
|
||||||
if err := ensurePathExists(path); err != nil {
|
|
||||||
return composeFiles, err
|
|
||||||
}
|
|
||||||
logrus.Debugf("no COMPOSE_FILE detected, loading default: %s", path)
|
|
||||||
composeFiles = append(composeFiles, path)
|
|
||||||
return composeFiles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(composeFileEnvVar, ":") {
|
|
||||||
path := fmt.Sprintf("%s/%s/%s", RECIPES_DIR, recipe, composeFileEnvVar)
|
|
||||||
if err := ensurePathExists(path); err != nil {
|
|
||||||
return composeFiles, err
|
|
||||||
}
|
|
||||||
logrus.Debugf("COMPOSE_FILE detected, loading %s", path)
|
|
||||||
composeFiles = append(composeFiles, path)
|
|
||||||
return composeFiles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
|
|
||||||
envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
|
|
||||||
if len(envVars) != numComposeFiles {
|
|
||||||
return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range envVars {
|
|
||||||
path := fmt.Sprintf("%s/%s/%s", RECIPES_DIR, recipe, file)
|
|
||||||
if err := ensurePathExists(path); err != nil {
|
|
||||||
return composeFiles, err
|
|
||||||
}
|
|
||||||
composeFiles = append(composeFiles, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
|
|
||||||
logrus.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), recipe)
|
|
||||||
|
|
||||||
return composeFiles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppComposeConfig retrieves a compose specification for a recipe. This
|
|
||||||
// specification is the result of a merge of all the compose.**.yml files in
|
|
||||||
// the recipe repository.
|
|
||||||
func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv AppEnv) (*composetypes.Config, error) {
|
|
||||||
compose, err := loader.LoadComposefile(opts, appEnv)
|
|
||||||
if err != nil {
|
|
||||||
return &composetypes.Config{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("retrieved %s for %s", compose.Filename, recipe)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetChaosVersionLabel adds the label 'coop-cloud.${STACK_NAME}.chaos-version=$(GIT_COMMIT)' to the app container
|
|
||||||
func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) {
|
|
||||||
for _, service := range compose.Services {
|
|
||||||
if service.Name == "app" {
|
|
||||||
logrus.Debugf("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName)
|
|
||||||
labelKey := fmt.Sprintf("coop-cloud.%s.chaos-version", stackName)
|
|
||||||
service.Deploy.Labels[labelKey] = chaosVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
|
|
||||||
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
|
|
||||||
for _, service := range compose.Services {
|
|
||||||
if service.Name == "app" {
|
|
||||||
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
|
|
||||||
logrus.Debugf("get label '%s'", labelKey)
|
|
||||||
if labelValue, ok := service.Deploy.Labels[labelKey]; ok {
|
|
||||||
return labelValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logrus.Debugf("no %s label found for %s", label, stackName)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTimeoutFromLabel reads the timeout value from docker label "coop-cloud.${STACK_NAME}.TIMEOUT" and returns 50 as default value
|
|
||||||
func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) {
|
|
||||||
timeout := 50 // Default Timeout
|
|
||||||
var err error = nil
|
|
||||||
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
|
|
||||||
logrus.Debugf("timeout label: %s", timeoutLabel)
|
|
||||||
timeout, err = strconv.Atoi(timeoutLabel)
|
|
||||||
}
|
|
||||||
return timeout, err
|
|
||||||
}
|
|
||||||
@ -1,48 +1,22 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.coopcloud.tech/coop-cloud/godotenv"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// getBaseDir retrieves the Abra base directory.
|
const MAX_SANITISED_APP_NAME_LENGTH = 45
|
||||||
func getBaseDir() string {
|
const MAX_DOCKER_SECRET_LENGTH = 64
|
||||||
home := os.ExpandEnv("$HOME/.abra")
|
|
||||||
if customAbraDir, exists := os.LookupEnv("ABRA_DIR"); exists && customAbraDir != "" {
|
|
||||||
home = customAbraDir
|
|
||||||
}
|
|
||||||
return home
|
|
||||||
}
|
|
||||||
|
|
||||||
var ABRA_DIR = getBaseDir()
|
|
||||||
var SERVERS_DIR = path.Join(ABRA_DIR, "servers")
|
|
||||||
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"
|
|
||||||
|
|
||||||
var BackupbotLabel = "coop-cloud.backupbot.enabled"
|
var BackupbotLabel = "coop-cloud.backupbot.enabled"
|
||||||
|
|
||||||
// envVarModifiers is a list of env var modifier strings. These are added to
|
|
||||||
// env vars as comments and modify their processing by Abra, e.g. determining
|
|
||||||
// how long secrets should be.
|
|
||||||
var envVarModifiers = []string{"length"}
|
|
||||||
|
|
||||||
// GetServers retrieves all servers.
|
// GetServers retrieves all servers.
|
||||||
func GetServers() ([]string, error) {
|
func GetServers() ([]string, error) {
|
||||||
var servers []string
|
var servers []string
|
||||||
@ -52,37 +26,16 @@ func GetServers() ([]string, error) {
|
|||||||
return servers, err
|
return servers, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("retrieved %v servers: %s", len(servers), servers)
|
var filtered []string
|
||||||
|
for _, s := range servers {
|
||||||
return servers, nil
|
if !strings.HasPrefix(s, ".") {
|
||||||
}
|
filtered = append(filtered, s)
|
||||||
|
}
|
||||||
// ReadEnv loads an app envivornment into a map.
|
|
||||||
func ReadEnv(filePath string) (AppEnv, error) {
|
|
||||||
var envVars AppEnv
|
|
||||||
|
|
||||||
envVars, _, err := godotenv.Read(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("read %s from %s", envVars, filePath)
|
log.Debugf("retrieved %v servers: %s", len(filtered), filtered)
|
||||||
|
|
||||||
return envVars, nil
|
return filtered, nil
|
||||||
}
|
|
||||||
|
|
||||||
// ReadEnv loads an app envivornment and their modifiers in two different maps.
|
|
||||||
func ReadEnvWithModifiers(filePath string) (AppEnv, AppModifiers, error) {
|
|
||||||
var envVars AppEnv
|
|
||||||
|
|
||||||
envVars, mods, err := godotenv.Read(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, mods, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("read %s from %s", envVars, filePath)
|
|
||||||
|
|
||||||
return envVars, mods, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadServerNames retrieves all server names.
|
// ReadServerNames retrieves all server names.
|
||||||
@ -93,7 +46,7 @@ func ReadServerNames() ([]string, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("read %s from %s", strings.Join(serverNames, ","), SERVERS_DIR)
|
log.Debugf("read %s from %s", strings.Join(serverNames, ","), SERVERS_DIR)
|
||||||
|
|
||||||
return serverNames, nil
|
return serverNames, nil
|
||||||
}
|
}
|
||||||
@ -117,7 +70,7 @@ func GetAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
|
|||||||
|
|
||||||
realPath, err := filepath.EvalSymlinks(filePath)
|
realPath, err := filepath.EvalSymlinks(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warningf("broken symlink in your abra config folders: %s", filePath)
|
log.Warnf("broken symlink in your abra config folders: %s", filePath)
|
||||||
} else {
|
} else {
|
||||||
realFile, err := os.Stat(realPath)
|
realFile, err := os.Stat(realPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -150,7 +103,7 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
|
|||||||
filePath := path.Join(directory, file.Name())
|
filePath := path.Join(directory, file.Name())
|
||||||
realDir, err := filepath.EvalSymlinks(filePath)
|
realDir, err := filepath.EvalSymlinks(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warningf("broken symlink in your abra config folders: %s", filePath)
|
log.Warnf("broken symlink in your abra config folders: %s", filePath)
|
||||||
} else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() {
|
} else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() {
|
||||||
// path is a directory
|
// path is a directory
|
||||||
folders = append(folders, file.Name())
|
folders = append(folders, file.Name())
|
||||||
@ -160,119 +113,3 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
|
|||||||
|
|
||||||
return folders, nil
|
return folders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadAbraShEnvVars reads env vars from an abra.sh recipe file.
|
|
||||||
func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
|
|
||||||
envVars := make(map[string]string)
|
|
||||||
|
|
||||||
file, err := os.Open(abraSh)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return envVars, nil
|
|
||||||
}
|
|
||||||
return envVars, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
exportRegex, err := regexp.Compile(`^export\s+(\w+=\w+)`)
|
|
||||||
if err != nil {
|
|
||||||
return envVars, err
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
txt := scanner.Text()
|
|
||||||
if exportRegex.MatchString(txt) {
|
|
||||||
splitVals := strings.Split(txt, "export ")
|
|
||||||
envVarDef := splitVals[len(splitVals)-1]
|
|
||||||
keyVal := strings.Split(envVarDef, "=")
|
|
||||||
if len(keyVal) != 2 {
|
|
||||||
return envVars, fmt.Errorf("couldn't parse %s", txt)
|
|
||||||
}
|
|
||||||
envVars[keyVal[0]] = keyVal[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(envVars) > 0 {
|
|
||||||
logrus.Debugf("read %s from %s", envVars, abraSh)
|
|
||||||
} else {
|
|
||||||
logrus.Debugf("read 0 env var exports from %s", abraSh)
|
|
||||||
}
|
|
||||||
|
|
||||||
return envVars, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type EnvVar struct {
|
|
||||||
Name string
|
|
||||||
Present bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckEnv(app App) ([]EnvVar, error) {
|
|
||||||
var envVars []EnvVar
|
|
||||||
|
|
||||||
envSamplePath := path.Join(RECIPES_DIR, app.Recipe, ".env.sample")
|
|
||||||
if _, err := os.Stat(envSamplePath); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return envVars, fmt.Errorf("%s does not exist?", envSamplePath)
|
|
||||||
}
|
|
||||||
return envVars, err
|
|
||||||
}
|
|
||||||
|
|
||||||
envSample, err := ReadEnv(envSamplePath)
|
|
||||||
if err != nil {
|
|
||||||
return envVars, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var keys []string
|
|
||||||
for key := range envSample {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
for _, key := range keys {
|
|
||||||
if _, ok := app.Env[key]; ok {
|
|
||||||
envVars = append(envVars, EnvVar{Name: key, Present: true})
|
|
||||||
} else {
|
|
||||||
envVars = append(envVars, EnvVar{Name: key, Present: false})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return envVars, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadAbraShCmdNames reads the names of commands.
|
|
||||||
func ReadAbraShCmdNames(abraSh string) ([]string, error) {
|
|
||||||
var cmdNames []string
|
|
||||||
|
|
||||||
file, err := os.Open(abraSh)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return cmdNames, nil
|
|
||||||
}
|
|
||||||
return cmdNames, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
cmdNameRegex, err := regexp.Compile(`(\w+)(\(\).*\{)`)
|
|
||||||
if err != nil {
|
|
||||||
return cmdNames, err
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
matches := cmdNameRegex.FindStringSubmatch(line)
|
|
||||||
if len(matches) > 0 {
|
|
||||||
cmdNames = append(cmdNames, matches[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cmdNames) > 0 {
|
|
||||||
logrus.Debugf("read %s from %s", strings.Join(cmdNames, " "), abraSh)
|
|
||||||
} else {
|
|
||||||
logrus.Debugf("read 0 command names from %s", abraSh)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmdNames, nil
|
|
||||||
}
|
|
||||||
|
|||||||
1
pkg/config/testdata/abraconfig1/abra.yaml
vendored
Normal file
1
pkg/config/testdata/abraconfig1/abra.yaml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
abraDir: foobar
|
||||||
0
pkg/config/testdata/abraconfig1/subdir/.gitkeep
vendored
Normal file
0
pkg/config/testdata/abraconfig1/subdir/.gitkeep
vendored
Normal file
0
pkg/config/testdata/abraconfig2/abra.yml
vendored
Normal file
0
pkg/config/testdata/abraconfig2/abra.yml
vendored
Normal file
0
pkg/config/testdata/abraconfig2/subdir/.gitkeep
vendored
Normal file
0
pkg/config/testdata/abraconfig2/subdir/.gitkeep
vendored
Normal file
@ -6,18 +6,19 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
containerTypes "github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetContainer retrieves a container. If noInput is false and the retrievd
|
// 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
|
// count of containers does not match 1, then a prompt is presented to let the
|
||||||
// user choose. A count of 0 is handled gracefully.
|
// 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) {
|
func GetContainer(c context.Context, cl *client.Client, filters filters.Args, noInput bool) (types.Container, error) {
|
||||||
containerOpts := types.ContainerListOptions{Filters: filters}
|
containerOpts := containerTypes.ListOptions{Filters: filters}
|
||||||
containers, err := cl.ContainerList(c, containerOpts)
|
containers, err := cl.ContainerList(c, containerOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.Container{}, err
|
return types.Container{}, err
|
||||||
@ -42,7 +43,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
|
|||||||
return types.Container{}, err
|
return types.Container{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Warnf("ambiguous container list received, prompting for input")
|
log.Warnf("ambiguous container list received, prompting for input")
|
||||||
|
|
||||||
var response string
|
var response string
|
||||||
prompt := &survey.Select{
|
prompt := &survey.Select{
|
||||||
@ -63,7 +64,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Panic("failed to match chosen container")
|
log.Fatal("failed to match chosen container")
|
||||||
}
|
}
|
||||||
|
|
||||||
return containers[0], nil
|
return containers[0], nil
|
||||||
|
|||||||
@ -9,12 +9,11 @@ import (
|
|||||||
func EnsureIPv4(domainName string) (string, error) {
|
func EnsureIPv4(domainName string) (string, error) {
|
||||||
ipv4, err := net.ResolveIPAddr("ip4", domainName)
|
ipv4, err := net.ResolveIPAddr("ip4", domainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", fmt.Errorf("%s: unable to resolve IPv4 address: %s", domainName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE(d1): e.g. when there is only an ipv6 record available
|
|
||||||
if ipv4 == nil {
|
if ipv4 == nil {
|
||||||
return "", fmt.Errorf("unable to resolve ipv4 address for %s", domainName)
|
return "", fmt.Errorf("%s: no IPv4 available", domainName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ipv4.String(), nil
|
return ipv4.String(), nil
|
||||||
|
|||||||
@ -17,8 +17,8 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) {
|
|||||||
// within the federation. if you're here because of a failing test, try
|
// within the federation. if you're here because of a failing test, try
|
||||||
// `dig +short <domain>` to ensure stuff matches first! If flakyness
|
// `dig +short <domain>` to ensure stuff matches first! If flakyness
|
||||||
// becomes an issue we can look into mocking
|
// becomes an issue we can look into mocking
|
||||||
|
{"docs.coopcloud.tech", "swarm-0.coopcloud.tech", true},
|
||||||
{"docs.coopcloud.tech", "coopcloud.tech", true},
|
{"docs.coopcloud.tech", "coopcloud.tech", true},
|
||||||
{"docs.coopcloud.tech", "swarm.autonomic.zone", true},
|
|
||||||
|
|
||||||
// NOTE(d1): special case handling for "--local"
|
// NOTE(d1): special case handling for "--local"
|
||||||
{"", "default", true},
|
{"", "default", true},
|
||||||
|
|||||||
90
pkg/envfile/envfile.go
Normal file
90
pkg/envfile/envfile.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package envfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
"git.coopcloud.tech/toolshed/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppEnv is a map of the values in an apps env config
|
||||||
|
type AppEnv = map[string]string
|
||||||
|
|
||||||
|
// AppModifiers is a map of modifiers in an apps env config
|
||||||
|
type AppModifiers = map[string]map[string]string
|
||||||
|
|
||||||
|
// ReadEnv loads an app envivornment into a map.
|
||||||
|
func ReadEnv(filePath string) (AppEnv, error) {
|
||||||
|
var envVars AppEnv
|
||||||
|
|
||||||
|
envVars, _, err := godotenv.Read(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return envVars, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadEnv loads an app envivornment and their modifiers in two different maps.
|
||||||
|
func ReadEnvWithModifiers(filePath string) (AppEnv, AppModifiers, error) {
|
||||||
|
var envVars AppEnv
|
||||||
|
|
||||||
|
envVars, mods, err := godotenv.Read(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, mods, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("read %s from %s", envVars, filePath)
|
||||||
|
|
||||||
|
return envVars, mods, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAbraShEnvVars reads env vars from an abra.sh recipe file.
|
||||||
|
func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
|
||||||
|
envVars := make(map[string]string)
|
||||||
|
|
||||||
|
file, err := os.Open(abraSh)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return envVars, nil
|
||||||
|
}
|
||||||
|
return envVars, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
exportRegex, err := regexp.Compile(`^export\s+(\w+=\w+)`)
|
||||||
|
if err != nil {
|
||||||
|
return envVars, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
txt := scanner.Text()
|
||||||
|
if exportRegex.MatchString(txt) {
|
||||||
|
splitVals := strings.Split(txt, "export ")
|
||||||
|
envVarDef := splitVals[len(splitVals)-1]
|
||||||
|
keyVal := strings.Split(envVarDef, "=")
|
||||||
|
if len(keyVal) != 2 {
|
||||||
|
return envVars, fmt.Errorf("couldn't parse %s", txt)
|
||||||
|
}
|
||||||
|
envVars[keyVal[0]] = keyVal[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(envVars) > 0 {
|
||||||
|
log.Debugf("read %s from %s", envVars, abraSh)
|
||||||
|
} else {
|
||||||
|
log.Debugf("read 0 env var exports from %s", abraSh)
|
||||||
|
}
|
||||||
|
|
||||||
|
return envVars, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnvVar struct {
|
||||||
|
Name string
|
||||||
|
Present bool
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user