Skip to content

Commit

Permalink
[k8s] Add pod security context to create options (#2008)
Browse files Browse the repository at this point in the history
* Add pod security context to create options
  • Loading branch information
dmolokanov authored Dec 2, 2019
1 parent f5482f5 commit cf2eba9
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public CombinedKubernetesConfig GetCombinedConfig(IModule module, IRuntimeInfo r
experimentalOptions.ForEach(parameters => createOptions.Volumes = parameters.Volumes);
experimentalOptions.ForEach(parameters => createOptions.NodeSelector = parameters.NodeSelector);
experimentalOptions.ForEach(parameters => createOptions.Resources = parameters.Resources);
experimentalOptions.ForEach(parameters => createOptions.SecurityContext = parameters.SecurityContext);
}

Option<ImagePullSecret> imagePullSecret = dockerConfig.AuthConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public CreatePodParameters(
HostConfig hostConfig,
string image,
IDictionary<string, string> labels)
: this(env?.ToList(), exposedPorts, hostConfig, image, labels, null, null, null)
: this(env?.ToList(), exposedPorts, hostConfig, image, labels, null, null, null, null)
{
}

Expand All @@ -31,7 +31,8 @@ public CreatePodParameters(
IDictionary<string, string> labels,
IDictionary<string, string> nodeSelector,
V1ResourceRequirements resources,
IReadOnlyList<KubernetesModuleVolumeSpec> volumes)
IReadOnlyList<KubernetesModuleVolumeSpec> volumes,
V1PodSecurityContext securityContext)
{
this.Env = Option.Maybe(env);
this.ExposedPorts = Option.Maybe(exposedPorts);
Expand All @@ -41,6 +42,7 @@ public CreatePodParameters(
this.NodeSelector = Option.Maybe(nodeSelector);
this.Resources = Option.Maybe(resources);
this.Volumes = Option.Maybe(volumes);
this.SecurityContext = Option.Maybe(securityContext);
}

internal static CreatePodParameters Create(
Expand All @@ -51,8 +53,9 @@ internal static CreatePodParameters Create(
IDictionary<string, string> labels = null,
IDictionary<string, string> nodeSelector = null,
V1ResourceRequirements resources = null,
IReadOnlyList<KubernetesModuleVolumeSpec> volumes = null)
=> new CreatePodParameters(env, exposedPorts, hostConfig, image, labels, nodeSelector, resources, volumes);
IReadOnlyList<KubernetesModuleVolumeSpec> volumes = null,
V1PodSecurityContext securityContext = null)
=> new CreatePodParameters(env, exposedPorts, hostConfig, image, labels, nodeSelector, resources, volumes, securityContext);

[JsonProperty("env", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
[JsonConverter(typeof(OptionConverter<IReadOnlyList<string>>))]
Expand Down Expand Up @@ -85,5 +88,9 @@ internal static CreatePodParameters Create(
[JsonProperty("volumes", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
[JsonConverter(typeof(OptionConverter<IReadOnlyList<KubernetesModuleVolumeSpec>>))]
public Option<IReadOnlyList<KubernetesModuleVolumeSpec>> Volumes { get; set; }

[JsonProperty("securityContext", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
[JsonConverter(typeof(OptionConverter<V1PodSecurityContext>))]
public Option<V1PodSecurityContext> SecurityContext { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ public class KubernetesExperimentalCreatePodParameters

public Option<IReadOnlyList<KubernetesModuleVolumeSpec>> Volumes { get; }

public Option<V1PodSecurityContext> SecurityContext { get; }

KubernetesExperimentalCreatePodParameters(
Option<IDictionary<string, string>> nodeSelector,
Option<V1ResourceRequirements> resources,
Option<IReadOnlyList<KubernetesModuleVolumeSpec>> volumes)
Option<IReadOnlyList<KubernetesModuleVolumeSpec>> volumes,
Option<V1PodSecurityContext> securityContext)
{
this.NodeSelector = nodeSelector;
this.Resources = resources;
this.Volumes = volumes;
this.SecurityContext = securityContext;
}

static class ExperimentalParameterNames
Expand All @@ -33,6 +37,7 @@ static class ExperimentalParameterNames
public const string NodeSelector = "NodeSelector";
public const string Resources = "Resources";
public const string Volumes = "Volumes";
public const string SecurityContext = "SecurityContext";
}

public static Option<KubernetesExperimentalCreatePodParameters> Parse(IDictionary<string, JToken> other)
Expand Down Expand Up @@ -61,7 +66,10 @@ static KubernetesExperimentalCreatePodParameters ParseParameters(JObject experim
var volumes = options.Get(ExperimentalParameterNames.Volumes)
.FlatMap(option => Option.Maybe(option.ToObject<IReadOnlyList<KubernetesModuleVolumeSpec>>()));

return new KubernetesExperimentalCreatePodParameters(nodeSelector, resources, volumes);
var securityContext = options.Get(ExperimentalParameterNames.SecurityContext)
.FlatMap(option => Option.Maybe(option.ToObject<V1PodSecurityContext>()));

return new KubernetesExperimentalCreatePodParameters(nodeSelector, resources, volumes, securityContext);
}

static Dictionary<string, JToken> PrepareSupportedOptionsStore(JObject experimental)
Expand All @@ -85,7 +93,8 @@ static Dictionary<string, JToken> PrepareSupportedOptionsStore(JObject experimen
{
ExperimentalParameterNames.NodeSelector,
ExperimentalParameterNames.Resources,
ExperimentalParameterNames.Volumes
ExperimentalParameterNames.Volumes,
ExperimentalParameterNames.SecurityContext
};

static class Events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,10 @@ V1PodTemplateSpec GetPod(string name, IModuleIdentity identity, KubernetesModule
.Select(pullSecretName => new V1LocalObjectReference(pullSecretName))
.ToList();

V1PodSecurityContext securityContext = this.runAsNonRoot
? new V1PodSecurityContext { RunAsNonRoot = true, RunAsUser = 1000 }
: null;
V1PodSecurityContext securityContext = module.Config.CreateOptions.SecurityContext.GetOrElse(
() => this.runAsNonRoot
? new V1PodSecurityContext { RunAsNonRoot = true, RunAsUser = 1000 }
: null);

return new V1PodTemplateSpec
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,40 @@ public void RunAsNonRootAndRunAsUser1000SecurityPolicyWhenSettingSet()
Assert.Equal(1000, deployment.Spec.Template.Spec.SecurityContext.RunAsUser);
}

[Fact]
public void PodSecurityContextFromCreateOptionsOverridesDefaultRunAsNonRootOptionsWhenProvided()
{
var identity = new ModuleIdentity("hostname", "gatewayhost", "deviceid", "Module1", Mock.Of<ICredentials>());
var securityContext = new V1PodSecurityContext { RunAsNonRoot = true, RunAsUser = 0 };
var config = new KubernetesConfig("image", CreatePodParameters.Create(securityContext: securityContext), Option.Some(new AuthConfig("user-registry1")));
var module = new KubernetesModule("module1", "v1", "docker", ModuleStatus.Running, RestartPolicy.Always, DefaultConfigurationInfo, EnvVarsDict, config, ImagePullPolicy.OnCreate, EdgeletModuleOwner);
var labels = new Dictionary<string, string>();
var mapper = CreateMapper(runAsNonRoot: true);

var deployment = mapper.CreateDeployment(identity, module, labels);

Assert.Equal(1, deployment.Spec.Template.Spec.ImagePullSecrets.Count);
Assert.Equal(true, deployment.Spec.Template.Spec.SecurityContext.RunAsNonRoot);
Assert.Equal(0, deployment.Spec.Template.Spec.SecurityContext.RunAsUser);
}

[Fact]
public void ApplyPodSecurityContextFromCreateOptionsWhenProvided()
{
var identity = new ModuleIdentity("hostname", "gatewayhost", "deviceid", "Module1", Mock.Of<ICredentials>());
var securityContext = new V1PodSecurityContext { RunAsNonRoot = true, RunAsUser = 20001 };
var config = new KubernetesConfig("image", CreatePodParameters.Create(securityContext: securityContext), Option.Some(new AuthConfig("user-registry1")));
var module = new KubernetesModule("module1", "v1", "docker", ModuleStatus.Running, RestartPolicy.Always, DefaultConfigurationInfo, EnvVarsDict, config, ImagePullPolicy.OnCreate, EdgeletModuleOwner);
var labels = new Dictionary<string, string>();
var mapper = CreateMapper();

var deployment = mapper.CreateDeployment(identity, module, labels);

Assert.Equal(1, deployment.Spec.Template.Spec.ImagePullSecrets.Count);
Assert.Equal(true, deployment.Spec.Template.Spec.SecurityContext.RunAsNonRoot);
Assert.Equal(20001, deployment.Spec.Template.Spec.SecurityContext.RunAsUser);
}

[Fact]
public void EdgeAgentEnvSettingsHaveLotsOfStuff()
{
Expand Down Expand Up @@ -564,7 +598,12 @@ public void EdgeAgentEnvSettingsHaveLotsOfStuff()
Assert.Equal("False", container.Env.Single(e => e.Name == "feature2").Value);
}

static KubernetesDeploymentMapper CreateMapper(string persistentVolumeName = "", string storageClassName = "", string proxyImagePullSecretName = null, bool runAsNonRoot = false, IDictionary<string, bool> experimentalFeatures = null)
static KubernetesDeploymentMapper CreateMapper(
string persistentVolumeName = "",
string storageClassName = "",
string proxyImagePullSecretName = null,
bool runAsNonRoot = false,
IDictionary<string, bool> experimentalFeatures = null)
=> new KubernetesDeploymentMapper(
"namespace",
"edgehub",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public void IgnoresUnsupportedOptions()
Assert.False(parameters.Volumes.HasValue);
Assert.False(parameters.NodeSelector.HasValue);
Assert.False(parameters.Resources.HasValue);
Assert.False(parameters.SecurityContext.HasValue);
}

[Fact]
Expand Down Expand Up @@ -256,5 +257,58 @@ public void ParsesVolumesExperimentalOptions()
volumeSpec.VolumeMounts.ForEach(mounts => Assert.Equal(true, mounts[0].ReadOnlyProperty));
volumeSpec.VolumeMounts.ForEach(mounts => Assert.Equal(string.Empty, mounts[0].SubPath));
}

[Fact]
public void ParsesNoneSecurityContextExperimentalOptions()
{
var experimental = new Dictionary<string, JToken>
{
["k8s-experimental"] = JToken.Parse("{ securityContext: null }")
};

var parameters = KubernetesExperimentalCreatePodParameters.Parse(experimental).OrDefault();

Assert.False(parameters.SecurityContext.HasValue);
}

[Fact]
public void ParsesEmptySecurityContextExperimentalOptions()
{
var experimental = new Dictionary<string, JToken>
{
["k8s-experimental"] = JToken.Parse("{ securityContext: { } }")
};

var parameters = KubernetesExperimentalCreatePodParameters.Parse(experimental).OrDefault();

Assert.True(parameters.SecurityContext.HasValue);
parameters.SecurityContext.ForEach(securityContext => Assert.Null(securityContext.RunAsGroup));
parameters.SecurityContext.ForEach(securityContext => Assert.Null(securityContext.RunAsUser));
parameters.SecurityContext.ForEach(securityContext => Assert.Null(securityContext.RunAsNonRoot));
parameters.SecurityContext.ForEach(securityContext => Assert.Null(securityContext.Sysctls));
parameters.SecurityContext.ForEach(securityContext => Assert.Null(securityContext.FsGroup));
parameters.SecurityContext.ForEach(securityContext => Assert.Null(securityContext.SeLinuxOptions));
parameters.SecurityContext.ForEach(securityContext => Assert.Null(securityContext.SupplementalGroups));
}

[Fact]
public void ParsesSomeSecurityContextExperimentalOptions()
{
var experimental = new Dictionary<string, JToken>
{
["k8s-experimental"] = JToken.Parse("{ securityContext: { runAsGroup: 1001, runAsUser: 1000, runAsNonRoot: true } }")
};

var parameters = KubernetesExperimentalCreatePodParameters.Parse(experimental).OrDefault();

Assert.True(parameters.SecurityContext.HasValue);
parameters.SecurityContext.ForEach(securityContext => Assert.Equal(1001, securityContext.RunAsGroup));
parameters.SecurityContext.ForEach(securityContext => Assert.Equal(1000, securityContext.RunAsUser));
parameters.SecurityContext.ForEach(securityContext => Assert.Equal(true, securityContext.RunAsNonRoot));
parameters.SecurityContext.ForEach(securityContext => Assert.Null(securityContext.Sysctls));
parameters.SecurityContext.ForEach(securityContext => Assert.Null(securityContext.FsGroup));
parameters.SecurityContext.ForEach(securityContext => Assert.Null(securityContext.SeLinuxOptions));
parameters.SecurityContext.ForEach(securityContext => Assert.Null(securityContext.SupplementalGroups));
}
}
}
21 changes: 21 additions & 0 deletions kubernetes/doc/create-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ We added CreateOptions for experimental features on Kubernetes. These options "o
"volumes": [{...}],
"resources": [{...}],
"nodeSelector": {...}
"securityContext": {...},
}
}
```
Expand Down Expand Up @@ -118,5 +119,25 @@ A `nodeSelector` section of config used for node selection constrain. It specifi
}
```

## Apply Pod Security Context

By default EdgeAgent doesn't make any assumptions about pod security context and allows to run containers under the user this container was built. With this section user can specify exact pod security policy module will run with.

### CreateOptions

A `securityContext` section of config used to apply a pod security context to a module pod spec. The section has the same structure as a Kubernetes [Pod spec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.12/#podsecuritycontext-v1-core).

`EdgeAgent` doesn't do any translations or interpretations of values but simply assigns value from module deployment to `securityContext` parameter of a pod spec.

```json
{
"k8s-experimental": {
"securityContext": {
"fsGroup": "1",
"runAsGroup": "1001",
"runAsUser": "1000",
...
}
}
}
```

0 comments on commit cf2eba9

Please sign in to comment.