1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-20 05:19:40 +02:00

feat(dockerExecute): Infer Kubernetes securityContext from dockerOptions (#4557)

* Allow running as different user on Kubernetes

Co-authored-by: Ralf Pannemans <ralf.pannemans@sap.com>
Co-authored-by: Johannes Dillmann <j.dillmann@sap.com>
Co-authored-by: Pavel Busko <pavel.busko@sap.com>

* infer securityContext from dockerOptions

Co-authored-by: Ralf Pannemans <ralf.pannemans@sap.com>
Co-authored-by: Pavel Busko <pavel.busko@sap.com>

* verify --user flag value

---------

Co-authored-by: Johannes Dillmann <j.dillmann@sap.com>
Co-authored-by: Ralf Pannemans <ralf.pannemans@sap.com>
Co-authored-by: Anil Keshav <anil.keshav@sap.com>
This commit is contained in:
Pavel Busko 2023-09-18 13:05:01 +02:00 committed by GitHub
parent e38ee67748
commit caee8db407
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 151 additions and 10 deletions

View File

@ -143,6 +143,101 @@ class DockerExecuteTest extends BasePiperTest {
assertTrue(bodyExecuted)
}
@Test
void testExecuteInsidePodWithCustomUserShort() throws Exception {
Map kubernetesConfig = [:]
helper.registerAllowedMethod('dockerExecuteOnKubernetes', [Map.class, Closure.class], { Map config, Closure body ->
kubernetesConfig = config
return body()
})
binding.setVariable('env', [ON_K8S: 'true'])
stepRule.step.dockerExecute(
script: nullScript,
dockerImage: 'maven:3.5-jdk-8-alpine',
dockerOptions: ["-u 0:0", "-v foo:bar"]
) {
bodyExecuted = true
}
assertTrue(loggingRule.log.contains('Executing inside a Kubernetes Pod'))
assertThat(kubernetesConfig.securityContext, is([
'runAsUser': 0,
'runAsGroup': 0
]))
assertTrue(bodyExecuted)
}
@Test
void testExecuteInsidePodWithCustomUserLong() throws Exception {
Map kubernetesConfig = [:]
helper.registerAllowedMethod('dockerExecuteOnKubernetes', [Map.class, Closure.class], { Map config, Closure body ->
kubernetesConfig = config
return body()
})
binding.setVariable('env', [ON_K8S: 'true'])
stepRule.step.dockerExecute(
script: nullScript,
dockerImage: 'maven:3.5-jdk-8-alpine',
dockerOptions: ["--user 0:0", "-v foo:bar"]
) {
bodyExecuted = true
}
assertTrue(loggingRule.log.contains('Executing inside a Kubernetes Pod'))
assertThat(kubernetesConfig.securityContext, is([
'runAsUser': 0,
'runAsGroup': 0
]))
assertTrue(bodyExecuted)
}
@Test
void testExecuteInsidePodWithCustomUserNoGroup() throws Exception {
Map kubernetesConfig = [:]
helper.registerAllowedMethod('dockerExecuteOnKubernetes', [Map.class, Closure.class], { Map config, Closure body ->
kubernetesConfig = config
return body()
})
binding.setVariable('env', [ON_K8S: 'true'])
stepRule.step.dockerExecute(
script: nullScript,
dockerImage: 'maven:3.5-jdk-8-alpine',
dockerOptions: ["-v foo:bar", "-u 0"]
) {
bodyExecuted = true
}
assertTrue(loggingRule.log.contains('Executing inside a Kubernetes Pod'))
assertThat(kubernetesConfig.securityContext, is([
'runAsUser': 0
]))
assertTrue(bodyExecuted)
}
@Test
void testExecuteInsidePodWithCustomUserGroupString() throws Exception {
Map kubernetesConfig = [:]
helper.registerAllowedMethod('dockerExecuteOnKubernetes', [Map.class, Closure.class], { Map config, Closure body ->
kubernetesConfig = config
return body()
})
binding.setVariable('env', [ON_K8S: 'true'])
stepRule.step.dockerExecute(
script: nullScript,
dockerImage: 'maven:3.5-jdk-8-alpine',
dockerOptions: ["-v foo:bar", "-u root:wheel"]
) {
bodyExecuted = true
}
assertTrue(loggingRule.log.contains('Executing inside a Kubernetes Pod'))
assertThat(kubernetesConfig.securityContext, is([
'runAsUser': 'root',
'runAsGroup': 'wheel'
]))
assertTrue(bodyExecuted)
}
@Test
void testExecuteInsideDockerContainer() throws Exception {
stepRule.step.dockerExecute(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine') {

View File

@ -181,6 +181,8 @@ void call(Map parameters = [:], body) {
config.dockerEnvVars?.each { key, value ->
dockerEnvVars << "$key=$value"
}
def securityContext = securityContextFromOptions(config.dockerOptions)
if (env.POD_NAME && isContainerDefined(config)) {
container(getContainerDefined(config)) {
withEnv(dockerEnvVars) {
@ -205,6 +207,7 @@ void call(Map parameters = [:], body) {
dockerWorkspace: config.dockerWorkspace,
stashContent: config.stashContent,
stashNoDefaultExcludes: config.stashNoDefaultExcludes,
securityContext: securityContext,
]
if (config.sidecarImage) {
@ -217,6 +220,7 @@ void call(Map parameters = [:], body) {
sidecarEnvVars: parameters.sidecarEnvVars,
]
}
dockerExecuteOnKubernetes(dockerExecuteOnKubernetesParams) {
echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Pod"
body()
@ -340,20 +344,40 @@ private getDockerOptions(Map dockerEnvVars, Map dockerVolumeBind, def dockerOpti
}
if (dockerOptions) {
if (dockerOptions instanceof CharSequence) {
dockerOptions = [dockerOptions]
}
if (dockerOptions instanceof List) {
dockerOptions.each { String option ->
options << escapeBlanks(option)
}
} else {
throw new IllegalArgumentException("Unexpected type for dockerOptions. Expected was either a list or a string. Actual type was: '${dockerOptions.getClass()}'")
}
options.addAll(dockerOptionsToList(dockerOptions))
}
return options.join(' ')
}
@NonCPS
def securityContextFromOptions(dockerOptions) {
Map securityContext = [:]
if (!dockerOptions) {
return null
}
def userOption = dockerOptionsToList(dockerOptions).find { (it.startsWith("-u ") || it.startsWith("--user ")) }
if (!userOption) {
return null
}
def userOptionParts = userOption.split(" ")
if (userOptionParts.size() != 2) {
throw new IllegalArgumentException("Unexpected --user flag value in dockerOptions '${userOption}'")
}
def userGroupIds = userOptionParts[1].split(":")
securityContext.runAsUser = userGroupIds[0].isInteger() ? userGroupIds[0].toInteger() : userGroupIds[0]
if (userGroupIds.size() == 2) {
securityContext.runAsGroup = userGroupIds[1].isInteger() ? userGroupIds[1].toInteger() : userGroupIds[1]
}
return securityContext
}
boolean isContainerDefined(config) {
Map containerMap = ContainerMap.instance.getMap()
@ -383,6 +407,28 @@ boolean isKubernetes() {
return Boolean.valueOf(env.ON_K8S)
}
@NonCPS
def dockerOptionsToList(dockerOptions) {
def options = []
if (!dockerOptions) {
return options
}
if (dockerOptions instanceof CharSequence) {
dockerOptions = [dockerOptions]
}
if (dockerOptions instanceof List) {
dockerOptions.each { String option ->
options << escapeBlanks(option)
}
} else {
throw new IllegalArgumentException("Unexpected type for dockerOptions. Expected was either a list or a string. Actual type was: '${dockerOptions.getClass()}'")
}
return options
}
/*
* Escapes blanks for values in key/value pairs
* E.g. <code>description=Lorem ipsum</code> is