@brutalsystems/dray 0.1.7 → 0.1.9
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/status.js +7 -2
- package/src/config/repo.js +8 -0
- package/src/core/depsCache.js +17 -3
- 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
|
@@ -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/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/depsCache.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
const fs = require('node:fs'); const path = require('node:path'); const crypto = require('node:crypto');
|
|
2
|
+
const { execFileSync } = require('node:child_process');
|
|
2
3
|
const { DRAY_HOME } = require('../constants');
|
|
3
|
-
|
|
4
|
+
|
|
5
|
+
// Whether the deps base image tag exists in the local Docker image store. A
|
|
6
|
+
// `docker system prune` can evict it while the lockfiles are unchanged — the hash
|
|
7
|
+
// check alone would then skip the rebuild and the main build fails at
|
|
8
|
+
// `FROM <depsImage.tag>: not found`.
|
|
9
|
+
function depsImagePresent(tag) {
|
|
10
|
+
try { execFileSync('docker', ['image', 'inspect', tag], { stdio: 'ignore' }); return true; }
|
|
11
|
+
catch { return false; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// `imagePresent` is injectable for tests; defaults to the real docker check.
|
|
15
|
+
function needsDepsRebuild(image, repoPath, repo, stateDir = path.join(DRAY_HOME, 'depshash'), imagePresent = depsImagePresent) {
|
|
4
16
|
if (!image.depsImage) return false;
|
|
5
17
|
const h = crypto.createHash('sha256');
|
|
6
18
|
for (const rel of image.depsImage.rebuildOn || []) {
|
|
@@ -11,7 +23,9 @@ function needsDepsRebuild(image, repoPath, repo, stateDir = path.join(DRAY_HOME,
|
|
|
11
23
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
12
24
|
const file = path.join(stateDir, `${repo}__${image.name}.depshash`);
|
|
13
25
|
const prev = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
14
|
-
|
|
26
|
+
// Rebuild when the lockfiles changed OR the cached deps image tag is gone.
|
|
27
|
+
const present = !image.depsImage.tag || imagePresent(image.depsImage.tag);
|
|
28
|
+
if (prev === digest && present) return false;
|
|
15
29
|
fs.writeFileSync(file, digest); return true;
|
|
16
30
|
}
|
|
17
|
-
module.exports = { needsDepsRebuild };
|
|
31
|
+
module.exports = { needsDepsRebuild, depsImagePresent };
|
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 };
|