@fluxup/install 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/bin/cli.js +137 -0
- package/package.json +37 -0
- package/src/docker.js +48 -0
- package/src/env.js +44 -0
- package/src/env.schema.js +170 -0
- package/src/ghcr.js +22 -0
- package/src/preflight.js +41 -0
- package/src/s3.js +45 -0
- package/src/secrets.js +42 -0
- package/src/wizard.js +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) 2026
|
|
2
|
+
Diovani Bernardi da Motta
|
|
3
|
+
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
This software and associated documentation files (the "Software") are the
|
|
7
|
+
exclusive property of the Authors.
|
|
8
|
+
|
|
9
|
+
No permission is granted to use, copy, modify, merge, publish, distribute,
|
|
10
|
+
sublicense, and/or sell copies of the Software, in whole or in part, without
|
|
11
|
+
explicit prior written permission from the Authors.
|
|
12
|
+
|
|
13
|
+
The Software may not be used for commercial purposes without a separate
|
|
14
|
+
written commercial agreement signed by the Authors.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
20
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
## Visao geral
|
|
2
|
+
|
|
3
|
+
Este repositório centraliza o processo de instalação da plataforma FluxUp em servidores Docker on-premises.
|
|
4
|
+
Contém a CLI de instalação interativa (`npx @fluxup/install`), o `docker-compose.yml` baseado em imagens pré-publicadas no GHCR, e a configuração de nginx que serve o frontend e faz proxy para os serviços internos.
|
|
5
|
+
|
|
6
|
+
> Contexto de negócio completo: [github.com/fluxup-platform/profile](https://github.com/fluxup-platform/profile/blob/main/README.md)
|
|
7
|
+
|
|
8
|
+
## Tecnologias
|
|
9
|
+
|
|
10
|
+
| Tecnologia | Versão mínima | Papel |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| Node.js | 18 | Runtime da CLI npx |
|
|
13
|
+
| Docker Engine | 20.10 | Execução dos containers |
|
|
14
|
+
| Docker Compose | v2 | Orquestração dos serviços |
|
|
15
|
+
| AWS SDK for JS | v3 | Download do S3 e acesso ao Secrets Manager |
|
|
16
|
+
| nginx | alpine | Reverse proxy e servidor de arquivos estáticos |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Arquitetura
|
|
21
|
+
|
|
22
|
+
O instalador opera em 8 etapas sequenciais:
|
|
23
|
+
|
|
24
|
+
1. **Preflight** — verifica `docker` e `docker compose` v2 no servidor
|
|
25
|
+
2. **Wizard** — coleta variáveis de ambiente via prompts interativos por grupo
|
|
26
|
+
3. **Secrets Manager** — busca o token GHCR no AWS Secrets Manager (em memória, nunca em disco)
|
|
27
|
+
4. **GHCR Login** — `docker login ghcr.io --password-stdin` com o token obtido
|
|
28
|
+
5. **Download S3** — baixa o `docker-compose.yml` do bucket S3 via SDK
|
|
29
|
+
6. **Geração do .env** — cria `<dir>/.env` com permissão `0600` (sem token GHCR)
|
|
30
|
+
7. **Pull + Up** — `docker compose pull` seguido de `docker compose up -d`
|
|
31
|
+
8. **Status** — exibe containers ativos e URLs de acesso
|
|
32
|
+
|
|
33
|
+
O nginx embute o build do Angular frontend como imagem GHCR. O servidor on-premises não precisa de código-fonte — apenas Docker.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Como executar a instalação
|
|
38
|
+
|
|
39
|
+
### Pré-requisitos no servidor
|
|
40
|
+
|
|
41
|
+
- Docker Engine ≥ 20.10 instalado e em execução
|
|
42
|
+
- Docker Compose v2 disponível (`docker compose version`)
|
|
43
|
+
- Node.js ≥ 18 disponível no `PATH` (apenas para o npx — pode ser removido após instalar)
|
|
44
|
+
- Acesso à internet: GHCR, bucket S3 e AWS Secrets Manager
|
|
45
|
+
|
|
46
|
+
### Execução
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx @fluxup/install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
O wizard guiará pela configuração de todas as variáveis necessárias.
|
|
53
|
+
|
|
54
|
+
### Flags disponíveis
|
|
55
|
+
|
|
56
|
+
| Flag | Descrição |
|
|
57
|
+
|------|-----------|
|
|
58
|
+
| `--dir <path>` | Diretório de instalação (padrão: `./fluxup`) |
|
|
59
|
+
| `--non-interactive` | Lê variáveis já exportadas no shell — útil para CI/automação |
|
|
60
|
+
| `--no-pull` | Pula o `docker compose pull` |
|
|
61
|
+
|
|
62
|
+
### Modo não-interativo (CI/automação)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export GHCR_USERNAME="Fluxup Deploy Bot"
|
|
66
|
+
export GHCR_SECRET_NAME="fluxup/ghcr-token"
|
|
67
|
+
export IMAGE_TAG="1.4.3"
|
|
68
|
+
export S3_BUCKET="meu-bucket"
|
|
69
|
+
export S3_KEY="releases/1.4.3/docker-compose.yml"
|
|
70
|
+
export AWS_REGION="us-east-1"
|
|
71
|
+
export AWS_ACCESS_KEY_ID="..."
|
|
72
|
+
export AWS_SECRET_ACCESS_KEY="..."
|
|
73
|
+
# ... demais variáveis
|
|
74
|
+
|
|
75
|
+
npx @fluxup/install --non-interactive --dir /opt/fluxup
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Configuração do secret GHCR no AWS Secrets Manager
|
|
81
|
+
|
|
82
|
+
Crie um secret com valor JSON:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{ "token": "ghp_..." }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
O nome do secret é configurado via `GHCR_SECRET_NAME` (padrão: `fluxup/ghcr-token`).
|
|
89
|
+
O token **nunca** é armazenado em disco — apenas em memória durante o login.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Estrutura do repositório
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
compose/docker-compose.yml — compose baseado em imagens (fonte; CI publica no S3)
|
|
97
|
+
nginx/Dockerfile — build nginx + Angular frontend (usado só pelo CI)
|
|
98
|
+
nginx/nginx.conf — configuração do nginx (proxy + SPA fallback)
|
|
99
|
+
bin/cli.js — entrypoint da CLI
|
|
100
|
+
src/
|
|
101
|
+
preflight.js — verifica pré-requisitos Docker
|
|
102
|
+
wizard.js — prompts interativos agrupados
|
|
103
|
+
env.schema.js — definição declarativa de todas as variáveis
|
|
104
|
+
secrets.js — busca token GHCR no AWS Secrets Manager
|
|
105
|
+
ghcr.js — docker login via --password-stdin
|
|
106
|
+
s3.js — download do compose do bucket S3
|
|
107
|
+
env.js — geração do .env
|
|
108
|
+
docker.js — docker compose pull / up / ps
|
|
109
|
+
.github/workflows/
|
|
110
|
+
publish-nginx.yml — publica ghcr.io/fluxup-platform/fluxup-nginx:<ver>
|
|
111
|
+
upload-compose.yml — sobe compose no S3 em cada release tag
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Release
|
|
117
|
+
|
|
118
|
+
Em cada tag `vX.Y.Z`, os workflows CI executam automaticamente:
|
|
119
|
+
|
|
120
|
+
1. `publish-nginx.yml` — builda o nginx com o frontend Angular e publica `ghcr.io/fluxup-platform/fluxup-nginx:X.Y.Z` no GHCR
|
|
121
|
+
2. `upload-compose.yml` — sobe `compose/docker-compose.yml` para `s3://<bucket>/releases/X.Y.Z/docker-compose.yml`
|
|
122
|
+
|
|
123
|
+
O instalador então usa `IMAGE_TAG=X.Y.Z` e `S3_KEY=releases/X.Y.Z/docker-compose.yml` para instalar a versão correta.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Contribuição
|
|
128
|
+
|
|
129
|
+
1. Leia o [CONTRIBUTING.md](CONTRIBUTING.md) antes de começar.
|
|
130
|
+
2. Crie uma branch de feature: `git checkout -b feature/descricao-curta`.
|
|
131
|
+
3. Valide localmente: `npm install && node bin/cli.js --help`.
|
|
132
|
+
4. Abra um pull request com descrição objetiva das mudanças.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Autores
|
|
137
|
+
|
|
138
|
+
- [Diovani Motta](https://github.com/diovanibmotta)
|
|
139
|
+
- [João Cristofoloni](https://github.com/IJhonC)
|
|
140
|
+
- [João Puel](https://github.com/joaopuel)
|
|
141
|
+
- [Jonas Magalhães](https://github.com/JonasVMagalhaes)
|
|
142
|
+
|
|
143
|
+
## Contato
|
|
144
|
+
|
|
145
|
+
Dúvidas ou sugestões: [contato@fluxup.com.br](mailto:contato@fluxup.com.br)
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* fluxup-install — FluxUp on-premises Docker installation CLI.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @fluxup/install
|
|
7
|
+
* npx @fluxup/install --dir /opt/fluxup
|
|
8
|
+
* npx @fluxup/install --non-interactive (reads env vars already exported in the shell)
|
|
9
|
+
* npx @fluxup/install --no-pull (skips docker compose pull)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join, resolve } from 'node:path';
|
|
13
|
+
import { mkdirSync } from 'node:fs';
|
|
14
|
+
import * as p from '@clack/prompts';
|
|
15
|
+
import pc from 'picocolors';
|
|
16
|
+
|
|
17
|
+
import { preflight } from '../src/preflight.js';
|
|
18
|
+
import { runWizard } from '../src/wizard.js';
|
|
19
|
+
import { fetchGhcrToken } from '../src/secrets.js';
|
|
20
|
+
import { loginGhcr } from '../src/ghcr.js';
|
|
21
|
+
import { downloadCompose } from '../src/s3.js';
|
|
22
|
+
import { writeEnvFile } from '../src/env.js';
|
|
23
|
+
import { pullImages, upServices, showStatus } from '../src/docker.js';
|
|
24
|
+
|
|
25
|
+
// ─── Flag parsing ─────────────────────────────────────────────────────────────
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const flags = {
|
|
28
|
+
dir: args[args.indexOf('--dir') + 1] ?? './fluxup',
|
|
29
|
+
nonInteractive: args.includes('--non-interactive'),
|
|
30
|
+
noPull: args.includes('--no-pull'),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const installDir = resolve(flags.dir);
|
|
34
|
+
|
|
35
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
36
|
+
async function main() {
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(pc.bgBlue(pc.white(' FluxUp Install ')));
|
|
39
|
+
console.log(pc.dim(`Install directory: ${installDir}\n`));
|
|
40
|
+
|
|
41
|
+
// 1. Preflight
|
|
42
|
+
p.log.step('Checking prerequisites...');
|
|
43
|
+
await preflight();
|
|
44
|
+
|
|
45
|
+
// 2. Wizard (or read from existing env vars in --non-interactive mode)
|
|
46
|
+
let answers;
|
|
47
|
+
if (flags.nonInteractive) {
|
|
48
|
+
p.log.info('Non-interactive mode: reading environment variables from shell.');
|
|
49
|
+
answers = Object.fromEntries(
|
|
50
|
+
process.env
|
|
51
|
+
? Object.entries(process.env).filter(([k]) => k.match(/^(GHCR|IMAGE|S3|AWS|DB|JPA|NGINX|BACKEND)/))
|
|
52
|
+
: []
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
answers = await runWizard();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. Fetch GHCR token from AWS Secrets Manager (in-memory only — never written to disk)
|
|
59
|
+
p.log.step('Fetching GHCR token from AWS Secrets Manager...');
|
|
60
|
+
let ghcrToken;
|
|
61
|
+
try {
|
|
62
|
+
ghcrToken = await fetchGhcrToken({
|
|
63
|
+
secretName: answers.GHCR_SECRET_NAME,
|
|
64
|
+
region: answers.AWS_REGION,
|
|
65
|
+
accessKeyId: answers.AWS_ACCESS_KEY_ID,
|
|
66
|
+
secretAccessKey: answers.AWS_SECRET_ACCESS_KEY,
|
|
67
|
+
});
|
|
68
|
+
p.log.success('GHCR token retrieved from Secrets Manager.');
|
|
69
|
+
} catch (err) {
|
|
70
|
+
p.log.error(`Failed to fetch GHCR token: ${err.message}`);
|
|
71
|
+
p.log.warn('Check your AWS credentials and the secret name (GHCR_SECRET_NAME).');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4. GHCR login
|
|
76
|
+
p.log.step('Authenticating with GHCR...');
|
|
77
|
+
try {
|
|
78
|
+
await loginGhcr({ username: answers.GHCR_USERNAME, token: ghcrToken });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
p.log.error(`GHCR login failed: ${err.message}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Discard token from memory
|
|
85
|
+
ghcrToken = null;
|
|
86
|
+
|
|
87
|
+
// 5. Create install directory and download docker-compose.yml from S3
|
|
88
|
+
mkdirSync(installDir, { recursive: true });
|
|
89
|
+
const composePath = join(installDir, 'docker-compose.yml');
|
|
90
|
+
|
|
91
|
+
p.log.step('Downloading docker-compose.yml from S3...');
|
|
92
|
+
try {
|
|
93
|
+
await downloadCompose({
|
|
94
|
+
bucket: answers.S3_BUCKET,
|
|
95
|
+
key: answers.S3_KEY,
|
|
96
|
+
region: answers.S3_REGION ?? answers.AWS_REGION,
|
|
97
|
+
accessKeyId: answers.S3_AWS_ACCESS_KEY_ID,
|
|
98
|
+
secretAccessKey: answers.S3_AWS_SECRET_ACCESS_KEY,
|
|
99
|
+
destPath: composePath,
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
p.log.error(`Failed to download compose from S3: ${err.message}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 6. Write .env (without GHCR token)
|
|
107
|
+
p.log.step('Generating .env file...');
|
|
108
|
+
const envPath = writeEnvFile(installDir, answers);
|
|
109
|
+
|
|
110
|
+
// 7. Pull + Up
|
|
111
|
+
try {
|
|
112
|
+
if (!flags.noPull) {
|
|
113
|
+
await pullImages(composePath, envPath);
|
|
114
|
+
}
|
|
115
|
+
await upServices(composePath, envPath);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
p.log.error(`Docker Compose failed: ${err.message}`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 8. Post-install
|
|
122
|
+
await showStatus(composePath, envPath);
|
|
123
|
+
|
|
124
|
+
const port = answers.NGINX_PORT ?? '80';
|
|
125
|
+
console.log('');
|
|
126
|
+
p.outro(
|
|
127
|
+
pc.green('✔ FluxUp installed successfully!') +
|
|
128
|
+
`\n\n Frontend: ${pc.cyan(`http://<server>:${port}/`)}\n` +
|
|
129
|
+
` API: ${pc.cyan(`http://<server>:${port}/api/`)}\n` +
|
|
130
|
+
` BFF: ${pc.cyan(`http://<server>:${port}/bff/`)}\n`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
main().catch((err) => {
|
|
135
|
+
console.error(pc.red('\nUnexpected error:'), err);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluxup/install",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "FluxUp on-premises Docker installation CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fluxup-install": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"lint": "node --check bin/cli.js src/*.js",
|
|
15
|
+
"semantic-release": "semantic-release"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@aws-sdk/client-s3": "^3.600.0",
|
|
19
|
+
"@aws-sdk/client-secrets-manager": "^3.600.0",
|
|
20
|
+
"@clack/prompts": "^0.9.0",
|
|
21
|
+
"execa": "^9.3.0",
|
|
22
|
+
"picocolors": "^1.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
26
|
+
"@semantic-release/git": "^10.0.1",
|
|
27
|
+
"@semantic-release/github": "^10.3.5",
|
|
28
|
+
"@semantic-release/npm": "^12.0.1",
|
|
29
|
+
"semantic-release": "^24.1.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/docker.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runs docker compose with the given files and arguments.
|
|
6
|
+
*/
|
|
7
|
+
function compose(composePath, envPath, args) {
|
|
8
|
+
return execa(
|
|
9
|
+
'docker',
|
|
10
|
+
['compose', '-f', composePath, '--env-file', envPath, ...args],
|
|
11
|
+
{ stdout: 'inherit', stderr: 'inherit' }
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Pulls all images declared in the compose file.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} composePath
|
|
19
|
+
* @param {string} envPath
|
|
20
|
+
*/
|
|
21
|
+
export async function pullImages(composePath, envPath) {
|
|
22
|
+
console.log(pc.bold('\nPulling images...'));
|
|
23
|
+
await compose(composePath, envPath, ['pull']);
|
|
24
|
+
console.log(pc.green('✔') + ' Pull complete.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Starts all services in detached mode.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} composePath
|
|
31
|
+
* @param {string} envPath
|
|
32
|
+
*/
|
|
33
|
+
export async function upServices(composePath, envPath) {
|
|
34
|
+
console.log(pc.bold('\nStarting containers...'));
|
|
35
|
+
await compose(composePath, envPath, ['up', '-d']);
|
|
36
|
+
console.log(pc.green('✔') + ' Containers running.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Displays container status.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} composePath
|
|
43
|
+
* @param {string} envPath
|
|
44
|
+
*/
|
|
45
|
+
export async function showStatus(composePath, envPath) {
|
|
46
|
+
console.log(pc.bold('\nContainer status:'));
|
|
47
|
+
await compose(composePath, envPath, ['ps']);
|
|
48
|
+
}
|
package/src/env.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { writeFileSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ENV_SCHEMA } from './env.schema.js';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Writes the .env file to the install directory.
|
|
8
|
+
* The GHCR token is NOT included — it is always fetched from Secrets Manager at runtime.
|
|
9
|
+
* Permission 0600 applied (owner read-only).
|
|
10
|
+
*
|
|
11
|
+
* @param {string} dir Directory where .env will be created
|
|
12
|
+
* @param {Record<string, string>} answers Wizard answers
|
|
13
|
+
* @returns {string} Path to the created .env file
|
|
14
|
+
*/
|
|
15
|
+
export function writeEnvFile(dir, answers) {
|
|
16
|
+
const envPath = join(dir, '.env');
|
|
17
|
+
|
|
18
|
+
const lines = ENV_SCHEMA
|
|
19
|
+
.filter((f) => !f.key.startsWith('GHCR_SECRET') || true) // includes GHCR_SECRET_NAME (name, not the token)
|
|
20
|
+
.map((f) => {
|
|
21
|
+
const val = answers[f.key] ?? '';
|
|
22
|
+
return `${f.key}=${val}`;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const content = [
|
|
26
|
+
'# Generated by fluxup-install — DO NOT commit',
|
|
27
|
+
'# GHCR token is not here — fetched from AWS Secrets Manager at runtime',
|
|
28
|
+
'',
|
|
29
|
+
...lines,
|
|
30
|
+
'',
|
|
31
|
+
].join('\n');
|
|
32
|
+
|
|
33
|
+
writeFileSync(envPath, content, { encoding: 'utf8' });
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
chmodSync(envPath, 0o600);
|
|
37
|
+
} catch {
|
|
38
|
+
// Windows does not support chmod — silently ignored
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(pc.green('✔') + ` .env created at ${envPath}`);
|
|
42
|
+
|
|
43
|
+
return envPath;
|
|
44
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative definition of all environment variables collected by the wizard.
|
|
3
|
+
*
|
|
4
|
+
* Fields:
|
|
5
|
+
* key — variable name in .env
|
|
6
|
+
* prompt — text displayed to the user
|
|
7
|
+
* default — default value (string) or undefined
|
|
8
|
+
* secret — true = masked input
|
|
9
|
+
* group — visual grouping in the wizard
|
|
10
|
+
*/
|
|
11
|
+
export const ENV_SCHEMA = [
|
|
12
|
+
// ─── GHCR ────────────────────────────────────────────────────────────────
|
|
13
|
+
{
|
|
14
|
+
group: 'GHCR (GitHub Container Registry)',
|
|
15
|
+
key: 'GHCR_USERNAME',
|
|
16
|
+
prompt: 'GHCR username (e.g. Fluxup Deploy Bot)',
|
|
17
|
+
default: 'Fluxup Deploy Bot',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
group: 'GHCR (GitHub Container Registry)',
|
|
21
|
+
key: 'IMAGE_TAG',
|
|
22
|
+
prompt: 'Docker image tag (e.g. 1.4.3)',
|
|
23
|
+
default: 'latest',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
group: 'GHCR (GitHub Container Registry)',
|
|
27
|
+
key: 'GHCR_SECRET_NAME',
|
|
28
|
+
prompt: 'Name/ARN of the AWS Secrets Manager secret that holds the GHCR token',
|
|
29
|
+
default: 'fluxup/ghcr-token',
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// ─── S3 ──────────────────────────────────────────────────────────────────
|
|
33
|
+
{
|
|
34
|
+
group: 'S3 (docker-compose source)',
|
|
35
|
+
key: 'S3_BUCKET',
|
|
36
|
+
prompt: 'S3 bucket name that stores docker-compose.yml',
|
|
37
|
+
default: 'fluxup-installer',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
group: 'S3 (docker-compose source)',
|
|
41
|
+
key: 'S3_KEY',
|
|
42
|
+
prompt: 'Object key in the bucket (e.g. v1.4.3/docker-compose.yml)',
|
|
43
|
+
default: 'docker-compose.yml',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
group: 'S3 (docker-compose source)',
|
|
47
|
+
key: 'S3_REGION',
|
|
48
|
+
prompt: 'AWS region of the S3 bucket',
|
|
49
|
+
default: 'us-east-1',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
group: 'S3 (docker-compose source)',
|
|
53
|
+
key: 'S3_AWS_ACCESS_KEY_ID',
|
|
54
|
+
prompt: 'AWS Access Key ID for the installer S3 bucket (read-only user)',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
group: 'S3 (docker-compose source)',
|
|
58
|
+
key: 'S3_AWS_SECRET_ACCESS_KEY',
|
|
59
|
+
prompt: 'AWS Secret Access Key for the installer S3 bucket (read-only user)',
|
|
60
|
+
secret: true,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// ─── AWS Credentials ─────────────────────────────────────────────────────
|
|
64
|
+
{
|
|
65
|
+
group: 'AWS Credentials',
|
|
66
|
+
key: 'AWS_REGION',
|
|
67
|
+
prompt: 'Default AWS region',
|
|
68
|
+
default: 'us-east-1',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
group: 'AWS Credentials',
|
|
72
|
+
key: 'AWS_ACCESS_KEY_ID',
|
|
73
|
+
prompt: 'AWS Access Key ID',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
group: 'AWS Credentials',
|
|
77
|
+
key: 'AWS_SECRET_ACCESS_KEY',
|
|
78
|
+
prompt: 'AWS Secret Access Key',
|
|
79
|
+
secret: true,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
group: 'AWS Credentials',
|
|
83
|
+
key: 'AWS_STS_BROKER_ROLE_ARN',
|
|
84
|
+
prompt: 'STS Broker IAM Role ARN',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
group: 'AWS Credentials',
|
|
88
|
+
key: 'AWS_SECRETSMANAGER_READER_ROLE_ARN',
|
|
89
|
+
prompt: 'Secrets Manager reader IAM Role ARN',
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// ─── Database ────────────────────────────────────────────────────────────
|
|
93
|
+
{
|
|
94
|
+
group: 'Database (PostgreSQL)',
|
|
95
|
+
key: 'DB_NAME',
|
|
96
|
+
prompt: 'Database name',
|
|
97
|
+
default: 'fluxup',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
group: 'Database (PostgreSQL)',
|
|
101
|
+
key: 'DB_USERNAME',
|
|
102
|
+
prompt: 'Database username',
|
|
103
|
+
default: 'fluxup',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
group: 'Database (PostgreSQL)',
|
|
107
|
+
key: 'DB_PASSWORD',
|
|
108
|
+
prompt: 'Database password',
|
|
109
|
+
secret: true,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
group: 'Database (PostgreSQL)',
|
|
113
|
+
key: 'JPA_DDL_AUTO',
|
|
114
|
+
prompt: 'JPA DDL Auto (update | validate | none)',
|
|
115
|
+
default: 'update',
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// ─── DynamoDB ────────────────────────────────────────────────────────────
|
|
119
|
+
{
|
|
120
|
+
group: 'DynamoDB',
|
|
121
|
+
key: 'AWS_DAILY_JOBS_TABLE',
|
|
122
|
+
prompt: 'DynamoDB daily jobs table name',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
group: 'DynamoDB',
|
|
126
|
+
key: 'AWS_DYNAMODB_DAILY_JOBS_ROLE_ARN',
|
|
127
|
+
prompt: 'DynamoDB access IAM Role ARN',
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// ─── SQS ─────────────────────────────────────────────────────────────────
|
|
131
|
+
{
|
|
132
|
+
group: 'SQS',
|
|
133
|
+
key: 'AWS_SQS_CONSUMER_ROLE_ARN',
|
|
134
|
+
prompt: 'SQS consumer IAM Role ARN',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
group: 'SQS',
|
|
138
|
+
key: 'AWS_SQS_PUBLISHER_ROLE_ARN',
|
|
139
|
+
prompt: 'SQS publisher IAM Role ARN',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
group: 'SQS',
|
|
143
|
+
key: 'AWS_DAILY_REQUEST_QUEUE_URL',
|
|
144
|
+
prompt: 'Daily request SQS queue URL',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
group: 'SQS',
|
|
148
|
+
key: 'AWS_DAILY_RESPONSE_QUEUE_URL',
|
|
149
|
+
prompt: 'Daily response SQS queue URL',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
group: 'SQS',
|
|
153
|
+
key: 'AWS_DAILY_DLQ_URL',
|
|
154
|
+
prompt: 'Dead Letter Queue SQS URL',
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// ─── Network ─────────────────────────────────────────────────────────────
|
|
158
|
+
{
|
|
159
|
+
group: 'Network',
|
|
160
|
+
key: 'NGINX_PORT',
|
|
161
|
+
prompt: 'nginx HTTP port on the host',
|
|
162
|
+
default: '80',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
group: 'Network',
|
|
166
|
+
key: 'BACKEND_API_TIMEOUT',
|
|
167
|
+
prompt: 'Backend API timeout in ms',
|
|
168
|
+
default: '5000',
|
|
169
|
+
},
|
|
170
|
+
];
|
package/src/ghcr.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authenticates with GHCR using docker login --password-stdin.
|
|
6
|
+
* The token stays in memory only (piped directly to the docker process).
|
|
7
|
+
*
|
|
8
|
+
* @param {object} opts
|
|
9
|
+
* @param {string} opts.username GHCR username
|
|
10
|
+
* @param {string} opts.token GHCR token (fetched from AWS Secrets Manager)
|
|
11
|
+
*/
|
|
12
|
+
export async function loginGhcr({ username, token }) {
|
|
13
|
+
console.log(pc.dim(` docker login ghcr.io -u "${username}" --password-stdin`));
|
|
14
|
+
|
|
15
|
+
await execa('docker', ['login', 'ghcr.io', '-u', username, '--password-stdin'], {
|
|
16
|
+
input: token,
|
|
17
|
+
stdout: 'inherit',
|
|
18
|
+
stderr: 'inherit',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
console.log(pc.green('✔') + ' GHCR login successful.');
|
|
22
|
+
}
|
package/src/preflight.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Checks that docker and docker compose v2 are available.
|
|
6
|
+
* Aborts with a clear message if any prerequisite is missing.
|
|
7
|
+
*/
|
|
8
|
+
export async function preflight() {
|
|
9
|
+
const checks = [
|
|
10
|
+
{
|
|
11
|
+
cmd: 'docker',
|
|
12
|
+
args: ['--version'],
|
|
13
|
+
label: 'Docker',
|
|
14
|
+
hint: 'Install Docker Engine: https://docs.docker.com/engine/install/',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
cmd: 'docker',
|
|
18
|
+
args: ['compose', 'version'],
|
|
19
|
+
label: 'Docker Compose v2',
|
|
20
|
+
hint: 'Compose v2 ships with Docker Engine >= 20.10. Update Docker.',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
let ok = true;
|
|
25
|
+
|
|
26
|
+
for (const { cmd, args, label, hint } of checks) {
|
|
27
|
+
try {
|
|
28
|
+
const { stdout } = await execa(cmd, args);
|
|
29
|
+
console.log(pc.green('✔') + ' ' + label + ' — ' + stdout.trim().split('\n')[0]);
|
|
30
|
+
} catch {
|
|
31
|
+
console.error(pc.red('✘') + ' ' + label + ' not found.');
|
|
32
|
+
console.error(' ' + pc.yellow(hint));
|
|
33
|
+
ok = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!ok) {
|
|
38
|
+
console.error(pc.red('\nMissing prerequisites. Install the required tools and try again.'));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/s3.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import { createWriteStream, mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Downloads docker-compose.yml from an S3 bucket to disk.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} opts
|
|
11
|
+
* @param {string} opts.bucket
|
|
12
|
+
* @param {string} opts.key
|
|
13
|
+
* @param {string} opts.region
|
|
14
|
+
* @param {string} opts.accessKeyId
|
|
15
|
+
* @param {string} opts.secretAccessKey
|
|
16
|
+
* @param {string} opts.destPath Local destination path (e.g. ./fluxup/docker-compose.yml)
|
|
17
|
+
*/
|
|
18
|
+
export async function downloadCompose({
|
|
19
|
+
bucket,
|
|
20
|
+
key,
|
|
21
|
+
region,
|
|
22
|
+
accessKeyId,
|
|
23
|
+
secretAccessKey,
|
|
24
|
+
destPath,
|
|
25
|
+
}) {
|
|
26
|
+
const client = new S3Client({
|
|
27
|
+
region,
|
|
28
|
+
credentials: { accessKeyId, secretAccessKey },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log(pc.dim(` s3://${bucket}/${key} → ${destPath}`));
|
|
32
|
+
|
|
33
|
+
const { Body } = await client.send(
|
|
34
|
+
new GetObjectCommand({ Bucket: bucket, Key: key })
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (!Body) {
|
|
38
|
+
throw new Error(`Object s3://${bucket}/${key} returned no Body.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
42
|
+
await pipeline(Body, createWriteStream(destPath));
|
|
43
|
+
|
|
44
|
+
console.log(pc.green('✔') + ` docker-compose.yml saved to ${destPath}`);
|
|
45
|
+
}
|
package/src/secrets.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SecretsManagerClient,
|
|
3
|
+
GetSecretValueCommand,
|
|
4
|
+
} from '@aws-sdk/client-secrets-manager';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fetches the GHCR token from AWS Secrets Manager at runtime.
|
|
8
|
+
* The token is kept in memory only — never written to disk.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} opts
|
|
11
|
+
* @param {string} opts.secretName Secret name or ARN (e.g. 'fluxup/ghcr-token')
|
|
12
|
+
* @param {string} opts.region AWS region
|
|
13
|
+
* @param {string} opts.accessKeyId
|
|
14
|
+
* @param {string} opts.secretAccessKey
|
|
15
|
+
* @returns {Promise<string>} GHCR token
|
|
16
|
+
*/
|
|
17
|
+
export async function fetchGhcrToken({ secretName, region, accessKeyId, secretAccessKey }) {
|
|
18
|
+
const client = new SecretsManagerClient({
|
|
19
|
+
region,
|
|
20
|
+
credentials: {
|
|
21
|
+
accessKeyId,
|
|
22
|
+
secretAccessKey,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const response = await client.send(
|
|
27
|
+
new GetSecretValueCommand({ SecretId: secretName })
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const raw = response.SecretString;
|
|
31
|
+
if (!raw) {
|
|
32
|
+
throw new Error(`Secret "${secretName}" has no SecretString.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Supports secret as JSON { "token": "ghp_..." } or plain string
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
return parsed.token ?? parsed.GHCR_TOKEN ?? raw;
|
|
39
|
+
} catch {
|
|
40
|
+
return raw.trim();
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/wizard.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { ENV_SCHEMA } from './env.schema.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Collects all environment variables via grouped interactive prompts.
|
|
7
|
+
* Fields marked with secret:true use masked input.
|
|
8
|
+
*
|
|
9
|
+
* @param {Record<string, string>} existing Pre-existing values (e.g. from --non-interactive env vars)
|
|
10
|
+
* @returns {Promise<Record<string, string>>}
|
|
11
|
+
*/
|
|
12
|
+
export async function runWizard(existing = {}) {
|
|
13
|
+
const answers = { ...existing };
|
|
14
|
+
|
|
15
|
+
const groups = [...new Set(ENV_SCHEMA.map((e) => e.group))];
|
|
16
|
+
|
|
17
|
+
p.intro(pc.bgBlue(pc.white(' FluxUp Install — Configuration ')));
|
|
18
|
+
|
|
19
|
+
for (const group of groups) {
|
|
20
|
+
const fields = ENV_SCHEMA.filter((e) => e.group === group);
|
|
21
|
+
|
|
22
|
+
p.log.step(pc.bold(group));
|
|
23
|
+
|
|
24
|
+
for (const field of fields) {
|
|
25
|
+
if (answers[field.key] !== undefined && answers[field.key] !== '') {
|
|
26
|
+
p.log.info(`${field.key} = ${field.secret ? '***' : answers[field.key]} (kept)`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const value = field.secret
|
|
31
|
+
? await p.password({
|
|
32
|
+
message: field.prompt,
|
|
33
|
+
validate(v) {
|
|
34
|
+
if (!field.default && !v) return 'Required.';
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
: await p.text({
|
|
38
|
+
message: field.prompt,
|
|
39
|
+
defaultValue: field.default,
|
|
40
|
+
placeholder: field.default ?? '',
|
|
41
|
+
validate(v) {
|
|
42
|
+
if (!field.default && !v) return 'Required.';
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (p.isCancel(value)) {
|
|
47
|
+
p.cancel('Installation cancelled.');
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
answers[field.key] = value || field.default || '';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
p.log.success('Configuration collected.');
|
|
56
|
+
|
|
57
|
+
return answers;
|
|
58
|
+
}
|