@nitra/cursor 1.8.156 → 1.8.157
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/CHANGELOG.md +18 -0
- package/bin/auto-rules.md +2 -0
- package/mdc/hasura.mdc +31 -0
- package/mdc/js-lint.mdc +29 -4
- package/mdc/js-mssql.mdc +1 -1
- package/mdc/js-run.mdc +3 -9
- package/mdc/npm-module.mdc +9 -1
- package/package.json +3 -2
- package/scripts/auto-rules.mjs +58 -27
- package/scripts/build-agents-commands.mjs +7 -7
- package/scripts/check-hasura.mjs +219 -0
- package/scripts/check-js-bun-db.mjs +64 -45
- package/scripts/check-js-lint.mjs +10 -8
- package/scripts/check-js-run.mjs +49 -29
- package/scripts/check-k8s.mjs +455 -197
- package/scripts/check-npm-module.mjs +55 -0
- package/scripts/utils/bun-sql-scan.mjs +5 -2
- package/scripts/utils/check-env-scan.mjs +89 -44
- package/scripts/utils/conn-imports-scan.mjs +13 -3
- package/scripts/utils/mssql-pool-scan.mjs +57 -38
package/scripts/check-k8s.mjs
CHANGED
|
@@ -5056,7 +5056,7 @@ async function validateDeploymentHpaPdbAndTopology(root, yamlFilesAbs, fail, pas
|
|
|
5056
5056
|
*
|
|
5057
5057
|
* Тег визначається лише по **останній** двокрапці; якщо після неї є `/` — це порт реєстру, не тег.
|
|
5058
5058
|
* @param {string} image рядок image
|
|
5059
|
-
* @returns {{ name: string, tag: string | null, hasDigest: boolean }}
|
|
5059
|
+
* @returns {{ name: string, tag: string | null, hasDigest: boolean }} ім'я (з реєстром/портом), тег (null якщо немає), та чи є digest
|
|
5060
5060
|
*/
|
|
5061
5061
|
export function splitImageNameTagDigest(image) {
|
|
5062
5062
|
if (image.includes('@')) {
|
|
@@ -5076,12 +5076,12 @@ export function splitImageNameTagDigest(image) {
|
|
|
5076
5076
|
/**
|
|
5077
5077
|
* Розпаковує YAML-скаляр з оточуючими лапками (single або double). Інші стилі (block scalar) — повертає як є.
|
|
5078
5078
|
* @param {string} raw сирий рядок-значення без trailing whitespace/comment
|
|
5079
|
-
* @returns {{ unquoted: string, quote: '' | "'" | '"' }}
|
|
5079
|
+
* @returns {{ unquoted: string, quote: '' | "'" | '"' }} текст без лапок та сам стиль лапок (порожній, якщо їх не було)
|
|
5080
5080
|
*/
|
|
5081
5081
|
function parseQuotedYamlScalar(raw) {
|
|
5082
5082
|
if (raw.length >= 2) {
|
|
5083
5083
|
const first = raw.charAt(0)
|
|
5084
|
-
const last = raw.
|
|
5084
|
+
const last = raw.at(-1)
|
|
5085
5085
|
if (first === '"' && last === '"') {
|
|
5086
5086
|
return { unquoted: raw.slice(1, -1), quote: '"' }
|
|
5087
5087
|
}
|
|
@@ -5108,100 +5108,203 @@ function requoteYamlScalar(value, quote) {
|
|
|
5108
5108
|
const KUSTOMIZATION_IMAGES_KEY_RE = /^images:\s*(?:#.*)?$/u
|
|
5109
5109
|
/** Regex: початок елемента масиву (`-` з відступом). Групує сам відступ перед `-`. */
|
|
5110
5110
|
const KUSTOMIZATION_LIST_ITEM_RE = /^(\s*)-\s/u
|
|
5111
|
-
/**
|
|
5112
|
-
|
|
5111
|
+
/**
|
|
5112
|
+
* Regex: значення поля (name / newName / newTag) у рядку, з опційним `- ` префіксом.
|
|
5113
|
+
* Захоплює увесь хвіст рядка одним капчером `valueWithTrailing`; коментар і trailing-пробіли
|
|
5114
|
+
* розбираються після матчу через {@link splitYamlValueAndTrailing} — це уникає вкладеного
|
|
5115
|
+
* `?` (lazy) + `?` (опційний хвіст), на який реагує `sonarjs/slow-regex`.
|
|
5116
|
+
*/
|
|
5117
|
+
const KUSTOMIZATION_IMAGE_FIELD_RE = /^(\s*(?:-\s+)?)(name|newName|newTag):(\s+)(\S[^\n]*)$/u
|
|
5118
|
+
|
|
5119
|
+
/**
|
|
5120
|
+
* Розщеплює правий бік YAML-рядка `<value>[<пробіли>#<comment>]` на «чистий» value та trailing
|
|
5121
|
+
* (пробіли + коментар), без використання regex з backtracking.
|
|
5122
|
+
* @param {string} valueWithTrailing «сирий» хвіст рядка після `<key>:<sep>`
|
|
5123
|
+
* @returns {{ value: string, trailing: string }} розбиті частини
|
|
5124
|
+
*/
|
|
5125
|
+
function splitYamlValueAndTrailing(valueWithTrailing) {
|
|
5126
|
+
const hashIdx = findCommentStart(valueWithTrailing)
|
|
5127
|
+
const upTo = hashIdx === -1 ? valueWithTrailing.length : hashIdx
|
|
5128
|
+
let valueEnd = upTo
|
|
5129
|
+
while (valueEnd > 0) {
|
|
5130
|
+
const code = valueWithTrailing.codePointAt(valueEnd - 1)
|
|
5131
|
+
if (code !== 32 && code !== 9 && code !== 10 && code !== 13) break
|
|
5132
|
+
valueEnd--
|
|
5133
|
+
}
|
|
5134
|
+
const value = valueWithTrailing.slice(0, valueEnd)
|
|
5135
|
+
return { value, trailing: valueWithTrailing.slice(valueEnd) }
|
|
5136
|
+
}
|
|
5137
|
+
|
|
5138
|
+
/**
|
|
5139
|
+
* Знаходить індекс стартового `#`-коментаря: перший `#`, перед яким є пробіл (інакше `#`
|
|
5140
|
+
* — частина значення). Повертає -1, якщо коментаря немає.
|
|
5141
|
+
* @param {string} s рядок (хвіст YAML-рядка)
|
|
5142
|
+
* @returns {number} індекс стартового `#` або -1
|
|
5143
|
+
*/
|
|
5144
|
+
function findCommentStart(s) {
|
|
5145
|
+
for (let i = 0; i < s.length; i++) {
|
|
5146
|
+
if (s.codePointAt(i) !== 35) continue
|
|
5147
|
+
if (i === 0) return i
|
|
5148
|
+
const prev = s.codePointAt(i - 1)
|
|
5149
|
+
if (prev === 32 || prev === 9) return i
|
|
5150
|
+
}
|
|
5151
|
+
return -1
|
|
5152
|
+
}
|
|
5153
|
+
/** Regex: рядок у блоці `images:` починається з пробілу/таба (належить блоку). */
|
|
5154
|
+
const KUSTOMIZATION_BLOCK_INDENT_RE = /^\s/u
|
|
5113
5155
|
|
|
5114
5156
|
/**
|
|
5115
5157
|
* Автофікс блоку `images:` у kustomization.yaml: зрізає `:tag` з `name` (digest `@…` не чіпає)
|
|
5116
5158
|
* і видаляє `newTag`, який збігається зі зрізаним тегом. Працює рядково, зберігаючи коментарі
|
|
5117
5159
|
* й форматування.
|
|
5118
5160
|
* @param {string} raw вміст файлу
|
|
5119
|
-
* @returns {{ changed: boolean, content: string }}
|
|
5161
|
+
* @returns {{ changed: boolean, content: string }} прапорець, чи були зміни, та (за потреби) очищений текст
|
|
5120
5162
|
*/
|
|
5121
5163
|
export function cleanupKustomizationImagesInYamlText(raw) {
|
|
5122
5164
|
const eol = raw.includes('\r\n') ? '\r\n' : '\n'
|
|
5123
5165
|
const lines = raw.split(YAML_LINE_SPLIT_RE)
|
|
5124
5166
|
|
|
5167
|
+
const imagesRange = findImagesBlockRange(lines)
|
|
5168
|
+
if (imagesRange === null) return { changed: false, content: raw }
|
|
5169
|
+
|
|
5170
|
+
const entries = splitImagesBlockEntries(lines, imagesRange.start, imagesRange.end)
|
|
5171
|
+
|
|
5172
|
+
/** @type {Map<number, string>} */
|
|
5173
|
+
const replacements = new Map()
|
|
5174
|
+
/** @type {Set<number>} */
|
|
5175
|
+
const removals = new Set()
|
|
5176
|
+
let changed = false
|
|
5177
|
+
|
|
5178
|
+
for (const entry of entries) {
|
|
5179
|
+
if (processImagesEntry(lines, entry, replacements, removals)) changed = true
|
|
5180
|
+
}
|
|
5181
|
+
|
|
5182
|
+
if (!changed) return { changed: false, content: raw }
|
|
5183
|
+
|
|
5184
|
+
/** @type {string[]} */
|
|
5185
|
+
const out = []
|
|
5186
|
+
for (const [i, line] of lines.entries()) {
|
|
5187
|
+
if (removals.has(i)) continue
|
|
5188
|
+
out.push(replacements.has(i) ? replacements.get(i) : line)
|
|
5189
|
+
}
|
|
5190
|
+
return { changed: true, content: out.join(eol) }
|
|
5191
|
+
}
|
|
5192
|
+
|
|
5193
|
+
/**
|
|
5194
|
+
* Знаходить діапазон рядків YAML, що належать блоку `images:` верхнього рівня.
|
|
5195
|
+
* @param {string[]} lines рядки файлу
|
|
5196
|
+
* @returns {{ start: number, end: number } | null} `start` — перший рядок ПІСЛЯ ключа `images:`,
|
|
5197
|
+
* `end` — перший рядок не з блоку (виключно), або null, якщо ключа немає
|
|
5198
|
+
*/
|
|
5199
|
+
function findImagesBlockRange(lines) {
|
|
5125
5200
|
let imagesStart = -1
|
|
5126
|
-
for (
|
|
5127
|
-
if (KUSTOMIZATION_IMAGES_KEY_RE.test(
|
|
5201
|
+
for (const [i, line] of lines.entries()) {
|
|
5202
|
+
if (KUSTOMIZATION_IMAGES_KEY_RE.test(line)) {
|
|
5128
5203
|
imagesStart = i + 1
|
|
5129
5204
|
break
|
|
5130
5205
|
}
|
|
5131
5206
|
}
|
|
5132
|
-
if (imagesStart === -1) return
|
|
5133
|
-
|
|
5207
|
+
if (imagesStart === -1) return null
|
|
5134
5208
|
let imagesEnd = lines.length
|
|
5135
5209
|
for (let i = imagesStart; i < lines.length; i++) {
|
|
5136
5210
|
const l = lines[i]
|
|
5137
|
-
if (l === '' ||
|
|
5211
|
+
if (l === '' || KUSTOMIZATION_BLOCK_INDENT_RE.test(l) || l.startsWith('#')) continue
|
|
5138
5212
|
imagesEnd = i
|
|
5139
5213
|
break
|
|
5140
5214
|
}
|
|
5215
|
+
return { start: imagesStart, end: imagesEnd }
|
|
5216
|
+
}
|
|
5141
5217
|
|
|
5218
|
+
/**
|
|
5219
|
+
* Розбиває діапазон рядків блоку `images:` на елементи списку (`- name: …`).
|
|
5220
|
+
* @param {string[]} lines рядки файлу
|
|
5221
|
+
* @param {number} blockStart перший рядок блоку (включно)
|
|
5222
|
+
* @param {number} blockEnd перший рядок не з блоку (виключно)
|
|
5223
|
+
* @returns {Array<{ start: number, end: number }>} діапазони рядків кожного елемента
|
|
5224
|
+
*/
|
|
5225
|
+
function splitImagesBlockEntries(lines, blockStart, blockEnd) {
|
|
5142
5226
|
/** @type {Array<{ start: number, end: number }>} */
|
|
5143
5227
|
const entries = []
|
|
5144
5228
|
let curStart = -1
|
|
5145
|
-
for (let i =
|
|
5146
|
-
if (KUSTOMIZATION_LIST_ITEM_RE.test(lines[i]))
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
}
|
|
5229
|
+
for (let i = blockStart; i < blockEnd; i++) {
|
|
5230
|
+
if (!KUSTOMIZATION_LIST_ITEM_RE.test(lines[i])) continue
|
|
5231
|
+
if (curStart >= 0) entries.push({ start: curStart, end: i })
|
|
5232
|
+
curStart = i
|
|
5150
5233
|
}
|
|
5151
|
-
if (curStart >= 0) entries.push({ start: curStart, end:
|
|
5234
|
+
if (curStart >= 0) entries.push({ start: curStart, end: blockEnd })
|
|
5235
|
+
return entries
|
|
5236
|
+
}
|
|
5152
5237
|
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5238
|
+
/**
|
|
5239
|
+
* Обробляє один елемент `images[]`: збирає `name` (зрізає :tag) і `newTag` (видаляє, якщо
|
|
5240
|
+
* збігається зі зрізаним). Записує плановані заміни/видалення в передані колекції.
|
|
5241
|
+
* @param {string[]} lines рядки файлу
|
|
5242
|
+
* @param {{ start: number, end: number }} entry діапазон рядків елемента
|
|
5243
|
+
* @param {Map<number, string>} replacements буфер замін «номер_рядка → новий рядок»
|
|
5244
|
+
* @param {Set<number>} removals буфер видалень «номер_рядка»
|
|
5245
|
+
* @returns {boolean} true, якщо для цього елемента запланована хоча б одна зміна
|
|
5246
|
+
*/
|
|
5247
|
+
function processImagesEntry(lines, entry, replacements, removals) {
|
|
5248
|
+
/** @type {string | null} */
|
|
5249
|
+
let strippedTag = null
|
|
5250
|
+
let nameProcessed = false
|
|
5251
|
+
/** @type {{ lineIdx: number, value: string } | null} */
|
|
5252
|
+
let newTagInfo = null
|
|
5253
|
+
let newTagProcessed = false
|
|
5157
5254
|
let changed = false
|
|
5158
5255
|
|
|
5159
|
-
for (
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
const m = lines[i].match(KUSTOMIZATION_IMAGE_FIELD_RE)
|
|
5169
|
-
if (m === null) continue
|
|
5170
|
-
const [, prefix, key, sep, valueRaw, trailing] = m
|
|
5171
|
-
if (key === 'name' && !nameProcessed) {
|
|
5172
|
-
nameProcessed = true
|
|
5173
|
-
const { unquoted, quote } = parseQuotedYamlScalar(valueRaw)
|
|
5174
|
-
const split = splitImageNameTagDigest(unquoted)
|
|
5175
|
-
if (split.tag !== null) {
|
|
5176
|
-
const newLine = `${prefix}name:${sep}${requoteYamlScalar(split.name, quote)}${trailing}`
|
|
5177
|
-
if (newLine !== lines[i]) {
|
|
5178
|
-
replacements.set(i, newLine)
|
|
5179
|
-
changed = true
|
|
5180
|
-
}
|
|
5181
|
-
strippedTag = split.tag
|
|
5182
|
-
}
|
|
5183
|
-
} else if (key === 'newTag' && !newTagProcessed) {
|
|
5184
|
-
newTagProcessed = true
|
|
5185
|
-
const { unquoted } = parseQuotedYamlScalar(valueRaw)
|
|
5186
|
-
newTagInfo = { lineIdx: i, value: unquoted }
|
|
5256
|
+
for (let i = entry.start; i < entry.end; i++) {
|
|
5257
|
+
const parsed = parseImagesEntryLine(lines[i])
|
|
5258
|
+
if (parsed === null) continue
|
|
5259
|
+
if (parsed.key === 'name' && !nameProcessed) {
|
|
5260
|
+
nameProcessed = true
|
|
5261
|
+
const result = applyNameStripTag(lines[i], parsed)
|
|
5262
|
+
if (result.replacement !== null) {
|
|
5263
|
+
replacements.set(i, result.replacement)
|
|
5264
|
+
changed = true
|
|
5187
5265
|
}
|
|
5266
|
+
strippedTag = result.strippedTag
|
|
5267
|
+
} else if (parsed.key === 'newTag' && !newTagProcessed) {
|
|
5268
|
+
newTagProcessed = true
|
|
5269
|
+
const { unquoted } = parseQuotedYamlScalar(parsed.value)
|
|
5270
|
+
newTagInfo = { lineIdx: i, value: unquoted }
|
|
5188
5271
|
}
|
|
5272
|
+
}
|
|
5189
5273
|
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
}
|
|
5274
|
+
if (newTagInfo !== null && strippedTag !== null && newTagInfo.value === strippedTag) {
|
|
5275
|
+
removals.add(newTagInfo.lineIdx)
|
|
5276
|
+
changed = true
|
|
5194
5277
|
}
|
|
5278
|
+
return changed
|
|
5279
|
+
}
|
|
5195
5280
|
|
|
5196
|
-
|
|
5281
|
+
/**
|
|
5282
|
+
* Парсить рядок YAML-поля `name|newName|newTag`, повертає його складники або null, якщо рядок
|
|
5283
|
+
* не відповідає формату.
|
|
5284
|
+
* @param {string} line рядок YAML
|
|
5285
|
+
* @returns {{ prefix: string, key: 'name' | 'newName' | 'newTag', sep: string, value: string, trailing: string } | null}
|
|
5286
|
+
* Розібрані поля або null
|
|
5287
|
+
*/
|
|
5288
|
+
function parseImagesEntryLine(line) {
|
|
5289
|
+
const m = line.match(KUSTOMIZATION_IMAGE_FIELD_RE)
|
|
5290
|
+
if (m === null) return null
|
|
5291
|
+
const [, prefix, key, sep, valueWithTrailing] = m
|
|
5292
|
+
const { value, trailing } = splitYamlValueAndTrailing(valueWithTrailing)
|
|
5293
|
+
return { prefix, key: /** @type {'name' | 'newName' | 'newTag'} */ (key), sep, value, trailing }
|
|
5294
|
+
}
|
|
5197
5295
|
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5296
|
+
/**
|
|
5297
|
+
* Якщо `value` містить тег — повертає новий рядок без тега та сам тег. Інакше — null/null.
|
|
5298
|
+
* @param {string} originalLine оригінальний рядок YAML
|
|
5299
|
+
* @param {{ prefix: string, sep: string, value: string, trailing: string }} parsed розібрані складники
|
|
5300
|
+
* @returns {{ replacement: string | null, strippedTag: string | null }} планована заміна (або null) і зрізаний тег
|
|
5301
|
+
*/
|
|
5302
|
+
function applyNameStripTag(originalLine, parsed) {
|
|
5303
|
+
const { unquoted, quote } = parseQuotedYamlScalar(parsed.value)
|
|
5304
|
+
const split = splitImageNameTagDigest(unquoted)
|
|
5305
|
+
if (split.tag === null) return { replacement: null, strippedTag: null }
|
|
5306
|
+
const newLine = `${parsed.prefix}name:${parsed.sep}${requoteYamlScalar(split.name, quote)}${parsed.trailing}`
|
|
5307
|
+
return { replacement: newLine === originalLine ? null : newLine, strippedTag: split.tag }
|
|
5205
5308
|
}
|
|
5206
5309
|
|
|
5207
5310
|
/** Regex: JSON6902 path для image окремого контейнера у Pod-шаблоні Deployment. */
|
|
@@ -5211,47 +5314,87 @@ const KUSTOMIZATION_DEPLOYMENT_CONTAINER_IMAGE_PATH_RE = /^\/spec\/template\/spe
|
|
|
5211
5314
|
* Якщо `patchObj` — JSON6902 з єдиною операцією `replace` на шляху image-контейнера у `Deployment`,
|
|
5212
5315
|
* повертає `{ deployName, containerIndex, newImage }`. Інакше null.
|
|
5213
5316
|
* @param {unknown} patchObj елемент масиву `patches[]`
|
|
5214
|
-
* @returns {{ deployName: string, containerIndex: number, newImage: string } | null}
|
|
5317
|
+
* @returns {{ deployName: string, containerIndex: number, newImage: string } | null} інформація про патч або null, якщо це не очікувана JSON6902-операція
|
|
5215
5318
|
*/
|
|
5216
5319
|
export function imageReplaceDeploymentPatchInfo(patchObj) {
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
const
|
|
5220
|
-
if (
|
|
5221
|
-
|
|
5320
|
+
const pr = asPlainObject(patchObj)
|
|
5321
|
+
if (pr === null) return null
|
|
5322
|
+
const deployName = deploymentTargetName(pr.target)
|
|
5323
|
+
if (deployName === null) return null
|
|
5324
|
+
if (typeof pr.patch !== 'string') return null
|
|
5325
|
+
|
|
5326
|
+
const parsedArr = tryParseSingleJson6902Array(pr.patch)
|
|
5327
|
+
if (parsedArr === null) return null
|
|
5328
|
+
const op = asPlainObject(parsedArr[0])
|
|
5329
|
+
if (op === null) return null
|
|
5330
|
+
|
|
5331
|
+
const containerIndex = singleImageReplaceContainerIndex(op)
|
|
5332
|
+
if (containerIndex === null) return null
|
|
5333
|
+
if (typeof op.value !== 'string' || op.value.trim() === '') return null
|
|
5334
|
+
|
|
5335
|
+
return {
|
|
5336
|
+
deployName,
|
|
5337
|
+
containerIndex,
|
|
5338
|
+
newImage: op.value.trim()
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
|
|
5342
|
+
/**
|
|
5343
|
+
* Перевіряє, що значення — це plain-object (не null, не масив), і повертає його з типом
|
|
5344
|
+
* `Record<string, unknown>` або null. Скорочує перевірки на початку гілок.
|
|
5345
|
+
* @param {unknown} value будь-що з YAML/JSON
|
|
5346
|
+
* @returns {Record<string, unknown> | null} вузол як plain-object або null
|
|
5347
|
+
*/
|
|
5348
|
+
function asPlainObject(value) {
|
|
5349
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return null
|
|
5350
|
+
return /** @type {Record<string, unknown>} */ (value)
|
|
5351
|
+
}
|
|
5352
|
+
|
|
5353
|
+
/**
|
|
5354
|
+
* Перевіряє, що `target` — це Deployment з непорожнім ім'ям, і повертає це ім'я (trimmed).
|
|
5355
|
+
* @param {unknown} target значення `patches[i].target`
|
|
5356
|
+
* @returns {string | null} ім'я Deployment або null
|
|
5357
|
+
*/
|
|
5358
|
+
function deploymentTargetName(target) {
|
|
5359
|
+
const t = asPlainObject(target)
|
|
5360
|
+
if (t === null) return null
|
|
5222
5361
|
if (t.kind !== 'Deployment') return null
|
|
5223
5362
|
if (typeof t.name !== 'string' || t.name.trim() === '') return null
|
|
5224
|
-
|
|
5363
|
+
return t.name.trim()
|
|
5364
|
+
}
|
|
5225
5365
|
|
|
5366
|
+
/**
|
|
5367
|
+
* Парсить `patch`-рядок як YAML-масив з рівно однією операцією JSON6902.
|
|
5368
|
+
* @param {string} patch текст YAML-масиву JSON6902-операцій
|
|
5369
|
+
* @returns {unknown[] | null} масив однієї операції або null
|
|
5370
|
+
*/
|
|
5371
|
+
function tryParseSingleJson6902Array(patch) {
|
|
5226
5372
|
let parsedArr
|
|
5227
5373
|
try {
|
|
5228
|
-
for (const d of parseAllDocuments(
|
|
5229
|
-
if (d.errors.length
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
}
|
|
5374
|
+
for (const d of parseAllDocuments(patch.trim())) {
|
|
5375
|
+
if (d.errors.length !== 0) continue
|
|
5376
|
+
const j = d.toJSON()
|
|
5377
|
+
if (Array.isArray(j)) {
|
|
5378
|
+
parsedArr = j
|
|
5379
|
+
break
|
|
5235
5380
|
}
|
|
5236
5381
|
}
|
|
5237
5382
|
} catch {
|
|
5238
5383
|
return null
|
|
5239
5384
|
}
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
if (op === null || typeof op !== 'object' || Array.isArray(op)) return null
|
|
5243
|
-
const oo = /** @type {Record<string, unknown>} */ (op)
|
|
5244
|
-
if (typeof oo.op !== 'string' || oo.op.toLowerCase() !== 'replace') return null
|
|
5245
|
-
if (typeof oo.path !== 'string') return null
|
|
5246
|
-
const m = oo.path.match(KUSTOMIZATION_DEPLOYMENT_CONTAINER_IMAGE_PATH_RE)
|
|
5247
|
-
if (m === null) return null
|
|
5248
|
-
if (typeof oo.value !== 'string' || oo.value.trim() === '') return null
|
|
5385
|
+
return Array.isArray(parsedArr) && parsedArr.length === 1 ? parsedArr : null
|
|
5386
|
+
}
|
|
5249
5387
|
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5388
|
+
/**
|
|
5389
|
+
* Якщо операція — `op: replace` на шляху контейнера-image, повертає індекс контейнера.
|
|
5390
|
+
* @param {Record<string, unknown>} op об'єкт операції JSON6902
|
|
5391
|
+
* @returns {number | null} індекс контейнера у `containers[N]` або null
|
|
5392
|
+
*/
|
|
5393
|
+
function singleImageReplaceContainerIndex(op) {
|
|
5394
|
+
if (typeof op.op !== 'string' || op.op.toLowerCase() !== 'replace') return null
|
|
5395
|
+
if (typeof op.path !== 'string') return null
|
|
5396
|
+
const m = op.path.match(KUSTOMIZATION_DEPLOYMENT_CONTAINER_IMAGE_PATH_RE)
|
|
5397
|
+
return m === null ? null : Number(m[1])
|
|
5255
5398
|
}
|
|
5256
5399
|
|
|
5257
5400
|
/**
|
|
@@ -5268,29 +5411,50 @@ async function findDeploymentContainerImageInFile(absPath, deployName, container
|
|
|
5268
5411
|
if (docs === undefined) return null
|
|
5269
5412
|
for (const d of docs) {
|
|
5270
5413
|
if (d.errors.length !== 0) continue
|
|
5271
|
-
const
|
|
5272
|
-
if (
|
|
5273
|
-
const oo = /** @type {Record<string, unknown>} */ (o)
|
|
5274
|
-
if (oo.kind !== 'Deployment') continue
|
|
5275
|
-
const meta = oo.metadata
|
|
5276
|
-
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) continue
|
|
5277
|
-
if (/** @type {Record<string, unknown>} */ (meta).name !== deployName) continue
|
|
5278
|
-
const spec = oo.spec
|
|
5279
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) continue
|
|
5280
|
-
const tmpl = /** @type {Record<string, unknown>} */ (spec).template
|
|
5281
|
-
if (tmpl === null || typeof tmpl !== 'object' || Array.isArray(tmpl)) continue
|
|
5282
|
-
const podSpec = /** @type {Record<string, unknown>} */ (tmpl).spec
|
|
5283
|
-
if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) continue
|
|
5284
|
-
const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
|
|
5285
|
-
if (!Array.isArray(containers) || containerIndex < 0 || containerIndex >= containers.length) continue
|
|
5286
|
-
const c = containers[containerIndex]
|
|
5287
|
-
if (c === null || typeof c !== 'object' || Array.isArray(c)) continue
|
|
5288
|
-
const img = /** @type {Record<string, unknown>} */ (c).image
|
|
5289
|
-
if (typeof img === 'string' && img.trim() !== '') return img.trim()
|
|
5414
|
+
const img = imageFromDeploymentDoc(d.toJSON(), deployName, containerIndex)
|
|
5415
|
+
if (img !== null) return img
|
|
5290
5416
|
}
|
|
5291
5417
|
return null
|
|
5292
5418
|
}
|
|
5293
5419
|
|
|
5420
|
+
/**
|
|
5421
|
+
* Витягує `containers[N].image` з YAML-документа, якщо він — Deployment з відповідним іменем.
|
|
5422
|
+
* @param {unknown} doc розпаршений YAML як plain JS-обʼєкт
|
|
5423
|
+
* @param {string} deployName очікуване `metadata.name` Deployment
|
|
5424
|
+
* @param {number} containerIndex індекс контейнера у `spec.template.spec.containers[]`
|
|
5425
|
+
* @returns {string | null} обрізаний `image` або null
|
|
5426
|
+
*/
|
|
5427
|
+
function imageFromDeploymentDoc(doc, deployName, containerIndex) {
|
|
5428
|
+
const oo = asPlainObject(doc)
|
|
5429
|
+
if (oo === null || oo.kind !== 'Deployment') return null
|
|
5430
|
+
const meta = asPlainObject(oo.metadata)
|
|
5431
|
+
if (meta === null || meta.name !== deployName) return null
|
|
5432
|
+
const containers = containersOfDeployment(oo)
|
|
5433
|
+
if (containers === null) return null
|
|
5434
|
+
if (containerIndex < 0 || containerIndex >= containers.length) return null
|
|
5435
|
+
const c = asPlainObject(containers[containerIndex])
|
|
5436
|
+
if (c === null) return null
|
|
5437
|
+
const img = c.image
|
|
5438
|
+
if (typeof img === 'string' && img.trim() !== '') return img.trim()
|
|
5439
|
+
return null
|
|
5440
|
+
}
|
|
5441
|
+
|
|
5442
|
+
/**
|
|
5443
|
+
* Витягує `spec.template.spec.containers` з обʼєкта Deployment (або null, якщо структура неповна).
|
|
5444
|
+
* @param {Record<string, unknown>} deployment plain-object документу Deployment
|
|
5445
|
+
* @returns {unknown[] | null} масив контейнерів або null
|
|
5446
|
+
*/
|
|
5447
|
+
function containersOfDeployment(deployment) {
|
|
5448
|
+
const spec = asPlainObject(deployment.spec)
|
|
5449
|
+
if (spec === null) return null
|
|
5450
|
+
const tmpl = asPlainObject(spec.template)
|
|
5451
|
+
if (tmpl === null) return null
|
|
5452
|
+
const podSpec = asPlainObject(tmpl.spec)
|
|
5453
|
+
if (podSpec === null) return null
|
|
5454
|
+
const containers = podSpec.containers
|
|
5455
|
+
return Array.isArray(containers) ? containers : null
|
|
5456
|
+
}
|
|
5457
|
+
|
|
5294
5458
|
/**
|
|
5295
5459
|
* Рекурсивно проходить дерево kustomization (resources / bases / components / crds), шукаючи
|
|
5296
5460
|
* `Deployment` із заданим іменем; повертає image потрібного контейнера або null, якщо не знайдено.
|
|
@@ -5312,24 +5476,52 @@ async function walkKustomizationForDeploymentImage(kustAbs, rootNorm, deployName
|
|
|
5312
5476
|
const refs = pathsFromKustomizationObject(obj)
|
|
5313
5477
|
|
|
5314
5478
|
for (const ref of refs) {
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5479
|
+
const resolved = normalizeKustomizationRef(ref, kustDir, rootNorm)
|
|
5480
|
+
if (resolved === null) continue
|
|
5481
|
+
const img = await imageFromResolvedKustomizationRef(resolved, rootNorm, deployName, containerIndex, visited)
|
|
5482
|
+
if (img !== null) return img
|
|
5483
|
+
}
|
|
5484
|
+
return null
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
/**
|
|
5488
|
+
* Перевіряє та нормалізує посилання `resources[]/components[]/bases[]` з kustomization.yaml.
|
|
5489
|
+
* Пропускає неприйнятні (URL, порожні, поза межами репо) посилання.
|
|
5490
|
+
* @param {unknown} ref значення з масиву посилань
|
|
5491
|
+
* @param {string} kustDir абсолютний каталог поточної kustomization
|
|
5492
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
5493
|
+
* @returns {string | null} абсолютний резолвлений шлях або null, якщо посилання треба пропустити
|
|
5494
|
+
*/
|
|
5495
|
+
function normalizeKustomizationRef(ref, kustDir, rootNorm) {
|
|
5496
|
+
if (typeof ref !== 'string' || ref.includes('://') || ref.trim() === '') return null
|
|
5497
|
+
const resolved = resolve(kustDir, ref.trim())
|
|
5498
|
+
return resolvedFilePathIsUnderRoot(rootNorm, resolved) ? resolved : null
|
|
5499
|
+
}
|
|
5500
|
+
|
|
5501
|
+
/**
|
|
5502
|
+
* Намагається отримати image для `<deployName>:<containerIndex>` з резолвленого посилання
|
|
5503
|
+
* (файл або підкаталог з kustomization.yaml).
|
|
5504
|
+
* @param {string} resolvedAbs абсолютний шлях файлу або каталогу
|
|
5505
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
5506
|
+
* @param {string} deployName ім'я Deployment
|
|
5507
|
+
* @param {number} containerIndex індекс контейнера
|
|
5508
|
+
* @param {Set<string>} visited нормалізовані відвідані kustomization.yaml
|
|
5509
|
+
* @returns {Promise<string | null>} знайдений image або null
|
|
5510
|
+
*/
|
|
5511
|
+
async function imageFromResolvedKustomizationRef(resolvedAbs, rootNorm, deployName, containerIndex, visited) {
|
|
5512
|
+
let st
|
|
5513
|
+
try {
|
|
5514
|
+
st = await stat(resolvedAbs)
|
|
5515
|
+
} catch {
|
|
5516
|
+
return null
|
|
5517
|
+
}
|
|
5518
|
+
if (st.isFile() && YAML_EXTENSION_RE.test(resolvedAbs)) {
|
|
5519
|
+
return findDeploymentContainerImageInFile(resolvedAbs, deployName, containerIndex)
|
|
5520
|
+
}
|
|
5521
|
+
if (st.isDirectory()) {
|
|
5522
|
+
const childK = join(resolvedAbs, 'kustomization.yaml')
|
|
5523
|
+
if (existsSync(childK)) {
|
|
5524
|
+
return walkKustomizationForDeploymentImage(childK, rootNorm, deployName, containerIndex, visited)
|
|
5333
5525
|
}
|
|
5334
5526
|
}
|
|
5335
5527
|
return null
|
|
@@ -5350,30 +5542,49 @@ async function walkKustomizationForDeploymentImage(kustAbs, rootNorm, deployName
|
|
|
5350
5542
|
* 5. Записує файл назад через `Document.toString()`.
|
|
5351
5543
|
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
5352
5544
|
* @param {string} rootNorm нормалізований корінь репо
|
|
5353
|
-
* @returns {Promise<{ changed: boolean, content?: string, errors: string[] }>}
|
|
5545
|
+
* @returns {Promise<{ changed: boolean, content?: string, errors: string[] }>} прапорець змін, новий вміст (за наявності) і список нефатальних помилок під час конвертації
|
|
5354
5546
|
*/
|
|
5355
5547
|
export async function convertImagePatchesToImagesInKustomization(kustAbs, rootNorm) {
|
|
5356
5548
|
const raw = await tryReadFileUtf8(kustAbs)
|
|
5357
5549
|
if (raw === undefined) return { changed: false, errors: [] }
|
|
5358
5550
|
|
|
5551
|
+
const parsed = parseKustomizationWithPatches(raw)
|
|
5552
|
+
if (parsed === null) return { changed: false, errors: [] }
|
|
5553
|
+
const { doc, candidates } = parsed
|
|
5554
|
+
if (candidates.length === 0) return { changed: false, errors: [] }
|
|
5555
|
+
|
|
5556
|
+
const { conversions, errors } = await buildPatchToImageConversions(kustAbs, rootNorm, candidates)
|
|
5557
|
+
if (conversions.length === 0) return { changed: false, errors }
|
|
5558
|
+
|
|
5559
|
+
if (!applyConversionsToDoc(doc, conversions)) return { changed: false, errors }
|
|
5560
|
+
|
|
5561
|
+
const content = doc.toString()
|
|
5562
|
+
if (content === raw) return { changed: false, errors }
|
|
5563
|
+
return { changed: true, content, errors }
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5566
|
+
/**
|
|
5567
|
+
* Парсить kustomization.yaml як Document і повертає його разом зі списком кандидатів-патчів
|
|
5568
|
+
* (`patches[i]` що відповідає `image-replace` для Deployment). Повертає null, якщо документ не
|
|
5569
|
+
* розпарсився, не є Kustomization або не має масиву `patches:`.
|
|
5570
|
+
* @param {string} raw текст файлу
|
|
5571
|
+
* @returns {{ doc: ReturnType<typeof parseDocument>, candidates: Array<{ index: number, info: { deployName: string, containerIndex: number, newImage: string } }> } | null}
|
|
5572
|
+
* document і список кандидатів, або null
|
|
5573
|
+
*/
|
|
5574
|
+
function parseKustomizationWithPatches(raw) {
|
|
5359
5575
|
let doc
|
|
5360
5576
|
try {
|
|
5361
5577
|
doc = parseDocument(raw)
|
|
5362
5578
|
} catch {
|
|
5363
|
-
return
|
|
5579
|
+
return null
|
|
5364
5580
|
}
|
|
5365
|
-
if (doc.errors.length > 0) return
|
|
5581
|
+
if (doc.errors.length > 0) return null
|
|
5366
5582
|
|
|
5367
|
-
const
|
|
5368
|
-
if (
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
if (rec.kind !== 'Kustomization') return { changed: false, errors: [] }
|
|
5373
|
-
if (typeof rec.apiVersion !== 'string' || !rec.apiVersion.startsWith(KUSTOMIZE_CONFIG_API_PREFIX)) {
|
|
5374
|
-
return { changed: false, errors: [] }
|
|
5375
|
-
}
|
|
5376
|
-
if (!Array.isArray(rec.patches)) return { changed: false, errors: [] }
|
|
5583
|
+
const rec = asPlainObject(doc.toJSON())
|
|
5584
|
+
if (rec === null) return null
|
|
5585
|
+
if (rec.kind !== 'Kustomization') return null
|
|
5586
|
+
if (typeof rec.apiVersion !== 'string' || !rec.apiVersion.startsWith(KUSTOMIZE_CONFIG_API_PREFIX)) return null
|
|
5587
|
+
if (!Array.isArray(rec.patches)) return null
|
|
5377
5588
|
|
|
5378
5589
|
/** @type {Array<{ index: number, info: { deployName: string, containerIndex: number, newImage: string } }>} */
|
|
5379
5590
|
const candidates = []
|
|
@@ -5381,56 +5592,82 @@ export async function convertImagePatchesToImagesInKustomization(kustAbs, rootNo
|
|
|
5381
5592
|
const info = imageReplaceDeploymentPatchInfo(p)
|
|
5382
5593
|
if (info !== null) candidates.push({ index: i, info })
|
|
5383
5594
|
}
|
|
5384
|
-
|
|
5595
|
+
return { doc, candidates }
|
|
5596
|
+
}
|
|
5385
5597
|
|
|
5598
|
+
/**
|
|
5599
|
+
* Для кожного кандидата шукає базовий image у дереві resources та формує запис конвертації
|
|
5600
|
+
* (або повідомлення про помилку, чому конвертація неможлива).
|
|
5601
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
5602
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
5603
|
+
* @param {Array<{ index: number, info: { deployName: string, containerIndex: number, newImage: string } }>} candidates кандидати з `patches[]`
|
|
5604
|
+
* @returns {Promise<{ conversions: Array<{ index: number, name: string, newName: string, newTag: string | null }>, errors: string[] }>}
|
|
5605
|
+
* результати конвертації та зібрані нефатальні помилки
|
|
5606
|
+
*/
|
|
5607
|
+
async function buildPatchToImageConversions(kustAbs, rootNorm, candidates) {
|
|
5386
5608
|
/** @type {Array<{ index: number, name: string, newName: string, newTag: string | null }>} */
|
|
5387
5609
|
const conversions = []
|
|
5388
5610
|
/** @type {string[]} */
|
|
5389
5611
|
const errors = []
|
|
5390
5612
|
|
|
5391
5613
|
for (const { index, info } of candidates) {
|
|
5392
|
-
/** @type {Set<string>} */
|
|
5393
|
-
const visited = new Set()
|
|
5394
5614
|
const baseImage = await walkKustomizationForDeploymentImage(
|
|
5395
5615
|
kustAbs,
|
|
5396
5616
|
rootNorm,
|
|
5397
5617
|
info.deployName,
|
|
5398
5618
|
info.containerIndex,
|
|
5399
|
-
|
|
5619
|
+
new Set()
|
|
5400
5620
|
)
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
`patches[${index}]: не знайдено Deployment ${info.deployName}.containers[${info.containerIndex}].image у дереві resources — конвертацію патча в images: пропущено (k8s.mdc)`
|
|
5404
|
-
)
|
|
5405
|
-
continue
|
|
5406
|
-
}
|
|
5407
|
-
const baseSplit = splitImageNameTagDigest(baseImage)
|
|
5408
|
-
if (baseSplit.hasDigest) {
|
|
5409
|
-
errors.push(
|
|
5410
|
-
`patches[${index}]: base image для ${info.deployName} містить digest (${baseImage}) — автоконвертацію патча пропущено (k8s.mdc)`
|
|
5411
|
-
)
|
|
5412
|
-
continue
|
|
5413
|
-
}
|
|
5414
|
-
const newSplit = splitImageNameTagDigest(info.newImage)
|
|
5415
|
-
if (newSplit.hasDigest) {
|
|
5416
|
-
errors.push(
|
|
5417
|
-
`patches[${index}]: значення патча для ${info.deployName} містить digest (${info.newImage}) — автоконвертацію пропущено (k8s.mdc)`
|
|
5418
|
-
)
|
|
5419
|
-
continue
|
|
5420
|
-
}
|
|
5421
|
-
const finalNewTag = newSplit.tag !== null && newSplit.tag !== baseSplit.tag ? newSplit.tag : null
|
|
5422
|
-
conversions.push({
|
|
5423
|
-
index,
|
|
5424
|
-
name: baseSplit.name,
|
|
5425
|
-
newName: newSplit.name,
|
|
5426
|
-
newTag: finalNewTag
|
|
5427
|
-
})
|
|
5621
|
+
const conversion = buildConversionForCandidate(index, info, baseImage, errors)
|
|
5622
|
+
if (conversion !== null) conversions.push(conversion)
|
|
5428
5623
|
}
|
|
5429
5624
|
|
|
5430
|
-
|
|
5625
|
+
return { conversions, errors }
|
|
5626
|
+
}
|
|
5627
|
+
|
|
5628
|
+
/**
|
|
5629
|
+
* Будує одну конвертацію `patches[index]` → `images[]` запис з відповідним `newTag`.
|
|
5630
|
+
* Якщо щось не так (немає baseImage, digest у base/new) — додає текст у `errors` і повертає null.
|
|
5631
|
+
* @param {number} index індекс патча в `patches[]`
|
|
5632
|
+
* @param {{ deployName: string, containerIndex: number, newImage: string }} info результат `imageReplaceDeploymentPatchInfo`
|
|
5633
|
+
* @param {string | null} baseImage знайдений базовий image або null
|
|
5634
|
+
* @param {string[]} errors буфер нефатальних помилок (мутується)
|
|
5635
|
+
* @returns {{ index: number, name: string, newName: string, newTag: string | null } | null} запис конвертації або null
|
|
5636
|
+
*/
|
|
5637
|
+
function buildConversionForCandidate(index, info, baseImage, errors) {
|
|
5638
|
+
if (baseImage === null) {
|
|
5639
|
+
errors.push(
|
|
5640
|
+
`patches[${index}]: не знайдено Deployment ${info.deployName}.containers[${info.containerIndex}].image у дереві resources — конвертацію патча в images: пропущено (k8s.mdc)`
|
|
5641
|
+
)
|
|
5642
|
+
return null
|
|
5643
|
+
}
|
|
5644
|
+
const baseSplit = splitImageNameTagDigest(baseImage)
|
|
5645
|
+
if (baseSplit.hasDigest) {
|
|
5646
|
+
errors.push(
|
|
5647
|
+
`patches[${index}]: base image для ${info.deployName} містить digest (${baseImage}) — автоконвертацію патча пропущено (k8s.mdc)`
|
|
5648
|
+
)
|
|
5649
|
+
return null
|
|
5650
|
+
}
|
|
5651
|
+
const newSplit = splitImageNameTagDigest(info.newImage)
|
|
5652
|
+
if (newSplit.hasDigest) {
|
|
5653
|
+
errors.push(
|
|
5654
|
+
`patches[${index}]: значення патча для ${info.deployName} містить digest (${info.newImage}) — автоконвертацію пропущено (k8s.mdc)`
|
|
5655
|
+
)
|
|
5656
|
+
return null
|
|
5657
|
+
}
|
|
5658
|
+
const finalNewTag = newSplit.tag !== null && newSplit.tag !== baseSplit.tag ? newSplit.tag : null
|
|
5659
|
+
return { index, name: baseSplit.name, newName: newSplit.name, newTag: finalNewTag }
|
|
5660
|
+
}
|
|
5431
5661
|
|
|
5662
|
+
/**
|
|
5663
|
+
* Видаляє конвертовані елементи з `patches:` (за потреби — і сам ключ) і дописує `images:`.
|
|
5664
|
+
* @param {ReturnType<typeof parseDocument>} doc документ kustomization.yaml
|
|
5665
|
+
* @param {Array<{ index: number, name: string, newName: string, newTag: string | null }>} conversions конвертації
|
|
5666
|
+
* @returns {boolean} true, якщо мутації відбулися (документ можна серіалізувати)
|
|
5667
|
+
*/
|
|
5668
|
+
function applyConversionsToDoc(doc, conversions) {
|
|
5432
5669
|
const patchesNode = doc.get('patches', true)
|
|
5433
|
-
if (!isSeq(patchesNode)) return
|
|
5670
|
+
if (!isSeq(patchesNode)) return false
|
|
5434
5671
|
|
|
5435
5672
|
const removeIdx = new Set(conversions.map(c => c.index))
|
|
5436
5673
|
for (let i = patchesNode.items.length - 1; i >= 0; i--) {
|
|
@@ -5445,15 +5682,11 @@ export async function convertImagePatchesToImagesInKustomization(kustAbs, rootNo
|
|
|
5445
5682
|
imagesNode = doc.createNode([])
|
|
5446
5683
|
doc.set('images', imagesNode)
|
|
5447
5684
|
}
|
|
5448
|
-
|
|
5449
5685
|
for (const { name, newName, newTag } of conversions) {
|
|
5450
5686
|
const entry = newTag === null ? { name, newName } : { name, newName, newTag }
|
|
5451
5687
|
imagesNode.add(doc.createNode(entry))
|
|
5452
5688
|
}
|
|
5453
|
-
|
|
5454
|
-
const content = doc.toString()
|
|
5455
|
-
if (content === raw) return { changed: false, errors }
|
|
5456
|
-
return { changed: true, content, errors }
|
|
5689
|
+
return true
|
|
5457
5690
|
}
|
|
5458
5691
|
|
|
5459
5692
|
/**
|
|
@@ -5470,28 +5703,53 @@ async function autofixKustomizationImagesYaml(root, yamlFilesAbs, fail, pass) {
|
|
|
5470
5703
|
const kusts = yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')
|
|
5471
5704
|
for (const kustAbs of kusts) {
|
|
5472
5705
|
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5706
|
+
await runImagePatchToImagesConversion(kustAbs, rel, rootNorm, fail, pass)
|
|
5707
|
+
await runKustomizationImagesCleanup(kustAbs, rel, fail, pass)
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5710
|
+
|
|
5711
|
+
/**
|
|
5712
|
+
* Прогон автоконвертації `patches[].image-replace` → `images:` для одного kustomization.yaml.
|
|
5713
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
5714
|
+
* @param {string} rel posix-шлях відносно кореня репо (для повідомлень)
|
|
5715
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
5716
|
+
* @param {(msg: string) => void} fail колбек повідомлення про помилку
|
|
5717
|
+
* @param {(msg: string) => void} pass колбек успішного повідомлення
|
|
5718
|
+
* @returns {Promise<void>} завершується після конвертації або реєстрації помилки
|
|
5719
|
+
*/
|
|
5720
|
+
async function runImagePatchToImagesConversion(kustAbs, rel, rootNorm, fail, pass) {
|
|
5721
|
+
try {
|
|
5722
|
+
const r = await convertImagePatchesToImagesInKustomization(kustAbs, rootNorm)
|
|
5723
|
+
for (const err of r.errors) fail(`${rel}: ${err}`)
|
|
5724
|
+
if (r.changed && r.content !== undefined) {
|
|
5725
|
+
await writeFile(kustAbs, r.content, 'utf8')
|
|
5726
|
+
pass(`${rel}: image-replace patch(es) конвертовано в images: (k8s.mdc)`)
|
|
5483
5727
|
}
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5728
|
+
} catch (error) {
|
|
5729
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
5730
|
+
fail(`${rel}: не вдалося конвертувати image-replace patches → images: (${msg})`)
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
|
|
5734
|
+
/**
|
|
5735
|
+
* Прогон чистильника `images:` (зрізає `:tag` з name й видаляє надлишковий `newTag`).
|
|
5736
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
5737
|
+
* @param {string} rel posix-шлях відносно кореня репо (для повідомлень)
|
|
5738
|
+
* @param {(msg: string) => void} fail колбек повідомлення про помилку
|
|
5739
|
+
* @param {(msg: string) => void} pass колбек успішного повідомлення
|
|
5740
|
+
* @returns {Promise<void>} завершується після очищення або реєстрації помилки
|
|
5741
|
+
*/
|
|
5742
|
+
async function runKustomizationImagesCleanup(kustAbs, rel, fail, pass) {
|
|
5743
|
+
try {
|
|
5744
|
+
const raw = await readFile(kustAbs, 'utf8')
|
|
5745
|
+
const r = cleanupKustomizationImagesInYamlText(raw)
|
|
5746
|
+
if (r.changed) {
|
|
5747
|
+
await writeFile(kustAbs, r.content, 'utf8')
|
|
5748
|
+
pass(`${rel}: images: cleanup — зрізано :tag з name й видалено надлишкове newTag (k8s.mdc)`)
|
|
5494
5749
|
}
|
|
5750
|
+
} catch (error) {
|
|
5751
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
5752
|
+
fail(`${rel}: не вдалося очистити images: (${msg})`)
|
|
5495
5753
|
}
|
|
5496
5754
|
}
|
|
5497
5755
|
|