Skip to content
This repository has been archived by the owner on Dec 15, 2021. It is now read-only.

Commit

Permalink
Add secrets to runtime configuration (#1169)
Browse files Browse the repository at this point in the history
  • Loading branch information
sepetrov authored Sep 9, 2020
1 parent b87adf8 commit 25d6376
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 48 deletions.
6 changes: 4 additions & 2 deletions docs/function-controller-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,16 @@ The same process should be followed for any trigger controller installed (Kafka,

## Using custom images

It is possible to configure the different images that Kubeless uses for deploy and execute functions. In this ConfigMap you can configure:
It is possible to configure the different images that Kubeless uses to deploy and execute functions. In this ConfigMap you can configure:

- Different or additional runtimes. For doing so it is possible to modify/add a runtime in the field `runtimeImages`. Runtimes are categorized by major version. See the guide for [implementing a new runtime](/docs/implementing-new-runtime) for more information. Each major version has:
- Different or additional runtimes. For doing so it is possible to modify/add a runtime in the field `runtime-images`. Runtimes are categorized by major version. See the guide for [implementing a new runtime](/docs/implementing-new-runtime) for more information. Each major version has:
- Name: Unique ID of the runtime. It should contain the runtime name and version.
- Version: Major and minor version of the runtime.
- Runtime Image: Image used to execute the function.
- Init Image: Image used for installing the function and/or dependencies.
- (Optional) Image Pull Secrets: Secret required to pull the image in case the repository is private.
- (Optional) Environment variables.
- (Optional) Secrets: Shared with the container as volumes mounted at `/var/run/secrets/kubeless.io/`.
- The image used to populate the base image with the function. This is called `provision-image`. This image should have at least `unzip`, `GNU tar`, `gzip`, `bzip2`, `xz` and `curl`. It is also possible to specify `provision-image-secret` to specify a secret to pull that image from a private registry.
- The image used to build function images. This is called `builder-image`. This image is optional since its usage can be disabled with the property `enable-build-step`. A Dockerfile to build this image can be found [here](https://github.com/kubeless/kubeless/tree/master/docker/function-image-builder). It is also possible to specify `builder-image-secret` to specify a secret to pull that image from a private registry.

Expand Down
48 changes: 46 additions & 2 deletions pkg/langruntime/langruntime.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (
"regexp"
"strings"

yaml "github.com/ghodss/yaml"
"github.com/ghodss/yaml"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)

Expand All @@ -35,6 +35,12 @@ type Image struct {
Image string `yaml:"image"`
Command string `yaml:"command,omitempty"`
Env map[string]string `yaml:"env,omitempty"`
Secrets []Secret `yaml:"secrets,omitempty"`
}

// Secret is a reference to a secret.
type Secret struct {
Name string `yaml:"name,omitempty"`
}

// RuntimeVersion is a struct with all the info about the images and secrets
Expand Down Expand Up @@ -231,6 +237,33 @@ func (l *Langruntimes) GetImageSecrets(runtime string) ([]v1.LocalObjectReferenc
return lors, nil
}

// GetInitContainerSecrets gets the secrets of the init container with name
func (l *Langruntimes) GetInitContainerSecrets(runtime, name string) ([]v1.LocalObjectReference, error) {
runtimeInf, err := l.findRuntimeVersion(runtime)
if err != nil {
return nil, err
}

if len(runtimeInf.Images) == 0 {
return nil, nil
}

var secrets []Secret
phase := name2phase(name)
for _, i := range runtimeInf.Images {
if i.Phase == phase {
secrets = append(secrets, i.Secrets...)
break
}
}
var refs []v1.LocalObjectReference
for _, s := range secrets {
refs = append(refs, v1.LocalObjectReference{Name: s.Name})
}

return refs, nil
}

func appendToCommand(orig string, command ...string) string {
if len(orig) > 0 {
return fmt.Sprintf("%s && %s", orig, strings.Join(command, " && "))
Expand Down Expand Up @@ -347,3 +380,14 @@ func (l *Langruntimes) GetCompilationContainer(runtime, funcName string, env []v
Resources: resources,
}, nil
}

// name2phase returns the phase of an init container
func name2phase(name string) string {
switch name {
case "compile":
return PhaseCompilation
case "install":
return PhaseInstallation
}
return name
}
71 changes: 33 additions & 38 deletions pkg/langruntime/langruntimetestutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,40 @@ import (
func AddFakeConfig(clientset *fake.Clientset) {

runtimeImages := `[
{
"ID": "python",
"compiled": false,
"depName": "requirements.txt",
"fileNameSuffix": ".py",
"livenessProbeInfo": {
"exec": {
"command": [
"curl",
"-f",
"http://localhost:8080/healthz"
]
},
"initialDelaySeconds": 5,
"periodseconds": 10
{
"ID": "python",
"compiled": false,
"depName": "requirements.txt",
"fileNameSuffix": ".py",
"livenessProbeInfo": {
"exec": {
"command": ["curl", "-f", "http://localhost:8080/healthz"]
},
"versions": [
{
"images": [
{
"command": "foo",
"image": "python:2.7",
"phase": "installation"
},
{
"image": "bar",
"phase": "runtime",
"env": {
"PYTHONPATH": "/kubeless/lib/python2.7/site-packages:/kubeless"
}
}
],
"name": "python27",
"version": "2.7",
"imagePullSecrets": [{"ImageSecret": "p1"}, {"ImageSecret": "p2"}]
},
]
}
]`
"initialDelaySeconds": 5,
"periodseconds": 10
},
"versions": [
{
"images": [
{
"command": "foo",
"image": "python:2.7",
"phase": "installation",
"secrets": [{"name": "my-secret"}]
},
{
"image": "bar",
"phase": "runtime",
"env": {"PYTHONPATH": "/kubeless/lib/python2.7/site-packages:/kubeless"}
}
],
"name": "python27",
"version": "2.7",
"imagePullSecrets": [{"ImageSecret": "p1"}, {"ImageSecret": "p2"}]
}
]
}
]`
cm := v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "kubeless-config",
Expand Down
2 changes: 1 addition & 1 deletion pkg/utils/k8sutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func GetKubelessClientOutCluster() (versioned.Interface, error) {
return kubelessClient, nil
}

//GetDefaultNamespace returns the namespace set in current cluster context
// GetDefaultNamespace returns the namespace set in current cluster context
func GetDefaultNamespace() string {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.DefaultClientConfig = &clientcmd.DefaultClientConfig
Expand Down
53 changes: 48 additions & 5 deletions pkg/utils/kubelessutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"unicode/utf8"
Expand All @@ -49,6 +51,9 @@ import (
"k8s.io/client-go/rest"
)

// secretsMountPath is the file system path where volumes populated with secrets are mounted.
const secretsMountPath = "/var/run/secrets/kubeless.io"

// GetFunctionPort returns the port for a function service
func GetFunctionPort(clientset kubernetes.Interface, namespace, functionName string) (string, error) {
svc, err := clientset.CoreV1().Services(namespace).Get(functionName, metav1.GetOptions{})
Expand Down Expand Up @@ -384,7 +389,7 @@ func populatePodSpec(funcObj *kubelessApi.Function, lr *langruntime.Langruntimes
result.InitContainers = []v1.Container{provisionContainer}
}

// Add the imagesecrets if present to pull images from private docker registry
// add the image secrets if present to pull images from private docker registry
if funcObj.Spec.Runtime != "" {
imageSecrets, err := lr.GetImageSecrets(funcObj.Spec.Runtime)
if err != nil {
Expand Down Expand Up @@ -430,13 +435,51 @@ func populatePodSpec(funcObj *kubelessApi.Function, lr *langruntime.Langruntimes
*compContainer,
)
}

// mount volumes with init container secrets specified in runtime configuration
lr.ReadConfigMap()
for i := 0; i < len(result.InitContainers); i++ {
secrets, err := lr.GetInitContainerSecrets(funcObj.Spec.Runtime, result.InitContainers[i].Name)
if err != nil {
return fmt.Errorf("Unable to fetch init container secrets for runtime %s at phase %s: %v", funcObj.Spec.Runtime, result.InitContainers[i].Name, err)
}
for _, secret := range secrets {
// add volume if not available in the pod spec already
var found bool
for _, vol := range result.Volumes {
if vol.Name == secret.Name && (vol.Secret == nil || vol.Secret.SecretName != secret.Name) {
return fmt.Errorf("Unable to add volume for secret %s, volume already defined %#v", secret.Name, vol)
}
if vol.Name == secret.Name && vol.Secret != nil && vol.Secret.SecretName == secret.Name {
found = true
break
}
}
if !found {
result.Volumes = append(result.Volumes, v1.Volume{
Name: secret.Name,
VolumeSource: v1.VolumeSource{
Secret: &v1.SecretVolumeSource{SecretName: secret.Name},
},
})
}

// add volume mount to the init container
result.InitContainers[i].VolumeMounts = append(result.InitContainers[i].VolumeMounts, v1.VolumeMount{
Name: secret.Name,
ReadOnly: true,
MountPath: filepath.Join(secretsMountPath, secret.Name),
})
}
}

return nil
}

// EnsureFuncImage creates a Job to build a function image
func EnsureFuncImage(client kubernetes.Interface, funcObj *kubelessApi.Function, lr *langruntime.Langruntimes, or []metav1.OwnerReference, imageName, tag, builderImage, registryHost, dockerSecretName, provisionImage string, registryTLSEnabled bool, imagePullSecrets []v1.LocalObjectReference) error {
if len(tag) < 64 {
return fmt.Errorf("Expecting sha256 as image tag")
return errors.New("Expecting sha256 as image tag")
}
jobName := fmt.Sprintf("build-%s-%s", funcObj.ObjectMeta.Name, tag[0:10])
_, err := client.BatchV1().Jobs(funcObj.ObjectMeta.Namespace).Get(jobName, metav1.GetOptions{})
Expand Down Expand Up @@ -578,7 +621,7 @@ func EnsureFuncDeployment(client kubernetes.Interface, funcObj *kubelessApi.Func
}
maxUnavailable := intstr.FromInt(0)

//add deployment and copy all func's Spec.Deployment to the deployment
// add deployment and copy all func's Spec.Deployment to the deployment
dpm := funcObj.Spec.Deployment.DeepCopy()
dpm.OwnerReferences = or
dpm.ObjectMeta.Name = funcObj.ObjectMeta.Name
Expand All @@ -592,7 +635,7 @@ func EnsureFuncDeployment(client kubernetes.Interface, funcObj *kubelessApi.Func
},
}

//append data to dpm deployment
// append data to dpm deployment
dpm.Labels = addDefaultLabel(mergeMap(dpm.Labels, funcObj.Labels))
dpm.Spec.Template.Labels = mergeMap(dpm.Spec.Template.Labels, funcObj.Labels)
dpm.Annotations = mergeMap(dpm.Annotations, funcObj.Annotations)
Expand All @@ -609,7 +652,7 @@ func EnsureFuncDeployment(client kubernetes.Interface, funcObj *kubelessApi.Func
if err != nil {
return err
}
//only resolve the image name and build the function if it has not been built already
// only resolve the image name and build the function if it has not been built already
if dpm.Spec.Template.Spec.Containers[0].Image == "" && prebuiltRuntimeImage == "" {
err := populatePodSpec(funcObj, lr, &dpm.Spec.Template.Spec, runtimeVolumeMount, provisionImage, imagePullSecrets)
if err != nil {
Expand Down
39 changes: 39 additions & 0 deletions pkg/utils/kubelessutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,26 @@ func TestEnsureImage(t *testing.T) {
if reflect.DeepEqual(jobs.Items[0].Spec.Template.Spec.ImagePullSecrets, pullSecrets) {
t.Error("Missing ImagePullSecrets")
}

// ensure my-secret is mounted as /var/run/secrets/kubeless.io/my-secret to install container
var container v1.Container
for _, c := range jobs.Items[0].Spec.Template.Spec.InitContainers {
if c.Name == "install" {
container = c
}
}
if len(container.Name) == 0 {
t.Fatalf("Cannot find init container %q", "install")
}
var found bool
for _, v := range container.VolumeMounts {
if v.MountPath == "/var/run/secrets/kubeless.io/my-secret" {
found = true
}
}
if !found {
t.Fatalf("Cannot find volume mount /var/run/secrets/kubeless.io/my-secret")
}
}

func getDefaultFunc(name, ns string) *kubelessApi.Function {
Expand Down Expand Up @@ -683,6 +703,25 @@ func TestEnsureDeployment(t *testing.T) {
t.Errorf("Resources must be set for init container")
}

// ensure my-secret is mounted as /var/run/secrets/kubeless.io/my-secret to install container
var container v1.Container
for _, c := range dpm.Spec.Template.Spec.InitContainers {
if c.Name == "install" {
container = c
}
}
if len(container.Name) == 0 {
t.Fatalf("Cannot find init container %q", "install")
}
var found bool
for _, v := range container.VolumeMounts {
if v.MountPath == "/var/run/secrets/kubeless.io/my-secret" {
found = true
}
}
if !found {
t.Fatalf("Cannot find volume mount /var/run/secrets/kubeless.io/my-secret")
}
}

func TestEnsureDeploymentWithoutFuncNorHandler(t *testing.T) {
Expand Down

0 comments on commit 25d6376

Please sign in to comment.