diff --git a/commands/register_integration_test.go b/commands/register_integration_test.go index c1e1f6217d3ae6e5b0b81859c8a270896a82e906..1b44e41815bccc1b87751f2b93cc671b732a3f62 100644 --- a/commands/register_integration_test.go +++ b/commands/register_integration_test.go @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/urfave/cli" - clihelpers "gitlab.com/gitlab-org/golang-cli-helpers" "gitlab.com/gitlab-org/gitlab-runner/commands" "gitlab.com/gitlab-org/gitlab-runner/common" @@ -27,10 +26,17 @@ import ( "gitlab.com/gitlab-org/gitlab-runner/helpers" "gitlab.com/gitlab-org/gitlab-runner/helpers/ssh" "gitlab.com/gitlab-org/gitlab-runner/shells" + clihelpers "gitlab.com/gitlab-org/golang-cli-helpers" ) const osTypeWindows = "windows" +var spaceReplacer = strings.NewReplacer(" ", "", "\t", "") + +type kv struct { + key, value string +} + func TestAccessLevelSetting(t *testing.T) { tests := map[string]struct { accessLevel commands.AccessLevel @@ -70,7 +76,7 @@ func TestAccessLevelSetting(t *testing.T) { "--access-level", string(testCase.accessLevel), } - _, output, err := testRegisterCommandRun(t, network, arguments...) + _, output, err := testRegisterCommandRun(t, network, nil, arguments...) if testCase.failureExpected { assert.EqualError(t, err, "command error: Given access-level is not valid. "+ @@ -111,6 +117,7 @@ func TestAskRunnerOverrideDefaultsForExecutors(t *testing.T) { func testRegisterCommandRun( t *testing.T, network common.Network, + env []kv, args ...string, ) (content, output string, err error) { config := &common.RunnerConfig{ @@ -122,6 +129,19 @@ func testRegisterCommandRun( }, } + for _, kv := range env { + err := os.Setenv(kv.key, kv.value) + if err != nil { + return "", "", err + } + } + + defer func() { + for _, kv := range env { + _ = os.Unsetenv(kv.key) + } + }() + hook := test.NewGlobal() defer func() { @@ -654,6 +674,9 @@ check_interval = 0 pod_annotations_overwrite_allowed = "" [runners.kubernetes.affinity] [runners.kubernetes.pod_security_context] + [runners.kubernetes.build_container_security_context] + [runners.kubernetes.helper_container_security_context] + [runners.kubernetes.service_container_security_context] [runners.kubernetes.volumes] [[runners.kubernetes.volumes.empty_dir]] @@ -704,7 +727,7 @@ check_interval = 0 tt.networkAssertions(network) - fileContent, _, err := testRegisterCommandRun(t, network, args...) + fileContent, _, err := testRegisterCommandRun(t, network, nil, args...) if tt.errExpected { require.Error(t, err) return @@ -796,9 +819,10 @@ func TestUnregisterOnFailure(t *testing.T) { func TestRegisterCommand(t *testing.T) { type testCase struct { - condition func() bool - arguments []string - expectedConfig string + condition func() bool + arguments []string + environment []kv + expectedConfigs []string } testCases := map[string]testCase{ @@ -808,11 +832,9 @@ func TestRegisterCommand(t *testing.T) { "--feature-flags", "FF_TEST_1:true", "--feature-flags", "FF_TEST_2:false", }, - expectedConfig: ` - [runners.feature_flags] - FF_TEST_1 = true - FF_TEST_2 = false -`, + expectedConfigs: []string{`[runners.feature_flags] + FF_TEST_1 = true + FF_TEST_2 = false`}, }, "shell defaults to pwsh on Windows with shell executor": { condition: func() bool { return runtime.GOOS == osTypeWindows }, @@ -820,9 +842,7 @@ func TestRegisterCommand(t *testing.T) { "--name", "test-runner", "--executor", "shell", }, - expectedConfig: ` - shell = "pwsh" -`, + expectedConfigs: []string{`shell = "pwsh"`}, }, "shell defaults to pwsh on Windows with docker-windows executor": { condition: func() bool { return runtime.GOOS == osTypeWindows }, @@ -831,9 +851,7 @@ func TestRegisterCommand(t *testing.T) { "--executor", "docker-windows", "--docker-image", "abc", }, - expectedConfig: ` - shell = "pwsh" -`, + expectedConfigs: []string{`shell = "pwsh"`}, }, "shell can be overridden to powershell on Windows with shell executor": { condition: func() bool { return runtime.GOOS == osTypeWindows }, @@ -842,9 +860,7 @@ func TestRegisterCommand(t *testing.T) { "--executor", "shell", "--shell", "powershell", }, - expectedConfig: ` - shell = "powershell" -`, + expectedConfigs: []string{`shell = "powershell"`}, }, "shell can be overridden to powershell on Windows with docker-windows executor": { condition: func() bool { return runtime.GOOS == osTypeWindows }, @@ -854,9 +870,40 @@ func TestRegisterCommand(t *testing.T) { "--shell", "powershell", "--docker-image", "abc", }, - expectedConfig: ` - shell = "powershell" -`, + expectedConfigs: []string{`shell = "powershell"`}, + }, + "kubernetes security context namespace": { + arguments: []string{ + "--executor", "kubernetes", + }, + environment: []kv{ + { + key: "KUBERNETES_BUILD_CONTAINER_SECURITY_CONTEXT_PRIVILEGED", + value: "true", + }, + { + key: "KUBERNETES_HELPER_CONTAINER_SECURITY_CONTEXT_RUN_AS_USER", + value: "1000", + }, + { + key: "KUBERNETES_SERVICE_CONTAINER_SECURITY_CONTEXT_RUN_AS_NON_ROOT", + value: "true", + }, + { + key: "KUBERNETES_SERVICE_CONTAINER_SECURITY_CONTEXT_CAPABILITIES_ADD", + value: "NET_RAW, NET_RAW1", + }, + }, + expectedConfigs: []string{` + [runners.kubernetes.build_container_security_context] + privileged = true`, ` + [runners.kubernetes.helper_container_security_context] + run_as_user = 1000`, ` + [runners.kubernetes.service_container_security_context] + run_as_non_root = true`, ` + [runners.kubernetes.service_container_security_context.capabilities] + add = ["NET_RAW, NET_RAW1"]`, + }, }, } @@ -875,10 +922,12 @@ func TestRegisterCommand(t *testing.T) { }). Once() - gotConfig, _, err := testRegisterCommandRun(t, network, tc.arguments...) + gotConfig, _, err := testRegisterCommandRun(t, network, tc.environment, tc.arguments...) require.NoError(t, err) - assert.Contains(t, gotConfig, tc.expectedConfig) + for _, expectedConfig := range tc.expectedConfigs { + assert.Contains(t, spaceReplacer.Replace(gotConfig), spaceReplacer.Replace(expectedConfig)) + } }) } } diff --git a/common/config.go b/common/config.go index 1b01f634bba72a07abffabab0dda5c9c3066badc..0e596792649b82c844357106055a14cd061bd9bc 100644 --- a/common/config.go +++ b/common/config.go @@ -281,81 +281,84 @@ func (p KubernetesDNSPolicy) Get() (api.DNSPolicy, error) { //nolint:lll type KubernetesConfig struct { - Host string `toml:"host" json:"host" long:"host" env:"KUBERNETES_HOST" description:"Optional Kubernetes master host URL (auto-discovery attempted if not specified)"` - CertFile string `toml:"cert_file,omitempty" json:"cert_file" long:"cert-file" env:"KUBERNETES_CERT_FILE" description:"Optional Kubernetes master auth certificate"` - KeyFile string `toml:"key_file,omitempty" json:"key_file" long:"key-file" env:"KUBERNETES_KEY_FILE" description:"Optional Kubernetes master auth private key"` - CAFile string `toml:"ca_file,omitempty" json:"ca_file" long:"ca-file" env:"KUBERNETES_CA_FILE" description:"Optional Kubernetes master auth ca certificate"` - BearerTokenOverwriteAllowed bool `toml:"bearer_token_overwrite_allowed" json:"bearer_token_overwrite_allowed" long:"bearer_token_overwrite_allowed" env:"KUBERNETES_BEARER_TOKEN_OVERWRITE_ALLOWED" description:"Bool to authorize builds to specify their own bearer token for creation."` - BearerToken string `toml:"bearer_token,omitempty" json:"bearer_token" long:"bearer_token" env:"KUBERNETES_BEARER_TOKEN" description:"Optional Kubernetes service account token used to start build pods."` - Image string `toml:"image" json:"image" long:"image" env:"KUBERNETES_IMAGE" description:"Default docker image to use for builds when none is specified"` - Namespace string `toml:"namespace" json:"namespace" long:"namespace" env:"KUBERNETES_NAMESPACE" description:"Namespace to run Kubernetes jobs in"` - NamespaceOverwriteAllowed string `toml:"namespace_overwrite_allowed" json:"namespace_overwrite_allowed" long:"namespace_overwrite_allowed" env:"KUBERNETES_NAMESPACE_OVERWRITE_ALLOWED" description:"Regex to validate 'KUBERNETES_NAMESPACE_OVERWRITE' value"` - Privileged *bool `toml:"privileged,omitzero" json:"privileged" long:"privileged" env:"KUBERNETES_PRIVILEGED" description:"Run all containers with the privileged flag enabled"` - AllowPrivilegeEscalation *bool `toml:"allow_privilege_escalation,omitzero" json:"allow_privilege_escalation" long:"allow-privilege-escalation" env:"KUBERNETES_ALLOW_PRIVILEGE_ESCALATION" description:"Run all containers with the security context allowPrivilegeEscalation flag enabled. When empty, it does not define the allowPrivilegeEscalation flag in the container SecurityContext and allows Kubernetes to use the default privilege escalation behavior."` - CPULimit string `toml:"cpu_limit,omitempty" json:"cpu_limit" long:"cpu-limit" env:"KUBERNETES_CPU_LIMIT" description:"The CPU allocation given to build containers"` - CPULimitOverwriteMaxAllowed string `toml:"cpu_limit_overwrite_max_allowed,omitempty" json:"cpu_limit_overwrite_max_allowed" long:"cpu-limit-overwrite-max-allowed" env:"KUBERNETES_CPU_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the cpu limit can be set to. Used with the KUBERNETES_CPU_LIMIT variable in the build."` - CPURequest string `toml:"cpu_request,omitempty" json:"cpu_request" long:"cpu-request" env:"KUBERNETES_CPU_REQUEST" description:"The CPU allocation requested for build containers"` - CPURequestOverwriteMaxAllowed string `toml:"cpu_request_overwrite_max_allowed,omitempty" json:"cpu_request_overwrite_max_allowed" long:"cpu-request-overwrite-max-allowed" env:"KUBERNETES_CPU_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the cpu request can be set to. Used with the KUBERNETES_CPU_REQUEST variable in the build."` - MemoryLimit string `toml:"memory_limit,omitempty" json:"memory_limit" long:"memory-limit" env:"KUBERNETES_MEMORY_LIMIT" description:"The amount of memory allocated to build containers"` - MemoryLimitOverwriteMaxAllowed string `toml:"memory_limit_overwrite_max_allowed,omitempty" json:"memory_limit_overwrite_max_allowed" long:"memory-limit-overwrite-max-allowed" env:"KUBERNETES_MEMORY_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the memory limit can be set to. Used with the KUBERNETES_MEMORY_LIMIT variable in the build."` - MemoryRequest string `toml:"memory_request,omitempty" json:"memory_request" long:"memory-request" env:"KUBERNETES_MEMORY_REQUEST" description:"The amount of memory requested from build containers"` - MemoryRequestOverwriteMaxAllowed string `toml:"memory_request_overwrite_max_allowed,omitempty" json:"memory_request_overwrite_max_allowed" long:"memory-request-overwrite-max-allowed" env:"KUBERNETES_MEMORY_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the memory request can be set to. Used with the KUBERNETES_MEMORY_REQUEST variable in the build."` - EphemeralStorageLimit string `toml:"ephemeral_storage_limit,omitempty" json:"ephemeral_storage_limit" long:"ephemeral-storage-limit" env:"KUBERNETES_EPHEMERAL_STORAGE_LIMIT" description:"The amount of ephemeral storage allocated to build containers"` - EphemeralStorageLimitOverwriteMaxAllowed string `toml:"ephemeral_storage_limit_overwrite_max_allowed,omitempty" json:"ephemeral_storage_limit_overwrite_max_allowed" long:"ephemeral-storage-limit-overwrite-max-allowed" env:"KUBERNETES_EPHEMERAL_STORAGE_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the ephemeral limit can be set to. Used with the KUBERNETES_EPHEMERAL_STORAGE_LIMIT variable in the build."` - EphemeralStorageRequest string `toml:"ephemeral_storage_request,omitempty" json:"ephemeral_storage_request" long:"ephemeral-storage-request" env:"KUBERNETES_EPHEMERAL_STORAGE_REQUEST" description:"The amount of ephemeral storage requested from build containers"` - EphemeralStorageRequestOverwriteMaxAllowed string `toml:"ephemeral_storage_request_overwrite_max_allowed,omitempty" json:"ephemeral_storage_request_overwrite_max_allowed" long:"ephemeral-storage-request-overwrite-max-allowed" env:"KUBERNETES_EPHEMERAL_STORAGE_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the ephemeral storage request can be set to. Used with the KUBERNETES_EPHEMERAL_STORAGE_REQUEST variable in the build."` - ServiceCPULimit string `toml:"service_cpu_limit,omitempty" json:"service_cpu_limit" long:"service-cpu-limit" env:"KUBERNETES_SERVICE_CPU_LIMIT" description:"The CPU allocation given to build service containers"` - ServiceCPULimitOverwriteMaxAllowed string `toml:"service_cpu_limit_overwrite_max_allowed,omitempty" json:"service_cpu_limit_overwrite_max_allowed" long:"service-cpu-limit-overwrite-max-allowed" env:"KUBERNETES_SERVICE_CPU_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service cpu limit can be set to. Used with the KUBERNETES_SERVICE_CPU_LIMIT variable in the build."` - ServiceCPURequest string `toml:"service_cpu_request,omitempty" json:"service_cpu_request" long:"service-cpu-request" env:"KUBERNETES_SERVICE_CPU_REQUEST" description:"The CPU allocation requested for build service containers"` - ServiceCPURequestOverwriteMaxAllowed string `toml:"service_cpu_request_overwrite_max_allowed,omitempty" json:"service_cpu_request_overwrite_max_allowed" long:"service-cpu-request-overwrite-max-allowed" env:"KUBERNETES_SERVICE_CPU_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service cpu request can be set to. Used with the KUBERNETES_SERVICE_CPU_REQUEST variable in the build."` - ServiceMemoryLimit string `toml:"service_memory_limit,omitempty" json:"service_memory_limit" long:"service-memory-limit" env:"KUBERNETES_SERVICE_MEMORY_LIMIT" description:"The amount of memory allocated to build service containers"` - ServiceMemoryLimitOverwriteMaxAllowed string `toml:"service_memory_limit_overwrite_max_allowed,omitempty" json:"service_memory_limit_overwrite_max_allowed" long:"service-memory-limit-overwrite-max-allowed" env:"KUBERNETES_SERVICE_MEMORY_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service memory limit can be set to. Used with the KUBERNETES_SERVICE_MEMORY_LIMIT variable in the build."` - ServiceMemoryRequest string `toml:"service_memory_request,omitempty" json:"service_memory_request" long:"service-memory-request" env:"KUBERNETES_SERVICE_MEMORY_REQUEST" description:"The amount of memory requested for build service containers"` - ServiceMemoryRequestOverwriteMaxAllowed string `toml:"service_memory_request_overwrite_max_allowed,omitempty" json:"service_memory_request_overwrite_max_allowed" long:"service-memory-request-overwrite-max-allowed" env:"KUBERNETES_SERVICE_MEMORY_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service memory request can be set to. Used with the KUBERNETES_SERVICE_MEMORY_REQUEST variable in the build."` - ServiceEphemeralStorageLimit string `toml:"service_ephemeral_storage_limit,omitempty" json:"service_ephemeral_storage_limit" long:"service-ephemeral_storage-limit" env:"KUBERNETES_SERVICE_EPHEMERAL_STORAGE_LIMIT" description:"The amount of ephemeral storage allocated to build service containers"` - ServiceEphemeralStorageLimitOverwriteMaxAllowed string `toml:"service_ephemeral_storage_limit_overwrite_max_allowed,omitempty" json:"service_ephemeral_storage_limit_overwrite_max_allowed" long:"service-ephemeral_storage-limit-overwrite-max-allowed" env:"KUBERNETES_SERVICE_EPHEMERAL_STORAGE_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service ephemeral storage limit can be set to. Used with the KUBERNETES_SERVICE_EPHEMERAL_STORAGE_LIMIT variable in the build."` - ServiceEphemeralStorageRequest string `toml:"service_ephemeral_storage_request,omitempty" json:"service_ephemeral_storage_request" long:"service-ephemeral_storage-request" env:"KUBERNETES_SERVICE_EPHEMERAL_STORAGE_REQUEST" description:"The amount of ephemeral storage requested for build service containers"` - ServiceEphemeralStorageRequestOverwriteMaxAllowed string `toml:"service_ephemeral_storage_request_overwrite_max_allowed,omitempty" json:"service_ephemeral_storage_request_overwrite_max_allowed" long:"service-ephemeral_storage-request-overwrite-max-allowed" env:"KUBERNETES_SERVICE_EPHEMERAL_STORAGE_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service ephemeral storage request can be set to. Used with the KUBERNETES_SERVICE_EPHEMERAL_STORAGE_REQUEST variable in the build."` - HelperCPULimit string `toml:"helper_cpu_limit,omitempty" json:"helper_cpu_limit" long:"helper-cpu-limit" env:"KUBERNETES_HELPER_CPU_LIMIT" description:"The CPU allocation given to build helper containers"` - HelperCPULimitOverwriteMaxAllowed string `toml:"helper_cpu_limit_overwrite_max_allowed,omitempty" json:"helper_cpu_limit_overwrite_max_allowed" long:"helper-cpu-limit-overwrite-max-allowed" env:"KUBERNETES_HELPER_CPU_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper cpu limit can be set to. Used with the KUBERNETES_HELPER_CPU_LIMIT variable in the build."` - HelperCPURequest string `toml:"helper_cpu_request,omitempty" json:"helper_cpu_request" long:"helper-cpu-request" env:"KUBERNETES_HELPER_CPU_REQUEST" description:"The CPU allocation requested for build helper containers"` - HelperCPURequestOverwriteMaxAllowed string `toml:"helper_cpu_request_overwrite_max_allowed,omitempty" json:"helper_cpu_request_overwrite_max_allowed" long:"helper-cpu-request-overwrite-max-allowed" env:"KUBERNETES_HELPER_CPU_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper cpu request can be set to. Used with the KUBERNETES_HELPER_CPU_REQUEST variable in the build."` - HelperMemoryLimit string `toml:"helper_memory_limit,omitempty" json:"helper_memory_limit" long:"helper-memory-limit" env:"KUBERNETES_HELPER_MEMORY_LIMIT" description:"The amount of memory allocated to build helper containers"` - HelperMemoryLimitOverwriteMaxAllowed string `toml:"helper_memory_limit_overwrite_max_allowed,omitempty" json:"helper_memory_limit_overwrite_max_allowed" long:"helper-memory-limit-overwrite-max-allowed" env:"KUBERNETES_HELPER_MEMORY_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper memory limit can be set to. Used with the KUBERNETES_HELPER_MEMORY_LIMIT variable in the build."` - HelperMemoryRequest string `toml:"helper_memory_request,omitempty" json:"helper_memory_request" long:"helper-memory-request" env:"KUBERNETES_HELPER_MEMORY_REQUEST" description:"The amount of memory requested for build helper containers"` - HelperMemoryRequestOverwriteMaxAllowed string `toml:"helper_memory_request_overwrite_max_allowed,omitempty" json:"helper_memory_request_overwrite_max_allowed" long:"helper-memory-request-overwrite-max-allowed" env:"KUBERNETES_HELPER_MEMORY_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper memory request can be set to. Used with the KUBERNETES_HELPER_MEMORY_REQUEST variable in the build."` - HelperEphemeralStorageLimit string `toml:"helper_ephemeral_storage_limit,omitempty" json:"helper_ephemeral_storage_limit" long:"helper-ephemeral_storage-limit" env:"KUBERNETES_HELPER_EPHEMERAL_STORAGE_LIMIT" description:"The amount of ephemeral storage allocated to build helper containers"` - HelperEphemeralStorageLimitOverwriteMaxAllowed string `toml:"helper_ephemeral_storage_limit_overwrite_max_allowed,omitempty" json:"helper_ephemeral_storage_limit_overwrite_max_allowed" long:"helper-ephemeral_storage-limit-overwrite-max-allowed" env:"KUBERNETES_HELPER_EPHEMERAL_STORAGE_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper ephemeral storage limit can be set to. Used with the KUBERNETES_HELPER_EPHEMERAL_STORAGE_LIMIT variable in the build."` - HelperEphemeralStorageRequest string `toml:"helper_ephemeral_storage_request,omitempty" json:"helper_ephemeral_storage_request" long:"helper-ephemeral_storage-request" env:"KUBERNETES_HELPER_EPHEMERAL_STORAGE_REQUEST" description:"The amount of ephemeral storage requested for build helper containers"` - HelperEphemeralStorageRequestOverwriteMaxAllowed string `toml:"helper_ephemeral_storage_request_overwrite_max_allowed,omitempty" json:"helper_ephemeral_storage_request_overwrite_max_allowed" long:"helper-ephemeral_storage-request-overwrite-max-allowed" env:"KUBERNETES_HELPER_EPHEMERAL_STORAGE_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper ephemeral storage request can be set to. Used with the KUBERNETES_HELPER_EPHEMERAL_STORAGE_REQUEST variable in the build."` - AllowedImages []string `toml:"allowed_images,omitempty" json:"allowed_images" long:"allowed-images" env:"KUBERNETES_ALLOWED_IMAGES" description:"Image allowlist"` - AllowedServices []string `toml:"allowed_services,omitempty" json:"allowed_services" long:"allowed-services" env:"KUBERNETES_ALLOWED_SERVICES" description:"Service allowlist"` - PullPolicy StringOrArray `toml:"pull_policy,omitempty" json:"pull_policy" long:"pull-policy" env:"KUBERNETES_PULL_POLICY" description:"Policy for if/when to pull a container image (never, if-not-present, always). The cluster default will be used if not set"` - NodeSelector map[string]string `toml:"node_selector,omitempty" json:"node_selector" long:"node-selector" env:"KUBERNETES_NODE_SELECTOR" description:"A toml table/json object of key:value. Value is expected to be a string. When set this will create pods on k8s nodes that match all the key:value pairs. Only one selector is supported through environment variable configuration."` - NodeTolerations map[string]string `toml:"node_tolerations,omitempty" json:"node_tolerations" long:"node-tolerations" env:"KUBERNETES_NODE_TOLERATIONS" description:"A toml table/json object of key=value:effect. Value and effect are expected to be strings. When set, pods will tolerate the given taints. Only one toleration is supported through environment variable configuration."` - Affinity KubernetesAffinity `toml:"affinity,omitempty" json:"affinity" long:"affinity" description:"Kubernetes Affinity setting that is used to select the node that spawns a pod"` - ImagePullSecrets []string `toml:"image_pull_secrets,omitempty" json:"image_pull_secrets" long:"image-pull-secrets" env:"KUBERNETES_IMAGE_PULL_SECRETS" description:"A list of image pull secrets that are used for pulling docker image"` - HelperImage string `toml:"helper_image,omitempty" json:"helper_image" long:"helper-image" env:"KUBERNETES_HELPER_IMAGE" description:"[ADVANCED] Override the default helper image used to clone repos and upload artifacts"` - HelperImageFlavor string `toml:"helper_image_flavor,omitempty" json:"helper_image_flavor" long:"helper-image-flavor" env:"KUBERNETES_HELPER_IMAGE_FLAVOR" description:"Set helper image flavor (alpine, ubuntu), defaults to alpine"` - TerminationGracePeriodSeconds *int64 `toml:"terminationGracePeriodSeconds,omitzero" json:"terminationGracePeriodSeconds" long:"terminationGracePeriodSeconds" env:"KUBERNETES_TERMINATIONGRACEPERIODSECONDS" description:"Duration after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. DEPRECATED: use KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS and KUBERNETES_CLEANUP_GRACE_PERIOD_SECONDS instead."` - PodTerminationGracePeriodSeconds *int64 `toml:"pod_termination_grace_period_seconds,omitzero" json:"pod_termination_grace_period_seconds" long:"pod_termination_grace_period_seconds" env:"KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS" description:"Pod-level setting which determines the duration in seconds which the pod has to terminate gracefully. After this, the processes are forcibly halted with a kill signal. Ignored if KUBERNETES_TERMINATIONGRACEPERIODSECONDS is specified."` - CleanupGracePeriodSeconds *int64 `toml:"cleanup_grace_period_seconds,omitzero" json:"cleanup_grace_period_seconds" long:"cleanup_grace_period_seconds" env:"KUBERNETES_CLEANUP_GRACE_PERIOD_SECONDS" description:"When cleaning up a pod on completion of a job, the duration in seconds which the pod has to terminate gracefully. After this, the processes are forcibly halted with a kill signal. Ignored if KUBERNETES_TERMINATIONGRACEPERIODSECONDS is specified."` - PollInterval int `toml:"poll_interval,omitzero" json:"poll_interval" long:"poll-interval" env:"KUBERNETES_POLL_INTERVAL" description:"How frequently, in seconds, the runner will poll the Kubernetes pod it has just created to check its status"` - PollTimeout int `toml:"poll_timeout,omitzero" json:"poll_timeout" long:"poll-timeout" env:"KUBERNETES_POLL_TIMEOUT" description:"The total amount of time, in seconds, that needs to pass before the runner will timeout attempting to connect to the pod it has just created (useful for queueing more builds that the cluster can handle at a time)"` - PodLabels map[string]string `toml:"pod_labels,omitempty" json:"pod_labels" long:"pod-labels" description:"A toml table/json object of key-value. Value is expected to be a string. When set, this will create pods with the given pod labels. Environment variables will be substituted for values here."` - ServiceAccount string `toml:"service_account,omitempty" json:"service_account" long:"service-account" env:"KUBERNETES_SERVICE_ACCOUNT" description:"Executor pods will use this Service Account to talk to kubernetes API"` - ServiceAccountOverwriteAllowed string `toml:"service_account_overwrite_allowed" json:"service_account_overwrite_allowed" long:"service_account_overwrite_allowed" env:"KUBERNETES_SERVICE_ACCOUNT_OVERWRITE_ALLOWED" description:"Regex to validate 'KUBERNETES_SERVICE_ACCOUNT' value"` - PodAnnotations map[string]string `toml:"pod_annotations,omitempty" json:"pod_annotations" long:"pod-annotations" description:"A toml table/json object of key-value. Value is expected to be a string. When set, this will create pods with the given annotations. Can be overwritten in build with KUBERNETES_POD_ANNOTATION_* variables"` - PodAnnotationsOverwriteAllowed string `toml:"pod_annotations_overwrite_allowed" json:"pod_annotations_overwrite_allowed" long:"pod_annotations_overwrite_allowed" env:"KUBERNETES_POD_ANNOTATIONS_OVERWRITE_ALLOWED" description:"Regex to validate 'KUBERNETES_POD_ANNOTATIONS_*' values"` - PodSecurityContext KubernetesPodSecurityContext `toml:"pod_security_context,omitempty" namespace:"pod-security-context" description:"A security context attached to each build pod"` - Volumes KubernetesVolumes `toml:"volumes"` - HostAliases []KubernetesHostAliases `toml:"host_aliases,omitempty" json:"host_aliases" long:"host_aliases" description:"Add a custom host-to-IP mapping"` - Services []Service `toml:"services,omitempty" json:"services" description:"Add service that is started with container"` - CapAdd []string `toml:"cap_add" json:"cap_add" long:"cap-add" env:"KUBERNETES_CAP_ADD" description:"Add Linux capabilities"` - CapDrop []string `toml:"cap_drop" json:"cap_drop" long:"cap-drop" env:"KUBERNETES_CAP_DROP" description:"Drop Linux capabilities"` - DNSPolicy KubernetesDNSPolicy `toml:"dns_policy,omitempty" json:"dns_policy" long:"dns-policy" env:"KUBERNETES_DNS_POLICY" description:"How Kubernetes should try to resolve DNS from the created pods. If unset, Kubernetes will use the default 'ClusterFirst'. Valid values are: none, default, cluster-first, cluster-first-with-host-net"` - DNSConfig KubernetesDNSConfig `toml:"dns_config" json:"dns_config" description:"Pod DNS config"` - ContainerLifecycle KubernetesContainerLifecyle `toml:"container_lifecycle,omitempty" json:"container_lifecycle,omitempty" description:"Actions that the management system should take in response to container lifecycle events"` + Host string `toml:"host" json:"host" long:"host" env:"KUBERNETES_HOST" description:"Optional Kubernetes master host URL (auto-discovery attempted if not specified)"` + CertFile string `toml:"cert_file,omitempty" json:"cert_file" long:"cert-file" env:"KUBERNETES_CERT_FILE" description:"Optional Kubernetes master auth certificate"` + KeyFile string `toml:"key_file,omitempty" json:"key_file" long:"key-file" env:"KUBERNETES_KEY_FILE" description:"Optional Kubernetes master auth private key"` + CAFile string `toml:"ca_file,omitempty" json:"ca_file" long:"ca-file" env:"KUBERNETES_CA_FILE" description:"Optional Kubernetes master auth ca certificate"` + BearerTokenOverwriteAllowed bool `toml:"bearer_token_overwrite_allowed" json:"bearer_token_overwrite_allowed" long:"bearer_token_overwrite_allowed" env:"KUBERNETES_BEARER_TOKEN_OVERWRITE_ALLOWED" description:"Bool to authorize builds to specify their own bearer token for creation."` + BearerToken string `toml:"bearer_token,omitempty" json:"bearer_token" long:"bearer_token" env:"KUBERNETES_BEARER_TOKEN" description:"Optional Kubernetes service account token used to start build pods."` + Image string `toml:"image" json:"image" long:"image" env:"KUBERNETES_IMAGE" description:"Default docker image to use for builds when none is specified"` + Namespace string `toml:"namespace" json:"namespace" long:"namespace" env:"KUBERNETES_NAMESPACE" description:"Namespace to run Kubernetes jobs in"` + NamespaceOverwriteAllowed string `toml:"namespace_overwrite_allowed" json:"namespace_overwrite_allowed" long:"namespace_overwrite_allowed" env:"KUBERNETES_NAMESPACE_OVERWRITE_ALLOWED" description:"Regex to validate 'KUBERNETES_NAMESPACE_OVERWRITE' value"` + Privileged *bool `toml:"privileged,omitzero" json:"privileged" long:"privileged" env:"KUBERNETES_PRIVILEGED" description:"Run all containers with the privileged flag enabled"` + AllowPrivilegeEscalation *bool `toml:"allow_privilege_escalation,omitzero" json:"allow_privilege_escalation" long:"allow-privilege-escalation" env:"KUBERNETES_ALLOW_PRIVILEGE_ESCALATION" description:"Run all containers with the security context allowPrivilegeEscalation flag enabled. When empty, it does not define the allowPrivilegeEscalation flag in the container SecurityContext and allows Kubernetes to use the default privilege escalation behavior."` + CPULimit string `toml:"cpu_limit,omitempty" json:"cpu_limit" long:"cpu-limit" env:"KUBERNETES_CPU_LIMIT" description:"The CPU allocation given to build containers"` + CPULimitOverwriteMaxAllowed string `toml:"cpu_limit_overwrite_max_allowed,omitempty" json:"cpu_limit_overwrite_max_allowed" long:"cpu-limit-overwrite-max-allowed" env:"KUBERNETES_CPU_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the cpu limit can be set to. Used with the KUBERNETES_CPU_LIMIT variable in the build."` + CPURequest string `toml:"cpu_request,omitempty" json:"cpu_request" long:"cpu-request" env:"KUBERNETES_CPU_REQUEST" description:"The CPU allocation requested for build containers"` + CPURequestOverwriteMaxAllowed string `toml:"cpu_request_overwrite_max_allowed,omitempty" json:"cpu_request_overwrite_max_allowed" long:"cpu-request-overwrite-max-allowed" env:"KUBERNETES_CPU_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the cpu request can be set to. Used with the KUBERNETES_CPU_REQUEST variable in the build."` + MemoryLimit string `toml:"memory_limit,omitempty" json:"memory_limit" long:"memory-limit" env:"KUBERNETES_MEMORY_LIMIT" description:"The amount of memory allocated to build containers"` + MemoryLimitOverwriteMaxAllowed string `toml:"memory_limit_overwrite_max_allowed,omitempty" json:"memory_limit_overwrite_max_allowed" long:"memory-limit-overwrite-max-allowed" env:"KUBERNETES_MEMORY_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the memory limit can be set to. Used with the KUBERNETES_MEMORY_LIMIT variable in the build."` + MemoryRequest string `toml:"memory_request,omitempty" json:"memory_request" long:"memory-request" env:"KUBERNETES_MEMORY_REQUEST" description:"The amount of memory requested from build containers"` + MemoryRequestOverwriteMaxAllowed string `toml:"memory_request_overwrite_max_allowed,omitempty" json:"memory_request_overwrite_max_allowed" long:"memory-request-overwrite-max-allowed" env:"KUBERNETES_MEMORY_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the memory request can be set to. Used with the KUBERNETES_MEMORY_REQUEST variable in the build."` + EphemeralStorageLimit string `toml:"ephemeral_storage_limit,omitempty" json:"ephemeral_storage_limit" long:"ephemeral-storage-limit" env:"KUBERNETES_EPHEMERAL_STORAGE_LIMIT" description:"The amount of ephemeral storage allocated to build containers"` + EphemeralStorageLimitOverwriteMaxAllowed string `toml:"ephemeral_storage_limit_overwrite_max_allowed,omitempty" json:"ephemeral_storage_limit_overwrite_max_allowed" long:"ephemeral-storage-limit-overwrite-max-allowed" env:"KUBERNETES_EPHEMERAL_STORAGE_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the ephemeral limit can be set to. Used with the KUBERNETES_EPHEMERAL_STORAGE_LIMIT variable in the build."` + EphemeralStorageRequest string `toml:"ephemeral_storage_request,omitempty" json:"ephemeral_storage_request" long:"ephemeral-storage-request" env:"KUBERNETES_EPHEMERAL_STORAGE_REQUEST" description:"The amount of ephemeral storage requested from build containers"` + EphemeralStorageRequestOverwriteMaxAllowed string `toml:"ephemeral_storage_request_overwrite_max_allowed,omitempty" json:"ephemeral_storage_request_overwrite_max_allowed" long:"ephemeral-storage-request-overwrite-max-allowed" env:"KUBERNETES_EPHEMERAL_STORAGE_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the ephemeral storage request can be set to. Used with the KUBERNETES_EPHEMERAL_STORAGE_REQUEST variable in the build."` + ServiceCPULimit string `toml:"service_cpu_limit,omitempty" json:"service_cpu_limit" long:"service-cpu-limit" env:"KUBERNETES_SERVICE_CPU_LIMIT" description:"The CPU allocation given to build service containers"` + ServiceCPULimitOverwriteMaxAllowed string `toml:"service_cpu_limit_overwrite_max_allowed,omitempty" json:"service_cpu_limit_overwrite_max_allowed" long:"service-cpu-limit-overwrite-max-allowed" env:"KUBERNETES_SERVICE_CPU_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service cpu limit can be set to. Used with the KUBERNETES_SERVICE_CPU_LIMIT variable in the build."` + ServiceCPURequest string `toml:"service_cpu_request,omitempty" json:"service_cpu_request" long:"service-cpu-request" env:"KUBERNETES_SERVICE_CPU_REQUEST" description:"The CPU allocation requested for build service containers"` + ServiceCPURequestOverwriteMaxAllowed string `toml:"service_cpu_request_overwrite_max_allowed,omitempty" json:"service_cpu_request_overwrite_max_allowed" long:"service-cpu-request-overwrite-max-allowed" env:"KUBERNETES_SERVICE_CPU_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service cpu request can be set to. Used with the KUBERNETES_SERVICE_CPU_REQUEST variable in the build."` + ServiceMemoryLimit string `toml:"service_memory_limit,omitempty" json:"service_memory_limit" long:"service-memory-limit" env:"KUBERNETES_SERVICE_MEMORY_LIMIT" description:"The amount of memory allocated to build service containers"` + ServiceMemoryLimitOverwriteMaxAllowed string `toml:"service_memory_limit_overwrite_max_allowed,omitempty" json:"service_memory_limit_overwrite_max_allowed" long:"service-memory-limit-overwrite-max-allowed" env:"KUBERNETES_SERVICE_MEMORY_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service memory limit can be set to. Used with the KUBERNETES_SERVICE_MEMORY_LIMIT variable in the build."` + ServiceMemoryRequest string `toml:"service_memory_request,omitempty" json:"service_memory_request" long:"service-memory-request" env:"KUBERNETES_SERVICE_MEMORY_REQUEST" description:"The amount of memory requested for build service containers"` + ServiceMemoryRequestOverwriteMaxAllowed string `toml:"service_memory_request_overwrite_max_allowed,omitempty" json:"service_memory_request_overwrite_max_allowed" long:"service-memory-request-overwrite-max-allowed" env:"KUBERNETES_SERVICE_MEMORY_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service memory request can be set to. Used with the KUBERNETES_SERVICE_MEMORY_REQUEST variable in the build."` + ServiceEphemeralStorageLimit string `toml:"service_ephemeral_storage_limit,omitempty" json:"service_ephemeral_storage_limit" long:"service-ephemeral_storage-limit" env:"KUBERNETES_SERVICE_EPHEMERAL_STORAGE_LIMIT" description:"The amount of ephemeral storage allocated to build service containers"` + ServiceEphemeralStorageLimitOverwriteMaxAllowed string `toml:"service_ephemeral_storage_limit_overwrite_max_allowed,omitempty" json:"service_ephemeral_storage_limit_overwrite_max_allowed" long:"service-ephemeral_storage-limit-overwrite-max-allowed" env:"KUBERNETES_SERVICE_EPHEMERAL_STORAGE_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service ephemeral storage limit can be set to. Used with the KUBERNETES_SERVICE_EPHEMERAL_STORAGE_LIMIT variable in the build."` + ServiceEphemeralStorageRequest string `toml:"service_ephemeral_storage_request,omitempty" json:"service_ephemeral_storage_request" long:"service-ephemeral_storage-request" env:"KUBERNETES_SERVICE_EPHEMERAL_STORAGE_REQUEST" description:"The amount of ephemeral storage requested for build service containers"` + ServiceEphemeralStorageRequestOverwriteMaxAllowed string `toml:"service_ephemeral_storage_request_overwrite_max_allowed,omitempty" json:"service_ephemeral_storage_request_overwrite_max_allowed" long:"service-ephemeral_storage-request-overwrite-max-allowed" env:"KUBERNETES_SERVICE_EPHEMERAL_STORAGE_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the service ephemeral storage request can be set to. Used with the KUBERNETES_SERVICE_EPHEMERAL_STORAGE_REQUEST variable in the build."` + HelperCPULimit string `toml:"helper_cpu_limit,omitempty" json:"helper_cpu_limit" long:"helper-cpu-limit" env:"KUBERNETES_HELPER_CPU_LIMIT" description:"The CPU allocation given to build helper containers"` + HelperCPULimitOverwriteMaxAllowed string `toml:"helper_cpu_limit_overwrite_max_allowed,omitempty" json:"helper_cpu_limit_overwrite_max_allowed" long:"helper-cpu-limit-overwrite-max-allowed" env:"KUBERNETES_HELPER_CPU_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper cpu limit can be set to. Used with the KUBERNETES_HELPER_CPU_LIMIT variable in the build."` + HelperCPURequest string `toml:"helper_cpu_request,omitempty" json:"helper_cpu_request" long:"helper-cpu-request" env:"KUBERNETES_HELPER_CPU_REQUEST" description:"The CPU allocation requested for build helper containers"` + HelperCPURequestOverwriteMaxAllowed string `toml:"helper_cpu_request_overwrite_max_allowed,omitempty" json:"helper_cpu_request_overwrite_max_allowed" long:"helper-cpu-request-overwrite-max-allowed" env:"KUBERNETES_HELPER_CPU_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper cpu request can be set to. Used with the KUBERNETES_HELPER_CPU_REQUEST variable in the build."` + HelperMemoryLimit string `toml:"helper_memory_limit,omitempty" json:"helper_memory_limit" long:"helper-memory-limit" env:"KUBERNETES_HELPER_MEMORY_LIMIT" description:"The amount of memory allocated to build helper containers"` + HelperMemoryLimitOverwriteMaxAllowed string `toml:"helper_memory_limit_overwrite_max_allowed,omitempty" json:"helper_memory_limit_overwrite_max_allowed" long:"helper-memory-limit-overwrite-max-allowed" env:"KUBERNETES_HELPER_MEMORY_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper memory limit can be set to. Used with the KUBERNETES_HELPER_MEMORY_LIMIT variable in the build."` + HelperMemoryRequest string `toml:"helper_memory_request,omitempty" json:"helper_memory_request" long:"helper-memory-request" env:"KUBERNETES_HELPER_MEMORY_REQUEST" description:"The amount of memory requested for build helper containers"` + HelperMemoryRequestOverwriteMaxAllowed string `toml:"helper_memory_request_overwrite_max_allowed,omitempty" json:"helper_memory_request_overwrite_max_allowed" long:"helper-memory-request-overwrite-max-allowed" env:"KUBERNETES_HELPER_MEMORY_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper memory request can be set to. Used with the KUBERNETES_HELPER_MEMORY_REQUEST variable in the build."` + HelperEphemeralStorageLimit string `toml:"helper_ephemeral_storage_limit,omitempty" json:"helper_ephemeral_storage_limit" long:"helper-ephemeral_storage-limit" env:"KUBERNETES_HELPER_EPHEMERAL_STORAGE_LIMIT" description:"The amount of ephemeral storage allocated to build helper containers"` + HelperEphemeralStorageLimitOverwriteMaxAllowed string `toml:"helper_ephemeral_storage_limit_overwrite_max_allowed,omitempty" json:"helper_ephemeral_storage_limit_overwrite_max_allowed" long:"helper-ephemeral_storage-limit-overwrite-max-allowed" env:"KUBERNETES_HELPER_EPHEMERAL_STORAGE_LIMIT_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper ephemeral storage limit can be set to. Used with the KUBERNETES_HELPER_EPHEMERAL_STORAGE_LIMIT variable in the build."` + HelperEphemeralStorageRequest string `toml:"helper_ephemeral_storage_request,omitempty" json:"helper_ephemeral_storage_request" long:"helper-ephemeral_storage-request" env:"KUBERNETES_HELPER_EPHEMERAL_STORAGE_REQUEST" description:"The amount of ephemeral storage requested for build helper containers"` + HelperEphemeralStorageRequestOverwriteMaxAllowed string `toml:"helper_ephemeral_storage_request_overwrite_max_allowed,omitempty" json:"helper_ephemeral_storage_request_overwrite_max_allowed" long:"helper-ephemeral_storage-request-overwrite-max-allowed" env:"KUBERNETES_HELPER_EPHEMERAL_STORAGE_REQUEST_OVERWRITE_MAX_ALLOWED" description:"If set, the max amount the helper ephemeral storage request can be set to. Used with the KUBERNETES_HELPER_EPHEMERAL_STORAGE_REQUEST variable in the build."` + AllowedImages []string `toml:"allowed_images,omitempty" json:"allowed_images" long:"allowed-images" env:"KUBERNETES_ALLOWED_IMAGES" description:"Image allowlist"` + AllowedServices []string `toml:"allowed_services,omitempty" json:"allowed_services" long:"allowed-services" env:"KUBERNETES_ALLOWED_SERVICES" description:"Service allowlist"` + PullPolicy StringOrArray `toml:"pull_policy,omitempty" json:"pull_policy" long:"pull-policy" env:"KUBERNETES_PULL_POLICY" description:"Policy for if/when to pull a container image (never, if-not-present, always). The cluster default will be used if not set"` + NodeSelector map[string]string `toml:"node_selector,omitempty" json:"node_selector" long:"node-selector" env:"KUBERNETES_NODE_SELECTOR" description:"A toml table/json object of key:value. Value is expected to be a string. When set this will create pods on k8s nodes that match all the key:value pairs. Only one selector is supported through environment variable configuration."` + NodeTolerations map[string]string `toml:"node_tolerations,omitempty" json:"node_tolerations" long:"node-tolerations" env:"KUBERNETES_NODE_TOLERATIONS" description:"A toml table/json object of key=value:effect. Value and effect are expected to be strings. When set, pods will tolerate the given taints. Only one toleration is supported through environment variable configuration."` + Affinity KubernetesAffinity `toml:"affinity,omitempty" json:"affinity" long:"affinity" description:"Kubernetes Affinity setting that is used to select the node that spawns a pod"` + ImagePullSecrets []string `toml:"image_pull_secrets,omitempty" json:"image_pull_secrets" long:"image-pull-secrets" env:"KUBERNETES_IMAGE_PULL_SECRETS" description:"A list of image pull secrets that are used for pulling docker image"` + HelperImage string `toml:"helper_image,omitempty" json:"helper_image" long:"helper-image" env:"KUBERNETES_HELPER_IMAGE" description:"[ADVANCED] Override the default helper image used to clone repos and upload artifacts"` + HelperImageFlavor string `toml:"helper_image_flavor,omitempty" json:"helper_image_flavor" long:"helper-image-flavor" env:"KUBERNETES_HELPER_IMAGE_FLAVOR" description:"Set helper image flavor (alpine, ubuntu), defaults to alpine"` + TerminationGracePeriodSeconds *int64 `toml:"terminationGracePeriodSeconds,omitzero" json:"terminationGracePeriodSeconds" long:"terminationGracePeriodSeconds" env:"KUBERNETES_TERMINATIONGRACEPERIODSECONDS" description:"Duration after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal.DEPRECATED: use KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS and KUBERNETES_CLEANUP_GRACE_PERIOD_SECONDS instead."` + PodTerminationGracePeriodSeconds *int64 `toml:"pod_termination_grace_period_seconds,omitzero" json:"pod_termination_grace_period_seconds" long:"pod_termination_grace_period_seconds" env:"KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS" description:"Pod-level setting which determines the duration in seconds which the pod has to terminate gracefully. After this, the processes are forcibly halted with a kill signal. Ignored if KUBERNETES_TERMINATIONGRACEPERIODSECONDS is specified."` + CleanupGracePeriodSeconds *int64 `toml:"cleanup_grace_period_seconds,omitzero" json:"cleanup_grace_period_seconds" long:"cleanup_grace_period_seconds" env:"KUBERNETES_CLEANUP_GRACE_PERIOD_SECONDS" description:"When cleaning up a pod on completion of a job, the duration in seconds which the pod has to terminate gracefully. After this, the processes are forcibly halted with a kill signal. Ignored if KUBERNETES_TERMINATIONGRACEPERIODSECONDS is specified."` + PollInterval int `toml:"poll_interval,omitzero" json:"poll_interval" long:"poll-interval" env:"KUBERNETES_POLL_INTERVAL" description:"How frequently, in seconds, the runner will poll the Kubernetes pod it has just created to check its status"` + PollTimeout int `toml:"poll_timeout,omitzero" json:"poll_timeout" long:"poll-timeout" env:"KUBERNETES_POLL_TIMEOUT" description:"The total amount of time, in seconds, that needs to pass before the runner will timeout attempting to connect to the pod it has just created (useful for queueing more builds that the cluster can handle at a time)"` + PodLabels map[string]string `toml:"pod_labels,omitempty" json:"pod_labels" long:"pod-labels" description:"A toml table/json object of key-value. Value is expected to be a string. When set, this will create pods with the given pod labels. Environment variables will be substituted for values here."` + ServiceAccount string `toml:"service_account,omitempty" json:"service_account" long:"service-account" env:"KUBERNETES_SERVICE_ACCOUNT" description:"Executor pods will use this Service Account to talk to kubernetes API"` + ServiceAccountOverwriteAllowed string `toml:"service_account_overwrite_allowed" json:"service_account_overwrite_allowed" long:"service_account_overwrite_allowed" env:"KUBERNETES_SERVICE_ACCOUNT_OVERWRITE_ALLOWED" description:"Regex to validate 'KUBERNETES_SERVICE_ACCOUNT' value"` + PodAnnotations map[string]string `toml:"pod_annotations,omitempty" json:"pod_annotations" long:"pod-annotations" description:"A toml table/json object of key-value. Value is expected to be a string. When set, this will create pods with the given annotations. Can be overwritten in build with KUBERNETES_POD_ANNOTATION_* variables"` + PodAnnotationsOverwriteAllowed string `toml:"pod_annotations_overwrite_allowed" json:"pod_annotations_overwrite_allowed" long:"pod_annotations_overwrite_allowed" env:"KUBERNETES_POD_ANNOTATIONS_OVERWRITE_ALLOWED" description:"Regex to validate 'KUBERNETES_POD_ANNOTATIONS_*' values"` + PodSecurityContext KubernetesPodSecurityContext `toml:"pod_security_context,omitempty" namespace:"pod-security-context" description:"A security context attached to each build pod"` + BuildContainerSecurityContext KubernetesContainerSecurityContext `toml:"build_container_security_context,omitempty" namespace:"build_container_security_context" description:"A security context attached to the build container inside the build pod"` + HelperContainerSecurityContext KubernetesContainerSecurityContext `toml:"helper_container_security_context,omitempty" namespace:"helper_container_security_context" description:"A security context attached to the helper container inside the build pod"` + ServiceContainerSecurityContext KubernetesContainerSecurityContext `toml:"service_container_security_context,omitempty" namespace:"service_container_security_context" description:"A security context attached to the service containers inside the build pod"` + Volumes KubernetesVolumes `toml:"volumes"` + HostAliases []KubernetesHostAliases `toml:"host_aliases,omitempty" json:"host_aliases" long:"host_aliases" description:"Add a custom host-to-IP mapping"` + Services []Service `toml:"services,omitempty" json:"services" description:"Add service that is started with container"` + CapAdd []string `toml:"cap_add" json:"cap_add" long:"cap-add" env:"KUBERNETES_CAP_ADD" description:"Add Linux capabilities"` + CapDrop []string `toml:"cap_drop" json:"cap_drop" long:"cap-drop" env:"KUBERNETES_CAP_DROP" description:"Drop Linux capabilities"` + DNSPolicy KubernetesDNSPolicy `toml:"dns_policy,omitempty" json:"dns_policy" long:"dns-policy" env:"KUBERNETES_DNS_POLICY" description:"How Kubernetes should try to resolve DNS from the created pods. If unset, Kubernetes will use the default 'ClusterFirst'. Valid values are: none, default, cluster-first, cluster-first-with-host-net"` + DNSConfig KubernetesDNSConfig `toml:"dns_config" json:"dns_config" description:"Pod DNS config"` + ContainerLifecycle KubernetesContainerLifecyle `toml:"container_lifecycle,omitempty" json:"container_lifecycle,omitempty" description:"Actions that the management system should take in response to container lifecycle events"` } //nolint:lll @@ -442,6 +445,123 @@ type KubernetesPodSecurityContext struct { SupplementalGroups []int64 `toml:"supplemental_groups,omitempty" long:"supplemental-groups" description:"A list of groups applied to the first process run in each container, in addition to the container's primary GID"` } +//nolint:lll +type KubernetesContainerCapabilities struct { + Add []api.Capability `toml:"add" long:"add" env:"@ADD" description:"List of capabilities to add to the build container"` + Drop []api.Capability `toml:"drop" long:"drop" env:"@DROP" description:"List of capabilities to drop from the build container"` +} + +//nolint:lll +type KubernetesContainerSecurityContext struct { + Capabilities *KubernetesContainerCapabilities `toml:"capabilities,omitempty" namespace:"capabilities" description:"The capabilities to add/drop when running the container"` + Privileged *bool `toml:"privileged" long:"privileged" env:"@PRIVILEGED" description:"Run container in privileged mode"` + RunAsUser *int64 `toml:"run_as_user,omitempty" long:"run-as-user" env:"@RUN_AS_USER" description:"The UID to run the entrypoint of the container process"` + RunAsGroup *int64 `toml:"run_as_group,omitempty" long:"run-as-group" env:"@RUN_AS_GROUP" description:"The GID to run the entrypoint of the container process"` + RunAsNonRoot *bool `toml:"run_as_non_root,omitempty" long:"run-as-non-root" env:"@RUN_AS_NON_ROOT" description:"Indicates that the container must run as a non-root user"` + ReadOnlyRootFilesystem *bool `toml:"read_only_root_filesystem" long:"read-only-root-filesystem" env:"@READ_ONLY_ROOT_FILESYSTEM" description:" Whether this container has a read-only root filesystem."` + AllowPrivilegeEscalation *bool `toml:"allow_privilege_escalation" long:"allow-privilege-escalation" env:"@ALLOW_PRIVILEGE_ESCALATION" description:"AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process"` +} + +func (c *KubernetesConfig) getCapabilities(defaultCapDrop []string) *api.Capabilities { + enabled := make(map[string]bool) + + for _, v := range defaultCapDrop { + enabled[v] = false + } + + for _, v := range c.CapAdd { + enabled[v] = true + } + + for _, v := range c.CapDrop { + enabled[v] = false + } + + if len(enabled) < 1 { + return nil + } + + return buildCapabilities(enabled) +} + +func buildCapabilities(enabled map[string]bool) *api.Capabilities { + capabilities := new(api.Capabilities) + + for c, add := range enabled { + if add { + capabilities.Add = append(capabilities.Add, api.Capability(c)) + continue + } + capabilities.Drop = append(capabilities.Drop, api.Capability(c)) + } + + return capabilities +} + +func (c *KubernetesConfig) GetContainerSecurityContext( + securityContext KubernetesContainerSecurityContext, + defaultCapDrop ...string, +) *api.SecurityContext { + return &api.SecurityContext{ + Capabilities: mergeCapabilitiesAddDrop( + c.getCapabilities(defaultCapDrop), + securityContext.getCapabilities(), + ), + Privileged: getContainerSecurityContextEffectiveFlagValue(securityContext.Privileged, c.Privileged), + AllowPrivilegeEscalation: getContainerSecurityContextEffectiveFlagValue( + securityContext.AllowPrivilegeEscalation, + c.AllowPrivilegeEscalation, + ), + RunAsGroup: securityContext.RunAsGroup, + RunAsNonRoot: securityContext.RunAsNonRoot, + RunAsUser: securityContext.RunAsUser, + ReadOnlyRootFilesystem: securityContext.ReadOnlyRootFilesystem, + } +} + +func mergeCapabilitiesAddDrop(capabilities ...*api.Capabilities) *api.Capabilities { + merged := &api.Capabilities{} + for _, c := range capabilities { + if c == nil { + continue + } + + if c.Add != nil { + merged.Add = c.Add + } + + if c.Drop != nil { + merged.Drop = c.Drop + } + } + + if merged.Add == nil && merged.Drop == nil { + return nil + } + + return merged +} + +func getContainerSecurityContextEffectiveFlagValue(containerValue, fallbackValue *bool) *bool { + if containerValue == nil { + return fallbackValue + } + + return containerValue +} + +func (c *KubernetesContainerSecurityContext) getCapabilities() *api.Capabilities { + capabilities := c.Capabilities + if capabilities == nil { + return nil + } + + return &api.Capabilities{ + Add: capabilities.Add, + Drop: capabilities.Drop, + } +} + //nolint:lll type KubernetesAffinity struct { NodeAffinity *KubernetesNodeAffinity `toml:"node_affinity,omitempty" json:"node_affinity" long:"node-affinity" description:"Node affinity is conceptually similar to nodeSelector -- it allows you to constrain which nodes your pod is eligible to be scheduled on, based on labels on the node."` diff --git a/common/config_test.go b/common/config_test.go index 2b36ee702713f8b66676eebaeed25dc2dc0dac53..66d1528cafa5f2daa06dff5207f5ca7ea05ee0a2 100644 --- a/common/config_test.go +++ b/common/config_test.go @@ -1306,6 +1306,351 @@ func TestRunnerSettings_IsFeatureFlagOn(t *testing.T) { } } +func TestEffectivePrivilege(t *testing.T) { + tests := map[string]struct { + pod bool + container bool + expected bool + }{ + "pod and container privileged": { + pod: true, + container: true, + expected: true, + }, + "pod privileged": { + pod: true, + container: false, + expected: false, + }, + "container privileged": { + pod: false, + container: true, + expected: true, + }, + "all unprivileged": { + pod: false, + container: false, + expected: false, + }, + } + + for tn, tt := range tests { + t.Run(tn, func(t *testing.T) { + effectivePrivileged := getContainerSecurityContextEffectiveFlagValue(&tt.container, &tt.pod) + require.NotNil(t, effectivePrivileged) + assert.Equal(t, tt.expected, *effectivePrivileged) + }) + } +} + +func TestContainerSecurityContext(t *testing.T) { + boolPtr := func(v bool) *bool { + return &v + } + + int64Ptr := func(v int64) *int64 { + return &v + } + + tests := map[string]struct { + getSecurityContext func(c *KubernetesConfig) *api.SecurityContext + getExpectedContainerSecurityContext func() *api.SecurityContext + }{ + "no container security context": { + getSecurityContext: func(c *KubernetesConfig) *api.SecurityContext { + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{}) + }, + getExpectedContainerSecurityContext: func() *api.SecurityContext { + return &api.SecurityContext{} + }, + }, + "run as user - container security context": { + getSecurityContext: func(c *KubernetesConfig) *api.SecurityContext { + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{ + RunAsUser: int64Ptr(1000), + }) + }, + getExpectedContainerSecurityContext: func() *api.SecurityContext { + runAsUser := int64(1000) + return &api.SecurityContext{ + RunAsUser: &runAsUser, + } + }, + }, + "privileged - container security context": { + getSecurityContext: func(c *KubernetesConfig) *api.SecurityContext { + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{ + Privileged: boolPtr(true), + }) + }, + getExpectedContainerSecurityContext: func() *api.SecurityContext { + return &api.SecurityContext{ + Privileged: boolPtr(true), + } + }, + }, + "container privileged override - container security context": { + getSecurityContext: func(c *KubernetesConfig) *api.SecurityContext { + c.Privileged = boolPtr(true) + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{ + Privileged: boolPtr(false), + RunAsUser: int64Ptr(65535), + }) + }, + getExpectedContainerSecurityContext: func() *api.SecurityContext { + runAsUser := int64(65535) + return &api.SecurityContext{ + Privileged: boolPtr(false), + RunAsUser: &runAsUser, + } + }, + }, + "allow privilege escalation - not set on container security context": { + getSecurityContext: func(c *KubernetesConfig) *api.SecurityContext { + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{ + AllowPrivilegeEscalation: boolPtr(true), + }) + }, + getExpectedContainerSecurityContext: func() *api.SecurityContext { + return &api.SecurityContext{ + AllowPrivilegeEscalation: boolPtr(true), + } + }, + }, + "allow privilege escalation - set on container security context": { + getSecurityContext: func(c *KubernetesConfig) *api.SecurityContext { + c.AllowPrivilegeEscalation = boolPtr(true) + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{ + AllowPrivilegeEscalation: boolPtr(false), + }) + }, + getExpectedContainerSecurityContext: func() *api.SecurityContext { + return &api.SecurityContext{ + AllowPrivilegeEscalation: boolPtr(false), + } + }, + }, + } + + for tn, tt := range tests { + t.Run(tn, func(t *testing.T) { + config := new(KubernetesConfig) + scExpected := tt.getExpectedContainerSecurityContext() + scActual := tt.getSecurityContext(config) + assert.Equal(t, scExpected, scActual) + }) + } +} + +func TestContainerSecurityCapabilities(t *testing.T) { + tests := map[string]struct { + getCapabilitiesFn func(c *KubernetesConfig) *api.Capabilities + expectedCapabilities *api.Capabilities + }{ + "container add": { + getCapabilitiesFn: func(c *KubernetesConfig) *api.Capabilities { + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{ + Capabilities: &KubernetesContainerCapabilities{ + Add: []api.Capability{"SYS_TIME"}, + }, + }).Capabilities + }, + expectedCapabilities: &api.Capabilities{ + Add: []api.Capability{"SYS_TIME"}, + Drop: nil, + }, + }, + "container drop": { + getCapabilitiesFn: func(c *KubernetesConfig) *api.Capabilities { + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{ + Capabilities: &KubernetesContainerCapabilities{ + Drop: []api.Capability{"SYS_TIME"}, + }, + }).Capabilities + }, + expectedCapabilities: &api.Capabilities{ + Add: nil, + Drop: []api.Capability{"SYS_TIME"}, + }, + }, + "container add and drop": { + getCapabilitiesFn: func(c *KubernetesConfig) *api.Capabilities { + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{ + Capabilities: &KubernetesContainerCapabilities{ + Add: []api.Capability{"SYS_TIME"}, + Drop: []api.Capability{"SYS_TIME"}, + }, + }).Capabilities + }, + expectedCapabilities: &api.Capabilities{ + Add: []api.Capability{"SYS_TIME"}, + Drop: []api.Capability{"SYS_TIME"}, + }, + }, + "container empty": { + getCapabilitiesFn: func(c *KubernetesConfig) *api.Capabilities { + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{}).Capabilities + }, + }, + "container when capAdd and capDrop exist": { + getCapabilitiesFn: func(c *KubernetesConfig) *api.Capabilities { + c.CapAdd = []string{"add"} + c.CapDrop = []string{"drop"} + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{}).Capabilities + }, + expectedCapabilities: &api.Capabilities{ + Add: []api.Capability{"add"}, + Drop: []api.Capability{"drop"}, + }, + }, + "container when capAdd and container capabilities exist": { + getCapabilitiesFn: func(c *KubernetesConfig) *api.Capabilities { + c.CapAdd = []string{"add"} + c.CapDrop = []string{"drop"} + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{ + Capabilities: &KubernetesContainerCapabilities{ + Add: []api.Capability{"add container"}, + }, + }).Capabilities + }, + expectedCapabilities: &api.Capabilities{ + Add: []api.Capability{"add container"}, + Drop: []api.Capability{"drop"}, + }, + }, + "container when capDrop and container capabilities exist": { + getCapabilitiesFn: func(c *KubernetesConfig) *api.Capabilities { + c.CapAdd = []string{"add"} + c.CapDrop = []string{"drop"} + return c.GetContainerSecurityContext(KubernetesContainerSecurityContext{ + Capabilities: &KubernetesContainerCapabilities{ + Drop: []api.Capability{"drop container"}, + }, + }).Capabilities + }, + expectedCapabilities: &api.Capabilities{ + Add: []api.Capability{"add"}, + Drop: []api.Capability{"drop container"}, + }, + }, + } + + for tn, tt := range tests { + t.Run(tn, func(t *testing.T) { + config := new(KubernetesConfig) + c := tt.getCapabilitiesFn(config) + assert.Equal(t, tt.expectedCapabilities, c) + }) + } +} + +func TestGetCapabilities(t *testing.T) { + tests := map[string]struct { + defaultCapDrop []string + capAdd []string + capDrop []string + assertCapabilities func(t *testing.T, a *api.Capabilities) + }{ + "no data provided": { + assertCapabilities: func(t *testing.T, a *api.Capabilities) { + assert.Nil(t, a) + }, + }, + "only default_cap_drop provided": { + defaultCapDrop: []string{"CAP_1", "CAP_2"}, + assertCapabilities: func(t *testing.T, a *api.Capabilities) { + require.NotNil(t, a) + assert.Empty(t, a.Add) + assert.Len(t, a.Drop, 2) + assert.Contains(t, a.Drop, api.Capability("CAP_1")) + assert.Contains(t, a.Drop, api.Capability("CAP_2")) + }, + }, + "only custom cap_add provided": { + capAdd: []string{"CAP_1", "CAP_2"}, + assertCapabilities: func(t *testing.T, a *api.Capabilities) { + require.NotNil(t, a) + assert.Len(t, a.Add, 2) + assert.Contains(t, a.Add, api.Capability("CAP_1")) + assert.Contains(t, a.Add, api.Capability("CAP_2")) + assert.Empty(t, a.Drop) + }, + }, + "only custom cap_drop provided": { + capDrop: []string{"CAP_1", "CAP_2"}, + assertCapabilities: func(t *testing.T, a *api.Capabilities) { + require.NotNil(t, a) + assert.Empty(t, a.Add) + assert.Len(t, a.Drop, 2) + assert.Contains(t, a.Drop, api.Capability("CAP_1")) + assert.Contains(t, a.Drop, api.Capability("CAP_2")) + }, + }, + "default_cap_drop and custom cap_drop sums": { + defaultCapDrop: []string{"CAP_1", "CAP_2"}, + capDrop: []string{"CAP_3", "CAP_4"}, + assertCapabilities: func(t *testing.T, a *api.Capabilities) { + require.NotNil(t, a) + assert.Empty(t, a.Add) + assert.Len(t, a.Drop, 4) + assert.Contains(t, a.Drop, api.Capability("CAP_1")) + assert.Contains(t, a.Drop, api.Capability("CAP_2")) + assert.Contains(t, a.Drop, api.Capability("CAP_3")) + assert.Contains(t, a.Drop, api.Capability("CAP_4")) + }, + }, + "default_cap_drop and custom cap_drop duplicate": { + defaultCapDrop: []string{"CAP_1", "CAP_2"}, + capDrop: []string{"CAP_2", "CAP_3"}, + assertCapabilities: func(t *testing.T, a *api.Capabilities) { + require.NotNil(t, a) + assert.Empty(t, a.Add) + assert.Len(t, a.Drop, 3) + assert.Contains(t, a.Drop, api.Capability("CAP_1")) + assert.Contains(t, a.Drop, api.Capability("CAP_2")) + assert.Contains(t, a.Drop, api.Capability("CAP_3")) + }, + }, + "default_cap_drop and custom cap_add intersect": { + defaultCapDrop: []string{"CAP_1", "CAP_2"}, + capAdd: []string{"CAP_2", "CAP_3"}, + assertCapabilities: func(t *testing.T, a *api.Capabilities) { + require.NotNil(t, a) + assert.Len(t, a.Add, 2) + assert.Contains(t, a.Add, api.Capability("CAP_2")) + assert.Contains(t, a.Add, api.Capability("CAP_3")) + assert.Len(t, a.Drop, 1) + assert.Contains(t, a.Drop, api.Capability("CAP_1")) + }, + }, + "default_cap_drop and custom cap_add intersect and cap_drop forces": { + defaultCapDrop: []string{"CAP_1", "CAP_2"}, + capAdd: []string{"CAP_2", "CAP_3"}, + capDrop: []string{"CAP_2", "CAP_4"}, + assertCapabilities: func(t *testing.T, a *api.Capabilities) { + require.NotNil(t, a) + assert.Len(t, a.Add, 1) + assert.Contains(t, a.Add, api.Capability("CAP_3")) + assert.Len(t, a.Drop, 3) + assert.Contains(t, a.Drop, api.Capability("CAP_1")) + assert.Contains(t, a.Drop, api.Capability("CAP_2")) + assert.Contains(t, a.Drop, api.Capability("CAP_4")) + }, + }, + } + + for tn, tt := range tests { + t.Run(tn, func(t *testing.T) { + c := KubernetesConfig{ + CapAdd: tt.capAdd, + CapDrop: tt.capDrop, + } + + tt.assertCapabilities(t, c.getCapabilities(tt.defaultCapDrop)) + }) + } +} + func TestKubernetesTerminationPeriod(t *testing.T) { tests := map[string]struct { cfg KubernetesConfig diff --git a/docs/executors/kubernetes.md b/docs/executors/kubernetes.md index 365b32bdff1b8470663dd2b7863a9c922e0257aa..1947e5c6a559517003a9ed0b43983211c2048396 100644 --- a/docs/executors/kubernetes.md +++ b/docs/executors/kubernetes.md @@ -165,6 +165,9 @@ The following settings help to define the behavior of GitLab Runner within Kuber | `pod_annotations_overwrite_allowed` | Regular expression to validate the contents of the pod annotations overwrite environment variable. When empty, it disables the pod annotations overwrite feature. | | `pod_labels` | A set of labels to be added to each build pod created by the runner. The value of these can include environment variables for expansion. | | `pod_security_context` | Configured through the configuration file, this sets a pod security context for the build pod. [Read more about security context](#using-security-context). | +| `build_container_security_context` | Sets a container security context for the build container. [Read more about security context](#using-security-context). | +| `helper_container_security_context` | Sets a container security context for the helper container. [Read more about security context](#using-security-context). | +| `service_container_security_context` | Sets a container security context for the service containers. [Read more about security context](#using-security-context). | | `pod_termination_grace_period_seconds` | Pod-level setting which determines the duration in seconds which the pod has to terminate gracefully. After this, the processes are forcibly halted with a kill signal. Ignored if `terminationGracePeriodSeconds` is specified. | | `poll_interval` | How frequently, in seconds, the runner will poll the Kubernetes pod it has just created to check its status (default = 3). | | `poll_timeout` | The amount of time, in seconds, that needs to pass before the runner will time out attempting to connect to the container it has just created. Useful for queueing more builds that the cluster can handle at a time (default = 180). | @@ -516,9 +519,9 @@ concurrent = 4 | Option | Type | Required | Description | |----------------------|------------|----------|-------------| | `fs_group` | `int` | No | A special supplemental group that applies to all containers in a pod. | -| `run_as_group` | `int` | No | The GID to run the entrypoint of the container process. | +| `run_as_group` | `int` | No | The GID to run the entry point of the container process. | | `run_as_non_root` | boolean | No | Indicates that the container must run as a non-root user. | -| `run_as_user` | `int` | No | The UID to run the entrypoint of the container process. | +| `run_as_user` | `int` | No | The UID to run the entry point of the container process. | | `supplemental_groups`| `int` list | No | A list of groups applied to the first process run in each container, in addition to the container's primary GID. | Assigning a security context to pods provides security to your Kubernetes cluster. For this to work you'll need to provide a helper @@ -553,12 +556,57 @@ check_interval = 30 url = "gitlab.example.com" executor = "kubernetes" [runners.kubernetes] - helper_image = "gitlab-registy.example.com/helper:latest" + helper_image = "gitlab-registry.example.com/helper:latest" + [runners.kubernetes.pod_security_context] + run_as_non_root = true + run_as_user = 59417 + run_as_group = 59417 + fs_group = 59417 +``` + +### Container security context + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/3116) in GitLab Runner 14.5. + +Use the [container security context](https://kubernetes.io/docs/concepts/policy/pod-security-policy/) configuration to configure the executor to set a container security policy on the build, helper, or service pods. + +| Option | Type | Required | Description | +|-----------------------|-------------|----------|-------------| +| `run_as_group` | int | No | The GID to run the entry point of the container process. | +| `run_as_non_root` | boolean | No | Indicates that the container must run as a non-root user. | +| `run_as_user` | int | No | The UID to run the entry point of the container process. | +| `capabilities.add` | string list | No | The capabilities to add when running the container. | +| `capabilities.drop` | string list | No | The capabilities to drop when running the container. | + +The example below sets a pod security context, then overrides `run_as_user` and `run_as_group` for both the build and helper containers. In the example, all service containers would inherit `run_as_user` and `run_as_group` from the pod security context. + +Example of setting pod security context in your `config.toml`: + +```toml +concurrent = 4 +check_interval = 30 + [[runners]] + name = "myRunner" + url = "gitlab.example.com" + executor = "kubernetes" + [runners.kubernetes] + helper_image = "gitlab-registry.example.com/helper:latest" [runners.kubernetes.pod_security_context] run_as_non_root = true run_as_user = 59417 run_as_group = 59417 fs_group = 59417 + [runners.kubernetes.build_container_security_context] + run_as_user = 65534 + run_as_group = 65534 + [runners.kubernetes.build_container_security_context.capabilities] + add = ["NET_ADMIN"] + [runners.kubernetes.helper_container_security_context] + run_as_user = 1000 + run_as_group = 1000 + [runners.kubernetes.service_container_security_context] + run_as_user = 1000 + run_as_group = 1000 ``` ## Using services diff --git a/executors/kubernetes/kubernetes.go b/executors/kubernetes/kubernetes.go index dafd37fa11bebec1e41df3d7ce71ff502d2318bd..7e1ab64115d09fa2087cd8db86ad3a08ca84b223 100644 --- a/executors/kubernetes/kubernetes.go +++ b/executors/kubernetes/kubernetes.go @@ -75,21 +75,6 @@ var ( errIncorrectShellType = fmt.Errorf("kubernetes executor incorrect shell type") ) -// GetDefaultCapDrop returns the default capabilities that should be dropped -// from a build container. -func GetDefaultCapDrop(os string) []string { - // windows does not support security context capabilities - if os == helperimage.OSTypeWindows { - return nil - } - - return []string{ - // Reasons for disabling NET_RAW by default were - // discussed in https://gitlab.com/gitlab-org/gitlab-runner/-/issues/26833 - "NET_RAW", - } -} - type commandTerminatedError struct { exitCode int } @@ -117,6 +102,16 @@ type kubernetesOptions struct { Services common.Services } +type containerBuildOpts struct { + name string + image string + imageDefinition common.Image + requests api.ResourceList + limits api.ResourceList + securityContext *api.SecurityContext + command []string +} + type executor struct { executors.AbstractExecutor @@ -691,12 +686,7 @@ func (s *executor) cleanupResources() { } //nolint:funlen -func (s *executor) buildContainer( - name, image string, - imageDefinition common.Image, - requests, limits api.ResourceList, - containerCommand ...string, -) (api.Container, error) { +func (s *executor) buildContainer(opts containerBuildOpts) (api.Container, error) { // check if the image/service is allowed internalImages := []string{ s.Config.Kubernetes.Image, @@ -707,16 +697,16 @@ func (s *executor) buildContainer( optionName string allowedImages []string ) - if strings.HasPrefix(name, "svc-") { + if strings.HasPrefix(opts.name, "svc-") { optionName = "services" allowedImages = s.Config.Kubernetes.AllowedServices - } else if name == buildContainerName { + } else if opts.name == buildContainerName { optionName = "images" allowedImages = s.Config.Kubernetes.AllowedImages } verifyAllowedImageOptions := common.VerifyAllowedImageOptions{ - Image: image, + Image: opts.image, OptionName: optionName, AllowedImages: allowedImages, InternalImages: internalImages, @@ -726,58 +716,50 @@ func (s *executor) buildContainer( return api.Container{}, err } - containerPorts := make([]api.ContainerPort, len(imageDefinition.Ports)) - proxyPorts := make([]proxy.Port, len(imageDefinition.Ports)) + containerPorts := make([]api.ContainerPort, len(opts.imageDefinition.Ports)) + proxyPorts := make([]proxy.Port, len(opts.imageDefinition.Ports)) - for i, port := range imageDefinition.Ports { + for i, port := range opts.imageDefinition.Ports { proxyPorts[i] = proxy.Port{Name: port.Name, Number: port.Number, Protocol: port.Protocol} containerPorts[i] = api.ContainerPort{ContainerPort: int32(port.Number)} } if len(proxyPorts) > 0 { - serviceName := imageDefinition.Alias + serviceName := opts.imageDefinition.Alias if serviceName == "" { - serviceName = name - if name != buildContainerName { - serviceName = fmt.Sprintf("proxy-%s", name) + serviceName = opts.name + if opts.name != buildContainerName { + serviceName = fmt.Sprintf("proxy-%s", opts.name) } } s.ProxyPool[serviceName] = s.newProxy(serviceName, proxyPorts) } - pullPolicy, err := s.pullManager.GetPullPolicyFor(image) + pullPolicy, err := s.pullManager.GetPullPolicyFor(opts.image) if err != nil { return api.Container{}, err } - command, args := s.getCommandAndArgs(imageDefinition, containerCommand...) + command, args := s.getCommandAndArgs(opts.imageDefinition, opts.command...) container := api.Container{ - Name: name, - Image: image, + Name: opts.name, + Image: opts.image, ImagePullPolicy: pullPolicy, Command: command, Args: args, Env: buildVariables(s.Build.GetAllVariables().PublicOrInternal()), Resources: api.ResourceRequirements{ - Limits: limits, - Requests: requests, + Limits: opts.limits, + Requests: opts.requests, }, - Ports: containerPorts, - VolumeMounts: s.getVolumeMounts(), - SecurityContext: &api.SecurityContext{ - Privileged: s.Config.Kubernetes.Privileged, - AllowPrivilegeEscalation: s.Config.Kubernetes.AllowPrivilegeEscalation, - Capabilities: getCapabilities( - GetDefaultCapDrop(s.helperImageInfo.OSType), - s.Config.Kubernetes.CapAdd, - s.Config.Kubernetes.CapDrop, - ), - }, - Lifecycle: s.prepareLifecycleHooks(), - Stdin: true, + Ports: containerPorts, + VolumeMounts: s.getVolumeMounts(), + SecurityContext: opts.securityContext, + Lifecycle: s.prepareLifecycleHooks(), + Stdin: true, } return container, nil @@ -1229,13 +1211,17 @@ func (s *executor) setupBuildPod(initContainers []api.Container) error { for i, service := range s.options.Services { resolvedImage := s.Build.GetAllVariables().ExpandValue(service.Name) var err error - podServices[i], err = s.buildContainer( - fmt.Sprintf("svc-%d", i), - resolvedImage, - service, - s.configurationOverwrites.serviceRequests, - s.configurationOverwrites.serviceLimits, - ) + podServices[i], err = s.buildContainer(containerBuildOpts{ + name: fmt.Sprintf("svc-%d", i), + image: resolvedImage, + imageDefinition: service, + requests: s.configurationOverwrites.serviceRequests, + limits: s.configurationOverwrites.serviceLimits, + securityContext: s.Config.Kubernetes.GetContainerSecurityContext( + s.Config.Kubernetes.ServiceContainerSecurityContext, + s.defaultCapDrop()..., + ), + }) if err != nil { return err } @@ -1300,6 +1286,20 @@ func (s *executor) setupBuildPod(initContainers []api.Container) error { return nil } +func (s *executor) defaultCapDrop() []string { + os := s.helperImageInfo.OSType + // windows does not support security context capabilities + if os == helperimage.OSTypeWindows { + return nil + } + + return []string{ + // Reasons for disabling NET_RAW by default were + // discussed in https://gitlab.com/gitlab-org/gitlab-runner/-/issues/26833 + "NET_RAW", + } +} + //nolint:funlen func (s *executor) preparePodConfig( labels, annotations map[string]string, @@ -1308,34 +1308,39 @@ func (s *executor) preparePodConfig( hostAliases []api.HostAlias, initContainers []api.Container, ) (api.Pod, error) { - buildImage := s.Build.GetAllVariables().ExpandValue(s.options.Image.Name) - dockerCmdForBuildContainer := s.BuildShell.DockerCommand if s.Build.IsFeatureFlagOn(featureflags.KubernetesHonorEntrypoint) && !s.Build.IsFeatureFlagOn(featureflags.UseLegacyKubernetesExecutionStrategy) { dockerCmdForBuildContainer = []string{} } - buildContainer, err := s.buildContainer( - buildContainerName, - buildImage, - s.options.Image, - s.configurationOverwrites.buildRequests, - s.configurationOverwrites.buildLimits, - dockerCmdForBuildContainer..., - ) + buildContainer, err := s.buildContainer(containerBuildOpts{ + name: buildContainerName, + image: s.Build.GetAllVariables().ExpandValue(s.options.Image.Name), + imageDefinition: s.options.Image, + requests: s.configurationOverwrites.buildRequests, + limits: s.configurationOverwrites.buildLimits, + securityContext: s.Config.Kubernetes.GetContainerSecurityContext( + s.Config.Kubernetes.BuildContainerSecurityContext, + s.defaultCapDrop()..., + ), + command: dockerCmdForBuildContainer, + }) if err != nil { return api.Pod{}, fmt.Errorf("building build container: %w", err) } - helperContainer, err := s.buildContainer( - helperContainerName, - s.getHelperImage(), - common.Image{}, - s.configurationOverwrites.helperRequests, - s.configurationOverwrites.helperLimits, - s.BuildShell.DockerCommand..., - ) + helperContainer, err := s.buildContainer(containerBuildOpts{ + name: helperContainerName, + image: s.getHelperImage(), + requests: s.configurationOverwrites.helperRequests, + limits: s.configurationOverwrites.helperLimits, + securityContext: s.Config.Kubernetes.GetContainerSecurityContext( + s.Config.Kubernetes.HelperContainerSecurityContext, + s.defaultCapDrop()..., + ), + command: s.BuildShell.DockerCommand, + }) if err != nil { return api.Pod{}, fmt.Errorf("building helper container: %w", err) } diff --git a/executors/kubernetes/kubernetes_test.go b/executors/kubernetes/kubernetes_test.go index a44cf6b0eec88e627dd370de35fc291ef6ccaaa3..5fe6df2132bb97a6f1a8304fcf038b381727079c 100644 --- a/executors/kubernetes/kubernetes_test.go +++ b/executors/kubernetes/kubernetes_test.go @@ -4743,3 +4743,60 @@ func TestLifecyclePrepare(t *testing.T) { }) } } + +func TestBuildContainerSecurityContext(t *testing.T) { + tests := map[string]struct { + getSecurityContext func() *api.SecurityContext + }{ + "build security context": { + getSecurityContext: func() *api.SecurityContext { + runAsNonRoot := true + readOnlyRootFileSystem := true + privileged := false + allowPrivilageEscalation := false + var uid int64 = 1000 + var gid int64 = 1000 + return &api.SecurityContext{ + RunAsNonRoot: &runAsNonRoot, + ReadOnlyRootFilesystem: &readOnlyRootFileSystem, + Privileged: &privileged, + AllowPrivilegeEscalation: &allowPrivilageEscalation, + RunAsUser: &uid, + RunAsGroup: &gid, + Capabilities: &api.Capabilities{ + Drop: []api.Capability{"ALL"}, + }, + } + }, + }, + "no security context": { + getSecurityContext: func() *api.SecurityContext { + return nil + }, + }, + } + + mockPullManager := &pull.MockManager{} + mockPullManager.On("GetPullPolicyFor", mock.Anything). + Return(api.PullAlways, nil). + Times(2) + + defer mockPullManager.AssertExpectations(t) + + executor := newExecutor() + executor.pullManager = mockPullManager + executor.Build = new(common.Build) + executor.Config.Kubernetes = new(common.KubernetesConfig) + + for tn, tt := range tests { + t.Run(tn, func(t *testing.T) { + opts := containerBuildOpts{ + name: buildContainerName, + securityContext: tt.getSecurityContext(), + } + container, err := executor.buildContainer(opts) + require.NoError(t, err) + assert.Equal(t, tt.getSecurityContext(), container.SecurityContext) + }) + } +} diff --git a/executors/kubernetes/util.go b/executors/kubernetes/util.go index 904ecf94d31fc15832b5cf8c99a2bb7d530a8af2..86a140eea9be0b568c76bf4437f8a173d4fb773c 100644 --- a/executors/kubernetes/util.go +++ b/executors/kubernetes/util.go @@ -309,42 +309,6 @@ func buildVariables(bv common.JobVariables) []api.EnvVar { return e } -func getCapabilities(defaultCapDrop []string, capAdd []string, capDrop []string) *api.Capabilities { - enabled := make(map[string]bool) - - for _, v := range defaultCapDrop { - enabled[v] = false - } - - for _, v := range capAdd { - enabled[v] = true - } - - for _, v := range capDrop { - enabled[v] = false - } - - if len(enabled) < 1 { - return nil - } - - return buildCapabilities(enabled) -} - -func buildCapabilities(enabled map[string]bool) *api.Capabilities { - capabilities := new(api.Capabilities) - - for c, add := range enabled { - if add { - capabilities.Add = append(capabilities.Add, api.Capability(c)) - continue - } - capabilities.Drop = append(capabilities.Drop, api.Capability(c)) - } - - return capabilities -} - // Sanitize labels to match Kubernetes restrictions from https://kubernetes.io/ // /docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set //nolint:gocognit diff --git a/executors/kubernetes/util_test.go b/executors/kubernetes/util_test.go index 9fefcd4ec506023024e1c67bb6549de5942e52d7..b103f8420dff6a05de50b4383de866ade07540a0 100644 --- a/executors/kubernetes/util_test.go +++ b/executors/kubernetes/util_test.go @@ -503,108 +503,6 @@ func testVersionAndCodec() (version string, codec runtime.Codec) { return } -func TestGetCapabilities(t *testing.T) { - tests := map[string]struct { - defaultCapDrop []string - capAdd []string - capDrop []string - assertCapabilities func(t *testing.T, a *api.Capabilities) - }{ - "no data provided": { - assertCapabilities: func(t *testing.T, a *api.Capabilities) { - assert.Nil(t, a) - }, - }, - "only default_cap_drop provided": { - defaultCapDrop: []string{"CAP_1", "CAP_2"}, - assertCapabilities: func(t *testing.T, a *api.Capabilities) { - require.NotNil(t, a) - assert.Empty(t, a.Add) - assert.Len(t, a.Drop, 2) - assert.Contains(t, a.Drop, api.Capability("CAP_1")) - assert.Contains(t, a.Drop, api.Capability("CAP_2")) - }, - }, - "only custom cap_add provided": { - capAdd: []string{"CAP_1", "CAP_2"}, - assertCapabilities: func(t *testing.T, a *api.Capabilities) { - require.NotNil(t, a) - assert.Len(t, a.Add, 2) - assert.Contains(t, a.Add, api.Capability("CAP_1")) - assert.Contains(t, a.Add, api.Capability("CAP_2")) - assert.Empty(t, a.Drop) - }, - }, - "only custom cap_drop provided": { - capDrop: []string{"CAP_1", "CAP_2"}, - assertCapabilities: func(t *testing.T, a *api.Capabilities) { - require.NotNil(t, a) - assert.Empty(t, a.Add) - assert.Len(t, a.Drop, 2) - assert.Contains(t, a.Drop, api.Capability("CAP_1")) - assert.Contains(t, a.Drop, api.Capability("CAP_2")) - }, - }, - "default_cap_drop and custom cap_drop sums": { - defaultCapDrop: []string{"CAP_1", "CAP_2"}, - capDrop: []string{"CAP_3", "CAP_4"}, - assertCapabilities: func(t *testing.T, a *api.Capabilities) { - require.NotNil(t, a) - assert.Empty(t, a.Add) - assert.Len(t, a.Drop, 4) - assert.Contains(t, a.Drop, api.Capability("CAP_1")) - assert.Contains(t, a.Drop, api.Capability("CAP_2")) - assert.Contains(t, a.Drop, api.Capability("CAP_3")) - assert.Contains(t, a.Drop, api.Capability("CAP_4")) - }, - }, - "default_cap_drop and custom cap_drop duplicate": { - defaultCapDrop: []string{"CAP_1", "CAP_2"}, - capDrop: []string{"CAP_2", "CAP_3"}, - assertCapabilities: func(t *testing.T, a *api.Capabilities) { - require.NotNil(t, a) - assert.Empty(t, a.Add) - assert.Len(t, a.Drop, 3) - assert.Contains(t, a.Drop, api.Capability("CAP_1")) - assert.Contains(t, a.Drop, api.Capability("CAP_2")) - assert.Contains(t, a.Drop, api.Capability("CAP_3")) - }, - }, - "default_cap_drop and custom cap_add intersect": { - defaultCapDrop: []string{"CAP_1", "CAP_2"}, - capAdd: []string{"CAP_2", "CAP_3"}, - assertCapabilities: func(t *testing.T, a *api.Capabilities) { - require.NotNil(t, a) - assert.Len(t, a.Add, 2) - assert.Contains(t, a.Add, api.Capability("CAP_2")) - assert.Contains(t, a.Add, api.Capability("CAP_3")) - assert.Len(t, a.Drop, 1) - assert.Contains(t, a.Drop, api.Capability("CAP_1")) - }, - }, - "default_cap_drop and custom cap_add intersect and cap_drop forces": { - defaultCapDrop: []string{"CAP_1", "CAP_2"}, - capAdd: []string{"CAP_2", "CAP_3"}, - capDrop: []string{"CAP_2", "CAP_4"}, - assertCapabilities: func(t *testing.T, a *api.Capabilities) { - require.NotNil(t, a) - assert.Len(t, a.Add, 1) - assert.Contains(t, a.Add, api.Capability("CAP_3")) - assert.Len(t, a.Drop, 3) - assert.Contains(t, a.Drop, api.Capability("CAP_1")) - assert.Contains(t, a.Drop, api.Capability("CAP_2")) - assert.Contains(t, a.Drop, api.Capability("CAP_4")) - }, - }, - } - - for tn, tt := range tests { - t.Run(tn, func(t *testing.T) { - tt.assertCapabilities(t, getCapabilities(tt.defaultCapDrop, tt.capAdd, tt.capDrop)) - }) - } -} - func TestSanitizeLabel(t *testing.T) { tests := []struct { Name string