diff --git a/cmd/build.go b/cmd/build.go index 1aaf6ef69a..9abb9543ca 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -13,6 +13,7 @@ import ( pack "knative.dev/func/pkg/builders/buildpacks" "knative.dev/func/pkg/builders/s2i" "knative.dev/func/pkg/config" + "knative.dev/func/pkg/docker" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/oci" ) @@ -29,7 +30,7 @@ SYNOPSIS {{rootCmdUse}} build [-r|--registry] [--builder] [--builder-image] [--push] [--username] [--password] [--token] [--platform] [-p|--path] [-c|--confirm] [-v|--verbose] - [--build-timestamp] [--registry-insecure] + [--build-timestamp] [--registry-insecure] [--registry-authfile] DESCRIPTION @@ -69,7 +70,7 @@ EXAMPLES SuggestFor: []string{"biuld", "buidl", "built"}, PreRunE: bindEnv("image", "path", "builder", "registry", "confirm", "push", "builder-image", "base-image", "platform", "verbose", - "build-timestamp", "registry-insecure", "username", "password", "token"), + "build-timestamp", "registry-insecure", "registry-authfile", "username", "password", "token"), RunE: func(cmd *cobra.Command, args []string) error { return runBuild(cmd, args, newClient) }, @@ -102,6 +103,7 @@ EXAMPLES cmd.Flags().StringP("registry", "r", cfg.Registry, "Container registry + registry namespace. (ex 'ghcr.io/myuser'). The full image name is automatically determined using this along with function name. ($FUNC_REGISTRY)") cmd.Flags().Bool("registry-insecure", cfg.RegistryInsecure, "Skip TLS certificate verification when communicating in HTTPS with the registry ($FUNC_REGISTRY_INSECURE)") + cmd.Flags().String("registry-authfile", "", "Path to a authentication file containing registry credentials ($FUNC_REGISTRY_AUTHFILE)") // Function-Context Flags: // Options whose value is available on the function with context only @@ -293,6 +295,9 @@ type buildConfig struct { // Build with the current timestamp as the created time for docker image. // This is only useful for buildpacks builder. WithTimestamp bool + + // RegistryAuthfile is the path to a docker-config file containing registry credentials. + RegistryAuthfile string } // newBuildConfig gathers options into a single build request. @@ -305,16 +310,17 @@ func newBuildConfig() buildConfig { Verbose: viper.GetBool("verbose"), RegistryInsecure: viper.GetBool("registry-insecure"), }, - BuilderImage: viper.GetString("builder-image"), - BaseImage: viper.GetString("base-image"), - Image: viper.GetString("image"), - Path: viper.GetString("path"), - Platform: viper.GetString("platform"), - Push: viper.GetBool("push"), - Username: viper.GetString("username"), - Password: viper.GetString("password"), - Token: viper.GetString("token"), - WithTimestamp: viper.GetBool("build-timestamp"), + BuilderImage: viper.GetString("builder-image"), + BaseImage: viper.GetString("base-image"), + Image: viper.GetString("image"), + Path: viper.GetString("path"), + Platform: viper.GetString("platform"), + Push: viper.GetBool("push"), + Username: viper.GetString("username"), + Password: viper.GetString("password"), + Token: viper.GetString("token"), + WithTimestamp: viper.GetBool("build-timestamp"), + RegistryAuthfile: viper.GetString("registry-authfile"), } } @@ -479,10 +485,12 @@ func (c buildConfig) Validate(cmd *cobra.Command) (err error) { // deployment is not the contiainer, but rather the running service. func (c buildConfig) clientOptions() ([]fn.Option, error) { o := []fn.Option{fn.WithRegistry(c.Registry)} + + t := newTransport(c.RegistryInsecure) + creds := newCredentialsProvider(config.Dir(), t, c.RegistryAuthfile) + switch c.Builder { case builders.Host: - t := newTransport(c.RegistryInsecure) // may provide a custom impl which proxies - creds := newCredentialsProvider(config.Dir(), t) o = append(o, fn.WithBuilder(oci.NewBuilder(builders.Host, c.Verbose)), fn.WithPusher(oci.NewPusher(c.RegistryInsecure, false, c.Verbose, @@ -495,12 +503,20 @@ func (c buildConfig) clientOptions() ([]fn.Option, error) { fn.WithBuilder(pack.NewBuilder( pack.WithName(builders.Pack), pack.WithTimestamp(c.WithTimestamp), - pack.WithVerbose(c.Verbose)))) + pack.WithVerbose(c.Verbose))), + fn.WithPusher(docker.NewPusher( + docker.WithCredentialsProvider(creds), + docker.WithTransport(t), + docker.WithVerbose(c.Verbose)))) case builders.S2I: o = append(o, fn.WithBuilder(s2i.NewBuilder( s2i.WithName(builders.S2I), - s2i.WithVerbose(c.Verbose)))) + s2i.WithVerbose(c.Verbose))), + fn.WithPusher(docker.NewPusher( + docker.WithCredentialsProvider(creds), + docker.WithTransport(t), + docker.WithVerbose(c.Verbose)))) default: return o, builders.ErrUnknownBuilder{Name: c.Builder, Known: KnownBuilders()} } diff --git a/cmd/client.go b/cmd/client.go index d299dcd547..c32afe4cdd 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -56,9 +56,9 @@ func NewTestClient(options ...fn.Option) ClientFactory { // 'Verbose' indicates the system should write out a higher amount of logging. func NewClient(cfg ClientConfig, options ...fn.Option) (*fn.Client, func()) { var ( - t = newTransport(cfg.InsecureSkipVerify) // may provide a custom impl which proxies - c = newCredentialsProvider(config.Dir(), t) // for accessing registries - d = newKnativeDeployer(cfg.Verbose) // default deployer (can be overridden via options) + t = newTransport(cfg.InsecureSkipVerify) // may provide a custom impl which proxies + c = newCredentialsProvider(config.Dir(), t, "") // for accessing registries + d = newKnativeDeployer(cfg.Verbose) // default deployer (can be overridden via options) pp = newTektonPipelinesProvider(c, cfg.Verbose) o = []fn.Option{ // standard (shared) options for all commands fn.WithVerbose(cfg.Verbose), @@ -101,7 +101,8 @@ func newTransport(insecureSkipVerify bool) fnhttp.RoundTripCloser { // newCredentialsProvider returns a credentials provider which possibly // has cluster-flavor specific additional credential loaders to take advantage // of features or configuration nuances of cluster variants. -func newCredentialsProvider(configPath string, t http.RoundTripper) oci.CredentialsProvider { +// If authFilePath is provided (non-empty), it will be used as the primary auth file. +func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath string) oci.CredentialsProvider { additionalLoaders := append(k8s.GetOpenShiftDockerCredentialLoaders(), k8s.GetGoogleCredentialLoader()...) additionalLoaders = append(additionalLoaders, k8s.GetECRCredentialLoader()...) additionalLoaders = append(additionalLoaders, k8s.GetACRCredentialLoader()...) @@ -112,6 +113,11 @@ func newCredentialsProvider(configPath string, t http.RoundTripper) oci.Credenti creds.WithAdditionalCredentialLoaders(additionalLoaders...), } + // If a custom auth file path is provided, use it + if authFilePath != "" { + options = append(options, creds.WithAuthFilePath(authFilePath)) + } + // Other cluster variants can be supported here return creds.NewCredentialsProvider(configPath, options...) } diff --git a/cmd/deploy.go b/cmd/deploy.go index 050664fad7..4c2b588712 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -37,7 +37,7 @@ SYNOPSIS [-b|--build] [--builder] [--builder-image] [-p|--push] [--domain] [--platform] [--build-timestamp] [--pvc-size] [--service-account] [-c|--confirm] [-v|--verbose] - [--registry-insecure] [--remote-storage-class] + [--registry-insecure] [--registry-authfile] [--remote-storage-class] DESCRIPTION @@ -132,8 +132,9 @@ EXAMPLES PreRunE: bindEnv("build", "build-timestamp", "builder", "builder-image", "base-image", "confirm", "domain", "env", "git-branch", "git-dir", "git-url", "image", "namespace", "path", "platform", "push", "pvc-size", - "service-account", "deployer", "registry", "registry-insecure", "remote", - "username", "password", "token", "verbose", "remote-storage-class"), + "service-account", "deployer", "registry", "registry-insecure", + "registry-authfile", "remote", "username", "password", "token", "verbose", + "remote-storage-class"), RunE: func(cmd *cobra.Command, args []string) error { return runDeploy(cmd, newClient) }, @@ -161,6 +162,7 @@ EXAMPLES cmd.Flags().StringP("registry", "r", cfg.Registry, "Container registry + registry namespace. (ex 'ghcr.io/myuser'). The full image name is automatically determined using this along with function name. ($FUNC_REGISTRY)") cmd.Flags().Bool("registry-insecure", cfg.RegistryInsecure, "Skip TLS certificate verification when communicating in HTTPS with the registry ($FUNC_REGISTRY_INSECURE)") + cmd.Flags().String("registry-authfile", "", "Path to a authentication file containing registry credentials ($FUNC_REGISTRY_AUTHFILE)") // Function-Context Flags: // Options whose value is available on the function with context only @@ -918,6 +920,13 @@ func (c deployConfig) clientOptions() ([]fn.Option, error) { return o, err } + t := newTransport(c.RegistryInsecure) + creds := newCredentialsProvider(config.Dir(), t, c.RegistryAuthfile) + + // Override the pipelines provider to use custom credentials + // This is needed for remote builds (deploy --remote) + o = append(o, fn.WithPipelinesProvider(newTektonPipelinesProvider(creds, c.Verbose))) + // Add the appropriate deployer based on deploy type deployer := c.Deployer if deployer == "" { diff --git a/docs/reference/func_build.md b/docs/reference/func_build.md index fde126d2c6..505f2a1709 100644 --- a/docs/reference/func_build.md +++ b/docs/reference/func_build.md @@ -12,7 +12,7 @@ SYNOPSIS func build [-r|--registry] [--builder] [--builder-image] [--push] [--username] [--password] [--token] [--platform] [-p|--path] [-c|--confirm] [-v|--verbose] - [--build-timestamp] [--registry-insecure] + [--build-timestamp] [--registry-insecure] [--registry-authfile] DESCRIPTION @@ -57,19 +57,20 @@ func build ### Options ``` - --base-image string Override the base image for your function (host builder only) - --build-timestamp Use the actual time as the created time for the docker image. This is only useful for buildpacks builder. - -b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". ($FUNC_BUILDER) (default "pack") - --builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE) - -c, --confirm Prompt to confirm options interactively ($FUNC_CONFIRM) - -h, --help help for build - -i, --image string Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry ($FUNC_IMAGE) - -p, --path string Path to the function. Default is current directory ($FUNC_PATH) - --platform string Optionally specify a target platform, for example "linux/amd64" when using the s2i build strategy - -u, --push Attempt to push the function image to the configured registry after being successfully built - -r, --registry string Container registry + registry namespace. (ex 'ghcr.io/myuser'). The full image name is automatically determined using this along with function name. ($FUNC_REGISTRY) - --registry-insecure Skip TLS certificate verification when communicating in HTTPS with the registry ($FUNC_REGISTRY_INSECURE) - -v, --verbose Print verbose logs ($FUNC_VERBOSE) + --base-image string Override the base image for your function (host builder only) + --build-timestamp Use the actual time as the created time for the docker image. This is only useful for buildpacks builder. + -b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". ($FUNC_BUILDER) (default "pack") + --builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE) + -c, --confirm Prompt to confirm options interactively ($FUNC_CONFIRM) + -h, --help help for build + -i, --image string Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry ($FUNC_IMAGE) + -p, --path string Path to the function. Default is current directory ($FUNC_PATH) + --platform string Optionally specify a target platform, for example "linux/amd64" when using the s2i build strategy + -u, --push Attempt to push the function image to the configured registry after being successfully built + -r, --registry string Container registry + registry namespace. (ex 'ghcr.io/myuser'). The full image name is automatically determined using this along with function name. ($FUNC_REGISTRY) + --registry-authfile string Path to a authentication file containing registry credentials ($FUNC_REGISTRY_AUTHFILE) + --registry-insecure Skip TLS certificate verification when communicating in HTTPS with the registry ($FUNC_REGISTRY_INSECURE) + -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` ### SEE ALSO diff --git a/docs/reference/func_deploy.md b/docs/reference/func_deploy.md index cbcac0d76d..52edd1616a 100644 --- a/docs/reference/func_deploy.md +++ b/docs/reference/func_deploy.md @@ -14,7 +14,7 @@ SYNOPSIS [-b|--build] [--builder] [--builder-image] [-p|--push] [--domain] [--platform] [--build-timestamp] [--pvc-size] [--service-account] [-c|--confirm] [-v|--verbose] - [--registry-insecure] [--remote-storage-class] + [--registry-insecure] [--registry-authfile] [--remote-storage-class] DESCRIPTION @@ -133,6 +133,7 @@ func deploy -u, --push Push the function image to registry before deploying. ($FUNC_PUSH) (default true) --pvc-size string When triggering a remote deployment, set a custom volume size to allocate for the build operation ($FUNC_PVC_SIZE) -r, --registry string Container registry + registry namespace. (ex 'ghcr.io/myuser'). The full image name is automatically determined using this along with function name. ($FUNC_REGISTRY) + --registry-authfile string Path to a authentication file containing registry credentials ($FUNC_REGISTRY_AUTHFILE) --registry-insecure Skip TLS certificate verification when communicating in HTTPS with the registry ($FUNC_REGISTRY_INSECURE) -R, --remote Trigger a remote deployment. Default is to deploy and build from the local system ($FUNC_REMOTE) --remote-storage-class string Specify a storage class to use for the volume on-cluster during remote builds diff --git a/pkg/creds/credentials.go b/pkg/creds/credentials.go index 3a83a371f8..0fcaccdbca 100644 --- a/pkg/creds/credentials.go +++ b/pkg/creds/credentials.go @@ -94,6 +94,14 @@ type credentialsProvider struct { type Opt func(opts *credentialsProvider) +// WithAuthFilePath sets a custom path to a docker-config file containing registry credentials. +// If not specified, the default path (configPath/auth.json) will be used. +func WithAuthFilePath(path string) Opt { + return func(opts *credentialsProvider) { + opts.authFilePath = path + } +} + // WithPromptForCredentials sets custom callback that is supposed to // interactively ask for credentials in case the credentials cannot be found in configuration files. // The callback may be called multiple times in case incorrect credentials were returned before. @@ -187,7 +195,10 @@ func NewCredentialsProvider(configPath string, opts ...Opt) oci.CredentialsProvi return oci.Credentials{}, ErrCredentialsNotFound }) - c.authFilePath = filepath.Join(configPath, "auth.json") + // Set authFilePath if not already set by WithAuthFilePath option + if c.authFilePath == "" { + c.authFilePath = filepath.Join(configPath, "auth.json") + } sys := &containersTypes.SystemContext{ AuthFilePath: c.authFilePath, } diff --git a/pkg/creds/credentials_test.go b/pkg/creds/credentials_test.go index 46e3786f27..2e8a88099a 100644 --- a/pkg/creds/credentials_test.go +++ b/pkg/creds/credentials_test.go @@ -642,11 +642,9 @@ func TestCredentialsWithoutHome(t *testing.T) { ) got, err := credentialsProvider(context.Background(), tt.args.registry+"/someorg/someimage:sometag") - // ASSERT if err != nil { - t.Errorf("%v", err) - return + t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(got, tt.want) { t.Errorf("got: %v, want: %v", got, tt.want) @@ -803,8 +801,7 @@ func TestCredentialsHomePermissions(t *testing.T) { got, err := credentialsProvider(context.Background(), tt.args.registry+"/someorg/someimage:sometag") if err != nil { - t.Errorf("%v", err) - return + t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(got, tt.want) { t.Errorf("got: %v, want: %v", got, tt.want) @@ -815,10 +812,85 @@ func TestCredentialsHomePermissions(t *testing.T) { } } +func TestCredentialsFromAuthfile(t *testing.T) { + tests := []struct { + name string + verifyCredentials creds.VerifyCredentialsCallback + authFileContent string + image string + want Credentials + }{ + { + name: "Single registry auth file", + verifyCredentials: correctVerifyCbk, + authFileContent: fmt.Sprintf(` +{ + "auths": { + "docker.io": { + "auth": "%s" + } + } +} +`, base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", dockerIoUser, dockerIoUserPwd)))), + image: "docker.io/someorg/someimage:sometag", + want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd}, + }, + { + name: "Auth file with multiple registries", + verifyCredentials: correctVerifyCbk, + authFileContent: fmt.Sprintf(` +{ + "auths": { + "docker.io": { + "auth": "%s" + }, + "quay.io": { + "auth": "%s" + } + } +} +`, base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", dockerIoUser, dockerIoUserPwd))), + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", quayIoUser, quayIoUserPwd)))), + image: "quay.io/someorg/someimage:sometag", + want: Credentials{Username: quayIoUser, Password: quayIoUserPwd}, + }, + } + + // reset HOME to the original value after tests since they may change it + defer func() { + os.Setenv("HOME", homeTempDir) + }() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetHomeDir(t) + + authFile := fmt.Sprintf("%s/authfile.json", t.TempDir()) + if err := os.WriteFile(authFile, []byte(tt.authFileContent), 06444); err != nil { + t.Fatalf("failed to write auth file: %s", err) + } + + credentialsProvider := creds.NewCredentialsProvider( + testConfigPath(t), + creds.WithVerifyCredentials(tt.verifyCredentials), + creds.WithAuthFilePath(authFile), + ) + + got, err := credentialsProvider(context.Background(), tt.image) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("got: %v, want: %v", got, tt.want) + } + }) + } +} + // ********************** helper functions below **************************** \\ func resetHomeDir(t *testing.T) { - t.TempDir() + t.Helper() if err := os.RemoveAll(homeTempDir); err != nil { t.Fatal(err) } @@ -829,6 +901,7 @@ func resetHomeDir(t *testing.T) { // resetHomePermissions resets the HOME perms to 0700 (same as resetHomeDir(t)) func resetHomePermissions(t *testing.T) { + t.Helper() if err := os.Chmod(homeTempDir, 0700); err != nil { t.Fatal(err) }