Handle private docker registry auth

This adds support for private docker registries, by adding
an optional `registryAuth` field in the CreateFunctionRequest.
Auth must be passed as base64-encoded basic auth, similar to
how done in Docker file store credentials (~/.docker/config.json).
Credentials are then passed to swarm at service creation.
This commit is contained in:
Sebastien Guilloux 2017-05-30 15:32:05 +02:00 committed by Alex Ellis
parent 6f68b72c21
commit 9e711b3b5d
5 changed files with 222 additions and 0 deletions

View File

@ -155,6 +155,9 @@ definitions:
items: items:
type: "string" type: "string"
description: "Overrides to environmental variables" description: "Overrides to environmental variables"
registryAuth:
type: "string"
description: "Private registry base64-encoded basic auth (as present in ~/.docker/config.json)"
externalDocs: externalDocs:
description: "More documentation available on Github" description: "More documentation available on Github"
url: "http://docs.get-faas.com" url: "http://docs.get-faas.com"

86
docs/managing-images.md Normal file
View File

@ -0,0 +1,86 @@
# Managing images
## Using private Docker registries
FaaS supports running functions from Docker images in private Docker registries.
The registry credentials can be passed on function deployment, and are then handled by Swarm for image polling.
### Deploy functions with private registries credentials
A `POST` request on `/system/function` allows you to specify private registry credentials, as a base64-encoded basic auth (user:password).
```
curl -XPOST /system/functions -d {
"service": "functionName",
"image": "privateregistry.domain.com/user/function",
"envProcess": "/usr/bin/myprocess",
"network": "func_functions",
"registryAuth": "dXNlcjpwYXNzd29yZA=="
}
```
Base64-encoded basic auth can be resolved using your registry username and password:
````
echo -n "user:password" | base64
````
You can also find it in your `~/.docker/config.json` Docker credentials store, as a result of the `docker login` command:
```
cat ~/.docker/config.json
{
"auths": {
"privateregistry.domain.com": {
"auth": "dXNlcjpwYXNzd29yZA=="
}
}
}
```
### Deploy your own private Docker registry
If you wish to deploy your own private registry, you can follow [Docker official documentation](https://docs.docker.com/registry/deploying/).
A quick way to get started for a private registry with TLS and authentication
is to create a VM with port 443 open to the world (for letsencrypt registration), and a registered DNS ($YOURHOST).
Then, create these two files in the current directory:
```
# docker-compose.yml
version: '2'
services:
registry:
restart: always
image: registry:2
ports:
- 5000:5000
- 443:5000
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
REGISTRY_HTTP_TLS_LETSENCRYPT_CACHEFILE: /letsencrypt/cache
REGISTRY_HTTP_TLS_LETSENCRYPT_EMAIL: your@email.com
volumes:
- ./data:/var/lib/registry
- ./auth:/auth
- ./letsencrypt:/letsencrypt
```
```
# auth/htpasswd (generated with `docker run --entrypoint htpasswd registry:2 -Bbn testuser testpassword`)
testuser:$2y$05$Bl9siDMe7ieQHLM8e7ifaOklKrHmXymbMqfmqXs7zssj6MMGQW4le
```
Your registry is ready to be deployed by running `docker-compose up -d`.
On the client machine, you can now login and use the newly setup registry:
```
docker pull ubuntu && docker tag ubuntu $YOURHOST/ubuntu
docker login $YOURHOST # will add encoded registry credentials to ~/.docker/config.json
Username: testuser
Password: testpassword
docker push $YOURHOST/ubuntu
```
Images pushed to this registry can be used as functions with FaaS, provided you pass the appropriate `registryAuth` parameter at deployment time.

View File

@ -5,19 +5,24 @@ package handlers
import ( import (
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"strings"
"io/ioutil" "io/ioutil"
"github.com/alexellis/faas/gateway/metrics" "github.com/alexellis/faas/gateway/metrics"
"github.com/alexellis/faas/gateway/requests" "github.com/alexellis/faas/gateway/requests"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/registry"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
io_prometheus_client "github.com/prometheus/client_model/go" io_prometheus_client "github.com/prometheus/client_model/go"
) )
@ -160,6 +165,16 @@ func MakeNewFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Clie
// w.WriteHeader(http.StatusNotImplemented) // w.WriteHeader(http.StatusNotImplemented)
options := types.ServiceCreateOptions{} options := types.ServiceCreateOptions{}
if len(request.RegistryAuth) > 0 {
auth, err := BuildEncodedAuthConfig(request.RegistryAuth, request.Image)
if err != nil {
log.Println("Error while building registry auth configuration", err)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid registry auth"))
return
}
options.EncodedRegistryAuth = auth
}
spec := makeSpec(&request) spec := makeSpec(&request)
response, err := c.ServiceCreate(context.Background(), spec, options) response, err := c.ServiceCreate(context.Background(), spec, options)
@ -208,3 +223,43 @@ func makeSpec(request *requests.CreateFunctionRequest) swarm.ServiceSpec {
return spec return spec
} }
func BuildEncodedAuthConfig(basicAuthB64 string, dockerImage string) (string, error) {
// extract registry server address
distributionRef, err := reference.ParseNormalizedNamed(dockerImage)
if err != nil {
return "", err
}
repoInfo, err := registry.ParseRepositoryInfo(distributionRef)
if err != nil {
return "", err
}
// extract registry user & password
user, password, err := userPasswordFromBasicAuth(basicAuthB64)
if err != nil {
return "", err
}
// build encoded registry auth config
buf, err := json.Marshal(types.AuthConfig{
Username: user,
Password: password,
ServerAddress: repoInfo.Index.Name,
})
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(buf), nil
}
func userPasswordFromBasicAuth(basicAuthB64 string) (string, string, error) {
c, err := base64.StdEncoding.DecodeString(basicAuthB64)
if err != nil {
return "", "", err
}
cs := string(c)
s := strings.IndexByte(cs, ':')
if s < 0 {
return "", "", errors.New("Invalid basic auth")
}
return cs[:s], cs[s+1:], nil
}

View File

@ -18,6 +18,11 @@ type CreateFunctionRequest struct {
// EnvVars provides overrides for functions. // EnvVars provides overrides for functions.
EnvVars map[string]string `json:"envVars"` EnvVars map[string]string `json:"envVars"`
// RegistryAuth is the registry authentication (optional)
// in the same encoded format as Docker native credentials
// (see ~/.docker/config.json)
RegistryAuth string `json:"registryAuth,omitempty"`
} }
type DeleteFunctionRequest struct { type DeleteFunctionRequest struct {

View File

@ -0,0 +1,73 @@
// Copyright (c) Alex Ellis 2017. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
package tests
import (
"encoding/base64"
"encoding/json"
"strings"
"testing"
"github.com/alexellis/faas/gateway/handlers"
"github.com/docker/docker/api/types"
)
func TestBuildEncodedAuthConfig(t *testing.T) {
// custom repository with valid data
assertValidEncodedAuthConfig(t, "user", "password", "my.repository.com/user/imagename", "my.repository.com")
assertValidEncodedAuthConfig(t, "user", "weird:password:", "my.repository.com/user/imagename", "my.repository.com")
assertValidEncodedAuthConfig(t, "userWithNoPassword", "", "my.repository.com/user/imagename", "my.repository.com")
assertValidEncodedAuthConfig(t, "", "", "my.repository.com/user/imagename", "my.repository.com")
// docker hub default repository
assertValidEncodedAuthConfig(t, "user", "password", "user/imagename", "docker.io")
assertValidEncodedAuthConfig(t, "", "", "user/imagename", "docker.io")
// invalid base64 basic auth
assertEncodedAuthError(t, "invalidBasicAuth", "my.repository.com/user/imagename")
// invalid docker image name
assertEncodedAuthError(t, b64BasicAuth("user", "password"), "")
assertEncodedAuthError(t, b64BasicAuth("user", "password"), "invalid name")
}
func assertValidEncodedAuthConfig(t *testing.T, user, password, imageName, expectedRegistryHost string) {
encodedAuthConfig, err := handlers.BuildEncodedAuthConfig(b64BasicAuth(user, password), imageName)
if err != nil {
t.Log("Unexpected error while building auth config with correct values")
t.Fail()
}
authConfig := &types.AuthConfig{}
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuthConfig))
if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {
t.Log("Invalid encoded auth", err)
t.Fail()
}
if user != authConfig.Username {
t.Log("Auth config username mismatch", user, authConfig.Username)
t.Fail()
}
if password != authConfig.Password {
t.Log("Auth config password mismatch", password, authConfig.Password)
t.Fail()
}
if expectedRegistryHost != authConfig.ServerAddress {
t.Log("Auth config registry server address mismatch", expectedRegistryHost, authConfig.ServerAddress)
t.Fail()
}
}
func assertEncodedAuthError(t *testing.T, b64BasicAuth, imageName string) {
_, err := handlers.BuildEncodedAuthConfig(b64BasicAuth, imageName)
if err == nil {
t.Log("Expected an error to be returned")
t.Fail()
}
}
func b64BasicAuth(user, password string) string {
return base64.StdEncoding.EncodeToString([]byte(user + ":" + password))
}