You've already forked sap-jenkins-library
mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-09-16 09:26:22 +02:00
Support for secretfile type neo credential for deployment (#2537)
* Use Oauth bearer token credentials to deploy to neo * Add test case * Add test file * Fix code climate issues * Add code review changes
This commit is contained in:
committed by
GitHub
parent
ee7279c8fc
commit
efe3ab36f8
@@ -351,6 +351,7 @@ steps:
|
||||
neo:
|
||||
size: 'lite'
|
||||
credentialsId: 'CI_CREDENTIALS_ID'
|
||||
credentialType: 'UsernamePassword'
|
||||
portalLandscape: "cloudnwcportal"
|
||||
multicloudDeploy:
|
||||
cfTargets: []
|
||||
|
@@ -1,18 +1,17 @@
|
||||
import com.sap.piper.StepAssertions
|
||||
import com.sap.piper.Utils
|
||||
|
||||
import groovy.lang.Script
|
||||
import hudson.AbortException
|
||||
|
||||
import util.JenkinsReadFileRule
|
||||
import util.JenkinsReadJsonRule
|
||||
|
||||
import static org.hamcrest.Matchers.allOf
|
||||
import static org.hamcrest.Matchers.contains
|
||||
import static org.hamcrest.Matchers.containsString
|
||||
import static org.hamcrest.Matchers.hasItem
|
||||
import static org.hamcrest.Matchers.not
|
||||
import static org.junit.Assert.assertThat
|
||||
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.BaseMatcher
|
||||
import org.hamcrest.Description
|
||||
import org.jenkinsci.plugins.credentialsbinding.impl.CredentialNotFoundException
|
||||
@@ -43,6 +42,7 @@ class NeoDeployTest extends BasePiperTest {
|
||||
private JenkinsLoggingRule loggingRule = new JenkinsLoggingRule(this)
|
||||
private JenkinsShellCallRule shellRule = new JenkinsShellCallRule(this)
|
||||
private JenkinsStepRule stepRule = new JenkinsStepRule(this)
|
||||
private JenkinsReadFileRule readFileRule = new JenkinsReadFileRule(this, 'test/resources')
|
||||
private JenkinsFileExistsRule fileExistsRule = new JenkinsFileExistsRule(this, ['warArchive.war', 'archive.mtar', 'war.properties'])
|
||||
|
||||
@Rule
|
||||
@@ -56,8 +56,10 @@ class NeoDeployTest extends BasePiperTest {
|
||||
.around(new JenkinsCredentialsRule(this)
|
||||
.withCredentials('myCredentialsId', 'anonymous', '********')
|
||||
.withCredentials('CI_CREDENTIALS_ID', 'defaultUser', '********')
|
||||
.withCredentials('testOauthId', 'clientId', '********'))
|
||||
.withCredentials('testOauthId', 'clientId', '********')
|
||||
.withCredentials("OauthDataFileId","oauth.json"))
|
||||
.around(new JenkinsReadJsonRule(this))
|
||||
.around(readFileRule)
|
||||
.around(stepRule)
|
||||
.around(new JenkinsLockRule(this))
|
||||
.around(new JenkinsWithEnvRule(this))
|
||||
@@ -290,6 +292,33 @@ class NeoDeployTest extends BasePiperTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
void deployWithBearerTokenCredentials_success(){
|
||||
|
||||
nullScript.commonPipelineEnvironment.setMtarFilePath('archive.mtar')
|
||||
|
||||
|
||||
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, "https:\\/\\/api\\.test\\.com\\/oauth2\\/apitoken\\/v1", "{\"access_token\":\"xxx\"}")
|
||||
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, "https:\\/\\/slservice\\.test\\.host\\.com\\/slservice\\/v1\\/oauth\\/accounts\\/testUser123\\/mtars", "{\"id\":123}")
|
||||
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, "https:\\/\\/slservice\\.test\\.host\\.com\\/slservice\\/v1\\/oauth\\/accounts\\/testUser123\\/mtars", "{\"state\":\"DONE\"}")
|
||||
|
||||
|
||||
stepRule.step.neoDeploy(
|
||||
script: nullScript,
|
||||
source: archiveName,
|
||||
deployMode: 'mta',
|
||||
neo: [
|
||||
host: 'test.host.com',
|
||||
account: 'testUser123',
|
||||
credentialsId: 'OauthDataFileId',
|
||||
credentialType: 'SecretFile'
|
||||
],
|
||||
)
|
||||
|
||||
Assert.assertThat(shellRule.shell[0], containsString("#!/bin/bash curl --fail --silent --show-error --retry 12 -XPOST -u \"abc123:testclientsecret123\" \"https://api.test.com/oauth2/apitoken/v1?grant_type=client_credentials\""))
|
||||
Assert.assertThat(shellRule.shell[1], containsString("#!/bin/bash curl --fail --silent --show-error --retry 12 -XPOST -H \"Authorization: Bearer xxx\" -F file=@\"archive.mtar\" \"https://slservice.test.host.com/slservice/v1/oauth/accounts/testUser123/mtars\""))
|
||||
}
|
||||
|
||||
@Test
|
||||
void archivePathFromCPETest() {
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import com.lesfurets.jenkins.unit.BasePipelineTest
|
||||
import groovy.json.JsonSlurper
|
||||
import org.jenkinsci.plugins.credentialsbinding.impl.CredentialNotFoundException
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
@@ -27,9 +28,9 @@ class JenkinsCredentialsRule implements TestRule {
|
||||
return this
|
||||
}
|
||||
|
||||
JenkinsCredentialsRule withCredentials(String credentialsId, String token) {
|
||||
credentials.put(credentialsId, [token: token])
|
||||
return this
|
||||
JenkinsCredentialsRule withCredentials(String credentialsId, String secretTextOrFilePath) {
|
||||
credentials.put(credentialsId, [token: secretTextOrFilePath])
|
||||
return this
|
||||
}
|
||||
|
||||
JenkinsCredentialsRule reset(){
|
||||
@@ -67,6 +68,15 @@ class JenkinsCredentialsRule implements TestRule {
|
||||
"Could not find credentials entry with ID '${m.credentialsId}'")
|
||||
})
|
||||
|
||||
testInstance.helper.registerAllowedMethod('file', [Map.class],
|
||||
{ m ->
|
||||
if (credentials.keySet().contains(m.credentialsId)) { bindingTypes[m.credentialsId] = 'file'; return m }
|
||||
// this is what really happens in case of an unknown credentials id,
|
||||
// checked with reality using credentials plugin 2.1.18.
|
||||
throw new CredentialNotFoundException(
|
||||
"Could not find credentials entry with ID '${m.credentialsId}'")
|
||||
})
|
||||
|
||||
testInstance.helper.registerAllowedMethod('withCredentials', [List, Closure], { config, closure ->
|
||||
// there can be multiple credentials defined for the closure; collecting the necessary binding
|
||||
// preparations and destructions before executing closure
|
||||
@@ -97,7 +107,17 @@ class JenkinsCredentialsRule implements TestRule {
|
||||
destructions.add({
|
||||
binding.setProperty(tokenVariable, null)
|
||||
})
|
||||
} else {
|
||||
}
|
||||
else if (credentialsBindingType == "file") {
|
||||
fileContentVariable = cred.variable
|
||||
preparations.add({
|
||||
binding.setProperty(fileContentVariable, creds?.token)
|
||||
})
|
||||
destructions.add({
|
||||
binding.setProperty(fileContentVariable, null)
|
||||
})
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException("Unknown binding type")
|
||||
}
|
||||
}
|
||||
|
5
test/resources/oauth.json
Normal file
5
test/resources/oauth.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"oauthClientId" : "abc123",
|
||||
"oauthClientSecret" : "testclientsecret123",
|
||||
"oauthServiceUrl" : "https://api.test.com/oauth2"
|
||||
}
|
@@ -1,3 +1,5 @@
|
||||
import com.cloudbees.groovy.cps.NonCPS
|
||||
|
||||
import com.sap.piper.GenerateDocumentation
|
||||
import com.sap.piper.ConfigurationHelper
|
||||
import com.sap.piper.Utils
|
||||
@@ -26,10 +28,15 @@ import static com.sap.piper.Prerequisites.checkScript
|
||||
*/
|
||||
'application',
|
||||
/**
|
||||
* The Jenkins credentials containing user and password used for SAP CP deployment.
|
||||
* The Jenkins credentials containing either user and password (UsernamePassword type credential) or json containing clientId, client secret and oauth service url (SecretFile type credential) used for SAP CP deployment.
|
||||
* @parentConfigKey neo
|
||||
*/
|
||||
'credentialsId',
|
||||
/**
|
||||
* The Jenkins credential of type 'UsernamePassword' or 'SecretFile'.
|
||||
* @parentConfigKey neo
|
||||
*/
|
||||
'credentialType',
|
||||
/**
|
||||
* Map of environment variables in the form of KEY: VALUE.
|
||||
* @parentConfigKey neo
|
||||
@@ -172,6 +179,7 @@ void call(parameters = [:]) {
|
||||
configuration = configHelper
|
||||
.withMandatoryProperty('source')
|
||||
.withMandatoryProperty('neo/credentialsId')
|
||||
.withMandatoryProperty('neo/credentialType')
|
||||
.withMandatoryProperty('neo/application', null, isWarParamsDeployMode)
|
||||
.withMandatoryProperty('neo/runtime', null, isWarParamsDeployMode)
|
||||
.withMandatoryProperty('neo/runtimeVersion', null, isWarParamsDeployMode)
|
||||
@@ -203,13 +211,52 @@ void call(parameters = [:]) {
|
||||
stepParam3: parameters?.script == null,
|
||||
], configuration)
|
||||
|
||||
if(configuration.neo.credentialType == 'UsernamePassword'){
|
||||
withCredentials([usernamePassword(
|
||||
credentialsId: configuration.neo.credentialsId,
|
||||
passwordVariable: 'NEO_PASSWORD',
|
||||
usernameVariable: 'NEO_USERNAME')]) {
|
||||
|
||||
withCredentials([usernamePassword(
|
||||
credentialsId: configuration.neo.credentialsId,
|
||||
passwordVariable: 'NEO_PASSWORD',
|
||||
usernameVariable: 'NEO_USERNAME')]) {
|
||||
assertPasswordRules(NEO_PASSWORD)
|
||||
|
||||
assertPasswordRules(NEO_PASSWORD)
|
||||
dockerExecute(
|
||||
script: script,
|
||||
dockerImage: configuration.dockerImage,
|
||||
dockerEnvVars: configuration.dockerEnvVars,
|
||||
dockerOptions: configuration.dockerOptions
|
||||
) {
|
||||
|
||||
StepAssertions.assertFileExists(this, configuration.source)
|
||||
|
||||
for(CharSequence extensionFile in extensionFileNames) {
|
||||
StepAssertions.assertFileExists(this, extensionFile)
|
||||
}
|
||||
|
||||
NeoCommandHelper neoCommandHelper = new NeoCommandHelper(
|
||||
this,
|
||||
deployMode,
|
||||
configuration.neo,
|
||||
extensionFileNames,
|
||||
NEO_USERNAME,
|
||||
NEO_PASSWORD,
|
||||
configuration.source
|
||||
)
|
||||
|
||||
lock("$STEP_NAME:${neoCommandHelper.resourceLock()}") {
|
||||
deploy(script, configuration, neoCommandHelper, configuration.dockerImage, deployMode)
|
||||
}
|
||||
if(configuration.neo.invalidateCache == true) {
|
||||
if (configuration.deployMode == 'mta') {
|
||||
echo "Triggering invalidation of cache for html5 applications"
|
||||
invalidateCache(configuration)
|
||||
} else {
|
||||
echo "Invalidation of cache is ignored. It is performed only for html5 applications."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(configuration.neo.credentialType == 'SecretFile'){
|
||||
|
||||
dockerExecute(
|
||||
script: script,
|
||||
@@ -217,36 +264,14 @@ void call(parameters = [:]) {
|
||||
dockerEnvVars: configuration.dockerEnvVars,
|
||||
dockerOptions: configuration.dockerOptions
|
||||
) {
|
||||
|
||||
StepAssertions.assertFileExists(this, configuration.source)
|
||||
|
||||
for(CharSequence extensionFile in extensionFileNames) {
|
||||
StepAssertions.assertFileExists(this, extensionFile)
|
||||
}
|
||||
|
||||
NeoCommandHelper neoCommandHelper = new NeoCommandHelper(
|
||||
this,
|
||||
deployMode,
|
||||
configuration.neo,
|
||||
extensionFileNames,
|
||||
NEO_USERNAME,
|
||||
NEO_PASSWORD,
|
||||
configuration.source
|
||||
)
|
||||
|
||||
lock("$STEP_NAME:${neoCommandHelper.resourceLock()}") {
|
||||
deploy(script, configuration, neoCommandHelper, configuration.dockerImage, deployMode)
|
||||
}
|
||||
if(configuration.neo.invalidateCache == true) {
|
||||
if (configuration.deployMode == 'mta') {
|
||||
echo "Triggering invalidation of cache for html5 applications"
|
||||
invalidateCache(configuration)
|
||||
} else {
|
||||
echo "Invalidation of cache is ignored. It is performed only for html5 applications."
|
||||
}
|
||||
withCredentials([file(credentialsId: configuration.neo.credentialsId, variable: 'oauth_deploy_cred')]) {
|
||||
deployWithBearerToken(oauth_deploy_cred, configuration, script)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
error "Unsupported type of neo deploy credential."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,6 +398,76 @@ private deploy(script, Map configuration, NeoCommandHelper neoCommandHelper, doc
|
||||
}
|
||||
}
|
||||
|
||||
private deployWithBearerToken(def credentialFilePath, Map configuration, Script script){
|
||||
|
||||
def deployArchive = script.commonPipelineEnvironment.getMtarFilePath()
|
||||
def host = configuration.neo.host
|
||||
def account = configuration.neo.account
|
||||
|
||||
def credentialFileContent = readFile(credentialFilePath)
|
||||
def credentialsMap = parseJson(credentialFileContent)
|
||||
|
||||
def oauthClientId = credentialsMap.oauthClientId
|
||||
def oauthClientSecret = credentialsMap.oauthClientSecret
|
||||
def oauthUrl = credentialsMap.oauthServiceUrl
|
||||
|
||||
echo "[${STEP_NAME}] Retrieving oauth token..."
|
||||
|
||||
def myCurl = "curl --fail --silent --show-error --retry 12"
|
||||
def token_json = sh(
|
||||
script: """#!/bin/bash
|
||||
${myCurl} -XPOST -u \"${oauthClientId}:${oauthClientSecret}\" \"${oauthUrl}/apitoken/v1?grant_type=client_credentials"
|
||||
""",
|
||||
returnStdout: true
|
||||
)
|
||||
def responseJson = readJSON text: token_json
|
||||
def token = responseJson.access_token
|
||||
|
||||
echo "[${STEP_NAME}] Deploying '${deployArchive}' to '${account}'..."
|
||||
|
||||
def deploymentContentResponse = sh(
|
||||
script: """#!/bin/bash
|
||||
${myCurl} -XPOST -H \"Authorization: Bearer ${token}\" -F file=@\"${deployArchive}\" \"https://slservice.${host}/slservice/v1/oauth/accounts/${account}/mtars\"
|
||||
""",
|
||||
returnStdout: true
|
||||
)
|
||||
def deploymentJson = readJSON text: deploymentContentResponse
|
||||
def deploymentId = deploymentJson.id
|
||||
|
||||
echo "[${STEP_NAME}] Deployment Id is '${deploymentId}'."
|
||||
|
||||
def statusPollScript = """#!/bin/bash
|
||||
${myCurl} -XGET -H \"Authorization: Bearer ${token}\" \"https://slservice.${host}/slservice/v1/oauth/accounts/${account}/mtars/${deploymentId}\"
|
||||
"""
|
||||
def statusResponse = sh(script: statusPollScript, returnStdout: true)
|
||||
def statusJson = readJSON text: statusResponse
|
||||
def state = statusJson.state
|
||||
|
||||
while (state == 'RUNNING') {
|
||||
sleep(10)
|
||||
statusResponse = sh(script: statusPollScript, returnStdout: true)
|
||||
statusJson = readJSON text: statusResponse
|
||||
state = statusJson.state
|
||||
echo "${STEP_NAME}] Deployment is still running..."
|
||||
}
|
||||
|
||||
if (state == 'DONE') {
|
||||
echo "[${STEP_NAME}] Deployment has succeeded."
|
||||
} else if (state == 'FAILED') {
|
||||
if(statusJson.progress[0]?.modules[0]?.error?.internalMessage) {
|
||||
def message = statusJson.progress[0].modules[0].error.internalMessage
|
||||
echo "[${STEP_NAME}] Deployment has failed with the message: ${message}"
|
||||
error "[${STEP_NAME}] Deployment failure message: ${message}"
|
||||
} else {
|
||||
echo "[${STEP_NAME}] Deployment has failed with response: ${statusResponse}"
|
||||
error "[${STEP_NAME}] Deployment failure reason: ${statusResponse}"
|
||||
}
|
||||
} else {
|
||||
echo "[${STEP_NAME}] Unknown status '${state}'"
|
||||
error "[${STEP_NAME}] Deployment failed with unknown status: ${state}"
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAppRunning(NeoCommandHelper commandHelper) {
|
||||
def status = sh script: "${commandHelper.statusCommand()} || true", returnStdout: true
|
||||
return status.contains('Status: STARTED')
|
||||
@@ -405,3 +500,14 @@ private getDefaultSource(Script script, Map configuration, DeployMode deployMode
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
//Convert LazyMap instance produced after jsonSluper to a groovy based LinkedHashMap to overcome serialization issue
|
||||
@NonCPS
|
||||
def parseJson(credentialFileContent) {
|
||||
def lazyMap = new groovy.json.JsonSlurper().parseText(credentialFileContent)
|
||||
def map = [:]
|
||||
for (prop in lazyMap) {
|
||||
map[prop.key] = prop.value
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
Reference in New Issue
Block a user