@leverege/build-tools 2.46.1 → 2.47.1
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 +1 -1
- package/src/Docker.mjs +33 -11
- package/src/Utils.mjs +33 -1
- package/src/docker-to-registry.mjs +121 -5
- package/src/k8x.sh +17 -23
package/package.json
CHANGED
package/src/Docker.mjs
CHANGED
|
@@ -3,7 +3,15 @@ import fs from 'node:fs'
|
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import handlebars from 'handlebars'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
errorExit,
|
|
8
|
+
getDateTimestamp,
|
|
9
|
+
getGcpAccount,
|
|
10
|
+
log,
|
|
11
|
+
proceed,
|
|
12
|
+
shellCmd,
|
|
13
|
+
warning
|
|
14
|
+
} from './Utils.mjs'
|
|
7
15
|
|
|
8
16
|
// The contents of these variables were initially located in files that live in
|
|
9
17
|
// the build-tools repository, but it just became simpler to pull the contents
|
|
@@ -122,12 +130,9 @@ dunm() {
|
|
|
122
130
|
}
|
|
123
131
|
`
|
|
124
132
|
|
|
125
|
-
// Just uses the old dockreate of formatting the date.
|
|
126
|
-
const dateTimestamp = await shellCmd( 'date +%Y%m%d-%H%M' )
|
|
127
|
-
|
|
128
133
|
const defaultSettings = {
|
|
129
134
|
apkadds : '',
|
|
130
|
-
date :
|
|
135
|
+
date : getDateTimestamp(),
|
|
131
136
|
nodeimage : 'iron-alpine',
|
|
132
137
|
npmlogging : '--silent',
|
|
133
138
|
pluginfile : '# NO PLUGIN',
|
|
@@ -217,8 +222,11 @@ const validateCloudBuildBucket = async ( artifactProject ) => {
|
|
|
217
222
|
}
|
|
218
223
|
}
|
|
219
224
|
|
|
220
|
-
const buildContainerImage = async ( {
|
|
225
|
+
const buildContainerImage = async ( {
|
|
226
|
+
artifactProject, artifactRegistry, containerName, imageVersion, isNpmWorkspace,
|
|
227
|
+
} ) => {
|
|
221
228
|
const buildWorkspace = './build/workspace' // passed to Cloud Build
|
|
229
|
+
const imageName = `${artifactRegistry}/images/${containerName}`
|
|
222
230
|
|
|
223
231
|
if ( fs.existsSync( buildWorkspace ) ) {
|
|
224
232
|
fs.renameSync( buildWorkspace, `${buildWorkspace}-junk` )
|
|
@@ -235,26 +243,40 @@ const buildContainerImage = async ( { artifactProject, artifactRegistry, contain
|
|
|
235
243
|
fs.cpSync( `${process.env.HOME}/.npmrc`, './build/.npmrc' )
|
|
236
244
|
fs.cpSync( './docker/bashrc', './build/bashrc' )
|
|
237
245
|
fs.cpSync( './package.json', `${buildWorkspace}/package.json` )
|
|
238
|
-
fs.
|
|
246
|
+
fs.cpSync( './package-lock.json', `${buildWorkspace}/package-lock.json` )
|
|
247
|
+
if ( isNpmWorkspace ) {
|
|
248
|
+
fs.rm( './package-lock.json', ( err ) => {} ) // no leaf node locks in workspaces
|
|
249
|
+
}
|
|
239
250
|
|
|
240
251
|
setPreviousBuild( imageVersion ) // update docker/.previous file
|
|
241
252
|
|
|
253
|
+
const { fullName : builderName } = await getGcpAccount()
|
|
242
254
|
const gcloudBuild = `time gcloud builds submit --project ${artifactProject}`
|
|
243
255
|
const gcloudLogs = `--gcs-log-dir ${formCloudBuildBucketName( artifactProject )}/log`
|
|
244
|
-
const gcloudTags = `--tag ${
|
|
256
|
+
const gcloudTags = `--tag ${imageName}:${imageVersion}`
|
|
245
257
|
|
|
246
258
|
log( chalk.green.bold( `
|
|
247
|
-
*** Submitting Build ***
|
|
259
|
+
*** Submitting Build as ${builderName} ***
|
|
248
260
|
${gcloudBuild} \\
|
|
249
261
|
${gcloudLogs} \\
|
|
250
262
|
${gcloudTags}
|
|
251
263
|
` ) )
|
|
252
264
|
|
|
265
|
+
if ( process.env.DRY_RUN === '1' ) {
|
|
266
|
+
log( '\n***Exiting from DRY_RUN\n' )
|
|
267
|
+
process.exit( 0 )
|
|
268
|
+
}
|
|
269
|
+
|
|
253
270
|
await shellCmd( `${gcloudBuild} ${gcloudLogs} ${gcloudTags} ./build`, { stdio : 'inherit' } )
|
|
271
|
+
|
|
272
|
+
const imageTag = `${defaultSettings.date}-${defaultSettings.nodeimage}-${builderName}`
|
|
273
|
+
await shellCmd(
|
|
274
|
+
`gcloud container images add-tag --quiet ${imageName}:${imageVersion} ${imageName}:${imageTag} ${imageName}:latest`
|
|
275
|
+
)
|
|
254
276
|
}
|
|
255
277
|
|
|
256
278
|
const repoCleanup = async () => {
|
|
257
|
-
// remove
|
|
279
|
+
// remove garbage from the repo
|
|
258
280
|
const garbage = [
|
|
259
281
|
'build',
|
|
260
282
|
'dist',
|
|
@@ -266,7 +288,7 @@ const repoCleanup = async () => {
|
|
|
266
288
|
]
|
|
267
289
|
garbage.forEach( trash => fs.rmSync( trash, { recursive : true, force : true } ) )
|
|
268
290
|
|
|
269
|
-
fs.writeFileSync( 'docker/.gitignore', '.previous' )
|
|
291
|
+
fs.writeFileSync( 'docker/.gitignore', '.previous\nDockerfile' )
|
|
270
292
|
|
|
271
293
|
await shellCmd( 'git add -f docker' )
|
|
272
294
|
|
package/src/Utils.mjs
CHANGED
|
@@ -68,6 +68,18 @@ export const shellCmd = async ( cmdstr, opts = {} ) => {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// returns YYYMMDD-hhmm
|
|
72
|
+
export const getDateTimestamp = () => {
|
|
73
|
+
const fullDTS = new Date().toISOString().replace( /[-T:]/g, '' ).slice( 0, 14 )
|
|
74
|
+
return `${fullDTS.slice( 0, 8 )}-${fullDTS.slice( 8, 12 )}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const getGcpAccount = async () => {
|
|
78
|
+
const fullAccount = await shellCmd( 'gcloud config get account' )
|
|
79
|
+
const [ fullName, domain ] = fullAccount.split( '@' )
|
|
80
|
+
return { fullAccount, fullName, domain }
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
export const getGcpSecret = async ( gcpProject, secretName, opts ) => {
|
|
72
84
|
const secretFetchCmd = `gcloud secrets versions access latest --project=${gcpProject} --secret=${secretName}`
|
|
73
85
|
let secretString
|
|
@@ -100,6 +112,22 @@ export const getGitRootDirectory = async () => {
|
|
|
100
112
|
}
|
|
101
113
|
}
|
|
102
114
|
|
|
115
|
+
export const getGitUpstream = async () => {
|
|
116
|
+
let branch
|
|
117
|
+
try {
|
|
118
|
+
branch = await shellCmd( 'git rev-parse --abbrev-ref HEAD' )
|
|
119
|
+
const upstream = await shellCmd( `git rev-parse --abbrev-ref ${branch}@{u}` )
|
|
120
|
+
return { branch, upstream }
|
|
121
|
+
} catch ( err ) {
|
|
122
|
+
return { branch, upstream : undefined }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const gitRepoIsDirty = async () => {
|
|
127
|
+
const cleanliness = await shellCmd( 'git status --porcelain' )
|
|
128
|
+
return cleanliness?.length
|
|
129
|
+
}
|
|
130
|
+
|
|
103
131
|
export const parseJsonFile = async ( jsonFile ) => {
|
|
104
132
|
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
105
133
|
if ( existsSync( jsonFile ) ) {
|
|
@@ -290,7 +318,7 @@ https://console.cloud.google.com/artifacts/browse/leverege-registry?project=leve
|
|
|
290
318
|
return { artifactRegistry, ...packageJson }
|
|
291
319
|
}
|
|
292
320
|
|
|
293
|
-
export const parseHelmChart = async ( helmroot ) => {
|
|
321
|
+
export const parseHelmChart = async ( helmroot = './helm' ) => {
|
|
294
322
|
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
295
323
|
const chartFiles = readdirSync( helmroot, { encoding : 'utf8', recursive : true } )
|
|
296
324
|
const chartYaml = YAML.load( readFileSync( `${helmroot}/Chart.yaml`, 'utf8' ) )
|
|
@@ -308,6 +336,7 @@ export const parseHelmChart = async ( helmroot ) => {
|
|
|
308
336
|
export const analyzeRepository = async () => {
|
|
309
337
|
const gitRoot = await getGitRootDirectory()
|
|
310
338
|
const subPkgs = await glob( `${gitRoot}/packages/**/package.json`, { ignore : '/**/node_modules/**' } )
|
|
339
|
+
const helmChart = await parseHelmChart()
|
|
311
340
|
const isMonoRepo = subPkgs?.length > 0
|
|
312
341
|
|
|
313
342
|
// Verify the presence of a root package.json file for all monorepos
|
|
@@ -355,6 +384,7 @@ export const analyzeRepository = async () => {
|
|
|
355
384
|
const containerName = closestPackageJson.name
|
|
356
385
|
const artifactProject = closestPackageJson.artifactRegistry.project
|
|
357
386
|
const artifactRegistry = closestPackageJson.leverege.registry
|
|
387
|
+
const packageVersion = `v${closestPackageJson.version}`
|
|
358
388
|
|
|
359
389
|
const currentDir = process.cwd()
|
|
360
390
|
const closestPkg = await packageUp()
|
|
@@ -381,10 +411,12 @@ export const analyzeRepository = async () => {
|
|
|
381
411
|
closestPkg,
|
|
382
412
|
gitRoot,
|
|
383
413
|
gitRemote,
|
|
414
|
+
helmChart,
|
|
384
415
|
isMonoRepo,
|
|
385
416
|
isNpmWorkspace,
|
|
386
417
|
rootPackageJson,
|
|
387
418
|
closestPackageJson,
|
|
419
|
+
packageVersion,
|
|
388
420
|
subPkgs,
|
|
389
421
|
wsName,
|
|
390
422
|
}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
debug, log,
|
|
9
9
|
errorExit,
|
|
10
10
|
analyzeRepository,
|
|
11
|
+
getGitUpstream,
|
|
12
|
+
gitRepoIsDirty,
|
|
11
13
|
proceed,
|
|
12
14
|
shellCmd } from './Utils.mjs'
|
|
13
15
|
|
|
@@ -16,23 +18,55 @@ import docker from './Docker.mjs'
|
|
|
16
18
|
// refresh-npm-token does not throw so no need to try, but it emits in debug
|
|
17
19
|
const refreshErr = await shellCmd( 'refresh-npm-token' )
|
|
18
20
|
if ( refreshErr && !process.env.BUILD_TOOLS_DEBUG ) {
|
|
19
|
-
|
|
21
|
+
log( refreshErr ) // most likely the token refreshed message
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
// Expected to be invoked like docker-to-registry v1.2.3
|
|
23
25
|
const imageVersion = process.argv[2]
|
|
26
|
+
if ( !imageVersion ) {
|
|
27
|
+
errorExit( '***Error: docker-to-registry requires an image version' )
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
const willGitTag = imageVersion.match( /^v\d+\.\d+\.\d+$/ )
|
|
25
31
|
const tagInfo = willGitTag ?
|
|
26
32
|
chalk.yellow.bold( 'will be git tagged' ) :
|
|
27
33
|
chalk.red( 'BETA RELEASE WILL NOT BE GIT TAGGED' )
|
|
28
34
|
|
|
35
|
+
const repoIsDirty = await gitRepoIsDirty()
|
|
36
|
+
|
|
37
|
+
if ( willGitTag && repoIsDirty ) {
|
|
38
|
+
log( `
|
|
39
|
+
${chalk.red.bold( '***ERROR: attempting to tag a non-beta dirty git repository' )}
|
|
40
|
+
|
|
41
|
+
You are attempting to create a tagged release image for pushing to the
|
|
42
|
+
artifact registry, but there are locally modified files. This is not
|
|
43
|
+
allowed since the applied tag will not be relevant to the image version
|
|
44
|
+
due to the pending commits.
|
|
45
|
+
|
|
46
|
+
Available options for proceeding:
|
|
47
|
+
|
|
48
|
+
${chalk.green.bold( '1) cleanly commit all local mods assuming relevance' )}
|
|
49
|
+
${chalk.yellow( '2) eliminate unwanted local modifications' )}
|
|
50
|
+
${chalk.red( '3) stash anything that is irrelevant to this release' )}
|
|
51
|
+
|
|
52
|
+
` )
|
|
53
|
+
process.exit( 1 )
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
const repoDescr = await analyzeRepository()
|
|
30
57
|
debug( { repoDescr }, '<==The Repo Description' )
|
|
31
58
|
|
|
32
59
|
const dockerInfo = await docker.generateDockerfile()
|
|
33
60
|
debug( { dockerInfo }, '<==The Docker Info' )
|
|
34
61
|
|
|
35
|
-
const {
|
|
62
|
+
const {
|
|
63
|
+
artifactProject,
|
|
64
|
+
artifactRegistry,
|
|
65
|
+
packageVersion,
|
|
66
|
+
containerName,
|
|
67
|
+
helmChart,
|
|
68
|
+
isNpmWorkspace,
|
|
69
|
+
} = repoDescr
|
|
36
70
|
|
|
37
71
|
if ( imageVersion === 'guide-me' ) {
|
|
38
72
|
await docker.validateCloudBuildBucket( artifactProject )
|
|
@@ -50,6 +84,9 @@ if ( imageVersion === 'guide-me' ) {
|
|
|
50
84
|
log( `
|
|
51
85
|
|
|
52
86
|
|
|
87
|
+
|
|
88
|
+
${chalk.green.bold( '-------------------- Potential Helm Chart Mods --------------------' )}
|
|
89
|
+
|
|
53
90
|
Moving to the new artifact-registry for docker image storage may also require
|
|
54
91
|
helm chart changes depending on how old the current charts are. Charts that
|
|
55
92
|
contain helm/values.yaml files that resemble this:
|
|
@@ -89,6 +126,31 @@ if ( imageVersion === 'guide-me' ) {
|
|
|
89
126
|
process.exit( 0 )
|
|
90
127
|
}
|
|
91
128
|
|
|
129
|
+
// do chart checks too
|
|
130
|
+
const chart = helmChart?.yaml['Chart.yaml']
|
|
131
|
+
const values = helmChart?.yaml['values.yaml']
|
|
132
|
+
const expectedChartRegistry = `${artifactRegistry}/images`
|
|
133
|
+
|
|
134
|
+
if ( expectedChartRegistry !== values.image.registry ) {
|
|
135
|
+
log( `
|
|
136
|
+
${chalk.red.bold( '***Error: mismatched package.json registry and helm/values.yaml' )}
|
|
137
|
+
|
|
138
|
+
The registry specified in the package.json leverege stanza does not line up
|
|
139
|
+
with the image registry in helm/values.yaml. The naming convention expects
|
|
140
|
+
the '/images' suffix to be added to the package.json registry string and then
|
|
141
|
+
stored as the image.registry in the helm/values.yaml file.
|
|
142
|
+
|
|
143
|
+
Current settings:
|
|
144
|
+
package.json registry => ${chalk.yellow.bold( artifactRegistry )}
|
|
145
|
+
values.yaml registry => ${chalk.red.bold( values.image.registry )}
|
|
146
|
+
|
|
147
|
+
Expected settings:
|
|
148
|
+
values.yaml registry => ${chalk.green.bold( expectedChartRegistry )}
|
|
149
|
+
` )
|
|
150
|
+
|
|
151
|
+
process.exit( 1 )
|
|
152
|
+
}
|
|
153
|
+
|
|
92
154
|
// Give the summary and the user a chance to proceed or not
|
|
93
155
|
log( `
|
|
94
156
|
${chalk.green.bold( 'Build Information:' )}
|
|
@@ -97,7 +159,7 @@ log( `
|
|
|
97
159
|
Registry: ${chalk.green.bold( artifactRegistry )}
|
|
98
160
|
Image Folder: ${chalk.yellow.bold( registryFolder )}
|
|
99
161
|
Monorepo: ${chalk.green.bold( repoDescr.isMonoRepo )}
|
|
100
|
-
NPM Workspace: ${chalk.green.bold(
|
|
162
|
+
NPM Workspace: ${chalk.green.bold( isNpmWorkspace )}
|
|
101
163
|
|
|
102
164
|
${chalk.green.bold( 'Docker Information:' )}
|
|
103
165
|
NodeImage: ${chalk.green.bold( dockerInfo.nodeimage )}
|
|
@@ -108,11 +170,65 @@ log( `
|
|
|
108
170
|
NPM Logs: ${chalk.green.bold( dockerInfo.npmlogging )}
|
|
109
171
|
` )
|
|
110
172
|
|
|
173
|
+
if ( imageVersion !== packageVersion ) {
|
|
174
|
+
log( `
|
|
175
|
+
${chalk.yellow.bold( '***WARNING: specified version does not match package.json version' )}
|
|
176
|
+
package.json => ${chalk.green.bold( packageVersion )}
|
|
177
|
+
specified => ${chalk.red.bold( imageVersion )}
|
|
178
|
+
` )
|
|
179
|
+
|
|
180
|
+
if ( willGitTag ) {
|
|
181
|
+
log( chalk.red.bold( '... and you are attempting to release so NOPE!\n' ) )
|
|
182
|
+
process.exit( 1 )
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const repoInfo = await getGitUpstream()
|
|
187
|
+
|
|
188
|
+
if ( willGitTag && !repoInfo.upstream ) {
|
|
189
|
+
log( `
|
|
190
|
+
${chalk.red.bold( '***ERROR: the current branch must have an upstream' )}
|
|
191
|
+
|
|
192
|
+
You are attempting to create a tagged release image for pushing to the
|
|
193
|
+
container registry, but the current branch does not have an upstream to
|
|
194
|
+
push to. This is not allowed since the applied tag will be stranded here
|
|
195
|
+
in your local repository, which is what we are trying to avoid.
|
|
196
|
+
|
|
197
|
+
Available options for proceeding:` )
|
|
198
|
+
log( chalk.green.bold( `
|
|
199
|
+
1) set an upstream for this branch via:
|
|
200
|
+
git push --set-upstream origin ${repoInfo.branch}` ) )
|
|
201
|
+
log( chalk.yellow.bold( `
|
|
202
|
+
2) rebase, squash and merge onto a branch with an upstream` ) )
|
|
203
|
+
|
|
204
|
+
process.exit( 1 )
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if ( willGitTag ) {
|
|
208
|
+
if ( chart.appVersion !== imageVersion ) {
|
|
209
|
+
log( `
|
|
210
|
+
${chalk.yellow.bold( '***WARNING: the helm/Chart.yaml appVersion does not align with the build version' )}
|
|
211
|
+
chart appVersion => ${chalk.yellow.bold( chart.appVersion )}
|
|
212
|
+
build version => ${chalk.green.bold( imageVersion )}
|
|
213
|
+
` )
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
111
217
|
await proceed()
|
|
112
218
|
|
|
113
|
-
if (
|
|
219
|
+
if ( isNpmWorkspace ) {
|
|
114
220
|
log( chalk.yellow.bold( 'Generating an NPM workspace package-lock.json file' ) )
|
|
115
221
|
await shellCmd( 'npm install --package-lock-only --workspaces false', { stdio : 'inherit' } )
|
|
116
222
|
}
|
|
117
223
|
|
|
118
|
-
await docker.buildContainerImage( { artifactProject, artifactRegistry, containerName, imageVersion } )
|
|
224
|
+
await docker.buildContainerImage( { artifactProject, artifactRegistry, containerName, imageVersion, isNpmWorkspace } )
|
|
225
|
+
|
|
226
|
+
if ( willGitTag ) {
|
|
227
|
+
const tagDescription = `docker_${imageVersion}`
|
|
228
|
+
const fullImageTag = repoDescr.isMonoRepo ? `${repoDescr.containerName}/${imageVersion}` : imageVersion
|
|
229
|
+
log( chalk.green.bold( `Tagging and Pushing ${fullImageTag}\n` ) )
|
|
230
|
+
await shellCmd( `git tag -f ${fullImageTag} -m ${tagDescription}` )
|
|
231
|
+
await shellCmd( 'git push --follow-tags' )
|
|
232
|
+
} else {
|
|
233
|
+
log( chalk.yellow.bold( '\n*** Skipped git tagging - beta release' ) )
|
|
234
|
+
}
|
package/src/k8x.sh
CHANGED
|
@@ -16,6 +16,21 @@ cmd=${@:-"bash"}
|
|
|
16
16
|
ns="default"
|
|
17
17
|
|
|
18
18
|
case "$cmd" in
|
|
19
|
+
"help")
|
|
20
|
+
cat<<K8X_HELP
|
|
21
|
+
|
|
22
|
+
Usage: `color g 'k8x <target>'` where target can be:
|
|
23
|
+
|
|
24
|
+
`color g cnpg` - run bash on a cloudnative-pg cluster node
|
|
25
|
+
`color g es8` - run bash on an elasticsearch8 node
|
|
26
|
+
`color g psql` - run psql on a legacy database node
|
|
27
|
+
`color g redis` - run redis-cli on a redis master/replica
|
|
28
|
+
`color g help` - this right here
|
|
29
|
+
|
|
30
|
+
K8X_HELP
|
|
31
|
+
exit 0
|
|
32
|
+
;;
|
|
33
|
+
|
|
19
34
|
"bash")
|
|
20
35
|
# invoked simply as k8x
|
|
21
36
|
;;
|
|
@@ -36,29 +51,8 @@ case "$cmd" in
|
|
|
36
51
|
FZFQ="--query=redis" && cmd='redis-cli'
|
|
37
52
|
;;
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
then
|
|
42
|
-
cat<<BAD_K8X
|
|
43
|
-
|
|
44
|
-
`color r ***Error:` unrecognized k8x target => `color y "$cmd"`
|
|
45
|
-
BAD_K8X
|
|
46
|
-
k8x.sh help
|
|
47
|
-
exit 1
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
cat<<K8X_HELP
|
|
51
|
-
|
|
52
|
-
Usage: `color g 'k8x <target>'` where target can be:
|
|
53
|
-
|
|
54
|
-
`color g cnpg` - run bash on a cloudnative-pg cluster node
|
|
55
|
-
`color g es8` - run bash on an elasticsearch8 node
|
|
56
|
-
`color g psql` - run psql on a legacy database node
|
|
57
|
-
`color g redis` - run redis-cli on a redis master/replica
|
|
58
|
-
`color g help` - this right here
|
|
59
|
-
|
|
60
|
-
K8X_HELP
|
|
61
|
-
exit 0
|
|
54
|
+
*)
|
|
55
|
+
FZFQ="--query=$cmd" && cmd='bash'
|
|
62
56
|
;;
|
|
63
57
|
esac
|
|
64
58
|
|