Skip to content

Commit

Permalink
feat(dockerExecute): Infer Kubernetes securityContext from dockerOpti…
Browse files Browse the repository at this point in the history
…ons (#4557)

* Allow running as different user on Kubernetes

Co-authored-by: Ralf Pannemans <[email protected]>
Co-authored-by: Johannes Dillmann <[email protected]>
Co-authored-by: Pavel Busko <[email protected]>

* infer securityContext from dockerOptions

Co-authored-by: Ralf Pannemans <[email protected]>
Co-authored-by: Pavel Busko <[email protected]>

* verify --user flag value

---------

Co-authored-by: Johannes Dillmann <[email protected]>
Co-authored-by: Ralf Pannemans <[email protected]>
Co-authored-by: Anil Keshav <[email protected]>
  • Loading branch information
4 people authored Sep 18, 2023
1 parent e38ee67 commit caee8db
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 10 deletions.
95 changes: 95 additions & 0 deletions test/groovy/DockerExecuteTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
66 changes: 56 additions & 10 deletions vars/dockerExecute.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -205,6 +207,7 @@ void call(Map parameters = [:], body) {
dockerWorkspace: config.dockerWorkspace,
stashContent: config.stashContent,
stashNoDefaultExcludes: config.stashNoDefaultExcludes,
securityContext: securityContext,
]

if (config.sidecarImage) {
Expand All @@ -217,6 +220,7 @@ void call(Map parameters = [:], body) {
sidecarEnvVars: parameters.sidecarEnvVars,
]
}

dockerExecuteOnKubernetes(dockerExecuteOnKubernetesParams) {
echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Pod"
body()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit caee8db

Please sign in to comment.