Skip to content

Consider all SHA's of a manifest #355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
16 changes: 8 additions & 8 deletions cmd/app/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,14 @@ func (o *Options) addAppFlags(fs *pflag.FlagSet) {
"If enabled, all containers will be tested, unless they have the "+
fmt.Sprintf(`annotation "%s/${my-container}=false".`, api.EnableAnnotationKey))

fs.DurationVarP(&o.CacheTimeout,
"image-cache-timeout", "c", time.Minute*30,
"The time for an image version in the cache to be considered fresh. Images "+
"will be rechecked after this interval.")

fs.StringVarP(&o.LogLevel,
"log-level", "v", "info",
"Log level (debug, info, warn, error, fatal, panic).")

fs.DurationVarP(&o.GracefulShutdownTimeout,
"graceful-shutdown-timeout", "", 10*time.Second,
"Time that the manager should wait for all controller to shutdown.")
fs.DurationVarP(&o.CacheTimeout,
"image-cache-timeout", "c", time.Minute*30,
"The time for an image version in the cache to be considered fresh. Images "+
"will be rechecked after this interval.")

fs.DurationVarP(&o.RequeueDuration,
"requeue-duration", "r", time.Hour,
Expand All @@ -146,6 +142,10 @@ func (o *Options) addAppFlags(fs *pflag.FlagSet) {
fs.DurationVarP(&o.CacheSyncPeriod,
"cache-sync-period", "", 5*time.Hour,
"The time in which all resources should be updated.")

fs.DurationVarP(&o.GracefulShutdownTimeout,
"graceful-shutdown-timeout", "", 10*time.Second,
"Time that the manager should wait for all controller to shutdown.")
}

func (o *Options) addAuthFlags(fs *pflag.FlagSet) {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ require (
github.com/bombsimon/logrusr/v4 v4.1.0
github.com/go-chi/transport v0.5.0
github.com/gofri/go-github-ratelimit v1.1.1
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.20.3
github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20250225234217-098045d5e61f
github.com/google/go-github/v70 v70.0.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/jarcoal/httpmock v1.4.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/stretchr/testify v1.10.0
golang.org/x/time v0.11.0
sigs.k8s.io/controller-runtime v0.20.4
)

Expand Down Expand Up @@ -92,6 +92,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20241111191718-6bce25ecf029 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
Expand Down Expand Up @@ -128,7 +129,6 @@ require (
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.11.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
Expand Down
16 changes: 8 additions & 8 deletions pkg/api/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import "regexp"
type Options struct {
OverrideURL *string `json:"override-url,omitempty"`

MatchRegex *string `json:"match-regex,omitempty"`

PinMajor *int64 `json:"pin-major,omitempty"`
PinMinor *int64 `json:"pin-minor,omitempty"`
PinPatch *int64 `json:"pin-patch,omitempty"`

RegexMatcher *regexp.Regexp `json:"-"`

// UseSHA cannot be used with any other options
UseSHA bool `json:"use-sha,omitempty"`
// Resolve SHA to a TAG
ResolveSHAToTags bool `json:"resolve-sha-to-tags,omitempty"`

MatchRegex *string `json:"match-regex,omitempty"`

// UseMetaData defines whether tags with '-alpha', '-debian.0' etc. is
// permissible.
UseMetaData bool `json:"use-metadata,omitempty"`

PinMajor *int64 `json:"pin-major,omitempty"`
PinMinor *int64 `json:"pin-minor,omitempty"`
PinPatch *int64 `json:"pin-patch,omitempty"`

RegexMatcher *regexp.Regexp `json:"-"`
}
20 changes: 20 additions & 0 deletions pkg/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ type ImageTag struct {
Timestamp time.Time `json:"timestamp"`
OS OS `json:"os,omitempty"`
Architecture Architecture `json:"architecture,omitempty"`

// If this is a Manifest list we need to keep them together
Children []*ImageTag `json:"children,omitempty"`
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect that it would be better to represent this as

type ImageTag struct {
    Tag string
    Manifests []Manifest
}

type Manifest {
   SHA string
   Timestamp time.Time
   OS OS
   Architecture Architecture
}


func (i *ImageTag) MatchesSHA(sha string) bool {
if sha == i.SHA {
return true
}
for _, known := range i.Children {
if known.SHA == sha {
return true
}
}
return false
}

type Platform struct {
OS OS
Architecture Architecture
}

type OS string
Expand Down
205 changes: 24 additions & 181 deletions pkg/client/acr/acr.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,26 @@ import (
"io"
"net/http"
"sync"
"time"

"github.com/MicahParks/keyfunc/v3"

"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/golang-jwt/jwt/v5"

"github.com/jetstack/version-checker/pkg/api"
"github.com/jetstack/version-checker/pkg/client/util"
)

// Ensure that we are an ImageClient
var _ api.ImageClient = (*Client)(nil)

const (
userAgent = "jetstack/version-checker"
requiredScope = "repository:*:metadata_read"
)

type Client struct {
Options

cacheMu sync.Mutex
cachedACRClient map[string]*acrClient
}
Options

type acrClient struct {
tokenExpiry time.Time
*autorest.Client
cacheMu sync.Mutex
}

type Options struct {
Expand All @@ -44,18 +37,6 @@ type Options struct {
JWKSURI string
}

type AccessTokenResponse struct {
AccessToken string `json:"access_token"`
}

type ManifestResponse struct {
Manifests []struct {
Digest string `json:"digest"`
CreatedTime time.Time `json:"createdTime"`
Tags []string `json:"tags"`
} `json:"manifests"`
}

func New(opts Options) (*Client, error) {
if len(opts.RefreshToken) > 0 &&
(len(opts.Username) > 0 || len(opts.Password) > 0) {
Expand Down Expand Up @@ -90,27 +71,33 @@ func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.Imag
host, err)
}

var tags []api.ImageTag
// Create a map of tags, so that when we come up with additional Tags
// we can add them as Children
tags := map[string]api.ImageTag{}

for _, manifest := range manifestResp.Manifests {
if len(manifest.Tags) == 0 {
tags = append(tags, api.ImageTag{
SHA: manifest.Digest,
Timestamp: manifest.CreatedTime,
})
// Base data shared across tags
base := api.ImageTag{
SHA: manifest.Digest,
Timestamp: manifest.CreatedTime,
OS: manifest.OS,
Architecture: manifest.Architecture,
}

// No tags, use digest as the key
if len(manifest.Tags) == 0 {
tags[base.SHA] = base
continue
}

for _, tag := range manifest.Tags {
tags = append(tags, api.ImageTag{
SHA: manifest.Digest,
Timestamp: manifest.CreatedTime,
Tag: tag,
})
current := base // copy the base
current.Tag = tag // set tag value

util.BuildTags(tags, tag, &current)
}
}

return tags, nil
return util.TagMaptoList(tags), nil
}

func (c *Client) getManifestsWithClient(ctx context.Context, client *acrClient, host, repo, image string) (*http.Response, error) {
Expand Down Expand Up @@ -148,147 +135,3 @@ func (c *Client) getManifestsWithClient(ctx context.Context, client *acrClient,

return resp, nil
}

func (c *Client) getACRClient(ctx context.Context, host string) (*acrClient, error) {
c.cacheMu.Lock()
defer c.cacheMu.Unlock()

if client, ok := c.cachedACRClient[host]; ok && time.Now().After(client.tokenExpiry) {
return client, nil
}

var (
client *acrClient
accessTokenClient *autorest.Client
accessTokenReq *http.Request
err error
)
if len(c.RefreshToken) > 0 {
accessTokenClient, accessTokenReq, err = c.getAccessTokenRequesterForRefreshToken(ctx, host)
} else {
accessTokenClient, accessTokenReq, err = c.getAccessTokenRequesterForBasicAuth(ctx, host)
}
if err != nil {
return nil, err
}
if client, err = c.getAuthorizedClient(accessTokenClient, accessTokenReq, host); err != nil {
return nil, err
}

c.cachedACRClient[host] = client

return client, nil
}

func (c *Client) getAccessTokenRequesterForBasicAuth(ctx context.Context, host string) (*autorest.Client, *http.Request, error) {
client := autorest.NewClientWithUserAgent(userAgent)
client.Authorizer = autorest.NewBasicAuthorizer(c.Username, c.Password)
urlParameters := map[string]interface{}{
"url": "https://" + host,
}

preparer := autorest.CreatePreparer(
autorest.WithCustomBaseURL("{url}", urlParameters),
autorest.WithPath("/oauth2/token"),
autorest.WithQueryParameters(map[string]interface{}{
"scope": requiredScope,
"service": host,
}),
)
req, err := preparer.Prepare((&http.Request{}).WithContext(ctx))
if err != nil {
return nil, nil, err
}

return &client, req, nil
}

func (c *Client) getAccessTokenRequesterForRefreshToken(ctx context.Context, host string) (*autorest.Client, *http.Request, error) {
client := autorest.NewClientWithUserAgent(userAgent)
urlParameters := map[string]interface{}{
"url": "https://" + host,
}

formDataParameters := map[string]interface{}{
"grant_type": "refresh_token",
"refresh_token": c.RefreshToken,
"scope": requiredScope,
"service": host,
}

preparer := autorest.CreatePreparer(
autorest.AsPost(),
autorest.WithCustomBaseURL("{url}", urlParameters),
autorest.WithPath("/oauth2/token"),
autorest.WithFormData(autorest.MapToValues(formDataParameters)))
req, err := preparer.Prepare((&http.Request{}).WithContext(ctx))
if err != nil {
return nil, nil, err
}
return &client, req, nil
}

func (c *Client) getAuthorizedClient(client *autorest.Client, req *http.Request, host string) (*acrClient, error) {
resp, err := autorest.SendWithSender(client, req,
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...),
)
if err != nil {
return nil, fmt.Errorf("%s: failed to request access token: %s",
host, err)
}
defer func() { _ = resp.Body.Close() }()

var respToken AccessTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&respToken); err != nil {
return nil, fmt.Errorf("%s: failed to decode access token response: %s",
host, err)
}

exp, err := c.getTokenExpiration(respToken.AccessToken)
if err != nil {
return nil, fmt.Errorf("%s: %s", host, err)
}

token := &adal.Token{
RefreshToken: "", // empty if access_token was retrieved with basic auth. but client is not reused after expiry anyway (see cachedACRClient)
AccessToken: respToken.AccessToken,
}

client.Authorizer = autorest.NewBearerAuthorizer(token)

return &acrClient{
tokenExpiry: exp,
Client: client,
}, nil
}

func (c *Client) getTokenExpiration(tokenString string) (time.Time, error) {
jwtParser := jwt.NewParser(jwt.WithoutClaimsValidation())
var token *jwt.Token
var err error
if c.JWKSURI != "" {
var k keyfunc.Keyfunc
k, err = keyfunc.NewDefaultCtx(context.TODO(), []string{c.JWKSURI})
if err != nil {
return time.Time{}, err
}
token, err = jwtParser.Parse(tokenString, k.Keyfunc)
} else {
token, _, err = jwtParser.ParseUnverified(tokenString, jwt.MapClaims{})
}
if err != nil {
return time.Time{}, err
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return time.Time{}, fmt.Errorf("failed to process claims in access token")
}

if exp, ok := claims["exp"].(float64); ok {
timestamp := time.Unix(int64(exp), 0)
return timestamp, nil
}

return time.Time{}, fmt.Errorf("failed to find 'exp' claim in access token")
}
Loading
Loading