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

Get credentials from AWS ECR when needed #174

Merged
merged 6 commits into from
Oct 14, 2021
Merged
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
76 changes: 76 additions & 0 deletions controllers/imagerepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"time"

Expand All @@ -45,6 +47,10 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/predicate"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ecr"

"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/events"
"github.com/fluxcd/pkg/runtime/metrics"
Expand Down Expand Up @@ -74,6 +80,8 @@ type ImageRepositoryReconciler struct {
DatabaseWriter
DatabaseReader
}

AwsAutoLogin bool // automatically attempt to get credentials for images in ECR
}

type ImageRepositoryReconcilerOptions struct {
Expand Down Expand Up @@ -184,6 +192,54 @@ func (r *ImageRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Requ
return ctrl.Result{RequeueAfter: when}, nil
}

// parseAwsImage returns the AWS account ID and region and `true` if
// the image repository is hosted in AWS's Elastic Container Registry,
// otherwise empty strings and `false`.
func parseAwsImage(image string) (accountId, awsEcrRegion string, ok bool) {
registryPartRe := regexp.MustCompile(`([0-9+]*).dkr.ecr.([^/.]*)\.(amazonaws\.com[.cn]*)/([^:]+):?(.*)`)
registryParts := registryPartRe.FindAllStringSubmatch(image, -1)
if len(registryParts) < 1 {
return "", "", false
}
return registryParts[0][1], registryParts[0][2], true
}

// getAwsEcrLoginAuth obtains authentication for ECR given the account
// ID and region (taken from the image). This assumes that the pod has
// IAM permissions to get an authentication token, which will usually
// be the case if it's running in EKS, and may need additional setup
// otherwise (visit
// https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ as a
// starting point).
func getAwsECRLoginAuth(accountId, awsEcrRegion string) (authn.AuthConfig, error) {
// No caching of tokens is attempted; the quota for getting an
// auth token is high enough that getting a token every time you
// scan an image is viable for O(1000) images per region. See
// https://docs.aws.amazon.com/general/latest/gr/ecr.html.
var authConfig authn.AuthConfig

accountIDs := []string{accountId}
ecrService := ecr.New(session.Must(session.NewSession(&aws.Config{Region: aws.String(awsEcrRegion)})))
ecrToken, err := ecrService.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{
RegistryIds: aws.StringSlice(accountIDs),
})
if err != nil {
return authConfig, err
}

token, err := base64.StdEncoding.DecodeString(*ecrToken.AuthorizationData[0].AuthorizationToken)
if err != nil {
return authConfig, err
}

tokenSplit := strings.Split(string(token), ":")
authConfig = authn.AuthConfig{
Username: tokenSplit[0],
Password: tokenSplit[1],
}
return authConfig, nil
}

func (r *ImageRepositoryReconciler) scan(ctx context.Context, imageRepo *imagev1.ImageRepository, ref name.Reference) error {
timeout := imageRepo.GetTimeout()
ctx, cancel := context.WithTimeout(ctx, timeout)
Expand Down Expand Up @@ -215,6 +271,26 @@ func (r *ImageRepositoryReconciler) scan(ctx context.Context, imageRepo *imagev1
return err
}
options = append(options, remote.WithAuth(auth))
} else if accountId, awsEcrRegion, ok := parseAwsImage(imageRepo.Spec.Image); ok {
if r.AwsAutoLogin {
logr.FromContext(ctx).Info("Logging in to AWS ECR for " + imageRepo.Spec.Image)

authConfig, err := getAwsECRLoginAuth(accountId, awsEcrRegion)
if err != nil {
imagev1.SetImageRepositoryReadiness(
imageRepo,
metav1.ConditionFalse,
meta.ReconciliationFailedReason,
err.Error(),
)
return err
}

auth := authn.FromConfig(authConfig)
options = append(options, remote.WithAuth(auth))
} else {
logr.FromContext(ctx).Info("No image credentials secret referenced, and ECR authentication is not enabled. To enable, set the controller flag --aws-autologin-for-ecr")
}
}

if imageRepo.Spec.CertSecretRef != nil {
Expand Down
23 changes: 18 additions & 5 deletions docs/spec/v1beta1/imagerepositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,30 @@ type ImageRepositorySpec struct {
The `Suspend` field can be set to `true` to stop the controller scanning the image repository
specified; remove the field value or set to `false` to resume scanning.

### Authentication
### Authentication

The `secretRef` names a secret in the same namespace that holds credentials for accessing the image
repository. This secret is expected to be in the same format as for
[`imagePullSecrets`][image-pull-secrets]. The usual way to create such a secret is with

kubectl create secret docker-registry ...

If you are running on a platform (e.g., AWS) that links service permissions (e.g., access to ECR) to
service accounts, you may need to create the secret using tooling for that platform instead. There
is advice specific to some platforms [in the image automation guide][image-auto-provider-secrets].
For a publicly accessible image repository, you will not need to provide a `secretRef`.

For a publicly accessible image repository, you don't need to provide a `secretRef`.
#### ECR and EKS

When running in [<abbr title="Elastic Kubernetes Service">EKS</abbr>][EKS] and using [<abbr
title="Elastic Container Registry">ECR</abbr>][ECR] to store images, you should be able to rely on
the controller retrieving credentials automatically. The controller must be run with the flag
`--aws-autologin-for-ecr` set for this to work. The advice under "Other platforms" below will also
work for ECR.

#### Other platforms

If you are running on another platform that links service permissions to service accounts, you will
need to create the secret using tooling for that platform, rather than directly with `kubectl create
secret`. There is advice specific to some platforms [in the image automation
guide][image-auto-provider-secrets].

### TLS Certificates

Expand Down Expand Up @@ -248,3 +259,5 @@ and reference it under `secretRef`.
[image-auto-provider-secrets]: https://toolkit.fluxcd.io/guides/image-update/#imagerepository-cloud-providers-authentication
[pem-encoding]: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail
[sops-guide]: https://toolkit.fluxcd.io/guides/mozilla-sops/
[EKS]: https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html
[ECR]: https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ replace github.com/fluxcd/image-reflector-controller/api => ./api

require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/aws/aws-sdk-go v1.33.18
github.com/dgraph-io/badger/v3 v3.2103.1
github.com/fluxcd/image-reflector-controller/api v0.12.0
github.com/fluxcd/pkg/apis/meta v0.10.0
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/aws/aws-sdk-go v1.33.18 h1:Ccy1SV2SsgJU3rfrD+SOhQ0jvuzfrFuja/oKI86ruPw=
github.com/aws/aws-sdk-go v1.33.18/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
Expand Down Expand Up @@ -165,6 +167,7 @@ github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8
github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
Expand Down Expand Up @@ -284,6 +287,8 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func main() {
storagePath string
storageValueLogFileSize int64
concurrent int
awsAutoLogin bool
)

flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
Expand All @@ -79,6 +80,8 @@ func main() {
flag.StringVar(&storagePath, "storage-path", "/data", "Where to store the persistent database of image metadata")
flag.Int64Var(&storageValueLogFileSize, "storage-value-log-file-size", 1<<28, "Set the database's memory mapped value log file size in bytes. Effective memory usage is about two times this size.")
flag.IntVar(&concurrent, "concurrent", 4, "The number of concurrent resource reconciles.")
flag.BoolVar(&awsAutoLogin, "aws-autologin-for-ecr", false, "(AWS) Attempt to get credentials for images in Elastic Container Registry, when no secret is referenced")
stefanprodan marked this conversation as resolved.
Show resolved Hide resolved

clientOptions.BindFlags(flag.CommandLine)
logOptions.BindFlags(flag.CommandLine)
leaderElectionOptions.BindFlags(flag.CommandLine)
Expand Down Expand Up @@ -144,6 +147,7 @@ func main() {
ExternalEventRecorder: eventRecorder,
MetricsRecorder: metricsRecorder,
Database: db,
AwsAutoLogin: awsAutoLogin,
}).SetupWithManager(mgr, controllers.ImageRepositoryReconcilerOptions{
MaxConcurrentReconciles: concurrent,
}); err != nil {
Expand Down