From 47e7cee0b5e09bdb20ebe5b1929c6c0538fbd3b3 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Wed, 23 Oct 2024 16:29:05 +0300 Subject: [PATCH 01/24] Add suport for ECR repositorires Signed-off-by: alexey.komyakov --- go.mod | 15 +++++++++ go.sum | 32 ++++++++++++++++++++ main.go | 2 ++ pkg/registry/checker.go | 67 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) diff --git a/go.mod b/go.mod index 2fe3fc4..00a9e14 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/flant/k8s-image-availability-exporter go 1.22.0 require ( + github.com/aws/aws-sdk-go-v2 v1.32.2 + github.com/aws/aws-sdk-go-v2/config v1.28.0 + github.com/aws/aws-sdk-go-v2/service/ecr v1.36.2 github.com/gammazero/deque v0.2.1 github.com/google/go-containerregistry v0.20.2 github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20240129192428-8dadbe76ff8c @@ -17,6 +20,17 @@ require ( ) require ( + github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect + github.com/aws/smithy-go v1.22.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect @@ -37,6 +51,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.9 // indirect diff --git a/go.sum b/go.sum index 84de1c9..cf7948e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,31 @@ +github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI= +github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/config v1.28.0 h1:FosVYWcqEtWNxHn8gB/Vs6jOlNwSoyOCA/g/sxyySOQ= +github.com/aws/aws-sdk-go-v2/config v1.28.0/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/ecr v1.36.2 h1:VDQaVwGOokbd3VUbHF+wupiffdrbAZPdQnr5XZMJqrs= +github.com/aws/aws-sdk-go-v2/service/ecr v1.36.2/go.mod h1:lvUlMghKYmSxSfv0vU7pdU/8jSY+s0zpG8xXhaGKCw0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -54,6 +82,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/main.go b/main.go index adf916b..8a30ae5 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,7 @@ func main() { bindAddr := flag.String("bind-address", ":8080", "address:port to bind /metrics endpoint to") namespaceLabels := flag.String("namespace-label", "", "namespace label for checks") insecureSkipVerify := flag.Bool("skip-registry-cert-verification", false, "whether to skip registries' certificate verification") + ecrImagesExists := flag.Bool("ecr-images-exists", false, "whether images from ECR in your project") plainHTTP := flag.Bool("allow-plain-http", false, "whether to fallback to HTTP scheme for registries that don't support HTTPS") // named after the ctr cli flag defaultRegistry := flag.String("default-registry", "", fmt.Sprintf("default registry to use in absence of a fully qualified image name, defaults to %q", name.DefaultRegistry)) flag.Var(&cp, "capath", "path to a file that contains CA certificates in the PEM format") // named after the curl cli flag @@ -86,6 +87,7 @@ func main() { stopCh.Done(), kubeClient, *insecureSkipVerify, + *ecrImagesExists, *plainHTTP, cp, forceCheckDisabledControllerKindsParser.ParsedKinds, diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index 1c67871..2bd5912 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -22,6 +22,10 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ecr" + "github.com/aws/aws-sdk-go-v2/service/ecr/types" "github.com/sirupsen/logrus" k8sapierrors "k8s.io/apimachinery/pkg/api/errors" @@ -46,6 +50,7 @@ type registryCheckerConfig struct { defaultRegistry string plainHTTP bool mirrorsMap map[string]string + ecrImagesExists bool } type Checker struct { @@ -74,6 +79,7 @@ func NewChecker( stopCh <-chan struct{}, kubeClient *kubernetes.Clientset, skipVerify bool, + ecrImagesExists bool, plainHTTP bool, caPths []string, forceCheckDisabledControllerKinds []string, @@ -125,6 +131,7 @@ func NewChecker( defaultRegistry: defaultRegistry, plainHTTP: plainHTTP, mirrorsMap: mirrorsMap, + ecrImagesExists: ecrImagesExists, }, } @@ -316,6 +323,11 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k return checkImageNameParseErr(log, err) } + region := parseRegion(ref) + if isImageInEcr(ref, region) || rc.config.ecrImagesExists{ + return store.Available + } + imgErr := wait.ExponentialBackoff(wait.Backoff{ Duration: time.Second, Factor: 2, @@ -370,6 +382,30 @@ func parseImageName(image string, defaultRegistry string, plainHTTP bool) (name. return ref, nil } +func parseAccountId(reference name.Reference) string { + registry := reference.Context().RegistryStr() + + parts := strings.Split(registry, ".") + if len(parts) > 0 { + accountID := parts[0] + return accountID + } + + return "" +} + +func parseRegion(reference name.Reference) string { + registry := reference.Context().RegistryStr() + + parts := strings.Split(registry, ".") + if len(parts) > 3 { + region := parts[3] + return region + } + + return "" +} + func check(ref name.Reference, kc authn.Keychain, registryTransport http.RoundTripper) (store.AvailabilityMode, error) { var imgErr error @@ -407,3 +443,34 @@ func check(ref name.Reference, kc authn.Keychain, registryTransport http.RoundTr return availMode, imgErr } + +func isImageInEcr(ref name.Reference, region string) bool { + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) + if err != nil { + logrus.Errorf("Failed to load AWS SDK config: %v", err) + return false + } + + ecrClient := ecr.NewFromConfig(cfg) + input := &ecr.BatchGetImageInput{ + RegistryId: aws.String(parseAccountId(ref)), + RepositoryName: aws.String(ref.Context().RepositoryStr()), + ImageIds: []types.ImageIdentifier{ + {ImageTag: aws.String(ref.Identifier())}, + }, + } + + result, err := ecrClient.BatchGetImage(context.TODO(), input) + if err != nil { + logrus.Warningf("Error retrieving image from ECR: %v", err) + return false + } + + if len(result.Images) > 0 { + logrus.Infof("Image '%s' found in ECR.", ref.Context().Name()) + return true + } + + logrus.Infof("Image '%s' not found in ECR.", ref.Context().Name()) + return false +} \ No newline at end of file From 881efaa112a070167b4649c7b39ec877844ba7f4 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Wed, 23 Oct 2024 16:40:18 +0300 Subject: [PATCH 02/24] go lint Signed-off-by: alexey.komyakov --- pkg/registry/checker.go | 100 ++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index 2bd5912..849b91a 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -20,12 +20,12 @@ import ( "github.com/prometheus/client_golang/prometheus" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ecr" + "github.com/aws/aws-sdk-go-v2/service/ecr/types" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/ecr" - "github.com/aws/aws-sdk-go-v2/service/ecr/types" "github.com/sirupsen/logrus" k8sapierrors "k8s.io/apimachinery/pkg/api/errors" @@ -323,8 +323,8 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k return checkImageNameParseErr(log, err) } - region := parseRegion(ref) - if isImageInEcr(ref, region) || rc.config.ecrImagesExists{ + region := parseRegion(ref) + if isImageInEcr(ref, region) || rc.config.ecrImagesExists { return store.Available } @@ -382,28 +382,28 @@ func parseImageName(image string, defaultRegistry string, plainHTTP bool) (name. return ref, nil } -func parseAccountId(reference name.Reference) string { - registry := reference.Context().RegistryStr() +func parseAccountID(reference name.Reference) string { + registry := reference.Context().RegistryStr() - parts := strings.Split(registry, ".") - if len(parts) > 0 { - accountID := parts[0] - return accountID - } + parts := strings.Split(registry, ".") + if len(parts) > 0 { + accountID := parts[0] + return accountID + } - return "" + return "" } func parseRegion(reference name.Reference) string { - registry := reference.Context().RegistryStr() + registry := reference.Context().RegistryStr() - parts := strings.Split(registry, ".") - if len(parts) > 3 { - region := parts[3] - return region - } + parts := strings.Split(registry, ".") + if len(parts) > 3 { + region := parts[3] + return region + } - return "" + return "" } func check(ref name.Reference, kc authn.Keychain, registryTransport http.RoundTripper) (store.AvailabilityMode, error) { @@ -445,32 +445,32 @@ func check(ref name.Reference, kc authn.Keychain, registryTransport http.RoundTr } func isImageInEcr(ref name.Reference, region string) bool { - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) - if err != nil { - logrus.Errorf("Failed to load AWS SDK config: %v", err) - return false - } - - ecrClient := ecr.NewFromConfig(cfg) - input := &ecr.BatchGetImageInput{ - RegistryId: aws.String(parseAccountId(ref)), - RepositoryName: aws.String(ref.Context().RepositoryStr()), - ImageIds: []types.ImageIdentifier{ - {ImageTag: aws.String(ref.Identifier())}, - }, - } - - result, err := ecrClient.BatchGetImage(context.TODO(), input) - if err != nil { - logrus.Warningf("Error retrieving image from ECR: %v", err) - return false - } - - if len(result.Images) > 0 { - logrus.Infof("Image '%s' found in ECR.", ref.Context().Name()) - return true - } - - logrus.Infof("Image '%s' not found in ECR.", ref.Context().Name()) - return false -} \ No newline at end of file + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) + if err != nil { + logrus.Errorf("Failed to load AWS SDK config: %v", err) + return false + } + + ecrClient := ecr.NewFromConfig(cfg) + input := &ecr.BatchGetImageInput{ + RegistryId: aws.String(parseAccountID(ref)), + RepositoryName: aws.String(ref.Context().RepositoryStr()), + ImageIds: []types.ImageIdentifier{ + {ImageTag: aws.String(ref.Identifier())}, + }, + } + + result, err := ecrClient.BatchGetImage(context.TODO(), input) + if err != nil { + logrus.Warningf("Error retrieving image from ECR: %v", err) + return false + } + + if len(result.Images) > 0 { + logrus.Infof("Image '%s' found in ECR.", ref.Context().Name()) + return true + } + + logrus.Infof("Image '%s' not found in ECR.", ref.Context().Name()) + return false +} From 42b09a5d83b88dd55428dc5f7bf0fa90b72da857 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Wed, 23 Oct 2024 20:13:12 +0300 Subject: [PATCH 03/24] change operator Signed-off-by: alexey.komyakov --- pkg/registry/checker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index 849b91a..1d8150f 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -324,7 +324,7 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k } region := parseRegion(ref) - if isImageInEcr(ref, region) || rc.config.ecrImagesExists { + if isImageInEcr(ref, region) && rc.config.ecrImagesExists { return store.Available } From f048c93d836dbc29b9bd08a1fa138ba2b56b0b0a Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Thu, 24 Oct 2024 23:52:05 +0300 Subject: [PATCH 04/24] fixes + logs Signed-off-by: alexey.komyakov --- main.go | 2 -- pkg/registry/checker.go | 29 +++++++++++++++-------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/main.go b/main.go index 8a30ae5..adf916b 100644 --- a/main.go +++ b/main.go @@ -38,7 +38,6 @@ func main() { bindAddr := flag.String("bind-address", ":8080", "address:port to bind /metrics endpoint to") namespaceLabels := flag.String("namespace-label", "", "namespace label for checks") insecureSkipVerify := flag.Bool("skip-registry-cert-verification", false, "whether to skip registries' certificate verification") - ecrImagesExists := flag.Bool("ecr-images-exists", false, "whether images from ECR in your project") plainHTTP := flag.Bool("allow-plain-http", false, "whether to fallback to HTTP scheme for registries that don't support HTTPS") // named after the ctr cli flag defaultRegistry := flag.String("default-registry", "", fmt.Sprintf("default registry to use in absence of a fully qualified image name, defaults to %q", name.DefaultRegistry)) flag.Var(&cp, "capath", "path to a file that contains CA certificates in the PEM format") // named after the curl cli flag @@ -87,7 +86,6 @@ func main() { stopCh.Done(), kubeClient, *insecureSkipVerify, - *ecrImagesExists, *plainHTTP, cp, forceCheckDisabledControllerKindsParser.ParsedKinds, diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index 1d8150f..47017ba 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -50,7 +50,6 @@ type registryCheckerConfig struct { defaultRegistry string plainHTTP bool mirrorsMap map[string]string - ecrImagesExists bool } type Checker struct { @@ -79,7 +78,6 @@ func NewChecker( stopCh <-chan struct{}, kubeClient *kubernetes.Clientset, skipVerify bool, - ecrImagesExists bool, plainHTTP bool, caPths []string, forceCheckDisabledControllerKinds []string, @@ -131,7 +129,6 @@ func NewChecker( defaultRegistry: defaultRegistry, plainHTTP: plainHTTP, mirrorsMap: mirrorsMap, - ecrImagesExists: ecrImagesExists, }, } @@ -323,9 +320,11 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k return checkImageNameParseErr(log, err) } - region := parseRegion(ref) - if isImageInEcr(ref, region) && rc.config.ecrImagesExists { - return store.Available + if strings.Contains(ref.Context().RegistryStr(), "amazonaws.com") { + region, _ := parseRegion(ref) + if isImageInEcr(ref, region) { + return store.Available + } } imgErr := wait.ExponentialBackoff(wait.Backoff{ @@ -382,28 +381,30 @@ func parseImageName(image string, defaultRegistry string, plainHTTP bool) (name. return ref, nil } -func parseAccountID(reference name.Reference) string { +func parseAccountID(reference name.Reference) (string, error) { registry := reference.Context().RegistryStr() parts := strings.Split(registry, ".") if len(parts) > 0 { accountID := parts[0] - return accountID + return accountID, nil } - return "" + logrus.Infof("Uri '%s' doesn't match the default template while parsing account id.", registry) + return "", nil } -func parseRegion(reference name.Reference) string { +func parseRegion(reference name.Reference) (string, error) { registry := reference.Context().RegistryStr() parts := strings.Split(registry, ".") if len(parts) > 3 { region := parts[3] - return region + return region, nil } - return "" + logrus.Infof("Uri '%s' doesn't match the default template while parsing region.", registry) + return "", nil } func check(ref name.Reference, kc authn.Keychain, registryTransport http.RoundTripper) (store.AvailabilityMode, error) { @@ -452,8 +453,9 @@ func isImageInEcr(ref name.Reference, region string) bool { } ecrClient := ecr.NewFromConfig(cfg) + accountID, _ := parseAccountID(ref) input := &ecr.BatchGetImageInput{ - RegistryId: aws.String(parseAccountID(ref)), + RegistryId: aws.String(accountID), RepositoryName: aws.String(ref.Context().RepositoryStr()), ImageIds: []types.ImageIdentifier{ {ImageTag: aws.String(ref.Identifier())}, @@ -467,7 +469,6 @@ func isImageInEcr(ref name.Reference, region string) bool { } if len(result.Images) > 0 { - logrus.Infof("Image '%s' found in ECR.", ref.Context().Name()) return true } From 8ca7f0ae742fce1a591ee2276f8f748d8bc7c936 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Fri, 25 Oct 2024 11:57:34 +0300 Subject: [PATCH 05/24] ecrClient now is ecternal function Signed-off-by: alexey.komyakov --- pkg/registry/checker.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index 47017ba..fb4a675 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -407,6 +407,16 @@ func parseRegion(reference name.Reference) (string, error) { return "", nil } +func awsClient(region string) (*ecr.Client, error) { + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) + if err != nil { + return nil, err + } + + client := ecr.NewFromConfig(cfg) + return client, nil +} + func check(ref name.Reference, kc authn.Keychain, registryTransport http.RoundTripper) (store.AvailabilityMode, error) { var imgErr error @@ -446,13 +456,12 @@ func check(ref name.Reference, kc authn.Keychain, registryTransport http.RoundTr } func isImageInEcr(ref name.Reference, region string) bool { - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) + ecrClient, err := awsClient(region) if err != nil { - logrus.Errorf("Failed to load AWS SDK config: %v", err) + logrus.Warningf("Failed to load aws configuration: %v", err) return false } - ecrClient := ecr.NewFromConfig(cfg) accountID, _ := parseAccountID(ref) input := &ecr.BatchGetImageInput{ RegistryId: aws.String(accountID), From 7427acaf07403421f627409a3c575308156cb22b Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Fri, 25 Oct 2024 14:08:38 +0300 Subject: [PATCH 06/24] add logs for new parsers and new flag Signed-off-by: alexey.komyakov --- main.go | 2 ++ pkg/registry/checker.go | 18 +++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index adf916b..4714fc1 100644 --- a/main.go +++ b/main.go @@ -39,6 +39,7 @@ func main() { namespaceLabels := flag.String("namespace-label", "", "namespace label for checks") insecureSkipVerify := flag.Bool("skip-registry-cert-verification", false, "whether to skip registries' certificate verification") plainHTTP := flag.Bool("allow-plain-http", false, "whether to fallback to HTTP scheme for registries that don't support HTTPS") // named after the ctr cli flag + ecrImagesExists := flag.Bool("ecr-images-exists", false, "whether images from ECR in your project") defaultRegistry := flag.String("default-registry", "", fmt.Sprintf("default registry to use in absence of a fully qualified image name, defaults to %q", name.DefaultRegistry)) flag.Var(&cp, "capath", "path to a file that contains CA certificates in the PEM format") // named after the curl cli flag flag.Var(&mirrors, "image-mirror", "Add a mirror repository (format: original=mirror)") @@ -86,6 +87,7 @@ func main() { stopCh.Done(), kubeClient, *insecureSkipVerify, + *ecrImagesExists *plainHTTP, cp, forceCheckDisabledControllerKindsParser.ParsedKinds, diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index fb4a675..ed1fa22 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -50,6 +50,7 @@ type registryCheckerConfig struct { defaultRegistry string plainHTTP bool mirrorsMap map[string]string + ecrImagesExists bool } type Checker struct { @@ -79,6 +80,7 @@ func NewChecker( kubeClient *kubernetes.Clientset, skipVerify bool, plainHTTP bool, + ecrImagesExists bool, caPths []string, forceCheckDisabledControllerKinds []string, ignoredImages []regexp.Regexp, @@ -129,6 +131,7 @@ func NewChecker( defaultRegistry: defaultRegistry, plainHTTP: plainHTTP, mirrorsMap: mirrorsMap, + ecrImagesExists: ecrImagesExists, }, } @@ -320,9 +323,12 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k return checkImageNameParseErr(log, err) } - if strings.Contains(ref.Context().RegistryStr(), "amazonaws.com") { - region, _ := parseRegion(ref) - if isImageInEcr(ref, region) { + if rc.config.ecrImagesExists && strings.Contains(ref.Context().RegistryStr(), "amazonaws.com") { + region, err := parseRegion(ref) + if err != nil { + return checkImageNameParseErr(log, err) + } + if isImageInEcr(ref, region){ return store.Available } } @@ -390,8 +396,7 @@ func parseAccountID(reference name.Reference) (string, error) { return accountID, nil } - logrus.Infof("Uri '%s' doesn't match the default template while parsing account id.", registry) - return "", nil + return "", fmt.Errorf("something went wrong with URI \"%s\" while parsing account id", registry) } func parseRegion(reference name.Reference) (string, error) { @@ -403,8 +408,7 @@ func parseRegion(reference name.Reference) (string, error) { return region, nil } - logrus.Infof("Uri '%s' doesn't match the default template while parsing region.", registry) - return "", nil + return "", fmt.Errorf("URI \"%s\" doesn't match the default template while parsing region", registry) } func awsClient(region string) (*ecr.Client, error) { From a52943c7f8c12d5aabe828770d0b169b3d913dfb Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Fri, 25 Oct 2024 14:09:14 +0300 Subject: [PATCH 07/24] fix description of flag Signed-off-by: alexey.komyakov --- README.md | 2 ++ main.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b449e32..4a68747 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,8 @@ Usage of k8s-image-availability-exporter: namespace label for checks -skip-registry-cert-verification whether to skip registries' certificate verification + -ecr-images-exists + whether images from ECR in your cluster ``` ## Metrics diff --git a/main.go b/main.go index 4714fc1..44de2a4 100644 --- a/main.go +++ b/main.go @@ -39,7 +39,7 @@ func main() { namespaceLabels := flag.String("namespace-label", "", "namespace label for checks") insecureSkipVerify := flag.Bool("skip-registry-cert-verification", false, "whether to skip registries' certificate verification") plainHTTP := flag.Bool("allow-plain-http", false, "whether to fallback to HTTP scheme for registries that don't support HTTPS") // named after the ctr cli flag - ecrImagesExists := flag.Bool("ecr-images-exists", false, "whether images from ECR in your project") + ecrImagesExists := flag.Bool("ecr-images-exists", false, "whether images from ECR in your cluster") defaultRegistry := flag.String("default-registry", "", fmt.Sprintf("default registry to use in absence of a fully qualified image name, defaults to %q", name.DefaultRegistry)) flag.Var(&cp, "capath", "path to a file that contains CA certificates in the PEM format") // named after the curl cli flag flag.Var(&mirrors, "image-mirror", "Add a mirror repository (format: original=mirror)") From 2abd5b25f87a38198b71dd17cb9191edce530a08 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Fri, 25 Oct 2024 14:13:11 +0300 Subject: [PATCH 08/24] linter Signed-off-by: alexey.komyakov --- main.go | 2 +- pkg/registry/checker.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 44de2a4..8cb0bc2 100644 --- a/main.go +++ b/main.go @@ -87,7 +87,7 @@ func main() { stopCh.Done(), kubeClient, *insecureSkipVerify, - *ecrImagesExists + *ecrImagesExists, *plainHTTP, cp, forceCheckDisabledControllerKindsParser.ParsedKinds, diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index ed1fa22..35597f2 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -328,7 +328,7 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k if err != nil { return checkImageNameParseErr(log, err) } - if isImageInEcr(ref, region){ + if isImageInEcr(ref, region) { return store.Available } } From a9a1fc4938da5c5988f607d1cb56f2be6d465401 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Mon, 28 Oct 2024 13:45:09 +0300 Subject: [PATCH 09/24] design fixes Signed-off-by: alexey.komyakov --- pkg/registry/checker.go | 100 +++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 53 deletions(-) diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index 35597f2..bf89005 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -53,6 +53,12 @@ type registryCheckerConfig struct { ecrImagesExists bool } +type ECRDetails struct { + AccountID string + Region string + Domain string +} + type Checker struct { imageStore *store.ImageStore @@ -323,14 +329,13 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k return checkImageNameParseErr(log, err) } - if rc.config.ecrImagesExists && strings.Contains(ref.Context().RegistryStr(), "amazonaws.com") { - region, err := parseRegion(ref) - if err != nil { - return checkImageNameParseErr(log, err) - } - if isImageInEcr(ref, region) { + ecrDetails, err := parseECRDetails(ref.Context().RegistryStr()) + if err == nil && rc.config.ecrImagesExists { + if rc.isImageInEcr(log, ref, ecrDetails) { return store.Available } + } else if err != nil { + log.Error(err) } imgErr := wait.ExponentialBackoff(wait.Backoff{ @@ -387,32 +392,51 @@ func parseImageName(image string, defaultRegistry string, plainHTTP bool) (name. return ref, nil } -func parseAccountID(reference name.Reference) (string, error) { - registry := reference.Context().RegistryStr() - - parts := strings.Split(registry, ".") - if len(parts) > 0 { - accountID := parts[0] - return accountID, nil +func parseECRDetails(registryStr string) (ECRDetails, error) { + parts := strings.SplitN(registryStr, ".", 5) + if len(parts) <= 3 || !strings.Contains(registryStr, "amazonaws.com") { + return ECRDetails{}, fmt.Errorf("%q doesn't match the ECR template", registryStr) } - return "", fmt.Errorf("something went wrong with URI \"%s\" while parsing account id", registry) + return ECRDetails{ + AccountID: parts[0], + Region: parts[3], + Domain: registryStr, + }, nil } -func parseRegion(reference name.Reference) (string, error) { - registry := reference.Context().RegistryStr() +func (rc *Checker) isImageInEcr(log *logrus.Entry, ref name.Reference, details ECRDetails) bool { + ctx := context.TODO() + ecrClient, err := rc.awsRegionalClient(ctx, details.Region) + if err != nil { + log.Warningf("Failed to load aws configuration: %v", err) + return false + } + + input := &ecr.BatchGetImageInput{ + RegistryId: aws.String(details.AccountID), + RepositoryName: aws.String(ref.Context().RepositoryStr()), + ImageIds: []types.ImageIdentifier{ + {ImageTag: aws.String(ref.Identifier())}, + }, + } + + result, err := ecrClient.BatchGetImage(ctx, input) + if err != nil { + log.Warningf("Error retrieving image from ECR: %v", err) + return false + } - parts := strings.Split(registry, ".") - if len(parts) > 3 { - region := parts[3] - return region, nil + if len(result.Images) > 0 { + return true } - return "", fmt.Errorf("URI \"%s\" doesn't match the default template while parsing region", registry) + log.Infof("Image %q not found in ECR.", ref.Context().Name()) + return false } -func awsClient(region string) (*ecr.Client, error) { - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) +func (rc *Checker) awsRegionalClient(ctx context.Context, region string) (*ecr.Client, error) { + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) if err != nil { return nil, err } @@ -458,33 +482,3 @@ func check(ref name.Reference, kc authn.Keychain, registryTransport http.RoundTr return availMode, imgErr } - -func isImageInEcr(ref name.Reference, region string) bool { - ecrClient, err := awsClient(region) - if err != nil { - logrus.Warningf("Failed to load aws configuration: %v", err) - return false - } - - accountID, _ := parseAccountID(ref) - input := &ecr.BatchGetImageInput{ - RegistryId: aws.String(accountID), - RepositoryName: aws.String(ref.Context().RepositoryStr()), - ImageIds: []types.ImageIdentifier{ - {ImageTag: aws.String(ref.Identifier())}, - }, - } - - result, err := ecrClient.BatchGetImage(context.TODO(), input) - if err != nil { - logrus.Warningf("Error retrieving image from ECR: %v", err) - return false - } - - if len(result.Images) > 0 { - return true - } - - logrus.Infof("Image '%s' not found in ECR.", ref.Context().Name()) - return false -} From 7c2db4ba93b89f0e3e05d03bf5f36f6d7b0aaa22 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Wed, 30 Oct 2024 22:12:12 +0300 Subject: [PATCH 10/24] fixes Signed-off-by: alexey.komyakov --- main.go | 2 - pkg/providers/amazon.go | 89 +++++++++++++++++++++++++++++++++++++++++ pkg/registry/checker.go | 79 ++++-------------------------------- 3 files changed, 96 insertions(+), 74 deletions(-) create mode 100644 pkg/providers/amazon.go diff --git a/main.go b/main.go index 8cb0bc2..adf916b 100644 --- a/main.go +++ b/main.go @@ -39,7 +39,6 @@ func main() { namespaceLabels := flag.String("namespace-label", "", "namespace label for checks") insecureSkipVerify := flag.Bool("skip-registry-cert-verification", false, "whether to skip registries' certificate verification") plainHTTP := flag.Bool("allow-plain-http", false, "whether to fallback to HTTP scheme for registries that don't support HTTPS") // named after the ctr cli flag - ecrImagesExists := flag.Bool("ecr-images-exists", false, "whether images from ECR in your cluster") defaultRegistry := flag.String("default-registry", "", fmt.Sprintf("default registry to use in absence of a fully qualified image name, defaults to %q", name.DefaultRegistry)) flag.Var(&cp, "capath", "path to a file that contains CA certificates in the PEM format") // named after the curl cli flag flag.Var(&mirrors, "image-mirror", "Add a mirror repository (format: original=mirror)") @@ -87,7 +86,6 @@ func main() { stopCh.Done(), kubeClient, *insecureSkipVerify, - *ecrImagesExists, *plainHTTP, cp, forceCheckDisabledControllerKindsParser.ParsedKinds, diff --git a/pkg/providers/amazon.go b/pkg/providers/amazon.go new file mode 100644 index 0000000..faa50e9 --- /dev/null +++ b/pkg/providers/amazon.go @@ -0,0 +1,89 @@ +package providers + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ecr" + "github.com/google/go-containerregistry/pkg/authn" +) + +type ECRDetails struct { + Region string + Domain string +} + +type customKeychain struct { + authenticator authn.Authenticator +} + +func (kc *customKeychain) Resolve(_ authn.Resource) (authn.Authenticator, error) { + return kc.authenticator, nil +} + +func GetECRAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) { + ecrDetails, err := parseECRDetails(registryStr) + if err != nil { + return nil, err + } + + ecrClient, err := awsRegionalClient(ctx, ecrDetails.Region) + if err != nil { + return nil, fmt.Errorf("error loading AWS config: %w", err) + } + + authTokenOutput, err := ecrClient.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{}) + if err != nil { + return nil, fmt.Errorf("error getting ECR authorization token: %w", err) + } + + if len(authTokenOutput.AuthorizationData) == 0 { + return nil, fmt.Errorf("no authorization data received from ECR") + } + + authData := authTokenOutput.AuthorizationData[0] + decodedToken, err := base64.StdEncoding.DecodeString(*authData.AuthorizationToken) + if err != nil { + return nil, fmt.Errorf("error decoding authorization token: %w", err) + } + + credentials := strings.SplitN(string(decodedToken), ":", 2) + if len(credentials) != 2 { + return nil, fmt.Errorf("invalid authorization token format") + } + authConfig := authn.AuthConfig{ + Username: credentials[0], + Password: credentials[1], + } + auth := authn.FromConfig(authConfig) + return &customKeychain{authenticator: auth}, nil +} + +func IsEcrURL(url string) bool { + parts := strings.SplitN(url, ".", 5) + if len(parts) <= 3 || !strings.Contains(url, "amazonaws.com") { + return false + } + return true +} + +func parseECRDetails(registryStr string) (ECRDetails, error) { + parts := strings.SplitN(registryStr, ".", 5) + return ECRDetails{ + Region: parts[3], + Domain: registryStr, + }, nil +} + +func awsRegionalClient(ctx context.Context, region string) (*ecr.Client, error) { + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + if err != nil { + return nil, err + } + + client := ecr.NewFromConfig(cfg) + return client, nil +} diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index bf89005..2ec5842 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/flant/k8s-image-availability-exporter/pkg/providers" "github.com/flant/k8s-image-availability-exporter/pkg/version" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/v1/remote/transport" @@ -20,10 +21,6 @@ import ( "github.com/prometheus/client_golang/prometheus" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/ecr" - "github.com/aws/aws-sdk-go-v2/service/ecr/types" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/sirupsen/logrus" @@ -50,13 +47,6 @@ type registryCheckerConfig struct { defaultRegistry string plainHTTP bool mirrorsMap map[string]string - ecrImagesExists bool -} - -type ECRDetails struct { - AccountID string - Region string - Domain string } type Checker struct { @@ -86,7 +76,6 @@ func NewChecker( kubeClient *kubernetes.Clientset, skipVerify bool, plainHTTP bool, - ecrImagesExists bool, caPths []string, forceCheckDisabledControllerKinds []string, ignoredImages []regexp.Regexp, @@ -137,7 +126,6 @@ func NewChecker( defaultRegistry: defaultRegistry, plainHTTP: plainHTTP, mirrorsMap: mirrorsMap, - ecrImagesExists: ecrImagesExists, }, } @@ -329,13 +317,13 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k return checkImageNameParseErr(log, err) } - ecrDetails, err := parseECRDetails(ref.Context().RegistryStr()) - if err == nil && rc.config.ecrImagesExists { - if rc.isImageInEcr(log, ref, ecrDetails) { - return store.Available + if providers.IsEcrURL(ref.Context().RegistryStr()) { + ecrAuth, err := providers.GetECRAuthKeychain(context.Background(), ref.Context().RegistryStr()) + if err != nil { + log.WithError(err).Error("Error while getting token") + return store.UnknownError } - } else if err != nil { - log.Error(err) + kc = ecrAuth } imgErr := wait.ExponentialBackoff(wait.Backoff{ @@ -392,59 +380,6 @@ func parseImageName(image string, defaultRegistry string, plainHTTP bool) (name. return ref, nil } -func parseECRDetails(registryStr string) (ECRDetails, error) { - parts := strings.SplitN(registryStr, ".", 5) - if len(parts) <= 3 || !strings.Contains(registryStr, "amazonaws.com") { - return ECRDetails{}, fmt.Errorf("%q doesn't match the ECR template", registryStr) - } - - return ECRDetails{ - AccountID: parts[0], - Region: parts[3], - Domain: registryStr, - }, nil -} - -func (rc *Checker) isImageInEcr(log *logrus.Entry, ref name.Reference, details ECRDetails) bool { - ctx := context.TODO() - ecrClient, err := rc.awsRegionalClient(ctx, details.Region) - if err != nil { - log.Warningf("Failed to load aws configuration: %v", err) - return false - } - - input := &ecr.BatchGetImageInput{ - RegistryId: aws.String(details.AccountID), - RepositoryName: aws.String(ref.Context().RepositoryStr()), - ImageIds: []types.ImageIdentifier{ - {ImageTag: aws.String(ref.Identifier())}, - }, - } - - result, err := ecrClient.BatchGetImage(ctx, input) - if err != nil { - log.Warningf("Error retrieving image from ECR: %v", err) - return false - } - - if len(result.Images) > 0 { - return true - } - - log.Infof("Image %q not found in ECR.", ref.Context().Name()) - return false -} - -func (rc *Checker) awsRegionalClient(ctx context.Context, region string) (*ecr.Client, error) { - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) - if err != nil { - return nil, err - } - - client := ecr.NewFromConfig(cfg) - return client, nil -} - func check(ref name.Reference, kc authn.Keychain, registryTransport http.RoundTripper) (store.AvailabilityMode, error) { var imgErr error From fafcf1b46f389cc7662b98834f68d647e12e5132 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Thu, 31 Oct 2024 15:54:08 +0300 Subject: [PATCH 11/24] make provider as interface Signed-off-by: alexey.komyakov --- pkg/providers/amazon.go | 35 +++++++++++++++++++---------------- pkg/registry/checker.go | 8 ++++++-- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/pkg/providers/amazon.go b/pkg/providers/amazon.go index faa50e9..3669a6b 100644 --- a/pkg/providers/amazon.go +++ b/pkg/providers/amazon.go @@ -11,26 +11,24 @@ import ( "github.com/google/go-containerregistry/pkg/authn" ) -type ECRDetails struct { - Region string - Domain string +type ECRProvider interface { + GetECRAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) + IsEcrURL(url string) bool } -type customKeychain struct { - authenticator authn.Authenticator -} +type awsECRProvider struct{} -func (kc *customKeychain) Resolve(_ authn.Resource) (authn.Authenticator, error) { - return kc.authenticator, nil +func NewECRProvider() ECRProvider { + return &awsECRProvider{} } -func GetECRAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) { +func (p *awsECRProvider) GetECRAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) { ecrDetails, err := parseECRDetails(registryStr) if err != nil { return nil, err } - ecrClient, err := awsRegionalClient(ctx, ecrDetails.Region) + ecrClient, err := awsRegionalClient(ctx, ecrDetails) if err != nil { return nil, fmt.Errorf("error loading AWS config: %w", err) } @@ -62,7 +60,7 @@ func GetECRAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain return &customKeychain{authenticator: auth}, nil } -func IsEcrURL(url string) bool { +func (p *awsECRProvider) IsEcrURL(url string) bool { parts := strings.SplitN(url, ".", 5) if len(parts) <= 3 || !strings.Contains(url, "amazonaws.com") { return false @@ -70,12 +68,9 @@ func IsEcrURL(url string) bool { return true } -func parseECRDetails(registryStr string) (ECRDetails, error) { +func parseECRDetails(registryStr string) (string, error) { parts := strings.SplitN(registryStr, ".", 5) - return ECRDetails{ - Region: parts[3], - Domain: registryStr, - }, nil + return parts[3], nil } func awsRegionalClient(ctx context.Context, region string) (*ecr.Client, error) { @@ -87,3 +82,11 @@ func awsRegionalClient(ctx context.Context, region string) (*ecr.Client, error) client := ecr.NewFromConfig(cfg) return client, nil } + +type customKeychain struct { + authenticator authn.Authenticator +} + +func (kc *customKeychain) Resolve(_ authn.Resource) (authn.Authenticator, error) { + return kc.authenticator, nil +} diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index 2ec5842..dbda6f3 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -60,6 +60,8 @@ type Checker struct { cronJobsInformer batchv1informers.CronJobInformer secretsInformer corev1informers.SecretInformer + amazonProvider providers.ECRProvider + controllerIndexers ControllerIndexers ignoredImagesRegex []regexp.Regexp @@ -116,6 +118,8 @@ func NewChecker( cronJobsInformer: informerFactory.Batch().V1().CronJobs(), secretsInformer: informerFactory.Core().V1().Secrets(), + amazonProvider: providers.NewECRProvider(), + ignoredImagesRegex: ignoredImages, registryTransport: roundTripper, @@ -317,8 +321,8 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k return checkImageNameParseErr(log, err) } - if providers.IsEcrURL(ref.Context().RegistryStr()) { - ecrAuth, err := providers.GetECRAuthKeychain(context.Background(), ref.Context().RegistryStr()) + if rc.amazonProvider.IsEcrURL(ref.Context().RegistryStr()) { + ecrAuth, err := rc.amazonProvider.GetECRAuthKeychain(context.Background(), ref.Context().RegistryStr()) if err != nil { log.WithError(err).Error("Error while getting token") return store.UnknownError From 7a8a06dcb789a727ff0f9ea784226b80d02d5ce3 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Thu, 31 Oct 2024 16:49:16 +0300 Subject: [PATCH 12/24] clear README Signed-off-by: alexey.komyakov --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 4a68747..b449e32 100644 --- a/README.md +++ b/README.md @@ -161,8 +161,6 @@ Usage of k8s-image-availability-exporter: namespace label for checks -skip-registry-cert-verification whether to skip registries' certificate verification - -ecr-images-exists - whether images from ECR in your cluster ``` ## Metrics From e50c6c037728c1ad6aa8f72505f4c09b9dc2c680 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Thu, 31 Oct 2024 16:51:18 +0300 Subject: [PATCH 13/24] go mod tify Signed-off-by: alexey.komyakov --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 00a9e14..44dc049 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/flant/k8s-image-availability-exporter go 1.22.0 require ( - github.com/aws/aws-sdk-go-v2 v1.32.2 github.com/aws/aws-sdk-go-v2/config v1.28.0 github.com/aws/aws-sdk-go-v2/service/ecr v1.36.2 github.com/gammazero/deque v0.2.1 @@ -20,6 +19,7 @@ require ( ) require ( + github.com/aws/aws-sdk-go-v2 v1.32.2 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect From 53c286cb5f4f66a87ecbf3a9d8f8ec3267b6e031 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Fri, 1 Nov 2024 14:06:52 +0300 Subject: [PATCH 14/24] unified interface for providers Signed-off-by: alexey.komyakov --- pkg/providers/amazon.go | 24 +++++------------------- pkg/providers/provider.go | 22 ++++++++++++++++++++++ pkg/registry/checker.go | 22 +++++++++++----------- 3 files changed, 38 insertions(+), 30 deletions(-) create mode 100644 pkg/providers/provider.go diff --git a/pkg/providers/amazon.go b/pkg/providers/amazon.go index 3669a6b..815fef5 100644 --- a/pkg/providers/amazon.go +++ b/pkg/providers/amazon.go @@ -12,8 +12,7 @@ import ( ) type ECRProvider interface { - GetECRAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) - IsEcrURL(url string) bool + GetAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) } type awsECRProvider struct{} @@ -22,13 +21,8 @@ func NewECRProvider() ECRProvider { return &awsECRProvider{} } -func (p *awsECRProvider) GetECRAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) { - ecrDetails, err := parseECRDetails(registryStr) - if err != nil { - return nil, err - } - - ecrClient, err := awsRegionalClient(ctx, ecrDetails) +func (p *awsECRProvider) GetAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) { + ecrClient, err := awsRegionalClient(ctx, parseECRDetails(registryStr)) if err != nil { return nil, fmt.Errorf("error loading AWS config: %w", err) } @@ -60,17 +54,9 @@ func (p *awsECRProvider) GetECRAuthKeychain(ctx context.Context, registryStr str return &customKeychain{authenticator: auth}, nil } -func (p *awsECRProvider) IsEcrURL(url string) bool { - parts := strings.SplitN(url, ".", 5) - if len(parts) <= 3 || !strings.Contains(url, "amazonaws.com") { - return false - } - return true -} - -func parseECRDetails(registryStr string) (string, error) { +func parseECRDetails(registryStr string) string { parts := strings.SplitN(registryStr, ".", 5) - return parts[3], nil + return parts[3] } func awsRegionalClient(ctx context.Context, region string) (*ecr.Client, error) { diff --git a/pkg/providers/provider.go b/pkg/providers/provider.go new file mode 100644 index 0000000..a30093c --- /dev/null +++ b/pkg/providers/provider.go @@ -0,0 +1,22 @@ +package providers + +import ( + "context" + "fmt" + + "github.com/google/go-containerregistry/pkg/authn" +) + +type ContainerRegistryProvider interface { + GetAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) +} + +func FindProviderKeychain(ctx context.Context, registryStr string, providers []ContainerRegistryProvider) (authn.Keychain, error) { + for _, provider := range providers { + keychain, err := provider.GetAuthKeychain(ctx, registryStr) + if err == nil { + return keychain, nil + } + } + return nil, fmt.Errorf("can't find proper provider for URL: %s", registryStr) +} diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index dbda6f3..0859aa5 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -60,8 +60,6 @@ type Checker struct { cronJobsInformer batchv1informers.CronJobInformer secretsInformer corev1informers.SecretInformer - amazonProvider providers.ECRProvider - controllerIndexers ControllerIndexers ignoredImagesRegex []regexp.Regexp @@ -71,6 +69,8 @@ type Checker struct { kubeClient *kubernetes.Clientset config registryCheckerConfig + + providers []providers.ContainerRegistryProvider } func NewChecker( @@ -109,6 +109,8 @@ func NewChecker( roundTripper := transport.NewUserAgent(customTransport, fmt.Sprintf("k8s-image-availability-exporter/%s", version.Version)) + ECRProvider := providers.NewECRProvider() + rc := &Checker{ serviceAccountInformer: informerFactory.Core().V1().ServiceAccounts(), namespacesInformer: informerFactory.Core().V1().Namespaces(), @@ -118,8 +120,6 @@ func NewChecker( cronJobsInformer: informerFactory.Batch().V1().CronJobs(), secretsInformer: informerFactory.Core().V1().Secrets(), - amazonProvider: providers.NewECRProvider(), - ignoredImagesRegex: ignoredImages, registryTransport: roundTripper, @@ -131,6 +131,10 @@ func NewChecker( plainHTTP: plainHTTP, mirrorsMap: mirrorsMap, }, + + providers: []providers.ContainerRegistryProvider{ + ECRProvider, + }, } rc.imageStore = store.NewImageStore(rc.Check, checkBatchSize, failedCheckBatchSize) @@ -321,13 +325,9 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k return checkImageNameParseErr(log, err) } - if rc.amazonProvider.IsEcrURL(ref.Context().RegistryStr()) { - ecrAuth, err := rc.amazonProvider.GetECRAuthKeychain(context.Background(), ref.Context().RegistryStr()) - if err != nil { - log.WithError(err).Error("Error while getting token") - return store.UnknownError - } - kc = ecrAuth + kChain, err := providers.FindProviderKeychain(context.Background(), ref.Context().RegistryStr(), rc.providers) + if err == nil { + kc = kChain } imgErr := wait.ExponentialBackoff(wait.Backoff{ From 9e37e84cae689a391abf6851e58e765daac9a6ab Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Fri, 1 Nov 2024 18:03:51 +0300 Subject: [PATCH 15/24] fixes Signed-off-by: alexey.komyakov --- pkg/providers/{ => amazon}/amazon.go | 17 ++++++++--------- pkg/providers/provider.go | 17 +++++++++-------- pkg/registry/checker.go | 16 +++++----------- 3 files changed, 22 insertions(+), 28 deletions(-) rename pkg/providers/{ => amazon}/amazon.go (86%) diff --git a/pkg/providers/amazon.go b/pkg/providers/amazon/amazon.go similarity index 86% rename from pkg/providers/amazon.go rename to pkg/providers/amazon/amazon.go index 815fef5..e787606 100644 --- a/pkg/providers/amazon.go +++ b/pkg/providers/amazon/amazon.go @@ -1,4 +1,4 @@ -package providers +package amazon import ( "context" @@ -11,17 +11,13 @@ import ( "github.com/google/go-containerregistry/pkg/authn" ) -type ECRProvider interface { - GetAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) -} - -type awsECRProvider struct{} +type AwsECRProvider struct{} -func NewECRProvider() ECRProvider { - return &awsECRProvider{} +func NewECRProvider() *AwsECRProvider { + return &AwsECRProvider{} } -func (p *awsECRProvider) GetAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) { +func (p *AwsECRProvider) GetAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) { ecrClient, err := awsRegionalClient(ctx, parseECRDetails(registryStr)) if err != nil { return nil, fmt.Errorf("error loading AWS config: %w", err) @@ -56,6 +52,9 @@ func (p *awsECRProvider) GetAuthKeychain(ctx context.Context, registryStr string func parseECRDetails(registryStr string) string { parts := strings.SplitN(registryStr, ".", 5) + if len(parts) < 3 { + return "" + } return parts[3] } diff --git a/pkg/providers/provider.go b/pkg/providers/provider.go index a30093c..e0535d4 100644 --- a/pkg/providers/provider.go +++ b/pkg/providers/provider.go @@ -3,20 +3,21 @@ package providers import ( "context" "fmt" + "strings" + "github.com/flant/k8s-image-availability-exporter/pkg/providers/amazon" "github.com/google/go-containerregistry/pkg/authn" ) -type ContainerRegistryProvider interface { +type Provider interface { GetAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) } -func FindProviderKeychain(ctx context.Context, registryStr string, providers []ContainerRegistryProvider) (authn.Keychain, error) { - for _, provider := range providers { - keychain, err := provider.GetAuthKeychain(ctx, registryStr) - if err == nil { - return keychain, nil - } +func GetProvider(registryStr string) (Provider, error) { + switch { + case strings.Contains(registryStr, "amazonaws.com"): + return amazon.NewECRProvider(), nil + default: + return nil, fmt.Errorf("unsupported registry") } - return nil, fmt.Errorf("can't find proper provider for URL: %s", registryStr) } diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index 0859aa5..43b7e05 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -69,8 +69,6 @@ type Checker struct { kubeClient *kubernetes.Clientset config registryCheckerConfig - - providers []providers.ContainerRegistryProvider } func NewChecker( @@ -109,8 +107,6 @@ func NewChecker( roundTripper := transport.NewUserAgent(customTransport, fmt.Sprintf("k8s-image-availability-exporter/%s", version.Version)) - ECRProvider := providers.NewECRProvider() - rc := &Checker{ serviceAccountInformer: informerFactory.Core().V1().ServiceAccounts(), namespacesInformer: informerFactory.Core().V1().Namespaces(), @@ -131,10 +127,6 @@ func NewChecker( plainHTTP: plainHTTP, mirrorsMap: mirrorsMap, }, - - providers: []providers.ContainerRegistryProvider{ - ECRProvider, - }, } rc.imageStore = store.NewImageStore(rc.Check, checkBatchSize, failedCheckBatchSize) @@ -324,10 +316,12 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k if err != nil { return checkImageNameParseErr(log, err) } - - kChain, err := providers.FindProviderKeychain(context.Background(), ref.Context().RegistryStr(), rc.providers) + p, err := providers.GetProvider(ref.Context().RegistryStr()) if err == nil { - kc = kChain + kChain, err := p.GetAuthKeychain(context.TODO(), ref.Context().RegistryStr()) + if err == nil { + kc = kChain + } } imgErr := wait.ExponentialBackoff(wait.Backoff{ From bbc7e530212d31c17c5e10ef8208702d43dd5c61 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Tue, 5 Nov 2024 13:56:15 +0300 Subject: [PATCH 16/24] fixes Signed-off-by: alexey.komyakov --- pkg/providers/amazon/amazon.go | 29 ++++++++++++++++------------- pkg/providers/k8s/k8s.go | 29 +++++++++++++++++++++++++++++ pkg/providers/provider.go | 24 ++++++++++++++++++------ pkg/registry/checker.go | 29 +++++++++++++++-------------- pkg/registry/indexers.go | 13 ++----------- 5 files changed, 80 insertions(+), 44 deletions(-) create mode 100644 pkg/providers/k8s/k8s.go diff --git a/pkg/providers/amazon/amazon.go b/pkg/providers/amazon/amazon.go index e787606..bce0b23 100644 --- a/pkg/providers/amazon/amazon.go +++ b/pkg/providers/amazon/amazon.go @@ -3,51 +3,54 @@ package amazon import ( "context" "encoding/base64" - "fmt" "strings" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ecr" "github.com/google/go-containerregistry/pkg/authn" + "github.com/sirupsen/logrus" ) -type AwsECRProvider struct{} +type Provider struct{} -func NewECRProvider() *AwsECRProvider { - return &AwsECRProvider{} +func NewProvider() *Provider { + return &Provider{} } -func (p *AwsECRProvider) GetAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) { - ecrClient, err := awsRegionalClient(ctx, parseECRDetails(registryStr)) +func (p Provider) GetAuthKeychain(registryStr string) authn.Keychain { + ecrClient, err := awsRegionalClient(context.TODO(), parseECRDetails(registryStr)) if err != nil { - return nil, fmt.Errorf("error loading AWS config: %w", err) + logrus.Panic(err) } - authTokenOutput, err := ecrClient.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{}) + authTokenOutput, err := ecrClient.GetAuthorizationToken(context.TODO(), &ecr.GetAuthorizationTokenInput{}) if err != nil { - return nil, fmt.Errorf("error getting ECR authorization token: %w", err) + logrus.Panic(err) } if len(authTokenOutput.AuthorizationData) == 0 { - return nil, fmt.Errorf("no authorization data received from ECR") + logrus.Panic("no authorization data received from ECR") + return nil } authData := authTokenOutput.AuthorizationData[0] decodedToken, err := base64.StdEncoding.DecodeString(*authData.AuthorizationToken) if err != nil { - return nil, fmt.Errorf("error decoding authorization token: %w", err) + logrus.Panic("error decoding authorization token: %w", err) + return nil } credentials := strings.SplitN(string(decodedToken), ":", 2) if len(credentials) != 2 { - return nil, fmt.Errorf("invalid authorization token format") + logrus.Panic("invalid authorization token format") + return nil } authConfig := authn.AuthConfig{ Username: credentials[0], Password: credentials[1], } auth := authn.FromConfig(authConfig) - return &customKeychain{authenticator: auth}, nil + return &customKeychain{authenticator: auth} } func parseECRDetails(registryStr string) string { diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go new file mode 100644 index 0000000..2fc012b --- /dev/null +++ b/pkg/providers/k8s/k8s.go @@ -0,0 +1,29 @@ +package k8s + +import ( + "context" + kubeauth "github.com/google/go-containerregistry/pkg/authn/kubernetes" + "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + + "github.com/google/go-containerregistry/pkg/authn" +) + +type Provider struct { + pullSecretsGetter func(image string) []corev1.Secret +} + +func NewProvider(pullSecretsGetter func(image string) []corev1.Secret) *Provider { + return &Provider{ + pullSecretsGetter: pullSecretsGetter, + } +} + +func (p Provider) GetAuthKeychain(registryStr string) authn.Keychain { + dereferencedPullSecrets := p.pullSecretsGetter(registryStr) + kc, err := kubeauth.NewFromPullSecrets(context.TODO(), dereferencedPullSecrets) + if err != nil { + logrus.Panic(err) + } + return kc +} diff --git a/pkg/providers/provider.go b/pkg/providers/provider.go index e0535d4..074daf7 100644 --- a/pkg/providers/provider.go +++ b/pkg/providers/provider.go @@ -1,23 +1,35 @@ package providers import ( - "context" - "fmt" + corev1 "k8s.io/api/core/v1" "strings" "github.com/flant/k8s-image-availability-exporter/pkg/providers/amazon" + "github.com/flant/k8s-image-availability-exporter/pkg/providers/k8s" "github.com/google/go-containerregistry/pkg/authn" ) type Provider interface { - GetAuthKeychain(ctx context.Context, registryStr string) (authn.Keychain, error) + GetAuthKeychain(registryStr string) authn.Keychain } -func GetProvider(registryStr string) (Provider, error) { +type ProviderRegistry map[string]Provider + +func NewProviderChain(pullSecretsGetter func(image string) []corev1.Secret) ProviderRegistry { + return map[string]Provider{ + "amazon": amazon.NewProvider(), + "k8s": k8s.NewProvider(pullSecretsGetter), + } +} + +type ImagePullSecretsFunc func(image string) []corev1.Secret + +func (p ProviderRegistry) GetAuthKeychain(registryStr string) authn.Keychain { switch { case strings.Contains(registryStr, "amazonaws.com"): - return amazon.NewECRProvider(), nil + return p["amazon"].GetAuthKeychain(registryStr) + default: - return nil, fmt.Errorf("unsupported registry") + return p["k8s"].GetAuthKeychain(registryStr) } } diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index 43b7e05..45df139 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -6,18 +6,18 @@ import ( "crypto/x509" "errors" "fmt" - "net/http" - "os" - "regexp" - "strings" - "time" - "github.com/flant/k8s-image-availability-exporter/pkg/providers" "github.com/flant/k8s-image-availability-exporter/pkg/version" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/v1/remote/transport" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/tools/cache" + "net/http" + "os" + "regexp" + "strings" + "time" "github.com/prometheus/client_golang/prometheus" @@ -69,6 +69,8 @@ type Checker struct { kubeClient *kubernetes.Clientset config registryCheckerConfig + + providerRegistry providers.ProviderRegistry } func NewChecker( @@ -254,6 +256,12 @@ func NewChecker( rc.imageStore.RunGC(rc.controllerIndexers.GetContainerInfosForImage) + pullSecretsGetter := func(image string) []corev1.Secret { + return rc.controllerIndexers.GetImagePullSecrets(image) + } + pc := providers.NewProviderChain(pullSecretsGetter) + rc.providerRegistry = pc + return rc } @@ -291,7 +299,7 @@ imagesLoop: } func (rc *Checker) Check(imageName string) store.AvailabilityMode { - keyChain := rc.controllerIndexers.GetKeychainForImage(imageName) + keyChain := rc.providerRegistry.GetAuthKeychain(imageName) log := logrus.WithField("image_name", imageName) return rc.checkImageAvailability(log, imageName, keyChain) @@ -316,13 +324,6 @@ func (rc *Checker) checkImageAvailability(log *logrus.Entry, imageName string, k if err != nil { return checkImageNameParseErr(log, err) } - p, err := providers.GetProvider(ref.Context().RegistryStr()) - if err == nil { - kChain, err := p.GetAuthKeychain(context.TODO(), ref.Context().RegistryStr()) - if err == nil { - kc = kChain - } - } imgErr := wait.ExponentialBackoff(wait.Backoff{ Duration: time.Second, diff --git a/pkg/registry/indexers.go b/pkg/registry/indexers.go index 086e15e..c79a7a3 100644 --- a/pkg/registry/indexers.go +++ b/pkg/registry/indexers.go @@ -1,14 +1,11 @@ package registry import ( - "context" "fmt" "slices" "strings" "github.com/flant/k8s-image-availability-exporter/pkg/store" - "github.com/google/go-containerregistry/pkg/authn" - kubeauth "github.com/google/go-containerregistry/pkg/authn/kubernetes" "github.com/sirupsen/logrus" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" @@ -257,7 +254,7 @@ func (ci ControllerIndexers) GetContainerInfosForImage(image string) (ret []stor return } -func (ci ControllerIndexers) GetKeychainForImage(image string) authn.Keychain { +func (ci ControllerIndexers) GetImagePullSecrets(image string) []corev1.Secret { objs := ci.GetObjectsByImageIndex(image) var refSet = map[string]struct{}{} @@ -284,11 +281,5 @@ func (ci ControllerIndexers) GetKeychainForImage(image string) authn.Keychain { if len(dereferencedPullSecrets) == 0 { return nil } - - kc, err := kubeauth.NewFromPullSecrets(context.TODO(), dereferencedPullSecrets) - if err != nil { - logrus.Panic(err) - } - - return kc + return dereferencedPullSecrets } From 5a2a93e0059c231b8b969cde31ea8dd13a0ba66e Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Wed, 6 Nov 2024 08:57:02 +0300 Subject: [PATCH 17/24] logging Signed-off-by: alexey.komyakov --- pkg/providers/amazon/amazon.go | 20 +++++++++----------- pkg/providers/k8s/k8s.go | 8 ++++---- pkg/providers/provider.go | 4 ++-- pkg/registry/checker.go | 6 ++++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pkg/providers/amazon/amazon.go b/pkg/providers/amazon/amazon.go index bce0b23..0166fb3 100644 --- a/pkg/providers/amazon/amazon.go +++ b/pkg/providers/amazon/amazon.go @@ -4,11 +4,11 @@ import ( "context" "encoding/base64" "strings" + "fmt" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ecr" "github.com/google/go-containerregistry/pkg/authn" - "github.com/sirupsen/logrus" ) type Provider struct{} @@ -17,40 +17,38 @@ func NewProvider() *Provider { return &Provider{} } -func (p Provider) GetAuthKeychain(registryStr string) authn.Keychain { +func (p Provider) GetAuthKeychain(registryStr string) (authn.Keychain, error) { ecrClient, err := awsRegionalClient(context.TODO(), parseECRDetails(registryStr)) if err != nil { - logrus.Panic(err) + return nil, fmt.Errorf("error loading AWS config: %w", err) } authTokenOutput, err := ecrClient.GetAuthorizationToken(context.TODO(), &ecr.GetAuthorizationTokenInput{}) if err != nil { - logrus.Panic(err) + return nil, fmt.Errorf("error getting ECR authorization token: %w", err) } + if len(authTokenOutput.AuthorizationData) == 0 { - logrus.Panic("no authorization data received from ECR") - return nil + return nil, fmt.Errorf("no authorization data received from ECR") } authData := authTokenOutput.AuthorizationData[0] decodedToken, err := base64.StdEncoding.DecodeString(*authData.AuthorizationToken) if err != nil { - logrus.Panic("error decoding authorization token: %w", err) - return nil + return nil, fmt.Errorf("error decoding authorization token: %w", err) } credentials := strings.SplitN(string(decodedToken), ":", 2) if len(credentials) != 2 { - logrus.Panic("invalid authorization token format") - return nil + return nil, fmt.Errorf("invalid authorization token format") } authConfig := authn.AuthConfig{ Username: credentials[0], Password: credentials[1], } auth := authn.FromConfig(authConfig) - return &customKeychain{authenticator: auth} + return &customKeychain{authenticator: auth}, nil } func parseECRDetails(registryStr string) string { diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go index 2fc012b..7ab6f71 100644 --- a/pkg/providers/k8s/k8s.go +++ b/pkg/providers/k8s/k8s.go @@ -3,8 +3,8 @@ package k8s import ( "context" kubeauth "github.com/google/go-containerregistry/pkg/authn/kubernetes" - "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" + "fmt" "github.com/google/go-containerregistry/pkg/authn" ) @@ -19,11 +19,11 @@ func NewProvider(pullSecretsGetter func(image string) []corev1.Secret) *Provider } } -func (p Provider) GetAuthKeychain(registryStr string) authn.Keychain { +func (p Provider) GetAuthKeychain(registryStr string) (authn.Keychain, error) { dereferencedPullSecrets := p.pullSecretsGetter(registryStr) kc, err := kubeauth.NewFromPullSecrets(context.TODO(), dereferencedPullSecrets) if err != nil { - logrus.Panic(err) + return nil, fmt.Errorf("error while processing keychain from secrets: %w", err) } - return kc + return kc, nil } diff --git a/pkg/providers/provider.go b/pkg/providers/provider.go index 074daf7..a22394c 100644 --- a/pkg/providers/provider.go +++ b/pkg/providers/provider.go @@ -10,7 +10,7 @@ import ( ) type Provider interface { - GetAuthKeychain(registryStr string) authn.Keychain + GetAuthKeychain(registryStr string) (authn.Keychain, error) } type ProviderRegistry map[string]Provider @@ -24,7 +24,7 @@ func NewProviderChain(pullSecretsGetter func(image string) []corev1.Secret) Prov type ImagePullSecretsFunc func(image string) []corev1.Secret -func (p ProviderRegistry) GetAuthKeychain(registryStr string) authn.Keychain { +func (p ProviderRegistry) GetAuthKeychain(registryStr string) (authn.Keychain, error) { switch { case strings.Contains(registryStr, "amazonaws.com"): return p["amazon"].GetAuthKeychain(registryStr) diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index 45df139..de6a2bd 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -299,8 +299,10 @@ imagesLoop: } func (rc *Checker) Check(imageName string) store.AvailabilityMode { - keyChain := rc.providerRegistry.GetAuthKeychain(imageName) - + keyChain, err := rc.providerRegistry.GetAuthKeychain(imageName) + if err != nil { + panic(err) + } log := logrus.WithField("image_name", imageName) return rc.checkImageAvailability(log, imageName, keyChain) } From 0a530c696411f36392f693be665920886d0b982d Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Wed, 6 Nov 2024 09:02:13 +0300 Subject: [PATCH 18/24] linter Signed-off-by: alexey.komyakov --- pkg/providers/amazon/amazon.go | 3 +-- pkg/providers/k8s/k8s.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/providers/amazon/amazon.go b/pkg/providers/amazon/amazon.go index 0166fb3..c53c0fc 100644 --- a/pkg/providers/amazon/amazon.go +++ b/pkg/providers/amazon/amazon.go @@ -3,8 +3,8 @@ package amazon import ( "context" "encoding/base64" - "strings" "fmt" + "strings" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ecr" @@ -28,7 +28,6 @@ func (p Provider) GetAuthKeychain(registryStr string) (authn.Keychain, error) { return nil, fmt.Errorf("error getting ECR authorization token: %w", err) } - if len(authTokenOutput.AuthorizationData) == 0 { return nil, fmt.Errorf("no authorization data received from ECR") } diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go index 7ab6f71..fdd7292 100644 --- a/pkg/providers/k8s/k8s.go +++ b/pkg/providers/k8s/k8s.go @@ -2,9 +2,9 @@ package k8s import ( "context" + "fmt" kubeauth "github.com/google/go-containerregistry/pkg/authn/kubernetes" corev1 "k8s.io/api/core/v1" - "fmt" "github.com/google/go-containerregistry/pkg/authn" ) From aec81312cc0514d9be1afbf570bec54b18d130e4 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Wed, 6 Nov 2024 11:51:31 +0300 Subject: [PATCH 19/24] error handling Signed-off-by: alexey.komyakov --- pkg/registry/checker.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index de6a2bd..f78c3d4 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -301,7 +301,8 @@ imagesLoop: func (rc *Checker) Check(imageName string) store.AvailabilityMode { keyChain, err := rc.providerRegistry.GetAuthKeychain(imageName) if err != nil { - panic(err) + logrus.Warn("error while getting keychain for: ", err) + return store.UnknownError } log := logrus.WithField("image_name", imageName) return rc.checkImageAvailability(log, imageName, keyChain) From 48bc4bfdd6e9c1040e1ffe776096077c2de42087 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Mon, 11 Nov 2024 14:11:57 +0300 Subject: [PATCH 20/24] specify authn failure instead of UnknownError Signed-off-by: alexey.komyakov --- pkg/registry/checker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index f78c3d4..b7bb883 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -302,7 +302,7 @@ func (rc *Checker) Check(imageName string) store.AvailabilityMode { keyChain, err := rc.providerRegistry.GetAuthKeychain(imageName) if err != nil { logrus.Warn("error while getting keychain for: ", err) - return store.UnknownError + return store.AuthnFailure } log := logrus.WithField("image_name", imageName) return rc.checkImageAvailability(log, imageName, keyChain) From 9af74961fcc7f2adb7cdf0b106ddb9d09a0699cc Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Mon, 18 Nov 2024 16:34:17 +0300 Subject: [PATCH 21/24] some fixes Signed-off-by: alexey.komyakov --- go.mod | 4 +++ go.sum | 19 +++++++++++ pkg/providers/amazon/amazon.go | 58 +++++++++++++++++----------------- pkg/providers/k8s/k8s.go | 4 +-- pkg/providers/provider.go | 24 ++++++++------ pkg/registry/checker.go | 7 +--- 6 files changed, 70 insertions(+), 46 deletions(-) diff --git a/go.mod b/go.mod index 44dc049..0e83abd 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/flant/k8s-image-availability-exporter go 1.22.0 require ( + github.com/aws/aws-node-termination-handler v1.22.1 github.com/aws/aws-sdk-go-v2/config v1.28.0 github.com/aws/aws-sdk-go-v2/service/ecr v1.36.2 github.com/gammazero/deque v0.2.1 @@ -56,6 +57,8 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -67,6 +70,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/rs/zerolog v1.29.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/vbatts/tar-split v0.11.5 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/go.sum b/go.sum index cf7948e..b06fc22 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/aws/aws-node-termination-handler v1.22.1 h1:I9veOuJgRbHTuSGW575M3qVkjIWQg2lycALvl5tDh34= +github.com/aws/aws-node-termination-handler v1.22.1/go.mod h1:iZL1G2QRvIiK2ac7i7iy05dbvh4OHa6NtBRuSS527Fw= +github.com/aws/aws-sdk-go v1.55.4 h1:u7sFWQQs5ivGuYvCxi7gJI8nN/P9Dq04huLaw39a4lg= +github.com/aws/aws-sdk-go v1.55.4/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI= github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= github.com/aws/aws-sdk-go-v2/config v1.28.0 h1:FosVYWcqEtWNxHn8gB/Vs6jOlNwSoyOCA/g/sxyySOQ= @@ -32,6 +36,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -58,6 +63,7 @@ github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZC github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -102,6 +108,13 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -134,6 +147,9 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= +github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -170,7 +186,10 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= diff --git a/pkg/providers/amazon/amazon.go b/pkg/providers/amazon/amazon.go index c53c0fc..2efcc43 100644 --- a/pkg/providers/amazon/amazon.go +++ b/pkg/providers/amazon/amazon.go @@ -5,27 +5,35 @@ import ( "encoding/base64" "fmt" "strings" + "time" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ecr" "github.com/google/go-containerregistry/pkg/authn" + "github.com/aws/aws-node-termination-handler/pkg/ec2metadata" ) - -type Provider struct{} +type Provider struct { + ecrClient *ecr.Client + authToken *authn.AuthConfig + authTokenExpiry time.Time +} func NewProvider() *Provider { - return &Provider{} + cfg, _ := config.LoadDefaultConfig(context.TODO(), config.WithRegion(requestEC2Region())) + ecrClient := ecr.NewFromConfig(cfg) + return &Provider{ecrClient: ecrClient} } -func (p Provider) GetAuthKeychain(registryStr string) (authn.Keychain, error) { - ecrClient, err := awsRegionalClient(context.TODO(), parseECRDetails(registryStr)) - if err != nil { - return nil, fmt.Errorf("error loading AWS config: %w", err) +func (p *Provider) GetAuthKeychain(_ string) (authn.Keychain, error) { + const bufferPeriod = time.Hour + + if p.authToken != nil && time.Now().Before(p.authTokenExpiry.Add(-bufferPeriod)) { + return &customKeychain{authenticator: authn.FromConfig(*p.authToken)}, nil } - authTokenOutput, err := ecrClient.GetAuthorizationToken(context.TODO(), &ecr.GetAuthorizationTokenInput{}) + authTokenOutput, err := p.ecrClient.GetAuthorizationToken(context.TODO(), &ecr.GetAuthorizationTokenInput{}) if err != nil { - return nil, fmt.Errorf("error getting ECR authorization token: %w", err) + return nil, err } if len(authTokenOutput.AuthorizationData) == 0 { @@ -35,43 +43,35 @@ func (p Provider) GetAuthKeychain(registryStr string) (authn.Keychain, error) { authData := authTokenOutput.AuthorizationData[0] decodedToken, err := base64.StdEncoding.DecodeString(*authData.AuthorizationToken) if err != nil { - return nil, fmt.Errorf("error decoding authorization token: %w", err) + return nil, err } credentials := strings.SplitN(string(decodedToken), ":", 2) if len(credentials) != 2 { return nil, fmt.Errorf("invalid authorization token format") } - authConfig := authn.AuthConfig{ + + p.authToken = &authn.AuthConfig{ Username: credentials[0], Password: credentials[1], } - auth := authn.FromConfig(authConfig) - return &customKeychain{authenticator: auth}, nil -} + p.authTokenExpiry = *authData.ExpiresAt -func parseECRDetails(registryStr string) string { - parts := strings.SplitN(registryStr, ".", 5) - if len(parts) < 3 { - return "" - } - return parts[3] + return &customKeychain{authenticator: authn.FromConfig(*p.authToken)}, nil } -func awsRegionalClient(ctx context.Context, region string) (*ecr.Client, error) { - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) - if err != nil { - return nil, err - } + +func requestEC2Region() string { + ec2metadataClient := ec2metadata.New("http://169.254.169.254", 1) + metadata := ec2metadataClient.GetNodeMetadata() - client := ecr.NewFromConfig(cfg) - return client, nil + return metadata.Region } type customKeychain struct { authenticator authn.Authenticator } -func (kc *customKeychain) Resolve(_ authn.Resource) (authn.Authenticator, error) { - return kc.authenticator, nil +func (c *customKeychain) Resolve(resource authn.Resource) (authn.Authenticator, error) { + return c.authenticator, nil } diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go index fdd7292..87d6c66 100644 --- a/pkg/providers/k8s/k8s.go +++ b/pkg/providers/k8s/k8s.go @@ -19,8 +19,8 @@ func NewProvider(pullSecretsGetter func(image string) []corev1.Secret) *Provider } } -func (p Provider) GetAuthKeychain(registryStr string) (authn.Keychain, error) { - dereferencedPullSecrets := p.pullSecretsGetter(registryStr) +func (p Provider) GetAuthKeychain(registry string) (authn.Keychain, error) { + dereferencedPullSecrets := p.pullSecretsGetter(registry) kc, err := kubeauth.NewFromPullSecrets(context.TODO(), dereferencedPullSecrets) if err != nil { return nil, fmt.Errorf("error while processing keychain from secrets: %w", err) diff --git a/pkg/providers/provider.go b/pkg/providers/provider.go index a22394c..ddf6ee6 100644 --- a/pkg/providers/provider.go +++ b/pkg/providers/provider.go @@ -2,7 +2,7 @@ package providers import ( corev1 "k8s.io/api/core/v1" - "strings" + "regexp" "github.com/flant/k8s-image-availability-exporter/pkg/providers/amazon" "github.com/flant/k8s-image-availability-exporter/pkg/providers/k8s" @@ -10,26 +10,32 @@ import ( ) type Provider interface { - GetAuthKeychain(registryStr string) (authn.Keychain, error) + GetAuthKeychain(registry string) (authn.Keychain, error) } type ProviderRegistry map[string]Provider func NewProviderChain(pullSecretsGetter func(image string) []corev1.Secret) ProviderRegistry { + amazonProvider := amazon.NewProvider() + k8sProvider := k8s.NewProvider(pullSecretsGetter) + return map[string]Provider{ - "amazon": amazon.NewProvider(), - "k8s": k8s.NewProvider(pullSecretsGetter), + "amazon": amazonProvider, + "k8s": k8sProvider, } } type ImagePullSecretsFunc func(image string) []corev1.Secret -func (p ProviderRegistry) GetAuthKeychain(registryStr string) (authn.Keychain, error) { - switch { - case strings.Contains(registryStr, "amazonaws.com"): - return p["amazon"].GetAuthKeychain(registryStr) +var ( + amazonURLRegex = regexp.MustCompile(`^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com(?:\.cn)?/([^:]+):(.+)$`) +) +func (p ProviderRegistry) GetAuthKeychain(registry string) (authn.Keychain, error) { + switch { + case amazonURLRegex.MatchString(registry): + return p["amazon"].GetAuthKeychain(registry) default: - return p["k8s"].GetAuthKeychain(registryStr) + return p["k8s"].GetAuthKeychain(registry) } } diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index b7bb883..b7b8927 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -10,7 +10,6 @@ import ( "github.com/flant/k8s-image-availability-exporter/pkg/version" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/v1/remote/transport" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/tools/cache" "net/http" @@ -256,11 +255,7 @@ func NewChecker( rc.imageStore.RunGC(rc.controllerIndexers.GetContainerInfosForImage) - pullSecretsGetter := func(image string) []corev1.Secret { - return rc.controllerIndexers.GetImagePullSecrets(image) - } - pc := providers.NewProviderChain(pullSecretsGetter) - rc.providerRegistry = pc + rc.providerRegistry = providers.NewProviderChain(rc.controllerIndexers.GetImagePullSecrets) return rc } From 323e92e5b7bcba50b8cd57415bf26032e019f7cb Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Mon, 18 Nov 2024 16:35:07 +0300 Subject: [PATCH 22/24] lint Signed-off-by: alexey.komyakov --- pkg/providers/amazon/amazon.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/providers/amazon/amazon.go b/pkg/providers/amazon/amazon.go index 2efcc43..cc1b289 100644 --- a/pkg/providers/amazon/amazon.go +++ b/pkg/providers/amazon/amazon.go @@ -7,14 +7,15 @@ import ( "strings" "time" + "github.com/aws/aws-node-termination-handler/pkg/ec2metadata" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ecr" "github.com/google/go-containerregistry/pkg/authn" - "github.com/aws/aws-node-termination-handler/pkg/ec2metadata" ) + type Provider struct { - ecrClient *ecr.Client - authToken *authn.AuthConfig + ecrClient *ecr.Client + authToken *authn.AuthConfig authTokenExpiry time.Time } @@ -60,7 +61,6 @@ func (p *Provider) GetAuthKeychain(_ string) (authn.Keychain, error) { return &customKeychain{authenticator: authn.FromConfig(*p.authToken)}, nil } - func requestEC2Region() string { ec2metadataClient := ec2metadata.New("http://169.254.169.254", 1) metadata := ec2metadataClient.GetNodeMetadata() From de6dd8cfd314075cf226b500d8c08cfab1f87f93 Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Mon, 18 Nov 2024 16:45:54 +0300 Subject: [PATCH 23/24] fixes Signed-off-by: alexey.komyakov --- pkg/providers/amazon/amazon.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/providers/amazon/amazon.go b/pkg/providers/amazon/amazon.go index cc1b289..b9c4520 100644 --- a/pkg/providers/amazon/amazon.go +++ b/pkg/providers/amazon/amazon.go @@ -72,6 +72,6 @@ type customKeychain struct { authenticator authn.Authenticator } -func (c *customKeychain) Resolve(resource authn.Resource) (authn.Authenticator, error) { - return c.authenticator, nil +func (kc *customKeychain) Resolve(_ authn.Resource) (authn.Authenticator, error) { + return kc.authenticator, nil } From 6953797ece36cc4cca64c4036204b3353b864e8f Mon Sep 17 00:00:00 2001 From: "alexey.komyakov" Date: Tue, 26 Nov 2024 15:43:59 +0300 Subject: [PATCH 24/24] fixes Signed-off-by: alexey.komyakov --- pkg/providers/amazon/amazon.go | 28 +++++++++++++++++++++------- pkg/providers/k8s/k8s.go | 5 +++++ pkg/providers/provider.go | 15 +++++++-------- pkg/registry/checker.go | 9 +++++++-- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/pkg/providers/amazon/amazon.go b/pkg/providers/amazon/amazon.go index b9c4520..718482e 100644 --- a/pkg/providers/amazon/amazon.go +++ b/pkg/providers/amazon/amazon.go @@ -11,25 +11,30 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ecr" "github.com/google/go-containerregistry/pkg/authn" + "github.com/sirupsen/logrus" ) type Provider struct { ecrClient *ecr.Client - authToken *authn.AuthConfig + authToken authn.AuthConfig authTokenExpiry time.Time + name string } func NewProvider() *Provider { - cfg, _ := config.LoadDefaultConfig(context.TODO(), config.WithRegion(requestEC2Region())) + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(requestEC2Region())) + if err != nil { + logrus.Warn("error while loading config for new aws provider ", err) + } ecrClient := ecr.NewFromConfig(cfg) - return &Provider{ecrClient: ecrClient} + return &Provider{ecrClient: ecrClient, name: "amazon"} } func (p *Provider) GetAuthKeychain(_ string) (authn.Keychain, error) { const bufferPeriod = time.Hour - if p.authToken != nil && time.Now().Before(p.authTokenExpiry.Add(-bufferPeriod)) { - return &customKeychain{authenticator: authn.FromConfig(*p.authToken)}, nil + if p.authToken.Username != "" && time.Now().Before(p.authTokenExpiry.Add(-bufferPeriod)) { + return &customKeychain{authenticator: authn.FromConfig(p.authToken)}, nil } authTokenOutput, err := p.ecrClient.GetAuthorizationToken(context.TODO(), &ecr.GetAuthorizationTokenInput{}) @@ -42,6 +47,11 @@ func (p *Provider) GetAuthKeychain(_ string) (authn.Keychain, error) { } authData := authTokenOutput.AuthorizationData[0] + + if authData.AuthorizationToken == nil || *authData.AuthorizationToken == "" { + return nil, fmt.Errorf("authorization token is missing or empty") + } + decodedToken, err := base64.StdEncoding.DecodeString(*authData.AuthorizationToken) if err != nil { return nil, err @@ -52,13 +62,13 @@ func (p *Provider) GetAuthKeychain(_ string) (authn.Keychain, error) { return nil, fmt.Errorf("invalid authorization token format") } - p.authToken = &authn.AuthConfig{ + p.authToken = authn.AuthConfig{ Username: credentials[0], Password: credentials[1], } p.authTokenExpiry = *authData.ExpiresAt - return &customKeychain{authenticator: authn.FromConfig(*p.authToken)}, nil + return &customKeychain{authenticator: authn.FromConfig(p.authToken)}, nil } func requestEC2Region() string { @@ -75,3 +85,7 @@ type customKeychain struct { func (kc *customKeychain) Resolve(_ authn.Resource) (authn.Authenticator, error) { return kc.authenticator, nil } + +func (p Provider) GetName() string { + return p.name +} diff --git a/pkg/providers/k8s/k8s.go b/pkg/providers/k8s/k8s.go index 87d6c66..449125e 100644 --- a/pkg/providers/k8s/k8s.go +++ b/pkg/providers/k8s/k8s.go @@ -11,11 +11,13 @@ import ( type Provider struct { pullSecretsGetter func(image string) []corev1.Secret + name string } func NewProvider(pullSecretsGetter func(image string) []corev1.Secret) *Provider { return &Provider{ pullSecretsGetter: pullSecretsGetter, + name: "k8s", } } @@ -27,3 +29,6 @@ func (p Provider) GetAuthKeychain(registry string) (authn.Keychain, error) { } return kc, nil } +func (p Provider) GetName() string { + return p.name +} diff --git a/pkg/providers/provider.go b/pkg/providers/provider.go index ddf6ee6..c7ee052 100644 --- a/pkg/providers/provider.go +++ b/pkg/providers/provider.go @@ -4,25 +4,24 @@ import ( corev1 "k8s.io/api/core/v1" "regexp" - "github.com/flant/k8s-image-availability-exporter/pkg/providers/amazon" - "github.com/flant/k8s-image-availability-exporter/pkg/providers/k8s" "github.com/google/go-containerregistry/pkg/authn" ) type Provider interface { + GetName() string GetAuthKeychain(registry string) (authn.Keychain, error) } type ProviderRegistry map[string]Provider -func NewProviderChain(pullSecretsGetter func(image string) []corev1.Secret) ProviderRegistry { - amazonProvider := amazon.NewProvider() - k8sProvider := k8s.NewProvider(pullSecretsGetter) +func NewProviderChain(providers ...Provider) ProviderRegistry { + p := make(ProviderRegistry) - return map[string]Provider{ - "amazon": amazonProvider, - "k8s": k8sProvider, + for _, provider := range providers { + p[provider.GetName()] = provider } + + return p } type ImagePullSecretsFunc func(image string) []corev1.Secret diff --git a/pkg/registry/checker.go b/pkg/registry/checker.go index b7b8927..a71faa1 100644 --- a/pkg/registry/checker.go +++ b/pkg/registry/checker.go @@ -7,6 +7,8 @@ import ( "errors" "fmt" "github.com/flant/k8s-image-availability-exporter/pkg/providers" + "github.com/flant/k8s-image-availability-exporter/pkg/providers/amazon" + "github.com/flant/k8s-image-availability-exporter/pkg/providers/k8s" "github.com/flant/k8s-image-availability-exporter/pkg/version" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/v1/remote/transport" @@ -254,8 +256,11 @@ func NewChecker( logrus.Info("Caches populated successfully") rc.imageStore.RunGC(rc.controllerIndexers.GetContainerInfosForImage) - - rc.providerRegistry = providers.NewProviderChain(rc.controllerIndexers.GetImagePullSecrets) + registry := providers.NewProviderChain( + amazon.NewProvider(), + k8s.NewProvider(rc.controllerIndexers.GetImagePullSecrets), + ) + rc.providerRegistry = registry return rc }