You've already forked sap-jenkins-library
							
							
				mirror of
				https://github.com/SAP/jenkins-library.git
				synced 2025-10-30 23:57:50 +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
						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