@brutalsystems/dray 0.1.8 → 0.1.10
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/README.md +7 -2
- package/package.json +1 -1
- package/src/cli/commands/add.js +14 -1
- package/src/cli/commands/status.js +7 -2
- package/src/config/registry.js +9 -3
- package/src/config/repo.js +8 -0
- package/src/core/engine.js +6 -1
- package/src/core/resolve.js +7 -0
- package/src/primitives/git.js +5 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Requires `docker buildx`, `kubectl`, `git`, `aws`, and (for pilets/secrets) `sop
|
|
|
35
35
|
dray # interactive menu
|
|
36
36
|
dray list # registered repos + targets
|
|
37
37
|
dray ship <repo>:<target> # deps?→build→push(SHA)→render+apply→rollout
|
|
38
|
-
dray ship <repo> # all enabled workloads in the repo
|
|
38
|
+
dray ship <repo> # all enabled workloads in the repo (skips manual ones)
|
|
39
39
|
dray apply <repo>:<target> # render + apply manifests only
|
|
40
40
|
dray rollout <repo>:<target>
|
|
41
41
|
dray status <repo> # running image SHA vs HEAD
|
|
@@ -72,8 +72,13 @@ Global flags: `--dry-run` (print the plan, run nothing), `--allow-dirty`
|
|
|
72
72
|
// kind: deployment (rolled out) or cronjob (applied only)
|
|
73
73
|
{ "name": "app", "kind": "deployment", "image": "app",
|
|
74
74
|
"manifests": [".k8s/deployment.yaml", ".k8s/service.yaml"], "stampImages": ["app"] },
|
|
75
|
-
// disabled: kept in config but skipped (e.g. served elsewhere)
|
|
75
|
+
// disabled: kept in config but skipped (e.g. served elsewhere).
|
|
76
|
+
// Targeting it by name is an error.
|
|
76
77
|
{ "name": "legacy", "kind": "deployment", "image": "app", "manifests": ["..."], "disabled": true },
|
|
78
|
+
// manual: deployed only by `dray ship <repo>:<name>`, never by a bare-repo
|
|
79
|
+
// ship or --all. Use it to keep a workload out of CI (which ships the bare
|
|
80
|
+
// repo) while still deploying it on demand. Its image is skipped too.
|
|
81
|
+
{ "name": "jobs", "kind": "deployment", "image": "jobs", "manifests": ["..."], "manual": true },
|
|
77
82
|
// image-less: apply-only (third-party image lives literally in the manifest)
|
|
78
83
|
{ "name": "searxng", "kind": "deployment", "manifests": [".k8s/searxng.yaml"] }
|
|
79
84
|
],
|
package/package.json
CHANGED
package/src/cli/commands/add.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
const fs = require('node:fs'); const path = require('node:path');
|
|
1
2
|
const { loadRepoConfig } = require('../../config/repo'); const { addRepo } = require('../../config/registry');
|
|
3
|
+
|
|
4
|
+
// The registry is consulted from arbitrary working directories, so a stored path
|
|
5
|
+
// MUST be absolute. `dray add .` used to store "." verbatim, which then resolved
|
|
6
|
+
// against whatever cwd dray ran in later -- two repos added that way both pointed
|
|
7
|
+
// at "the current directory", and `dray ship <a>` from repo b deployed b under a's
|
|
8
|
+
// name. realpath so symlinked checkouts land on one canonical path.
|
|
9
|
+
function resolveRepoPath(repoPath) {
|
|
10
|
+
const p = path.resolve(repoPath || process.cwd());
|
|
11
|
+
try { return fs.realpathSync(p); } catch { return p; }
|
|
12
|
+
}
|
|
13
|
+
|
|
2
14
|
module.exports = function registerAdd(program) {
|
|
3
15
|
program.command('add [repoPath]').description('Register a repo').action((repoPath) => {
|
|
4
|
-
const p = repoPath
|
|
16
|
+
const p = resolveRepoPath(repoPath); const config = loadRepoConfig(p);
|
|
5
17
|
addRepo(undefined, { name: config.name, path: p, config }); console.log(`registered ${config.name} → ${p}`);
|
|
6
18
|
});
|
|
7
19
|
};
|
|
20
|
+
module.exports.resolveRepoPath = resolveRepoPath;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { loadGlobalConfig } = require('../../config/global'); const { allRepos } = require('../../config/registry');
|
|
2
|
-
const { resolveTargets } = require('../../core/resolve'); const { runningImage } = require('../../primitives/kubectl'); const { currentSha } = require('../../primitives/git');
|
|
2
|
+
const { resolveTargets } = require('../../core/resolve'); const { runningImage } = require('../../primitives/kubectl'); const { currentSha, lsRemote } = require('../../primitives/git');
|
|
3
3
|
module.exports = function registerStatus(program) {
|
|
4
4
|
program.command('status [target]').description('Running image SHA vs HEAD').option('--all', 'all repos')
|
|
5
5
|
.action(async (target, opts) => {
|
|
@@ -7,7 +7,12 @@ module.exports = function registerStatus(program) {
|
|
|
7
7
|
const units = resolveTargets({ registry: allRepos(), globalDefaults: defaults, spec: opts.all || !target ? '--all' : target }).filter((u) => u.workload);
|
|
8
8
|
for (const u of units) {
|
|
9
9
|
const live = await runningImage({ workload: u.workload, kind: u.kind, context: u.defaults.context, namespace: u.defaults.namespace });
|
|
10
|
-
|
|
10
|
+
// A git-source image builds from its remote, not the repo `status` runs in —
|
|
11
|
+
// so compare against the remote HEAD, not the local (unrelated) repo's.
|
|
12
|
+
const src = u.image && u.image.source;
|
|
13
|
+
const head = src && src.git
|
|
14
|
+
? await lsRemote({ git: src.git, ref: src.ref || 'HEAD' }).catch(() => '?')
|
|
15
|
+
: await currentSha(u.repoPath).catch(() => '?');
|
|
11
16
|
const tag = live.includes(':') ? live.split(':').pop() : '(none)';
|
|
12
17
|
console.log(`${u.repo}:${u.workload} running ${tag} ${tag === head ? 'up-to-date' : `behind (HEAD ${head})`}`);
|
|
13
18
|
}
|
package/src/config/registry.js
CHANGED
|
@@ -10,16 +10,22 @@ function removeRepo(file, name) { const r = loadRegistry(file); delete r[name];
|
|
|
10
10
|
// without a manual `dray reload`. Falls back to the snapshot when the repo
|
|
11
11
|
// isn't checked out here (stat throws) or its config is momentarily invalid
|
|
12
12
|
// (loadRepoConfig throws) — a broken repo must not break `list`/`--all`.
|
|
13
|
-
function refreshEntry(entry) {
|
|
13
|
+
function refreshEntry(entry, name) {
|
|
14
14
|
try {
|
|
15
15
|
const mtime = Math.floor(fs.statSync(path.join(entry.path, '.dray', 'config.json')).mtimeMs);
|
|
16
16
|
if (mtime <= (Date.parse(entry.addedAt) || 0)) return entry;
|
|
17
|
-
|
|
17
|
+
const config = loadRepoConfig(entry.path);
|
|
18
|
+
// Only adopt a config that still belongs to this entry. A path pointing at a
|
|
19
|
+
// different project (a relative path left by an old `dray add .`, or a moved
|
|
20
|
+
// checkout) would otherwise have its config silently written into this key and
|
|
21
|
+
// PERSISTED -- turning `dray ship <name>` into a deploy of someone else's repo.
|
|
22
|
+
if (name !== undefined && config.name !== name) return entry;
|
|
23
|
+
return { ...entry, config, addedAt: new Date(mtime).toISOString() };
|
|
18
24
|
} catch { return entry; }
|
|
19
25
|
}
|
|
20
26
|
function allRepos(file = REGISTRY) {
|
|
21
27
|
const reg = loadRegistry(file); let changed = false;
|
|
22
|
-
for (const [n, e] of Object.entries(reg)) { const f = refreshEntry(e); if (f !== e) { reg[n] = f; changed = true; } }
|
|
28
|
+
for (const [n, e] of Object.entries(reg)) { const f = refreshEntry(e, n); if (f !== e) { reg[n] = f; changed = true; } }
|
|
23
29
|
if (changed) saveRegistry(file, reg);
|
|
24
30
|
return reg;
|
|
25
31
|
}
|
package/src/config/repo.js
CHANGED
|
@@ -21,6 +21,14 @@ function validateRepoConfig(cfg) {
|
|
|
21
21
|
if (!WORKLOAD_KINDS.has(w.kind)) e.push(`workload "${w.name}" kind must be deployment|cronjob`);
|
|
22
22
|
if (w.image !== undefined && !imageNames.has(w.image)) e.push(`workload "${w.name}" references unknown image "${w.image}"`);
|
|
23
23
|
if (!Array.isArray(w.manifests) || w.manifests.length === 0) e.push(`workload "${w.name}" needs a non-empty manifests list`);
|
|
24
|
+
// Both gate deployment on truthiness, so a JSON string ("false", "no") would
|
|
25
|
+
// silently skip the workload -- in `manual`'s case, silently drop it from CI.
|
|
26
|
+
// Fail loud instead of deploying something different from what was meant.
|
|
27
|
+
for (const flag of ['disabled', 'manual']) {
|
|
28
|
+
if (w[flag] !== undefined && typeof w[flag] !== 'boolean') {
|
|
29
|
+
e.push(`workload "${w.name}" ${flag} must be a boolean, got ${typeof w[flag]}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
24
32
|
}
|
|
25
33
|
for (const s of cfg.secrets || []) {
|
|
26
34
|
if (!s.name) e.push('secret missing name');
|
package/src/core/engine.js
CHANGED
|
@@ -37,7 +37,12 @@ async function execute(steps, { dryRun = false, allowDirty = false, deps } = {})
|
|
|
37
37
|
}
|
|
38
38
|
const sha = await shaForUnit(u, d, shaCache, dryRun, allowDirty);
|
|
39
39
|
if (u._clone && !clones.includes(u._clone)) clones.push(u._clone);
|
|
40
|
-
|
|
40
|
+
// Real builds use the clone dir. On dry-run we skip the clone, so show the
|
|
41
|
+
// git source (not the local repo `dray` was invoked from) — otherwise the
|
|
42
|
+
// printed command misleadingly implies the local Dockerfile is built.
|
|
43
|
+
const src = u.image && u.image.source;
|
|
44
|
+
const cwd = u._clone ? u._clone.dir
|
|
45
|
+
: (dryRun && src && src.git ? `<git ${src.git}@${src.ref || 'HEAD'} — cloned at build>` : u.repoPath);
|
|
41
46
|
if (step.kind === 'deps') { if (d.depsCache.needsDepsRebuild(u.image, cwd, u.repo)) await d.docker.buildDeps(u.image, cwd, dryRun); }
|
|
42
47
|
else if (step.kind === 'build') await d.docker.buildImage({ ecrUri: u.repoUri, sha, dockerfile: u.image.dockerfile, context: u.image.context || '.', platform: u.defaults.platform, cwd, dryRun, buildArgs: await d.buildArgs.resolveBuildArgs(u.image, cwd, { dryRun }) });
|
|
43
48
|
else if (step.kind === 'push') {
|
package/src/core/resolve.js
CHANGED
|
@@ -38,6 +38,13 @@ function unitsForRepo(name, entry, globalDefaults, filter) {
|
|
|
38
38
|
}
|
|
39
39
|
continue;
|
|
40
40
|
}
|
|
41
|
+
// Deployed only when explicitly targeted (dray ship repo:name). Skipped by a
|
|
42
|
+
// bare-repo ship and --all -- that is what CI runs, so `manual` keeps a
|
|
43
|
+
// workload out of the automatic pipeline while still allowing an on-demand
|
|
44
|
+
// deploy. Unlike `disabled`, targeting it by name is not an error. The image
|
|
45
|
+
// is skipped with it: the image-only fallback below only fires when NO
|
|
46
|
+
// workload claims the image, and a skipped manual workload still claims it.
|
|
47
|
+
if (w.manual && !filter) continue;
|
|
41
48
|
mk(images.get(w.image), w);
|
|
42
49
|
}
|
|
43
50
|
for (const img of images.values()) {
|
package/src/primitives/git.js
CHANGED
|
@@ -2,6 +2,10 @@ const fs = require('node:fs'); const os = require('node:os'); const path = requi
|
|
|
2
2
|
const { run } = require('./exec');
|
|
3
3
|
async function currentSha(cwd) { const { stdout } = await run('git', ['rev-parse', '--short', 'HEAD'], { cwd, capture: true }); return stdout.trim(); }
|
|
4
4
|
async function isDirty(cwd) { const { stdout } = await run('git', ['status', '--porcelain'], { cwd, capture: true }); return stdout.trim().length > 0; }
|
|
5
|
+
// The HEAD sha of a git-source image's REMOTE ref, without cloning — so `status`
|
|
6
|
+
// compares against the actual build source, not the local repo it happens to be
|
|
7
|
+
// invoked from. Short form to match `currentSha`.
|
|
8
|
+
async function lsRemote({ git, ref }) { const { stdout } = await run('git', ['ls-remote', git, ref], { capture: true }); return (stdout.trim().split(/\s+/)[0] || '').slice(0, 7); }
|
|
5
9
|
async function cloneToTmp({ git, ref }) {
|
|
6
10
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dray-clone-'));
|
|
7
11
|
// Works for branch/tag refs always; arbitrary commit SHAs require the remote
|
|
@@ -13,4 +17,4 @@ async function cloneToTmp({ git, ref }) {
|
|
|
13
17
|
const sha = await currentSha(dir);
|
|
14
18
|
return { dir, sha, cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) };
|
|
15
19
|
}
|
|
16
|
-
module.exports = { currentSha, isDirty, cloneToTmp };
|
|
20
|
+
module.exports = { currentSha, isDirty, cloneToTmp, lsRemote };
|