keel/secrets/secrets.go

381 lines
10 KiB
Go

package secrets
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"github.com/keel-hq/keel/provider/helm"
"github.com/keel-hq/keel/provider/kubernetes"
"github.com/keel-hq/keel/types"
v1 "k8s.io/api/core/v1"
log "github.com/sirupsen/logrus"
)
// const dockerConfigJSONKey = ".dockerconfigjson"
const dockerConfigKey = ".dockercfg"
const dockerConfigJSONKey = ".dockerconfigjson"
// common errors
var (
ErrNamespaceNotSpecified = errors.New("namespace not specified")
ErrSecretsNotSpecified = errors.New("no secrets were specified")
)
// Getter - generic secret getter interface
type Getter interface {
Get(image *types.TrackedImage) (*types.Credentials, error)
}
// DefaultGetter - default kubernetes secret getter implementation
type DefaultGetter struct {
kubernetesImplementer kubernetes.Implementer
defaultDockerConfig DockerCfg // default configuration supplied by optional environment variable
}
// NewGetter - create new default getter
func NewGetter(implementer kubernetes.Implementer, defaultDockerConfig DockerCfg) *DefaultGetter {
// initialising empty configuration
if defaultDockerConfig == nil {
defaultDockerConfig = make(DockerCfg)
}
return &DefaultGetter{
kubernetesImplementer: implementer,
defaultDockerConfig: defaultDockerConfig,
}
}
// Get - get secret for tracked image
func (g *DefaultGetter) Get(image *types.TrackedImage) (*types.Credentials, error) {
if image.Namespace == "" {
return nil, ErrNamespaceNotSpecified
}
// checking in default creds
creds, found := g.lookupDefaultDockerConfig(image)
if found {
return creds, nil
}
switch image.Provider {
case helm.ProviderName:
if len(image.Secrets) == 0 {
// looking up secrets based on selector
secrets, err := g.lookupSecrets(image)
if err != nil {
return nil, err
}
// populating secrets
image.Secrets = secrets
}
}
if len(image.Secrets) == 0 {
return nil, ErrSecretsNotSpecified
}
return g.getCredentialsFromSecret(image)
}
func (g *DefaultGetter) lookupDefaultDockerConfig(image *types.TrackedImage) (*types.Credentials, bool) {
return credentialsFromConfig(image, g.defaultDockerConfig)
}
func (g *DefaultGetter) lookupSecrets(image *types.TrackedImage) ([]string, error) {
secrets := []string{}
selector, ok := image.Meta["selector"]
if !ok {
// nothing
return secrets, nil
}
podList, err := g.kubernetesImplementer.Pods(image.Namespace, selector)
if err != nil {
return secrets, err
}
for _, pod := range podList.Items {
podSecrets := getPodImagePullSecrets(&pod)
log.WithFields(log.Fields{
"namespace": image.Namespace,
"provider": image.Provider,
"registry": image.Image.Registry(),
"image": image.Image.Repository(),
"pod_selector": selector,
"secrets": podSecrets,
}).Debug("secrets.defaultGetter.lookupSecrets: pod secrets found")
secrets = append(secrets, podSecrets...)
}
if len(secrets) == 0 {
log.WithFields(log.Fields{
"namespace": image.Namespace,
"provider": image.Provider,
"registry": image.Image.Registry(),
"image": image.Image.Repository(),
"pod_selector": selector,
"pods_checked": len(podList.Items),
}).Debug("secrets.defaultGetter.lookupSecrets: no secrets for image found")
}
return secrets, nil
}
func getPodImagePullSecrets(pod *v1.Pod) []string {
var secrets []string
for _, s := range pod.Spec.ImagePullSecrets {
secrets = append(secrets, s.Name)
}
return secrets
}
func (g *DefaultGetter) getCredentialsFromSecret(image *types.TrackedImage) (*types.Credentials, error) {
credentials := &types.Credentials{}
secretFound := false
for _, secretRef := range image.Secrets {
secret, err := g.kubernetesImplementer.Secret(image.Namespace, secretRef)
if err != nil {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"error": err,
}).Warn("secrets.defaultGetter: failed to get secret")
continue
}
var dockerCfg DockerCfg
switch secret.Type {
case v1.SecretTypeDockercfg:
secretDataBts, ok := secret.Data[dockerConfigKey]
if !ok {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"type": secret.Type,
"data": secret.Data,
}).Warn("secrets.defaultGetter: secret is missing key '.dockerconfig', ensure that key exists")
continue
}
dockerCfg, err = decodeSecret(secretDataBts)
if err != nil {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"secret_data": string(secretDataBts),
"error": err,
}).Error("secrets.defaultGetter: failed to decode secret")
continue
}
secretFound = true
case v1.SecretTypeDockerConfigJson:
secretDataBts, ok := secret.Data[dockerConfigJSONKey]
if !ok {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"type": secret.Type,
"data": secret.Data,
}).Warn("secrets.defaultGetter: secret is missing key '.dockerconfigjson', ensure that key exists")
continue
}
secretFound = true
dockerCfg, err = DecodeDockerCfgJson(secretDataBts)
if err != nil {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"secret_data": string(secretDataBts),
"error": err,
}).Error("secrets.defaultGetter: failed to decode secret")
continue
}
secretFound = true
default:
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"type": secret.Type,
}).Warn("secrets.defaultGetter: supplied secret is not kubernetes.io/dockercfg, ignoring")
continue
}
creds, found := credentialsFromConfig(image, dockerCfg)
if found {
return creds, nil
} else {
log.WithFields(log.Fields{
"secret_ref": secretRef,
"image": image.Image.String(),
}).Warn("secrets.defaultGetter: registry not found among secrets")
}
}
if secretFound {
log.WithFields(log.Fields{
"namespace": image.Namespace,
"provider": image.Provider,
"registry": image.Image.Registry(),
"image": image.Image.Repository(),
"secrets": image.Secrets,
}).Warn("secrets.defaultGetter.lookupSecrets: secret found but couldn't detect authentication for the desired registry")
} else if len(image.Secrets) > 0 {
log.WithFields(log.Fields{
"namespace": image.Namespace,
"provider": image.Provider,
"registry": image.Image.Registry(),
"image": image.Image.Repository(),
"secrets": image.Secrets,
}).Errorf("secrets.defaultGetter.lookupSecrets: docker credentials were not found among secrets, is secret in the namespace '%s'?", image.Namespace)
}
return credentials, nil
}
func credentialsFromConfig(image *types.TrackedImage, cfg DockerCfg) (*types.Credentials, bool) {
credentials := &types.Credentials{}
found := false
imageRegistry := image.Image.Registry()
// looking for our registry
for registry, auth := range cfg {
if registryMatches(imageRegistry, registry) {
if auth.Username != "" && auth.Password != "" {
credentials.Username = auth.Username
credentials.Password = auth.Password
} else if auth.Auth != "" {
username, password, err := decodeBase64Secret(auth.Auth)
if err != nil {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"registry": registry,
"error": err,
}).Error("secrets.defaultGetter: failed to decode auth secret")
continue
}
credentials.Username = username
credentials.Password = password
found = true
} else {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"registry": registry,
}).Warn("secrets.defaultGetter: secret doesn't have username, password and base64 encoded auth, skipping")
continue
}
log.WithFields(log.Fields{
"namespace": image.Namespace,
"provider": image.Provider,
"registry": image.Image.Registry(),
"image": image.Image.Repository(),
}).Debug("secrets.defaultGetter: secret looked up successfully")
return credentials, true
}
}
return credentials, found
}
func decodeBase64Secret(authSecret string) (username, password string, err error) {
decoded, err := base64.StdEncoding.DecodeString(authSecret)
if err != nil {
return
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
return "", "", fmt.Errorf("unexpected auth secret format")
}
return parts[0], parts[1], nil
}
func EncodeBase64Secret(username, password string) string {
return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
}
func hostname(registry string) (string, error) {
if strings.HasPrefix(registry, "http://") || strings.HasPrefix(registry, "https://") {
u, err := url.Parse(registry)
if err != nil {
return "", err
}
return u.Hostname(), nil
}
return registry, nil
}
func domainOnly(registry string) string {
if strings.Contains(registry, ":") {
return strings.Split(registry, ":")[0]
}
return registry
}
func decodeSecret(data []byte) (DockerCfg, error) {
var cfg DockerCfg
err := json.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
return cfg, nil
}
func DecodeDockerCfgJson(data []byte) (DockerCfg, error) {
// var cfg DockerCfg
var cfg DockerCfgJSON
err := json.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
return cfg.Auths, nil
}
func EncodeDockerCfgJson(cfg *DockerCfg) ([]byte, error) {
return json.Marshal(&DockerCfgJSON{
Auths: *cfg,
})
}
// DockerCfgJSON - secret structure when dockerconfigjson is used
type DockerCfgJSON struct {
Auths DockerCfg `json:"auths"`
}
// DockerCfg - registry_name=auth
type DockerCfg map[string]*Auth
// Auth - auth
type Auth struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Auth string `json:"auth"`
}