Переделка CI/CD

This commit is contained in:
2025-09-09 12:43:41 +03:00
parent 68bf3f9056
commit 9c8261d031
6 changed files with 398 additions and 213 deletions
+270 -72
View File
@@ -1,88 +1,286 @@
@Library(['jlib']) _ /*
! Jenkins Watchdog Pipeline
node("docker") { Этот Pipeline реализует сборку на Kubernetes worker-ноде с контролем и завершением со стороны Jenkins master-ноды.
// Временная метка в логе Jenkins Master отслеживает статус worker'а в реальном времени и завершает процесс в случае ошибки, таймаута или успешного завершения.
timestamps() {
// Цветные букавы в логе Jenkins
ansiColor("xterm"){
// Получаем актуальную версию репы, где лежит пайп
checkout scm
env.stand = env.BRANCH_NAME
if (!(stand in ["prod", "test", "dev"])) {
error("stand не соответствует 'prod', 'test' или 'dev'")
}
stage("Prepare environments") {
envConfig = "build/vars/build.yaml"
support.getVarsV2(envConfig)
// пока не будет готов отдельный dev стенд
env.namespace = "${env.namespace}-${stand}"
env.registryContainerName = "${env.registryContainerName}-${stand}"
env.version = sh(script: "git rev-parse HEAD", returnStdout: true).trim()
println "Success prepare"
}
stage("Scan Image") {
/* Убрал на время для перехода на Helm всех микросервисов
def scannerHome = tool 'SonarScanner';
// Волшебный ключ -Dsonar.qualitygate.wait=true
withCredentials([string(credentialsId: 'sonarqube-access-personal-account-backend', variable: 'tokenAccess')]) {
sh "${scannerHome}/bin/sonar-scanner -Dsonar.sources=. -Dsonar.projectKey=personal-account-backend -Dsonar.host.url=https://sq.adl.nubes.ru -Dsonar.login=${tokenAccess}"
}
*/ */
}
stage("Build image") { // Статусы воркера. Необходимо мастеру для определения состояния джоба
println "Start building image ${env.registryContainerName}:${env.version}" def workerFailed = false
if (registryRepoPath) def workerSuccess = false
env.imageName = "${registryRepoPath}/${env.registryContainerName}" def DEBUGGING_MODE = false
else // Мапа для запуска мастера и воркера параллельно
env.imageName = "${env.registryContainerName}" def parallelSteps = [:]
app = docker.build("${env.imageName}:${env.version}", "--no-cache -f ./build/Dockerfile .") // Блок констант
// Мапа, в которой будут переменные из vars/build.yaml
buildVars = [:]
buildVars.system = [:]
buildVars.kubeconfig = "config.kube"
println "Start uploading image ${env.registryContainerName}:${env.version} to Nexus" // Подгрузка либы. Версия в свойствах папки Jenkins
library "jlib"
cmdbVars = constants.cmdbVars
docker.withRegistry("https://${registryUrl}/${registryRepoPath}", "registry-jenkins-cred") { TimeoutTime = [
app.push("${env.version}") time: binding.hasVariable('timeoutTime') ? timeoutTime : (cmdbVars?.system?.timeoutTime ?: 20),
app.push("latest") unit: binding.hasVariable('timeoutUnit') ? timeoutUnit : (cmdbVars?.system?.timeoutUnit ?: "MINUTES")
} ]
}
stage("Update version in Kubernetes") { parallelSteps["worker"] = {
dir ("build") { node("docker") {
withCredentials([vaultString(credentialsId: "${env.vaultPath}", variable: "pem")]) { timestamps() {
sh """ ansiColor("xterm"){
set +x dir (env.BUILD_NUMBER) {
echo "${pem}" | base64 -d > auth.config try {
set -x stage ("Prepare environments") {
""" // Выкачивание проекта из Git
checkout scm
// Определение типа джобы:
// - сборка и пуш в кубер: DEV / PR
// - проверка образа (из DEV) и пуш в кубер: TEST / PROD
logger.info("Targeting stand for deploy")
// Если PR - в DEV
if (env.CHANGE_ID)
buildVars.gitBranch = "dev"
else
buildVars.gitBranch = env.BRANCH_NAME
// Проверка на нужные бренчи
if (!(buildVars.gitBranch in ["prod", "test", "dev"])) {
logger.err("Ветка в Git не соответствует 'prod', 'test' или 'dev'", true)
} }
convertJinjaV2("./helm/values.yaml", debug=true) envConfig = "build/vars/build.yaml"
support.getVarsAll(envConfig, buildVars)
sh """ logger.info("Get kubeconfig")
helm upgrade --wait \ try {
--kubeconfig auth.config \ withCredentials([vaultString(credentialsId: "${buildVars.vaultPath}", variable: "pem")]) {
--install \ runsh """
--namespace ${env.namespace} \ set +x
-f ./helm/values.yaml \ echo "${pem}" | base64 -d > ${ buildVars.kubeconfig }
${env.appName} ./helm/ set -x
""" """, true
}
logger.info("kubeconfig getting successfully")
} catch (Exception kubeconfigGetError) {
logger.err("Не удалось получить кубконфиг для подключению к Kubernetes-кластеру. Ошибка: ${kubeconfigGetError.message}", true)
}
sh "kubectl --kubeconfig=auth.config get pods --namespace ${env.namespace}" logger.info("Getting version...")
try {
version = runsh buildVars.getCommandVersion
logger.info("Version app: ${ version }")
} catch (Exception versionGetError) {
logger.err("Не удалось получить версию приложения. Ошибка: ${versionGetError.message}", true)
}
}
// Получение токена для авторизации в GITEA
withCredentials([vaultString(credentialsId: "gitea.services.ngcloud.ru-deck-token", variable: "token")]) { GITEA_AUTH_TOKEN = token }
// Сборка Image (Только DEV)
if (buildVars.gitBranch == "dev") {
if (buildVars.needBuildLib.toBoolean()) {
stage ("Check library") {
headers = [ [ name: "Authorization", value: "Bearer ${ GITEA_AUTH_TOKEN }" ] ]
// Проверка что либа существует
try {
retry(3) {
httpcall.get( url="https://gitea.services.ngcloud.ru/api/v1/packages/deck/npm/%40nubes%2F${ buildVars.app.name }/${ version }", headerList=headers)
}
needToBuildLib = false
} catch (Exception errGetPackage) {
logger.info("Package does not exist. It's OK")
needToBuildLib = true
}
}
stage ("Build library") {
if (needToBuildLib.toBoolean()) {
retry(3) {
try {
docker.build("${ buildVars.registryImageName }:${ version }", "--no-cache --build-arg GITEA_AUTH_TOKEN=${ GITEA_AUTH_TOKEN } -f ./build/Dockerfile_build_lib .")
} catch (Exception errorDocker) {
logger.err("Не удалось собрать библиотеку. Ошибка: ${ errorDocker.message }", true)
}
}
}
}
}
stage ("Build image") {
catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
logger.info("Start building image ${ buildVars.registryContainerName }:${ version }")
retry(3) {
try {
app = docker.build("${ buildVars.registryImageName }:${ version }", "--no-cache --build-arg GITEA_AUTH_TOKEN=${ GITEA_AUTH_TOKEN } -f ./build/Dockerfile .")
} catch (Exception errorDocker) {
logger.err("Не удалось собрать основной образ. Ошибка: ${ errorDocker.message }", true)
}
}
}
logger.info("Start uploading image ${ buildVars.registryImageName }:${ version }")
docker.withRegistry("https://${ buildVars.registryUrl }/${ buildVars.repoPath }", "registry-jenkins-cred") {
// retry на случай если 500 случайно отдаст registry
retry(3) {
try {
app.push("${ version }")
} catch (Exception errorDocker) {
logger.err("Не удалось запушить образ в Registry. Ошибка: ${ errorDocker.message }", true)
}
}
}
}
}
// Проверка Image в Registry (Всё кроме DEV)
if (buildVars.gitBranch != "dev") {
stage("Check if image exist") {
docker.withRegistry("https://${ buildVars.registryUrl }/${ buildVars.repoPath }", "registry-jenkins-cred") {
def img = docker.image("${ buildVars.registryImageName }:${ version }")
// TODO переписать либу docker под себя
// try/catch не поможет, ошибка выведет всегда 'script returned exit code 1'
// retry на случай если 500 случайно отдаст registry
retry(3) {
try {
img.pull()
} catch (Exception errorDocker) {
logger.err("Не удалось получить образ из Registry. Ошибка: ${ errorDocker.message }", true)
}
}
}
}
}
// Деплой в Kubernetes (все стенды)
stage("Deploy to K8s test") {
logger.info("Check exist pods in k8s...")
retry(3) {
runsh "kubectl --kubeconfig=${ buildVars.kubeconfig } --namespace ${ buildVars.namespace } get po"
}
logger.info("Prepare list of args on helm...")
commandArgs = [
"--set deployment.image.repository=${ buildVars.registryUrl }/${ buildVars.registryImageName }",
"--set deployment.readinessProbe.httpGet.path=${ buildVars.app.health }",
"--set deployment.containerPort=${ buildVars.app.port }",
"--set deployment.resources.requests.cpu=${ buildVars.app.cpu }m",
"--set deployment.resources.requests.memory=${ buildVars.app.mem }Mi",
"--set deployment.resources.limits.cpu=${ buildVars.app.cpu }m",
"--set deployment.resources.limits.memory=${ buildVars.app.mem }Mi",
"--set deployment.image.tag=${ version }",
"--set stand=${ buildVars.gitBranch }"
]
// Если включен Vault
if (buildVars.needStoreVaultSecret.toBoolean()) {
commandArgs.add("--set vault.secretStore=${ buildVars.namespace }")
commandArgs.add("--set serviceAccountRole=${ buildVars.namespace }-sa")
commandArgs.add("--set vault.secrets=\"{${ buildVars.storedSecretsList.join(',') }}\"")
commandArgs.add("--set vault.enable=true")
} else {
commandArgs.add("--set vault.enable=false")
}
installArgs = [
"--kubeconfig ${ buildVars.kubeconfig }",
"--install",
"--namespace ${ buildVars.namespace }",
]
cmdTemplate = "helm template ${commandArgs.join(' ')} ./build/helm/"
cmd = "helm upgrade ${ buildVars.app.name } ${installArgs.join(' ')} ${commandArgs.join(' ')} ./build/helm/"
logger.info("""Command for push...
${cmd}
""")
logger.info("Template helm-release...")
runsh cmdTemplate
logger.info("Push helm-release...")
retry(3) {
// От того, что Helm-чарт будет ждать, мало что изменится
runsh cmd
}
logger.info("Check exist pods in k8s...")
retry(3) {
runsh "kubectl --kubeconfig=${ buildVars.kubeconfig } --namespace ${ buildVars.namespace } get po"
}
// Нужны проверки после установки
buildVars.application = [:]
buildVars.application.typeOfProject = "deployments.apps"
buildVars.application.namespace = buildVars.namespace
// sleep ибо загрузка манифестов в K8S занимает какое-то время
sleep(5)
try {
k8sStagesV2.correctReplicaActive(cmdbVars=buildVars, applicationName=buildVars.app.name)
// Чтобы не оставлять мусора за собой в эвентах
sh "kubectl --kubeconfig=${ buildVars.kubeconfig } --namespace ${ buildVars.namespace } delete events --all"
} catch (Exception errorRun) {
logger.info("Не удалось произвести проверку работы микросервиса. Дебаг:")
runsh "kubectl --kubeconfig=${ buildVars.kubeconfig } --namespace ${ buildVars.namespace } events | grep ${ buildVars.app.name }"
runsh "kubectl --kubeconfig=${ buildVars.kubeconfig } --namespace ${ buildVars.namespace } logs -l 'app=${ buildVars.app.name }'"
error("Не удалось произвести проверку работы микросервиса. Детали в логах pipeline")
}
}
currentBuild.result = 'SUCCESS'
}
catch (Exception err) {
logger.err("Произошла ошибка")
logger.err(err.message, true)
currentBuild.description = err.message
currentBuild.result = "FAILURE"
}
finally {
if (currentBuild.result == 'FAILURE') {
echo "Worker stage действительно упал — ставим флаг workerFailed"
workerFailed = true
} else if (currentBuild.result == 'SUCCESS') {
echo "Worker stage завершился успешно - ставим флаг workerSuccess"
workerSuccess = true
} else {
echo "Неопознаный currentBuild.result: ${currentBuild.result} — ставим флаг workerFailed"
workerFailed = true
}
runsh "rm -rf ${env.BUILD_NUMBER}"
}
}}}
}
}
parallelSteps["master"] = {
node("master") {
try {
stage ("Master: start watchdog") {
timeout(time: TimeoutTime.time, unit: TimeoutTime.unit) {
while (true) {
// Следим за окончанием сборки
if (DEBUGGING_MODE) {
sleep 1
// Только в режиме отладки используем echo
echo "DEBUG: workerFailed = ${workerFailed}"
echo "DEBUG: workerSuccess = ${workerSuccess}"
}
// Проверка флагов без использования шагов Jenkins
if (workerFailed) {
echo "Watchdog: обнаружен сбой в воркере"
break
} else if (workerSuccess) {
echo "Watchdog: воркер завершил работу в Jenkins"
break
}
}
} }
} }
runsh "rm -rf ${env.BUILD_NUMBER}"
println "Success build job. Now start cleanup" }
catch (Exception masterErrorTimeout) {
cleanWs( logger.err("Ошибка по таймауту")
deleteDirs: true,
disableDeferredWipeout: true
)
} }
} }
} }
parallel parallelSteps
+27 -31
View File
@@ -1,53 +1,49 @@
---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
labels: labels:
app: "{{ .Chart.Name }}" app: '{{ .Chart.Name }}'
stand: "{{ .Values.stand }}" stand: '{{ .Values.stand }}'
name: "{{ .Chart.Name }}" name: '{{ .Chart.Name }}'
spec: spec:
replicas: 1 replicas: 1
selector: selector:
matchLabels: matchLabels:
app: "{{ .Chart.Name }}" app: '{{ .Chart.Name }}'
stand: "{{ .Values.stand }}" stand: '{{ .Values.stand }}'
template: template:
metadata: metadata:
labels: labels:
app: "{{ .Chart.Name }}" app: '{{ .Chart.Name }}'
stand: "{{ .Values.stand }}" stand: '{{ .Values.stand }}'
contragentCode: 'WZ01112' contragentCode: 'WZ01112'
billingObject: 'None' billingObject: 'None'
spec: spec:
{{- if .Values.vault.enabled }} {{- if .Values.vault.enable }}
serviceAccountName: "{{ .Values.vault.serviceAccountName }}" serviceAccountName: '{{ .Values.serviceAccountRole }}'
{{- end }} {{- end }}
containers: containers:
# Основное приложение - image: '{{ .Values.deployment.image.repository }}:{{.Values.deployment.image.tag }}'
- name: app
image: "{{ .Values.mainContainer.image.repository }}:{{ .Values.mainContainer.image.tag }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
name: app
ports: ports:
- containerPort: {{ .Values.mainContainer.containerPort }} - containerPort: {{ .Values.deployment.containerPort }}
protocol: TCP protocol: TCP
resources:
{{- toYaml .Values.mainContainer.resources | nindent 12 }}
readinessProbe: readinessProbe:
failureThreshold: 3 {{- toYaml (omit .Values.deployment.readinessProbe "httpGet") | nindent 12 }}
initialDelaySeconds: 10 httpGet:
periodSeconds: 30 {{- toYaml .Values.deployment.readinessProbe.httpGet | nindent 14 }}
successThreshold: 1 port: {{ .Values.deployment.containerPort }}
timeoutSeconds: 2
{{- toYaml .Values.mainContainer.readinessProbe | nindent 12 }}
livenessProbe: livenessProbe:
failureThreshold: 3 {{- toYaml (omit .Values.deployment.livenessProbe "tcpSocket") | nindent 12 }}
initialDelaySeconds: 30 tcpSocket:
periodSeconds: 10 port: {{ .Values.deployment.containerPort }}
successThreshold: 1
timeoutSeconds: 1 resources:
{{- toYaml .Values.mainContainer.livenessProbe | nindent 12 }} {{- toYaml .Values.deployment.resources | nindent 12 }}
{{- if .Values.vault.enabled }} {{- if .Values.vault.enable }}
envFrom: envFrom:
- secretRef: - secretRef:
name: "{{ .Values.vault.target.name }}" name: '{{ .Chart.Name }}-secrets'
{{- end }} {{- end }}
+8 -8
View File
@@ -2,15 +2,15 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: "{{ .Chart.Name }}" name: '{{ .Chart.Name }}'
labels: labels:
app: "{{ .Chart.Name }}" app: '{{ .Chart.Name }}'
stand: "{{ .Values.stand }}" stand: '{{ .Values.stand }}'
spec: spec:
ports: ports:
- port: {{ .Values.mainContainer.containerPort }} - port: {{ .Values.deployment.containerPort }}
targetPort: {{ .Values.mainContainer.containerPort }} targetPort: {{ .Values.deployment.containerPort }}
protocol: TCP protocol: TCP
selector: selector:
app: "{{ .Chart.Name }}" app: '{{ .Chart.Name }}'
stand: "{{ .Values.stand }}" stand: '{{ .Values.stand }}'
+13 -10
View File
@@ -1,20 +1,23 @@
{{- if .Values.vault.enabled }} {{- if .Values.vault.enable }}
---
apiVersion: external-secrets.io/v1beta1 apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret kind: ExternalSecret
metadata: metadata:
name: "{{ .Chart.Name }}" name: '{{ .Chart.Name }}'
labels:
app: '{{ .Chart.Name }}'
spec: spec:
refreshInterval: "60s" refreshInterval: '60s'
secretStoreRef: secretStoreRef:
name: "{{ .Values.vault.secretStore }}" name: '{{ .Values.vault.secretStore }}' # имя SecretStore
kind: SecretStore kind: SecretStore
target: target:
name: "{{ .Values.vault.target.name }}" name: '{{ .Chart.Name }}-secrets' # имя будущего секрета kubernetes
data: data:
{{- range .Values.vault.data }} {{- range .Values.vault.secrets }}
- secretKey: {{ .secretKey }} - secretKey: {{ . }}
remoteRef: remoteRef:
key: "{{ .remoteRef.key }}" key: "deck/kubernetes/{{ $.Values.stand }}/{{ $.Chart.Name }}"
property: "{{ .remoteRef.property }}" property: {{ . }}
{{- end }} {{- end }}
{{- end }} {{- end }}
+31 -77
View File
@@ -1,88 +1,42 @@
# # Пример для DEV сборки
# Данный конфигурационный файл не является конечным исполнением stand: dev
# Параметры подставлены исключительно для тестов и могут быть измененены в соответствии с Pipeline
#
stand: {{ stand }} deployment:
mainContainer:
###### Репозиторий
image: image:
repository: "{{ registryUrl }}/{{ imageName }}" repository: drn.tst.nubes.ru/deck-auth-api-test
tag: {{ version }} tag: $VERSION
containerPort: {{ appPort }} containerPort: 8080
###### Ресурсы
resources: resources:
requests: requests: # Начальные запросы
cpu: "500m" cpu: "500m"
memory: "1024Mi" memory: "256Mi"
limits: limits:
cpu: "1000m" cpu: "2" # Максимальные лимиты
memory: "1024Mi" memory: "1024Mi"
###### Пробы контейнера
readinessProbe: readinessProbe:
tcpSocket: failureThreshold: 5
port: {{ appPort }} initialDelaySeconds: 10
livenessProbe: periodSeconds: 30
successThreshold: 1
timeoutSeconds: 5
httpGet: httpGet:
path: /api/v1 path: /api/v1/health
port: {{ appPort }}
scheme: HTTP scheme: HTTP
livenessProbe:
# Для Vault есть условие failureThreshold: 5
# Предварительно в Namespace должен быть создан SecretStore initialDelaySeconds: 30
# periodSeconds: 15
# Читать: https://confluence.ad.nubes.ru/pages/viewpage.action?pageId=58626585 successThreshold: 1
# (Вариант 2. Используя external-operator) timeoutSeconds: 1
vault: vault:
secretStore: "{{ namespace }}" enable: true
enabled: true secretStore: deck-test
serviceAccountName: "{{ namespace }}-sa" secrets:
target: - VITE_BACKEND_HOST
name: "{{ appName }}-envs" - VITE_AUTH_HOST
data: - VITE_KEYCLOAK_CLIENT_ID
- secretKey: LUCEE_EXTENSIONS - VITE_KEYCLOAK_REALM
remoteRef: - VITE_KEYCLOAK_REDIRECT_URI
key: "deck/kubernetes/{{ stand}}/{{ appName }}" - VITE_KEYCLOAK_URL
property: LUCEE_EXTENSIONS - VITE_KEYCLOAK_CLIENT_SECRET
- secretKey: ORCHESTRATOR_AUTH
remoteRef:
key: "deck/kubernetes/{{ stand}}/{{ appName }}"
property: ORCHESTRATOR_AUTH
- secretKey: cfconfig_datasources_cmdb_database
remoteRef:
key: "deck/kubernetes/{{ stand}}/{{ appName }}"
property: cfconfig_datasources_cmdb_database
- secretKey: cfconfig_datasources_cmdb_dbdriver
remoteRef:
key: "deck/kubernetes/{{ stand}}/{{ appName }}"
property: cfconfig_datasources_cmdb_dbdriver
- secretKey: cfconfig_datasources_cmdb_host
remoteRef:
key: "deck/kubernetes/{{ stand}}/{{ appName }}"
property: cfconfig_datasources_cmdb_host
- secretKey: cfconfig_datasources_cmdb_name
remoteRef:
key: "deck/kubernetes/{{ stand}}/{{ appName }}"
property: cfconfig_datasources_cmdb_name
- secretKey: cfconfig_datasources_cmdb_password
remoteRef:
key: "deck/kubernetes/{{ stand}}/{{ appName }}"
property: cfconfig_datasources_cmdb_password
- secretKey: cfconfig_datasources_cmdb_port
remoteRef:
key: "deck/kubernetes/{{ stand}}/{{ appName }}"
property: cfconfig_datasources_cmdb_port
- secretKey: cfconfig_datasources_cmdb_username
remoteRef:
key: "deck/kubernetes/{{ stand}}/{{ appName }}"
property: cfconfig_datasources_cmdb_username
+47 -13
View File
@@ -1,14 +1,48 @@
main: ##### Переменные #####
registryUrl: 'registry.ngcloud.ru' # Где хранится приложение ######################
registryRepoPath: 'deck' # Где хранится приложение (registry.ngcloud.ru/deck) app:
appName: "svc-api" # наименование контейнера в K8S # Имя приложения / Имя микросервиса
registryContainerName: "deck-{{ appName }}" # Наименование приложения в Nexus name: "svc-api"
vaultPath: "config.deck-{{ appName }}-{{ stand }}" # Путь где лежит инфраструктурный kubeconfig для установки Helm-чарта # Порт на котором приложение будет работать
namespace: "deck" # Namespace приложения, где он будет развёрнут port: 8080
appPort: 8080 # Порт, на котором работает приложение # Health-Check приложения
health: "/api/v1"
# CPU пода (milicores)
cpu: 1000
# Mem пода (megabytes)
mem: 1024
version: # Получение версии приложения
release: 0 # Используется для построения дашбордов / публикации в Registry
major: 0 getCommandVersion: "awk -F'\"' '/APIVersion/ {print \\$2; exit}' v1/Application.cfc"
minor: "{{ BUILD_NUMBER }}"
version: "{{ release }}.{{ major }}.{{ minor }}" # Если включено, то необходимо сделать файл Dockerfile_build_lib
# Либа будет загружена в deck/npm/nubes/{appName}/{version}
needBuildLib: false
# Нужно ли хранить секреты в Vault
needStoreVaultSecret: true
# Наименование key, которые нужно забрать из Vault
storedSecretsList:
- "LUCEE_EXTENSIONS"
- "ORCHESTRATOR_AUTH"
- "cfconfig_datasources_cmdb_database"
- "cfconfig_datasources_cmdb_dbdriver"
- "cfconfig_datasources_cmdb_host"
- "cfconfig_datasources_cmdb_name"
- "cfconfig_datasources_cmdb_port"
- "cfconfig_datasources_cmdb_username"
- "cfconfig_datasources_cmdb_password"
##### Константы #####
#####################
# Путь до секрета в Vault
vaultPath: "config.deck-${ app.name }-${ gitBranch }"
# Где хранится образ (Registry.app)
registryUrl: "registry.ngcloud.ru"
registryRepoPath: "deck"
registryContainerName: "deck-${ app.name }"
registryImageName: "${ registryRepoPath ? registryRepoPath + '/' + registryContainerName : registryContainerName }"
# В какой namespace попадает проект в Kubernetes (по хорошему нужно изменить)
# Ибо счас всё хранится в одном кубере. Кубер умрёт - 3 сренда в OUT
namespace: "deck-${ gitBranch }"