@leverege/build-tools 2.115.1 → 2.116.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/package.json +5 -5
- package/src/Utils.mjs +2 -2
- package/src/bash-funcs +9 -0
- package/src/chart-compass.mjs +17 -0
- package/src/circle-orb-it/circle-orb-it.mjs +2 -0
- package/src/custom-images/kubectl/build-kubetcl.sh +1 -1
- package/src/gh-audit-log.mjs +155 -0
- package/src/github-actions-it.mjs +47 -57
- package/src/helm-charts/cnpg-operator/helmup.bootstrap +23 -0
- package/src/helm-charts/cnpg-operator/helmup.plugin +1 -1
- package/src/helm-charts/prom-operator/helmup.plugin +1 -1
- package/src/helm-charts/triton-inference-leash/.nohelm +0 -0
- package/src/helm-charts/triton-inference-leash/README.md +88 -0
- package/src/helm-charts/triton-inference-leash/follow-me.yaml +229 -0
- package/src/helm-charts/triton-inference-leash/helmdn.plugin +3 -0
- package/src/helm-charts/triton-inference-leash/helmup.plugin +16 -0
- package/src/helmup.sh +2 -2
- package/src/k8scale.sh +2 -0
- package/src/k8x.sh +2 -0
- package/src/klog.sh +4 -0
- package/src/overwhelm.mjs +4 -1
- package/src/push-my-chart.mjs +14 -0
- package/src/repo-panopticon.mjs +5 -2
- /package/src/{crane-lift.sh → custom-images/bitnami-audits/crane-lift.sh} +0 -0
- /package/src/{k8cryo.sh → velero-cryo.sh} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leverege/build-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.116.0",
|
|
4
4
|
"description": "A collection of build / support tools for Leverege developers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"build-tools": "src/build-tools.mjs",
|
|
24
24
|
"chart-compass": "src/chart-compass.mjs",
|
|
25
25
|
"chart-to-registry": "src/chart-to-registry.mjs",
|
|
26
|
-
"circle-orb-it": "src/circle-orb-it/circle-orb-it.mjs",
|
|
27
26
|
"clone-cnpg-from-snapshot": "src/clone-cnpg-from-snapshot.mjs",
|
|
28
27
|
"count-lines-of-code": "src/count-lines-of-code.mjs",
|
|
29
28
|
"dashboard": "src/dashboard/dashboard.mjs",
|
|
@@ -38,6 +37,7 @@
|
|
|
38
37
|
"generate-docs": "src/generate-docs.mjs",
|
|
39
38
|
"getfbcfg": "src/getfbcfg.mjs",
|
|
40
39
|
"getjson": "src/getjson.mjs",
|
|
40
|
+
"gh-audit-log": "src/gh-audit-log.mjs",
|
|
41
41
|
"git-tar": "src/git-tar.sh",
|
|
42
42
|
"github-actions-it": "src/github-actions-it.mjs",
|
|
43
43
|
"helm-audit": "src/helm-audit.mjs",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"@kubernetes/client-node": "^
|
|
92
|
+
"@kubernetes/client-node": "^2.0.0",
|
|
93
93
|
"@leverege/jsdoc-template": "^1.0.1",
|
|
94
94
|
"chalk": "^6.0.0",
|
|
95
95
|
"command-line-args": "^6.0.2",
|
|
@@ -100,10 +100,10 @@
|
|
|
100
100
|
"execa": "^10.0.1",
|
|
101
101
|
"find-yarn-workspace-root": "^2.0.0",
|
|
102
102
|
"glob": "^13.0.6",
|
|
103
|
-
"googleapis": "^
|
|
103
|
+
"googleapis": "^176.0.0",
|
|
104
104
|
"handlebars": "^4.7.9",
|
|
105
105
|
"ignore": "^7.0.6",
|
|
106
|
-
"inquirer": "^14.0
|
|
106
|
+
"inquirer": "^14.1.0",
|
|
107
107
|
"js-yaml": "^4.2.0",
|
|
108
108
|
"jsdoc": "^4.0.5",
|
|
109
109
|
"ms": "^2.1.3",
|
package/src/Utils.mjs
CHANGED
|
@@ -338,14 +338,14 @@ export const parseJsonFile = async ( jsonFile ) => {
|
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
// Parses the ./package.json file and verifies there is a properly formatted
|
|
342
|
-
// leverege.registry section present.
|
|
343
341
|
const deprecated = ( opts ) => {
|
|
344
342
|
log( chalk.red.bold( `\n ***DEPRECATED: ${opts.deprecationError}` ) )
|
|
345
343
|
log( chalk.white( opts.deprecationInfo ) )
|
|
346
344
|
process.exit( 1 )
|
|
347
345
|
}
|
|
348
346
|
|
|
347
|
+
// Parses the ./package.json file and verifies there is a properly formatted
|
|
348
|
+
// leverege.registry section present.
|
|
349
349
|
export const parsePackageJson = async ( packageFileName = './package.json' ) => {
|
|
350
350
|
if ( !fs.existsSync( packageFileName ) ) { return undefined }
|
|
351
351
|
|
package/src/bash-funcs
CHANGED
package/src/chart-compass.mjs
CHANGED
|
@@ -113,6 +113,23 @@ const buildCompassMap = ( yamlFile ) => {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
const chartCompass = YAML.load( fs.readFileSync( yamlFile, 'utf8' ) ) // eslint-disable-line security/detect-non-literal-fs-filename
|
|
116
|
+
|
|
117
|
+
if ( !Array.isArray( chartCompass ) ) {
|
|
118
|
+
errorExit( `${yamlFile}: expected a YAML array at the top level` )
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for ( const entry of chartCompass ) {
|
|
122
|
+
const repositories = entry.repositories || {}
|
|
123
|
+
for ( const [ repo, chartMap ] of Object.entries( repositories ) ) {
|
|
124
|
+
if ( chartMap === null || typeof chartMap !== 'object' || Array.isArray( chartMap ) ) {
|
|
125
|
+
errorExit(
|
|
126
|
+
`${yamlFile}: repository "${repo}" must be a map of chart entries (each chart key needs ": {}" or explicit config).\n` +
|
|
127
|
+
` Got: ${JSON.stringify( chartMap )?.slice( 0, 120 )}`
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
116
133
|
return chartCompass
|
|
117
134
|
}
|
|
118
135
|
|
|
@@ -57,6 +57,8 @@ if ( options.version ) {
|
|
|
57
57
|
process.exit( 0 )
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
warning( 'circle-orb-it is deprecated. Use github-actions-it instead.' )
|
|
61
|
+
|
|
60
62
|
const gitRoot = await getGitRootDirectory()
|
|
61
63
|
if ( !gitRoot ) {
|
|
62
64
|
errorExit( 'This command must be run inside a git repository' )
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { $ } from 'zx'
|
|
3
|
+
import { program } from 'commander'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
|
|
6
|
+
import { enableDebug, errorExit, log } from './Utils.mjs'
|
|
7
|
+
|
|
8
|
+
$.verbose = false
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ORG = 'Leverege'
|
|
11
|
+
const DEFAULT_LIMIT = 50
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name( 'gh-audit-log' )
|
|
15
|
+
.description( chalk.green( 'Query the Leverege GitHub org audit log' ) )
|
|
16
|
+
.option( '--org <org>', 'GitHub org', DEFAULT_ORG )
|
|
17
|
+
.option( '--team <slug>', 'Filter to a team (e.g. workwatch-admins) — applied client-side, GitHub has no server-side team qualifier' )
|
|
18
|
+
.option( '--repo <name>', 'Filter to a repo (e.g. tpi-viz) — server-side' )
|
|
19
|
+
.option( '--actor <login>', 'Filter to a specific actor' )
|
|
20
|
+
.option( '--action <phrase>', 'Filter by action prefix (e.g. team, repo, org)' )
|
|
21
|
+
.option( '--since <date>', 'Only show events after this date (YYYY-MM-DD)' )
|
|
22
|
+
.option( '--phrase <raw>', 'Raw audit-log search phrase, combined with the above' )
|
|
23
|
+
.option( '--limit <n>', 'Max events to show', DEFAULT_LIMIT )
|
|
24
|
+
.option( '--json', 'Print raw JSON instead of a formatted table' )
|
|
25
|
+
.option( '--debug', 'Enable debug logging' )
|
|
26
|
+
.addHelpText( 'after', `
|
|
27
|
+
${chalk.yellow( 'Examples:' )}
|
|
28
|
+
${chalk.green( 'gh-audit-log --team workwatch-admins' )}
|
|
29
|
+
${chalk.green( 'gh-audit-log --actor leverege-jphelps --since 2026-08-14' )}
|
|
30
|
+
${chalk.green( 'gh-audit-log --action repo --since 2026-08-01 --json' )}
|
|
31
|
+
|
|
32
|
+
${chalk.yellow( 'Notes:' )}
|
|
33
|
+
Requires org-owner access — this hits ${chalk.green( 'GET /orgs/{org}/audit-log' )}, same
|
|
34
|
+
endpoint used throughout the GitHub access-control work this tool grew out of.
|
|
35
|
+
|
|
36
|
+
GitHub's audit-log search has no server-side "team" qualifier, even though events carry
|
|
37
|
+
a team field — ${chalk.green( '--team' )} is matched client-side after fetching.
|
|
38
|
+
|
|
39
|
+
This tool is built for access-control auditing, not as a general org-activity browser.
|
|
40
|
+
Unless you pass --action / --phrase / --repo / --actor, --action defaults to 'team' —
|
|
41
|
+
otherwise an unscoped query pages through the entire org log, which is large even over
|
|
42
|
+
a few days (routine org activity plus a chatty self-hosted-runner bot that registers
|
|
43
|
+
itself hourly). --since also defaults to 7 days ago unless given explicitly. Use
|
|
44
|
+
--action org (or --phrase) to look outside team/permission events, e.g. org role changes.
|
|
45
|
+
` )
|
|
46
|
+
.parse()
|
|
47
|
+
|
|
48
|
+
const opts = program.opts()
|
|
49
|
+
if ( opts.debug ) { enableDebug(); $.verbose = true }
|
|
50
|
+
|
|
51
|
+
// ---------- Build the audit-log search phrase (server-side qualifiers only) ----------
|
|
52
|
+
|
|
53
|
+
// This tool exists for access-control auditing, not as a general org-activity browser —
|
|
54
|
+
// default to action:team (team/permission changes) unless the caller opts into something
|
|
55
|
+
// broader via --action/--phrase/--repo/--actor. Without this, an unscoped query pages
|
|
56
|
+
// through the entire org log, which is large even over a few days (routine org activity
|
|
57
|
+
// plus a chatty self-hosted-runner bot that registers hourly).
|
|
58
|
+
const hasExplicitScope = opts.action || opts.phrase || opts.repo || opts.actor
|
|
59
|
+
const action = opts.action ?? ( !opts.phrase && !hasExplicitScope ? 'team' : undefined )
|
|
60
|
+
const since = opts.since ?? sevenDaysAgo()
|
|
61
|
+
|
|
62
|
+
const phraseParts = []
|
|
63
|
+
if ( opts.repo ) { phraseParts.push( `repo:${opts.org}/${opts.repo}` ) }
|
|
64
|
+
if ( opts.actor ) { phraseParts.push( `actor:${opts.actor}` ) }
|
|
65
|
+
if ( action ) { phraseParts.push( `action:${action}` ) }
|
|
66
|
+
if ( since ) { phraseParts.push( `created:>${since}` ) }
|
|
67
|
+
if ( opts.phrase ) { phraseParts.push( opts.phrase ) }
|
|
68
|
+
const phrase = phraseParts.join( ' ' )
|
|
69
|
+
|
|
70
|
+
function sevenDaysAgo() {
|
|
71
|
+
const d = new Date()
|
|
72
|
+
d.setDate( d.getDate() - 7 )
|
|
73
|
+
return d.toISOString().slice( 0, 10 )
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------- Fetch ----------
|
|
77
|
+
|
|
78
|
+
async function fetchAuditLog( { org, phrase : searchPhrase } ) {
|
|
79
|
+
const phraseArgs = searchPhrase ? [ '-f', `phrase=${searchPhrase}` ] : []
|
|
80
|
+
|
|
81
|
+
let stdout
|
|
82
|
+
try {
|
|
83
|
+
( { stdout } = await $`gh api orgs/${org}/audit-log -X GET --paginate ${phraseArgs}` )
|
|
84
|
+
} catch ( error ) {
|
|
85
|
+
errorExit( `Failed to query audit log: ${error.stderr ?? error.message ?? error}` )
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// `gh api --paginate` concatenates one JSON array per page back-to-back —
|
|
89
|
+
// split and re-flatten rather than assuming a single JSON document.
|
|
90
|
+
const pages = stdout
|
|
91
|
+
.split( /(?<=\])\s*(?=\[)/ )
|
|
92
|
+
.filter( chunk => chunk.trim().length > 0 )
|
|
93
|
+
.map( chunk => JSON.parse( chunk ) )
|
|
94
|
+
|
|
95
|
+
return pages.flat()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const fetched = await fetchAuditLog( { org : opts.org, phrase } )
|
|
99
|
+
const events = opts.team ?
|
|
100
|
+
fetched.filter( e => e.team === `${opts.org}/${opts.team}` ) :
|
|
101
|
+
fetched
|
|
102
|
+
const limited = events.slice( 0, Number( opts.limit ) )
|
|
103
|
+
|
|
104
|
+
if ( opts.json ) {
|
|
105
|
+
log( JSON.stringify( limited, null, 2 ) )
|
|
106
|
+
process.exit( 0 )
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if ( limited.length === 0 ) {
|
|
110
|
+
log( chalk.yellow( 'No matching audit log events found.' ) )
|
|
111
|
+
process.exit( 0 )
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------- Formatted table ----------
|
|
115
|
+
|
|
116
|
+
const rows = limited.map( e => ( {
|
|
117
|
+
when : new Date( e['@timestamp'] ).toLocaleString(),
|
|
118
|
+
actor : e.actor ?? '-',
|
|
119
|
+
action : e.action ?? '-',
|
|
120
|
+
team : e.team ? e.team.replace( `${opts.org}/`, '' ) : '-',
|
|
121
|
+
repo : e.repo ? e.repo.replace( `${opts.org}/`, '' ) : '-',
|
|
122
|
+
permission : e.permission ?? '-',
|
|
123
|
+
} ) )
|
|
124
|
+
|
|
125
|
+
const widths = [ 'when', 'actor', 'action', 'team', 'repo', 'permission' ].reduce( ( acc, key ) => {
|
|
126
|
+
acc[key] = Math.max( key.length, ...rows.map( r => String( r[key] ).length ) )
|
|
127
|
+
return acc
|
|
128
|
+
}, {} )
|
|
129
|
+
|
|
130
|
+
const PERMISSION_COLORS = {
|
|
131
|
+
admin : chalk.red,
|
|
132
|
+
maintain : chalk.yellow,
|
|
133
|
+
write : chalk.green,
|
|
134
|
+
triage : chalk.gray,
|
|
135
|
+
read : chalk.gray,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const printRow = ( row, { header = false } = {} ) => {
|
|
139
|
+
const cells = Object.entries( widths ).map( ( [ key, width ] ) => {
|
|
140
|
+
// pad the plain text first — ANSI color codes have zero visible width but
|
|
141
|
+
// count toward .length, so padding *after* colorizing would misalign columns
|
|
142
|
+
const padded = String( row[key] ).padEnd( width )
|
|
143
|
+
if ( header ) { return chalk.bold( padded ) }
|
|
144
|
+
if ( key === 'permission' ) { return ( PERMISSION_COLORS[row[key]] ?? chalk.dim )( padded ) }
|
|
145
|
+
if ( key === 'actor' ) { return row[key].endsWith( '[bot]' ) ? chalk.dim( padded ) : padded }
|
|
146
|
+
if ( key === 'when' ) { return chalk.dim( padded ) }
|
|
147
|
+
return padded
|
|
148
|
+
} )
|
|
149
|
+
log( cells.join( ' ' ) )
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
printRow( { when : 'WHEN', actor : 'ACTOR', action : 'ACTION', team : 'TEAM', repo : 'REPO', permission : 'PERM' }, { header : true } )
|
|
153
|
+
rows.forEach( row => printRow( row ) )
|
|
154
|
+
|
|
155
|
+
log( chalk.dim( `\n${limited.length} of ${events.length} matching events shown (--limit ${opts.limit} to change)` ) )
|
|
@@ -18,12 +18,24 @@ import {
|
|
|
18
18
|
} from './Utils.mjs'
|
|
19
19
|
|
|
20
20
|
const GH_ACTIONS_REF = 'development'
|
|
21
|
-
const CIRCLE_YML = '.circleci/config.yml'
|
|
22
21
|
const GH_WORKFLOWS_DIR = '.github/workflows'
|
|
23
22
|
const GH_CI_YML = `${GH_WORKFLOWS_DIR}/ci.yml`
|
|
23
|
+
const GH_DEPENDABOT_YML = '.github/dependabot.yml'
|
|
24
24
|
const DEFAULT_COVERAGE_THRESHOLD = 70
|
|
25
25
|
const DEFAULT_CACHE_CONFIG = '{"type":"inMemory"}'
|
|
26
26
|
|
|
27
|
+
const DEPENDABOT_CONTENT = `version: 2
|
|
28
|
+
updates:
|
|
29
|
+
- package-ecosystem: "npm"
|
|
30
|
+
directory: "/"
|
|
31
|
+
schedule:
|
|
32
|
+
interval: "weekly"
|
|
33
|
+
open-pull-requests-limit: 10
|
|
34
|
+
ignore:
|
|
35
|
+
- dependency-name: "eslint"
|
|
36
|
+
versions: [">=10"]
|
|
37
|
+
`
|
|
38
|
+
|
|
27
39
|
program
|
|
28
40
|
.name( 'github-actions-it' )
|
|
29
41
|
.description( chalk.green( 'Installs or updates GitHub Actions CI configuration for a Leverege library repository' ) )
|
|
@@ -31,11 +43,12 @@ program
|
|
|
31
43
|
.option( '--dry-run', 'Show what would be written without making changes' )
|
|
32
44
|
.addHelpText( 'after', `
|
|
33
45
|
${chalk.yellow( 'Files managed:' )}
|
|
34
|
-
${chalk.green( GH_CI_YML )}
|
|
46
|
+
${chalk.green( GH_CI_YML )} GitHub Actions CI workflow
|
|
47
|
+
${chalk.green( GH_DEPENDABOT_YML )} weekly npm dependency updates
|
|
35
48
|
|
|
36
49
|
${chalk.yellow( 'Notes:' )}
|
|
37
50
|
Run from anywhere inside a git repository.
|
|
38
|
-
|
|
51
|
+
Preserves settings from an existing ci.yml when updating.
|
|
39
52
|
Redis is always available on port 6379 — no configuration needed.
|
|
40
53
|
Yarn auth and nodeLinker are handled by the workflow writing ~/.yarnrc.yml on the runner.
|
|
41
54
|
|
|
@@ -53,60 +66,11 @@ if ( !gitRoot ) {
|
|
|
53
66
|
errorExit( 'This command must be run inside a git repository' )
|
|
54
67
|
}
|
|
55
68
|
|
|
56
|
-
// ----------
|
|
57
|
-
|
|
58
|
-
const circleYmlPath = path.join( gitRoot, CIRCLE_YML )
|
|
69
|
+
// ---------- Defaults ----------
|
|
59
70
|
|
|
60
71
|
let enableGcp = false
|
|
61
72
|
let coverageThreshold = DEFAULT_COVERAGE_THRESHOLD
|
|
62
73
|
let cacheConfig = DEFAULT_CACHE_CONFIG
|
|
63
|
-
let redisEnabled = false
|
|
64
|
-
let isYarnFromCircle = false
|
|
65
|
-
|
|
66
|
-
if ( fs.existsSync( circleYmlPath ) ) {
|
|
67
|
-
const circleConfig = YAML.load( fs.readFileSync( circleYmlPath, 'utf8' ) )
|
|
68
|
-
|
|
69
|
-
const allJobs = []
|
|
70
|
-
for ( const workflow of Object.values( circleConfig.workflows ?? {} ) ) {
|
|
71
|
-
for ( const job of ( workflow.jobs ?? [] ) ) {
|
|
72
|
-
if ( typeof job === 'object' ) {
|
|
73
|
-
for ( const [ name, config ] of Object.entries( job ) ) {
|
|
74
|
-
allJobs.push( { name, config: config ?? {} } )
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const coverageJob = allJobs.find( j => j.name === 'leverege/code-coverage' )
|
|
81
|
-
if ( coverageJob ) {
|
|
82
|
-
const { config } = coverageJob
|
|
83
|
-
coverageThreshold = config['coverage-threshold'] ?? DEFAULT_COVERAGE_THRESHOLD
|
|
84
|
-
cacheConfig = config['cache-config'] ?? DEFAULT_CACHE_CONFIG
|
|
85
|
-
redisEnabled = config['enable-redis'] === true
|
|
86
|
-
|
|
87
|
-
const ctx = config.context
|
|
88
|
-
const contexts = Array.isArray( ctx ) ? ctx : ( ctx ? [ ctx ] : [] )
|
|
89
|
-
enableGcp = contexts.includes( 'leverege-gcp' )
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
isYarnFromCircle = allJobs.some( j => j.config['pkg-manager'] === 'yarn-berry' )
|
|
93
|
-
|
|
94
|
-
log( chalk.blue( '\nParsed CircleCI config:' ) )
|
|
95
|
-
log( ` pkg-manager: ${isYarnFromCircle ? 'yarn-berry' : 'npm'}` )
|
|
96
|
-
log( ` enable-gcp: ${enableGcp}` )
|
|
97
|
-
log( ` coverage-threshold: ${coverageThreshold}` )
|
|
98
|
-
log( ` cache-config: ${cacheConfig}` )
|
|
99
|
-
log( ` enable-redis: ${redisEnabled}` )
|
|
100
|
-
} else {
|
|
101
|
-
log( chalk.yellow( '\nNo .circleci/config.yml found — using defaults' ) )
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const isYarn = isYarnFromCircle || getIsYarnProject( gitRoot )
|
|
105
|
-
log( chalk.blue( `\nPackage manager: ${isYarn ? 'yarn-berry' : 'npm'}` ) )
|
|
106
|
-
|
|
107
|
-
if ( redisEnabled ) {
|
|
108
|
-
log( chalk.yellow( '\nRedis detected — Redis service is always available on port 6379 in GitHub Actions' ) )
|
|
109
|
-
}
|
|
110
74
|
|
|
111
75
|
// ---------- Determine what needs writing ----------
|
|
112
76
|
|
|
@@ -116,13 +80,33 @@ let ciYmlExists = false
|
|
|
116
80
|
if ( fs.existsSync( ciYmlPath ) ) {
|
|
117
81
|
ciYmlExists = true
|
|
118
82
|
const existingCi = fs.readFileSync( ciYmlPath, 'utf8' )
|
|
119
|
-
if ( existingCi.includes( `
|
|
83
|
+
if ( existingCi.includes( `node-ci.yml@${GH_ACTIONS_REF}` ) ) {
|
|
120
84
|
log( chalk.yellow( '\nExisting GitHub Actions CI config found — will update' ) )
|
|
85
|
+
try {
|
|
86
|
+
const existingParsed = YAML.load( existingCi )
|
|
87
|
+
const existingWith = Object.values( existingParsed?.jobs ?? {} )[0]?.with ?? {}
|
|
88
|
+
if ( existingWith['enable-gcp'] != null ) enableGcp = existingWith['enable-gcp']
|
|
89
|
+
if ( existingWith['coverage-threshold'] != null ) coverageThreshold = existingWith['coverage-threshold']
|
|
90
|
+
if ( existingWith['cache-config'] != null ) cacheConfig = existingWith['cache-config']
|
|
91
|
+
log( chalk.blue( '\nPreserved from existing ci.yml:' ) )
|
|
92
|
+
log( ` enable-gcp: ${enableGcp}` )
|
|
93
|
+
log( ` coverage-threshold: ${coverageThreshold}` )
|
|
94
|
+
log( ` cache-config: ${cacheConfig}` )
|
|
95
|
+
} catch {
|
|
96
|
+
log( chalk.yellow( 'Could not parse existing ci.yml — using defaults' ) )
|
|
97
|
+
}
|
|
121
98
|
} else {
|
|
122
99
|
log( chalk.yellow( '\nExisting .github/workflows/ci.yml found (not managed by github-actions-it) — will overwrite' ) )
|
|
123
100
|
}
|
|
124
101
|
}
|
|
125
102
|
|
|
103
|
+
if ( fs.existsSync( path.join( gitRoot, '.circleci/config.yml' ) ) ) {
|
|
104
|
+
log( chalk.yellow( '\n*** WARNING: .circleci/config.yml detected — CircleCI is deprecated. Remove it once GitHub Actions CI is confirmed green.\n' ) )
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const isYarn = getIsYarnProject( gitRoot )
|
|
108
|
+
log( chalk.blue( `\nPackage manager: ${isYarn ? 'yarn-berry' : 'npm'}` ) )
|
|
109
|
+
|
|
126
110
|
// ---------- Generate content ----------
|
|
127
111
|
|
|
128
112
|
function generateCiYml() {
|
|
@@ -147,11 +131,11 @@ function generateCiYml() {
|
|
|
147
131
|
' workflow_dispatch:',
|
|
148
132
|
'',
|
|
149
133
|
'jobs:',
|
|
150
|
-
' ci:',
|
|
134
|
+
' node-ci:',
|
|
151
135
|
' permissions:',
|
|
152
136
|
' id-token: write',
|
|
153
137
|
' contents: read',
|
|
154
|
-
` uses: Leverege/github-actions/.github/workflows/
|
|
138
|
+
` uses: Leverege/github-actions/.github/workflows/node-ci.yml@${GH_ACTIONS_REF}`,
|
|
155
139
|
]
|
|
156
140
|
|
|
157
141
|
if ( withLines.length > 0 ) {
|
|
@@ -168,6 +152,8 @@ function generateCiYml() {
|
|
|
168
152
|
if ( options.dryRun ) {
|
|
169
153
|
log( chalk.green( `\n[dry-run] Would write ${GH_CI_YML}:\n` ) )
|
|
170
154
|
log( generateCiYml() )
|
|
155
|
+
log( chalk.green( `\n[dry-run] Would write ${GH_DEPENDABOT_YML}:\n` ) )
|
|
156
|
+
log( DEPENDABOT_CONTENT )
|
|
171
157
|
process.exit( 0 )
|
|
172
158
|
}
|
|
173
159
|
|
|
@@ -183,5 +169,9 @@ mkdirSafe( path.join( gitRoot, GH_WORKFLOWS_DIR ) )
|
|
|
183
169
|
fs.writeFileSync( ciYmlPath, generateCiYml() )
|
|
184
170
|
log( chalk.green( `\nGitHub Actions CI config written => ${GH_CI_YML}` ) )
|
|
185
171
|
|
|
172
|
+
const dependabotPath = path.join( gitRoot, GH_DEPENDABOT_YML )
|
|
173
|
+
fs.writeFileSync( dependabotPath, DEPENDABOT_CONTENT )
|
|
174
|
+
log( chalk.green( `Dependabot config written => ${GH_DEPENDABOT_YML}` ) )
|
|
175
|
+
|
|
186
176
|
log( chalk.yellow( '\nNext steps:' ) )
|
|
187
|
-
log( ` git add ${GH_CI_YML} && git commit -m "CHORE add GitHub Actions CI"\n` )
|
|
177
|
+
log( ` git add ${GH_CI_YML} ${GH_DEPENDABOT_YML} && git commit -m "CHORE add GitHub Actions CI"\n` )
|
|
@@ -124,6 +124,29 @@ SKIP_CNPG_SA_CREATION
|
|
|
124
124
|
gsutil ubla set on $BARMAN_BUCKET # uniform bucket access
|
|
125
125
|
fi
|
|
126
126
|
|
|
127
|
+
# Apply WAL lifecycle policy idempotently: Coldline after 7 days, delete after 30.
|
|
128
|
+
# Matches the 21-day snapshot retention window with buffer.
|
|
129
|
+
if gsutil lifecycle get $BARMAN_BUCKET 2>/dev/null | grep -q "COLDLINE"; then
|
|
130
|
+
printf "\nBarman bucket lifecycle already configured - skipping\n"
|
|
131
|
+
else
|
|
132
|
+
printf "\nApplying lifecycle policy to barman bucket `color g $BARMAN_BUCKET`\n"
|
|
133
|
+
cat<<LIFECYCLE | gsutil lifecycle set /dev/stdin $BARMAN_BUCKET
|
|
134
|
+
{
|
|
135
|
+
"rule": [
|
|
136
|
+
{
|
|
137
|
+
"action": {"type": "SetStorageClass", "storageClass": "COLDLINE"},
|
|
138
|
+
"condition": {"age": 7}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"action": {"type": "Delete"},
|
|
142
|
+
"condition": {"age": 30}
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
LIFECYCLE
|
|
147
|
+
exitOnError $? "Failed to set lifecycle policy on $BARMAN_BUCKET"
|
|
148
|
+
fi
|
|
149
|
+
|
|
127
150
|
# define the snapshot storage class
|
|
128
151
|
printf "\nApplying the CNPG VolumeSnapshotClass `color g cnpg-snapshotclass` definition\n"
|
|
129
152
|
cat<<EOSNAPSC | kubectl apply -f -
|
|
@@ -8,7 +8,7 @@ showInstalling "The Prometheus Operator (kube-prometheus-stack)"
|
|
|
8
8
|
|
|
9
9
|
# https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack
|
|
10
10
|
OCI_CHART="oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack"
|
|
11
|
-
[ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="
|
|
11
|
+
[ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="88.5.4"
|
|
12
12
|
helm upgrade $NS --install prometheus-stack $OCI_CHART \
|
|
13
13
|
--values prom-operator/prometheus-stack.yaml \
|
|
14
14
|
--version $PROMETHEUS_STACK_CHART_VERSION $HELM_WHAT
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# triton-inference-leash
|
|
2
|
+
|
|
3
|
+
Watches for GCP zone drift between a leader pod (`triton-inference-server`) and one or more follower pods, and rolls the follower back into alignment when they diverge.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Inter-zone egress between co-located services costs ~$50–70/day/cluster. Pod affinity rules keep them co-located at scheduling time, but if the leader reschedules to a different zone (node failure, eviction, etc.) the existing follower pods stay put. This watcher detects the drift and rolls the follower deployment so its affinity rules re-land it in the leader's zone.
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
Every 60 seconds the watcher:
|
|
12
|
+
1. Fetches all node → zone mappings in a single `kubectl get nodes` call
|
|
13
|
+
2. Looks up the zone of the running leader pod
|
|
14
|
+
3. Looks up the zone of **every** running follower pod
|
|
15
|
+
4. If any follower pod is outside the leader's zone → triggers `kubectl rollout restart` on the follower deployment
|
|
16
|
+
5. The existing pod affinity on the follower reschedules all pods into the leader's zone
|
|
17
|
+
|
|
18
|
+
A 5-minute cooldown prevents back-to-back rollouts if something is thrashing.
|
|
19
|
+
|
|
20
|
+
## Deploy
|
|
21
|
+
|
|
22
|
+
Before deploying, set `FOLLOWER_LABEL` and `FOLLOWER_DEPLOY` in the Deployment env block — both default to `REPLACE_ME_BEFORE_DEPLOYING` and the container will exit with an error if left unset.
|
|
23
|
+
|
|
24
|
+
Deploy it:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
helmup triton-inference-leash
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Shut it down:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
helmdn triton-inference-leash
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Debug mode
|
|
37
|
+
|
|
38
|
+
Debug mode logs the actions the watcher **would** take without actually rolling the follower. Useful for validating zone detection before going live.
|
|
39
|
+
|
|
40
|
+
To deploy with debug on, set `DEBUG: "true"` in the Deployment env block before applying:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
- name: DEBUG
|
|
44
|
+
value: "true"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then watch the logs:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
kubectl logs -f -n default -l app=triton-inference-leash
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You should see `DRY-RUN: kubectl rollout restart ...` instead of an actual roll when a mismatch is detected. Set `DEBUG: "false"` and re-apply to go live.
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
| Variable | Default | Description |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `NAMESPACE` | `default` | Namespace where both services run |
|
|
60
|
+
| `LEADER_LABEL` | `app.kubernetes.io/instance=triton-inference-server` | Label selector for leader pods |
|
|
61
|
+
| `FOLLOWER_DEPLOY` | *(required)* | Helm release name of the follower service — used as the Deployment name and to derive its pod label (`app.kubernetes.io/instance=<name>`) |
|
|
62
|
+
| `POLL_INTERVAL` | `60` | Seconds between zone checks |
|
|
63
|
+
| `COOLDOWN` | `300` | Seconds to wait before triggering another rollout |
|
|
64
|
+
| `DEBUG` | `false` | Dry-run mode — logs actions without executing them |
|
|
65
|
+
|
|
66
|
+
## Monitoring
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Tail live logs
|
|
70
|
+
k8s log triton-inference-leash
|
|
71
|
+
|
|
72
|
+
# Check pod status
|
|
73
|
+
kubectl get pod -n default -l app=triton-inference-leash
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Log output examples:
|
|
77
|
+
```
|
|
78
|
+
2026-08-20T14:00:00Z OK: leader=us-central1-b follower=[us-central1-b]
|
|
79
|
+
2026-08-20T15:00:00Z OK: leader=us-central1-b follower=[us-central1-b]
|
|
80
|
+
2026-08-20T15:01:00Z MISMATCH: leader=us-central1-b follower=[us-central1-b us-central1-c] — rolling my-follower-service
|
|
81
|
+
2026-08-20T15:03:12Z Rollout complete
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`OK` lines are logged once per hour in normal mode, and every poll cycle in debug mode.
|
|
85
|
+
|
|
86
|
+
## Adapting for other clusters
|
|
87
|
+
|
|
88
|
+
Copy the manifest, fill in `FOLLOWER_LABEL` and `FOLLOWER_DEPLOY` for the target cluster, and apply. Everything else can be left at defaults.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Watches for zone drift between a leader pod (triton-inference-server) and follower pods.
|
|
2
|
+
# When they land in different zones, inter-zone egress costs ~$50-70/day/cluster.
|
|
3
|
+
# On mismatch, rolls the follower deployment so its pod affinity re-lands it with the leader.
|
|
4
|
+
#
|
|
5
|
+
---
|
|
6
|
+
apiVersion: v1
|
|
7
|
+
kind: ServiceAccount
|
|
8
|
+
metadata:
|
|
9
|
+
name: triton-inference-leash
|
|
10
|
+
namespace: default
|
|
11
|
+
---
|
|
12
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
13
|
+
kind: ClusterRole
|
|
14
|
+
metadata:
|
|
15
|
+
name: triton-inference-leash-nodes
|
|
16
|
+
rules:
|
|
17
|
+
- apiGroups: [""]
|
|
18
|
+
resources: ["nodes"]
|
|
19
|
+
verbs: ["get", "list"]
|
|
20
|
+
---
|
|
21
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
22
|
+
kind: ClusterRoleBinding
|
|
23
|
+
metadata:
|
|
24
|
+
name: triton-inference-leash-nodes
|
|
25
|
+
subjects:
|
|
26
|
+
- kind: ServiceAccount
|
|
27
|
+
name: triton-inference-leash
|
|
28
|
+
namespace: default
|
|
29
|
+
roleRef:
|
|
30
|
+
kind: ClusterRole
|
|
31
|
+
name: triton-inference-leash-nodes
|
|
32
|
+
apiGroup: rbac.authorization.k8s.io
|
|
33
|
+
---
|
|
34
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
35
|
+
kind: Role
|
|
36
|
+
metadata:
|
|
37
|
+
name: triton-inference-leash
|
|
38
|
+
namespace: default
|
|
39
|
+
rules:
|
|
40
|
+
- apiGroups: [""]
|
|
41
|
+
resources: ["pods"]
|
|
42
|
+
verbs: ["get", "list"]
|
|
43
|
+
- apiGroups: ["apps"]
|
|
44
|
+
resources: ["deployments"]
|
|
45
|
+
verbs: ["get", "list", "watch", "patch"]
|
|
46
|
+
- apiGroups: ["apps"]
|
|
47
|
+
resources: ["replicasets"]
|
|
48
|
+
verbs: ["get", "list", "watch"]
|
|
49
|
+
---
|
|
50
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
51
|
+
kind: RoleBinding
|
|
52
|
+
metadata:
|
|
53
|
+
name: triton-inference-leash
|
|
54
|
+
namespace: default
|
|
55
|
+
subjects:
|
|
56
|
+
- kind: ServiceAccount
|
|
57
|
+
name: triton-inference-leash
|
|
58
|
+
namespace: default
|
|
59
|
+
roleRef:
|
|
60
|
+
kind: Role
|
|
61
|
+
name: triton-inference-leash
|
|
62
|
+
apiGroup: rbac.authorization.k8s.io
|
|
63
|
+
---
|
|
64
|
+
apiVersion: v1
|
|
65
|
+
kind: ConfigMap
|
|
66
|
+
metadata:
|
|
67
|
+
name: triton-inference-leash-script
|
|
68
|
+
namespace: default
|
|
69
|
+
data:
|
|
70
|
+
leash.sh: |
|
|
71
|
+
#!/bin/bash
|
|
72
|
+
set -uo pipefail
|
|
73
|
+
|
|
74
|
+
NAMESPACE="${NAMESPACE:-default}"
|
|
75
|
+
LEADER_LABEL="${LEADER_LABEL:-app.kubernetes.io/instance=triton-inference-server}"
|
|
76
|
+
# FOLLOWER_LABEL: Helm release name of the follower service.
|
|
77
|
+
# Used as both the Deployment name and to derive its pod selector
|
|
78
|
+
# (app.kubernetes.io/instance=<FOLLOWER_LABEL>). Set in the Deployment env block.
|
|
79
|
+
POLL_INTERVAL="${POLL_INTERVAL:-60}"
|
|
80
|
+
COOLDOWN="${COOLDOWN:-300}"
|
|
81
|
+
DEBUG="${DEBUG:-false}"
|
|
82
|
+
|
|
83
|
+
log() { echo "$(date -u '+%Y-%m-%dT%H:%M:%SZ') $*"; }
|
|
84
|
+
dryrun() { [ "$DEBUG" = "true" ] && log "DRY-RUN: $*" || eval "$@"; }
|
|
85
|
+
|
|
86
|
+
[ -z "${FOLLOWER_LABEL:-}" ] && { log "ERROR: FOLLOWER_LABEL is required"; exit 1; }
|
|
87
|
+
FOLLOWER_SELECTOR="app.kubernetes.io/instance=${FOLLOWER_LABEL}"
|
|
88
|
+
|
|
89
|
+
# nodeName -> zone for every node, fetched in a single call per poll.
|
|
90
|
+
# kubectl process startup is a CPU burst, so the loop keeps its call count flat
|
|
91
|
+
# regardless of follower replica count.
|
|
92
|
+
declare -A NODE_ZONE
|
|
93
|
+
|
|
94
|
+
load_node_zones() {
|
|
95
|
+
local name zone
|
|
96
|
+
NODE_ZONE=()
|
|
97
|
+
while read -r name zone; do
|
|
98
|
+
[ -n "$name" ] && NODE_ZONE[$name]=$zone
|
|
99
|
+
done < <(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.metadata.labels.topology\.kubernetes\.io/zone}{"\n"}{end}' 2>/dev/null)
|
|
100
|
+
[ "${#NODE_ZONE[@]}" -gt 0 ]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Returns the node names of every running pod matching a label
|
|
104
|
+
get_pod_nodes() {
|
|
105
|
+
kubectl get pods -n "$NAMESPACE" -l "$1" \
|
|
106
|
+
--field-selector=status.phase=Running \
|
|
107
|
+
-o jsonpath='{.items[*].spec.nodeName}' 2>/dev/null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
log "triton-inference-leash starting (poll=${POLL_INTERVAL}s cooldown=${COOLDOWN}s debug=${DEBUG})"
|
|
111
|
+
log " leader: ${LEADER_LABEL}"
|
|
112
|
+
log " follower: ${FOLLOWER_SELECTOR}"
|
|
113
|
+
|
|
114
|
+
last_rollout=0
|
|
115
|
+
last_ok_log=0
|
|
116
|
+
|
|
117
|
+
while true; do
|
|
118
|
+
sleep "$POLL_INTERVAL"
|
|
119
|
+
|
|
120
|
+
load_node_zones || {
|
|
121
|
+
log "SKIP: node list unavailable"
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
leader_nodes=$(get_pod_nodes "$LEADER_LABEL")
|
|
126
|
+
leader_zone=${NODE_ZONE[${leader_nodes%% *}]:-}
|
|
127
|
+
[ -z "$leader_zone" ] && {
|
|
128
|
+
log "SKIP: leader pod not running or node lookup failed"
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
follower_nodes=$(get_pod_nodes "$FOLLOWER_SELECTOR")
|
|
133
|
+
[ -z "$follower_nodes" ] && {
|
|
134
|
+
log "SKIP: follower pods not running or node lookup failed"
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Flag mismatch if any follower pod is outside the leader's zone
|
|
139
|
+
mismatch=false
|
|
140
|
+
follower_zones=""
|
|
141
|
+
for node in $follower_nodes; do
|
|
142
|
+
zone=${NODE_ZONE[$node]:-}
|
|
143
|
+
[ -z "$zone" ] && continue
|
|
144
|
+
follower_zones="${follower_zones}${zone}"$'\n'
|
|
145
|
+
[ "$zone" != "$leader_zone" ] && mismatch=true
|
|
146
|
+
done
|
|
147
|
+
|
|
148
|
+
follower_zone_summary=$(echo "$follower_zones" | sort -u | tr '\n' ' ' | xargs)
|
|
149
|
+
|
|
150
|
+
now=$(date +%s)
|
|
151
|
+
|
|
152
|
+
if [ "$mismatch" = "false" ]; then
|
|
153
|
+
if [ "$DEBUG" = "true" ] || [ $((now - last_ok_log)) -ge 3600 ]; then
|
|
154
|
+
log "OK: leader=${leader_zone} follower=[${follower_zone_summary}]"
|
|
155
|
+
last_ok_log=$now
|
|
156
|
+
fi
|
|
157
|
+
continue
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
since=$((now - last_rollout))
|
|
161
|
+
if [ "$since" -lt "$COOLDOWN" ]; then
|
|
162
|
+
log "MISMATCH (leader=${leader_zone} follower=[${follower_zone_summary}]) — cooldown ${since}s/${COOLDOWN}s, skipping"
|
|
163
|
+
continue
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
log "MISMATCH: leader=${leader_zone} follower=[${follower_zone_summary}] — rolling ${FOLLOWER_LABEL}"
|
|
167
|
+
dryrun kubectl rollout restart deployment/"$FOLLOWER_LABEL" -n "$NAMESPACE"
|
|
168
|
+
last_rollout=$(date +%s)
|
|
169
|
+
dryrun kubectl rollout status deployment/"$FOLLOWER_LABEL" -n "$NAMESPACE" --timeout=5m \
|
|
170
|
+
&& log "Rollout complete" \
|
|
171
|
+
|| log "WARNING: rollout status timed out — check deployment manually"
|
|
172
|
+
done
|
|
173
|
+
---
|
|
174
|
+
apiVersion: apps/v1
|
|
175
|
+
kind: Deployment
|
|
176
|
+
metadata:
|
|
177
|
+
name: triton-inference-leash
|
|
178
|
+
namespace: default
|
|
179
|
+
labels:
|
|
180
|
+
app: triton-inference-leash
|
|
181
|
+
spec:
|
|
182
|
+
replicas: 1
|
|
183
|
+
selector:
|
|
184
|
+
matchLabels:
|
|
185
|
+
app: triton-inference-leash
|
|
186
|
+
template:
|
|
187
|
+
metadata:
|
|
188
|
+
labels:
|
|
189
|
+
app: triton-inference-leash
|
|
190
|
+
spec:
|
|
191
|
+
serviceAccountName: triton-inference-leash
|
|
192
|
+
terminationGracePeriodSeconds: 10
|
|
193
|
+
containers:
|
|
194
|
+
- name: leash
|
|
195
|
+
image: us-docker.pkg.dev/leverege-registry/system/images/leverege-kubectl:v1.31.0
|
|
196
|
+
command: ["/bin/bash", "/scripts/leash.sh"]
|
|
197
|
+
env:
|
|
198
|
+
- name: NAMESPACE
|
|
199
|
+
value: "default"
|
|
200
|
+
- name: LEADER_LABEL
|
|
201
|
+
value: "app.kubernetes.io/instance=triton-inference-server"
|
|
202
|
+
# Helm release name of the follower service — e.g. pitcrew-vision-logic-server
|
|
203
|
+
- name: FOLLOWER_LABEL
|
|
204
|
+
value: "REPLACE_ME_BEFORE_DEPLOYING"
|
|
205
|
+
- name: POLL_INTERVAL
|
|
206
|
+
value: "60"
|
|
207
|
+
- name: COOLDOWN
|
|
208
|
+
value: "300"
|
|
209
|
+
- name: DEBUG
|
|
210
|
+
value: "false"
|
|
211
|
+
resources:
|
|
212
|
+
# No cpu limit on purpose. Each kubectl invocation is a sub-second
|
|
213
|
+
# full-core burst, so any CFS ceiling below a core clips nearly every
|
|
214
|
+
# period the container is awake and keeps ContainerHighThrottleRate
|
|
215
|
+
# firing on an otherwise idle pod. The request carries the scheduling
|
|
216
|
+
# weight; memory stays capped.
|
|
217
|
+
requests:
|
|
218
|
+
cpu: 50m
|
|
219
|
+
memory: 64Mi
|
|
220
|
+
limits:
|
|
221
|
+
memory: 128Mi
|
|
222
|
+
volumeMounts:
|
|
223
|
+
- name: scripts
|
|
224
|
+
mountPath: /scripts
|
|
225
|
+
volumes:
|
|
226
|
+
- name: scripts
|
|
227
|
+
configMap:
|
|
228
|
+
name: triton-inference-leash-script
|
|
229
|
+
defaultMode: 0755
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
showInstalling "Triton Inference Leash"
|
|
4
|
+
|
|
5
|
+
if grep -q "REPLACE_ME_BEFORE_DEPLOYING" triton-inference-leash/follow-me.yaml; then
|
|
6
|
+
echo "ERROR: follow-me.yaml still contains placeholder values — set FOLLOWER_LABEL and FOLLOWER_DEPLOY before deploying"
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
kubectl apply -f triton-inference-leash/follow-me.yaml $K8S_WHAT
|
|
11
|
+
|
|
12
|
+
cat<<MONITOR_LEASH
|
|
13
|
+
|
|
14
|
+
Monitor with => `color g "kubectl logs -f -n default -l app=triton-inference-leash"`
|
|
15
|
+
|
|
16
|
+
MONITOR_LEASH
|
package/src/helmup.sh
CHANGED
|
@@ -59,7 +59,7 @@ for arg in "$@"; do
|
|
|
59
59
|
export BUILD_TOOLS_DEBUG=1
|
|
60
60
|
;;
|
|
61
61
|
--dry-run)
|
|
62
|
-
export HELM_WHAT="--dry-run --debug"
|
|
62
|
+
export HELM_WHAT="--dry-run=client --debug"
|
|
63
63
|
export K8S_WHAT="--dry-run=server -o yaml"
|
|
64
64
|
;;
|
|
65
65
|
*)
|
|
@@ -741,7 +741,7 @@ function doInstall() {
|
|
|
741
741
|
[ ! -z "$CHARTVER" ] && CHARTVER="--version $CHARTVER"
|
|
742
742
|
|
|
743
743
|
# set the dry-run flag on what
|
|
744
|
-
[ "$INVOKED_AS" == "helmwhat" ] && HELM_WHAT="--dry-run --debug" && K8S_WHAT="--dry-run=server -o yaml"
|
|
744
|
+
[ "$INVOKED_AS" == "helmwhat" ] && HELM_WHAT="--dry-run=client --debug" && K8S_WHAT="--dry-run=server -o yaml"
|
|
745
745
|
|
|
746
746
|
# helminit is used to redeploy an existing service
|
|
747
747
|
if [[ "$INVOKED_AS" == "helminit" ]] && isLeveregeService $SERVICE;
|
package/src/k8scale.sh
CHANGED
|
@@ -45,8 +45,10 @@ do
|
|
|
45
45
|
RESULTS="`color g SUCCESS`"
|
|
46
46
|
if [ "$DIRECTION" == "roll" ];
|
|
47
47
|
then
|
|
48
|
+
btDeprecated "See => `color g 'k8s roll --help'`"
|
|
48
49
|
kubectl rollout restart deployment $SERVICE &> $DEVNULL
|
|
49
50
|
else
|
|
51
|
+
btDeprecated "See => `color g 'k8s scale --help'`"
|
|
50
52
|
kubectl scale --replicas=$REPLICAS deployment $SERVICE &> $DEVNULL
|
|
51
53
|
fi
|
|
52
54
|
[ $? -eq 1 ] && RESULTS="`color r FAILURE`"
|
package/src/k8x.sh
CHANGED
package/src/klog.sh
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
+
. `build-tools --bashfun`
|
|
4
|
+
|
|
3
5
|
overwhelm
|
|
4
6
|
|
|
5
7
|
DTS="`date +\"%Y%m%d-%H%M%S\"`"
|
|
6
8
|
PRETTY="pino-pretty -c -t 'HH:MM:ss.l' -i 'severity'"
|
|
7
9
|
IFS="/" read APP <<< "$1"
|
|
8
10
|
|
|
11
|
+
btDeprecated "See => `color g 'k8s log --help'`"
|
|
12
|
+
|
|
9
13
|
if [ ! -z "$APP" ];
|
|
10
14
|
then
|
|
11
15
|
NS=${2:-"default"}
|
package/src/overwhelm.mjs
CHANGED
|
@@ -400,6 +400,9 @@ log( `\n\nExecuting: ${chalk.bold.yellow( cmd )}\n\n` )
|
|
|
400
400
|
try {
|
|
401
401
|
await shellCmd( cmd )
|
|
402
402
|
} catch ( error ) {
|
|
403
|
-
|
|
403
|
+
if ( error.stderr ) {
|
|
404
|
+
err( chalk.red( error.stderr ) )
|
|
405
|
+
}
|
|
406
|
+
err( chalk.red( '\n***ERROR: failed to fetch the Kubernetes context\n' ) )
|
|
404
407
|
process.exit( 1 )
|
|
405
408
|
}
|
package/src/push-my-chart.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
parseHelmChart,
|
|
12
12
|
proceed,
|
|
13
13
|
shellCmd,
|
|
14
|
+
sleep,
|
|
14
15
|
warning,
|
|
15
16
|
} from './Utils.mjs'
|
|
16
17
|
|
|
@@ -29,6 +30,19 @@ without disrupting current configurations.` ) )
|
|
|
29
30
|
program.parse( process.argv )
|
|
30
31
|
const opts = program.opts()
|
|
31
32
|
|
|
33
|
+
// DEPRECATED
|
|
34
|
+
log( `
|
|
35
|
+
${chalk.red.bold( '*** DEPRECATED *** the museum is closing *** DEPRECATED ***' )}
|
|
36
|
+
|
|
37
|
+
${chalk.yellow( 'modify the package.json museum script to use chart-to-registry instead' )}
|
|
38
|
+
|
|
39
|
+
${chalk.green.bold( ' "museum": "chart-to-registry",' )}
|
|
40
|
+
|
|
41
|
+
` )
|
|
42
|
+
|
|
43
|
+
await sleep( 3000 )
|
|
44
|
+
// DEPRECATED
|
|
45
|
+
|
|
32
46
|
const {
|
|
33
47
|
chartName,
|
|
34
48
|
chartVersion,
|
package/src/repo-panopticon.mjs
CHANGED
|
@@ -29,7 +29,10 @@ const CHECKS = [
|
|
|
29
29
|
label : 'cir',
|
|
30
30
|
fn : ( { gitRoot } ) => {
|
|
31
31
|
const cfgPath = path.join( gitRoot, '.circleci', 'config.yml' )
|
|
32
|
-
if ( !fs.existsSync( cfgPath ) )
|
|
32
|
+
if ( !fs.existsSync( cfgPath ) ) {
|
|
33
|
+
const hasGHA = fs.existsSync( path.join( gitRoot, '.github', 'workflows', 'ci.yml' ) )
|
|
34
|
+
return { value : hasGHA ? chalk.cyan( '★' ) : SYM_BAD }
|
|
35
|
+
}
|
|
33
36
|
try {
|
|
34
37
|
const content = fs.readFileSync( cfgPath, 'utf8' )
|
|
35
38
|
const match = content.match( /leverege\/circle-nodejs-ci@(\d+\.\d+\.\d+)/ )
|
|
@@ -339,7 +342,7 @@ ${chalk.yellow( 'Columns:' )}
|
|
|
339
342
|
${chalk.green( 'tag' )} latest semver git tag (green=match, yellow=mismatch, red=untagged)
|
|
340
343
|
${chalk.green( 'name' )} orange if dirty: pull/fetch failed (regular repos) or local modifications present (workspace packages)
|
|
341
344
|
${chalk.green( 'scope' )} package scope: @org, PRVT, or PUBL
|
|
342
|
-
${chalk.green( 'cir' )} CI status: ✘=missing, L=legacy, 1=orb v1, green 2/★=orb v2 current, yellow 2/★=orb v2 stale
|
|
345
|
+
${chalk.green( 'cir' )} CI status: ✘=missing, cyan ★=fully on GHA, L=legacy, 1=orb v1, green 2/★=orb v2 current, yellow 2/★=orb v2 stale
|
|
343
346
|
${chalk.green( 'ESM' )} "type": "module" in package.json
|
|
344
347
|
${chalk.green( 'har' )} .har directory present at repo root
|
|
345
348
|
${chalk.green( 'lnt' )} ESLint version from node_modules (✘=no lint script, ?=not installed, yellow=v8, green=v9+)
|
|
File without changes
|
|
File without changes
|