Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Docker credential helpers #789

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions cmd/keel/main.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package main

import (
"context"
"os"
"os/signal"
"path/filepath"
"time"

"context"

kingpin "github.com/alecthomas/kingpin/v2"
"github.com/prometheus/client_golang/prometheus"

Expand Down Expand Up @@ -50,6 +49,7 @@ import (

// credentials helpers
_ "github.com/keel-hq/keel/extension/credentialshelper/aws"
_ "github.com/keel-hq/keel/extension/credentialshelper/dockerhelper"
_ "github.com/keel-hq/keel/extension/credentialshelper/gcr"
secretsCredentialsHelper "github.com/keel-hq/keel/extension/credentialshelper/secrets"

Expand Down Expand Up @@ -361,7 +361,6 @@ type TriggerOpts struct {
// should go through all providers (or not if there is a reason) and submit events)
// func setupTriggers(ctx context.Context, providers provider.Providers, approvalsManager approvals.Manager, grc *k8s.GenericResourceCache, k8sClient kubernetes.Implementer) (teardown func()) {
func setupTriggers(ctx context.Context, opts *TriggerOpts) (teardown func()) {

authenticator := auth.New(&auth.Opts{
Username: os.Getenv(constants.EnvBasicAuthUser),
Password: os.Getenv(constants.EnvBasicAuthPassword),
Expand Down
120 changes: 120 additions & 0 deletions extension/credentialshelper/dockerhelper/dockerhelper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package dockerhelper

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"time"

"github.com/jellydator/ttlcache/v3"
"github.com/keel-hq/keel/extension/credentialshelper"
"github.com/keel-hq/keel/types"

log "github.com/sirupsen/logrus"
)

const defaultCacheTTL = 30 * time.Minute

var helperBinary = os.Getenv("DOCKER_CREDENTIALS_HELPER")

type DockerSecret struct {
Username string `json:"Username"`
Secret string `json:"Secret"`
}

type CredentialsHelper struct {
executor executor
cache *ttlcache.Cache[string, *types.Credentials]
path string
enabled bool
}

type executor interface {
Run(string, string) ([]byte, error)
}

type executorImpl struct{}

func init() {
credentialshelper.RegisterCredentialsHelper("dockerhelper", New())
}

func (e *executorImpl) Run(path, input string) ([]byte, error) {
cmd := exec.Command(path, "get")
defer func() {
if cmd.Process != nil {
cmd.Process.Kill()
}
}()
stdin, err := cmd.StdinPipe()
if err != nil {
log.WithError(err).Error("credentialshelper.dockerhelper: failed to get stdin pipe")
return nil, err
}
stdin.Write([]byte(input))
stdin.Close()
secrets, err := cmd.Output()
if err != nil {
log.WithError(err).Error("credentialshelper.dockerhelper: failed to get credentials")
return nil, err
}
return secrets, nil
}

// New creates a new docker credentials helper.
func New() *CredentialsHelper {
ch := &CredentialsHelper{}
if helperBinary == "" {
return ch
}
// Look up the binary at DOCKER_CREDENTIALS_HELPER.
path, err := exec.LookPath(helperBinary)
if err != nil {
log.WithError(err).Error("credentialshelper.dockerhelper: failed to find DOCKER_CREDENTIALS_HELPER")
return ch
}
ch.path = path
ch.executor = &executorImpl{}
ch.cache = ttlcache.New(
ttlcache.WithTTL[string, *types.Credentials](defaultCacheTTL),
ttlcache.WithDisableTouchOnHit[string, *types.Credentials](),
)
ch.enabled = true
return ch
}

// IsEnabled returns a bool whether this credentials helper has been initialized.
func (h *CredentialsHelper) IsEnabled() bool {
return h.enabled
}

// GetCredentials - finds credentials.
func (h *CredentialsHelper) GetCredentials(image *types.TrackedImage) (*types.Credentials, error) {
if !h.enabled {
return nil, fmt.Errorf("not initialized")
}
registry := image.Image.Registry()
creds := h.cache.Get(registry)
if creds != nil {
log.WithField("registry", registry).Debug("credentialshelper.dockerhelper: cache hit")
return creds.Value(), nil
}
// Run the credentials helper at h.path and pass the registry via stdin.
secrets, err := h.executor.Run(h.path, registry)
if err != nil {
return nil, err
}
dockerSecret := DockerSecret{}
if err = json.Unmarshal(secrets, &dockerSecret); err != nil {
log.WithError(err).Error("credentialshelper.dockerhelper: failed to unmarshal credentials")
return nil, err
}
crds := &types.Credentials{
Username: dockerSecret.Username,
Password: dockerSecret.Secret,
}
log.WithField("registry", registry).Debug("credentialshelper.dockerhelper: cache miss")
h.cache.Set(registry, crds, 0)
return crds, nil
}
155 changes: 155 additions & 0 deletions extension/credentialshelper/dockerhelper/dockerhelper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package dockerhelper

import (
"fmt"
"testing"

"github.com/jellydator/ttlcache/v3"
"github.com/keel-hq/keel/types"
"github.com/keel-hq/keel/util/image"
)

type testExecutor struct {
err error
output []byte
}

func (e *testExecutor) Run(path, input string) ([]byte, error) {
return e.output, e.err
}

func TestGetCredentials(t *testing.T) {
image, err := image.Parse("docker.io/keel-hq/keel:latest")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
testCases := []struct {
name string
input *types.TrackedImage
executorErr error
executorOutput []byte
expected *types.Credentials
}{
{
name: "error",
input: &types.TrackedImage{
Image: image,
},
executorErr: fmt.Errorf("error"),
expected: nil,
},
{
name: "success",
input: &types.TrackedImage{
Image: image,
},
executorOutput: []byte(`{"Username":"testUser","Secret":"testPW"}`),
expected: &types.Credentials{
Username: "testUser",
Password: "testPW",
},
},
{
name: "invalid json",
input: &types.TrackedImage{
Image: image,
},
executorOutput: []byte(`invalid json`),
expected: nil,
executorErr: fmt.Errorf("invalid json"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
helper := &CredentialsHelper{
executor: &testExecutor{
err: tc.executorErr,
output: tc.executorOutput,
},
cache: ttlcache.New(ttlcache.WithTTL[string, *types.Credentials](defaultCacheTTL)),
enabled: true,
}
creds, err := helper.GetCredentials(tc.input)
if err != nil {
if tc.executorErr == nil {
t.Errorf("unexpected error: %v", err)
}
} else {
if creds.Username != tc.expected.Username {
t.Errorf("expected username %s, got %s", tc.expected.Username, creds.Username)
}
if creds.Password != tc.expected.Password {
t.Errorf("expected password %s, got %s", tc.expected.Password, creds.Password)
}
}
})
}
}

func TestGetCredentialsFromCache(t *testing.T) {
image, err := image.Parse("docker.io/keel-hq/keel:latest")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
helper := &CredentialsHelper{
executor: &testExecutor{
err: nil,
output: []byte(`{"Username":"testUser","Secret":"testPW"}`),
},
cache: ttlcache.New(ttlcache.WithTTL[string, *types.Credentials](defaultCacheTTL)),
enabled: true,
}
creds, err := helper.GetCredentials(&types.TrackedImage{
Image: image,
})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if creds.Password != "testPW" || creds.Username != "testUser" {
t.Errorf("unexpected credentials: %v", creds)
}
helper.executor.(*testExecutor).err = fmt.Errorf("error")
creds, err = helper.GetCredentials(&types.TrackedImage{
Image: image,
})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if creds.Password != "testPW" || creds.Username != "testUser" {
t.Errorf("unexpected credentials: %v", creds)
}
}

func TestGetCredentialsExpiredCache(t *testing.T) {
image, err := image.Parse("docker.io/keel-hq/keel:latest")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
helper := &CredentialsHelper{
executor: &testExecutor{
err: nil,
output: []byte(`{"Username":"testUser","Secret":"testPW"}`),
},
cache: ttlcache.New(ttlcache.WithTTL[string, *types.Credentials](defaultCacheTTL)),
enabled: true,
}
creds, err := helper.GetCredentials(&types.TrackedImage{
Image: image,
})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if creds.Password != "testPW" || creds.Username != "testUser" {
t.Errorf("unexpected credentials: %v", creds)
}
helper.cache.DeleteAll()
creds, err = helper.GetCredentials(&types.TrackedImage{
Image: image,
})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if creds.Password != "testPW" || creds.Username != "testUser" {
t.Errorf("unexpected credentials: %v", creds)
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ require (
github.com/huandu/xstrings v1.5.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jellydator/ttlcache/v3 v3.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
Expand Down