323 lines
8.5 KiB
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"`
|
|
}
|