1
0
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:
Srinikitha Kondreddy
2021-01-26 09:29:44 +01:00
committed by GitHub
parent ee7279c8fc
commit efe3ab36f8
5 changed files with 202 additions and 41 deletions

View File

@@ -351,6 +351,7 @@ steps:
neo:
size: 'lite'
credentialsId: 'CI_CREDENTIALS_ID'
credentialType: 'UsernamePassword'
portalLandscape: "cloudnwcportal"
multicloudDeploy:
cfTargets: []

View File

@@ -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() {

View File

@@ -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")
}
}

View File

@@ -0,0 +1,5 @@
{
"oauthClientId" : "abc123",
"oauthClientSecret" : "testclientsecret123",
"oauthServiceUrl" : "https://api.test.com/oauth2"
}

View File

@@ -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
}