forked from nashtech-garage/yas
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJenkinsfile.build
More file actions
540 lines (463 loc) · 18.7 KB
/
Copy pathJenkinsfile.build
File metadata and controls
540 lines (463 loc) · 18.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
import groovy.transform.Field
@Field def SERVICE_PARAM_MAP = [
cart : 'TAG_CART',
tax : 'TAG_TAX',
order : 'TAG_ORDER',
product : 'TAG_PRODUCT',
media : 'TAG_MEDIA',
rating : 'TAG_RATING',
customer : 'TAG_CUSTOMER',
location : 'TAG_LOCATION',
inventory : 'TAG_INVENTORY',
search : 'TAG_SEARCH',
payment : 'TAG_PAYMENT',
promotion : 'TAG_PROMOTION',
recommendation : 'TAG_RECOMMENDATION',
sampledata : 'TAG_SAMPLEDATA',
webhook : 'TAG_WEBHOOK',
'backoffice-bff': 'TAG_BACKOFFICE_BFF',
'storefront-bff': 'TAG_STOREFRONT_BFF',
'backoffice-ui' : 'TAG_BACKOFFICE_UI',
'storefront-ui' : 'TAG_STOREFRONT_UI'
]
@Field def SERVICE_IMAGE_KEY_MAP = [
'backoffice-ui': 'ui',
'storefront-ui': 'ui'
]
@Field def RESOLVED_BRANCH_BY_SERVICE = [:]
@Field def RESOLVED_TAG_BY_SERVICE = [:]
@Field def NON_MAIN_SERVICES = []
@Field def FRONTEND_ENTRY_SERVICES = ['storefront-ui', 'backoffice-ui']
def normalizeName(String input) {
return input.toLowerCase().replaceAll('[^a-z0-9-]', '-').replaceAll('-+', '-').replaceAll('^-|-$', '')
}
def normalizeBranchInput(String rawValue) {
def value = (rawValue ?: 'main').trim()
if (!value) {
return 'main'
}
value = value.replaceFirst('^refs/heads/', '')
value = value.replaceFirst('^origin/', '')
return value
}
def validateBranchName(String branchName) {
if (branchName == 'main') {
return
}
if (!(branchName ==~ /^[A-Za-z0-9._\\/-]+$/)) {
error("Invalid branch name '${branchName}'. Allowed characters: letters, numbers, '.', '_', '-', '/'.")
}
}
def resolveBranchToShortCommit(String repositoryUrl, String branchName) {
validateBranchName(branchName)
if (branchName == 'main') {
return 'main'
}
def output = sh(
script: "git ls-remote --heads '${repositoryUrl}' 'refs/heads/${branchName}'",
returnStdout: true
).trim()
if (!output) {
error("Branch '${branchName}' does not exist in source repository: ${repositoryUrl}")
}
def commitSha = output.tokenize()[0]
if (!commitSha || commitSha.length() < 8) {
error("Could not resolve a valid commit SHA for branch '${branchName}'.")
}
return commitSha.take(8)
}
def printDomainInstructions(String deployDomain) {
def publicIp = sh(
script: "curl -s ifconfig.me || curl -s icanhazip.com",
returnStdout: true
).trim() ?: '127.0.0.1'
def minikubeIp = sh(
script: "kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type==\"InternalIP\")].address}' || true",
returnStdout: true
).trim()
echo "=========================================================================="
echo "How to Access the Test Environment"
echo "=========================================================================="
echo "Your services are deployed with the domain: ${deployDomain}"
echo "- Internal IP of Minikube: ${minikubeIp}"
echo "- Public IP of the VM: ${publicIp}"
echo ""
echo "PLEASE CONFIGURE THE HOSTS FILE ON YOUR LOCAL MACHINE:"
echo "- Windows: C:\\Windows\\System32\\drivers\\etc\\hosts"
echo "- MacOS/Linux: /etc/hosts"
echo ""
echo "Please add the following entries to your hosts file to access the services:"
echo "------------------------------------------------"
echo "${publicIp} api.${deployDomain}"
echo "${publicIp} storefront.${deployDomain}"
echo "${publicIp} backoffice.${deployDomain}"
echo "------------------------------------------------"
echo ""
echo "After saving the hosts file, access the test environment using the following links:"
echo "API Gateway: http://api.${deployDomain}/swagger-ui"
echo "Storefront UI: http://storefront.${deployDomain}"
echo "Backoffice UI: http://backoffice.${deployDomain}"
echo "=========================================================================="
}
def deployService(
def scriptCtx,
String service,
String testNamespace,
String deployDomain,
String sharedValuesFile,
Map resolvedTagByService,
Map resolvedBranchByService,
Map serviceImageKeyMap,
List frontendEntryServices
) {
def resolvedTag = resolvedTagByService[service]
def branchName = resolvedBranchByService[service]
def imageKey = serviceImageKeyMap.get(service, 'backend')
def isUiService = serviceImageKeyMap.containsKey(service)
def ingressHost = 'api.' + deployDomain
if (service == 'backoffice-bff') {
ingressHost = 'backoffice.' + deployDomain
} else if (service == 'storefront-bff') {
ingressHost = 'storefront.' + deployDomain
}
if (!resolvedTag) {
scriptCtx.error("Missing resolved tag for service '${service}'.")
}
if (isUiService) {
scriptCtx.sh """
helm dependency build charts/${service}
helm upgrade --install ${service} charts/${service} \\
-n ${testNamespace} \\
-f ${sharedValuesFile} \\
--set ${imageKey}.image.tag=${resolvedTag} \\
--wait --timeout 10m
"""
} else {
scriptCtx.sh """
helm dependency build charts/${service}
helm upgrade --install ${service} charts/${service} \\
-n ${testNamespace} \\
-f ${sharedValuesFile} \\
--set ${imageKey}.image.tag=${resolvedTag} \\
--set backend.ingress.host=${ingressHost} \\
--wait --timeout 10m
"""
}
scriptCtx.echo("Deployed ${service} from branch '${branchName}' with image tag '${resolvedTag}'.")
}
def seedMediaAssets(def scriptCtx, String namespace, String workspaceDir) {
def sampleImagesPath = "${workspaceDir}/sampledata/images/sample"
scriptCtx.sh """
set -e
if [ ! -d '${sampleImagesPath}' ]; then
echo "Skipping media asset seeding: '${sampleImagesPath}' not found."
exit 0
fi
kubectl rollout status deployment/media -n '${namespace}' --timeout=180s
MEDIA_POD=\$(kubectl get pod -n '${namespace}' -l app.kubernetes.io/name=media -o jsonpath='{.items[0].metadata.name}')
if [ -z "\$MEDIA_POD" ]; then
echo "Skipping media asset seeding: media pod not found in namespace '${namespace}'."
exit 0
fi
kubectl exec -n '${namespace}' "\$MEDIA_POD" -- mkdir -p /images
kubectl cp '${sampleImagesPath}' '${namespace}/'"\$MEDIA_POD":/images/
echo "Media assets seeded to pod '\$MEDIA_POD' in namespace '${namespace}'."
"""
}
def applyNamespaceApiGateway(def scriptCtx, String chartsDir, String namespace, String deployDomain) {
def gatewayManifest = "${chartsDir}/infrastructure/scripts/nginx-api-gateway.yaml"
scriptCtx.sh """
set -e
if [ ! -f '${gatewayManifest}' ]; then
echo "Skipping API gateway apply: '${gatewayManifest}' not found."
exit 0
fi
TMP_MANIFEST=\$(mktemp)
cp '${gatewayManifest}' "\$TMP_MANIFEST"
# Replace hardcoded namespace and host with the current developer namespace and domain.
sed -i "s|namespace: test-minhhuy-baseline|namespace: ${namespace}|g" "\$TMP_MANIFEST"
sed -i "s|\\.test-minhhuy-baseline\\.svc\\.cluster\\.local|.${namespace}.svc.cluster.local|g" "\$TMP_MANIFEST"
sed -i "s|host: api.yas.test.com|host: api.${deployDomain}|g" "\$TMP_MANIFEST"
kubectl apply -f "\$TMP_MANIFEST"
kubectl rollout status deployment/nginx -n '${namespace}' --timeout=180s
rm -f "\$TMP_MANIFEST"
echo "Applied namespace-scoped API gateway to namespace '${namespace}' with host 'api.${deployDomain}'."
"""
}
pipeline {
agent any
options {
timestamps()
disableConcurrentBuilds()
}
parameters {
string(name: 'TAG_CART', defaultValue: 'main', description: 'Input branch name for cart (e.g., main, dev_cart_service)')
string(name: 'TAG_TAX', defaultValue: 'main', description: 'Input branch name for tax (e.g., main, dev_tax_service)')
string(name: 'TAG_ORDER', defaultValue: 'main', description: 'Input branch name for order (e.g., main, dev_order_service)')
string(name: 'TAG_PRODUCT', defaultValue: 'main', description: 'Input branch name for product (e.g., main, dev_product_service)')
string(name: 'TAG_MEDIA', defaultValue: 'main', description: 'Input branch name for media (e.g., main, dev_media_service)')
string(name: 'TAG_RATING', defaultValue: 'main', description: 'Input branch name for rating (e.g., main, dev_rating_service)')
string(name: 'TAG_CUSTOMER', defaultValue: 'main', description: 'Input branch name for customer (e.g., main, dev_customer_service)')
string(name: 'TAG_LOCATION', defaultValue: 'main', description: 'Input branch name for location (e.g., main, dev_location_service)')
string(name: 'TAG_INVENTORY', defaultValue: 'main', description: 'Input branch name for inventory (e.g., main, dev_inventory_service)')
string(name: 'TAG_SEARCH', defaultValue: 'main', description: 'Input branch name for search (e.g., main, dev_search_service)')
string(name: 'TAG_PAYMENT', defaultValue: 'main', description: 'Input branch name for payment (e.g., main, dev_payment_service)')
string(name: 'TAG_PROMOTION', defaultValue: 'main', description: 'Input branch name for promotion (e.g., main, dev_promotion_service)')
string(name: 'TAG_RECOMMENDATION', defaultValue: 'main', description: 'Input branch name for recommendation (e.g., main, dev_recommendation_service)')
string(name: 'TAG_SAMPLEDATA', defaultValue: 'main', description: 'Input branch name for sampledata (e.g., main, dev_sampledata_service)')
string(name: 'TAG_WEBHOOK', defaultValue: 'main', description: 'Input branch name for webhook (e.g., main, dev_webhook_service)')
string(name: 'TAG_BACKOFFICE_BFF', defaultValue: 'main', description: 'Input branch name for backoffice-bff (e.g., main, dev_backoffice-bff_service)')
string(name: 'TAG_STOREFRONT_BFF', defaultValue: 'main', description: 'Input branch name for storefront-bff (e.g., main, dev_storefront-bff_service)')
string(name: 'TAG_BACKOFFICE_UI', defaultValue: 'main', description: 'Input branch name for backoffice-ui (e.g., main, dev_backoffice-ui_service)')
string(name: 'TAG_STOREFRONT_UI', defaultValue: 'main', description: 'Input branch name for storefront-ui (e.g., main, dev_storefront-ui_service)')
}
environment {
SOURCE_REPO_URL = 'https://github.com/AkiraTomori/DevOps-YAS.git'
GITOPS_REPO_URL = 'https://github.com/AkiraTomori/ArgoCD-Advanced.git'
GITOPS_TOKEN_CREDENTIALS_ID = 'gitops-token'
GITOPS_DIR = 'gitops-yas'
GITOPS_COMMIT_USER = 'jenkins-bot'
GITOPS_COMMIT_EMAIL = 'jenkins@local'
CHARTS_DIR = 'gitops-yas'
INFRASTRUCTURE_DIR = 'infrastructure'
HELM_SHARED_VALUES_FILE = 'environments/test/values-shared.yaml'
LEGACY_CLUSTER_CONFIG_FILE = 'infrastructure/scripts/cluster-config.yaml'
CLEAN_JOB_NAME = 'developer_clean'
CLEAN_JOB_TOKEN = 'yas-destroy-token'
}
stages {
stage('Checkout DevOps Repository') {
steps {
checkout scm
}
}
stage('Resolve Branch Inputs To Image Tags') {
steps {
script {
def branchToServices = [:]
SERVICE_PARAM_MAP.each { service, paramName ->
def normalizedBranch = normalizeBranchInput(params[paramName])
RESOLVED_BRANCH_BY_SERVICE[service] = normalizedBranch
if (normalizedBranch != 'main') {
NON_MAIN_SERVICES << service
}
if (!branchToServices.containsKey(normalizedBranch)) {
branchToServices[normalizedBranch] = []
}
branchToServices[normalizedBranch] << service
}
def branchToTag = ['main': 'latest']
branchToServices.each { branchName, services ->
if (branchName == 'main') {
return
}
try {
def shortCommit = resolveBranchToShortCommit(env.SOURCE_REPO_URL, branchName)
branchToTag[branchName] = shortCommit
echo "Resolved branch '${branchName}' -> image tag '${shortCommit}' for services: ${services.join(', ')}"
} catch (Exception e) {
error("Failed to resolve branch '${branchName}' for services [${services.join(', ')}]. ${e.getMessage()}")
}
}
SERVICE_PARAM_MAP.keySet().each { service ->
def branchName = RESOLVED_BRANCH_BY_SERVICE[service]
RESOLVED_TAG_BY_SERVICE[service] = branchToTag[branchName]
}
echo "Final service -> branch map: ${RESOLVED_BRANCH_BY_SERVICE}"
echo "Final service -> image tag map: ${RESOLVED_TAG_BY_SERVICE}"
}
}
}
stage('Resolve Namespace') {
steps {
script {
def causes = currentBuild.getBuildCauses()
def rawUserId = causes.find { it.userId }?.userId ?: 'anonymous'
def userId = normalizeName(rawUserId)
def suffix = NON_MAIN_SERVICES ? NON_MAIN_SERVICES.join('-') : 'baseline'
suffix = normalizeName(suffix)
def namespace = normalizeName("test-${userId}-${suffix}")
if (namespace.length() > 63) {
namespace = namespace.take(63).replaceAll('-+$', '')
}
env.TEST_NAMESPACE = namespace
env.CLEAN_JOB_URL = "${env.JENKINS_URL}job/${env.CLEAN_JOB_NAME}/buildWithParameters?token=${env.CLEAN_JOB_TOKEN}&TARGET_NAMESPACE=${env.TEST_NAMESPACE}&FORCE_DELETE=true"
echo "Resolved Jenkins user id: ${userId}"
echo "Changed services: ${NON_MAIN_SERVICES ? NON_MAIN_SERVICES.join(', ') : 'none'}"
echo "Generated namespace: ${env.TEST_NAMESPACE}"
}
}
}
stage('Clone ArgoCD Charts Repository') {
steps {
script {
def repoNoProtocol = env.GITOPS_REPO_URL.replaceFirst('https://', '')
withCredentials([string(credentialsId: env.GITOPS_TOKEN_CREDENTIALS_ID, variable: 'GITOPS_TOKEN')]) {
sh """
rm -rf ${env.GITOPS_DIR}
git clone --depth 1 https://x-access-token:${GITOPS_TOKEN}@${repoNoProtocol} ${env.GITOPS_DIR}
"""
}
}
}
}
stage('Create Namespace') {
steps {
sh """
kubectl create namespace ${env.TEST_NAMESPACE} --dry-run=client -o yaml | kubectl apply -f -
kubectl label namespace ${env.TEST_NAMESPACE} managed-by=developer-build --overwrite
kubectl label namespace ${env.TEST_NAMESPACE} owner=${normalizeName((currentBuild.getBuildCauses().find { it.userId }?.userId ?: 'anonymous'))} --overwrite
"""
}
}
stage('Deploy Shared Configuration') {
steps {
dir("${env.CHARTS_DIR}") {
sh """
helm repo add stakater https://stakater.github.io/stakater-charts
helm repo update
helm dependency build charts/yas-configuration
helm upgrade --install yas-configuration charts/yas-configuration \\
-n ${env.TEST_NAMESPACE} \\
-f ${env.HELM_SHARED_VALUES_FILE} \\
--wait --timeout 10m
"""
}
}
}
stage('Load Deploy Domain') {
steps {
script {
env.DEPLOY_DOMAIN = sh(
script: "yq -r '.global.domain' ${env.CHARTS_DIR}/${env.HELM_SHARED_VALUES_FILE}",
returnStdout: true
).trim()
if (!env.DEPLOY_DOMAIN) {
error("Domain is missing in ${env.CHARTS_DIR}/${env.HELM_SHARED_VALUES_FILE} (.global.domain)")
}
echo "Loaded deploy domain from values-shared.yaml: ${env.DEPLOY_DOMAIN}"
}
}
}
stage('Validate Domain Consistency') {
steps {
script {
def legacyDomain = sh(
script: "yq -r '.domain // \"\"' ${env.CHARTS_DIR}/${env.LEGACY_CLUSTER_CONFIG_FILE} 2>/dev/null || true",
returnStdout: true
).trim()
if (legacyDomain && legacyDomain != env.DEPLOY_DOMAIN) {
error("Domain mismatch detected: values-shared(.global.domain)='${env.DEPLOY_DOMAIN}' vs legacy cluster-config(.domain)='${legacyDomain}'.")
}
if (legacyDomain) {
echo "Domain consistency check passed against legacy cluster-config.yaml: ${legacyDomain}"
} else {
echo "Legacy cluster-config domain not found. Skipping consistency check."
}
}
}
}
stage('Deploy Services') {
steps {
script {
dir("${env.CHARTS_DIR}") {
def prioritizedServices = ['storefront-ui', 'storefront-bff', 'backoffice-ui', 'backoffice-bff']
echo "Deploying Prioritized Services & Swagger in PARALLEL..."
def prioritizedTasks = [:]
prioritizedServices.each { service ->
if (SERVICE_PARAM_MAP.containsKey(service)) {
def svc = service
prioritizedTasks["Deploy ${svc}"] = {
deployService(
this,
svc,
env.TEST_NAMESPACE,
env.DEPLOY_DOMAIN,
env.HELM_SHARED_VALUES_FILE,
RESOLVED_TAG_BY_SERVICE,
RESOLVED_BRANCH_BY_SERVICE,
SERVICE_IMAGE_KEY_MAP,
FRONTEND_ENTRY_SERVICES
)
}
}
}
prioritizedTasks["Deploy swagger-ui"] = {
sh """
helm dependency build charts/swagger-ui
helm upgrade --install swagger-ui charts/swagger-ui \\
-n ${env.TEST_NAMESPACE} \\
-f ${env.HELM_SHARED_VALUES_FILE} \\
--set ingress.host=api.${env.DEPLOY_DOMAIN} \\
--wait --timeout 10m
"""
}
parallel prioritizedTasks
def remainingServices = SERVICE_PARAM_MAP.keySet().findAll { !prioritizedServices.contains(it) }
echo "Deploying remaining Backend services in PARALLEL: ${remainingServices.join(', ')}"
def remainingTasks = [:]
remainingServices.each { service ->
def svc = service
remainingTasks["Deploy ${svc}"] = {
deployService(
this,
svc,
env.TEST_NAMESPACE,
env.DEPLOY_DOMAIN,
env.HELM_SHARED_VALUES_FILE,
RESOLVED_TAG_BY_SERVICE,
RESOLVED_BRANCH_BY_SERVICE,
SERVICE_IMAGE_KEY_MAP,
FRONTEND_ENTRY_SERVICES
)
}
}
parallel remainingTasks
}
}
}
}
stage('Apply Namespace API Gateway (Swagger)') {
steps {
script {
applyNamespaceApiGateway(this, env.CHARTS_DIR, env.TEST_NAMESPACE, env.DEPLOY_DOMAIN)
}
}
}
stage('Seed Media Assets') {
steps {
script {
seedMediaAssets(this, env.TEST_NAMESPACE, env.WORKSPACE)
}
}
}
}
post {
always {
cleanWs()
}
success {
script {
echo "=========================================================================="
echo "DEPLOYMENT SUCCESSFUL!"
echo "Your test environment '${env.TEST_NAMESPACE}' is ready."
printDomainInstructions(env.DEPLOY_DOMAIN)
echo "To clean up this environment, click: ${env.CLEAN_JOB_URL}"
echo "=========================================================================="
currentBuild.displayName = "#${currentBuild.number} - ${env.TEST_NAMESPACE}"
currentBuild.description = "Deployment successful. Cleanup URL: ${env.CLEAN_JOB_URL}"
}
}
failure {
script {
echo "=========================================================================="
echo "DEPLOYMENT FAILED!"
echo "Please check the logs above for details."
echo "Cleanup URL: ${env.CLEAN_JOB_URL}"
echo "=========================================================================="
currentBuild.displayName = "#${currentBuild.number} - ${env.TEST_NAMESPACE}"
currentBuild.description = "Deployment failed. Cleanup URL: ${env.CLEAN_JOB_URL}"
}
}
}
}