Repository: knative-sandbox/kn-plugin-func
Pull request #688 (2021-11-30 19:10:27)

feat: In cluster dialer to proxy TCP connections to unexposed services
Added code that will proxy TCP connections via in cluster pod.
This is useful for accessing k8s services that are not exposed.

The connections are created using standard Go dial function:
`DialContext(ctx context.Context, network string, addr string) (net.Conn, error)`.

Mechanism:
Upon creation of dialer we create a pod with command `sleep infinity`.
Then each time `DialContext` function is called we execute `socat - TCP:addr` in the pod and use its stdio as source for our implementation of `net.Conn`.


knative-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative-sandbox/kn-plugin-func/pull/688#" title="Author self-approved">matejvasek</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative-sandbox%2Fkn-plugin-func).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/knative-sandbox/kn-plugin-func/blob/main/OWNERS)~~ [matejvasek]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/688?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) Report
> Merging [#688](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/688?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (f6be626) into [main](https://codecov.io/gh/knative-sandbox/kn-plugin-func/commit/ccf00152be0ceba1794267f8e03a09cb32fee514?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (ccf0015) will **increase** coverage by `0.11%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/688/graphs/tree.svg?width=650&height=150&src=pr&token=da1B9bMsQ9&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox)](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/688?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox)

```diff
@@            Coverage Diff             @@
##             main     #688      +/-   ##
==========================================
+ Coverage   38.03%   38.15%   +0.11%     
==========================================
  Files          42       42              
  Lines        3917     3916       -1     
==========================================
+ Hits         1490     1494       +4     
+ Misses       2225     2219       -6     
- Partials      202      203       +1     
```


| [Impacted Files](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/688?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) | Coverage Δ | |
|---|---|---|
| [cmd/root.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/688/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y21kL3Jvb3QuZ28=) | `47.16% <0.00%> (-0.50%)` | :arrow_down: |
| [docker/pusher.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/688/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-ZG9ja2VyL3B1c2hlci5nbw==) | `32.43% <0.00%> (+0.45%)` | :arrow_up: |
| [client.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/688/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y2xpZW50Lmdv) | `63.55% <0.00%> (+2.05%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/688?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/688?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox). Last update [ccf0015...f6be626](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/688?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox).


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 2bad057e..bfb0ecd6 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -28,52 +28,14 @@ jobs:
     needs: test
     runs-on: ubuntu-latest
     steps:
-      # Create a release, or update the release PR
-      - uses: GoogleCloudPlatform/release-please-action@v2.24.1
-        id: release
-        with:
-          token: ${{ secrets.GITHUB_TOKEN }}
-          release-type: simple
-          bump-minor-pre-major: true
-      # Checkout
       - uses: actions/checkout@v2
-      # Tag
-      # If a release was created, tag `vX.Y.Z` synchronously which:
-      #   1. Triggers release-please to create draft release, allowing manual
-      #      proofreading (and probably editing) of release notes.
-      #      (often raw commit messages are a bit overwhelming, overly granular
-      #      or at least could use some context when included as release notes).
-      #   2. Ensures the tag exists for subsequent action tasks which rely on
-      #      this metadata, such as including version in release binaries.
-      #      The release-please action does add this tag, but asynchronously.
-      # Note that tag is created annotated such that it shows tagging metadata
-      # when queried rather than just the associated commit metadata.
-      - name: tag
-        if: ${{ steps.release.outputs.release_created }}
-        run: |
-          git config user.name github-actions[bot]
-          git tag -d v${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}} || true
-          git push origin :v${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}} || true
-          git tag -a v${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}} -m "Release v${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}}"
-          git push origin v${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}} || true
-
       - uses: actions/setup-go@v2
         with: 
           go-version: "1.16"
-
       # Standard build tasks
       - name: Build
         run: make cross-platform
-          # NOTE:
-          # release-please adds the version asynchronously. Without using the
-          # synchonous tagging step above, the version can be explicitly passed
-          # to the build using the following environment variable.  However this
-          # has the side-effect of causing inter-relese binaries to not include
-          # verbose version information, because the special version `tip` is
-          # overriden with a blank string in those cases.
-          # VERS: ${{ steps.release.outputs.tag_name }}
-
-      # Upload all build artifacts whether it's a release or not
+      # Upload all build artifacts
       - uses: actions/upload-artifact@v2
         with:
           name: OSX Binary
@@ -86,40 +48,3 @@ jobs:
         with:
           name: Windows Binary
           path: func_windows_amd64.exe
-
-      # The following steps are only executed if this is a release
-      - name: Compress Binaries
-        if: ${{ steps.release.outputs.release_created }}
-        run: gzip func_darwin_amd64 func_linux_amd64 func_windows_amd64.exe
-
-      # Upload all binaries
-      - name: Upload Darwin Binary
-        uses: actions/upload-release-asset@v1
-        if: ${{ steps.release.outputs.release_created }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        with:
-          upload_url: ${{ steps.release.outputs.upload_url }}
-          asset_path: ./func_darwin_amd64.gz
-          asset_name: func_darwin_amd64.gz
-          asset_content_type: application/x-gzip
-      - name: Upload Linux Binary
-        uses: actions/upload-release-asset@v1
-        if: ${{ steps.release.outputs.release_created }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        with:
-          upload_url: ${{ steps.release.outputs.upload_url }}
-          asset_path: ./func_linux_amd64.gz
-          asset_name: func_linux_amd64.gz
-          asset_content_type: application/x-gzip
-      - name: Upload Windows Binary
-        uses: actions/upload-release-asset@v1
-        if: ${{ steps.release.outputs.release_created }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        with:
-          upload_url: ${{ steps.release.outputs.upload_url }}
-          asset_path: ./func_windows_amd64.exe.gz
-          asset_name: func_windows_amd64.exe.gz
-          asset_content_type: application/x-gzip
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index d5291f51..4967e8ff 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -9,8 +9,7 @@ aliases:
   - julz
   - maximilien
   autoscaling-wg-leads:
-  - markusthoemmes
-  - vagababov
+  - julz
   client-wg-leads:
   - dsimansk
   - navidshaikh
@@ -21,7 +20,6 @@ aliases:
   - rhuss
   container-freezer-approvers:
   - julz
-  - markusthoemmes
   - psschwei
   control-protocol-approvers:
   - devguyio
@@ -163,7 +161,7 @@ aliases:
   - nicolaferraro
   - rhuss
   knative-admin:
-  - bsnchan
+  - csantanapr
   - dprotaso
   - evankanderson
   - julz
@@ -171,9 +169,9 @@ aliases:
   - knative-prow-robot
   - knative-prow-updater-robot
   - knative-test-reporter-robot
+  - lance
   - markusthoemmes
   - matzew
-  - mbehrendt
   - nak3
   - pmorie
   - rhuss
@@ -236,14 +234,13 @@ aliases:
   serving-writers:
   - ZhiminXiang
   - dprotaso
-  - markusthoemmes
+  - julz
   - nak3
-  - vagababov
   source-wg-leads:
   - lionelvillard
   steering-committee:
-  - bsnchan
-  - mbehrendt
+  - csantanapr
+  - lance
   - pmorie
   - thisisnotapril
   - vaikas
diff --git a/client.go b/client.go
index a02754f6..10188eec 100644
--- a/client.go
+++ b/client.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
+	"runtime/debug"
 	"time"
 
 	"github.com/mitchellh/go-homedir"
@@ -26,12 +27,17 @@ const (
 	// DefaultVersion is the initial value for string members whose implicit type
 	// is a semver.
 	DefaultVersion = "0.0.0"
+
+	// DefaultConfigPath is used in the unlikely event that
+	// the user has no home directory (no ~), there is no
+	// XDG_CONFIG_HOME set, and no WithConfigPath was used.
+	DefaultConfigPath = ".config/func"
 )
 
 // Client for managing Function instances.
 type Client struct {
-	repositories     *Repositories    // Repositories management
-	templates        *Templates       // Templates management
+	repositoriesPath string           // path to repositories
+	repositoriesURI  string           // repo URI (overrides repositories path)
 	verbose          bool             // print verbose logs
 	builder          Builder          // Builds a runnable image source
 	pusher           Pusher           // Pushes Funcation image to a remote
@@ -44,6 +50,8 @@ type Client struct {
 	registry         string           // default registry for OCI image tags
 	progressListener ProgressListener // progress listener
 	emitter          Emitter          // Emits CloudEvents to functions
+	repositories     *Repositories    // Repositories management
+	templates        *Templates       // Templates management
 }
 
 // ErrNotBuilt indicates the Function has not yet been built.
@@ -161,14 +169,8 @@ type Emitter interface {
 
 // New client for Function management.
 func New(options ...Option) *Client {
-	// Assert the global config directory exists (including child directories
-	// such as repositories)
-	assertConfigDir()
-
 	// Instantiate client with static defaults.
 	c := &Client{
-		repositories:     &Repositories{},
-		templates:        &Templates{},
 		builder:          &noopBuilder{output: os.Stdout},
 		pusher:           &noopPusher{output: os.Stdout},
 		deployer:         &noopDeployer{output: os.Stdout},
@@ -178,54 +180,61 @@ func New(options ...Option) *Client {
 		dnsProvider:      &noopDNSProvider{output: os.Stdout},
 		progressListener: &NoopProgressListener{},
 		emitter:          &noopEmitter{},
+		repositoriesPath: filepath.Join(ConfigPath(), "repositories"),
 	}
-	c.repositories = newRepositories(c)
-	c.templates = newTemplates(c)
 	for _, o := range options {
 		o(c)
 	}
+	// Initialize sub-managers using now-fully-initialized client.
+	c.repositories = newRepositories(c)
+	c.templates = newTemplates(c)
+
+	// Trigger the creation of the config and repository paths
+	_ = ConfigPath()         // Config is package-global scoped
+	_ = c.RepositoriesPath() // Repositories is Client-specific
+
 	return c
 }
 
-// assertConfigDir makes a best-effort attempt to create the config directory
-// (including required subdirectories).
-func assertConfigDir() {
-	// NOTE: regarding running as a user with no home directory:
-	// The default is .config/func in current working directory when there is no
-	// available HOME in which to find .`~/.config/func`.
-	// Since it is expected that the code elsewhere never assume the config
-	// directory exists (doing so is a racing condition), it is valid to simply
-	// handle errors at this level.
-	if err := os.MkdirAll(RepositoriesPath(), 0700); err != nil {
-		fmt.Fprintf(os.Stderr, "Error creating '%v': %v", RepositoriesPath(), err)
-	}
-}
+// The default config path is evaluated in the following order, from lowest
+// to highest precedence.
+// 1.  The static default is DefaultConfigPath (./.config/func)
+// 2.  ~/.config/func if it exists (can be expanded: user has a home dir)
+// 3.  The value of $XDG_CONFIG_PATH/func if the environment variable exists.
+// The path will be created if it does not already exist.
+func ConfigPath() (path string) {
+	path = DefaultConfigPath
 
-// RepositoriesPath is the static default path to repositories used by a Client.
-// This path can be overridden on intantiation of a client using the
-// WithRepositories option.
-func RepositoriesPath() string {
-	return filepath.Join(ConfigPath(), "repositories")
-}
+	// ~/.config/func is the default if ~ can be expanded
+	if home, err := homedir.Expand("~"); err == nil {
+		path = filepath.Join(home, ".config", "func")
+	}
 
-// ConfigPath returns the default config directory path used by the Client.
-// The default is ~/.config/func.  If defined, XDG_CONFIG_HOME is used.
-// In the event of an error calculating ~ (no home directory for the current
-// user), the relative path '.config/func' is used.
-func ConfigPath() string {
 	// 'XDG_CONFIG_HOME/func' takes precidence if defined
 	if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
-		return filepath.Join(xdg, "func")
+		path = filepath.Join(xdg, "func")
 	}
 
-	// ~/.config/func is the default if ~ can be expanded
-	if home, err := homedir.Expand("~"); err == nil {
-		return filepath.Join(home, ".config", "func")
-	}
+	mkdir(path) // make sure it exists
+	return
+}
+
+// RepositoriesPath accesses the currently effective repositories path,
+// which defaults to [ConfigPath]/repositories but can be set explicitly using
+// the WithRepositories option when creating the client..
+// The path will be created if it does not already exist.
+func (c *Client) RepositoriesPath() (path string) {
+	path = c.repositories.Path()
+	mkdir(path) // make sure it exists
+	return
+}
 
-	// The default (edge case) is to return a relative path of .config inidicating
-	// the current working directory when neither aforementioned exist.
-	return ".config/func"
+// RepositoriesPath is a convenience method for accessing the default path to
+// repositories that will be used by new instances of a Client unless options
+// such as WithRepositories are used to override.
+// The path will be created if it does not already exist.
+func RepositoriesPath() string {
+	return New().RepositoriesPath()
 }
 
 // OPTIONS
@@ -308,21 +317,21 @@ func WithDNSProvider(provider DNSProvider) Option {
 	}
 }
 
-// WithRepositories sets the location to use for extensible template repositories.
-// Extensible template repositories are additional templates that exist on disk and are
-// not built into the binary.
+// WithRepositories sets the location to use for extensible template
+// repositories.  Extensible template repositories are additional templates
+// that exist on disk and are not built into the binary.
 func WithRepositories(path string) Option {
 	return func(c *Client) {
-		c.Repositories().SetPath(path)
+		c.repositoriesPath = path
 	}
 }
 
-// WithRepository sets a specific URL to a Git repository from which to pull templates.
-// This setting's existence precldes the use of either the inbuilt templates or any
-// repositories from the extensible repositories path.
+// WithRepository sets a specific URL to a Git repository from which to pull
+// templates.  This setting's existence precldes the use of either the inbuilt
+// templates or any repositories from the extensible repositories path.
 func WithRepository(uri string) Option {
 	return func(c *Client) {
-		c.Repositories().SetRemote(uri)
+		c.repositoriesURI = uri
 	}
 }
 
@@ -593,15 +602,7 @@ func (c *Client) Deploy(ctx context.Context, path string) (err error) {
 		return ErrNotBuilt
 	}
 
-	// Push the image for the named service to the configured registry
-	imageDigest, err := c.pusher.Push(ctx, f)
-	if err != nil {
-		return
-	}
-
-	// Record the Image Digest pushed.
-	f.ImageDigest = imageDigest
-	if err = f.Write(); err != nil {
+	if err = c.Push(ctx, &f); err != nil {
 		return
 	}
 
@@ -715,6 +716,18 @@ func (c *Client) Emit(ctx context.Context, endpoint string) error {
 	return c.emitter.Emit(ctx, endpoint)
 }
 
+// Push the image for the named service to the configured registry
+func (c *Client) Push(ctx context.Context, f *Function) (err error) {
+	imageDigest, err := c.pusher.Push(ctx, *f)
+	if err != nil {
+		return
+	}
+
+	// Record the Image Digest pushed.
+	f.ImageDigest = imageDigest
+	return f.Write()
+}
+
 // DEFAULTS
 // ---------
 
@@ -780,3 +793,14 @@ func (p *NoopProgressListener) Increment(m string) {}
 func (p *NoopProgressListener) Complete(m string)  {}
 func (p *NoopProgressListener) Stopping()          {}
 func (p *NoopProgressListener) Done()              {}
+
+// mkdir attempts to mkdir, writing any errors to stderr.
+func mkdir(path string) {
+	// Since it is expected that the code elsewhere never assume directories
+	// exist (doing so is a racing condition), it is valid to simply
+	// handle errors at this level.
+	if err := os.MkdirAll(path, 0700); err != nil {
+		fmt.Fprintf(os.Stderr, "Error creating '%v': %v", path, err)
+		debug.PrintStack()
+	}
+}
diff --git a/cmd/build.go b/cmd/build.go
index 8e1986b5..8ba3c46b 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -11,6 +11,7 @@ import (
 
 	fn "knative.dev/kn-plugin-func"
 	"knative.dev/kn-plugin-func/buildpacks"
+	"knative.dev/kn-plugin-func/docker"
 	"knative.dev/kn-plugin-func/progress"
 )
 
@@ -24,11 +25,31 @@ func newBuildClient(cfg buildConfig) (*fn.Client, error) {
 	builder := buildpacks.NewBuilder()
 	listener := progress.New()
 
+	pusherOption := fn.WithPusher(nil)
+	if cfg.Push {
+		credentialsProvider := docker.NewCredentialsProvider(
+			newCredentialsCallback(),
+			docker.CheckAuth,
+			newChooseHelperCallback(),
+		)
+		pusher, err := docker.NewPusher(
+			docker.WithCredentialsProvider(credentialsProvider),
+			docker.WithProgressListener(listener),
+		)
+
+		if err != nil {
+			return nil, err
+		}
+		pusher.Verbose = cfg.Verbose
+		pusherOption = fn.WithPusher(pusher)
+	}
 	builder.Verbose = cfg.Verbose
 	listener.Verbose = cfg.Verbose
 
 	return fn.New(
 		fn.WithBuilder(builder),
+		// fn.WithPusher(pusher),
+		pusherOption,
 		fn.WithProgressListener(listener),
 		fn.WithRegistry(cfg.Registry), // for image name when --image not provided
 		fn.WithVerbose(cfg.Verbose)), nil
@@ -64,14 +85,15 @@ kn func build
 kn func build --builder cnbs/sample-builder:bionic
 `,
 		SuggestFor: []string{"biuld", "buidl", "built"},
-		PreRunE:    bindEnv("image", "path", "builder", "registry", "confirm"),
+		PreRunE:    bindEnv("image", "path", "builder", "registry", "confirm", "push"),
 	}
 
 	cmd.Flags().StringP("builder", "b", "", "Buildpack builder, either an as a an image name or a mapping name.\nSpecified value is stored in func.yaml for subsequent builds.")
 	cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
 	cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE)")
-	cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
 	cmd.Flags().StringP("registry", "r", "", "Registry + namespace part of the image to build, ex 'quay.io/myuser'.  The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
+	cmd.Flags().BoolP("push", "u", false, "Attempt to push the function image after being successfully built")
+	setPathFlag(cmd)
 
 	if err := cmd.RegisterFlagCompletionFunc("builder", CompleteBuilderList); err != nil {
 		fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
@@ -84,7 +106,7 @@ kn func build --builder cnbs/sample-builder:bionic
 	return cmd
 }
 
-func ValidNamespaceAndRegistry() survey.Validator {
+func ValidNamespaceAndRegistry(path string) survey.Validator {
 	return func(val interface{}) error {
 
 		// if the value passed in is the zero value of the appropriate type
@@ -92,10 +114,10 @@ func ValidNamespaceAndRegistry() survey.Validator {
 			return errors.New("Value is required")
 		}
 
-		_, err := fn.DerivedImage("", val.(string)) //image can be derived without any error
+		_, err := fn.DerivedImage(path, val.(string)) //image can be derived without any error
 
 		if err != nil {
-			return errors.New(val.(string) + " Registry and Namespace are required (ie. docker.io/tigerteam). The image name will be derived from the function name.")
+			return fmt.Errorf("Invalid registry [%s] %v", val.(string), err)
 		}
 		return nil
 	}
@@ -120,6 +142,14 @@ func runBuild(cmd *cobra.Command, _ []string, clientFn buildClientFn) (err error
 		return fmt.Errorf("the given path '%v' does not contain an initialized function. Please create one at this path before deploying", config.Path)
 	}
 
+	// If a registry name was provided as a command line flag, it should be validated
+	if config.Registry != "" {
+		err = ValidNamespaceAndRegistry(config.Path)(config.Registry)
+		if err != nil {
+			return
+		}
+	}
+
 	// If the Function does not yet have an image name and one was not provided on the command line
 	if function.Image == "" {
 		//  AND a --registry was not provided, then we need to
@@ -129,7 +159,7 @@ func runBuild(cmd *cobra.Command, _ []string, clientFn buildClientFn) (err error
 
 			err = survey.AskOne(
 				&survey.Input{Message: "Registry for Function images:"},
-				&config.Registry, survey.WithValidator(ValidNamespaceAndRegistry()))
+				&config.Registry, survey.WithValidator(ValidNamespaceAndRegistry(config.Path)))
 			if err != nil {
 				if err == terminal.InterruptErr {
 					return nil
@@ -162,7 +192,11 @@ func runBuild(cmd *cobra.Command, _ []string, clientFn buildClientFn) (err error
 		return err
 	}
 
-	return client.Build(cmd.Context(), config.Path)
+	err = client.Build(cmd.Context(), config.Path)
+	if err == nil && config.Push {
+		err = client.Push(cmd.Context(), &function)
+	}
+	return
 }
 
 type buildConfig struct {
@@ -198,6 +232,7 @@ func newBuildConfig() buildConfig {
 		Verbose:  viper.GetBool("verbose"), // defined on root
 		Confirm:  viper.GetBool("confirm"),
 		Builder:  viper.GetString("builder"),
+		Push:     viper.GetBool("push"),
 	}
 }
 
diff --git a/cmd/config.go b/cmd/config.go
index a604789a..b27429fb 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -45,7 +45,7 @@ var defaultLoaderSaver standardLoaderSaver
 
 func init() {
 	root.AddCommand(configCmd)
-	configCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
+	setPathFlag(configCmd)
 	configCmd.AddCommand(NewConfigLabelsCmd(defaultLoaderSaver))
 }
 
diff --git a/cmd/config_envs.go b/cmd/config_envs.go
index 1c5523ae..8d4684d9 100644
--- a/cmd/config_envs.go
+++ b/cmd/config_envs.go
@@ -15,12 +15,12 @@ import (
 )
 
 func init() {
+	setPathFlag(configEnvsCmd)
+	setPathFlag(configEnvsAddCmd)
+	setPathFlag(configEnvsRemoveCmd)
 	configCmd.AddCommand(configEnvsCmd)
-	configEnvsCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
 	configEnvsCmd.AddCommand(configEnvsAddCmd)
-	configEnvsAddCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
 	configEnvsCmd.AddCommand(configEnvsRemoveCmd)
-	configEnvsRemoveCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
 }
 
 var configEnvsCmd = &cobra.Command{
diff --git a/cmd/config_labels.go b/cmd/config_labels.go
index 86de13f9..e10b92c9 100644
--- a/cmd/config_labels.go
+++ b/cmd/config_labels.go
@@ -79,11 +79,11 @@ directory or from the directory specified with --path.
 		},
 	}
 
-	configLabelsCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
+	setPathFlag(configLabelsCmd)
+	setPathFlag(configLabelsAddCmd)
+	setPathFlag(configLabelsRemoveCmd)
 	configLabelsCmd.AddCommand(configLabelsAddCmd)
-	configLabelsAddCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
 	configLabelsCmd.AddCommand(configLabelsRemoveCmd)
-	configLabelsRemoveCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
 
 	return configLabelsCmd
 }
diff --git a/cmd/config_volumes.go b/cmd/config_volumes.go
index 9b26cb7b..4d2f8c60 100644
--- a/cmd/config_volumes.go
+++ b/cmd/config_volumes.go
@@ -14,12 +14,12 @@ import (
 )
 
 func init() {
+	setPathFlag(configVolumesCmd)
+	setPathFlag(configVolumesAddCmd)
+	setPathFlag(configVolumesRemoveCmd)
 	configCmd.AddCommand(configVolumesCmd)
-	configVolumesCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
 	configVolumesCmd.AddCommand(configVolumesAddCmd)
-	configVolumesAddCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
 	configVolumesCmd.AddCommand(configVolumesRemoveCmd)
-	configVolumesRemoveCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
 }
 
 var configVolumesCmd = &cobra.Command{
diff --git a/cmd/create.go b/cmd/create.go
index 5c80f8d9..2920fd9e 100644
--- a/cmd/create.go
+++ b/cmd/create.go
@@ -246,7 +246,7 @@ func newCreateConfig(args []string, clientFn createClientFn) (cfg createConfig,
 	// it is still available as an environment variable.
 	repositories = os.Getenv("FUNC_REPOSITORIES")
 	if repositories == "" { // if no env var provided
-		repositories = fn.RepositoriesPath() // use ~/.config/func/repositories
+		repositories = fn.New().RepositoriesPath() // use ~/.config/func/repositories
 	}
 
 	// Config is the final default values based off the execution context.
diff --git a/cmd/delete.go b/cmd/delete.go
index df0bd818..7de33a55 100644
--- a/cmd/delete.go
+++ b/cmd/delete.go
@@ -63,8 +63,8 @@ kn func delete -n apps myfunc
 	}
 
 	cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
-	cmd.Flags().StringP("path", "p", cwd(), "Path to the function project that should be undeployed (Env: $FUNC_PATH)")
-	cmd.Flags().StringP("namespace", "n", "", "Namespace of the function to undeploy. By default, the namespace in func.yaml is used or the actual active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
+	setNamespaceFlag(cmd)
+	setPathFlag(cmd)
 
 	cmd.RunE = func(cmd *cobra.Command, args []string) error {
 		return runDelete(cmd, args, clientFn)
diff --git a/cmd/deploy.go b/cmd/deploy.go
index eb95a8e5..8ff33687 100644
--- a/cmd/deploy.go
+++ b/cmd/deploy.go
@@ -93,10 +93,10 @@ kn func deploy --image quay.io/myuser/myfunc -n myns
 		"You may provide this flag multiple times for setting multiple environment variables. "+
 		"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")
 	cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE)")
-	cmd.Flags().StringP("namespace", "n", "", "Namespace of the function to undeploy. By default, the namespace in func.yaml is used or the actual active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
-	cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
 	cmd.Flags().StringP("registry", "r", "", "Registry + namespace part of the image to build, ex 'quay.io/myuser'.  The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
 	cmd.Flags().BoolP("build", "b", true, "Build the image before deploying (Env: $FUNC_BUILD)")
+	setPathFlag(cmd)
+	setNamespaceFlag(cmd)
 
 	cmd.RunE = func(cmd *cobra.Command, args []string) error {
 		return runDeploy(cmd, args, clientFn)
diff --git a/cmd/emit.go b/cmd/emit.go
index aa88cf29..d695ec36 100644
--- a/cmd/emit.go
+++ b/cmd/emit.go
@@ -80,7 +80,7 @@ kn func emit --sink "http://my.event.broker.com"
 	cmd.Flags().StringP("data", "d", "", "Any arbitrary string to be sent as the CloudEvent data. Ignored if --file is provided  (Env: $FUNC_DATA)")
 	cmd.Flags().StringP("file", "f", "", "Path to a local file containing CloudEvent data to be sent  (Env: $FUNC_FILE)")
 	cmd.Flags().StringP("content-type", "c", "application/json", "The MIME Content-Type for the CloudEvent data  (Env: $FUNC_CONTENT_TYPE)")
-	cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory. Ignored when --sink is provided (Env: $FUNC_PATH)")
+	setPathFlag(cmd)
 
 	cmd.RunE = func(cmd *cobra.Command, args []string) error {
 		return runEmit(cmd, args, clientFn)
diff --git a/cmd/info.go b/cmd/info.go
index 9a0e633b..781cc364 100644
--- a/cmd/info.go
+++ b/cmd/info.go
@@ -56,9 +56,9 @@ kn func info --output yaml --path myotherfunc
 		PreRunE:           bindEnv("namespace", "output", "path"),
 	}
 
-	cmd.Flags().StringP("namespace", "n", "", "Namespace of the function. By default, the namespace in func.yaml is used or the actual active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
+	setNamespaceFlag(cmd)
 	cmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml|url) (Env: $FUNC_OUTPUT)")
-	cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
+	setPathFlag(cmd)
 
 	if err := cmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList); err != nil {
 		fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
diff --git a/cmd/list.go b/cmd/list.go
index 57ecf99b..e0169213 100644
--- a/cmd/list.go
+++ b/cmd/list.go
@@ -62,8 +62,8 @@ kn func list --all-namespaces --output json
 	}
 
 	cmd.Flags().BoolP("all-namespaces", "A", false, "List functions in all namespaces. If set, the --namespace flag is ignored.")
-	cmd.Flags().StringP("namespace", "n", "", "Namespace to search for functions. By default, the functions of the actual active namespace are listed. (Env: $FUNC_NAMESPACE)")
 	cmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml) (Env: $FUNC_OUTPUT)")
+	setNamespaceFlag(cmd)
 
 	if err := cmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList); err != nil {
 		fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
diff --git a/cmd/root.go b/cmd/root.go
index 8257d971..0fe0f969 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -156,7 +156,7 @@ func cwd() (cwd string) {
 // bindFunc which conforms to the cobra PreRunE method signature
 type bindFunc func(*cobra.Command, []string) error
 
-// bindEnv returns a bindFunc that binds env vars to the namd flags.
+// bindEnv returns a bindFunc that binds env vars to the named flags.
 func bindEnv(flags ...string) bindFunc {
 	return func(cmd *cobra.Command, args []string) (err error) {
 		for _, flag := range flags {
@@ -328,3 +328,13 @@ func mergeEnvs(envs []fn.Env, envToUpdate *util.OrderedMap, envToRemove []string
 
 	return envs, nil
 }
+
+// setPathFlag ensures common text/wording when the --path flag is used
+func setPathFlag(cmd *cobra.Command) {
+	cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
+}
+
+// setNamespaceFlag ensures common text/wording when the --namespace flag is used
+func setNamespaceFlag(cmd *cobra.Command) {
+	cmd.Flags().StringP("namespace", "n", "", "The namespace on the cluster. By default, the namespace in func.yaml is used or the currently active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
+}
diff --git a/cmd/run.go b/cmd/run.go
index 508774d8..5455c85b 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -50,7 +50,7 @@ kn func run
 		"Environment variable to set in the form NAME=VALUE. "+
 			"You may provide this flag multiple times for setting multiple environment variables. "+
 			"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")
-	cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
+	setPathFlag(cmd)
 
 	cmd.RunE = func(cmd *cobra.Command, args []string) error {
 		return runRun(cmd, args, clientFn)
diff --git a/docker/pusher.go b/docker/pusher.go
index ace57860..73e0fff3 100644
--- a/docker/pusher.go
+++ b/docker/pusher.go
@@ -90,10 +90,6 @@ func NewCredentialsProvider(
 		}
 	}
 
-	// Creating an instance of the fn.Client ensures that the config path
-	// exists:
-	_ = fn.New()
-
 	authFilePath := filepath.Join(fn.ConfigPath(), "auth.json")
 	sys := &containersTypes.SystemContext{
 		AuthFilePath: authFilePath,
diff --git a/docs/guides/podman.md b/docs/guides/podman.md
index 825580be..d197dcd6 100644
--- a/docs/guides/podman.md
+++ b/docs/guides/podman.md
@@ -14,3 +14,35 @@ Then set the environment variable `DOCKER_HOST` to the socket so `func` knows wh
 ❯ func build -v
 ```
 Now you may use `func` as usual.
+
+## macOS
+
+Under macOS you need to use `podman machine` as `podman` is native to Linux.
+
+```
+❯ brew install podman
+❯ podman machine init --cpus=2 --disk-size=30 --memory=8192
+❯ podman machine start
+❯ ssh-add -k $HOME/.ssh/podman-machine-default
+❯ export DOCKER_HOST=(podman system connection ls --format="{{.URI}}" | grep root)
+```
+
+## Known issues
+
+* In func version 0.19, some runtimes might be unable to build a function by using podman. You might see an error message similar to the following:
+
+    ```
+    ERROR: failed to image: error during connect: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.40/info": EOF
+    ```
+  * The following workaround exists for this issue:
+    * Update the podman service by adding --time=0 to the service ExecStart definition in the podman configuration file at `/etc/systemd/user/podman.service`.You might need to copy the default configuration file from `/usr/lib/systemd/user/podman.service` first.
+      </br></br>
+      Example service configuration:
+
+      `ExecStart=/usr/bin/podman $LOGGING system service --time=0`
+      </br></br>
+      After editing the configuration files you need to reload/restart service:
+      ```
+      ❯ systemctl --user daemon-reload
+      ❯ systemctl restart --user podman.socket
+      ```
diff --git a/go.sum b/go.sum
index e534543e..6f8671fd 100644
--- a/go.sum
+++ b/go.sum
@@ -405,6 +405,7 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
 github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@@ -871,6 +872,7 @@ github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxd
 github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
 github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
+github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
 github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
 github.com/moby/sys/mount v0.2.0 h1:WhCW5B355jtxndN5ovugJlMFJawbUODuW8fSnEH6SSM=
 github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM=
diff --git a/k8s/dialer.go b/k8s/dialer.go
new file mode 100644
index 00000000..9af287bc
--- /dev/null
+++ b/k8s/dialer.go
@@ -0,0 +1,324 @@
+package k8s
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"time"
+
+	coreV1 "k8s.io/api/core/v1"
+	metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/apimachinery/pkg/util/rand"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/kubernetes/scheme"
+	v1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	restclient "k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/remotecommand"
+)
+
+const (
+	socatImage = "alpine/socat:1.7.4.2-r0"
+)
+
+type ContextDialer interface {
+	DialContext(ctx context.Context, network string, addr string) (net.Conn, error)
+	Close() error
+}
+
+// NewInClusterDialer creates context dialer that will dial TCP connections via POD running in k8s cluster.
+// This is useful when accessing k8s services that are not exposed outside cluster (e.g. openshift image registry).
+//
+// Usage:
+//
+//     dialer, err := k8s.NewInClusterDialer(ctx)
+//     if err != nil {
+//         return err
+//     }
+//     defer dialer.Close()
+//
+//     transport := &http.Transport{
+//         DialContext: dialer.DialContext,
+//     }
+//
+//     var client = http.Client{
+//         Transport: transport,
+//     }
+func NewInClusterDialer(ctx context.Context) (ContextDialer, error) {
+	c := &contextDialer{}
+	err := c.startDialerPod(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return c, nil
+}
+
+type contextDialer struct {
+	coreV1    v1.CoreV1Interface
+	restConf  *restclient.Config
+	podName   string
+	namespace string
+}
+
+func (c *contextDialer) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) {
+	if !(network == "tcp" || network == "tcp4" || network == "tcp6") {
+		return nil, fmt.Errorf("unsupported network: %q", network)
+	}
+
+	execDone := make(chan struct{})
+	pr, pw, conn := newConn(execDone)
+
+	go func() {
+		defer close(execDone)
+		errOut := bytes.NewBuffer(nil)
+		err := c.exec(addr, pr, pw, errOut)
+		if err != nil {
+			err = fmt.Errorf("failed to exec in pod: %w (stderr: %q)", err, errOut.String())
+			_ = pr.CloseWithError(err)
+			_ = pw.CloseWithError(err)
+		}
+	}()
+
+	return conn, nil
+}
+
+func (c *contextDialer) Close() error {
+	ctx, cancel := context.WithTimeout(context.Background(), time.Minute*1)
+	defer cancel()
+	delOpts := metaV1.DeleteOptions{}
+
+	return c.coreV1.Pods(c.namespace).Delete(ctx, c.podName, delOpts)
+}
+
+func (c *contextDialer) startDialerPod(ctx context.Context) (err error) {
+	cliConf := GetClientConfig()
+	c.restConf, err = cliConf.ClientConfig()
+	if err != nil {
+		return
+	}
+
+	err = setConfigDefaults(c.restConf)
+	if err != nil {
+		return
+	}
+
+	client, err := kubernetes.NewForConfig(c.restConf)
+	if err != nil {
+		return
+	}
+	c.coreV1 = client.CoreV1()
+
+	c.namespace, err = GetNamespace("")
+	if err != nil {
+		return
+	}
+
+	pods := client.CoreV1().Pods(c.namespace)
+
+	c.podName = "in-cluster-dialer-" + rand.String(5)
+
+	defer func() {
+		if err != nil {
+			c.Close()
+		}
+	}()
+
+	pod := &coreV1.Pod{
+		ObjectMeta: metaV1.ObjectMeta{
+			Name:        c.podName,
+			Labels:      nil,
+			Annotations: nil,
+		},
+		Spec: coreV1.PodSpec{
+			Containers: []coreV1.Container{
+				{
+					Name:    c.podName,
+					Image:   socatImage,
+					Stdin:   true,
+					Command: []string{"sleep", "infinity"},
+				},
+			},
+			DNSPolicy:     coreV1.DNSClusterFirst,
+			RestartPolicy: coreV1.RestartPolicyNever,
+		},
+	}
+	creatOpts := metaV1.CreateOptions{}
+
+	ready := c.podReady(ctx)
+
+	_, err = pods.Create(ctx, pod, creatOpts)
+	if err != nil {
+		return
+	}
+
+	select {
+	case err = <-ready:
+	case <-ctx.Done():
+		err = ctx.Err()
+	case <-time.After(time.Minute * 1):
+		err = errors.New("timeout")
+	}
+
+	if err != nil {
+		err = fmt.Errorf("failed to start dialer container: %w", err)
+	}
+
+	return err
+}
+
+func (c *contextDialer) exec(hostPort string, in io.Reader, out, errOut io.Writer) error {
+
+	restClient := c.coreV1.RESTClient()
+	req := restClient.Post().
+		Resource("pods").
+		Name(c.podName).
+		Namespace(c.namespace).
+		SubResource("exec")
+	req.VersionedParams(&coreV1.PodExecOptions{
+		Command:   []string{"socat", "-", fmt.Sprintf("TCP:%s", hostPort)},
+		Container: c.podName,
+		Stdin:     true,
+		Stdout:    true,
+		Stderr:    true,
+		TTY:       false,
+	}, scheme.ParameterCodec)
+
+	executor, err := remotecommand.NewSPDYExecutor(c.restConf, "POST", req.URL())
+	if err != nil {
+		return err
+	}
+
+	return executor.Stream(remotecommand.StreamOptions{
+		Stdin:  in,
+		Stdout: out,
+		Stderr: errOut,
+		Tty:    false,
+	})
+}
+
+func (c *contextDialer) podReady(ctx context.Context) (errChan <-chan error) {
+	d := make(chan error)
+	errChan = d
+
+	pods := c.coreV1.Pods(c.namespace)
+
+	nameSelector := fields.OneTermEqualSelector("metadata.name", c.podName).String()
+	listOpts := metaV1.ListOptions{
+		Watch:         true,
+		FieldSelector: nameSelector,
+	}
+	watcher, err := pods.Watch(ctx, listOpts)
+	if err != nil {
+		return
+	}
+
+	go func() {
+		defer watcher.Stop()
+		ch := watcher.ResultChan()
+		for event := range ch {
+			pod := event.Object.(*coreV1.Pod)
+
+			if event.Type == watch.Modified {
+				for _, status := range pod.Status.ContainerStatuses {
+					if status.Ready {
+						d <- nil
+						return
+					}
+					if status.State.Waiting != nil {
+						switch status.State.Waiting.Reason {
+						case "ErrImagePull",
+							"CreateContainerError",
+							"CreateContainerConfigError",
+							"InvalidImageName",
+							"CrashLoopBackOff",
+							"ImagePullBackOff":
+							d <- fmt.Errorf("reason: %v, message: %v",
+								status.State.Waiting.Reason,
+								status.State.Waiting.Message)
+							return
+						default:
+							continue
+						}
+					}
+				}
+			}
+		}
+	}()
+
+	return
+}
+
+func setConfigDefaults(config *restclient.Config) error {
+	gv := coreV1.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/api"
+	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = restclient.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+type addr struct{}
+
+func (a addr) Network() string {
+	return "pod-stdio"
+}
+
+func (a addr) String() string {
+	return "pod-stdio"
+}
+
+type conn struct {
+	pr       *io.PipeReader
+	pw       *io.PipeWriter
+	execDone <-chan struct{}
+}
+
+func (c conn) Read(b []byte) (n int, err error) {
+	return c.pr.Read(b)
+}
+
+func (c conn) Write(b []byte) (n int, err error) {
+	return c.pw.Write(b)
+}
+
+func (c conn) Close() error {
+	err := c.pw.Close()
+	if err != nil {
+		return fmt.Errorf("failed to close writer: %w", err)
+	}
+	<-c.execDone
+	err = c.pr.Close()
+	if err != nil {
+		return fmt.Errorf("failed to close reader: %w", err)
+	}
+	return nil
+}
+
+func (c conn) LocalAddr() net.Addr {
+	return addr{}
+}
+
+func (c conn) RemoteAddr() net.Addr {
+	return addr{}
+}
+
+func (c conn) SetDeadline(t time.Time) error { return nil }
+
+func (c conn) SetReadDeadline(t time.Time) error { return nil }
+
+func (c conn) SetWriteDeadline(t time.Time) error { return nil }
+
+func newConn(execDone <-chan struct{}) (*io.PipeReader, *io.PipeWriter, conn) {
+	pr0, pw0 := io.Pipe()
+	pr1, pw1 := io.Pipe()
+	rwc := conn{pr: pr0, pw: pw1, execDone: execDone}
+	return pr1, pw0, rwc
+}
diff --git a/repositories.go b/repositories.go
index 4f9dee18..a797fb12 100644
--- a/repositories.go
+++ b/repositories.go
@@ -31,9 +31,16 @@ type Repositories struct {
 	path string
 
 	// Optional uri of a single repo to use in leau of embedded and extensible.
+	// Enables single-repository mode.  This replaces the default embedded repo
+	// and extended repositories.  This is an important mode for both diskless
+	// (config-less) operation, such as security-restrited environments, and for
+	// running as a library in which case environmental settings should be
+	// ignored in favor of a more functional approach in which only inputs affect
+	// outputs.
 	remote string
 
-	// backreference to the client enabling full api access for the repo manager
+	// backreference to the client enabling this repositorires manager to
+	// have full API access.
 	client *Client
 }
 
@@ -42,32 +49,17 @@ type Repositories struct {
 // full client API during implementations.
 func newRepositories(client *Client) *Repositories {
 	return &Repositories{
-		path:   DefaultRepositoriesPath,
 		client: client,
+		path:   client.repositoriesPath,
+		remote: client.repositoriesURI,
 	}
 }
 
-// SetPath to repositories under management.
-func (r *Repositories) SetPath(path string) {
-	r.path = path
-}
-
 // Path returns the currently active repositories path under management.
 func (r *Repositories) Path() string {
 	return r.path
 }
 
-// SetRemote enables single-repository mode.
-// Enables single-repository mode.  This replaces the default embedded repo
-// and extended repositories.  This is an important mode for both diskless
-// (config-less) operation, such as security-restrited environments, and for
-// running as a library in which case environmental settings should be
-// ignored in favor of a more functional approach in which only inputs affect
-// outputs.
-func (r *Repositories) SetRemote(uri string) {
-	r.remote = uri
-}
-
 // List all repositories the current configuration of the repo manager has
 // defined.
 func (r *Repositories) List() ([]string, error) {
diff --git a/templates.go b/templates.go
index 97c9043a..59d3cf90 100644
--- a/templates.go
+++ b/templates.go
@@ -7,6 +7,7 @@ package function
 
 import (
 	"errors"
+	"os"
 	"path/filepath"
 	"strings"
 
@@ -161,7 +162,11 @@ func (t *Templates) Write(f Function) (Function, error) {
 	}
 
 	// Copy the template files from the repo filesystem to the new Function's root
-	return f, copy(templatePath, f.Root, repo.FS)
+	// removing the manifest (if it exists; errors ignored)
+	err = copy(templatePath, f.Root, repo.FS)              // copy everything
+	_ = os.Remove(filepath.Join(f.Root, templateManifest)) // except the manifest
+
+	return f, err
 }
 
 // Embedding Directives
diff --git a/third_party/VENDOR-LICENSE/github.com/moby/spdystream/LICENSE b/third_party/VENDOR-LICENSE/github.com/moby/spdystream/LICENSE
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/third_party/VENDOR-LICENSE/github.com/moby/spdystream/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/third_party/VENDOR-LICENSE/github.com/moby/spdystream/NOTICE b/third_party/VENDOR-LICENSE/github.com/moby/spdystream/NOTICE
new file mode 100644
index 00000000..b9b11c9a
--- /dev/null
+++ b/third_party/VENDOR-LICENSE/github.com/moby/spdystream/NOTICE
@@ -0,0 +1,5 @@
+SpdyStream
+Copyright 2014-2021 Docker Inc.
+
+This product includes software developed at
+Docker Inc. (https://www.docker.com/).
diff --git a/vendor/github.com/moby/spdystream/CONTRIBUTING.md b/vendor/github.com/moby/spdystream/CONTRIBUTING.md
new file mode 100644
index 00000000..d4eddcc5
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/CONTRIBUTING.md
@@ -0,0 +1,13 @@
+# Contributing to SpdyStream
+
+Want to hack on spdystream? Awesome! Here are instructions to get you
+started.
+
+SpdyStream is a part of the [Docker](https://docker.io) project, and follows
+the same rules and principles. If you're already familiar with the way
+Docker does things, you'll feel right at home.
+
+Otherwise, go read
+[Docker's contributions guidelines](https://github.com/dotcloud/docker/blob/master/CONTRIBUTING.md).
+
+Happy hacking!
diff --git a/vendor/github.com/moby/spdystream/LICENSE b/vendor/github.com/moby/spdystream/LICENSE
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/vendor/github.com/moby/spdystream/MAINTAINERS b/vendor/github.com/moby/spdystream/MAINTAINERS
new file mode 100644
index 00000000..26e5ec82
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/MAINTAINERS
@@ -0,0 +1,40 @@
+# Spdystream maintainers file
+#
+# This file describes who runs the moby/spdystream project and how.
+# This is a living document - if you see something out of date or missing, speak up!
+#
+# It is structured to be consumable by both humans and programs.
+# To extract its contents programmatically, use any TOML-compliant parser.
+#
+# This file is compiled into the MAINTAINERS file in docker/opensource.
+#
+[Org]
+	[Org."Core maintainers"]
+		people = [
+			"adisky",
+			"dims",
+			"dmcgowan",
+		]
+
+[people]
+
+# A reference list of all people associated with the project.
+# All other sections should refer to people by their canonical key
+# in the people section.
+
+	# ADD YOURSELF HERE IN ALPHABETICAL ORDER
+
+	[people.adisky]
+	Name = "Aditi Sharma"
+	Email = "adi.sky17@gmail.com"
+	GitHub = "adisky"
+
+	[people.dims]
+	Name = "Davanum Srinivas"
+	Email = "davanum@gmail.com"
+	GitHub = "dims"
+
+	[people.dmcgowan]
+	Name = "Derek McGowan"
+	Email = "derek@mcg.dev"
+	GitHub = "dmcgowan"
diff --git a/vendor/github.com/moby/spdystream/NOTICE b/vendor/github.com/moby/spdystream/NOTICE
new file mode 100644
index 00000000..b9b11c9a
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/NOTICE
@@ -0,0 +1,5 @@
+SpdyStream
+Copyright 2014-2021 Docker Inc.
+
+This product includes software developed at
+Docker Inc. (https://www.docker.com/).
diff --git a/vendor/github.com/moby/spdystream/README.md b/vendor/github.com/moby/spdystream/README.md
new file mode 100644
index 00000000..b84e9834
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/README.md
@@ -0,0 +1,77 @@
+# SpdyStream
+
+A multiplexed stream library using spdy
+
+## Usage
+
+Client example (connecting to mirroring server without auth)
+
+```go
+package main
+
+import (
+	"fmt"
+	"github.com/moby/spdystream"
+	"net"
+	"net/http"
+)
+
+func main() {
+	conn, err := net.Dial("tcp", "localhost:8080")
+	if err != nil {
+		panic(err)
+	}
+	spdyConn, err := spdystream.NewConnection(conn, false)
+	if err != nil {
+		panic(err)
+	}
+	go spdyConn.Serve(spdystream.NoOpStreamHandler)
+	stream, err := spdyConn.CreateStream(http.Header{}, nil, false)
+	if err != nil {
+		panic(err)
+	}
+
+	stream.Wait()
+
+	fmt.Fprint(stream, "Writing to stream")
+
+	buf := make([]byte, 25)
+	stream.Read(buf)
+	fmt.Println(string(buf))
+
+	stream.Close()
+}
+```
+
+Server example (mirroring server without auth)
+
+```go
+package main
+
+import (
+	"github.com/moby/spdystream"
+	"net"
+)
+
+func main() {
+	listener, err := net.Listen("tcp", "localhost:8080")
+	if err != nil {
+		panic(err)
+	}
+	for {
+		conn, err := listener.Accept()
+		if err != nil {
+			panic(err)
+		}
+		spdyConn, err := spdystream.NewConnection(conn, true)
+		if err != nil {
+			panic(err)
+		}
+		go spdyConn.Serve(spdystream.MirrorStreamHandler)
+	}
+}
+```
+
+## Copyright and license
+
+Copyright 2013-2021 Docker, inc. Released under the [Apache 2.0 license](LICENSE).
diff --git a/vendor/github.com/moby/spdystream/connection.go b/vendor/github.com/moby/spdystream/connection.go
new file mode 100644
index 00000000..d906bb05
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/connection.go
@@ -0,0 +1,972 @@
+/*
+   Copyright 2014-2021 Docker Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package spdystream
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/moby/spdystream/spdy"
+)
+
+var (
+	ErrInvalidStreamId   = errors.New("Invalid stream id")
+	ErrTimeout           = errors.New("Timeout occurred")
+	ErrReset             = errors.New("Stream reset")
+	ErrWriteClosedStream = errors.New("Write on closed stream")
+)
+
+const (
+	FRAME_WORKERS = 5
+	QUEUE_SIZE    = 50
+)
+
+type StreamHandler func(stream *Stream)
+
+type AuthHandler func(header http.Header, slot uint8, parent uint32) bool
+
+type idleAwareFramer struct {
+	f              *spdy.Framer
+	conn           *Connection
+	writeLock      sync.Mutex
+	resetChan      chan struct{}
+	setTimeoutLock sync.Mutex
+	setTimeoutChan chan time.Duration
+	timeout        time.Duration
+}
+
+func newIdleAwareFramer(framer *spdy.Framer) *idleAwareFramer {
+	iaf := &idleAwareFramer{
+		f:         framer,
+		resetChan: make(chan struct{}, 2),
+		// setTimeoutChan needs to be buffered to avoid deadlocks when calling setIdleTimeout at about
+		// the same time the connection is being closed
+		setTimeoutChan: make(chan time.Duration, 1),
+	}
+	return iaf
+}
+
+func (i *idleAwareFramer) monitor() {
+	var (
+		timer          *time.Timer
+		expired        <-chan time.Time
+		resetChan      = i.resetChan
+		setTimeoutChan = i.setTimeoutChan
+	)
+Loop:
+	for {
+		select {
+		case timeout := <-i.setTimeoutChan:
+			i.timeout = timeout
+			if timeout == 0 {
+				if timer != nil {
+					timer.Stop()
+				}
+			} else {
+				if timer == nil {
+					timer = time.NewTimer(timeout)
+					expired = timer.C
+				} else {
+					timer.Reset(timeout)
+				}
+			}
+		case <-resetChan:
+			if timer != nil && i.timeout > 0 {
+				timer.Reset(i.timeout)
+			}
+		case <-expired:
+			i.conn.streamCond.L.Lock()
+			streams := i.conn.streams
+			i.conn.streams = make(map[spdy.StreamId]*Stream)
+			i.conn.streamCond.Broadcast()
+			i.conn.streamCond.L.Unlock()
+			go func() {
+				for _, stream := range streams {
+					stream.resetStream()
+				}
+				i.conn.Close()
+			}()
+		case <-i.conn.closeChan:
+			if timer != nil {
+				timer.Stop()
+			}
+
+			// Start a goroutine to drain resetChan. This is needed because we've seen
+			// some unit tests with large numbers of goroutines get into a situation
+			// where resetChan fills up, at least 1 call to Write() is still trying to
+			// send to resetChan, the connection gets closed, and this case statement
+			// attempts to grab the write lock that Write() already has, causing a
+			// deadlock.
+			//
+			// See https://github.com/moby/spdystream/issues/49 for more details.
+			go func() {
+				for range resetChan {
+				}
+			}()
+
+			go func() {
+				for range setTimeoutChan {
+				}
+			}()
+
+			i.writeLock.Lock()
+			close(resetChan)
+			i.resetChan = nil
+			i.writeLock.Unlock()
+
+			i.setTimeoutLock.Lock()
+			close(i.setTimeoutChan)
+			i.setTimeoutChan = nil
+			i.setTimeoutLock.Unlock()
+
+			break Loop
+		}
+	}
+
+	// Drain resetChan
+	for range resetChan {
+	}
+}
+
+func (i *idleAwareFramer) WriteFrame(frame spdy.Frame) error {
+	i.writeLock.Lock()
+	defer i.writeLock.Unlock()
+	if i.resetChan == nil {
+		return io.EOF
+	}
+	err := i.f.WriteFrame(frame)
+	if err != nil {
+		return err
+	}
+
+	i.resetChan <- struct{}{}
+
+	return nil
+}
+
+func (i *idleAwareFramer) ReadFrame() (spdy.Frame, error) {
+	frame, err := i.f.ReadFrame()
+	if err != nil {
+		return nil, err
+	}
+
+	// resetChan should never be closed since it is only closed
+	// when the connection has closed its closeChan. This closure
+	// only occurs after all Reads have finished
+	// TODO (dmcgowan): refactor relationship into connection
+	i.resetChan <- struct{}{}
+
+	return frame, nil
+}
+
+func (i *idleAwareFramer) setIdleTimeout(timeout time.Duration) {
+	i.setTimeoutLock.Lock()
+	defer i.setTimeoutLock.Unlock()
+
+	if i.setTimeoutChan == nil {
+		return
+	}
+
+	i.setTimeoutChan <- timeout
+}
+
+type Connection struct {
+	conn   net.Conn
+	framer *idleAwareFramer
+
+	closeChan      chan bool
+	goneAway       bool
+	lastStreamChan chan<- *Stream
+	goAwayTimeout  time.Duration
+	closeTimeout   time.Duration
+
+	streamLock *sync.RWMutex
+	streamCond *sync.Cond
+	streams    map[spdy.StreamId]*Stream
+
+	nextIdLock       sync.Mutex
+	receiveIdLock    sync.Mutex
+	nextStreamId     spdy.StreamId
+	receivedStreamId spdy.StreamId
+
+	pingIdLock sync.Mutex
+	pingId     uint32
+	pingChans  map[uint32]chan error
+
+	shutdownLock sync.Mutex
+	shutdownChan chan error
+	hasShutdown  bool
+
+	// for testing https://github.com/moby/spdystream/pull/56
+	dataFrameHandler func(*spdy.DataFrame) error
+}
+
+// NewConnection creates a new spdy connection from an existing
+// network connection.
+func NewConnection(conn net.Conn, server bool) (*Connection, error) {
+	framer, framerErr := spdy.NewFramer(conn, conn)
+	if framerErr != nil {
+		return nil, framerErr
+	}
+	idleAwareFramer := newIdleAwareFramer(framer)
+	var sid spdy.StreamId
+	var rid spdy.StreamId
+	var pid uint32
+	if server {
+		sid = 2
+		rid = 1
+		pid = 2
+	} else {
+		sid = 1
+		rid = 2
+		pid = 1
+	}
+
+	streamLock := new(sync.RWMutex)
+	streamCond := sync.NewCond(streamLock)
+
+	session := &Connection{
+		conn:   conn,
+		framer: idleAwareFramer,
+
+		closeChan:     make(chan bool),
+		goAwayTimeout: time.Duration(0),
+		closeTimeout:  time.Duration(0),
+
+		streamLock:       streamLock,
+		streamCond:       streamCond,
+		streams:          make(map[spdy.StreamId]*Stream),
+		nextStreamId:     sid,
+		receivedStreamId: rid,
+
+		pingId:    pid,
+		pingChans: make(map[uint32]chan error),
+
+		shutdownChan: make(chan error),
+	}
+	session.dataFrameHandler = session.handleDataFrame
+	idleAwareFramer.conn = session
+	go idleAwareFramer.monitor()
+
+	return session, nil
+}
+
+// Ping sends a ping frame across the connection and
+// returns the response time
+func (s *Connection) Ping() (time.Duration, error) {
+	pid := s.pingId
+	s.pingIdLock.Lock()
+	if s.pingId > 0x7ffffffe {
+		s.pingId = s.pingId - 0x7ffffffe
+	} else {
+		s.pingId = s.pingId + 2
+	}
+	s.pingIdLock.Unlock()
+	pingChan := make(chan error)
+	s.pingChans[pid] = pingChan
+	defer delete(s.pingChans, pid)
+
+	frame := &spdy.PingFrame{Id: pid}
+	startTime := time.Now()
+	writeErr := s.framer.WriteFrame(frame)
+	if writeErr != nil {
+		return time.Duration(0), writeErr
+	}
+	select {
+	case <-s.closeChan:
+		return time.Duration(0), errors.New("connection closed")
+	case err, ok := <-pingChan:
+		if ok && err != nil {
+			return time.Duration(0), err
+		}
+		break
+	}
+	return time.Since(startTime), nil
+}
+
+// Serve handles frames sent from the server, including reply frames
+// which are needed to fully initiate connections.  Both clients and servers
+// should call Serve in a separate goroutine before creating streams.
+func (s *Connection) Serve(newHandler StreamHandler) {
+	// use a WaitGroup to wait for all frames to be drained after receiving
+	// go-away.
+	var wg sync.WaitGroup
+
+	// Parition queues to ensure stream frames are handled
+	// by the same worker, ensuring order is maintained
+	frameQueues := make([]*PriorityFrameQueue, FRAME_WORKERS)
+	for i := 0; i < FRAME_WORKERS; i++ {
+		frameQueues[i] = NewPriorityFrameQueue(QUEUE_SIZE)
+
+		// Ensure frame queue is drained when connection is closed
+		go func(frameQueue *PriorityFrameQueue) {
+			<-s.closeChan
+			frameQueue.Drain()
+		}(frameQueues[i])
+
+		wg.Add(1)
+		go func(frameQueue *PriorityFrameQueue) {
+			// let the WaitGroup know this worker is done
+			defer wg.Done()
+
+			s.frameHandler(frameQueue, newHandler)
+		}(frameQueues[i])
+	}
+
+	var (
+		partitionRoundRobin int
+		goAwayFrame         *spdy.GoAwayFrame
+	)
+Loop:
+	for {
+		readFrame, err := s.framer.ReadFrame()
+		if err != nil {
+			if err != io.EOF {
+				debugMessage("frame read error: %s", err)
+			} else {
+				debugMessage("(%p) EOF received", s)
+			}
+			break
+		}
+		var priority uint8
+		var partition int
+		switch frame := readFrame.(type) {
+		case *spdy.SynStreamFrame:
+			if s.checkStreamFrame(frame) {
+				priority = frame.Priority
+				partition = int(frame.StreamId % FRAME_WORKERS)
+				debugMessage("(%p) Add stream frame: %d ", s, frame.StreamId)
+				s.addStreamFrame(frame)
+			} else {
+				debugMessage("(%p) Rejected stream frame: %d ", s, frame.StreamId)
+				continue
+			}
+		case *spdy.SynReplyFrame:
+			priority = s.getStreamPriority(frame.StreamId)
+			partition = int(frame.StreamId % FRAME_WORKERS)
+		case *spdy.DataFrame:
+			priority = s.getStreamPriority(frame.StreamId)
+			partition = int(frame.StreamId % FRAME_WORKERS)
+		case *spdy.RstStreamFrame:
+			priority = s.getStreamPriority(frame.StreamId)
+			partition = int(frame.StreamId % FRAME_WORKERS)
+		case *spdy.HeadersFrame:
+			priority = s.getStreamPriority(frame.StreamId)
+			partition = int(frame.StreamId % FRAME_WORKERS)
+		case *spdy.PingFrame:
+			priority = 0
+			partition = partitionRoundRobin
+			partitionRoundRobin = (partitionRoundRobin + 1) % FRAME_WORKERS
+		case *spdy.GoAwayFrame:
+			// hold on to the go away frame and exit the loop
+			goAwayFrame = frame
+			break Loop
+		default:
+			priority = 7
+			partition = partitionRoundRobin
+			partitionRoundRobin = (partitionRoundRobin + 1) % FRAME_WORKERS
+		}
+		frameQueues[partition].Push(readFrame, priority)
+	}
+	close(s.closeChan)
+
+	// wait for all frame handler workers to indicate they've drained their queues
+	// before handling the go away frame
+	wg.Wait()
+
+	if goAwayFrame != nil {
+		s.handleGoAwayFrame(goAwayFrame)
+	}
+
+	// now it's safe to close remote channels and empty s.streams
+	s.streamCond.L.Lock()
+	// notify streams that they're now closed, which will
+	// unblock any stream Read() calls
+	for _, stream := range s.streams {
+		stream.closeRemoteChannels()
+	}
+	s.streams = make(map[spdy.StreamId]*Stream)
+	s.streamCond.Broadcast()
+	s.streamCond.L.Unlock()
+}
+
+func (s *Connection) frameHandler(frameQueue *PriorityFrameQueue, newHandler StreamHandler) {
+	for {
+		popFrame := frameQueue.Pop()
+		if popFrame == nil {
+			return
+		}
+
+		var frameErr error
+		switch frame := popFrame.(type) {
+		case *spdy.SynStreamFrame:
+			frameErr = s.handleStreamFrame(frame, newHandler)
+		case *spdy.SynReplyFrame:
+			frameErr = s.handleReplyFrame(frame)
+		case *spdy.DataFrame:
+			frameErr = s.dataFrameHandler(frame)
+		case *spdy.RstStreamFrame:
+			frameErr = s.handleResetFrame(frame)
+		case *spdy.HeadersFrame:
+			frameErr = s.handleHeaderFrame(frame)
+		case *spdy.PingFrame:
+			frameErr = s.handlePingFrame(frame)
+		case *spdy.GoAwayFrame:
+			frameErr = s.handleGoAwayFrame(frame)
+		default:
+			frameErr = fmt.Errorf("unhandled frame type: %T", frame)
+		}
+
+		if frameErr != nil {
+			debugMessage("frame handling error: %s", frameErr)
+		}
+	}
+}
+
+func (s *Connection) getStreamPriority(streamId spdy.StreamId) uint8 {
+	stream, streamOk := s.getStream(streamId)
+	if !streamOk {
+		return 7
+	}
+	return stream.priority
+}
+
+func (s *Connection) addStreamFrame(frame *spdy.SynStreamFrame) {
+	var parent *Stream
+	if frame.AssociatedToStreamId != spdy.StreamId(0) {
+		parent, _ = s.getStream(frame.AssociatedToStreamId)
+	}
+
+	stream := &Stream{
+		streamId:   frame.StreamId,
+		parent:     parent,
+		conn:       s,
+		startChan:  make(chan error),
+		headers:    frame.Headers,
+		finished:   (frame.CFHeader.Flags & spdy.ControlFlagUnidirectional) != 0x00,
+		replyCond:  sync.NewCond(new(sync.Mutex)),
+		dataChan:   make(chan []byte),
+		headerChan: make(chan http.Header),
+		closeChan:  make(chan bool),
+		priority:   frame.Priority,
+	}
+	if frame.CFHeader.Flags&spdy.ControlFlagFin != 0x00 {
+		stream.closeRemoteChannels()
+	}
+
+	s.addStream(stream)
+}
+
+// checkStreamFrame checks to see if a stream frame is allowed.
+// If the stream is invalid, then a reset frame with protocol error
+// will be returned.
+func (s *Connection) checkStreamFrame(frame *spdy.SynStreamFrame) bool {
+	s.receiveIdLock.Lock()
+	defer s.receiveIdLock.Unlock()
+	if s.goneAway {
+		return false
+	}
+	validationErr := s.validateStreamId(frame.StreamId)
+	if validationErr != nil {
+		go func() {
+			resetErr := s.sendResetFrame(spdy.ProtocolError, frame.StreamId)
+			if resetErr != nil {
+				debugMessage("reset error: %s", resetErr)
+			}
+		}()
+		return false
+	}
+	return true
+}
+
+func (s *Connection) handleStreamFrame(frame *spdy.SynStreamFrame, newHandler StreamHandler) error {
+	stream, ok := s.getStream(frame.StreamId)
+	if !ok {
+		return fmt.Errorf("Missing stream: %d", frame.StreamId)
+	}
+
+	newHandler(stream)
+
+	return nil
+}
+
+func (s *Connection) handleReplyFrame(frame *spdy.SynReplyFrame) error {
+	debugMessage("(%p) Reply frame received for %d", s, frame.StreamId)
+	stream, streamOk := s.getStream(frame.StreamId)
+	if !streamOk {
+		debugMessage("Reply frame gone away for %d", frame.StreamId)
+		// Stream has already gone away
+		return nil
+	}
+	if stream.replied {
+		// Stream has already received reply
+		return nil
+	}
+	stream.replied = true
+
+	// TODO Check for error
+	if (frame.CFHeader.Flags & spdy.ControlFlagFin) != 0x00 {
+		s.remoteStreamFinish(stream)
+	}
+
+	close(stream.startChan)
+
+	return nil
+}
+
+func (s *Connection) handleResetFrame(frame *spdy.RstStreamFrame) error {
+	stream, streamOk := s.getStream(frame.StreamId)
+	if !streamOk {
+		// Stream has already been removed
+		return nil
+	}
+	s.removeStream(stream)
+	stream.closeRemoteChannels()
+
+	if !stream.replied {
+		stream.replied = true
+		stream.startChan <- ErrReset
+		close(stream.startChan)
+	}
+
+	stream.finishLock.Lock()
+	stream.finished = true
+	stream.finishLock.Unlock()
+
+	return nil
+}
+
+func (s *Connection) handleHeaderFrame(frame *spdy.HeadersFrame) error {
+	stream, streamOk := s.getStream(frame.StreamId)
+	if !streamOk {
+		// Stream has already gone away
+		return nil
+	}
+	if !stream.replied {
+		// No reply received...Protocol error?
+		return nil
+	}
+
+	// TODO limit headers while not blocking (use buffered chan or goroutine?)
+	select {
+	case <-stream.closeChan:
+		return nil
+	case stream.headerChan <- frame.Headers:
+	}
+
+	if (frame.CFHeader.Flags & spdy.ControlFlagFin) != 0x00 {
+		s.remoteStreamFinish(stream)
+	}
+
+	return nil
+}
+
+func (s *Connection) handleDataFrame(frame *spdy.DataFrame) error {
+	debugMessage("(%p) Data frame received for %d", s, frame.StreamId)
+	stream, streamOk := s.getStream(frame.StreamId)
+	if !streamOk {
+		debugMessage("(%p) Data frame gone away for %d", s, frame.StreamId)
+		// Stream has already gone away
+		return nil
+	}
+	if !stream.replied {
+		debugMessage("(%p) Data frame not replied %d", s, frame.StreamId)
+		// No reply received...Protocol error?
+		return nil
+	}
+
+	debugMessage("(%p) (%d) Data frame handling", stream, stream.streamId)
+	if len(frame.Data) > 0 {
+		stream.dataLock.RLock()
+		select {
+		case <-stream.closeChan:
+			debugMessage("(%p) (%d) Data frame not sent (stream shut down)", stream, stream.streamId)
+		case stream.dataChan <- frame.Data:
+			debugMessage("(%p) (%d) Data frame sent", stream, stream.streamId)
+		}
+		stream.dataLock.RUnlock()
+	}
+	if (frame.Flags & spdy.DataFlagFin) != 0x00 {
+		s.remoteStreamFinish(stream)
+	}
+	return nil
+}
+
+func (s *Connection) handlePingFrame(frame *spdy.PingFrame) error {
+	if s.pingId&0x01 != frame.Id&0x01 {
+		return s.framer.WriteFrame(frame)
+	}
+	pingChan, pingOk := s.pingChans[frame.Id]
+	if pingOk {
+		close(pingChan)
+	}
+	return nil
+}
+
+func (s *Connection) handleGoAwayFrame(frame *spdy.GoAwayFrame) error {
+	debugMessage("(%p) Go away received", s)
+	s.receiveIdLock.Lock()
+	if s.goneAway {
+		s.receiveIdLock.Unlock()
+		return nil
+	}
+	s.goneAway = true
+	s.receiveIdLock.Unlock()
+
+	if s.lastStreamChan != nil {
+		stream, _ := s.getStream(frame.LastGoodStreamId)
+		go func() {
+			s.lastStreamChan <- stream
+		}()
+	}
+
+	// Do not block frame handler waiting for closure
+	go s.shutdown(s.goAwayTimeout)
+
+	return nil
+}
+
+func (s *Connection) remoteStreamFinish(stream *Stream) {
+	stream.closeRemoteChannels()
+
+	stream.finishLock.Lock()
+	if stream.finished {
+		// Stream is fully closed, cleanup
+		s.removeStream(stream)
+	}
+	stream.finishLock.Unlock()
+}
+
+// CreateStream creates a new spdy stream using the parameters for
+// creating the stream frame.  The stream frame will be sent upon
+// calling this function, however this function does not wait for
+// the reply frame.  If waiting for the reply is desired, use
+// the stream Wait or WaitTimeout function on the stream returned
+// by this function.
+func (s *Connection) CreateStream(headers http.Header, parent *Stream, fin bool) (*Stream, error) {
+	// MUST synchronize stream creation (all the way to writing the frame)
+	// as stream IDs **MUST** increase monotonically.
+	s.nextIdLock.Lock()
+	defer s.nextIdLock.Unlock()
+
+	streamId := s.getNextStreamId()
+	if streamId == 0 {
+		return nil, fmt.Errorf("Unable to get new stream id")
+	}
+
+	stream := &Stream{
+		streamId:   streamId,
+		parent:     parent,
+		conn:       s,
+		startChan:  make(chan error),
+		headers:    headers,
+		dataChan:   make(chan []byte),
+		headerChan: make(chan http.Header),
+		closeChan:  make(chan bool),
+	}
+
+	debugMessage("(%p) (%p) Create stream", s, stream)
+
+	s.addStream(stream)
+
+	return stream, s.sendStream(stream, fin)
+}
+
+func (s *Connection) shutdown(closeTimeout time.Duration) {
+	// TODO Ensure this isn't called multiple times
+	s.shutdownLock.Lock()
+	if s.hasShutdown {
+		s.shutdownLock.Unlock()
+		return
+	}
+	s.hasShutdown = true
+	s.shutdownLock.Unlock()
+
+	var timeout <-chan time.Time
+	if closeTimeout > time.Duration(0) {
+		timeout = time.After(closeTimeout)
+	}
+	streamsClosed := make(chan bool)
+
+	go func() {
+		s.streamCond.L.Lock()
+		for len(s.streams) > 0 {
+			debugMessage("Streams opened: %d, %#v", len(s.streams), s.streams)
+			s.streamCond.Wait()
+		}
+		s.streamCond.L.Unlock()
+		close(streamsClosed)
+	}()
+
+	var err error
+	select {
+	case <-streamsClosed:
+		// No active streams, close should be safe
+		err = s.conn.Close()
+	case <-timeout:
+		// Force ungraceful close
+		err = s.conn.Close()
+		// Wait for cleanup to clear active streams
+		<-streamsClosed
+	}
+
+	if err != nil {
+		duration := 10 * time.Minute
+		time.AfterFunc(duration, func() {
+			select {
+			case err, ok := <-s.shutdownChan:
+				if ok {
+					debugMessage("Unhandled close error after %s: %s", duration, err)
+				}
+			default:
+			}
+		})
+		s.shutdownChan <- err
+	}
+	close(s.shutdownChan)
+}
+
+// Closes spdy connection by sending GoAway frame and initiating shutdown
+func (s *Connection) Close() error {
+	s.receiveIdLock.Lock()
+	if s.goneAway {
+		s.receiveIdLock.Unlock()
+		return nil
+	}
+	s.goneAway = true
+	s.receiveIdLock.Unlock()
+
+	var lastStreamId spdy.StreamId
+	if s.receivedStreamId > 2 {
+		lastStreamId = s.receivedStreamId - 2
+	}
+
+	goAwayFrame := &spdy.GoAwayFrame{
+		LastGoodStreamId: lastStreamId,
+		Status:           spdy.GoAwayOK,
+	}
+
+	err := s.framer.WriteFrame(goAwayFrame)
+	go s.shutdown(s.closeTimeout)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// CloseWait closes the connection and waits for shutdown
+// to finish.  Note the underlying network Connection
+// is not closed until the end of shutdown.
+func (s *Connection) CloseWait() error {
+	closeErr := s.Close()
+	if closeErr != nil {
+		return closeErr
+	}
+	shutdownErr, ok := <-s.shutdownChan
+	if ok {
+		return shutdownErr
+	}
+	return nil
+}
+
+// Wait waits for the connection to finish shutdown or for
+// the wait timeout duration to expire.  This needs to be
+// called either after Close has been called or the GOAWAYFRAME
+// has been received.  If the wait timeout is 0, this function
+// will block until shutdown finishes.  If wait is never called
+// and a shutdown error occurs, that error will be logged as an
+// unhandled error.
+func (s *Connection) Wait(waitTimeout time.Duration) error {
+	var timeout <-chan time.Time
+	if waitTimeout > time.Duration(0) {
+		timeout = time.After(waitTimeout)
+	}
+
+	select {
+	case err, ok := <-s.shutdownChan:
+		if ok {
+			return err
+		}
+	case <-timeout:
+		return ErrTimeout
+	}
+	return nil
+}
+
+// NotifyClose registers a channel to be called when the remote
+// peer inidicates connection closure.  The last stream to be
+// received by the remote will be sent on the channel.  The notify
+// timeout will determine the duration between go away received
+// and the connection being closed.
+func (s *Connection) NotifyClose(c chan<- *Stream, timeout time.Duration) {
+	s.goAwayTimeout = timeout
+	s.lastStreamChan = c
+}
+
+// SetCloseTimeout sets the amount of time close will wait for
+// streams to finish before terminating the underlying network
+// connection.  Setting the timeout to 0 will cause close to
+// wait forever, which is the default.
+func (s *Connection) SetCloseTimeout(timeout time.Duration) {
+	s.closeTimeout = timeout
+}
+
+// SetIdleTimeout sets the amount of time the connection may sit idle before
+// it is forcefully terminated.
+func (s *Connection) SetIdleTimeout(timeout time.Duration) {
+	s.framer.setIdleTimeout(timeout)
+}
+
+func (s *Connection) sendHeaders(headers http.Header, stream *Stream, fin bool) error {
+	var flags spdy.ControlFlags
+	if fin {
+		flags = spdy.ControlFlagFin
+	}
+
+	headerFrame := &spdy.HeadersFrame{
+		StreamId: stream.streamId,
+		Headers:  headers,
+		CFHeader: spdy.ControlFrameHeader{Flags: flags},
+	}
+
+	return s.framer.WriteFrame(headerFrame)
+}
+
+func (s *Connection) sendReply(headers http.Header, stream *Stream, fin bool) error {
+	var flags spdy.ControlFlags
+	if fin {
+		flags = spdy.ControlFlagFin
+	}
+
+	replyFrame := &spdy.SynReplyFrame{
+		StreamId: stream.streamId,
+		Headers:  headers,
+		CFHeader: spdy.ControlFrameHeader{Flags: flags},
+	}
+
+	return s.framer.WriteFrame(replyFrame)
+}
+
+func (s *Connection) sendResetFrame(status spdy.RstStreamStatus, streamId spdy.StreamId) error {
+	resetFrame := &spdy.RstStreamFrame{
+		StreamId: streamId,
+		Status:   status,
+	}
+
+	return s.framer.WriteFrame(resetFrame)
+}
+
+func (s *Connection) sendReset(status spdy.RstStreamStatus, stream *Stream) error {
+	return s.sendResetFrame(status, stream.streamId)
+}
+
+func (s *Connection) sendStream(stream *Stream, fin bool) error {
+	var flags spdy.ControlFlags
+	if fin {
+		flags = spdy.ControlFlagFin
+		stream.finished = true
+	}
+
+	var parentId spdy.StreamId
+	if stream.parent != nil {
+		parentId = stream.parent.streamId
+	}
+
+	streamFrame := &spdy.SynStreamFrame{
+		StreamId:             spdy.StreamId(stream.streamId),
+		AssociatedToStreamId: spdy.StreamId(parentId),
+		Headers:              stream.headers,
+		CFHeader:             spdy.ControlFrameHeader{Flags: flags},
+	}
+
+	return s.framer.WriteFrame(streamFrame)
+}
+
+// getNextStreamId returns the next sequential id
+// every call should produce a unique value or an error
+func (s *Connection) getNextStreamId() spdy.StreamId {
+	sid := s.nextStreamId
+	if sid > 0x7fffffff {
+		return 0
+	}
+	s.nextStreamId = s.nextStreamId + 2
+	return sid
+}
+
+// PeekNextStreamId returns the next sequential id and keeps the next id untouched
+func (s *Connection) PeekNextStreamId() spdy.StreamId {
+	sid := s.nextStreamId
+	return sid
+}
+
+func (s *Connection) validateStreamId(rid spdy.StreamId) error {
+	if rid > 0x7fffffff || rid < s.receivedStreamId {
+		return ErrInvalidStreamId
+	}
+	s.receivedStreamId = rid + 2
+	return nil
+}
+
+func (s *Connection) addStream(stream *Stream) {
+	s.streamCond.L.Lock()
+	s.streams[stream.streamId] = stream
+	debugMessage("(%p) (%p) Stream added, broadcasting: %d", s, stream, stream.streamId)
+	s.streamCond.Broadcast()
+	s.streamCond.L.Unlock()
+}
+
+func (s *Connection) removeStream(stream *Stream) {
+	s.streamCond.L.Lock()
+	delete(s.streams, stream.streamId)
+	debugMessage("(%p) (%p) Stream removed, broadcasting: %d", s, stream, stream.streamId)
+	s.streamCond.Broadcast()
+	s.streamCond.L.Unlock()
+}
+
+func (s *Connection) getStream(streamId spdy.StreamId) (stream *Stream, ok bool) {
+	s.streamLock.RLock()
+	stream, ok = s.streams[streamId]
+	s.streamLock.RUnlock()
+	return
+}
+
+// FindStream looks up the given stream id and either waits for the
+// stream to be found or returns nil if the stream id is no longer
+// valid.
+func (s *Connection) FindStream(streamId uint32) *Stream {
+	var stream *Stream
+	var ok bool
+	s.streamCond.L.Lock()
+	stream, ok = s.streams[spdy.StreamId(streamId)]
+	debugMessage("(%p) Found stream %d? %t", s, spdy.StreamId(streamId), ok)
+	for !ok && streamId >= uint32(s.receivedStreamId) {
+		s.streamCond.Wait()
+		stream, ok = s.streams[spdy.StreamId(streamId)]
+	}
+	s.streamCond.L.Unlock()
+	return stream
+}
+
+func (s *Connection) CloseChan() <-chan bool {
+	return s.closeChan
+}
diff --git a/vendor/github.com/moby/spdystream/go.mod b/vendor/github.com/moby/spdystream/go.mod
new file mode 100644
index 00000000..d9b9ad59
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/go.mod
@@ -0,0 +1,5 @@
+module github.com/moby/spdystream
+
+go 1.13
+
+require github.com/gorilla/websocket v1.4.2
diff --git a/vendor/github.com/moby/spdystream/go.sum b/vendor/github.com/moby/spdystream/go.sum
new file mode 100644
index 00000000..85efffd9
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/go.sum
@@ -0,0 +1,2 @@
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
diff --git a/vendor/github.com/moby/spdystream/handlers.go b/vendor/github.com/moby/spdystream/handlers.go
new file mode 100644
index 00000000..d68f61f8
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/handlers.go
@@ -0,0 +1,52 @@
+/*
+   Copyright 2014-2021 Docker Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package spdystream
+
+import (
+	"io"
+	"net/http"
+)
+
+// MirrorStreamHandler mirrors all streams.
+func MirrorStreamHandler(stream *Stream) {
+	replyErr := stream.SendReply(http.Header{}, false)
+	if replyErr != nil {
+		return
+	}
+
+	go func() {
+		io.Copy(stream, stream)
+		stream.Close()
+	}()
+	go func() {
+		for {
+			header, receiveErr := stream.ReceiveHeader()
+			if receiveErr != nil {
+				return
+			}
+			sendErr := stream.SendHeader(header, false)
+			if sendErr != nil {
+				return
+			}
+		}
+	}()
+}
+
+// NoopStreamHandler does nothing when stream connects.
+func NoOpStreamHandler(stream *Stream) {
+	stream.SendReply(http.Header{}, false)
+}
diff --git a/vendor/github.com/moby/spdystream/priority.go b/vendor/github.com/moby/spdystream/priority.go
new file mode 100644
index 00000000..d8eb3516
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/priority.go
@@ -0,0 +1,114 @@
+/*
+   Copyright 2014-2021 Docker Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package spdystream
+
+import (
+	"container/heap"
+	"sync"
+
+	"github.com/moby/spdystream/spdy"
+)
+
+type prioritizedFrame struct {
+	frame    spdy.Frame
+	priority uint8
+	insertId uint64
+}
+
+type frameQueue []*prioritizedFrame
+
+func (fq frameQueue) Len() int {
+	return len(fq)
+}
+
+func (fq frameQueue) Less(i, j int) bool {
+	if fq[i].priority == fq[j].priority {
+		return fq[i].insertId < fq[j].insertId
+	}
+	return fq[i].priority < fq[j].priority
+}
+
+func (fq frameQueue) Swap(i, j int) {
+	fq[i], fq[j] = fq[j], fq[i]
+}
+
+func (fq *frameQueue) Push(x interface{}) {
+	*fq = append(*fq, x.(*prioritizedFrame))
+}
+
+func (fq *frameQueue) Pop() interface{} {
+	old := *fq
+	n := len(old)
+	*fq = old[0 : n-1]
+	return old[n-1]
+}
+
+type PriorityFrameQueue struct {
+	queue        *frameQueue
+	c            *sync.Cond
+	size         int
+	nextInsertId uint64
+	drain        bool
+}
+
+func NewPriorityFrameQueue(size int) *PriorityFrameQueue {
+	queue := make(frameQueue, 0, size)
+	heap.Init(&queue)
+
+	return &PriorityFrameQueue{
+		queue: &queue,
+		size:  size,
+		c:     sync.NewCond(&sync.Mutex{}),
+	}
+}
+
+func (q *PriorityFrameQueue) Push(frame spdy.Frame, priority uint8) {
+	q.c.L.Lock()
+	defer q.c.L.Unlock()
+	for q.queue.Len() >= q.size {
+		q.c.Wait()
+	}
+	pFrame := &prioritizedFrame{
+		frame:    frame,
+		priority: priority,
+		insertId: q.nextInsertId,
+	}
+	q.nextInsertId = q.nextInsertId + 1
+	heap.Push(q.queue, pFrame)
+	q.c.Signal()
+}
+
+func (q *PriorityFrameQueue) Pop() spdy.Frame {
+	q.c.L.Lock()
+	defer q.c.L.Unlock()
+	for q.queue.Len() == 0 {
+		if q.drain {
+			return nil
+		}
+		q.c.Wait()
+	}
+	frame := heap.Pop(q.queue).(*prioritizedFrame).frame
+	q.c.Signal()
+	return frame
+}
+
+func (q *PriorityFrameQueue) Drain() {
+	q.c.L.Lock()
+	defer q.c.L.Unlock()
+	q.drain = true
+	q.c.Broadcast()
+}
diff --git a/vendor/github.com/moby/spdystream/spdy/dictionary.go b/vendor/github.com/moby/spdystream/spdy/dictionary.go
new file mode 100644
index 00000000..392232f1
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/spdy/dictionary.go
@@ -0,0 +1,203 @@
+/*
+   Copyright 2014-2021 Docker Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package spdy
+
+// headerDictionary is the dictionary sent to the zlib compressor/decompressor.
+var headerDictionary = []byte{
+	0x00, 0x00, 0x00, 0x07, 0x6f, 0x70, 0x74, 0x69,
+	0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, 0x04, 0x68,
+	0x65, 0x61, 0x64, 0x00, 0x00, 0x00, 0x04, 0x70,
+	0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x03, 0x70,
+	0x75, 0x74, 0x00, 0x00, 0x00, 0x06, 0x64, 0x65,
+	0x6c, 0x65, 0x74, 0x65, 0x00, 0x00, 0x00, 0x05,
+	0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x00, 0x00,
+	0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x00,
+	0x00, 0x00, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70,
+	0x74, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65,
+	0x74, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x63, 0x63,
+	0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f,
+	0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x0f,
+	0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c,
+	0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00,
+	0x00, 0x00, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70,
+	0x74, 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73,
+	0x00, 0x00, 0x00, 0x03, 0x61, 0x67, 0x65, 0x00,
+	0x00, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77,
+	0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x68,
+	0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
+	0x6e, 0x00, 0x00, 0x00, 0x0d, 0x63, 0x61, 0x63,
+	0x68, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72,
+	0x6f, 0x6c, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x6f,
+	0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+	0x00, 0x00, 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74,
+	0x65, 0x6e, 0x74, 0x2d, 0x62, 0x61, 0x73, 0x65,
+	0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, 0x6e, 0x74,
+	0x65, 0x6e, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f,
+	0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10,
+	0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d,
+	0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65,
+	0x00, 0x00, 0x00, 0x0e, 0x63, 0x6f, 0x6e, 0x74,
+	0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67,
+	0x74, 0x68, 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f,
+	0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x6f,
+	0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00,
+	0x00, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
+	0x74, 0x2d, 0x6d, 0x64, 0x35, 0x00, 0x00, 0x00,
+	0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74,
+	0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00,
+	0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
+	0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x00, 0x00,
+	0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00,
+	0x00, 0x04, 0x65, 0x74, 0x61, 0x67, 0x00, 0x00,
+	0x00, 0x06, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74,
+	0x00, 0x00, 0x00, 0x07, 0x65, 0x78, 0x70, 0x69,
+	0x72, 0x65, 0x73, 0x00, 0x00, 0x00, 0x04, 0x66,
+	0x72, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x04, 0x68,
+	0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x08, 0x69,
+	0x66, 0x2d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00,
+	0x00, 0x00, 0x11, 0x69, 0x66, 0x2d, 0x6d, 0x6f,
+	0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2d, 0x73,
+	0x69, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x0d,
+	0x69, 0x66, 0x2d, 0x6e, 0x6f, 0x6e, 0x65, 0x2d,
+	0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00,
+	0x08, 0x69, 0x66, 0x2d, 0x72, 0x61, 0x6e, 0x67,
+	0x65, 0x00, 0x00, 0x00, 0x13, 0x69, 0x66, 0x2d,
+	0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69,
+	0x65, 0x64, 0x2d, 0x73, 0x69, 0x6e, 0x63, 0x65,
+	0x00, 0x00, 0x00, 0x0d, 0x6c, 0x61, 0x73, 0x74,
+	0x2d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65,
+	0x64, 0x00, 0x00, 0x00, 0x08, 0x6c, 0x6f, 0x63,
+	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00,
+	0x0c, 0x6d, 0x61, 0x78, 0x2d, 0x66, 0x6f, 0x72,
+	0x77, 0x61, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00,
+	0x06, 0x70, 0x72, 0x61, 0x67, 0x6d, 0x61, 0x00,
+	0x00, 0x00, 0x12, 0x70, 0x72, 0x6f, 0x78, 0x79,
+	0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74,
+	0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, 0x00,
+	0x13, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2d, 0x61,
+	0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
+	0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05,
+	0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, 0x00,
+	0x07, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x72,
+	0x00, 0x00, 0x00, 0x0b, 0x72, 0x65, 0x74, 0x72,
+	0x79, 0x2d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x00,
+	0x00, 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65,
+	0x72, 0x00, 0x00, 0x00, 0x02, 0x74, 0x65, 0x00,
+	0x00, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c,
+	0x65, 0x72, 0x00, 0x00, 0x00, 0x11, 0x74, 0x72,
+	0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2d, 0x65,
+	0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00,
+	0x00, 0x00, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61,
+	0x64, 0x65, 0x00, 0x00, 0x00, 0x0a, 0x75, 0x73,
+	0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74,
+	0x00, 0x00, 0x00, 0x04, 0x76, 0x61, 0x72, 0x79,
+	0x00, 0x00, 0x00, 0x03, 0x76, 0x69, 0x61, 0x00,
+	0x00, 0x00, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69,
+	0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, 0x77, 0x77,
+	0x77, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e,
+	0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00,
+	0x00, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64,
+	0x00, 0x00, 0x00, 0x03, 0x67, 0x65, 0x74, 0x00,
+	0x00, 0x00, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
+	0x73, 0x00, 0x00, 0x00, 0x06, 0x32, 0x30, 0x30,
+	0x20, 0x4f, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x76,
+	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00,
+	0x00, 0x08, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31,
+	0x2e, 0x31, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72,
+	0x6c, 0x00, 0x00, 0x00, 0x06, 0x70, 0x75, 0x62,
+	0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x0a, 0x73,
+	0x65, 0x74, 0x2d, 0x63, 0x6f, 0x6f, 0x6b, 0x69,
+	0x65, 0x00, 0x00, 0x00, 0x0a, 0x6b, 0x65, 0x65,
+	0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x00,
+	0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69,
+	0x6e, 0x31, 0x30, 0x30, 0x31, 0x30, 0x31, 0x32,
+	0x30, 0x31, 0x32, 0x30, 0x32, 0x32, 0x30, 0x35,
+	0x32, 0x30, 0x36, 0x33, 0x30, 0x30, 0x33, 0x30,
+	0x32, 0x33, 0x30, 0x33, 0x33, 0x30, 0x34, 0x33,
+	0x30, 0x35, 0x33, 0x30, 0x36, 0x33, 0x30, 0x37,
+	0x34, 0x30, 0x32, 0x34, 0x30, 0x35, 0x34, 0x30,
+	0x36, 0x34, 0x30, 0x37, 0x34, 0x30, 0x38, 0x34,
+	0x30, 0x39, 0x34, 0x31, 0x30, 0x34, 0x31, 0x31,
+	0x34, 0x31, 0x32, 0x34, 0x31, 0x33, 0x34, 0x31,
+	0x34, 0x34, 0x31, 0x35, 0x34, 0x31, 0x36, 0x34,
+	0x31, 0x37, 0x35, 0x30, 0x32, 0x35, 0x30, 0x34,
+	0x35, 0x30, 0x35, 0x32, 0x30, 0x33, 0x20, 0x4e,
+	0x6f, 0x6e, 0x2d, 0x41, 0x75, 0x74, 0x68, 0x6f,
+	0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65,
+	0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61,
+	0x74, 0x69, 0x6f, 0x6e, 0x32, 0x30, 0x34, 0x20,
+	0x4e, 0x6f, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x65,
+	0x6e, 0x74, 0x33, 0x30, 0x31, 0x20, 0x4d, 0x6f,
+	0x76, 0x65, 0x64, 0x20, 0x50, 0x65, 0x72, 0x6d,
+	0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x34,
+	0x30, 0x30, 0x20, 0x42, 0x61, 0x64, 0x20, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x34, 0x30,
+	0x31, 0x20, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68,
+	0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x34, 0x30,
+	0x33, 0x20, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64,
+	0x64, 0x65, 0x6e, 0x34, 0x30, 0x34, 0x20, 0x4e,
+	0x6f, 0x74, 0x20, 0x46, 0x6f, 0x75, 0x6e, 0x64,
+	0x35, 0x30, 0x30, 0x20, 0x49, 0x6e, 0x74, 0x65,
+	0x72, 0x6e, 0x61, 0x6c, 0x20, 0x53, 0x65, 0x72,
+	0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6f,
+	0x72, 0x35, 0x30, 0x31, 0x20, 0x4e, 0x6f, 0x74,
+	0x20, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65,
+	0x6e, 0x74, 0x65, 0x64, 0x35, 0x30, 0x33, 0x20,
+	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20,
+	0x55, 0x6e, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61,
+	0x62, 0x6c, 0x65, 0x4a, 0x61, 0x6e, 0x20, 0x46,
+	0x65, 0x62, 0x20, 0x4d, 0x61, 0x72, 0x20, 0x41,
+	0x70, 0x72, 0x20, 0x4d, 0x61, 0x79, 0x20, 0x4a,
+	0x75, 0x6e, 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x41,
+	0x75, 0x67, 0x20, 0x53, 0x65, 0x70, 0x74, 0x20,
+	0x4f, 0x63, 0x74, 0x20, 0x4e, 0x6f, 0x76, 0x20,
+	0x44, 0x65, 0x63, 0x20, 0x30, 0x30, 0x3a, 0x30,
+	0x30, 0x3a, 0x30, 0x30, 0x20, 0x4d, 0x6f, 0x6e,
+	0x2c, 0x20, 0x54, 0x75, 0x65, 0x2c, 0x20, 0x57,
+	0x65, 0x64, 0x2c, 0x20, 0x54, 0x68, 0x75, 0x2c,
+	0x20, 0x46, 0x72, 0x69, 0x2c, 0x20, 0x53, 0x61,
+	0x74, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x2c, 0x20,
+	0x47, 0x4d, 0x54, 0x63, 0x68, 0x75, 0x6e, 0x6b,
+	0x65, 0x64, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f,
+	0x68, 0x74, 0x6d, 0x6c, 0x2c, 0x69, 0x6d, 0x61,
+	0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x2c, 0x69,
+	0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, 0x67,
+	0x2c, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x67,
+	0x69, 0x66, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69,
+	0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78,
+	0x6d, 0x6c, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69,
+	0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78,
+	0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c,
+	0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c,
+	0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74,
+	0x2f, 0x6a, 0x61, 0x76, 0x61, 0x73, 0x63, 0x72,
+	0x69, 0x70, 0x74, 0x2c, 0x70, 0x75, 0x62, 0x6c,
+	0x69, 0x63, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74,
+	0x65, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65,
+	0x3d, 0x67, 0x7a, 0x69, 0x70, 0x2c, 0x64, 0x65,
+	0x66, 0x6c, 0x61, 0x74, 0x65, 0x2c, 0x73, 0x64,
+	0x63, 0x68, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65,
+	0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x63,
+	0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x69,
+	0x73, 0x6f, 0x2d, 0x38, 0x38, 0x35, 0x39, 0x2d,
+	0x31, 0x2c, 0x75, 0x74, 0x66, 0x2d, 0x2c, 0x2a,
+	0x2c, 0x65, 0x6e, 0x71, 0x3d, 0x30, 0x2e,
+}
diff --git a/vendor/github.com/moby/spdystream/spdy/read.go b/vendor/github.com/moby/spdystream/spdy/read.go
new file mode 100644
index 00000000..75ea045b
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/spdy/read.go
@@ -0,0 +1,364 @@
+/*
+   Copyright 2014-2021 Docker Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package spdy
+
+import (
+	"compress/zlib"
+	"encoding/binary"
+	"io"
+	"net/http"
+	"strings"
+)
+
+func (frame *SynStreamFrame) read(h ControlFrameHeader, f *Framer) error {
+	return f.readSynStreamFrame(h, frame)
+}
+
+func (frame *SynReplyFrame) read(h ControlFrameHeader, f *Framer) error {
+	return f.readSynReplyFrame(h, frame)
+}
+
+func (frame *RstStreamFrame) read(h ControlFrameHeader, f *Framer) error {
+	frame.CFHeader = h
+	if err := binary.Read(f.r, binary.BigEndian, &frame.StreamId); err != nil {
+		return err
+	}
+	if err := binary.Read(f.r, binary.BigEndian, &frame.Status); err != nil {
+		return err
+	}
+	if frame.Status == 0 {
+		return &Error{InvalidControlFrame, frame.StreamId}
+	}
+	if frame.StreamId == 0 {
+		return &Error{ZeroStreamId, 0}
+	}
+	return nil
+}
+
+func (frame *SettingsFrame) read(h ControlFrameHeader, f *Framer) error {
+	frame.CFHeader = h
+	var numSettings uint32
+	if err := binary.Read(f.r, binary.BigEndian, &numSettings); err != nil {
+		return err
+	}
+	frame.FlagIdValues = make([]SettingsFlagIdValue, numSettings)
+	for i := uint32(0); i < numSettings; i++ {
+		if err := binary.Read(f.r, binary.BigEndian, &frame.FlagIdValues[i].Id); err != nil {
+			return err
+		}
+		frame.FlagIdValues[i].Flag = SettingsFlag((frame.FlagIdValues[i].Id & 0xff000000) >> 24)
+		frame.FlagIdValues[i].Id &= 0xffffff
+		if err := binary.Read(f.r, binary.BigEndian, &frame.FlagIdValues[i].Value); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (frame *PingFrame) read(h ControlFrameHeader, f *Framer) error {
+	frame.CFHeader = h
+	if err := binary.Read(f.r, binary.BigEndian, &frame.Id); err != nil {
+		return err
+	}
+	if frame.Id == 0 {
+		return &Error{ZeroStreamId, 0}
+	}
+	if frame.CFHeader.Flags != 0 {
+		return &Error{InvalidControlFrame, StreamId(frame.Id)}
+	}
+	return nil
+}
+
+func (frame *GoAwayFrame) read(h ControlFrameHeader, f *Framer) error {
+	frame.CFHeader = h
+	if err := binary.Read(f.r, binary.BigEndian, &frame.LastGoodStreamId); err != nil {
+		return err
+	}
+	if frame.CFHeader.Flags != 0 {
+		return &Error{InvalidControlFrame, frame.LastGoodStreamId}
+	}
+	if frame.CFHeader.length != 8 {
+		return &Error{InvalidControlFrame, frame.LastGoodStreamId}
+	}
+	if err := binary.Read(f.r, binary.BigEndian, &frame.Status); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (frame *HeadersFrame) read(h ControlFrameHeader, f *Framer) error {
+	return f.readHeadersFrame(h, frame)
+}
+
+func (frame *WindowUpdateFrame) read(h ControlFrameHeader, f *Framer) error {
+	frame.CFHeader = h
+	if err := binary.Read(f.r, binary.BigEndian, &frame.StreamId); err != nil {
+		return err
+	}
+	if frame.CFHeader.Flags != 0 {
+		return &Error{InvalidControlFrame, frame.StreamId}
+	}
+	if frame.CFHeader.length != 8 {
+		return &Error{InvalidControlFrame, frame.StreamId}
+	}
+	if err := binary.Read(f.r, binary.BigEndian, &frame.DeltaWindowSize); err != nil {
+		return err
+	}
+	return nil
+}
+
+func newControlFrame(frameType ControlFrameType) (controlFrame, error) {
+	ctor, ok := cframeCtor[frameType]
+	if !ok {
+		return nil, &Error{Err: InvalidControlFrame}
+	}
+	return ctor(), nil
+}
+
+var cframeCtor = map[ControlFrameType]func() controlFrame{
+	TypeSynStream:    func() controlFrame { return new(SynStreamFrame) },
+	TypeSynReply:     func() controlFrame { return new(SynReplyFrame) },
+	TypeRstStream:    func() controlFrame { return new(RstStreamFrame) },
+	TypeSettings:     func() controlFrame { return new(SettingsFrame) },
+	TypePing:         func() controlFrame { return new(PingFrame) },
+	TypeGoAway:       func() controlFrame { return new(GoAwayFrame) },
+	TypeHeaders:      func() controlFrame { return new(HeadersFrame) },
+	TypeWindowUpdate: func() controlFrame { return new(WindowUpdateFrame) },
+}
+
+func (f *Framer) uncorkHeaderDecompressor(payloadSize int64) error {
+	if f.headerDecompressor != nil {
+		f.headerReader.N = payloadSize
+		return nil
+	}
+	f.headerReader = io.LimitedReader{R: f.r, N: payloadSize}
+	decompressor, err := zlib.NewReaderDict(&f.headerReader, []byte(headerDictionary))
+	if err != nil {
+		return err
+	}
+	f.headerDecompressor = decompressor
+	return nil
+}
+
+// ReadFrame reads SPDY encoded data and returns a decompressed Frame.
+func (f *Framer) ReadFrame() (Frame, error) {
+	var firstWord uint32
+	if err := binary.Read(f.r, binary.BigEndian, &firstWord); err != nil {
+		return nil, err
+	}
+	if firstWord&0x80000000 != 0 {
+		frameType := ControlFrameType(firstWord & 0xffff)
+		version := uint16(firstWord >> 16 & 0x7fff)
+		return f.parseControlFrame(version, frameType)
+	}
+	return f.parseDataFrame(StreamId(firstWord & 0x7fffffff))
+}
+
+func (f *Framer) parseControlFrame(version uint16, frameType ControlFrameType) (Frame, error) {
+	var length uint32
+	if err := binary.Read(f.r, binary.BigEndian, &length); err != nil {
+		return nil, err
+	}
+	flags := ControlFlags((length & 0xff000000) >> 24)
+	length &= 0xffffff
+	header := ControlFrameHeader{version, frameType, flags, length}
+	cframe, err := newControlFrame(frameType)
+	if err != nil {
+		return nil, err
+	}
+	if err = cframe.read(header, f); err != nil {
+		return nil, err
+	}
+	return cframe, nil
+}
+
+func parseHeaderValueBlock(r io.Reader, streamId StreamId) (http.Header, error) {
+	var numHeaders uint32
+	if err := binary.Read(r, binary.BigEndian, &numHeaders); err != nil {
+		return nil, err
+	}
+	var e error
+	h := make(http.Header, int(numHeaders))
+	for i := 0; i < int(numHeaders); i++ {
+		var length uint32
+		if err := binary.Read(r, binary.BigEndian, &length); err != nil {
+			return nil, err
+		}
+		nameBytes := make([]byte, length)
+		if _, err := io.ReadFull(r, nameBytes); err != nil {
+			return nil, err
+		}
+		name := string(nameBytes)
+		if name != strings.ToLower(name) {
+			e = &Error{UnlowercasedHeaderName, streamId}
+			name = strings.ToLower(name)
+		}
+		if h[name] != nil {
+			e = &Error{DuplicateHeaders, streamId}
+		}
+		if err := binary.Read(r, binary.BigEndian, &length); err != nil {
+			return nil, err
+		}
+		value := make([]byte, length)
+		if _, err := io.ReadFull(r, value); err != nil {
+			return nil, err
+		}
+		valueList := strings.Split(string(value), headerValueSeparator)
+		for _, v := range valueList {
+			h.Add(name, v)
+		}
+	}
+	if e != nil {
+		return h, e
+	}
+	return h, nil
+}
+
+func (f *Framer) readSynStreamFrame(h ControlFrameHeader, frame *SynStreamFrame) error {
+	frame.CFHeader = h
+	var err error
+	if err = binary.Read(f.r, binary.BigEndian, &frame.StreamId); err != nil {
+		return err
+	}
+	if err = binary.Read(f.r, binary.BigEndian, &frame.AssociatedToStreamId); err != nil {
+		return err
+	}
+	if err = binary.Read(f.r, binary.BigEndian, &frame.Priority); err != nil {
+		return err
+	}
+	frame.Priority >>= 5
+	if err = binary.Read(f.r, binary.BigEndian, &frame.Slot); err != nil {
+		return err
+	}
+	reader := f.r
+	if !f.headerCompressionDisabled {
+		err := f.uncorkHeaderDecompressor(int64(h.length - 10))
+		if err != nil {
+			return err
+		}
+		reader = f.headerDecompressor
+	}
+	frame.Headers, err = parseHeaderValueBlock(reader, frame.StreamId)
+	if !f.headerCompressionDisabled && (err == io.EOF && f.headerReader.N == 0 || f.headerReader.N != 0) {
+		err = &Error{WrongCompressedPayloadSize, 0}
+	}
+	if err != nil {
+		return err
+	}
+	for h := range frame.Headers {
+		if invalidReqHeaders[h] {
+			return &Error{InvalidHeaderPresent, frame.StreamId}
+		}
+	}
+	if frame.StreamId == 0 {
+		return &Error{ZeroStreamId, 0}
+	}
+	return nil
+}
+
+func (f *Framer) readSynReplyFrame(h ControlFrameHeader, frame *SynReplyFrame) error {
+	frame.CFHeader = h
+	var err error
+	if err = binary.Read(f.r, binary.BigEndian, &frame.StreamId); err != nil {
+		return err
+	}
+	reader := f.r
+	if !f.headerCompressionDisabled {
+		err := f.uncorkHeaderDecompressor(int64(h.length - 4))
+		if err != nil {
+			return err
+		}
+		reader = f.headerDecompressor
+	}
+	frame.Headers, err = parseHeaderValueBlock(reader, frame.StreamId)
+	if !f.headerCompressionDisabled && (err == io.EOF && f.headerReader.N == 0 || f.headerReader.N != 0) {
+		err = &Error{WrongCompressedPayloadSize, 0}
+	}
+	if err != nil {
+		return err
+	}
+	for h := range frame.Headers {
+		if invalidRespHeaders[h] {
+			return &Error{InvalidHeaderPresent, frame.StreamId}
+		}
+	}
+	if frame.StreamId == 0 {
+		return &Error{ZeroStreamId, 0}
+	}
+	return nil
+}
+
+func (f *Framer) readHeadersFrame(h ControlFrameHeader, frame *HeadersFrame) error {
+	frame.CFHeader = h
+	var err error
+	if err = binary.Read(f.r, binary.BigEndian, &frame.StreamId); err != nil {
+		return err
+	}
+	reader := f.r
+	if !f.headerCompressionDisabled {
+		err := f.uncorkHeaderDecompressor(int64(h.length - 4))
+		if err != nil {
+			return err
+		}
+		reader = f.headerDecompressor
+	}
+	frame.Headers, err = parseHeaderValueBlock(reader, frame.StreamId)
+	if !f.headerCompressionDisabled && (err == io.EOF && f.headerReader.N == 0 || f.headerReader.N != 0) {
+		err = &Error{WrongCompressedPayloadSize, 0}
+	}
+	if err != nil {
+		return err
+	}
+	var invalidHeaders map[string]bool
+	if frame.StreamId%2 == 0 {
+		invalidHeaders = invalidReqHeaders
+	} else {
+		invalidHeaders = invalidRespHeaders
+	}
+	for h := range frame.Headers {
+		if invalidHeaders[h] {
+			return &Error{InvalidHeaderPresent, frame.StreamId}
+		}
+	}
+	if frame.StreamId == 0 {
+		return &Error{ZeroStreamId, 0}
+	}
+	return nil
+}
+
+func (f *Framer) parseDataFrame(streamId StreamId) (*DataFrame, error) {
+	var length uint32
+	if err := binary.Read(f.r, binary.BigEndian, &length); err != nil {
+		return nil, err
+	}
+	var frame DataFrame
+	frame.StreamId = streamId
+	frame.Flags = DataFlags(length >> 24)
+	length &= 0xffffff
+	frame.Data = make([]byte, length)
+	if _, err := io.ReadFull(f.r, frame.Data); err != nil {
+		return nil, err
+	}
+	if frame.StreamId == 0 {
+		return nil, &Error{ZeroStreamId, 0}
+	}
+	return &frame, nil
+}
diff --git a/vendor/github.com/moby/spdystream/spdy/types.go b/vendor/github.com/moby/spdystream/spdy/types.go
new file mode 100644
index 00000000..a254a43a
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/spdy/types.go
@@ -0,0 +1,291 @@
+/*
+   Copyright 2014-2021 Docker Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package spdy implements the SPDY protocol (currently SPDY/3), described in
+// http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3.
+package spdy
+
+import (
+	"bytes"
+	"compress/zlib"
+	"io"
+	"net/http"
+)
+
+// Version is the protocol version number that this package implements.
+const Version = 3
+
+// ControlFrameType stores the type field in a control frame header.
+type ControlFrameType uint16
+
+const (
+	TypeSynStream    ControlFrameType = 0x0001
+	TypeSynReply     ControlFrameType = 0x0002
+	TypeRstStream    ControlFrameType = 0x0003
+	TypeSettings     ControlFrameType = 0x0004
+	TypePing         ControlFrameType = 0x0006
+	TypeGoAway       ControlFrameType = 0x0007
+	TypeHeaders      ControlFrameType = 0x0008
+	TypeWindowUpdate ControlFrameType = 0x0009
+)
+
+// ControlFlags are the flags that can be set on a control frame.
+type ControlFlags uint8
+
+const (
+	ControlFlagFin                   ControlFlags = 0x01
+	ControlFlagUnidirectional        ControlFlags = 0x02
+	ControlFlagSettingsClearSettings ControlFlags = 0x01
+)
+
+// DataFlags are the flags that can be set on a data frame.
+type DataFlags uint8
+
+const (
+	DataFlagFin DataFlags = 0x01
+)
+
+// MaxDataLength is the maximum number of bytes that can be stored in one frame.
+const MaxDataLength = 1<<24 - 1
+
+// headerValueSepator separates multiple header values.
+const headerValueSeparator = "\x00"
+
+// Frame is a single SPDY frame in its unpacked in-memory representation. Use
+// Framer to read and write it.
+type Frame interface {
+	write(f *Framer) error
+}
+
+// ControlFrameHeader contains all the fields in a control frame header,
+// in its unpacked in-memory representation.
+type ControlFrameHeader struct {
+	// Note, high bit is the "Control" bit.
+	version   uint16 // spdy version number
+	frameType ControlFrameType
+	Flags     ControlFlags
+	length    uint32 // length of data field
+}
+
+type controlFrame interface {
+	Frame
+	read(h ControlFrameHeader, f *Framer) error
+}
+
+// StreamId represents a 31-bit value identifying the stream.
+type StreamId uint32
+
+// SynStreamFrame is the unpacked, in-memory representation of a SYN_STREAM
+// frame.
+type SynStreamFrame struct {
+	CFHeader             ControlFrameHeader
+	StreamId             StreamId
+	AssociatedToStreamId StreamId // stream id for a stream which this stream is associated to
+	Priority             uint8    // priority of this frame (3-bit)
+	Slot                 uint8    // index in the server's credential vector of the client certificate
+	Headers              http.Header
+}
+
+// SynReplyFrame is the unpacked, in-memory representation of a SYN_REPLY frame.
+type SynReplyFrame struct {
+	CFHeader ControlFrameHeader
+	StreamId StreamId
+	Headers  http.Header
+}
+
+// RstStreamStatus represents the status that led to a RST_STREAM.
+type RstStreamStatus uint32
+
+const (
+	ProtocolError RstStreamStatus = iota + 1
+	InvalidStream
+	RefusedStream
+	UnsupportedVersion
+	Cancel
+	InternalError
+	FlowControlError
+	StreamInUse
+	StreamAlreadyClosed
+	InvalidCredentials
+	FrameTooLarge
+)
+
+// RstStreamFrame is the unpacked, in-memory representation of a RST_STREAM
+// frame.
+type RstStreamFrame struct {
+	CFHeader ControlFrameHeader
+	StreamId StreamId
+	Status   RstStreamStatus
+}
+
+// SettingsFlag represents a flag in a SETTINGS frame.
+type SettingsFlag uint8
+
+const (
+	FlagSettingsPersistValue SettingsFlag = 0x1
+	FlagSettingsPersisted    SettingsFlag = 0x2
+)
+
+// SettingsFlag represents the id of an id/value pair in a SETTINGS frame.
+type SettingsId uint32
+
+const (
+	SettingsUploadBandwidth SettingsId = iota + 1
+	SettingsDownloadBandwidth
+	SettingsRoundTripTime
+	SettingsMaxConcurrentStreams
+	SettingsCurrentCwnd
+	SettingsDownloadRetransRate
+	SettingsInitialWindowSize
+	SettingsClientCretificateVectorSize
+)
+
+// SettingsFlagIdValue is the unpacked, in-memory representation of the
+// combined flag/id/value for a setting in a SETTINGS frame.
+type SettingsFlagIdValue struct {
+	Flag  SettingsFlag
+	Id    SettingsId
+	Value uint32
+}
+
+// SettingsFrame is the unpacked, in-memory representation of a SPDY
+// SETTINGS frame.
+type SettingsFrame struct {
+	CFHeader     ControlFrameHeader
+	FlagIdValues []SettingsFlagIdValue
+}
+
+// PingFrame is the unpacked, in-memory representation of a PING frame.
+type PingFrame struct {
+	CFHeader ControlFrameHeader
+	Id       uint32 // unique id for this ping, from server is even, from client is odd.
+}
+
+// GoAwayStatus represents the status in a GoAwayFrame.
+type GoAwayStatus uint32
+
+const (
+	GoAwayOK GoAwayStatus = iota
+	GoAwayProtocolError
+	GoAwayInternalError
+)
+
+// GoAwayFrame is the unpacked, in-memory representation of a GOAWAY frame.
+type GoAwayFrame struct {
+	CFHeader         ControlFrameHeader
+	LastGoodStreamId StreamId // last stream id which was accepted by sender
+	Status           GoAwayStatus
+}
+
+// HeadersFrame is the unpacked, in-memory representation of a HEADERS frame.
+type HeadersFrame struct {
+	CFHeader ControlFrameHeader
+	StreamId StreamId
+	Headers  http.Header
+}
+
+// WindowUpdateFrame is the unpacked, in-memory representation of a
+// WINDOW_UPDATE frame.
+type WindowUpdateFrame struct {
+	CFHeader        ControlFrameHeader
+	StreamId        StreamId
+	DeltaWindowSize uint32 // additional number of bytes to existing window size
+}
+
+// TODO: Implement credential frame and related methods.
+
+// DataFrame is the unpacked, in-memory representation of a DATA frame.
+type DataFrame struct {
+	// Note, high bit is the "Control" bit. Should be 0 for data frames.
+	StreamId StreamId
+	Flags    DataFlags
+	Data     []byte // payload data of this frame
+}
+
+// A SPDY specific error.
+type ErrorCode string
+
+const (
+	UnlowercasedHeaderName     ErrorCode = "header was not lowercased"
+	DuplicateHeaders           ErrorCode = "multiple headers with same name"
+	WrongCompressedPayloadSize ErrorCode = "compressed payload size was incorrect"
+	UnknownFrameType           ErrorCode = "unknown frame type"
+	InvalidControlFrame        ErrorCode = "invalid control frame"
+	InvalidDataFrame           ErrorCode = "invalid data frame"
+	InvalidHeaderPresent       ErrorCode = "frame contained invalid header"
+	ZeroStreamId               ErrorCode = "stream id zero is disallowed"
+)
+
+// Error contains both the type of error and additional values. StreamId is 0
+// if Error is not associated with a stream.
+type Error struct {
+	Err      ErrorCode
+	StreamId StreamId
+}
+
+func (e *Error) Error() string {
+	return string(e.Err)
+}
+
+var invalidReqHeaders = map[string]bool{
+	"Connection":        true,
+	"Host":              true,
+	"Keep-Alive":        true,
+	"Proxy-Connection":  true,
+	"Transfer-Encoding": true,
+}
+
+var invalidRespHeaders = map[string]bool{
+	"Connection":        true,
+	"Keep-Alive":        true,
+	"Proxy-Connection":  true,
+	"Transfer-Encoding": true,
+}
+
+// Framer handles serializing/deserializing SPDY frames, including compressing/
+// decompressing payloads.
+type Framer struct {
+	headerCompressionDisabled bool
+	w                         io.Writer
+	headerBuf                 *bytes.Buffer
+	headerCompressor          *zlib.Writer
+	r                         io.Reader
+	headerReader              io.LimitedReader
+	headerDecompressor        io.ReadCloser
+}
+
+// NewFramer allocates a new Framer for a given SPDY connection, represented by
+// a io.Writer and io.Reader. Note that Framer will read and write individual fields
+// from/to the Reader and Writer, so the caller should pass in an appropriately
+// buffered implementation to optimize performance.
+func NewFramer(w io.Writer, r io.Reader) (*Framer, error) {
+	compressBuf := new(bytes.Buffer)
+	compressor, err := zlib.NewWriterLevelDict(compressBuf, zlib.BestCompression, []byte(headerDictionary))
+	if err != nil {
+		return nil, err
+	}
+	framer := &Framer{
+		w:                w,
+		headerBuf:        compressBuf,
+		headerCompressor: compressor,
+		r:                r,
+	}
+	return framer, nil
+}
diff --git a/vendor/github.com/moby/spdystream/spdy/write.go b/vendor/github.com/moby/spdystream/spdy/write.go
new file mode 100644
index 00000000..ab6d91f3
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/spdy/write.go
@@ -0,0 +1,334 @@
+/*
+   Copyright 2014-2021 Docker Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package spdy
+
+import (
+	"encoding/binary"
+	"io"
+	"net/http"
+	"strings"
+)
+
+func (frame *SynStreamFrame) write(f *Framer) error {
+	return f.writeSynStreamFrame(frame)
+}
+
+func (frame *SynReplyFrame) write(f *Framer) error {
+	return f.writeSynReplyFrame(frame)
+}
+
+func (frame *RstStreamFrame) write(f *Framer) (err error) {
+	if frame.StreamId == 0 {
+		return &Error{ZeroStreamId, 0}
+	}
+	frame.CFHeader.version = Version
+	frame.CFHeader.frameType = TypeRstStream
+	frame.CFHeader.Flags = 0
+	frame.CFHeader.length = 8
+
+	// Serialize frame to Writer.
+	if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
+		return
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
+		return
+	}
+	if frame.Status == 0 {
+		return &Error{InvalidControlFrame, frame.StreamId}
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.Status); err != nil {
+		return
+	}
+	return
+}
+
+func (frame *SettingsFrame) write(f *Framer) (err error) {
+	frame.CFHeader.version = Version
+	frame.CFHeader.frameType = TypeSettings
+	frame.CFHeader.length = uint32(len(frame.FlagIdValues)*8 + 4)
+
+	// Serialize frame to Writer.
+	if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
+		return
+	}
+	if err = binary.Write(f.w, binary.BigEndian, uint32(len(frame.FlagIdValues))); err != nil {
+		return
+	}
+	for _, flagIdValue := range frame.FlagIdValues {
+		flagId := uint32(flagIdValue.Flag)<<24 | uint32(flagIdValue.Id)
+		if err = binary.Write(f.w, binary.BigEndian, flagId); err != nil {
+			return
+		}
+		if err = binary.Write(f.w, binary.BigEndian, flagIdValue.Value); err != nil {
+			return
+		}
+	}
+	return
+}
+
+func (frame *PingFrame) write(f *Framer) (err error) {
+	if frame.Id == 0 {
+		return &Error{ZeroStreamId, 0}
+	}
+	frame.CFHeader.version = Version
+	frame.CFHeader.frameType = TypePing
+	frame.CFHeader.Flags = 0
+	frame.CFHeader.length = 4
+
+	// Serialize frame to Writer.
+	if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
+		return
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.Id); err != nil {
+		return
+	}
+	return
+}
+
+func (frame *GoAwayFrame) write(f *Framer) (err error) {
+	frame.CFHeader.version = Version
+	frame.CFHeader.frameType = TypeGoAway
+	frame.CFHeader.Flags = 0
+	frame.CFHeader.length = 8
+
+	// Serialize frame to Writer.
+	if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
+		return
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.LastGoodStreamId); err != nil {
+		return
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.Status); err != nil {
+		return
+	}
+	return nil
+}
+
+func (frame *HeadersFrame) write(f *Framer) error {
+	return f.writeHeadersFrame(frame)
+}
+
+func (frame *WindowUpdateFrame) write(f *Framer) (err error) {
+	frame.CFHeader.version = Version
+	frame.CFHeader.frameType = TypeWindowUpdate
+	frame.CFHeader.Flags = 0
+	frame.CFHeader.length = 8
+
+	// Serialize frame to Writer.
+	if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
+		return
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
+		return
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.DeltaWindowSize); err != nil {
+		return
+	}
+	return nil
+}
+
+func (frame *DataFrame) write(f *Framer) error {
+	return f.writeDataFrame(frame)
+}
+
+// WriteFrame writes a frame.
+func (f *Framer) WriteFrame(frame Frame) error {
+	return frame.write(f)
+}
+
+func writeControlFrameHeader(w io.Writer, h ControlFrameHeader) error {
+	if err := binary.Write(w, binary.BigEndian, 0x8000|h.version); err != nil {
+		return err
+	}
+	if err := binary.Write(w, binary.BigEndian, h.frameType); err != nil {
+		return err
+	}
+	flagsAndLength := uint32(h.Flags)<<24 | h.length
+	if err := binary.Write(w, binary.BigEndian, flagsAndLength); err != nil {
+		return err
+	}
+	return nil
+}
+
+func writeHeaderValueBlock(w io.Writer, h http.Header) (n int, err error) {
+	n = 0
+	if err = binary.Write(w, binary.BigEndian, uint32(len(h))); err != nil {
+		return
+	}
+	n += 2
+	for name, values := range h {
+		if err = binary.Write(w, binary.BigEndian, uint32(len(name))); err != nil {
+			return
+		}
+		n += 2
+		name = strings.ToLower(name)
+		if _, err = io.WriteString(w, name); err != nil {
+			return
+		}
+		n += len(name)
+		v := strings.Join(values, headerValueSeparator)
+		if err = binary.Write(w, binary.BigEndian, uint32(len(v))); err != nil {
+			return
+		}
+		n += 2
+		if _, err = io.WriteString(w, v); err != nil {
+			return
+		}
+		n += len(v)
+	}
+	return
+}
+
+func (f *Framer) writeSynStreamFrame(frame *SynStreamFrame) (err error) {
+	if frame.StreamId == 0 {
+		return &Error{ZeroStreamId, 0}
+	}
+	// Marshal the headers.
+	var writer io.Writer = f.headerBuf
+	if !f.headerCompressionDisabled {
+		writer = f.headerCompressor
+	}
+	if _, err = writeHeaderValueBlock(writer, frame.Headers); err != nil {
+		return
+	}
+	if !f.headerCompressionDisabled {
+		f.headerCompressor.Flush()
+	}
+
+	// Set ControlFrameHeader.
+	frame.CFHeader.version = Version
+	frame.CFHeader.frameType = TypeSynStream
+	frame.CFHeader.length = uint32(len(f.headerBuf.Bytes()) + 10)
+
+	// Serialize frame to Writer.
+	if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
+		return err
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
+		return err
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.AssociatedToStreamId); err != nil {
+		return err
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.Priority<<5); err != nil {
+		return err
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.Slot); err != nil {
+		return err
+	}
+	if _, err = f.w.Write(f.headerBuf.Bytes()); err != nil {
+		return err
+	}
+	f.headerBuf.Reset()
+	return nil
+}
+
+func (f *Framer) writeSynReplyFrame(frame *SynReplyFrame) (err error) {
+	if frame.StreamId == 0 {
+		return &Error{ZeroStreamId, 0}
+	}
+	// Marshal the headers.
+	var writer io.Writer = f.headerBuf
+	if !f.headerCompressionDisabled {
+		writer = f.headerCompressor
+	}
+	if _, err = writeHeaderValueBlock(writer, frame.Headers); err != nil {
+		return
+	}
+	if !f.headerCompressionDisabled {
+		f.headerCompressor.Flush()
+	}
+
+	// Set ControlFrameHeader.
+	frame.CFHeader.version = Version
+	frame.CFHeader.frameType = TypeSynReply
+	frame.CFHeader.length = uint32(len(f.headerBuf.Bytes()) + 4)
+
+	// Serialize frame to Writer.
+	if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
+		return
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
+		return
+	}
+	if _, err = f.w.Write(f.headerBuf.Bytes()); err != nil {
+		return
+	}
+	f.headerBuf.Reset()
+	return
+}
+
+func (f *Framer) writeHeadersFrame(frame *HeadersFrame) (err error) {
+	if frame.StreamId == 0 {
+		return &Error{ZeroStreamId, 0}
+	}
+	// Marshal the headers.
+	var writer io.Writer = f.headerBuf
+	if !f.headerCompressionDisabled {
+		writer = f.headerCompressor
+	}
+	if _, err = writeHeaderValueBlock(writer, frame.Headers); err != nil {
+		return
+	}
+	if !f.headerCompressionDisabled {
+		f.headerCompressor.Flush()
+	}
+
+	// Set ControlFrameHeader.
+	frame.CFHeader.version = Version
+	frame.CFHeader.frameType = TypeHeaders
+	frame.CFHeader.length = uint32(len(f.headerBuf.Bytes()) + 4)
+
+	// Serialize frame to Writer.
+	if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
+		return
+	}
+	if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
+		return
+	}
+	if _, err = f.w.Write(f.headerBuf.Bytes()); err != nil {
+		return
+	}
+	f.headerBuf.Reset()
+	return
+}
+
+func (f *Framer) writeDataFrame(frame *DataFrame) (err error) {
+	if frame.StreamId == 0 {
+		return &Error{ZeroStreamId, 0}
+	}
+	if frame.StreamId&0x80000000 != 0 || len(frame.Data) > MaxDataLength {
+		return &Error{InvalidDataFrame, frame.StreamId}
+	}
+
+	// Serialize frame to Writer.
+	if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
+		return
+	}
+	flagsAndLength := uint32(frame.Flags)<<24 | uint32(len(frame.Data))
+	if err = binary.Write(f.w, binary.BigEndian, flagsAndLength); err != nil {
+		return
+	}
+	if _, err = f.w.Write(frame.Data); err != nil {
+		return
+	}
+	return nil
+}
diff --git a/vendor/github.com/moby/spdystream/stream.go b/vendor/github.com/moby/spdystream/stream.go
new file mode 100644
index 00000000..404e3c02
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/stream.go
@@ -0,0 +1,343 @@
+/*
+   Copyright 2014-2021 Docker Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package spdystream
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/moby/spdystream/spdy"
+)
+
+var (
+	ErrUnreadPartialData = errors.New("unread partial data")
+)
+
+type Stream struct {
+	streamId  spdy.StreamId
+	parent    *Stream
+	conn      *Connection
+	startChan chan error
+
+	dataLock sync.RWMutex
+	dataChan chan []byte
+	unread   []byte
+
+	priority   uint8
+	headers    http.Header
+	headerChan chan http.Header
+	finishLock sync.Mutex
+	finished   bool
+	replyCond  *sync.Cond
+	replied    bool
+	closeLock  sync.Mutex
+	closeChan  chan bool
+}
+
+// WriteData writes data to stream, sending a dataframe per call
+func (s *Stream) WriteData(data []byte, fin bool) error {
+	s.waitWriteReply()
+	var flags spdy.DataFlags
+
+	if fin {
+		flags = spdy.DataFlagFin
+		s.finishLock.Lock()
+		if s.finished {
+			s.finishLock.Unlock()
+			return ErrWriteClosedStream
+		}
+		s.finished = true
+		s.finishLock.Unlock()
+	}
+
+	dataFrame := &spdy.DataFrame{
+		StreamId: s.streamId,
+		Flags:    flags,
+		Data:     data,
+	}
+
+	debugMessage("(%p) (%d) Writing data frame", s, s.streamId)
+	return s.conn.framer.WriteFrame(dataFrame)
+}
+
+// Write writes bytes to a stream, calling write data for each call.
+func (s *Stream) Write(data []byte) (n int, err error) {
+	err = s.WriteData(data, false)
+	if err == nil {
+		n = len(data)
+	}
+	return
+}
+
+// Read reads bytes from a stream, a single read will never get more
+// than what is sent on a single data frame, but a multiple calls to
+// read may get data from the same data frame.
+func (s *Stream) Read(p []byte) (n int, err error) {
+	if s.unread == nil {
+		select {
+		case <-s.closeChan:
+			return 0, io.EOF
+		case read, ok := <-s.dataChan:
+			if !ok {
+				return 0, io.EOF
+			}
+			s.unread = read
+		}
+	}
+	n = copy(p, s.unread)
+	if n < len(s.unread) {
+		s.unread = s.unread[n:]
+	} else {
+		s.unread = nil
+	}
+	return
+}
+
+// ReadData reads an entire data frame and returns the byte array
+// from the data frame.  If there is unread data from the result
+// of a Read call, this function will return an ErrUnreadPartialData.
+func (s *Stream) ReadData() ([]byte, error) {
+	debugMessage("(%p) Reading data from %d", s, s.streamId)
+	if s.unread != nil {
+		return nil, ErrUnreadPartialData
+	}
+	select {
+	case <-s.closeChan:
+		return nil, io.EOF
+	case read, ok := <-s.dataChan:
+		if !ok {
+			return nil, io.EOF
+		}
+		return read, nil
+	}
+}
+
+func (s *Stream) waitWriteReply() {
+	if s.replyCond != nil {
+		s.replyCond.L.Lock()
+		for !s.replied {
+			s.replyCond.Wait()
+		}
+		s.replyCond.L.Unlock()
+	}
+}
+
+// Wait waits for the stream to receive a reply.
+func (s *Stream) Wait() error {
+	return s.WaitTimeout(time.Duration(0))
+}
+
+// WaitTimeout waits for the stream to receive a reply or for timeout.
+// When the timeout is reached, ErrTimeout will be returned.
+func (s *Stream) WaitTimeout(timeout time.Duration) error {
+	var timeoutChan <-chan time.Time
+	if timeout > time.Duration(0) {
+		timeoutChan = time.After(timeout)
+	}
+
+	select {
+	case err := <-s.startChan:
+		if err != nil {
+			return err
+		}
+		break
+	case <-timeoutChan:
+		return ErrTimeout
+	}
+	return nil
+}
+
+// Close closes the stream by sending an empty data frame with the
+// finish flag set, indicating this side is finished with the stream.
+func (s *Stream) Close() error {
+	select {
+	case <-s.closeChan:
+		// Stream is now fully closed
+		s.conn.removeStream(s)
+	default:
+		break
+	}
+	return s.WriteData([]byte{}, true)
+}
+
+// Reset sends a reset frame, putting the stream into the fully closed state.
+func (s *Stream) Reset() error {
+	s.conn.removeStream(s)
+	return s.resetStream()
+}
+
+func (s *Stream) resetStream() error {
+	// Always call closeRemoteChannels, even if s.finished is already true.
+	// This makes it so that stream.Close() followed by stream.Reset() allows
+	// stream.Read() to unblock.
+	s.closeRemoteChannels()
+
+	s.finishLock.Lock()
+	if s.finished {
+		s.finishLock.Unlock()
+		return nil
+	}
+	s.finished = true
+	s.finishLock.Unlock()
+
+	resetFrame := &spdy.RstStreamFrame{
+		StreamId: s.streamId,
+		Status:   spdy.Cancel,
+	}
+	return s.conn.framer.WriteFrame(resetFrame)
+}
+
+// CreateSubStream creates a stream using the current as the parent
+func (s *Stream) CreateSubStream(headers http.Header, fin bool) (*Stream, error) {
+	return s.conn.CreateStream(headers, s, fin)
+}
+
+// SetPriority sets the stream priority, does not affect the
+// remote priority of this stream after Open has been called.
+// Valid values are 0 through 7, 0 being the highest priority
+// and 7 the lowest.
+func (s *Stream) SetPriority(priority uint8) {
+	s.priority = priority
+}
+
+// SendHeader sends a header frame across the stream
+func (s *Stream) SendHeader(headers http.Header, fin bool) error {
+	return s.conn.sendHeaders(headers, s, fin)
+}
+
+// SendReply sends a reply on a stream, only valid to be called once
+// when handling a new stream
+func (s *Stream) SendReply(headers http.Header, fin bool) error {
+	if s.replyCond == nil {
+		return errors.New("cannot reply on initiated stream")
+	}
+	s.replyCond.L.Lock()
+	defer s.replyCond.L.Unlock()
+	if s.replied {
+		return nil
+	}
+
+	err := s.conn.sendReply(headers, s, fin)
+	if err != nil {
+		return err
+	}
+
+	s.replied = true
+	s.replyCond.Broadcast()
+	return nil
+}
+
+// Refuse sends a reset frame with the status refuse, only
+// valid to be called once when handling a new stream.  This
+// may be used to indicate that a stream is not allowed
+// when http status codes are not being used.
+func (s *Stream) Refuse() error {
+	if s.replied {
+		return nil
+	}
+	s.replied = true
+	return s.conn.sendReset(spdy.RefusedStream, s)
+}
+
+// Cancel sends a reset frame with the status canceled. This
+// can be used at any time by the creator of the Stream to
+// indicate the stream is no longer needed.
+func (s *Stream) Cancel() error {
+	return s.conn.sendReset(spdy.Cancel, s)
+}
+
+// ReceiveHeader receives a header sent on the other side
+// of the stream.  This function will block until a header
+// is received or stream is closed.
+func (s *Stream) ReceiveHeader() (http.Header, error) {
+	select {
+	case <-s.closeChan:
+		break
+	case header, ok := <-s.headerChan:
+		if !ok {
+			return nil, fmt.Errorf("header chan closed")
+		}
+		return header, nil
+	}
+	return nil, fmt.Errorf("stream closed")
+}
+
+// Parent returns the parent stream
+func (s *Stream) Parent() *Stream {
+	return s.parent
+}
+
+// Headers returns the headers used to create the stream
+func (s *Stream) Headers() http.Header {
+	return s.headers
+}
+
+// String returns the string version of stream using the
+// streamId to uniquely identify the stream
+func (s *Stream) String() string {
+	return fmt.Sprintf("stream:%d", s.streamId)
+}
+
+// Identifier returns a 32 bit identifier for the stream
+func (s *Stream) Identifier() uint32 {
+	return uint32(s.streamId)
+}
+
+// IsFinished returns whether the stream has finished
+// sending data
+func (s *Stream) IsFinished() bool {
+	return s.finished
+}
+
+// Implement net.Conn interface
+
+func (s *Stream) LocalAddr() net.Addr {
+	return s.conn.conn.LocalAddr()
+}
+
+func (s *Stream) RemoteAddr() net.Addr {
+	return s.conn.conn.RemoteAddr()
+}
+
+// TODO set per stream values instead of connection-wide
+
+func (s *Stream) SetDeadline(t time.Time) error {
+	return s.conn.conn.SetDeadline(t)
+}
+
+func (s *Stream) SetReadDeadline(t time.Time) error {
+	return s.conn.conn.SetReadDeadline(t)
+}
+
+func (s *Stream) SetWriteDeadline(t time.Time) error {
+	return s.conn.conn.SetWriteDeadline(t)
+}
+
+func (s *Stream) closeRemoteChannels() {
+	s.closeLock.Lock()
+	defer s.closeLock.Unlock()
+	select {
+	case <-s.closeChan:
+	default:
+		close(s.closeChan)
+	}
+}
diff --git a/vendor/github.com/moby/spdystream/utils.go b/vendor/github.com/moby/spdystream/utils.go
new file mode 100644
index 00000000..e9f7fffd
--- /dev/null
+++ b/vendor/github.com/moby/spdystream/utils.go
@@ -0,0 +1,32 @@
+/*
+   Copyright 2014-2021 Docker Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package spdystream
+
+import (
+	"log"
+	"os"
+)
+
+var (
+	DEBUG = os.Getenv("DEBUG")
+)
+
+func debugMessage(fmt string, args ...interface{}) {
+	if DEBUG != "" {
+		log.Printf(fmt, args...)
+	}
+}
diff --git a/vendor/k8s.io/apimachinery/pkg/util/httpstream/doc.go b/vendor/k8s.io/apimachinery/pkg/util/httpstream/doc.go
new file mode 100644
index 00000000..5893df5b
--- /dev/null
+++ b/vendor/k8s.io/apimachinery/pkg/util/httpstream/doc.go
@@ -0,0 +1,19 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package httpstream adds multiplexed streaming support to HTTP requests and
+// responses via connection upgrades.
+package httpstream // import "k8s.io/apimachinery/pkg/util/httpstream"
diff --git a/vendor/k8s.io/apimachinery/pkg/util/httpstream/httpstream.go b/vendor/k8s.io/apimachinery/pkg/util/httpstream/httpstream.go
new file mode 100644
index 00000000..00ce5f78
--- /dev/null
+++ b/vendor/k8s.io/apimachinery/pkg/util/httpstream/httpstream.go
@@ -0,0 +1,157 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package httpstream
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"time"
+)
+
+const (
+	HeaderConnection               = "Connection"
+	HeaderUpgrade                  = "Upgrade"
+	HeaderProtocolVersion          = "X-Stream-Protocol-Version"
+	HeaderAcceptedProtocolVersions = "X-Accepted-Stream-Protocol-Versions"
+)
+
+// NewStreamHandler defines a function that is called when a new Stream is
+// received. If no error is returned, the Stream is accepted; otherwise,
+// the stream is rejected. After the reply frame has been sent, replySent is closed.
+type NewStreamHandler func(stream Stream, replySent <-chan struct{}) error
+
+// NoOpNewStreamHandler is a stream handler that accepts a new stream and
+// performs no other logic.
+func NoOpNewStreamHandler(stream Stream, replySent <-chan struct{}) error { return nil }
+
+// Dialer knows how to open a streaming connection to a server.
+type Dialer interface {
+
+	// Dial opens a streaming connection to a server using one of the protocols
+	// specified (in order of most preferred to least preferred).
+	Dial(protocols ...string) (Connection, string, error)
+}
+
+// UpgradeRoundTripper is a type of http.RoundTripper that is able to upgrade
+// HTTP requests to support multiplexed bidirectional streams. After RoundTrip()
+// is invoked, if the upgrade is successful, clients may retrieve the upgraded
+// connection by calling UpgradeRoundTripper.Connection().
+type UpgradeRoundTripper interface {
+	http.RoundTripper
+	// NewConnection validates the response and creates a new Connection.
+	NewConnection(resp *http.Response) (Connection, error)
+}
+
+// ResponseUpgrader knows how to upgrade HTTP requests and responses to
+// add streaming support to them.
+type ResponseUpgrader interface {
+	// UpgradeResponse upgrades an HTTP response to one that supports multiplexed
+	// streams. newStreamHandler will be called asynchronously whenever the
+	// other end of the upgraded connection creates a new stream.
+	UpgradeResponse(w http.ResponseWriter, req *http.Request, newStreamHandler NewStreamHandler) Connection
+}
+
+// Connection represents an upgraded HTTP connection.
+type Connection interface {
+	// CreateStream creates a new Stream with the supplied headers.
+	CreateStream(headers http.Header) (Stream, error)
+	// Close resets all streams and closes the connection.
+	Close() error
+	// CloseChan returns a channel that is closed when the underlying connection is closed.
+	CloseChan() <-chan bool
+	// SetIdleTimeout sets the amount of time the connection may remain idle before
+	// it is automatically closed.
+	SetIdleTimeout(timeout time.Duration)
+}
+
+// Stream represents a bidirectional communications channel that is part of an
+// upgraded connection.
+type Stream interface {
+	io.ReadWriteCloser
+	// Reset closes both directions of the stream, indicating that neither client
+	// or server can use it any more.
+	Reset() error
+	// Headers returns the headers used to create the stream.
+	Headers() http.Header
+	// Identifier returns the stream's ID.
+	Identifier() uint32
+}
+
+// IsUpgradeRequest returns true if the given request is a connection upgrade request
+func IsUpgradeRequest(req *http.Request) bool {
+	for _, h := range req.Header[http.CanonicalHeaderKey(HeaderConnection)] {
+		if strings.Contains(strings.ToLower(h), strings.ToLower(HeaderUpgrade)) {
+			return true
+		}
+	}
+	return false
+}
+
+func negotiateProtocol(clientProtocols, serverProtocols []string) string {
+	for i := range clientProtocols {
+		for j := range serverProtocols {
+			if clientProtocols[i] == serverProtocols[j] {
+				return clientProtocols[i]
+			}
+		}
+	}
+	return ""
+}
+
+func commaSeparatedHeaderValues(header []string) []string {
+	var parsedClientProtocols []string
+	for i := range header {
+		for _, clientProtocol := range strings.Split(header[i], ",") {
+			if proto := strings.Trim(clientProtocol, " "); len(proto) > 0 {
+				parsedClientProtocols = append(parsedClientProtocols, proto)
+			}
+		}
+	}
+	return parsedClientProtocols
+}
+
+// Handshake performs a subprotocol negotiation. If the client did request a
+// subprotocol, Handshake will select the first common value found in
+// serverProtocols. If a match is found, Handshake adds a response header
+// indicating the chosen subprotocol. If no match is found, HTTP forbidden is
+// returned, along with a response header containing the list of protocols the
+// server can accept.
+func Handshake(req *http.Request, w http.ResponseWriter, serverProtocols []string) (string, error) {
+	clientProtocols := commaSeparatedHeaderValues(req.Header[http.CanonicalHeaderKey(HeaderProtocolVersion)])
+	if len(clientProtocols) == 0 {
+		return "", fmt.Errorf("unable to upgrade: %s is required", HeaderProtocolVersion)
+	}
+
+	if len(serverProtocols) == 0 {
+		panic(fmt.Errorf("unable to upgrade: serverProtocols is required"))
+	}
+
+	negotiatedProtocol := negotiateProtocol(clientProtocols, serverProtocols)
+	if len(negotiatedProtocol) == 0 {
+		for i := range serverProtocols {
+			w.Header().Add(HeaderAcceptedProtocolVersions, serverProtocols[i])
+		}
+		err := fmt.Errorf("unable to upgrade: unable to negotiate protocol: client supports %v, server accepts %v", clientProtocols, serverProtocols)
+		http.Error(w, err.Error(), http.StatusForbidden)
+		return "", err
+	}
+
+	w.Header().Add(HeaderProtocolVersion, negotiatedProtocol)
+	return negotiatedProtocol, nil
+}
diff --git a/vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy/connection.go b/vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy/connection.go
new file mode 100644
index 00000000..21b2568d
--- /dev/null
+++ b/vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy/connection.go
@@ -0,0 +1,187 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package spdy
+
+import (
+	"net"
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/moby/spdystream"
+	"k8s.io/apimachinery/pkg/util/httpstream"
+	"k8s.io/klog/v2"
+)
+
+// connection maintains state about a spdystream.Connection and its associated
+// streams.
+type connection struct {
+	conn             *spdystream.Connection
+	streams          []httpstream.Stream
+	streamLock       sync.Mutex
+	newStreamHandler httpstream.NewStreamHandler
+	ping             func() (time.Duration, error)
+}
+
+// NewClientConnection creates a new SPDY client connection.
+func NewClientConnection(conn net.Conn) (httpstream.Connection, error) {
+	return NewClientConnectionWithPings(conn, 0)
+}
+
+// NewClientConnectionWithPings creates a new SPDY client connection.
+//
+// If pingPeriod is non-zero, a background goroutine will send periodic Ping
+// frames to the server. Use this to keep idle connections through certain load
+// balancers alive longer.
+func NewClientConnectionWithPings(conn net.Conn, pingPeriod time.Duration) (httpstream.Connection, error) {
+	spdyConn, err := spdystream.NewConnection(conn, false)
+	if err != nil {
+		defer conn.Close()
+		return nil, err
+	}
+
+	return newConnection(spdyConn, httpstream.NoOpNewStreamHandler, pingPeriod, spdyConn.Ping), nil
+}
+
+// NewServerConnection creates a new SPDY server connection. newStreamHandler
+// will be invoked when the server receives a newly created stream from the
+// client.
+func NewServerConnection(conn net.Conn, newStreamHandler httpstream.NewStreamHandler) (httpstream.Connection, error) {
+	return NewServerConnectionWithPings(conn, newStreamHandler, 0)
+}
+
+// NewServerConnectionWithPings creates a new SPDY server connection.
+// newStreamHandler will be invoked when the server receives a newly created
+// stream from the client.
+//
+// If pingPeriod is non-zero, a background goroutine will send periodic Ping
+// frames to the server. Use this to keep idle connections through certain load
+// balancers alive longer.
+func NewServerConnectionWithPings(conn net.Conn, newStreamHandler httpstream.NewStreamHandler, pingPeriod time.Duration) (httpstream.Connection, error) {
+	spdyConn, err := spdystream.NewConnection(conn, true)
+	if err != nil {
+		defer conn.Close()
+		return nil, err
+	}
+
+	return newConnection(spdyConn, newStreamHandler, pingPeriod, spdyConn.Ping), nil
+}
+
+// newConnection returns a new connection wrapping conn. newStreamHandler
+// will be invoked when the server receives a newly created stream from the
+// client.
+func newConnection(conn *spdystream.Connection, newStreamHandler httpstream.NewStreamHandler, pingPeriod time.Duration, pingFn func() (time.Duration, error)) httpstream.Connection {
+	c := &connection{conn: conn, newStreamHandler: newStreamHandler, ping: pingFn}
+	go conn.Serve(c.newSpdyStream)
+	if pingPeriod > 0 && pingFn != nil {
+		go c.sendPings(pingPeriod)
+	}
+	return c
+}
+
+// createStreamResponseTimeout indicates how long to wait for the other side to
+// acknowledge the new stream before timing out.
+const createStreamResponseTimeout = 30 * time.Second
+
+// Close first sends a reset for all of the connection's streams, and then
+// closes the underlying spdystream.Connection.
+func (c *connection) Close() error {
+	c.streamLock.Lock()
+	for _, s := range c.streams {
+		// calling Reset instead of Close ensures that all streams are fully torn down
+		s.Reset()
+	}
+	c.streams = make([]httpstream.Stream, 0)
+	c.streamLock.Unlock()
+
+	// now that all streams are fully torn down, it's safe to call close on the underlying connection,
+	// which should be able to terminate immediately at this point, instead of waiting for any
+	// remaining graceful stream termination.
+	return c.conn.Close()
+}
+
+// CreateStream creates a new stream with the specified headers and registers
+// it with the connection.
+func (c *connection) CreateStream(headers http.Header) (httpstream.Stream, error) {
+	stream, err := c.conn.CreateStream(headers, nil, false)
+	if err != nil {
+		return nil, err
+	}
+	if err = stream.WaitTimeout(createStreamResponseTimeout); err != nil {
+		return nil, err
+	}
+
+	c.registerStream(stream)
+	return stream, nil
+}
+
+// registerStream adds the stream s to the connection's list of streams that
+// it owns.
+func (c *connection) registerStream(s httpstream.Stream) {
+	c.streamLock.Lock()
+	c.streams = append(c.streams, s)
+	c.streamLock.Unlock()
+}
+
+// CloseChan returns a channel that, when closed, indicates that the underlying
+// spdystream.Connection has been closed.
+func (c *connection) CloseChan() <-chan bool {
+	return c.conn.CloseChan()
+}
+
+// newSpdyStream is the internal new stream handler used by spdystream.Connection.Serve.
+// It calls connection's newStreamHandler, giving it the opportunity to accept or reject
+// the stream. If newStreamHandler returns an error, the stream is rejected. If not, the
+// stream is accepted and registered with the connection.
+func (c *connection) newSpdyStream(stream *spdystream.Stream) {
+	replySent := make(chan struct{})
+	err := c.newStreamHandler(stream, replySent)
+	rejectStream := (err != nil)
+	if rejectStream {
+		klog.Warningf("Stream rejected: %v", err)
+		stream.Reset()
+		return
+	}
+
+	c.registerStream(stream)
+	stream.SendReply(http.Header{}, rejectStream)
+	close(replySent)
+}
+
+// SetIdleTimeout sets the amount of time the connection may remain idle before
+// it is automatically closed.
+func (c *connection) SetIdleTimeout(timeout time.Duration) {
+	c.conn.SetIdleTimeout(timeout)
+}
+
+func (c *connection) sendPings(period time.Duration) {
+	t := time.NewTicker(period)
+	defer t.Stop()
+	for {
+		select {
+		case <-c.conn.CloseChan():
+			return
+		case <-t.C:
+		}
+		if _, err := c.ping(); err != nil {
+			klog.V(3).Infof("SPDY Ping failed: %v", err)
+			// Continue, in case this is a transient failure.
+			// c.conn.CloseChan above will tell us when the connection is
+			// actually closed.
+		}
+	}
+}
diff --git a/vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy/roundtripper.go b/vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy/roundtripper.go
new file mode 100644
index 00000000..4cb1cfad
--- /dev/null
+++ b/vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy/roundtripper.go
@@ -0,0 +1,369 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package spdy
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"crypto/tls"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
+	"strings"
+	"time"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/serializer"
+	"k8s.io/apimachinery/pkg/util/httpstream"
+	utilnet "k8s.io/apimachinery/pkg/util/net"
+	"k8s.io/apimachinery/third_party/forked/golang/netutil"
+)
+
+// SpdyRoundTripper knows how to upgrade an HTTP request to one that supports
+// multiplexed streams. After RoundTrip() is invoked, Conn will be set
+// and usable. SpdyRoundTripper implements the UpgradeRoundTripper interface.
+type SpdyRoundTripper struct {
+	//tlsConfig holds the TLS configuration settings to use when connecting
+	//to the remote server.
+	tlsConfig *tls.Config
+
+	/* TODO according to http://golang.org/pkg/net/http/#RoundTripper, a RoundTripper
+	   must be safe for use by multiple concurrent goroutines. If this is absolutely
+	   necessary, we could keep a map from http.Request to net.Conn. In practice,
+	   a client will create an http.Client, set the transport to a new insteace of
+	   SpdyRoundTripper, and use it a single time, so this hopefully won't be an issue.
+	*/
+	// conn is the underlying network connection to the remote server.
+	conn net.Conn
+
+	// Dialer is the dialer used to connect.  Used if non-nil.
+	Dialer *net.Dialer
+
+	// proxier knows which proxy to use given a request, defaults to http.ProxyFromEnvironment
+	// Used primarily for mocking the proxy discovery in tests.
+	proxier func(req *http.Request) (*url.URL, error)
+
+	// followRedirects indicates if the round tripper should examine responses for redirects and
+	// follow them.
+	followRedirects bool
+	// requireSameHostRedirects restricts redirect following to only follow redirects to the same host
+	// as the original request.
+	requireSameHostRedirects bool
+	// pingPeriod is a period for sending Ping frames over established
+	// connections.
+	pingPeriod time.Duration
+}
+
+var _ utilnet.TLSClientConfigHolder = &SpdyRoundTripper{}
+var _ httpstream.UpgradeRoundTripper = &SpdyRoundTripper{}
+var _ utilnet.Dialer = &SpdyRoundTripper{}
+
+// NewRoundTripper creates a new SpdyRoundTripper that will use the specified
+// tlsConfig.
+func NewRoundTripper(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool) *SpdyRoundTripper {
+	return NewRoundTripperWithConfig(RoundTripperConfig{
+		TLS:                      tlsConfig,
+		FollowRedirects:          followRedirects,
+		RequireSameHostRedirects: requireSameHostRedirects,
+	})
+}
+
+// NewRoundTripperWithProxy creates a new SpdyRoundTripper that will use the
+// specified tlsConfig and proxy func.
+func NewRoundTripperWithProxy(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool, proxier func(*http.Request) (*url.URL, error)) *SpdyRoundTripper {
+	return NewRoundTripperWithConfig(RoundTripperConfig{
+		TLS:                      tlsConfig,
+		FollowRedirects:          followRedirects,
+		RequireSameHostRedirects: requireSameHostRedirects,
+		Proxier:                  proxier,
+	})
+}
+
+// NewRoundTripperWithProxy creates a new SpdyRoundTripper with the specified
+// configuration.
+func NewRoundTripperWithConfig(cfg RoundTripperConfig) *SpdyRoundTripper {
+	if cfg.Proxier == nil {
+		cfg.Proxier = utilnet.NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment)
+	}
+	return &SpdyRoundTripper{
+		tlsConfig:                cfg.TLS,
+		followRedirects:          cfg.FollowRedirects,
+		requireSameHostRedirects: cfg.RequireSameHostRedirects,
+		proxier:                  cfg.Proxier,
+		pingPeriod:               cfg.PingPeriod,
+	}
+}
+
+// RoundTripperConfig is a set of options for an SpdyRoundTripper.
+type RoundTripperConfig struct {
+	// TLS configuration used by the round tripper.
+	TLS *tls.Config
+	// Proxier is a proxy function invoked on each request. Optional.
+	Proxier func(*http.Request) (*url.URL, error)
+	// PingPeriod is a period for sending SPDY Pings on the connection.
+	// Optional.
+	PingPeriod time.Duration
+
+	FollowRedirects          bool
+	RequireSameHostRedirects bool
+}
+
+// TLSClientConfig implements pkg/util/net.TLSClientConfigHolder for proper TLS checking during
+// proxying with a spdy roundtripper.
+func (s *SpdyRoundTripper) TLSClientConfig() *tls.Config {
+	return s.tlsConfig
+}
+
+// Dial implements k8s.io/apimachinery/pkg/util/net.Dialer.
+func (s *SpdyRoundTripper) Dial(req *http.Request) (net.Conn, error) {
+	conn, err := s.dial(req)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := req.Write(conn); err != nil {
+		conn.Close()
+		return nil, err
+	}
+
+	return conn, nil
+}
+
+// dial dials the host specified by req, using TLS if appropriate, optionally
+// using a proxy server if one is configured via environment variables.
+func (s *SpdyRoundTripper) dial(req *http.Request) (net.Conn, error) {
+	proxyURL, err := s.proxier(req)
+	if err != nil {
+		return nil, err
+	}
+
+	if proxyURL == nil {
+		return s.dialWithoutProxy(req.Context(), req.URL)
+	}
+
+	// ensure we use a canonical host with proxyReq
+	targetHost := netutil.CanonicalAddr(req.URL)
+
+	// proxying logic adapted from http://blog.h6t.eu/post/74098062923/golang-websocket-with-http-proxy-support
+	proxyReq := http.Request{
+		Method: "CONNECT",
+		URL:    &url.URL{},
+		Host:   targetHost,
+	}
+
+	if pa := s.proxyAuth(proxyURL); pa != "" {
+		proxyReq.Header = http.Header{}
+		proxyReq.Header.Set("Proxy-Authorization", pa)
+	}
+
+	proxyDialConn, err := s.dialWithoutProxy(req.Context(), proxyURL)
+	if err != nil {
+		return nil, err
+	}
+
+	proxyClientConn := httputil.NewProxyClientConn(proxyDialConn, nil)
+	_, err = proxyClientConn.Do(&proxyReq)
+	if err != nil && err != httputil.ErrPersistEOF {
+		return nil, err
+	}
+
+	rwc, _ := proxyClientConn.Hijack()
+
+	if req.URL.Scheme != "https" {
+		return rwc, nil
+	}
+
+	host, _, err := net.SplitHostPort(targetHost)
+	if err != nil {
+		return nil, err
+	}
+
+	tlsConfig := s.tlsConfig
+	switch {
+	case tlsConfig == nil:
+		tlsConfig = &tls.Config{ServerName: host}
+	case len(tlsConfig.ServerName) == 0:
+		tlsConfig = tlsConfig.Clone()
+		tlsConfig.ServerName = host
+	}
+
+	tlsConn := tls.Client(rwc, tlsConfig)
+
+	// need to manually call Handshake() so we can call VerifyHostname() below
+	if err := tlsConn.Handshake(); err != nil {
+		return nil, err
+	}
+
+	// Return if we were configured to skip validation
+	if tlsConfig.InsecureSkipVerify {
+		return tlsConn, nil
+	}
+
+	if err := tlsConn.VerifyHostname(tlsConfig.ServerName); err != nil {
+		return nil, err
+	}
+
+	return tlsConn, nil
+}
+
+// dialWithoutProxy dials the host specified by url, using TLS if appropriate.
+func (s *SpdyRoundTripper) dialWithoutProxy(ctx context.Context, url *url.URL) (net.Conn, error) {
+	dialAddr := netutil.CanonicalAddr(url)
+
+	if url.Scheme == "http" {
+		if s.Dialer == nil {
+			var d net.Dialer
+			return d.DialContext(ctx, "tcp", dialAddr)
+		} else {
+			return s.Dialer.DialContext(ctx, "tcp", dialAddr)
+		}
+	}
+
+	// TODO validate the TLSClientConfig is set up?
+	var conn *tls.Conn
+	var err error
+	if s.Dialer == nil {
+		conn, err = tls.Dial("tcp", dialAddr, s.tlsConfig)
+	} else {
+		conn, err = tls.DialWithDialer(s.Dialer, "tcp", dialAddr, s.tlsConfig)
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	// Return if we were configured to skip validation
+	if s.tlsConfig != nil && s.tlsConfig.InsecureSkipVerify {
+		return conn, nil
+	}
+
+	host, _, err := net.SplitHostPort(dialAddr)
+	if err != nil {
+		return nil, err
+	}
+	if s.tlsConfig != nil && len(s.tlsConfig.ServerName) > 0 {
+		host = s.tlsConfig.ServerName
+	}
+	err = conn.VerifyHostname(host)
+	if err != nil {
+		return nil, err
+	}
+
+	return conn, nil
+}
+
+// proxyAuth returns, for a given proxy URL, the value to be used for the Proxy-Authorization header
+func (s *SpdyRoundTripper) proxyAuth(proxyURL *url.URL) string {
+	if proxyURL == nil || proxyURL.User == nil {
+		return ""
+	}
+	credentials := proxyURL.User.String()
+	encodedAuth := base64.StdEncoding.EncodeToString([]byte(credentials))
+	return fmt.Sprintf("Basic %s", encodedAuth)
+}
+
+// RoundTrip executes the Request and upgrades it. After a successful upgrade,
+// clients may call SpdyRoundTripper.Connection() to retrieve the upgraded
+// connection.
+func (s *SpdyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	header := utilnet.CloneHeader(req.Header)
+	header.Add(httpstream.HeaderConnection, httpstream.HeaderUpgrade)
+	header.Add(httpstream.HeaderUpgrade, HeaderSpdy31)
+
+	var (
+		conn        net.Conn
+		rawResponse []byte
+		err         error
+	)
+
+	if s.followRedirects {
+		conn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, req.URL, header, req.Body, s, s.requireSameHostRedirects)
+	} else {
+		clone := utilnet.CloneRequest(req)
+		clone.Header = header
+		conn, err = s.Dial(clone)
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	responseReader := bufio.NewReader(
+		io.MultiReader(
+			bytes.NewBuffer(rawResponse),
+			conn,
+		),
+	)
+
+	resp, err := http.ReadResponse(responseReader, nil)
+	if err != nil {
+		if conn != nil {
+			conn.Close()
+		}
+		return nil, err
+	}
+
+	s.conn = conn
+
+	return resp, nil
+}
+
+// NewConnection validates the upgrade response, creating and returning a new
+// httpstream.Connection if there were no errors.
+func (s *SpdyRoundTripper) NewConnection(resp *http.Response) (httpstream.Connection, error) {
+	connectionHeader := strings.ToLower(resp.Header.Get(httpstream.HeaderConnection))
+	upgradeHeader := strings.ToLower(resp.Header.Get(httpstream.HeaderUpgrade))
+	if (resp.StatusCode != http.StatusSwitchingProtocols) || !strings.Contains(connectionHeader, strings.ToLower(httpstream.HeaderUpgrade)) || !strings.Contains(upgradeHeader, strings.ToLower(HeaderSpdy31)) {
+		defer resp.Body.Close()
+		responseError := ""
+		responseErrorBytes, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			responseError = "unable to read error from server response"
+		} else {
+			// TODO: I don't belong here, I should be abstracted from this class
+			if obj, _, err := statusCodecs.UniversalDecoder().Decode(responseErrorBytes, nil, &metav1.Status{}); err == nil {
+				if status, ok := obj.(*metav1.Status); ok {
+					return nil, &apierrors.StatusError{ErrStatus: *status}
+				}
+			}
+			responseError = string(responseErrorBytes)
+			responseError = strings.TrimSpace(responseError)
+		}
+
+		return nil, fmt.Errorf("unable to upgrade connection: %s", responseError)
+	}
+
+	return NewClientConnectionWithPings(s.conn, s.pingPeriod)
+}
+
+// statusScheme is private scheme for the decoding here until someone fixes the TODO in NewConnection
+var statusScheme = runtime.NewScheme()
+
+// ParameterCodec knows about query parameters used with the meta v1 API spec.
+var statusCodecs = serializer.NewCodecFactory(statusScheme)
+
+func init() {
+	statusScheme.AddUnversionedTypes(metav1.SchemeGroupVersion,
+		&metav1.Status{},
+	)
+}
diff --git a/vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy/upgrade.go b/vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy/upgrade.go
new file mode 100644
index 00000000..f17eb09e
--- /dev/null
+++ b/vendor/k8s.io/apimachinery/pkg/util/httpstream/spdy/upgrade.go
@@ -0,0 +1,120 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package spdy
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"strings"
+	"sync/atomic"
+	"time"
+
+	"k8s.io/apimachinery/pkg/util/httpstream"
+	"k8s.io/apimachinery/pkg/util/runtime"
+)
+
+const HeaderSpdy31 = "SPDY/3.1"
+
+// responseUpgrader knows how to upgrade HTTP responses. It
+// implements the httpstream.ResponseUpgrader interface.
+type responseUpgrader struct {
+	pingPeriod time.Duration
+}
+
+// connWrapper is used to wrap a hijacked connection and its bufio.Reader. All
+// calls will be handled directly by the underlying net.Conn with the exception
+// of Read and Close calls, which will consider data in the bufio.Reader. This
+// ensures that data already inside the used bufio.Reader instance is also
+// read.
+type connWrapper struct {
+	net.Conn
+	closed    int32
+	bufReader *bufio.Reader
+}
+
+func (w *connWrapper) Read(b []byte) (n int, err error) {
+	if atomic.LoadInt32(&w.closed) == 1 {
+		return 0, io.EOF
+	}
+	return w.bufReader.Read(b)
+}
+
+func (w *connWrapper) Close() error {
+	err := w.Conn.Close()
+	atomic.StoreInt32(&w.closed, 1)
+	return err
+}
+
+// NewResponseUpgrader returns a new httpstream.ResponseUpgrader that is
+// capable of upgrading HTTP responses using SPDY/3.1 via the
+// spdystream package.
+func NewResponseUpgrader() httpstream.ResponseUpgrader {
+	return NewResponseUpgraderWithPings(0)
+}
+
+// NewResponseUpgraderWithPings returns a new httpstream.ResponseUpgrader that
+// is capable of upgrading HTTP responses using SPDY/3.1 via the spdystream
+// package.
+//
+// If pingPeriod is non-zero, for each incoming connection a background
+// goroutine will send periodic Ping frames to the server. Use this to keep
+// idle connections through certain load balancers alive longer.
+func NewResponseUpgraderWithPings(pingPeriod time.Duration) httpstream.ResponseUpgrader {
+	return responseUpgrader{pingPeriod: pingPeriod}
+}
+
+// UpgradeResponse upgrades an HTTP response to one that supports multiplexed
+// streams. newStreamHandler will be called synchronously whenever the
+// other end of the upgraded connection creates a new stream.
+func (u responseUpgrader) UpgradeResponse(w http.ResponseWriter, req *http.Request, newStreamHandler httpstream.NewStreamHandler) httpstream.Connection {
+	connectionHeader := strings.ToLower(req.Header.Get(httpstream.HeaderConnection))
+	upgradeHeader := strings.ToLower(req.Header.Get(httpstream.HeaderUpgrade))
+	if !strings.Contains(connectionHeader, strings.ToLower(httpstream.HeaderUpgrade)) || !strings.Contains(upgradeHeader, strings.ToLower(HeaderSpdy31)) {
+		errorMsg := fmt.Sprintf("unable to upgrade: missing upgrade headers in request: %#v", req.Header)
+		http.Error(w, errorMsg, http.StatusBadRequest)
+		return nil
+	}
+
+	hijacker, ok := w.(http.Hijacker)
+	if !ok {
+		errorMsg := fmt.Sprintf("unable to upgrade: unable to hijack response")
+		http.Error(w, errorMsg, http.StatusInternalServerError)
+		return nil
+	}
+
+	w.Header().Add(httpstream.HeaderConnection, httpstream.HeaderUpgrade)
+	w.Header().Add(httpstream.HeaderUpgrade, HeaderSpdy31)
+	w.WriteHeader(http.StatusSwitchingProtocols)
+
+	conn, bufrw, err := hijacker.Hijack()
+	if err != nil {
+		runtime.HandleError(fmt.Errorf("unable to upgrade: error hijacking response: %v", err))
+		return nil
+	}
+
+	connWithBuf := &connWrapper{Conn: conn, bufReader: bufrw.Reader}
+	spdyConn, err := NewServerConnectionWithPings(connWithBuf, newStreamHandler, u.pingPeriod)
+	if err != nil {
+		runtime.HandleError(fmt.Errorf("unable to upgrade: error creating SPDY server connection: %v", err))
+		return nil
+	}
+
+	return spdyConn
+}
diff --git a/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go b/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go
new file mode 100644
index 00000000..82a473bb
--- /dev/null
+++ b/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go
@@ -0,0 +1,127 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package rand provides utilities related to randomization.
+package rand
+
+import (
+	"math/rand"
+	"sync"
+	"time"
+)
+
+var rng = struct {
+	sync.Mutex
+	rand *rand.Rand
+}{
+	rand: rand.New(rand.NewSource(time.Now().UnixNano())),
+}
+
+// Int returns a non-negative pseudo-random int.
+func Int() int {
+	rng.Lock()
+	defer rng.Unlock()
+	return rng.rand.Int()
+}
+
+// Intn generates an integer in range [0,max).
+// By design this should panic if input is invalid, <= 0.
+func Intn(max int) int {
+	rng.Lock()
+	defer rng.Unlock()
+	return rng.rand.Intn(max)
+}
+
+// IntnRange generates an integer in range [min,max).
+// By design this should panic if input is invalid, <= 0.
+func IntnRange(min, max int) int {
+	rng.Lock()
+	defer rng.Unlock()
+	return rng.rand.Intn(max-min) + min
+}
+
+// IntnRange generates an int64 integer in range [min,max).
+// By design this should panic if input is invalid, <= 0.
+func Int63nRange(min, max int64) int64 {
+	rng.Lock()
+	defer rng.Unlock()
+	return rng.rand.Int63n(max-min) + min
+}
+
+// Seed seeds the rng with the provided seed.
+func Seed(seed int64) {
+	rng.Lock()
+	defer rng.Unlock()
+
+	rng.rand = rand.New(rand.NewSource(seed))
+}
+
+// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n)
+// from the default Source.
+func Perm(n int) []int {
+	rng.Lock()
+	defer rng.Unlock()
+	return rng.rand.Perm(n)
+}
+
+const (
+	// We omit vowels from the set of available characters to reduce the chances
+	// of "bad words" being formed.
+	alphanums = "bcdfghjklmnpqrstvwxz2456789"
+	// No. of bits required to index into alphanums string.
+	alphanumsIdxBits = 5
+	// Mask used to extract last alphanumsIdxBits of an int.
+	alphanumsIdxMask = 1<<alphanumsIdxBits - 1
+	// No. of random letters we can extract from a single int63.
+	maxAlphanumsPerInt = 63 / alphanumsIdxBits
+)
+
+// String generates a random alphanumeric string, without vowels, which is n
+// characters long.  This will panic if n is less than zero.
+// How the random string is created:
+// - we generate random int63's
+// - from each int63, we are extracting multiple random letters by bit-shifting and masking
+// - if some index is out of range of alphanums we neglect it (unlikely to happen multiple times in a row)
+func String(n int) string {
+	b := make([]byte, n)
+	rng.Lock()
+	defer rng.Unlock()
+
+	randomInt63 := rng.rand.Int63()
+	remaining := maxAlphanumsPerInt
+	for i := 0; i < n; {
+		if remaining == 0 {
+			randomInt63, remaining = rng.rand.Int63(), maxAlphanumsPerInt
+		}
+		if idx := int(randomInt63 & alphanumsIdxMask); idx < len(alphanums) {
+			b[i] = alphanums[idx]
+			i++
+		}
+		randomInt63 >>= alphanumsIdxBits
+		remaining--
+	}
+	return string(b)
+}
+
+// SafeEncodeString encodes s using the same characters as rand.String. This reduces the chances of bad words and
+// ensures that strings generated from hash functions appear consistent throughout the API.
+func SafeEncodeString(s string) string {
+	r := make([]byte, len(s))
+	for i, b := range []rune(s) {
+		r[i] = alphanums[(int(b) % len(alphanums))]
+	}
+	return string(r)
+}
diff --git a/vendor/k8s.io/apimachinery/pkg/util/remotecommand/constants.go b/vendor/k8s.io/apimachinery/pkg/util/remotecommand/constants.go
new file mode 100644
index 00000000..acfeb827
--- /dev/null
+++ b/vendor/k8s.io/apimachinery/pkg/util/remotecommand/constants.go
@@ -0,0 +1,53 @@
+/*
+Copyright 2016 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package remotecommand
+
+import (
+	"time"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	DefaultStreamCreationTimeout = 30 * time.Second
+
+	// The SPDY subprotocol "channel.k8s.io" is used for remote command
+	// attachment/execution. This represents the initial unversioned subprotocol,
+	// which has the known bugs http://issues.k8s.io/13394 and
+	// http://issues.k8s.io/13395.
+	StreamProtocolV1Name = "channel.k8s.io"
+
+	// The SPDY subprotocol "v2.channel.k8s.io" is used for remote command
+	// attachment/execution. It is the second version of the subprotocol and
+	// resolves the issues present in the first version.
+	StreamProtocolV2Name = "v2.channel.k8s.io"
+
+	// The SPDY subprotocol "v3.channel.k8s.io" is used for remote command
+	// attachment/execution. It is the third version of the subprotocol and
+	// adds support for resizing container terminals.
+	StreamProtocolV3Name = "v3.channel.k8s.io"
+
+	// The SPDY subprotocol "v4.channel.k8s.io" is used for remote command
+	// attachment/execution. It is the 4th version of the subprotocol and
+	// adds support for exit codes.
+	StreamProtocolV4Name = "v4.channel.k8s.io"
+
+	NonZeroExitCodeReason = metav1.StatusReason("NonZeroExitCode")
+	ExitCodeCauseType     = metav1.CauseType("ExitCode")
+)
+
+var SupportedStreamingProtocols = []string{StreamProtocolV4Name, StreamProtocolV3Name, StreamProtocolV2Name, StreamProtocolV1Name}
diff --git a/vendor/k8s.io/apimachinery/third_party/forked/golang/netutil/addr.go b/vendor/k8s.io/apimachinery/third_party/forked/golang/netutil/addr.go
new file mode 100644
index 00000000..c70f431c
--- /dev/null
+++ b/vendor/k8s.io/apimachinery/third_party/forked/golang/netutil/addr.go
@@ -0,0 +1,27 @@
+package netutil
+
+import (
+	"net/url"
+	"strings"
+)
+
+// FROM: http://golang.org/src/net/http/client.go
+// Given a string of the form "host", "host:port", or "[ipv6::address]:port",
+// return true if the string includes a port.
+func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") }
+
+// FROM: http://golang.org/src/net/http/transport.go
+var portMap = map[string]string{
+	"http":  "80",
+	"https": "443",
+}
+
+// FROM: http://golang.org/src/net/http/transport.go
+// canonicalAddr returns url.Host but always with a ":port" suffix
+func CanonicalAddr(url *url.URL) string {
+	addr := url.Host
+	if !hasPort(addr) {
+		return addr + ":" + portMap[url.Scheme]
+	}
+	return addr
+}
diff --git a/vendor/k8s.io/client-go/tools/remotecommand/doc.go b/vendor/k8s.io/client-go/tools/remotecommand/doc.go
new file mode 100644
index 00000000..ac06a9cd
--- /dev/null
+++ b/vendor/k8s.io/client-go/tools/remotecommand/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package remotecommand adds support for executing commands in containers,
+// with support for separate stdin, stdout, and stderr streams, as well as
+// TTY.
+package remotecommand // import "k8s.io/client-go/tools/remotecommand"
diff --git a/vendor/k8s.io/client-go/tools/remotecommand/errorstream.go b/vendor/k8s.io/client-go/tools/remotecommand/errorstream.go
new file mode 100644
index 00000000..360276b6
--- /dev/null
+++ b/vendor/k8s.io/client-go/tools/remotecommand/errorstream.go
@@ -0,0 +1,55 @@
+/*
+Copyright 2016 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package remotecommand
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+
+	"k8s.io/apimachinery/pkg/util/runtime"
+)
+
+// errorStreamDecoder interprets the data on the error channel and creates a go error object from it.
+type errorStreamDecoder interface {
+	decode(message []byte) error
+}
+
+// watchErrorStream watches the errorStream for remote command error data,
+// decodes it with the given errorStreamDecoder, sends the decoded error (or nil if the remote
+// command exited successfully) to the returned error channel, and closes it.
+// This function returns immediately.
+func watchErrorStream(errorStream io.Reader, d errorStreamDecoder) chan error {
+	errorChan := make(chan error)
+
+	go func() {
+		defer runtime.HandleCrash()
+
+		message, err := ioutil.ReadAll(errorStream)
+		switch {
+		case err != nil && err != io.EOF:
+			errorChan <- fmt.Errorf("error reading from error stream: %s", err)
+		case len(message) > 0:
+			errorChan <- d.decode(message)
+		default:
+			errorChan <- nil
+		}
+		close(errorChan)
+	}()
+
+	return errorChan
+}
diff --git a/vendor/k8s.io/client-go/tools/remotecommand/reader.go b/vendor/k8s.io/client-go/tools/remotecommand/reader.go
new file mode 100644
index 00000000..d1f1be34
--- /dev/null
+++ b/vendor/k8s.io/client-go/tools/remotecommand/reader.go
@@ -0,0 +1,41 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package remotecommand
+
+import (
+	"io"
+)
+
+// readerWrapper delegates to an io.Reader so that only the io.Reader interface is implemented,
+// to keep io.Copy from doing things we don't want when copying from the reader to the data stream.
+//
+// If the Stdin io.Reader provided to remotecommand implements a WriteTo function (like bytes.Buffer does[1]),
+// io.Copy calls that method[2] to attempt to write the entire buffer to the stream in one call.
+// That results in an oversized call to spdystream.Stream#Write [3],
+// which results in a single oversized data frame[4] that is too large.
+//
+// [1] https://golang.org/pkg/bytes/#Buffer.WriteTo
+// [2] https://golang.org/pkg/io/#Copy
+// [3] https://github.com/kubernetes/kubernetes/blob/90295640ef87db9daa0144c5617afe889e7992b2/vendor/github.com/docker/spdystream/stream.go#L66-L73
+// [4] https://github.com/kubernetes/kubernetes/blob/90295640ef87db9daa0144c5617afe889e7992b2/vendor/github.com/docker/spdystream/spdy/write.go#L302-L304
+type readerWrapper struct {
+	reader io.Reader
+}
+
+func (r readerWrapper) Read(p []byte) (int, error) {
+	return r.reader.Read(p)
+}
diff --git a/vendor/k8s.io/client-go/tools/remotecommand/remotecommand.go b/vendor/k8s.io/client-go/tools/remotecommand/remotecommand.go
new file mode 100644
index 00000000..cb39faf7
--- /dev/null
+++ b/vendor/k8s.io/client-go/tools/remotecommand/remotecommand.go
@@ -0,0 +1,142 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package remotecommand
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+
+	"k8s.io/klog/v2"
+
+	"k8s.io/apimachinery/pkg/util/httpstream"
+	"k8s.io/apimachinery/pkg/util/remotecommand"
+	restclient "k8s.io/client-go/rest"
+	spdy "k8s.io/client-go/transport/spdy"
+)
+
+// StreamOptions holds information pertaining to the current streaming session:
+// input/output streams, if the client is requesting a TTY, and a terminal size queue to
+// support terminal resizing.
+type StreamOptions struct {
+	Stdin             io.Reader
+	Stdout            io.Writer
+	Stderr            io.Writer
+	Tty               bool
+	TerminalSizeQueue TerminalSizeQueue
+}
+
+// Executor is an interface for transporting shell-style streams.
+type Executor interface {
+	// Stream initiates the transport of the standard shell streams. It will transport any
+	// non-nil stream to a remote system, and return an error if a problem occurs. If tty
+	// is set, the stderr stream is not used (raw TTY manages stdout and stderr over the
+	// stdout stream).
+	Stream(options StreamOptions) error
+}
+
+type streamCreator interface {
+	CreateStream(headers http.Header) (httpstream.Stream, error)
+}
+
+type streamProtocolHandler interface {
+	stream(conn streamCreator) error
+}
+
+// streamExecutor handles transporting standard shell streams over an httpstream connection.
+type streamExecutor struct {
+	upgrader  spdy.Upgrader
+	transport http.RoundTripper
+
+	method    string
+	url       *url.URL
+	protocols []string
+}
+
+// NewSPDYExecutor connects to the provided server and upgrades the connection to
+// multiplexed bidirectional streams.
+func NewSPDYExecutor(config *restclient.Config, method string, url *url.URL) (Executor, error) {
+	wrapper, upgradeRoundTripper, err := spdy.RoundTripperFor(config)
+	if err != nil {
+		return nil, err
+	}
+	return NewSPDYExecutorForTransports(wrapper, upgradeRoundTripper, method, url)
+}
+
+// NewSPDYExecutorForTransports connects to the provided server using the given transport,
+// upgrades the response using the given upgrader to multiplexed bidirectional streams.
+func NewSPDYExecutorForTransports(transport http.RoundTripper, upgrader spdy.Upgrader, method string, url *url.URL) (Executor, error) {
+	return NewSPDYExecutorForProtocols(
+		transport, upgrader, method, url,
+		remotecommand.StreamProtocolV4Name,
+		remotecommand.StreamProtocolV3Name,
+		remotecommand.StreamProtocolV2Name,
+		remotecommand.StreamProtocolV1Name,
+	)
+}
+
+// NewSPDYExecutorForProtocols connects to the provided server and upgrades the connection to
+// multiplexed bidirectional streams using only the provided protocols. Exposed for testing, most
+// callers should use NewSPDYExecutor or NewSPDYExecutorForTransports.
+func NewSPDYExecutorForProtocols(transport http.RoundTripper, upgrader spdy.Upgrader, method string, url *url.URL, protocols ...string) (Executor, error) {
+	return &streamExecutor{
+		upgrader:  upgrader,
+		transport: transport,
+		method:    method,
+		url:       url,
+		protocols: protocols,
+	}, nil
+}
+
+// Stream opens a protocol streamer to the server and streams until a client closes
+// the connection or the server disconnects.
+func (e *streamExecutor) Stream(options StreamOptions) error {
+	req, err := http.NewRequest(e.method, e.url.String(), nil)
+	if err != nil {
+		return fmt.Errorf("error creating request: %v", err)
+	}
+
+	conn, protocol, err := spdy.Negotiate(
+		e.upgrader,
+		&http.Client{Transport: e.transport},
+		req,
+		e.protocols...,
+	)
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	var streamer streamProtocolHandler
+
+	switch protocol {
+	case remotecommand.StreamProtocolV4Name:
+		streamer = newStreamProtocolV4(options)
+	case remotecommand.StreamProtocolV3Name:
+		streamer = newStreamProtocolV3(options)
+	case remotecommand.StreamProtocolV2Name:
+		streamer = newStreamProtocolV2(options)
+	case "":
+		klog.V(4).Infof("The server did not negotiate a streaming protocol version. Falling back to %s", remotecommand.StreamProtocolV1Name)
+		fallthrough
+	case remotecommand.StreamProtocolV1Name:
+		streamer = newStreamProtocolV1(options)
+	}
+
+	return streamer.stream(conn)
+}
diff --git a/vendor/k8s.io/client-go/tools/remotecommand/resize.go b/vendor/k8s.io/client-go/tools/remotecommand/resize.go
new file mode 100644
index 00000000..c838f21b
--- /dev/null
+++ b/vendor/k8s.io/client-go/tools/remotecommand/resize.go
@@ -0,0 +1,33 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package remotecommand
+
+// TerminalSize and TerminalSizeQueue was a part of k8s.io/kubernetes/pkg/util/term
+// and were moved in order to decouple client from other term dependencies
+
+// TerminalSize represents the width and height of a terminal.
+type TerminalSize struct {
+	Width  uint16
+	Height uint16
+}
+
+// TerminalSizeQueue is capable of returning terminal resize events as they occur.
+type TerminalSizeQueue interface {
+	// Next returns the new terminal size after the terminal has been resized. It returns nil when
+	// monitoring has been stopped.
+	Next() *TerminalSize
+}
diff --git a/vendor/k8s.io/client-go/tools/remotecommand/v1.go b/vendor/k8s.io/client-go/tools/remotecommand/v1.go
new file mode 100644
index 00000000..ff0fbd2d
--- /dev/null
+++ b/vendor/k8s.io/client-go/tools/remotecommand/v1.go
@@ -0,0 +1,160 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package remotecommand
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+
+	"k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/httpstream"
+	"k8s.io/klog/v2"
+)
+
+// streamProtocolV1 implements the first version of the streaming exec & attach
+// protocol. This version has some bugs, such as not being able to detect when
+// non-interactive stdin data has ended. See http://issues.k8s.io/13394 and
+// http://issues.k8s.io/13395 for more details.
+type streamProtocolV1 struct {
+	StreamOptions
+
+	errorStream  httpstream.Stream
+	remoteStdin  httpstream.Stream
+	remoteStdout httpstream.Stream
+	remoteStderr httpstream.Stream
+}
+
+var _ streamProtocolHandler = &streamProtocolV1{}
+
+func newStreamProtocolV1(options StreamOptions) streamProtocolHandler {
+	return &streamProtocolV1{
+		StreamOptions: options,
+	}
+}
+
+func (p *streamProtocolV1) stream(conn streamCreator) error {
+	doneChan := make(chan struct{}, 2)
+	errorChan := make(chan error)
+
+	cp := func(s string, dst io.Writer, src io.Reader) {
+		klog.V(6).Infof("Copying %s", s)
+		defer klog.V(6).Infof("Done copying %s", s)
+		if _, err := io.Copy(dst, src); err != nil && err != io.EOF {
+			klog.Errorf("Error copying %s: %v", s, err)
+		}
+		if s == v1.StreamTypeStdout || s == v1.StreamTypeStderr {
+			doneChan <- struct{}{}
+		}
+	}
+
+	// set up all the streams first
+	var err error
+	headers := http.Header{}
+	headers.Set(v1.StreamType, v1.StreamTypeError)
+	p.errorStream, err = conn.CreateStream(headers)
+	if err != nil {
+		return err
+	}
+	defer p.errorStream.Reset()
+
+	// Create all the streams first, then start the copy goroutines. The server doesn't start its copy
+	// goroutines until it's received all of the streams. If the client creates the stdin stream and
+	// immediately begins copying stdin data to the server, it's possible to overwhelm and wedge the
+	// spdy frame handler in the server so that it is full of unprocessed frames. The frames aren't
+	// getting processed because the server hasn't started its copying, and it won't do that until it
+	// gets all the streams. By creating all the streams first, we ensure that the server is ready to
+	// process data before the client starts sending any. See https://issues.k8s.io/16373 for more info.
+	if p.Stdin != nil {
+		headers.Set(v1.StreamType, v1.StreamTypeStdin)
+		p.remoteStdin, err = conn.CreateStream(headers)
+		if err != nil {
+			return err
+		}
+		defer p.remoteStdin.Reset()
+	}
+
+	if p.Stdout != nil {
+		headers.Set(v1.StreamType, v1.StreamTypeStdout)
+		p.remoteStdout, err = conn.CreateStream(headers)
+		if err != nil {
+			return err
+		}
+		defer p.remoteStdout.Reset()
+	}
+
+	if p.Stderr != nil && !p.Tty {
+		headers.Set(v1.StreamType, v1.StreamTypeStderr)
+		p.remoteStderr, err = conn.CreateStream(headers)
+		if err != nil {
+			return err
+		}
+		defer p.remoteStderr.Reset()
+	}
+
+	// now that all the streams have been created, proceed with reading & copying
+
+	// always read from errorStream
+	go func() {
+		message, err := ioutil.ReadAll(p.errorStream)
+		if err != nil && err != io.EOF {
+			errorChan <- fmt.Errorf("Error reading from error stream: %s", err)
+			return
+		}
+		if len(message) > 0 {
+			errorChan <- fmt.Errorf("Error executing remote command: %s", message)
+			return
+		}
+	}()
+
+	if p.Stdin != nil {
+		// TODO this goroutine will never exit cleanly (the io.Copy never unblocks)
+		// because stdin is not closed until the process exits. If we try to call
+		// stdin.Close(), it returns no error but doesn't unblock the copy. It will
+		// exit when the process exits, instead.
+		go cp(v1.StreamTypeStdin, p.remoteStdin, readerWrapper{p.Stdin})
+	}
+
+	waitCount := 0
+	completedStreams := 0
+
+	if p.Stdout != nil {
+		waitCount++
+		go cp(v1.StreamTypeStdout, p.Stdout, p.remoteStdout)
+	}
+
+	if p.Stderr != nil && !p.Tty {
+		waitCount++
+		go cp(v1.StreamTypeStderr, p.Stderr, p.remoteStderr)
+	}
+
+Loop:
+	for {
+		select {
+		case <-doneChan:
+			completedStreams++
+			if completedStreams == waitCount {
+				break Loop
+			}
+		case err := <-errorChan:
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/vendor/k8s.io/client-go/tools/remotecommand/v2.go b/vendor/k8s.io/client-go/tools/remotecommand/v2.go
new file mode 100644
index 00000000..2f5561c9
--- /dev/null
+++ b/vendor/k8s.io/client-go/tools/remotecommand/v2.go
@@ -0,0 +1,200 @@
+/*
+Copyright 2015 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package remotecommand
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"sync"
+
+	"k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/runtime"
+)
+
+// streamProtocolV2 implements version 2 of the streaming protocol for attach
+// and exec. The original streaming protocol was metav1. As a result, this
+// version is referred to as version 2, even though it is the first actual
+// numbered version.
+type streamProtocolV2 struct {
+	StreamOptions
+
+	errorStream  io.Reader
+	remoteStdin  io.ReadWriteCloser
+	remoteStdout io.Reader
+	remoteStderr io.Reader
+}
+
+var _ streamProtocolHandler = &streamProtocolV2{}
+
+func newStreamProtocolV2(options StreamOptions) streamProtocolHandler {
+	return &streamProtocolV2{
+		StreamOptions: options,
+	}
+}
+
+func (p *streamProtocolV2) createStreams(conn streamCreator) error {
+	var err error
+	headers := http.Header{}
+
+	// set up error stream
+	headers.Set(v1.StreamType, v1.StreamTypeError)
+	p.errorStream, err = conn.CreateStream(headers)
+	if err != nil {
+		return err
+	}
+
+	// set up stdin stream
+	if p.Stdin != nil {
+		headers.Set(v1.StreamType, v1.StreamTypeStdin)
+		p.remoteStdin, err = conn.CreateStream(headers)
+		if err != nil {
+			return err
+		}
+	}
+
+	// set up stdout stream
+	if p.Stdout != nil {
+		headers.Set(v1.StreamType, v1.StreamTypeStdout)
+		p.remoteStdout, err = conn.CreateStream(headers)
+		if err != nil {
+			return err
+		}
+	}
+
+	// set up stderr stream
+	if p.Stderr != nil && !p.Tty {
+		headers.Set(v1.StreamType, v1.StreamTypeStderr)
+		p.remoteStderr, err = conn.CreateStream(headers)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (p *streamProtocolV2) copyStdin() {
+	if p.Stdin != nil {
+		var once sync.Once
+
+		// copy from client's stdin to container's stdin
+		go func() {
+			defer runtime.HandleCrash()
+
+			// if p.stdin is noninteractive, p.g. `echo abc | kubectl exec -i <pod> -- cat`, make sure
+			// we close remoteStdin as soon as the copy from p.stdin to remoteStdin finishes. Otherwise
+			// the executed command will remain running.
+			defer once.Do(func() { p.remoteStdin.Close() })
+
+			if _, err := io.Copy(p.remoteStdin, readerWrapper{p.Stdin}); err != nil {
+				runtime.HandleError(err)
+			}
+		}()
+
+		// read from remoteStdin until the stream is closed. this is essential to
+		// be able to exit interactive sessions cleanly and not leak goroutines or
+		// hang the client's terminal.
+		//
+		// TODO we aren't using go-dockerclient any more; revisit this to determine if it's still
+		// required by engine-api.
+		//
+		// go-dockerclient's current hijack implementation
+		// (https://github.com/fsouza/go-dockerclient/blob/89f3d56d93788dfe85f864a44f85d9738fca0670/client.go#L564)
+		// waits for all three streams (stdin/stdout/stderr) to finish copying
+		// before returning. When hijack finishes copying stdout/stderr, it calls
+		// Close() on its side of remoteStdin, which allows this copy to complete.
+		// When that happens, we must Close() on our side of remoteStdin, to
+		// allow the copy in hijack to complete, and hijack to return.
+		go func() {
+			defer runtime.HandleCrash()
+			defer once.Do(func() { p.remoteStdin.Close() })
+
+			// this "copy" doesn't actually read anything - it's just here to wait for
+			// the server to close remoteStdin.
+			if _, err := io.Copy(ioutil.Discard, p.remoteStdin); err != nil {
+				runtime.HandleError(err)
+			}
+		}()
+	}
+}
+
+func (p *streamProtocolV2) copyStdout(wg *sync.WaitGroup) {
+	if p.Stdout == nil {
+		return
+	}
+
+	wg.Add(1)
+	go func() {
+		defer runtime.HandleCrash()
+		defer wg.Done()
+		// make sure, packet in queue can be consumed.
+		// block in queue may lead to deadlock in conn.server
+		// issue: https://github.com/kubernetes/kubernetes/issues/96339
+		defer io.Copy(ioutil.Discard, p.remoteStdout)
+
+		if _, err := io.Copy(p.Stdout, p.remoteStdout); err != nil {
+			runtime.HandleError(err)
+		}
+	}()
+}
+
+func (p *streamProtocolV2) copyStderr(wg *sync.WaitGroup) {
+	if p.Stderr == nil || p.Tty {
+		return
+	}
+
+	wg.Add(1)
+	go func() {
+		defer runtime.HandleCrash()
+		defer wg.Done()
+		defer io.Copy(ioutil.Discard, p.remoteStderr)
+
+		if _, err := io.Copy(p.Stderr, p.remoteStderr); err != nil {
+			runtime.HandleError(err)
+		}
+	}()
+}
+
+func (p *streamProtocolV2) stream(conn streamCreator) error {
+	if err := p.createStreams(conn); err != nil {
+		return err
+	}
+
+	// now that all the streams have been created, proceed with reading & copying
+
+	errorChan := watchErrorStream(p.errorStream, &errorDecoderV2{})
+
+	p.copyStdin()
+
+	var wg sync.WaitGroup
+	p.copyStdout(&wg)
+	p.copyStderr(&wg)
+
+	// we're waiting for stdout/stderr to finish copying
+	wg.Wait()
+
+	// waits for errorStream to finish reading with an error or nil
+	return <-errorChan
+}
+
+// errorDecoderV2 interprets the error channel data as plain text.
+type errorDecoderV2 struct{}
+
+func (d *errorDecoderV2) decode(message []byte) error {
+	return fmt.Errorf("error executing remote command: %s", message)
+}
diff --git a/vendor/k8s.io/client-go/tools/remotecommand/v3.go b/vendor/k8s.io/client-go/tools/remotecommand/v3.go
new file mode 100644
index 00000000..846dd24a
--- /dev/null
+++ b/vendor/k8s.io/client-go/tools/remotecommand/v3.go
@@ -0,0 +1,111 @@
+/*
+Copyright 2016 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package remotecommand
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"sync"
+
+	"k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/runtime"
+)
+
+// streamProtocolV3 implements version 3 of the streaming protocol for attach
+// and exec. This version adds support for resizing the container's terminal.
+type streamProtocolV3 struct {
+	*streamProtocolV2
+
+	resizeStream io.Writer
+}
+
+var _ streamProtocolHandler = &streamProtocolV3{}
+
+func newStreamProtocolV3(options StreamOptions) streamProtocolHandler {
+	return &streamProtocolV3{
+		streamProtocolV2: newStreamProtocolV2(options).(*streamProtocolV2),
+	}
+}
+
+func (p *streamProtocolV3) createStreams(conn streamCreator) error {
+	// set up the streams from v2
+	if err := p.streamProtocolV2.createStreams(conn); err != nil {
+		return err
+	}
+
+	// set up resize stream
+	if p.Tty {
+		headers := http.Header{}
+		headers.Set(v1.StreamType, v1.StreamTypeResize)
+		var err error
+		p.resizeStream, err = conn.CreateStream(headers)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (p *streamProtocolV3) handleResizes() {
+	if p.resizeStream == nil || p.TerminalSizeQueue == nil {
+		return
+	}
+	go func() {
+		defer runtime.HandleCrash()
+
+		encoder := json.NewEncoder(p.resizeStream)
+		for {
+			size := p.TerminalSizeQueue.Next()
+			if size == nil {
+				return
+			}
+			if err := encoder.Encode(&size); err != nil {
+				runtime.HandleError(err)
+			}
+		}
+	}()
+}
+
+func (p *streamProtocolV3) stream(conn streamCreator) error {
+	if err := p.createStreams(conn); err != nil {
+		return err
+	}
+
+	// now that all the streams have been created, proceed with reading & copying
+
+	errorChan := watchErrorStream(p.errorStream, &errorDecoderV3{})
+
+	p.handleResizes()
+
+	p.copyStdin()
+
+	var wg sync.WaitGroup
+	p.copyStdout(&wg)
+	p.copyStderr(&wg)
+
+	// we're waiting for stdout/stderr to finish copying
+	wg.Wait()
+
+	// waits for errorStream to finish reading with an error or nil
+	return <-errorChan
+}
+
+type errorDecoderV3 struct {
+	errorDecoderV2
+}
diff --git a/vendor/k8s.io/client-go/tools/remotecommand/v4.go b/vendor/k8s.io/client-go/tools/remotecommand/v4.go
new file mode 100644
index 00000000..69ca934a
--- /dev/null
+++ b/vendor/k8s.io/client-go/tools/remotecommand/v4.go
@@ -0,0 +1,119 @@
+/*
+Copyright 2016 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package remotecommand
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+	"sync"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/remotecommand"
+	"k8s.io/client-go/util/exec"
+)
+
+// streamProtocolV4 implements version 4 of the streaming protocol for attach
+// and exec. This version adds support for exit codes on the error stream through
+// the use of metav1.Status instead of plain text messages.
+type streamProtocolV4 struct {
+	*streamProtocolV3
+}
+
+var _ streamProtocolHandler = &streamProtocolV4{}
+
+func newStreamProtocolV4(options StreamOptions) streamProtocolHandler {
+	return &streamProtocolV4{
+		streamProtocolV3: newStreamProtocolV3(options).(*streamProtocolV3),
+	}
+}
+
+func (p *streamProtocolV4) createStreams(conn streamCreator) error {
+	return p.streamProtocolV3.createStreams(conn)
+}
+
+func (p *streamProtocolV4) handleResizes() {
+	p.streamProtocolV3.handleResizes()
+}
+
+func (p *streamProtocolV4) stream(conn streamCreator) error {
+	if err := p.createStreams(conn); err != nil {
+		return err
+	}
+
+	// now that all the streams have been created, proceed with reading & copying
+
+	errorChan := watchErrorStream(p.errorStream, &errorDecoderV4{})
+
+	p.handleResizes()
+
+	p.copyStdin()
+
+	var wg sync.WaitGroup
+	p.copyStdout(&wg)
+	p.copyStderr(&wg)
+
+	// we're waiting for stdout/stderr to finish copying
+	wg.Wait()
+
+	// waits for errorStream to finish reading with an error or nil
+	return <-errorChan
+}
+
+// errorDecoderV4 interprets the json-marshaled metav1.Status on the error channel
+// and creates an exec.ExitError from it.
+type errorDecoderV4 struct{}
+
+func (d *errorDecoderV4) decode(message []byte) error {
+	status := metav1.Status{}
+	err := json.Unmarshal(message, &status)
+	if err != nil {
+		return fmt.Errorf("error stream protocol error: %v in %q", err, string(message))
+	}
+	switch status.Status {
+	case metav1.StatusSuccess:
+		return nil
+	case metav1.StatusFailure:
+		if status.Reason == remotecommand.NonZeroExitCodeReason {
+			if status.Details == nil {
+				return errors.New("error stream protocol error: details must be set")
+			}
+			for i := range status.Details.Causes {
+				c := &status.Details.Causes[i]
+				if c.Type != remotecommand.ExitCodeCauseType {
+					continue
+				}
+
+				rc, err := strconv.ParseUint(c.Message, 10, 8)
+				if err != nil {
+					return fmt.Errorf("error stream protocol error: invalid exit code value %q", c.Message)
+				}
+				return exec.CodeExitError{
+					Err:  fmt.Errorf("command terminated with exit code %d", rc),
+					Code: int(rc),
+				}
+			}
+
+			return fmt.Errorf("error stream protocol error: no %s cause given", remotecommand.ExitCodeCauseType)
+		}
+	default:
+		return errors.New("error stream protocol error: unknown error")
+	}
+
+	return fmt.Errorf(status.Message)
+}
diff --git a/vendor/k8s.io/client-go/transport/spdy/spdy.go b/vendor/k8s.io/client-go/transport/spdy/spdy.go
new file mode 100644
index 00000000..406d3cc1
--- /dev/null
+++ b/vendor/k8s.io/client-go/transport/spdy/spdy.go
@@ -0,0 +1,105 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package spdy
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"time"
+
+	"k8s.io/apimachinery/pkg/util/httpstream"
+	"k8s.io/apimachinery/pkg/util/httpstream/spdy"
+	restclient "k8s.io/client-go/rest"
+)
+
+// Upgrader validates a response from the server after a SPDY upgrade.
+type Upgrader interface {
+	// NewConnection validates the response and creates a new Connection.
+	NewConnection(resp *http.Response) (httpstream.Connection, error)
+}
+
+// RoundTripperFor returns a round tripper and upgrader to use with SPDY.
+func RoundTripperFor(config *restclient.Config) (http.RoundTripper, Upgrader, error) {
+	tlsConfig, err := restclient.TLSConfigFor(config)
+	if err != nil {
+		return nil, nil, err
+	}
+	proxy := http.ProxyFromEnvironment
+	if config.Proxy != nil {
+		proxy = config.Proxy
+	}
+	upgradeRoundTripper := spdy.NewRoundTripperWithConfig(spdy.RoundTripperConfig{
+		TLS:                      tlsConfig,
+		FollowRedirects:          true,
+		RequireSameHostRedirects: false,
+		Proxier:                  proxy,
+		PingPeriod:               time.Second * 5,
+	})
+	wrapper, err := restclient.HTTPWrappersForConfig(config, upgradeRoundTripper)
+	if err != nil {
+		return nil, nil, err
+	}
+	return wrapper, upgradeRoundTripper, nil
+}
+
+// dialer implements the httpstream.Dialer interface.
+type dialer struct {
+	client   *http.Client
+	upgrader Upgrader
+	method   string
+	url      *url.URL
+}
+
+var _ httpstream.Dialer = &dialer{}
+
+// NewDialer will create a dialer that connects to the provided URL and upgrades the connection to SPDY.
+func NewDialer(upgrader Upgrader, client *http.Client, method string, url *url.URL) httpstream.Dialer {
+	return &dialer{
+		client:   client,
+		upgrader: upgrader,
+		method:   method,
+		url:      url,
+	}
+}
+
+func (d *dialer) Dial(protocols ...string) (httpstream.Connection, string, error) {
+	req, err := http.NewRequest(d.method, d.url.String(), nil)
+	if err != nil {
+		return nil, "", fmt.Errorf("error creating request: %v", err)
+	}
+	return Negotiate(d.upgrader, d.client, req, protocols...)
+}
+
+// Negotiate opens a connection to a remote server and attempts to negotiate
+// a SPDY connection. Upon success, it returns the connection and the protocol selected by
+// the server. The client transport must use the upgradeRoundTripper - see RoundTripperFor.
+func Negotiate(upgrader Upgrader, client *http.Client, req *http.Request, protocols ...string) (httpstream.Connection, string, error) {
+	for i := range protocols {
+		req.Header.Add(httpstream.HeaderProtocolVersion, protocols[i])
+	}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, "", fmt.Errorf("error sending request: %v", err)
+	}
+	defer resp.Body.Close()
+	conn, err := upgrader.NewConnection(resp)
+	if err != nil {
+		return nil, "", err
+	}
+	return conn, resp.Header.Get(httpstream.HeaderProtocolVersion), nil
+}
diff --git a/vendor/k8s.io/client-go/util/exec/exec.go b/vendor/k8s.io/client-go/util/exec/exec.go
new file mode 100644
index 00000000..d170badb
--- /dev/null
+++ b/vendor/k8s.io/client-go/util/exec/exec.go
@@ -0,0 +1,52 @@
+/*
+Copyright 2014 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package exec
+
+// ExitError is an interface that presents an API similar to os.ProcessState, which is
+// what ExitError from os/exec is.  This is designed to make testing a bit easier and
+// probably loses some of the cross-platform properties of the underlying library.
+type ExitError interface {
+	String() string
+	Error() string
+	Exited() bool
+	ExitStatus() int
+}
+
+// CodeExitError is an implementation of ExitError consisting of an error object
+// and an exit code (the upper bits of os.exec.ExitStatus).
+type CodeExitError struct {
+	Err  error
+	Code int
+}
+
+var _ ExitError = CodeExitError{}
+
+func (e CodeExitError) Error() string {
+	return e.Err.Error()
+}
+
+func (e CodeExitError) String() string {
+	return e.Err.Error()
+}
+
+func (e CodeExitError) Exited() bool {
+	return true
+}
+
+func (e CodeExitError) ExitStatus() int {
+	return e.Code
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 823c1611..50db1d68 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -521,6 +521,9 @@ github.com/mitchellh/go-homedir
 github.com/mitchellh/ioprogress
 # github.com/mitchellh/mapstructure v1.4.1
 github.com/mitchellh/mapstructure
+# github.com/moby/spdystream v0.2.0
+github.com/moby/spdystream
+github.com/moby/spdystream/spdy
 # github.com/moby/sys/mount v0.2.0
 github.com/moby/sys/mount
 # github.com/moby/sys/mountinfo v0.4.1
@@ -984,6 +987,8 @@ k8s.io/apimachinery/pkg/util/diff
 k8s.io/apimachinery/pkg/util/duration
 k8s.io/apimachinery/pkg/util/errors
 k8s.io/apimachinery/pkg/util/framer
+k8s.io/apimachinery/pkg/util/httpstream
+k8s.io/apimachinery/pkg/util/httpstream/spdy
 k8s.io/apimachinery/pkg/util/intstr
 k8s.io/apimachinery/pkg/util/json
 k8s.io/apimachinery/pkg/util/jsonmergepatch
@@ -991,6 +996,8 @@ k8s.io/apimachinery/pkg/util/managedfields
 k8s.io/apimachinery/pkg/util/mergepatch
 k8s.io/apimachinery/pkg/util/naming
 k8s.io/apimachinery/pkg/util/net
+k8s.io/apimachinery/pkg/util/rand
+k8s.io/apimachinery/pkg/util/remotecommand
 k8s.io/apimachinery/pkg/util/runtime
 k8s.io/apimachinery/pkg/util/sets
 k8s.io/apimachinery/pkg/util/strategicpatch
@@ -1002,6 +1009,7 @@ k8s.io/apimachinery/pkg/util/yaml
 k8s.io/apimachinery/pkg/version
 k8s.io/apimachinery/pkg/watch
 k8s.io/apimachinery/third_party/forked/golang/json
+k8s.io/apimachinery/third_party/forked/golang/netutil
 k8s.io/apimachinery/third_party/forked/golang/reflect
 # k8s.io/cli-runtime v0.21.4
 k8s.io/cli-runtime/pkg/genericclioptions
@@ -1125,9 +1133,12 @@ k8s.io/client-go/tools/pager
 k8s.io/client-go/tools/record
 k8s.io/client-go/tools/record/util
 k8s.io/client-go/tools/reference
+k8s.io/client-go/tools/remotecommand
 k8s.io/client-go/transport
+k8s.io/client-go/transport/spdy
 k8s.io/client-go/util/cert
 k8s.io/client-go/util/connrotation
+k8s.io/client-go/util/exec
 k8s.io/client-go/util/flowcontrol
 k8s.io/client-go/util/homedir
 k8s.io/client-go/util/jsonpath


Test patch:
diff --git a/client_int_test.go b/client_int_test.go
index c6369db0..2497c329 100644
--- a/client_int_test.go
+++ b/client_int_test.go
@@ -161,7 +161,6 @@ func TestRemoteRepositories(t *testing.T) {
 	client := fn.New(
 		fn.WithRegistry(DefaultRegistry),
 		fn.WithRepository("https://github.com/boson-project/test-templates"),
-		fn.WithRepositories("testdata/repositories"),
 	)
 	err := client.Create(fn.Function{
 		Root:     ".",
diff --git a/cmd/build_test.go b/cmd/build_test.go
new file mode 100644
index 00000000..56b733dc
--- /dev/null
+++ b/cmd/build_test.go
@@ -0,0 +1,177 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"testing"
+
+	"github.com/ory/viper"
+	fn "knative.dev/kn-plugin-func"
+	"knative.dev/kn-plugin-func/mock"
+)
+
+// TestBuildInvalidRegistry ensures that running build specifying the name of the
+// registry explicitly as an argument invokes the registry validation code.
+func TestBuildInvalidRegistry(t *testing.T) {
+	var (
+		args    = []string{"--registry", "foo/bar/foobar/boofar"} // provide an invalid registry name
+		builder = mock.NewBuilder()                               // with a mock builder
+	)
+
+	// Run this test in a temporary directory
+	defer fromTempDir(t)()
+	// Write a func.yaml config which does not specify an image
+	funcYaml := `name: testymctestface
+namespace: ""
+runtime: go
+image: ""
+imageDigest: ""
+builder: quay.io/boson/faas-go-builder
+builders:
+  default: quay.io/boson/faas-go-builder
+envs: []
+annotations: {}
+labels: []
+created: 2021-01-01T00:00:00+00:00
+`
+	if err := ioutil.WriteFile("func.yaml", []byte(funcYaml), 0600); err != nil {
+		t.Fatal(err)
+	}
+
+	// Create a command with a client constructor fn that instantiates a client
+	// with a the mocked builder.
+	cmd := NewBuildCmd(func(_ buildConfig) (*fn.Client, error) {
+		return fn.New(fn.WithBuilder(builder)), nil
+	})
+
+	// Execute the command
+	cmd.SetArgs(args)
+	err := cmd.Execute()
+	if err == nil {
+		t.Fatal("Expected error")
+	}
+}
+
+func Test_runBuild(t *testing.T) {
+	tests := []struct {
+		name         string
+		pushFlag     bool
+		fileContents string
+		shouldBuild  bool
+		shouldPush   bool
+		wantErr      bool
+	}{
+		{
+			name:     "push flag triggers push after build",
+			pushFlag: true,
+			fileContents: `name: test-func
+runtime: go
+created: 2009-11-10 23:00:00`,
+			shouldBuild: true,
+			shouldPush:  true,
+		},
+		{
+			name:     "push flag with failing push",
+			pushFlag: true,
+			fileContents: `name: test-func
+runtime: go
+created: 2009-11-10 23:00:00`,
+			shouldBuild: true,
+			shouldPush:  true,
+			wantErr:     true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockPusher := mock.NewPusher()
+			failPusher := &mock.Pusher{
+				PushFn: func(f fn.Function) (string, error) {
+					return "", fmt.Errorf("push failed")
+				},
+			}
+			mockBuilder := mock.NewBuilder()
+			cmd := NewBuildCmd(func(bc buildConfig) (*fn.Client, error) {
+				pusher := mockPusher
+				if tt.wantErr {
+					pusher = failPusher
+				}
+				return fn.New(
+					fn.WithBuilder(mockBuilder),
+					fn.WithPusher(pusher),
+				), nil
+			})
+
+			tempDir, err := os.MkdirTemp("", "func-tests")
+			if err != nil {
+				t.Fatalf("temp dir couldn't be created %v", err)
+			}
+			t.Log("tempDir created:", tempDir)
+			t.Cleanup(func() {
+				os.RemoveAll(tempDir)
+			})
+
+			fullPath := tempDir + "/func.yaml"
+			tempFile, err := os.Create(fullPath)
+			if err != nil {
+				t.Fatalf("temp file couldn't be created %v", err)
+			}
+			_, err = tempFile.WriteString(tt.fileContents)
+			if err != nil {
+				t.Fatalf("file content was not written %v", err)
+			}
+
+			cmd.SetArgs([]string{"--path=" + tempDir})
+			viper.SetDefault("push", tt.pushFlag)
+			viper.SetDefault("registry", "docker.io/tigerteam")
+
+			err = cmd.Execute()
+			if tt.wantErr != (err != nil) {
+				t.Errorf("Wanted error %v but actually got %v", tt.wantErr, err)
+			}
+
+			if mockBuilder.BuildInvoked != tt.shouldBuild {
+				t.Errorf("Build execution expected: %v but was actually %v", tt.shouldBuild, mockBuilder.BuildInvoked)
+			}
+
+			if tt.shouldPush != (mockPusher.PushInvoked || failPusher.PushInvoked) {
+				t.Errorf("Push execution expected: %v but was actually mockPusher invoked: %v failPusher invoked %v", tt.shouldPush, mockPusher.PushInvoked, failPusher.PushInvoked)
+			}
+		})
+	}
+}
+
+func Test_newBuildClient(t *testing.T) {
+	tests := []struct {
+		name    string
+		cfg     buildConfig
+		succeed bool
+	}{
+		{
+			name: "push flag set to false avoids pusher instanciation",
+			cfg: buildConfig{
+				Push: false,
+			},
+			succeed: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client, err := newBuildClient(tt.cfg)
+			if err != nil {
+				t.Error(err)
+			}
+			defer func() {
+				if r := recover(); r != nil && tt.succeed {
+					t.Errorf("expected function call to succeed %v, got actually %v", tt.succeed, r)
+				}
+			}()
+			err = client.Push(context.TODO(), &fn.Function{})
+			if err != nil {
+				t.Error(err)
+			}
+		})
+	}
+}
diff --git a/k8s/dialer_test.go b/k8s/dialer_test.go
new file mode 100644
index 00000000..17e30923
--- /dev/null
+++ b/k8s/dialer_test.go
@@ -0,0 +1,175 @@
+//go:build integration
+// +build integration
+
+package k8s_test
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"regexp"
+	"strings"
+	"sync"
+	"testing"
+
+	"knative.dev/kn-plugin-func/k8s"
+
+	coreV1 "k8s.io/api/core/v1"
+	metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+)
+
+func TestDialInClusterService(t *testing.T) {
+	var err error
+	var ctx = context.Background()
+
+	cliSet, err := k8s.NewKubernetesClientset("")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	creatOpts := metaV1.CreateOptions{}
+	deleteOpts := metaV1.DeleteOptions{}
+
+	testingNS := &coreV1.Namespace{
+		ObjectMeta: metaV1.ObjectMeta{
+			Name: "dialer-test-ns",
+		},
+		Spec: coreV1.NamespaceSpec{},
+	}
+
+	_, err = cliSet.CoreV1().Namespaces().Create(ctx, testingNS, creatOpts)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer cliSet.CoreV1().Namespaces().Delete(ctx, testingNS.Name, deleteOpts)
+	t.Log("created namespace: ", testingNS.Name)
+
+	nginxPod := &coreV1.Pod{
+		ObjectMeta: metaV1.ObjectMeta{
+			Name:        "dialer-test-pod",
+			Labels:      map[string]string{"app": "dialer-test-app"},
+			Annotations: nil,
+		},
+		Spec: coreV1.PodSpec{
+			Containers: []coreV1.Container{
+				{
+					Name:  "dialer-testing-nginx",
+					Image: "nginx",
+				},
+			},
+			DNSPolicy:     coreV1.DNSClusterFirst,
+			RestartPolicy: coreV1.RestartPolicyNever,
+		},
+	}
+
+	_, err = cliSet.CoreV1().Pods(testingNS.Name).Create(ctx, nginxPod, creatOpts)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer cliSet.CoreV1().Pods(testingNS.Name).Delete(ctx, nginxPod.Name, deleteOpts)
+	t.Log("created pod: ", nginxPod.Name)
+
+	nginxService := &coreV1.Service{
+		ObjectMeta: metaV1.ObjectMeta{
+			Name: "dialer-test-service",
+		},
+		Spec: coreV1.ServiceSpec{
+			Ports: []coreV1.ServicePort{
+				{
+					Name:       "http",
+					Protocol:   coreV1.ProtocolTCP,
+					Port:       80,
+					TargetPort: intstr.FromInt(80),
+				},
+			},
+			Selector: map[string]string{
+				"app": "dialer-test-app",
+			},
+		},
+	}
+
+	nginxService, err = cliSet.CoreV1().Services(testingNS.Name).Create(ctx, nginxService, creatOpts)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer cliSet.CoreV1().Services(testingNS.Name).Delete(ctx, nginxService.Name, deleteOpts)
+	t.Log("created svc: ", nginxService.Name)
+
+	dialer, err := k8s.NewInClusterDialer(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer dialer.Close()
+
+	transport := &http.Transport{
+		DialContext: dialer.DialContext,
+	}
+
+	var client = http.Client{
+		Transport: transport,
+	}
+
+	svcInClusterURL := fmt.Sprintf("http://%s.%s.svc", nginxService.Name, nginxService.Namespace)
+	resp, err := client.Get(svcInClusterURL)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer resp.Body.Close()
+
+	runeReader := bufio.NewReader(resp.Body)
+	matched, err := regexp.MatchReader("Welcome to nginx!", runeReader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !matched {
+		t.Error("body doesn't contain 'Welcome to nginx!' substring")
+	}
+	if resp.StatusCode != 200 {
+		t.Errorf("unexpected status code: %d", resp.StatusCode)
+	}
+
+	var wg sync.WaitGroup
+	for i := 0; i < 10; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			resp, err := client.Get(svcInClusterURL)
+			if err != nil {
+				t.Fatal(err)
+			}
+			defer resp.Body.Close()
+			io.Copy(io.Discard, resp.Body)
+		}()
+	}
+	wg.Wait()
+}
+
+func TestDialUnreachable(t *testing.T) {
+	var ctx = context.Background()
+
+	dialer, err := k8s.NewInClusterDialer(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer dialer.Close()
+
+	transport := &http.Transport{
+		DialContext: dialer.DialContext,
+	}
+
+	var client = http.Client{
+		Transport: transport,
+	}
+
+	_, err = client.Get("http://does-not.exists.svc")
+	if err == nil {
+		t.Error("error was expected but got nil")
+		return
+	}
+	if !strings.Contains(err.Error(), "Name does not resolve") {
+		t.Errorf("error %q doesn't containe expected sub-string: ", err.Error())
+	}
+}
diff --git a/templates_test.go b/templates_test.go
index 2e97cf8e..bd0ee3f5 100644
--- a/templates_test.go
+++ b/templates_test.go
@@ -516,3 +516,38 @@ func TestRepositoryManifestBuildEnvs(t *testing.T) {
 		t.Fatalf("Unexpected difference between repository's manifest.yaml buildEnvs and Function BuildEnvs (-want, +got): %v", diff)
 	}
 }
+
+// TestTemplateManifestRemoved ensures that the manifest is not left in
+// the resultant Function after write.
+func TestTemplateManifestRemoved(t *testing.T) {
+	// create test directory
+	root := "testdata/testTemplateManifestRemoved"
+	defer Using(t, root)()
+
+	// Client whose internal templates will be used.
+	client := fn.New(
+		fn.WithRegistry(TestRegistry),
+		fn.WithRepositories("testdata/repositories"))
+
+	// write out a template
+	err := client.Create(fn.Function{
+		Root:     root,
+		Runtime:  "manifestedRuntime",
+		Template: "customLanguagePackRepo/manifestedTemplate",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Assert func.yaml exists as expected
+	_, err = os.Stat(filepath.Join(root, "func.yaml"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Assert manifest.yaml does not
+	_, err = os.Stat(filepath.Join(root, "manifest.yaml"))
+	if err == nil {
+		t.Fatal("manifest.yaml should not exist after write")
+	}
+
+}