381 lines
10 KiB
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"`
|
|
}
|