Skip to content

Commit 9d9b7bd

Browse files
committed
Implement AuthN and K8S ServiceAccount DockerFile Keychain support
1 parent be5d0ad commit 9d9b7bd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+2902
-2129
lines changed

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ WORKDIR /app/
66
RUN go mod download -x
77

88
ARG TARGETOS TARGETARCH
9-
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o ./bin/version-checker ./cmd/.
9+
ENV CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH
10+
RUN go build -o ./bin/version-checker ./cmd/.
1011

1112

1213
FROM alpine:3.21.3

cmd/app/app.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ func NewCommand(ctx context.Context) *cobra.Command {
3737
Use: "version-checker",
3838
Short: helpOutput,
3939
Long: helpOutput,
40+
PreRunE: func(cmd *cobra.Command, args []string) error {
41+
return opts.complete()
42+
},
4043
RunE: func(_ *cobra.Command, _ []string) error {
41-
opts.complete()
4244

4345
logLevel, err := logrus.ParseLevel(opts.LogLevel)
4446
if err != nil {
@@ -105,14 +107,14 @@ func NewCommand(ctx context.Context) *cobra.Command {
105107
metricsServer.RoundTripper,
106108
)
107109

108-
client, err := client.New(ctx, log, opts.Client)
110+
clientManager, err := client.NewManager(ctx, log, mgr.GetConfig(), opts.Client)
109111
if err != nil {
110112
return fmt.Errorf("failed to setup image registry clients: %s", err)
111113
}
112114

113115
c := controller.NewPodReconciler(opts.CacheTimeout,
114116
metricsServer,
115-
client,
117+
clientManager,
116118
mgr.GetClient(),
117119
log,
118120
opts.RequeueDuration,

cmd/app/options.go

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ const (
5050
envSelfhostedTokenPath = "TOKEN_PATH"
5151
envSelfhostedInsecure = "INSECURE"
5252
envSelfhostedCAPath = "CA_PATH"
53+
54+
// Used for kubernetes Credential Discovery
55+
envKeychainServiceAccountName = "AUTH_SERVICE_ACCOUNT_NAME"
56+
envKeychainNamespace = "AUTH_SERVICE_ACCOUNT_NAMESPACE"
57+
envKeychainImagePullSecrets = "AUTH_IMAGE_PULL_SECRETS"
58+
envKeychainUseMountSecrets = "AUTH_USE_MOUNT_SECRETS"
59+
// Duration in which to Refresh Credentials from Service Account
60+
envKeychainRefreshDuration = "AUTH_REFRESH_DURATION"
5361
)
5462

5563
var (
@@ -149,26 +157,63 @@ func (o *Options) addAppFlags(fs *pflag.FlagSet) {
149157
}
150158

151159
func (o *Options) addAuthFlags(fs *pflag.FlagSet) {
160+
161+
/// KEYCHAIN
162+
fs.StringVar(&o.Client.KeyChain.Namespace,
163+
"keychain-namespace", "",
164+
fmt.Sprintf(
165+
"Namespace inside of which service account and imagepullsecrets belong too (%s_%s).",
166+
envPrefix, envKeychainNamespace,
167+
))
168+
169+
fs.StringVar(&o.Client.KeyChain.ServiceAccountName,
170+
"keychain-service-account", "",
171+
fmt.Sprintf(
172+
"ServiceAccount used to fetch Image Pull Secrets from (%s_%s).",
173+
envPrefix, envKeychainServiceAccountName,
174+
))
175+
176+
fs.StringSliceVar(&o.Client.KeyChain.ImagePullSecrets,
177+
"keychain-image-pull-secrets", []string{},
178+
fmt.Sprintf(
179+
"Set of image pull secrets to include during authentication (%s_%s).",
180+
envPrefix, envKeychainImagePullSecrets,
181+
))
182+
183+
fs.BoolVar(&o.Client.KeyChain.UseMountSecrets,
184+
"keychain-use-mount-secrets", false,
185+
fmt.Sprintf("Include Mount Secrets during discovery (%s_%s).",
186+
envPrefix, envKeychainUseMountSecrets,
187+
))
188+
fs.DurationVar(&o.Client.AuthRefreshDuration,
189+
"keychain-refresh-duration", time.Hour,
190+
fmt.Sprintf("Duration credentials are refreshed (%s_%s).",
191+
envPrefix, envKeychainRefreshDuration,
192+
))
193+
152194
/// ACR
153195
fs.StringVar(&o.Client.ACR.Username,
154196
"acr-username", "",
155197
fmt.Sprintf(
156198
"Username to authenticate with azure container registry (%s_%s).",
157199
envPrefix, envACRUsername,
158200
))
201+
_ = fs.MarkDeprecated("acr-username", "use keychain instead")
159202
fs.StringVar(&o.Client.ACR.Password,
160203
"acr-password", "",
161204
fmt.Sprintf(
162205
"Password to authenticate with azure container registry (%s_%s).",
163206
envPrefix, envACRPassword,
164207
))
208+
_ = fs.MarkDeprecated("acr-password", "use keychain instead")
165209
fs.StringVar(&o.Client.ACR.RefreshToken,
166210
"acr-refresh-token", "",
167211
fmt.Sprintf(
168212
"Refresh token to authenticate with azure container registry. Cannot be used with "+
169213
"username/password (%s_%s).",
170214
envPrefix, envACRRefreshToken,
171215
))
216+
_ = fs.MarkDeprecated("acr-refresh-token", "use keychain instead")
172217
fs.StringVar(&o.Client.ACR.JWKSURI,
173218
"acr-jwks-uri", "",
174219
fmt.Sprintf(
@@ -184,19 +229,22 @@ func (o *Options) addAuthFlags(fs *pflag.FlagSet) {
184229
"Username to authenticate with docker registry (%s_%s).",
185230
envPrefix, envDockerUsername,
186231
))
232+
_ = fs.MarkDeprecated("docker-username", "use keychain instead")
187233
fs.StringVar(&o.Client.Docker.Password,
188234
"docker-password", "",
189235
fmt.Sprintf(
190236
"Password to authenticate with docker registry (%s_%s).",
191237
envPrefix, envDockerPassword,
192238
))
239+
_ = fs.MarkDeprecated("docker-password", "use keychain instead")
193240
fs.StringVar(&o.Client.Docker.Token,
194241
"docker-token", "",
195242
fmt.Sprintf(
196243
"Token to authenticate with docker registry. Cannot be used with "+
197244
"username/password (%s_%s).",
198245
envPrefix, envDockerToken,
199246
))
247+
_ = fs.MarkDeprecated("docker-token", "use keychain instead")
200248
///
201249

202250
/// ECR
@@ -233,6 +281,7 @@ func (o *Options) addAuthFlags(fs *pflag.FlagSet) {
233281
"Access token for read access to private GCR registries (%s_%s).",
234282
envPrefix, envGCRAccessToken,
235283
))
284+
_ = fs.MarkDeprecated("gcr-token", "use keychain instead")
236285
///
237286

238287
/// GHCR
@@ -242,6 +291,7 @@ func (o *Options) addAuthFlags(fs *pflag.FlagSet) {
242291
"Personal Access token for read access to GHCR releases (%s_%s).",
243292
envPrefix, envGHCRAccessToken,
244293
))
294+
_ = fs.MarkDeprecated("gchr-token", "use keychain instead")
245295
fs.StringVar(&o.Client.GHCR.Hostname,
246296
"gchr-hostname", "",
247297
fmt.Sprintf(
@@ -257,6 +307,7 @@ func (o *Options) addAuthFlags(fs *pflag.FlagSet) {
257307
"Access token for read access to private Quay registries (%s_%s).",
258308
envPrefix, envQuayToken,
259309
))
310+
_ = fs.MarkDeprecated("quay-token", "use keychain instead")
260311
///
261312

262313
/// Selfhosted
@@ -266,19 +317,22 @@ func (o *Options) addAuthFlags(fs *pflag.FlagSet) {
266317
"Username is authenticate with a selfhosted registry (%s_%s_%s).",
267318
envPrefix, envSelfhostedPrefix, envSelfhostedUsername,
268319
))
320+
_ = fs.MarkDeprecated("selfhosted-username", "use keychain instead")
269321
fs.StringVar(&o.selfhosted.Password,
270322
"selfhosted-password", "",
271323
fmt.Sprintf(
272324
"Password is authenticate with a selfhosted registry (%s_%s_%s).",
273325
envPrefix, envSelfhostedPrefix, envSelfhostedPassword,
274326
))
327+
_ = fs.MarkDeprecated("selfhosted-password", "use keychain instead")
275328
fs.StringVar(&o.selfhosted.Bearer,
276329
"selfhosted-token", "",
277330
fmt.Sprintf(
278331
"Token to authenticate to a selfhosted registry. Cannot be used with "+
279332
"username/password (%s_%s_%s).",
280333
envPrefix, envSelfhostedPrefix, envSelfhostedBearer,
281334
))
335+
_ = fs.MarkDeprecated("selfhosted-token", "use keychain instead")
282336
fs.StringVar(&o.selfhosted.TokenPath,
283337
"selfhosted-token-path", "",
284338
fmt.Sprintf(
@@ -305,12 +359,9 @@ func (o *Options) addAuthFlags(fs *pflag.FlagSet) {
305359
"THIS IS NOT RECOMMENDED AND IS INTENDED FOR DEBUGGING (%s_%s_%s)",
306360
envPrefix, envSelfhostedPrefix, envSelfhostedInsecure,
307361
))
308-
// if !validSelfHostedOpts(o) {
309-
// panic(fmt.Errorf("invalid self hosted configuration"))
310-
// }
311362
}
312363

313-
func (o *Options) complete() {
364+
func (o *Options) complete() error {
314365
o.Client.Selfhosted = make(map[string]*selfhosted.Options)
315366

316367
envs := os.Environ()
@@ -338,6 +389,9 @@ func (o *Options) complete() {
338389
{envGHCRHostname, &o.Client.GHCR.Hostname},
339390

340391
{envQuayToken, &o.Client.Quay.Token},
392+
393+
{envKeychainNamespace, &o.Client.KeyChain.Namespace},
394+
{envKeychainServiceAccountName, &o.Client.KeyChain.ServiceAccountName},
341395
} {
342396
for _, env := range envs {
343397
if o.assignEnv(env, opt.key, opt.assign) {
@@ -346,7 +400,7 @@ func (o *Options) complete() {
346400
}
347401
}
348402

349-
o.assignSelfhosted(envs)
403+
return o.assignSelfhosted(envs)
350404
}
351405

352406
func (o *Options) assignEnv(env, key string, assign *string) bool {
@@ -363,7 +417,24 @@ func (o *Options) assignEnv(env, key string, assign *string) bool {
363417
return false
364418
}
365419

366-
func (o *Options) assignSelfhosted(envs []string) {
420+
// assignSelfhosted processes a list of environment variables and assigns
421+
// self-hosted configuration options to the Options struct. It parses the
422+
// environment variables using predefined regular expressions to extract
423+
// self-hosted configuration details such as token path, bearer token, host,
424+
// username, password, insecure flag, and CA path.
425+
//
426+
// The function ensures that each self-hosted configuration is initialized
427+
// before assigning values. It also validates the self-hosted options after
428+
// processing all environment variables.
429+
//
430+
// Parameters:
431+
// - envs: A slice of strings representing environment variables in the
432+
// format "KEY=VALUE".
433+
//
434+
// Returns:
435+
// - error: An error if validation of the self-hosted options fails, or nil
436+
// if the operation is successful.
437+
func (o *Options) assignSelfhosted(envs []string) error {
367438
if o.Client.Selfhosted == nil {
368439
o.Client.Selfhosted = make(map[string]*selfhosted.Options)
369440
}
@@ -451,26 +522,40 @@ func (o *Options) assignSelfhosted(envs []string) {
451522
o.Client.Selfhosted[o.selfhosted.Host] = &o.selfhosted
452523
}
453524

454-
if !validSelfHostedOpts(o) {
455-
panic(fmt.Errorf("invalid self hosted configuration"))
456-
}
525+
return validateSelfHostedOpts(o)
457526
}
458527

459-
func validSelfHostedOpts(opts *Options) bool {
528+
// validateSelfHostedOpts validates the self-hosted options provided in the
529+
// Options struct. It checks both the options set using environment variables
530+
// and those set using flags.
531+
//
532+
// For options set using environment variables, it iterates through the list
533+
// of self-hosted options and ensures that each host is valid.
534+
//
535+
// For options set using flags, it validates the host in the selfhosted.Options
536+
// struct.
537+
//
538+
// Returns an error if any of the self-hosted options contain an invalid host,
539+
// otherwise returns nil.
540+
func validateSelfHostedOpts(opts *Options) error {
460541
// opts set using env vars
461542
if opts.Client.Selfhosted != nil {
462-
for _, selfHostedOpts := range opts.Client.Selfhosted {
463-
return isValidOption(selfHostedOpts.Host, "")
543+
for name, selfHostedOpts := range opts.Client.Selfhosted {
544+
if err := isValidOption(selfHostedOpts.Host, ""); !err {
545+
return fmt.Errorf("invalid self-hosted option for: %s", name)
546+
}
464547
}
465548
}
466549

467550
// opts set using flags
468551
if opts.selfhosted != (selfhosted.Options{}) {
469-
return isValidOption(opts.selfhosted.Host, "")
552+
if !isValidOption(opts.selfhosted.Host, "") {
553+
return fmt.Errorf("invalid self-hosted option for host: %s", opts.selfhosted.Host)
554+
}
470555
}
471-
return true
556+
return nil
472557
}
473558

474-
func isValidOption(option, invalid string) bool {
559+
func isValidOption(option, invalid any) bool {
475560
return option != invalid
476561
}

cmd/app/options_test.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import (
55
"testing"
66

77
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
89

910
"github.com/jetstack/version-checker/pkg/client"
1011
"github.com/jetstack/version-checker/pkg/client/acr"
11-
"github.com/jetstack/version-checker/pkg/client/docker"
12+
"github.com/jetstack/version-checker/pkg/client/dockerhub"
1213
"github.com/jetstack/version-checker/pkg/client/ecr"
1314
"github.com/jetstack/version-checker/pkg/client/gcr"
1415
"github.com/jetstack/version-checker/pkg/client/ghcr"
@@ -55,7 +56,7 @@ func TestComplete(t *testing.T) {
5556
RefreshToken: "acr-token",
5657
JWKSURI: "acr-jwks-uri",
5758
},
58-
Docker: docker.Options{
59+
Docker: dockerhub.Options{
5960
Username: "docker-username",
6061
Password: "docker-password",
6162
Token: "docker-token",
@@ -125,7 +126,7 @@ func TestComplete(t *testing.T) {
125126
RefreshToken: "acr-token",
126127
JWKSURI: "acr-jwks-uri",
127128
},
128-
Docker: docker.Options{
129+
Docker: dockerhub.Options{
129130
Username: "docker-username",
130131
Password: "docker-password",
131132
Token: "docker-token",
@@ -180,14 +181,15 @@ func TestComplete(t *testing.T) {
180181
t.Setenv(env[0], env[1])
181182
}
182183
o := new(Options)
183-
o.complete()
184+
err := o.complete()
185+
require.NoError(t, err)
184186

185187
assert.Exactly(t, test.expOptions, o.Client)
186188
})
187189
}
188190
}
189191

190-
func TestInvalidSelfhostedPanic(t *testing.T) {
192+
func TestInvalidSelfhostedEnv(t *testing.T) {
191193
tests := map[string]struct {
192194
envs []string
193195
}{
@@ -199,12 +201,8 @@ func TestInvalidSelfhostedPanic(t *testing.T) {
199201
}
200202
for name, test := range tests {
201203
t.Run(name, func(t *testing.T) {
202-
defer func() { _ = recover() }()
203-
204204
o := new(Options)
205-
o.assignSelfhosted(test.envs)
206-
207-
t.Errorf("did not panic")
205+
assert.Error(t, o.assignSelfhosted(test.envs))
208206
})
209207
}
210208
}
@@ -221,7 +219,7 @@ func TestInvalidSelfhostedOpts(t *testing.T) {
221219
"no self hosted host provided": {
222220
opts: Options{
223221
Client: client.Options{
224-
Selfhosted: map[string]*selfhosted.Options{"foo": &selfhosted.Options{
222+
Selfhosted: map[string]*selfhosted.Options{"foo": {
225223
Insecure: true,
226224
}},
227225
},
@@ -232,9 +230,12 @@ func TestInvalidSelfhostedOpts(t *testing.T) {
232230
for name, test := range tests {
233231
t.Run(name, func(t *testing.T) {
234232

235-
valid := validSelfHostedOpts(&test.opts)
236-
237-
assert.Equal(t, test.valid, valid)
233+
err := validateSelfHostedOpts(&test.opts)
234+
if test.valid {
235+
assert.NoError(t, err)
236+
} else {
237+
assert.Error(t, err)
238+
}
238239
})
239240
}
240241
}
@@ -353,7 +354,8 @@ func TestAssignSelfhosted(t *testing.T) {
353354
for name, test := range tests {
354355
t.Run(name, func(t *testing.T) {
355356
o := new(Options)
356-
o.assignSelfhosted(test.envs)
357+
err := o.assignSelfhosted(test.envs)
358+
require.NoError(t, err)
357359

358360
assert.Exactly(t, test.expOptions.Selfhosted, o.Client.Selfhosted)
359361
})

0 commit comments

Comments
 (0)