Commit 699b9431 authored by Tomasz Maczukin's avatar Tomasz Maczukin

Merge branch 'improvement/allow-kubernetes-namespace-overwrite' into 'master'

Kubernetes Namespace Overwrite

See merge request !444
parents 01bedbe3 811b939d
......@@ -123,6 +123,7 @@ type KubernetesConfig struct {
CAFile string `toml:"ca_file,omitempty" json:"ca_file" long:"ca-file" env:"KUBERNETES_CA_FILE" description:"Optional Kubernetes master auth ca certificate"`
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"`
CPUs string `toml:"cpus,omitempty" json:"cpus" long:"cpus" env:"KUBERNETES_CPUS" description:"(deprecated) The CPU allocation given to build containers"`
Memory string `toml:"memory,omitempty" json:"memory" long:"memory" env:"KUBERNETES_MEMORY" description:"(deprecated) The amount of memory allocated to build containers"`
......
......@@ -53,9 +53,12 @@ on the cluster.
## The keywords
The following keywords help to define the behaviour of the Runner within kubernetes:
The following keywords help to define the behaviour of the Runner within Kubernetes:
- `namespace`: Namespace to run Kubernetes Pods in
- `namespace_overwrite_allowed`: Regular expression to validate the contents of
the namespace overwrite environment variable (documented following). When empty,
it disables the namespace overwrite feature
- `privileged`: Run containers with the privileged flag
- `cpu_limit`: The CPU allocation given to build containers
- `memory_limit`: The amount of memory allocated to build containers
......@@ -86,6 +89,24 @@ The following keywords for resource limits are deprecated, please use the new on
- `helper_cpus`: The CPU allocation given to build helper containers
- `helper_memory`: The amount of memory allocated to build helper containers
### Overwriting Kubernetes Namespace
Additionally, Kubernetes namespace can be overwritten on `.gitlab-ci.yml` file, by using the variable
`KUBERNETES_NAMESPACE_OVERWRITE`.
This approach allow you to create a new isolated namespace dedicated for CI purposes, and deploy a custom
set of pods. The `Pods` spawned by the runner will take place on the overwritten namespace, for simple
and straight forward access between container during the CI stages.
``` yaml
variables:
KUBERNETES_NAMESPACE_OVERWRITE: ci-${CI_BUILD_REF_NAME}
```
Furthermore, to ensure only designated namespaces will be used during CI runs, inform the configuration
`namespace_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.
......@@ -106,6 +127,7 @@ concurrent = 4
key_file = "/etc/ssl/kubernetes/api.key"
ca_file = "/etc/ssl/kubernetes/ca.crt"
namespace = "gitlab"
namespace_overwrite_allowed = "ci-.*"
privileged = true
cpu_limit = "1"
memory_limit = "1Gi"
......
......@@ -72,12 +72,13 @@ func (p *ExecOptions) Run() error {
}
if pod.Status.Phase != api.PodRunning {
return fmt.Errorf("pod %s is not running and cannot execute commands; current phase is %s", p.PodName, pod.Status.Phase)
return fmt.Errorf("Pod '%s' (on namespace '%s') is not running and cannot execute commands; current phase is '%s'",
p.PodName, p.Namespace, pod.Status.Phase)
}
containerName := p.ContainerName
if len(containerName) == 0 {
log.Infof("defaulting container name to %s", pod.Spec.Containers[0].Name)
log.Infof("defaulting container name to '%s'", pod.Spec.Containers[0].Name)
containerName = pod.Spec.Containers[0].Name
}
......
......@@ -2,6 +2,7 @@ package kubernetes
import (
"fmt"
"regexp"
"strings"
"golang.org/x/net/context"
......@@ -37,6 +38,8 @@ type executor struct {
pod *api.Pod
options *kubernetesOptions
namespaceOverwrite string
buildLimits api.ResourceList
serviceLimits api.ResourceList
helperLimits api.ResourceList
......@@ -87,9 +90,8 @@ func (s *executor) setupResources() error {
return nil
}
func (s *executor) Prepare(globalConfig *common.Config, config *common.RunnerConfig, build *common.Build) error {
err := s.AbstractExecutor.Prepare(globalConfig, config, build)
if err != nil {
func (s *executor) Prepare(globalConfig *common.Config, config *common.RunnerConfig, build *common.Build) (err error) {
if err = s.AbstractExecutor.Prepare(globalConfig, config, build); err != nil {
return err
}
......@@ -101,8 +103,7 @@ func (s *executor) Prepare(globalConfig *common.Config, config *common.RunnerCon
return err
}
s.kubeClient, err = getKubeClient(config.Kubernetes)
if err != nil {
if s.kubeClient, err = getKubeClient(config.Kubernetes); err != nil {
return fmt.Errorf("error connecting to Kubernetes: %s", err.Error())
}
......@@ -114,6 +115,10 @@ func (s *executor) Prepare(globalConfig *common.Config, config *common.RunnerCon
return err
}
if err = s.overwriteNamespace(build); err != nil {
return err
}
if err = s.checkDefaults(); err != nil {
return err
}
......@@ -286,6 +291,7 @@ func (s *executor) runInContainer(ctx context.Context, name, command string) <-c
return errc
}
// checkDefaults Defines the configuration for the Pod on Kubernetes
func (s *executor) checkDefaults() error {
if s.options.Image == "" {
if s.Config.Kubernetes.Image == "" {
......@@ -296,9 +302,44 @@ func (s *executor) checkDefaults() error {
}
if s.Config.Kubernetes.Namespace == "" {
s.Warningln("Namespace is empty, therefore assuming 'default'.")
s.Config.Kubernetes.Namespace = "default"
}
s.Println("Using Kubernetes namespace:", s.Config.Kubernetes.Namespace)
return nil
}
// overwriteNamespace checks for variable in order to overwrite the configured
// namespace, as long as it complies to validation regular-expression, when
// expression is empty the overwrite is disabled.
func (s *executor) overwriteNamespace(build *common.Build) error {
if s.Config.Kubernetes.NamespaceOverwriteAllowed == "" {
s.Debugln("Configuration entry 'namespace_overwrite_allowed' is empty, using configured namespace.")
return nil
}
// looking for namespace overwrite variable, and expanding for interpolation
s.namespaceOverwrite = build.Variables.Expand().Get("KUBERNETES_NAMESPACE_OVERWRITE")
if s.namespaceOverwrite == "" {
return nil
}
var err error
var r *regexp.Regexp
if r, err = regexp.Compile(s.Config.Kubernetes.NamespaceOverwriteAllowed); 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
}
......
......@@ -8,6 +8,7 @@ import (
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"
"testing"
"time"
......@@ -196,6 +197,7 @@ func TestPrepare(t *testing.T) {
options: &kubernetesOptions{
Image: "test-image",
},
namespaceOverwrite: "",
serviceLimits: api.ResourceList{
api.ResourceCPU: resource.MustParse("100m"),
api.ResourceMemory: resource.MustParse("200Mi"),
......@@ -219,20 +221,22 @@ func TestPrepare(t *testing.T) {
RunnerConfig: &common.RunnerConfig{
RunnerSettings: common.RunnerSettings{
Kubernetes: &common.KubernetesConfig{
Host: "test-server",
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",
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,
},
},
},
......@@ -242,6 +246,9 @@ func TestPrepare(t *testing.T) {
Options: common.BuildOptions{
"image": "test-image",
},
Variables: []common.BuildVariable{
common.BuildVariable{Key: "KUBERNETES_NAMESPACE_OVERWRITE", Value: "namespacee"},
},
},
Runner: &common.RunnerConfig{},
},
......@@ -249,6 +256,7 @@ func TestPrepare(t *testing.T) {
options: &kubernetesOptions{
Image: "test-image",
},
namespaceOverwrite: "namespacee",
serviceLimits: api.ResourceList{
api.ResourceCPU: resource.MustParse("100m"),
api.ResourceMemory: resource.MustParse("200Mi"),
......@@ -310,6 +318,7 @@ func TestPrepare(t *testing.T) {
options: &kubernetesOptions{
Image: "test-image",
},
namespaceOverwrite: "",
serviceLimits: api.ResourceList{
api.ResourceCPU: resource.MustParse("100m"),
api.ResourceMemory: resource.MustParse("202Mi"),
......@@ -327,38 +336,75 @@ func TestPrepare(t *testing.T) {
helperRequests: api.ResourceList{},
},
},
{
GlobalConfig: &common.Config{},
RunnerConfig: &common.RunnerConfig{
RunnerSettings: common.RunnerSettings{
Kubernetes: &common.KubernetesConfig{
Namespace: "namespace",
Host: "test-server",
},
},
},
Build: &common.Build{
GetBuildResponse: common.GetBuildResponse{
Sha: "1234567890",
Options: common.BuildOptions{
"image": "test-image",
},
Variables: []common.BuildVariable{
common.BuildVariable{Key: "KUBERNETES_NAMESPACE_OVERWRITE", Value: "namespace"},
},
},
Runner: &common.RunnerConfig{},
},
Expected: &executor{
options: &kubernetesOptions{
Image: "test-image",
},
namespaceOverwrite: "",
serviceLimits: api.ResourceList{},
buildLimits: api.ResourceList{},
helperLimits: api.ResourceList{},
serviceRequests: api.ResourceList{},
buildRequests: api.ResourceList{},
helperRequests: api.ResourceList{},
},
},
}
for _, test := range tests {
e := &executor{
AbstractExecutor: executors.AbstractExecutor{
ExecutorOptions: executorOptions,
},
}
for index, test := range tests {
t.Run(strconv.Itoa(index), func(t *testing.T) {
e := &executor{
AbstractExecutor: executors.AbstractExecutor{
ExecutorOptions: executorOptions,
},
}
err := e.Prepare(test.GlobalConfig, test.RunnerConfig, test.Build)
err := e.Prepare(test.GlobalConfig, test.RunnerConfig, test.Build)
if err != nil {
if test.Error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if !test.Error {
t.Errorf("Got error. Expected: %v", test.Expected)
if err != nil {
if test.Error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if !test.Error {
t.Errorf("Got error. Expected: %v", test.Expected)
}
return
}
continue
}
// Set this to nil so we aren't testing the functionality of the
// base AbstractExecutor's Prepare method
e.AbstractExecutor = executors.AbstractExecutor{}
// Set this to nil so we aren't testing the functionality of the
// base AbstractExecutor's Prepare method
e.AbstractExecutor = executors.AbstractExecutor{}
// TODO: Improve this so we don't have to nil-ify the kubeClient.
// It currently contains some moving parts that are failing, meaning
// we'll need to mock _something_
e.kubeClient = nil
assert.Equal(t, test.Expected, e)
// TODO: Improve this so we don't have to nil-ify the kubeClient.
// It currently contains some moving parts that are failing, meaning
// we'll need to mock _something_
e.kubeClient = nil
assert.Equal(t, test.Expected, e)
})
}
}
......@@ -680,6 +726,40 @@ func TestKubernetesBuildCancel(t *testing.T) {
assert.EqualError(t, err, "canceled")
}
func TestOverwriteNamespaceNotMatch(t *testing.T) {
if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") {
return
}
build := &common.Build{
GetBuildResponse: common.GetBuildResponse{
Sha: "1234567890",
Options: common.BuildOptions{
"image": "test-image",
},
Variables: []common.BuildVariable{
common.BuildVariable{Key: "KUBERNETES_NAMESPACE_OVERWRITE", Value: "namespace"},
},
},
Runner: &common.RunnerConfig{
RunnerSettings: common.RunnerSettings{
Executor: "kubernetes",
Kubernetes: &common.KubernetesConfig{
NamespaceOverwriteAllowed: "^not_a_match$",
},
},
},
SystemInterrupt: make(chan os.Signal, 1),
}
build.Options = map[string]interface{}{
"image": "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