#!groovy

import groovy.transform.Field

@Field
def pulumiPod = '''
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: main
    image: pulumi/pulumi:3.36.0
    command:
    - cat
    tty: true
    resources:
      requests:
        memory: "2000Mi"
        cpu: 1000m
      limits:
        memory: "8000Mi"
        cpu: 2000m
    env:
    - name: DOCKER_HOST
      value: tcp://localhost:2375
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /identity/token
    volumeMounts:
    - mountPath: /identity
      name: identity
  - name: dind
    image: docker:18.09-dind
    securityContext:
      privileged: true
  - name: auth
    image: gcr.io/pingcap-gardener/jenkins/gcp-web-identity:20201201
    env:
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /identity/token
    volumeMounts:
    - mountPath: /identity
      name: identity
  volumes:
  - emptyDir: {}
    name: identity
  restartPolicy: Never
'''

@Field
def sowPod = '''
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: main
    image: gcr.io/pingcap-gardener/jenkins/sow-pulumi:3.0.0-v20230307
    command:
    - cat
    tty: true
    env:
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /identity/token
    volumeMounts:
    - mountPath: /identity
      name: identity
  - name: auth
    image: gcr.io/pingcap-gardener/jenkins/gcp-web-identity:20201201
    env:
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /identity/token
    volumeMounts:
    - mountPath: /identity
      name: identity
  volumes:
  - emptyDir: {}
    name: identity
  restartPolicy: Never
'''

pipeline {
    agent any

    options {
        disableConcurrentBuilds()
    }
    environment {
        GOPATH = "/home/jenkins/agent/workspace/go"
        AWS_REGION = "us-west-2"
    }
    stages {
        stage('Utilities') {
            environment {
                PULUMI_CONFIG_PASSPHRASE = credentials("dbaas-dev-pulumi-pwd")
                PULUMI_BACKUP_BUCKET = "dbaas-dev-pulumi-backup"
                GIT_COMMIT_MSG = sh(returnStdout: true, script: "git log --oneline -n 1 --pretty=%B HEAD | head -n 1").trim()
            }
            agent {
                kubernetes {
                    yaml pulumiPod
                    defaultContainer "main"
                    customWorkspace "/home/jenkins/agent/workspace/go/src/github.com/tidbcloud/infra"
                }
            }
            stages {
                stage('Preview') {
                    when {
                        beforeAgent true
                        allOf {
                            changeRequest()
                            expression {
                                sh(returnStatus: true, script: 'git diff origin/${CHANGE_TARGET}...HEAD --name-only | grep -v ".*\\.md" | grep "^deploy/cicd/.*"') == 0
                            }
                            environment name: 'CHANGE_TARGET', value: 'master'
                        }
                    }
                    steps {
                        // We can only specify one target role to assume via environment variables, in order to
                        // assume multiple roles in a pulumi stack, use a credential file instead
                        sh '''
                        mkdir -p ~/.aws
                        cp deploy/cicd/credentials ~/.aws/credentials
                        '''
                        runPulumi("preview --diff", "deploy/cicd", "jenkins-us-west1", false)
                    }
                }
                stage('Deploy') {
                    when {
                        beforeAgent true
                        allOf {
                            not { changeRequest() }
                            changeset pattern: "deploy\\/cicd\\/.*", comparator: "REGEXP"
                            environment name: 'GIT_BRANCH', value: 'master'
                        }
                    }
                    steps {
                        sh '''
                        mkdir -p ~/.aws
                        cp deploy/cicd/credentials ~/.aws/credentials
                        '''
                        runPulumi("up --yes", "deploy/cicd", "jenkins-us-west1", false)
                    }
                    post {
                        unsuccessful {
                            script {
                                sendNotification("#dbaas-infra-cd", 'FAILURE')
                            }
                        }
                    }
                }
            }
        }
        stage('Delivery') {
            matrix {
                axes {
                    axis {
                        name 'STAGE'
                        values 'dev', 'staging', 'prod'
                    }
                }
                environment {
                    ENV = "${STAGE}"
                    KUBECONFIG = credentials("dbaas-${STAGE}-kubeconfig")
                    AWS_ROLE_ARN = credentials("dbaas-${STAGE}-aws-role")
                    DASHBOARD_PASSWORD = credentials("dbaas-${STAGE}-dashboard-secret")
                    PULUMI_CONFIG_PASSPHRASE = credentials("dbaas-${STAGE}-pulumi-pwd")
                    PULUMI_BACKUP_BUCKET = "dbaas-${STAGE}-pulumi-backup"
                    GIT_COMMIT_MSG = sh(returnStdout: true, script: "git log --oneline -n 1 --pretty=%B HEAD | head -n 1").trim()
                }
                when {
                    beforeAgent true
                    // BRANCH to ENV mapping
                    anyOf {
                        allOf {
                            anyOf {
                                environment name: 'GIT_BRANCH', value: 'master' // delivery
                                environment name: 'CHANGE_TARGET', value: 'master' // preview
                            }
                            expression { env.STAGE == 'dev' }
                        }
                        allOf {
                            anyOf {
                                environment name: 'GIT_BRANCH', value: 'staging' // delivery
                                environment name: 'CHANGE_TARGET', value: 'staging' // preview
                            }
                            expression { env.STAGE == 'staging' }
                        }
                        allOf {
                            anyOf {
                                environment name: 'GIT_BRANCH', value: 'prod' // delivery
                                environment name: 'CHANGE_TARGET', value: 'prod' // preview
                            }
                            expression { env.STAGE == 'prod' }
                        }
                    }
                }
                agent {
                    kubernetes {
                        yaml pulumiPod
                        defaultContainer "main"
                        customWorkspace "/home/jenkins/agent/workspace/go/src/github.com/tidbcloud/infra"
                    }
                }
                stages {
                    stage('Preview') {
                        when {
                            beforeAgent true
                            allOf {
                                changeRequest()
                                expression {
                                    sh(returnStatus: true, script: 'git diff origin/${CHANGE_TARGET}...HEAD --name-only | grep -v ".*\\.md" | grep "^deploy/.*"') == 0
                                }
                            }
                        }
                        stages {
                            stage('Pulumi Preview') {
                                when {
                                    beforeAgent true
                                    changeRequest()
                                }
                                steps {
                                    runPulumiForEnv("preview --diff", "${env.CHANGE_TARGET}")
                                }
                            }
                            stage('Gardener Preview') {
                                when {
                                    beforeAgent true
                                    anyOf {
                                        expression {
                                            sh(returnStatus: true, script: 'git diff origin/${CHANGE_TARGET}...HEAD --name-only | grep -v ".*\\.md" | grep "^deploy/garden/.*"') == 0
                                        }
                                    }
                                }
                                environment {
                                    AWS_ROLE_ARN = credentials("dbaas-${STAGE}-aws-role")
                                    GOOGLE_CREDENTIALS = credentials("dbaas-${STAGE}-gcp-text")
                                    GOOGLE_APPLICATION_CREDENTIALS = credentials("dbaas-${STAGE}-gcp-file")
                                }
                                agent {
                                    kubernetes {
                                        yaml sowPod
                                        defaultContainer "main"
                                    }
                                }
                                steps {
                                    runGarden("order -A")
                                }
                            }
                        }
                    }
                    stage('Destroy Dev Infra') {
                        when {
                            beforeAgent true
                            allOf {
                                not { changeRequest() }
                                anyOf {
                                    changeset pattern: "deploy\\/monitoring\\/.*", comparator: "REGEXP"
                                }
                                expression { env.STAGE == 'dev' }
                            }
                        }
                        steps {
                            runPulumiDestroy()
                        }
                        post {
                            unsuccessful {
                                script {
                                    sendNotification("#dbaas-infra-cd", 'FAILURE')
                                }
                            }
                        }
                    }
                    stage('Deploy Infra') {
                        when {
                            beforeAgent true
                            allOf {
                                not { changeRequest() }
                                anyOf {
                                    changeset pattern: "deploy\\/.*", comparator: "REGEXP"
                                }
                            }
                        }
                        stages {
                            stage('Build Observability Controller Image') {
                                when {
                                    beforeAgent true
                                    anyOf {
                                        anyOf {
                                            changeset pattern: "observability\\/.*", comparator: "REGEXP"
                                        }
                                    }
                                }
                                steps {
                                    buildObservabilityImage("${env.CHANGE_TARGET}")
                                }
                            }
                            stage('Pulumi Up') {
                                steps {
                                    runPulumiForEnv("up --yes")
                                }
                                post {
                                    unsuccessful {
                                        script {
                                            sendNotification("#dbaas-infra-cd", 'FAILURE')
                                        }
                                    }
                                }
                            }
                            stage('Deploy Gardener') {
                                agent {
                                    kubernetes {
                                        yaml sowPod
                                        defaultContainer "main"
                                    }
                                }
                                when {
                                    beforeAgent true
                                    anyOf {
                                        anyOf {
                                            changeset pattern: "deploy\\/garden\\/.*", comparator: "REGEXP"
                                        }
                                    }
                                }
                                environment {
                                    AWS_ROLE_ARN = credentials("dbaas-${STAGE}-aws-role")
                                    GOOGLE_CREDENTIALS = credentials("dbaas-${STAGE}-gcp-text")
                                    GOOGLE_APPLICATION_CREDENTIALS = credentials("dbaas-${STAGE}-gcp-file")
                                    GITHUB_BOT = credentials("github-sre-bot")
                                }
                                steps {
                                    sh 'echo "machine github.com login ${GITHUB_BOT_USR} password ${GITHUB_BOT_PSW}" > ~/.netrc'
                                    runGarden("deploy -A")
                                }
                                post {
                                    unsuccessful {
                                        script {
                                            sendNotification("#dbaas-infra-cd", 'FAILURE')
                                        }
                                    }
                                    success {
                                        script {
                                            sendNotification("#dbaas-infra-cd", 'SUCCESS')
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

def buildObservabilityImage(TARGET_BRANCH=""){
    if (TARGET_BRANCH == "master") {
        sh "./observability/build/build.sh"
    }
}

def runPulumi(SUB_COMMAND, DIR, STACK, BACKUP_STATE=true) {
    // unset KUBECONFIG to avoid creating resources without provider specified
    // unset HODSON_URL so that pulumi can detect the CI environment correctly
    sh """
    unset KUBECONFIG
    unset HUDSON_URL
    cd ${DIR}
    npm config set strict-ssl false
    npm cache clear --force
    npm install
    npm run check
    npm run lint
    pulumi login gs://tidbcloud-pulumi-state
    pulumi stack select --create --secrets-provider=passphrase ${STACK}
    pulumi ${SUB_COMMAND}
    """
    if (SUB_COMMAND.startsWith("up")) {
        // show-secrets get the stack with secret encrypt by the PULUMI_PASSPHRASE env
        if (BACKUP_STATE){
            sh """
            cd ${DIR}
            pulumi stack export --show-secrets --file ${STACK}.json
            ls -l ${STACK}.json
            aws sts get-caller-identity
            aws s3 ls s3://\${PULUMI_BACKUP_BUCKET}/
            aws s3 cp ${STACK}.json s3://\${PULUMI_BACKUP_BUCKET}
            """
        }
        def msg = "[<${env.RUN_DISPLAY_URL}| Pulumi up ${STACK}>] `Success`\nCommit: <https://github.com/tidbcloud/dbaas/commit/${env.GIT_COMMIT}|${env.GIT_COMMIT_MSG}>"
        slackSend channel: '#dbaas-infra-cd', color: 'good', teamDomain: 'pingcap', tokenCredentialId: 'slack-pingcap-token', message: "${msg}"
    }
    if (SUB_COMMAND.startsWith("destroy")) {
        sh """
        cd ${DIR}
        pulumi stack rm -y ${STACK}
        """
        def msg = "[<${env.RUN_DISPLAY_URL}| Pulumi destroy ${STACK}>] `Success`\nCommit: <https://github.com/tidbcloud/dbaas/commit/${env.GIT_COMMIT}|${env.GIT_COMMIT_MSG}>"
        slackSend channel: '#dbaas-infra-cd', color: 'good', teamDomain: 'pingcap', tokenCredentialId: 'slack-pingcap-token', message: "${msg}"
    }
}

def runPulumiDestroy() {
    def changeSet = getChangedFiles()
    echo "Change set: ${changeSet}"
    def projects = []
    for (int j = 0; j < projects.size(); j++) {
        def project = projects[j]
        def stacks = sh(script: "find deploy/${project} -name 'Pulumi.${env.ENV}*.yaml'", returnStdout: true).split('\n')
        for (int i = 0; i < changeSet.size(); i++) {
            def matcher = changeSet[i]=~/deploy\/${project}\/Pulumi\.(${env.ENV}-.*)\.yaml/
            if (matcher.matches() && !stacks.contains(changeSet[i])) {
                def stack = matcher[0][1]
                runPulumi("destroy -y", "deploy/${project}", stack)
            }
        }
    }
}

// Use git diff for PR check to workaround https://issues.jenkins-ci.org/browse/JENKINS-44849
def runPulumiForEnv(SUB_COMMAND, TARGET_BRANCH="") {
    def changeSet
    if (TARGET_BRANCH != "") {
        changeSet = sh(script: "git diff origin/${TARGET_BRANCH}...HEAD --name-only", returnStdout: true).split('\n')
    } else {
        changeSet = getChangedFiles()
    }
    echo "Change set: ${changeSet}"
    def projects = ["bastion", "iam", "gke", "cert", "monitoring", "robots", "metadb"]
    for (int j = 0; j < projects.size(); j++) {
        def project = projects[j]
        def stacks = sh(script: "find deploy/${project} -name 'Pulumi.${env.ENV}*.yaml'", returnStdout: true).split('\n')
        // BASE should be deployed first in eks
        if (project == "eks") {
            // HACK: we want to deploy BASE first, since BASE and SEED has same prefix, use alphabetical order directly
            stacks = stacks.sort()
        }
        def projectChanged = false
        def baseChanged = false
        for (int i = 0; i < changeSet.size(); i++) {
            def changedFile = changeSet[i]
            // If BASE stack is changed, seeds should also be updated since the output of BASE may change
            // FIXME: better dependency resolution
            if (project == "eks" && changedFile==~/deploy\/${project}\/Pulumi\.${env.ENV}-base(.*)\.yaml/) {
                baseChanged = true
            }
            if (!(changedFile==~/deploy\/${project}\/Pulumi\.(.*)\.yaml/) && changedFile==~/deploy\/${project}\/.*/) {
                projectChanged = true
                break
            }
        }
        for (int i = 0; i < stacks.size(); i++) {
            def stackFile = stacks[i]
            if (projectChanged || baseChanged ||
                    changeSet.contains(stackFile)) {
                def matcher = stackFile=~/deploy\/${project}\/Pulumi\.(.*)\.yaml/
                if (!matcher.matches()) {
                    continue
                }
                def stack = matcher[0][1]
                runPulumi(SUB_COMMAND, "deploy/${project}", stack)
            }
        }
    }
}

def runGarden(SUB_COMMAND) {
    sh "./deploy/garden/garden-setup.sh ${SUB_COMMAND}"
}

def sendNotification(CHANNEL="#dbaas-infra-cd", STATUS="SUCCESS") {
    def GIT_AUTHOR_NAME = sh(returnStdout: true, script: "git log -1 --pretty=format:'%an'").trim()
    def slackmsg = "[<${env.RUN_DISPLAY_URL}|${env.STAGE_NAME} to ${env.ENV}>] `${STATUS}`" + "\n" +
            "Commit: <https://github.com/tidbcloud/infra/commit/${env.GIT_COMMIT}|${env.GIT_COMMIT_MSG}>" + "\n" +
            "Branch: `${env.GIT_BRANCH}`, Authored by: `${GIT_AUTHOR_NAME}`"
    def COLOR = 'good'
    if (STATUS != "SUCCESS") {
        COLOR = 'danger'
    }
    slackSend channel: CHANNEL, color: COLOR, teamDomain: 'pingcap', tokenCredentialId: 'slack-pingcap-token', message: "${slackmsg}"
}

@NonCPS
String[] getChangedFiles() {
    def changedFiles = []
    for (changeLogSet in currentBuild.changeSets) {
        for (entry in changeLogSet.getItems()) {
            for (file in entry.getAffectedFiles()) {
                changedFiles.add(file.getPath())
            }
        }
    }
    return changedFiles
}
