package oci import ( "context" "errors" "fmt" "io" "log" "net" "net/http" "os" "testing" "time" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/oci/mock" . "knative.dev/func/pkg/testing" "github.com/google/go-containerregistry/pkg/registry" ) // TestPusher_Push ensures the base case that the pusher contacts the // registry with a correctly formed request. func TestPusher_Push(t *testing.T) { var ( root, done = Mktemp(t) verbose = false insecure = true anon = true success = false err error ) defer done() // Start a handler on an OS-chosen port which confirms that an incoming // requests looks for the most part like what we'd expect to see from a // container push. regLog := io.Discard if verbose { regLog = os.Stderr } regHandler := registry.New(registry.Logger(log.New(regLog, "img reg handler: ", log.LstdFlags))) handler := http.NewServeMux() handler.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { regHandler.ServeHTTP(res, req) if req.Method == http.MethodPut && req.URL.Path == "/v2/funcs/f/manifests/latest" { success = true } }) l, err := net.Listen("tcp4", "127.0.0.1:") if err != nil { t.Fatal(err) } s := http.Server{Handler: handler} go func() { if err = s.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) { fmt.Fprintf(os.Stderr, "error serving: %v", err) } }() defer s.Close() // Create and push a function client := fn.New( fn.WithBuilder(NewBuilder("", false)), fn.WithPusher(NewPusher(insecure, anon, verbose))) f := fn.Function{Root: root, Runtime: "go", Name: "f", Registry: l.Addr().String() + "/funcs"} if f, err = client.Init(f); err != nil { t.Fatal(err) } if f, err = client.Build(context.Background(), f); err != nil { t.Fatal(err) } if _, _, err = client.Push(context.Background(), f); err != nil { t.Fatal(err) } // Confirm the handler received at least on PUT request containing // an image index. if !success { t.Fatal("did not receive the image index JSON") } } // TestPusher_BasicAuth ensures that the pusher authenticates via basic auth when // supplied with a username/password via the context. func TestPusher_BasicAuth(t *testing.T) { var ( root, done = Mktemp(t) username = "username" password = "password" verbose = false successCh = make(chan bool, 100) // Many successes are queued on push err error ) defer done() // A mock registry with middleware which performs basic auth server := mock.NewRegistry() server.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { u, p, ok := r.BasicAuth() if !ok { // no header. ask for auth w.Header().Add("www-authenticate", "Basic realm=\"Registry Realm\"") http.Error(w, "Unauthorized", http.StatusUnauthorized) } else if u != username || p != password { // header exists, but creds are either missing or incorrect t.Fatalf("Unauthorized. Expected user %q pass %q, got user %q pass %q", username, password, u, p) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } else { // (at least one) request indicates authentication worked. // The channel has a large buffer because many are queued before // the receive instruction. successCh <- true } // always delegate to the registry impl which implements the protocol server.RegistryImpl.ServeHTTP(w, r) } defer server.Close() // Client // initialized with an OCI builder and pusher. client := fn.New( fn.WithBuilder(NewBuilder("", verbose)), fn.WithPusher(NewPusher(false, false, verbose))) // Function // Built and tagged to push to the mock registry f := fn.Function{ Root: root, Runtime: "go", Name: "f", Registry: server.Addr().String() + "/funcs"} if f, err = client.Init(f); err != nil { t.Fatal(err) } if f, err = client.Build(context.Background(), f); err != nil { t.Fatal(err) } // Push // Enables optional basic authentication via the push context to use instead // of the default behavior of using the multi-auth chain of config files // and various known credentials managers. ctx := context.Background() ctx = context.WithValue(ctx, fn.PushUsernameKey{}, username) ctx = context.WithValue(ctx, fn.PushPasswordKey{}, password) if _, _, err = client.Push(ctx, f); err != nil { t.Fatal(err) } select { case <-successCh: case <-time.NewTimer(10 * time.Second).C: t.Fatal("timed out waiting for a successful basic auth request") } }