Skip to content

Commit

Permalink
Basic auth (#2242)
Browse files Browse the repository at this point in the history
* feat: host builder basic auth

* update tests

* mark oci basic auth flags hidden

* cleanup
- Remove debug statements
- Fix test race
- Update licenses

* spelling and linting errors
  • Loading branch information
lkingland authored Mar 26, 2024
1 parent 9daaea3 commit 9beea04
Show file tree
Hide file tree
Showing 13 changed files with 578 additions and 64 deletions.
62 changes: 54 additions & 8 deletions cmd/build.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"errors"
"fmt"
"strings"
Expand All @@ -26,9 +27,10 @@ NAME
{{rootCmdUse}} build - Build a function container locally without deploying
SYNOPSIS
{{rootCmdUse}} build [-r|--registry] [--builder] [--builder-image] [--push]
{{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]
DESCRIPTION
Expand Down Expand Up @@ -66,7 +68,9 @@ EXAMPLES
`,
SuggestFor: []string{"biuld", "buidl", "built"},
PreRunE: bindEnv("image", "path", "builder", "registry", "confirm", "push", "builder-image", "platform", "verbose", "build-timestamp", "registry-insecure"),
PreRunE: bindEnv("image", "path", "builder", "registry", "confirm",
"push", "builder-image", "platform", "verbose", "build-timestamp",
"registry-insecure", "username", "password", "token"),
RunE: func(cmd *cobra.Command, args []string) error {
return runBuild(cmd, args, newClient)
},
Expand Down Expand Up @@ -108,15 +112,31 @@ EXAMPLES
"Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)")
cmd.Flags().StringP("image", "i", f.Image,
"Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry ($FUNC_IMAGE)")
cmd.Flags().BoolP("build-timestamp", "", false, "Use the actual time as the created time for the docker image. This is only useful for buildpacks builder.")

// Static Flags:
// Options which have static defaults only (not globally configurable nor
// persisted with the function)
// Options which are either empty or have static defaults only (not
// globally configurable nor persisted with the function)
cmd.Flags().BoolP("push", "u", false,
"Attempt to push the function image to the configured registry after being successfully built")
cmd.Flags().StringP("platform", "", "",
"Optionally specify a target platform, for example \"linux/amd64\" when using the s2i build strategy")
cmd.Flags().StringP("username", "", "",
"Username to use when pushing to the registry.")
cmd.Flags().StringP("password", "", "",
"Password to use when pushing to the registry.")
cmd.Flags().StringP("token", "", "",
"Token to use when pushing to the registry.")
cmd.Flags().BoolP("build-timestamp", "", false, "Use the actual time as the created time for the docker image. This is only useful for buildpacks builder.")

// Temporarily Hidden Basic Auth Flags
// Username, Password and Token flags, which plumb through basic auth, are
// currently only available on the experimental "host" builder, which is
// itself behind a feature flag FUNC_ENABLE_HOST_BUILDER. So set these
// flags to hidden until it's out of preview and they are plumbed through
// the docker pusher as well.
_ = cmd.Flags().MarkHidden("username")
_ = cmd.Flags().MarkHidden("password")
_ = cmd.Flags().MarkHidden("token")

// Oft-shared flags:
addConfirmFlag(cmd, cfg.Confirm)
Expand All @@ -142,10 +162,10 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro
if err = config.CreatePaths(); err != nil { // for possible auth.json usage
return
}
if cfg, err = newBuildConfig().Prompt(); err != nil {
if cfg, err = newBuildConfig().Prompt(); err != nil { // gather values into a single instruction set
return
}
if err = cfg.Validate(); err != nil {
if err = cfg.Validate(); err != nil { // Perform any pre-validation
return
}
if f, err = fn.NewFunction(cfg.Path); err != nil {
Expand All @@ -156,6 +176,8 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro
}
f = cfg.Configure(f) // Updates f at path to include build request values

cmd.SetContext(cfg.WithValues(cmd.Context())) // Some optional settings are passed via context

// Client
clientOptions, err := cfg.clientOptions()
if err != nil {
Expand Down Expand Up @@ -185,6 +207,16 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro
return f.Stamp()
}

// WithValues returns a context populated with values from the build config
// which are provided to the system via the context.
func (c buildConfig) WithValues(ctx context.Context) context.Context {
// Push
ctx = context.WithValue(ctx, fn.PushUsernameKey{}, c.Username)
ctx = context.WithValue(ctx, fn.PushPasswordKey{}, c.Password)
ctx = context.WithValue(ctx, fn.PushTokenKey{}, c.Token)
return ctx
}

type buildConfig struct {
// Globals (builder, confirm, registry, verbose)
config.Global
Expand All @@ -207,6 +239,17 @@ type buildConfig struct {
// Push the resulting image to the registry after building.
Push bool

// Username when specifying optional basic auth.
Username string

// Password when using optional basic auth. Should be provided along
// with Username.
Password string

// Token when performing basic auth using a bearer token. Should be
// exclusive with Username and Password.
Token string

// Build with the current timestamp as the created time for docker image.
// This is only useful for buildpacks builder.
WithTimestamp bool
Expand All @@ -227,6 +270,9 @@ func newBuildConfig() buildConfig {
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"),
}
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ func TestBuild_RegistryOrImageRequired(t *testing.T) {
testRegistryOrImageRequired(NewBuildCmd, t)
}

// TestBuild_Authentication ensures that Token and Username/Password auth
// propagate to pushers which support them.
func TestBuild_Authentication(t *testing.T) {
testAuthentication(NewBuildCmd, t)
}

// TestBuild_Push ensures that the build command properly pushes and respects
// the --push flag.
// - Push triggered after a successful build
Expand Down
19 changes: 18 additions & 1 deletion cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ EXAMPLES
`,
SuggestFor: []string{"delpoy", "deplyo"},
PreRunE: bindEnv("build", "build-timestamp", "builder", "builder-image", "confirm", "domain", "env", "git-branch", "git-dir", "git-url", "image", "namespace", "path", "platform", "push", "pvc-size", "service-account", "registry", "registry-insecure", "remote", "verbose"),
PreRunE: bindEnv("build", "build-timestamp", "builder", "builder-image", "confirm", "domain", "env", "git-branch", "git-dir", "git-url", "image", "namespace", "path", "platform", "push", "pvc-size", "service-account", "registry", "registry-insecure", "remote", "username", "password", "token", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runDeploy(cmd, newClient)
},
Expand Down Expand Up @@ -193,8 +193,24 @@ EXAMPLES
"Push the function image to registry before deploying. ($FUNC_PUSH)")
cmd.Flags().String("platform", "",
"Optionally specify a specific platform to build for (e.g. linux/amd64). ($FUNC_PLATFORM)")
cmd.Flags().StringP("username", "", "",
"Username to use when pushing to the registry.")
cmd.Flags().StringP("password", "", "",
"Password to use when pushing to the registry.")
cmd.Flags().StringP("token", "", "",
"Token to use when pushing to the registry.")
cmd.Flags().BoolP("build-timestamp", "", false, "Use the actual time as the created time for the docker image. This is only useful for buildpacks builder.")

// Temporarily Hidden Basic Auth Flags
// Username, Password and Token flags, which plumb through basic auth, are
// currently only available on the experimental "host" builder, which is
// itself behind a feature flag FUNC_ENABLE_HOST_BUILDER. So set these
// flags to hidden until it's out of preview and they are plumbed through
// the docker pusher as well.
_ = cmd.Flags().MarkHidden("username")
_ = cmd.Flags().MarkHidden("password")
_ = cmd.Flags().MarkHidden("token")

// Oft-shared flags:
addConfirmFlag(cmd, cfg.Confirm)
addPathFlag(cmd)
Expand Down Expand Up @@ -235,6 +251,7 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) {
if f, err = cfg.Configure(f); err != nil { // Updates f with deploy cfg
return
}
cmd.SetContext(cfg.WithValues(cmd.Context())) // Some optional settings are passed via context

// If using Openshift registry AND redeploying Function, update image registry
if f.Namespace != "" && f.Namespace != f.Deploy.Namespace && f.Deploy.Namespace != "" {
Expand Down
69 changes: 69 additions & 0 deletions cmd/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,75 @@ func testRegistryOrImageRequired(cmdFn commandConstructor, t *testing.T) {
}
}

// TestDeploy_Authentication ensures that Token and Username/Password auth
// propagate their values to pushers which support them.
func TestDeploy_Authentication(t *testing.T) {
testAuthentication(NewDeployCmd, t)
}

func testAuthentication(cmdFn commandConstructor, t *testing.T) {
// This test is currently focused on ensuring the flags for
// explicit credentials (bearer token and username/password) are respected
// and propagated to pushers which support this authentication method.
// Integration tests must be used to ensure correct integration between
// the system and credential helpers (Docker, ecs, acs)
t.Helper()

root := fromTempDirectory(t)
_, err := fn.New().Init(fn.Function{Runtime: "go", Root: root, Registry: TestRegistry})
if err != nil {
t.Fatal(err)
}

var (
testUser = "alice"
testPass = "123"
testToken = "example.jwt.token"
)

// Basic Auth: username/password
// -----------------------------
pusher := mock.NewPusher()
pusher.PushFn = func(ctx context.Context, _ fn.Function) (string, error) {
username, _ := ctx.Value(fn.PushUsernameKey{}).(string)
password, _ := ctx.Value(fn.PushPasswordKey{}).(string)

if username != testUser || password != testPass {
t.Fatalf("expected username %q, password %q. Got %q, %q", testUser, testPass, username, password)
}

return "", nil
}

cmd := cmdFn(NewTestClient(fn.WithPusher(pusher)))
t.Setenv("FUNC_ENABLE_HOST_BUILDER", "true") // host builder is currently behind this feature flag
cmd.SetArgs([]string{"--builder", "host", "--username", testUser, "--password", testPass})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}

// Basic Auth: token
// -----------------------------
pusher = mock.NewPusher()
pusher.PushFn = func(ctx context.Context, _ fn.Function) (string, error) {
token, _ := ctx.Value(fn.PushTokenKey{}).(string)

if token != testToken {
t.Fatalf("expected token %q, got %q", testToken, token)
}

return "", nil
}

cmd = cmdFn(NewTestClient(fn.WithPusher(pusher)))

cmd.SetArgs([]string{"--builder", "host", "--token", testToken})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}

}

// TestDeploy_RemoteBuildURLPermutations ensures that the remote, build and git-url flags
// are properly respected for all permutations, including empty.
func TestDeploy_RemoteBuildURLPermutations(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ require (
)

require (
cloud.google.com/go/compute v1.24.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect
contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect
dario.cat/mergo v1.0.0 // indirect
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
Expand Down Expand Up @@ -1008,8 +1012,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMey
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
Expand Down
12 changes: 12 additions & 0 deletions pkg/functions/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ type Pusher interface {
Push(ctx context.Context, f Function) (string, error)
}

// PushUsernameKey is a type available for use to communicate a basic
// authentication username to pushers which support this method.
type PushUsernameKey struct{}

// PushPasswordKey is a type available for use as a context key for
// providing a basic auth password to pushers which support this method.
type PushPasswordKey struct{}

// PushTokenKey is a type available for use as a context key for providing a
// token (for example a jwt bearer token) to pushers which support this method.
type PushTokenKey struct{}

// Deployer of function source to running status.
type Deployer interface {
// Deploy a function of given name, using given backing image.
Expand Down
4 changes: 2 additions & 2 deletions pkg/functions/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ func TestClient_New_Delegation(t *testing.T) {
return nil
}

pusher.PushFn = func(f fn.Function) (string, error) {
pusher.PushFn = func(_ context.Context, f fn.Function) (string, error) {
if f.Build.Image != expectedImage {
t.Fatalf("pusher expected image '%v', got '%v'", expectedImage, f.Build.Image)
}
Expand Down Expand Up @@ -843,7 +843,7 @@ func TestClient_Update(t *testing.T) {
}

// Pusher whose implementaiton verifies the expected image
pusher.PushFn = func(f fn.Function) (string, error) {
pusher.PushFn = func(_ context.Context, f fn.Function) (string, error) {
if f.Build.Image != expectedImage {
t.Fatalf("pusher expected image '%v', got '%v'", expectedImage, f.Build.Image)
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/mock/pusher.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ import (

type Pusher struct {
PushInvoked bool
PushFn func(fn.Function) (string, error)
PushFn func(context.Context, fn.Function) (string, error)
}

func NewPusher() *Pusher {
return &Pusher{
PushFn: func(fn.Function) (string, error) { return "", nil },
PushFn: func(context.Context, fn.Function) (string, error) { return "", nil },
}
}

func (i *Pusher) Push(ctx context.Context, f fn.Function) (string, error) {
i.PushInvoked = true
return i.PushFn(f)
return i.PushFn(ctx, f)
}
44 changes: 44 additions & 0 deletions pkg/oci/mock/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package mock

import (
"log"
"net"
"net/http"
"net/http/httptest"
"os"

impl "github.com/google/go-containerregistry/pkg/registry"
)

type Registry struct {
*httptest.Server

HandlerFunc http.HandlerFunc
RegistryImpl http.Handler
}

func NewRegistry() *Registry {
registryHandler := impl.New(impl.Logger(log.New(os.Stderr, "test registry: ", log.LstdFlags)))
r := &Registry{
RegistryImpl: registryHandler,
}
r.Server = httptest.NewServer(r)

return r
}

func (r *Registry) Addr() net.Addr {
return r.Server.Listener.Addr()
}

func (r *Registry) Close() {
r.Server.Close()
}

func (r *Registry) ServeHTTP(res http.ResponseWriter, req *http.Request) {
if r.HandlerFunc != nil {
r.HandlerFunc(res, req)
} else {
r.RegistryImpl.ServeHTTP(res, req)
}
}
Loading

0 comments on commit 9beea04

Please sign in to comment.