diff --git a/changelogs/unreleased/2621-jaygridley b/changelogs/unreleased/2621-jaygridley new file mode 100644 index 000000000..7862a3e8a --- /dev/null +++ b/changelogs/unreleased/2621-jaygridley @@ -0,0 +1 @@ +restic: add support for setting SecurityContext (runAsUser, runAsGroup) for restore diff --git a/pkg/builder/container_builder.go b/pkg/builder/container_builder.go index d58b3e03f..9f46133b6 100644 --- a/pkg/builder/container_builder.go +++ b/pkg/builder/container_builder.go @@ -88,6 +88,12 @@ func (b *ContainerBuilder) Resources(resources *corev1api.ResourceRequirements) return b } +// SecurityContext sets the container's SecurityContext. +func (b *ContainerBuilder) SecurityContext(securityContext *corev1api.SecurityContext) *ContainerBuilder { + b.object.SecurityContext = securityContext + return b +} + func (b *ContainerBuilder) Env(vars ...*corev1api.EnvVar) *ContainerBuilder { for _, v := range vars { b.object.Env = append(b.object.Env, *v) diff --git a/pkg/restore/restic_restore_action.go b/pkg/restore/restic_restore_action.go index 1dace423b..7c315703c 100644 --- a/pkg/restore/restic_restore_action.go +++ b/pkg/restore/restic_restore_action.go @@ -130,8 +130,16 @@ func (a *ResticRestoreAction) Execute(input *velero.RestoreItemActionExecuteInpu ) } + runAsRoot, runAsGroup := getSecurityContext(log, config) + + securityContext, err := kube.ParseSecurityContext(runAsRoot, runAsGroup) + if err != nil { + log.Errorf("Using default resource values, couldn't parse resource requirements: %s.", err) + } + initContainerBuilder := newResticInitContainerBuilder(image, string(input.Restore.UID)) initContainerBuilder.Resources(&resourceReqs) + initContainerBuilder.SecurityContext(&securityContext) for volumeName := range volumeSnapshots { mount := &corev1.VolumeMount{ @@ -211,6 +219,16 @@ func getResourceLimits(log logrus.FieldLogger, config *corev1.ConfigMap) (string return config.Data["cpuLimit"], config.Data["memLimit"] } +// getSecurityContext extracts securityContext runAsUser and runAsGroup from a ConfigMap. +func getSecurityContext(log logrus.FieldLogger, config *corev1.ConfigMap) (string, string) { + if config == nil { + log.Debug("No config found for plugin") + return "", "" + } + + return config.Data["secCtxRunAsUser"], config.Data["secCtxRunAsGroup"] +} + // TODO eventually this can move to pkg/plugin/framework since it'll be used across multiple // plugins. func getPluginConfig(kind framework.PluginKind, name string, client corev1client.ConfigMapInterface) (*corev1.ConfigMap, error) { diff --git a/pkg/restore/restic_restore_action_test.go b/pkg/restore/restic_restore_action_test.go index a85ae9b3b..053fe4bc5 100644 --- a/pkg/restore/restic_restore_action_test.go +++ b/pkg/restore/restic_restore_action_test.go @@ -111,6 +111,8 @@ func TestResticRestoreActionExecute(t *testing.T) { defaultCPURequestLimit, defaultMemRequestLimit, // limits ) + securityContext, _ := kube.ParseSecurityContext("", "") + var ( restoreName = "my-restore" backupName = "test-backup" @@ -134,6 +136,7 @@ func TestResticRestoreActionExecute(t *testing.T) { InitContainers( newResticInitContainerBuilder(initContainerImage(defaultImageBase), ""). Resources(&resourceReqs). + SecurityContext(&securityContext). VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()).Result()). Result(), }, @@ -150,6 +153,7 @@ func TestResticRestoreActionExecute(t *testing.T) { InitContainers( newResticInitContainerBuilder(initContainerImage(defaultImageBase), ""). Resources(&resourceReqs). + SecurityContext(&securityContext). VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()).Result(), builder.ForContainer("first-container", "").Result()). Result(), @@ -189,6 +193,7 @@ func TestResticRestoreActionExecute(t *testing.T) { InitContainers( newResticInitContainerBuilder(initContainerImage(defaultImageBase), ""). Resources(&resourceReqs). + SecurityContext(&securityContext). VolumeMounts(builder.ForVolumeMount("vol-1", "/restores/vol-1").Result(), builder.ForVolumeMount("vol-2", "/restores/vol-2").Result()).Result(), builder.ForContainer("first-container", "").Result()). Result(), diff --git a/pkg/util/kube/security_context.go b/pkg/util/kube/security_context.go new file mode 100644 index 000000000..3246d9d6d --- /dev/null +++ b/pkg/util/kube/security_context.go @@ -0,0 +1,48 @@ +/* +Copyright 2019 the Velero contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube + +import ( + "strconv" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" +) + +func ParseSecurityContext(runAsUser string, runAsGroup string) (corev1.SecurityContext, error) { + securityContext := corev1.SecurityContext{} + + if runAsUser != "" { + parsedRunAsUser, err := strconv.ParseInt(runAsUser, 10, 64) + if err != nil { + return securityContext, errors.WithStack(errors.Errorf(`Security context runAsUser "%s" is not a number`, runAsUser)) + } + + securityContext.RunAsUser = &parsedRunAsUser + } + + if runAsGroup != "" { + parsedRunAsGroup, err := strconv.ParseInt(runAsGroup, 10, 64) + if err != nil { + return securityContext, errors.WithStack(errors.Errorf(`Security context runAsGroup "%s" is not a number`, runAsGroup)) + } + + securityContext.RunAsGroup = &parsedRunAsGroup + } + + return securityContext, nil +} diff --git a/pkg/util/kube/security_context_test.go b/pkg/util/kube/security_context_test.go new file mode 100644 index 000000000..8ef41f368 --- /dev/null +++ b/pkg/util/kube/security_context_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2019 the Velero contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" +) + +func TestParseSecurityContext(t *testing.T) { + type args struct { + runAsUser string + runAsGroup string + } + tests := []struct { + name string + args args + wantErr bool + expected *corev1.SecurityContext + }{ + {"valid security context", args{"1001", "999"}, false, &corev1.SecurityContext{ + RunAsUser: pointInt64(1001), + RunAsGroup: pointInt64(999), + }}, + {"security context without runAsGroup", args{"1001", ""}, false, &corev1.SecurityContext{ + RunAsUser: pointInt64(1001), + }}, + {"security context without runAsUser", args{"", "999"}, false, &corev1.SecurityContext{ + RunAsGroup: pointInt64(999), + }}, + {"empty context without runAsUser", args{"", ""}, false, &corev1.SecurityContext{}}, + {"invalid security context runAsUser", args{"not a number", ""}, true, nil}, + {"invalid security context runAsGroup", args{"", "not a number"}, true, nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseSecurityContext(tt.args.runAsUser, tt.args.runAsGroup) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + if tt.expected == nil { + tt.expected = &corev1.SecurityContext{} + } + + assert.Equal(t, tt.expected.RunAsUser, got.RunAsUser) + assert.Equal(t, tt.expected.RunAsGroup, got.RunAsGroup) + }) + } +} + +func pointInt64(i int64) *int64 { + return &i +} diff --git a/site/docs/main/restic.md b/site/docs/main/restic.md index ac63ca993..ecfd34622 100644 --- a/site/docs/main/restic.md +++ b/site/docs/main/restic.md @@ -380,7 +380,11 @@ data: # If not set, it will default to "128Mi". A value of "0" is treated as unbounded. memLimit: 128Mi + # "secCtxRunAsUser sets the securityContext.runAsUser value on the restic init containers during restore." + secCtxRunAsUser: 1001 + # "secCtxRunAsGroup sets the securityContext.runAsGroup value on the restic init containers during restore." + secCtxRunAsGroup: 999 ``` ## Troubleshooting