Commit f253ea6a authored by Tomasz Maczukin's avatar Tomasz Maczukin

Merge branch 'master' into 'master'

adding support for kubernetes service account and override on gitlab-ci.yaml

See merge request !554
parents 551d36c9 9dfa302f
This diff is collapsed.
......@@ -6,7 +6,7 @@ possible with the use of the **Kubernetes** executor.
The **Kubernetes** executor, when used with GitLab CI, connects to the Kubernetes
API in the cluster creating a Pod for each GitLab CI Job. This Pod is made
up of, at the very least, a build container and an additional container for each
`service` defined by the GitLab CI yaml. The names for these containers
`service` defined by the GitLab CI yaml. The names for these containers
are as follows:
- The build container is `build`
......@@ -73,6 +73,14 @@ The following keywords help to define the behaviour of the Runner within Kuberne
- `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 timeout 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]
- `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.
- `service-account`: default service account to be used for making kubernetes api calls.
- `service_account_overwrite_allowed`: Regular expression to validate the contents of
the service account overwrite environment variable. When empty,
it disables the service account overwrite feature
### Configuring executor Service Account
You can set the `KUBERNETES_SERVICE_ACCOUNT` environment variable or use `--service-account` flag
### Overwriting Kubernetes Namespace
......@@ -92,6 +100,23 @@ Furthermore, to ensure only designated namespaces will be used during CI runs, i
`namespace_overwrite_allowed` with proper regular expression. When left empty the overwrite behaviour is
disabled.
### Overwriting Kubernetes Default Service Account
Additionally, Kubernetes service account can be overwritten on `.gitlab-ci.yml` file, by using the variable
`KUBERNETES_SERVICE_ACCOUNT_OVERWRITE`.
This approach allow you to specify a service account that is attached to the namespace, usefull when dealing
with complex RBAC configurations.
``` yaml
variables:
KUBERNETES_SERVICE_ACCOUNT_OVERWRITE: ci-service-account
```
usefull when overwritting the namespace and RBAC is setup in the cluster.
To ensure only designated service accounts will be used during CI runs, inform the configuration
`service_account_overwrite_allowed` or set the environment variable `KUBERNETES_SERVICE_ACCOUNT_OVERWRITE_ALLOWED`
with proper regular expression. When left empty the overwrite behaviour is disabled.
## Define keywords in the config toml
Each of the keywords can be defined in the `config.toml` for the gitlab runner.
......@@ -163,7 +188,7 @@ to contact the docker daemon in the other container be sure to include
Do *not* try to use an image that doesn't supply git and add the `GIT_STRATEGY=none`
environment variable for a job that you think doesn't need to do a fetch or clone.
Because Pods are ephemeral and do not keep state of previously run jobs your
checked out code will not exist in both the build and the docker service container.
checked out code will not exist in both the build and the docker service container.
Error's you might run into are things like `could not find git binary` and
the docker service complaining that it cannot follow some symlinks into your
build context because of the missing code.
......
......@@ -40,15 +40,15 @@ type executor struct {
credentials *api.Secret
options *kubernetesOptions
namespaceOverwrite string
buildLimits api.ResourceList
serviceLimits api.ResourceList
helperLimits api.ResourceList
buildRequests api.ResourceList
serviceRequests api.ResourceList
helperRequests api.ResourceList
pullPolicy common.KubernetesPullPolicy
namespaceOverwrite string
serviceAccountOverwrite string
buildLimits api.ResourceList
serviceLimits api.ResourceList
helperLimits api.ResourceList
buildRequests api.ResourceList
serviceRequests api.ResourceList
helperRequests api.ResourceList
pullPolicy common.KubernetesPullPolicy
}
func (s *executor) setupResources() error {
......@@ -79,7 +79,6 @@ func (s *executor) setupResources() error {
if s.helperRequests, err = limits(s.Config.Kubernetes.HelperCPURequest, s.Config.Kubernetes.HelperMemoryRequest); err != nil {
return fmt.Errorf("invalid helper requests specified: %s", err.Error())
}
return nil
}
......@@ -108,6 +107,10 @@ func (s *executor) Prepare(options common.ExecutorPrepareOptions) (err error) {
return err
}
if err = s.overwriteServiceAccount(options.Build); err != nil {
return err
}
s.prepareOptions(options.Build)
if err = s.checkDefaults(); err != nil {
......@@ -261,7 +264,6 @@ func (s *executor) setupBuildPod() error {
}
buildImage := s.Build.GetAllVariables().ExpandValue(s.options.Image)
pod, err := s.kubeClient.Pods(s.Config.Kubernetes.Namespace).Create(&api.Pod{
ObjectMeta: api.ObjectMeta{
GenerateName: s.Build.ProjectUniqueName(),
......@@ -277,8 +279,9 @@ func (s *executor) setupBuildPod() error {
},
},
},
RestartPolicy: api.RestartPolicyNever,
NodeSelector: s.Config.Kubernetes.NodeSelector,
ServiceAccountName: s.Config.Kubernetes.ServiceAccount,
RestartPolicy: api.RestartPolicyNever,
NodeSelector: s.Config.Kubernetes.NodeSelector,
Containers: append([]api.Container{
s.buildContainer("build", buildImage, s.buildRequests, s.buildLimits, s.BuildShell.DockerCommand...),
s.buildContainer("helper", s.Config.Kubernetes.GetHelperImage(), s.helperRequests, s.helperLimits, s.BuildShell.DockerCommand...),
......@@ -388,23 +391,55 @@ func (s *executor) overwriteNamespace(job *common.Build) error {
return nil
}
var err error
var r *regexp.Regexp
if r, err = regexp.Compile(s.Config.Kubernetes.NamespaceOverwriteAllowed); err != nil {
if err := overwriteRegexCheck(s.Config.Kubernetes.NamespaceOverwriteAllowed, s.namespaceOverwrite); err != nil {
return err
}
if match := r.MatchString(s.namespaceOverwrite); !match {
return fmt.Errorf("KUBERNETES_NAMESPACE_OVERWRITE='%s' does not match 'namespace_overwrite_allowed': '%s'",
s.namespaceOverwrite, s.Config.Kubernetes.NamespaceOverwriteAllowed)
}
s.Println("Overwritting configured namespace, from", s.Config.Kubernetes.Namespace, "to", s.namespaceOverwrite)
s.Config.Kubernetes.Namespace = s.namespaceOverwrite
return nil
}
// overwriteSercviceAccount checks for variable in order to overwrite the configured
// service account, as long as it complies to validation regular-expression, when
// expression is empty the overwrite is disabled.
func (s *executor) overwriteServiceAccount(job *common.Build) error {
if s.Config.Kubernetes.ServiceAccountOverwriteAllowed == "" {
s.Debugln("Configuration entry 'service_accunt_overwrite_allowed' is empty, disabling override.")
return nil
}
s.serviceAccountOverwrite = job.Variables.Expand().Get("KUBERNETES_SERVICE_ACCOUNT_OVERWRITE")
if s.serviceAccountOverwrite == "" {
return nil
}
if err := overwriteRegexCheck(s.Config.Kubernetes.ServiceAccountOverwriteAllowed, s.serviceAccountOverwrite); err != nil {
return err
}
s.Println("Overwritting configured ServiceAccount, from", s.Config.Kubernetes.ServiceAccount, "to", s.serviceAccountOverwrite)
s.Config.Kubernetes.ServiceAccount = s.serviceAccountOverwrite
return nil
}
//overwriteRegexCheck check if the regex provided for overwriting a config field matches the
//paramether provided, returns error if doesn't match
func overwriteRegexCheck(regex, value string) error {
var err error
var r *regexp.Regexp
if r, err = regexp.Compile(regex); err != nil {
return err
}
if match := r.MatchString(value); !match {
return fmt.Errorf("Provided value %s does not match regex %s", value, regex)
}
return nil
}
func createFn() common.Executor {
return &executor{
AbstractExecutor: executors.AbstractExecutor{
......
......@@ -224,22 +224,165 @@ func TestPrepare(t *testing.T) {
RunnerConfig: &common.RunnerConfig{
RunnerSettings: common.RunnerSettings{
Kubernetes: &common.KubernetesConfig{
Host: "test-server",
Namespace: "namespace",
NamespaceOverwriteAllowed: "^n.*?e$",
ServiceCPULimit: "100m",
ServiceMemoryLimit: "200Mi",
CPULimit: "1.5",
MemoryLimit: "4Gi",
HelperCPULimit: "50m",
HelperMemoryLimit: "100Mi",
ServiceCPURequest: "99m",
ServiceMemoryRequest: "5Mi",
CPURequest: "1",
MemoryRequest: "1.5Gi",
HelperCPURequest: "0.5m",
HelperMemoryRequest: "42Mi",
Privileged: false,
Host: "test-server",
ServiceAccount: "default",
ServiceAccountOverwriteAllowed: ".*",
ServiceCPULimit: "100m",
ServiceMemoryLimit: "200Mi",
CPULimit: "1.5",
MemoryLimit: "4Gi",
HelperCPULimit: "50m",
HelperMemoryLimit: "100Mi",
ServiceCPURequest: "99m",
ServiceMemoryRequest: "5Mi",
CPURequest: "1",
MemoryRequest: "1.5Gi",
HelperCPURequest: "0.5m",
HelperMemoryRequest: "42Mi",
Privileged: false,
},
},
},
Build: &common.Build{
JobResponse: common.JobResponse{
GitInfo: common.GitInfo{
Sha: "1234567890",
},
Image: common.Image{
Name: "test-image",
},
Variables: []common.JobVariable{
{Key: "KUBERNETES_SERVICE_ACCOUNT_OVERWRITE", Value: "not-default"},
},
},
Runner: &common.RunnerConfig{},
},
Expected: &executor{
options: &kubernetesOptions{
Image: "test-image",
},
serviceAccountOverwrite: "not-default",
serviceLimits: api.ResourceList{
api.ResourceCPU: resource.MustParse("100m"),
api.ResourceMemory: resource.MustParse("200Mi"),
},
buildLimits: api.ResourceList{
api.ResourceCPU: resource.MustParse("1.5"),
api.ResourceMemory: resource.MustParse("4Gi"),
},
helperLimits: api.ResourceList{
api.ResourceCPU: resource.MustParse("50m"),
api.ResourceMemory: resource.MustParse("100Mi"),
},
serviceRequests: api.ResourceList{
api.ResourceCPU: resource.MustParse("99m"),
api.ResourceMemory: resource.MustParse("5Mi"),
},
buildRequests: api.ResourceList{
api.ResourceCPU: resource.MustParse("1"),
api.ResourceMemory: resource.MustParse("1.5Gi"),
},
helperRequests: api.ResourceList{
api.ResourceCPU: resource.MustParse("0.5m"),
api.ResourceMemory: resource.MustParse("42Mi"),
},
},
Error: false,
},
{
GlobalConfig: &common.Config{},
RunnerConfig: &common.RunnerConfig{
RunnerSettings: common.RunnerSettings{
Kubernetes: &common.KubernetesConfig{
Host: "test-server",
ServiceAccount: "default",
ServiceAccountOverwriteAllowed: "allowed-.*",
ServiceCPULimit: "100m",
ServiceMemoryLimit: "200Mi",
CPULimit: "1.5",
MemoryLimit: "4Gi",
HelperCPULimit: "50m",
HelperMemoryLimit: "100Mi",
ServiceCPURequest: "99m",
ServiceMemoryRequest: "5Mi",
CPURequest: "1",
MemoryRequest: "1.5Gi",
HelperCPURequest: "0.5m",
HelperMemoryRequest: "42Mi",
Privileged: false,
},
},
},
Build: &common.Build{
JobResponse: common.JobResponse{
GitInfo: common.GitInfo{
Sha: "1234567890",
},
Image: common.Image{
Name: "test-image",
},
Variables: []common.JobVariable{
{Key: "KUBERNETES_SERVICE_ACCOUNT_OVERWRITE", Value: "not-default"},
},
},
Runner: &common.RunnerConfig{},
},
Expected: &executor{
options: &kubernetesOptions{
Image: "test-image",
},
namespaceOverwrite: "namespacee",
serviceLimits: api.ResourceList{
api.ResourceCPU: resource.MustParse("100m"),
api.ResourceMemory: resource.MustParse("200Mi"),
},
buildLimits: api.ResourceList{
api.ResourceCPU: resource.MustParse("1.5"),
api.ResourceMemory: resource.MustParse("4Gi"),
},
helperLimits: api.ResourceList{
api.ResourceCPU: resource.MustParse("50m"),
api.ResourceMemory: resource.MustParse("100Mi"),
},
serviceRequests: api.ResourceList{
api.ResourceCPU: resource.MustParse("99m"),
api.ResourceMemory: resource.MustParse("5Mi"),
},
buildRequests: api.ResourceList{
api.ResourceCPU: resource.MustParse("1"),
api.ResourceMemory: resource.MustParse("1.5Gi"),
},
helperRequests: api.ResourceList{
api.ResourceCPU: resource.MustParse("0.5m"),
api.ResourceMemory: resource.MustParse("42Mi"),
},
},
Error: true,
},
{
GlobalConfig: &common.Config{},
RunnerConfig: &common.RunnerConfig{
RunnerSettings: common.RunnerSettings{
Kubernetes: &common.KubernetesConfig{
Host: "test-server",
Namespace: "namespace",
ServiceAccount: "default",
ServiceAccountOverwriteAllowed: ".*",
NamespaceOverwriteAllowed: "^n.*?e$",
ServiceCPULimit: "100m",
ServiceMemoryLimit: "200Mi",
CPULimit: "1.5",
MemoryLimit: "4Gi",
HelperCPULimit: "50m",
HelperMemoryLimit: "100Mi",
ServiceCPURequest: "99m",
ServiceMemoryRequest: "5Mi",
CPURequest: "1",
MemoryRequest: "1.5Gi",
HelperCPURequest: "0.5m",
HelperMemoryRequest: "42Mi",
Privileged: false,
},
},
},
......@@ -911,6 +1054,40 @@ func TestOverwriteNamespaceNotMatch(t *testing.T) {
assert.Contains(t, err.Error(), "does not match")
}
func TestOverwriteServiceAccountNotMatch(t *testing.T) {
if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") {
return
}
build := &common.Build{
JobResponse: common.JobResponse{
GitInfo: common.GitInfo{
Sha: "1234567890",
},
Image: common.Image{
Name: "test-image",
},
Variables: []common.JobVariable{
{Key: "KUBERNETES_SERVICE_ACCOUNT_OVERWRITE", Value: "service-account"},
},
},
Runner: &common.RunnerConfig{
RunnerSettings: common.RunnerSettings{
Executor: "kubernetes",
Kubernetes: &common.KubernetesConfig{
ServiceAccountOverwriteAllowed: "^not_a_match$",
},
},
},
SystemInterrupt: make(chan os.Signal, 1),
}
build.Image.Name = "docker:git"
err := build.Run(&common.Config{}, &common.Trace{Writer: os.Stdout})
require.Error(t, err)
assert.Contains(t, err.Error(), "does not match")
}
type FakeReadCloser struct {
io.Reader
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment