keel/secrets/secrets.go

323 lines
8.5 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"
"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
}
// NewGetter - create new default getter
func NewGetter(implementer kubernetes.Implementer) *DefaultGetter {
return &DefaultGetter{
kubernetesImplementer: implementer,
}
}
// Get - get secret for tracked image
func (g *DefaultGetter) Get(image *types.TrackedImage) (*types.Credentials, error) {
if image.Namespace == "" {
return nil, ErrNamespaceNotSpecified
}
switch image.Provider {
case helm.ProviderName:
// looking up secrets based on selector
secrets, err := g.lookupSecrets(image)
if err != nil {
return nil, err
}
// populating secrets
image.Secrets = secrets
}
return g.getCredentialsFromSecret(image)
}
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{}
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
}
dockerCfg := make(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
}
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
}
dockerCfg, err = decodeJSONSecret(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
}
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
}
// looking for our registry
for registry, auth := range dockerCfg {
h, err := hostname(registry)
if err != nil {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"registry": registry,
"secret_ref": secretRef,
"error": err,
}).Error("secrets.defaultGetter: failed to parse hostname")
continue
}
if h == image.Image.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,
"secret_ref": secretRef,
"error": err,
}).Error("secrets.defaultGetter: failed to decode auth secret")
continue
}
credentials.Username = username
credentials.Password = password
} else {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"registry": registry,
"secret_ref": secretRef,
"error": err,
}).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, nil
}
}
}
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,
}).Warn("secrets.defaultGetter.lookupSecrets: docker credentials were not found among secrets")
}
return credentials, nil
}
func decodeBase64Secret(authSecret string) (username, password string, err error) {
decoded, err := base64.StdEncoding.DecodeString(authSecret)
if err != nil {
return
}
parts := strings.Split(string(decoded), ":")
if len(parts) != 2 {
return "", "", fmt.Errorf("unexpected auth secret format")
}
return parts[0], parts[1], nil
}
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 decodeSecret(data []byte) (DockerCfg, error) {
var cfg DockerCfg
err := json.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
return cfg, nil
}
func decodeJSONSecret(data []byte) (DockerCfg, error) {
var cfg DockerCfgJSON
err := json.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
return cfg.Auths, nil
}
// 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"`
}