diff --git a/docs/packaging.md b/docs/packaging.md index b7bed87e..6a76ef2a 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -111,6 +111,10 @@ Even though `kbld pkg/unpkg` commands use registry APIs directly, by default the } ``` +### Authenticating to Harbor + +You may have to provide `--registry-ca-cert-path` flag with a path to a CA certificate file for Harbor Registry API. + ### Notes - Produced tarball does not have duplicate image layers, as they are named by their digest (see `tar tvf /tmp/packaged-images.tar`). diff --git a/pkg/kbld/cmd/package.go b/pkg/kbld/cmd/package.go index b0da3324..916390df 100644 --- a/pkg/kbld/cmd/package.go +++ b/pkg/kbld/cmd/package.go @@ -5,10 +5,7 @@ import ( "os" "github.com/cppforlife/go-cli-ui/ui" - regauthn "github.com/google/go-containerregistry/pkg/authn" regname "github.com/google/go-containerregistry/pkg/name" - regv1 "github.com/google/go-containerregistry/pkg/v1" - regremote "github.com/google/go-containerregistry/pkg/v1/remote" cmdcore "github.com/k14s/kbld/pkg/kbld/cmd/core" ctlimg "github.com/k14s/kbld/pkg/kbld/image" regtarball "github.com/k14s/kbld/pkg/kbld/imagetarball" @@ -20,10 +17,13 @@ type PackageOptions struct { ui ui.UI depsFactory cmdcore.DepsFactory - FileFlags FileFlags - OutputPath string + FileFlags FileFlags + RegistryFlags RegistryFlags + OutputPath string } +var _ regtarball.TarDescriptorsMetadata = ctlimg.Registry{} + func NewPackageOptions(ui ui.UI, depsFactory cmdcore.DepsFactory) *PackageOptions { return &PackageOptions{ui: ui, depsFactory: depsFactory} } @@ -36,6 +36,7 @@ func NewPackageCmd(o *PackageOptions, flagsFactory cmdcore.FlagsFactory) *cobra. RunE: func(_ *cobra.Command, _ []string) error { return o.Run() }, } o.FileFlags.Set(cmd) + o.RegistryFlags.Set(cmd) cmd.Flags().StringVarP(&o.OutputPath, "output", "o", "", "Output tarball path") return cmd } @@ -107,30 +108,10 @@ func (o *PackageOptions) exportImages(imgRefsToExport map[string]struct{}, logge defer outputFile.Close() - tds, err := regtarball.NewTarDescriptors(refs, remoteTarDescritorsMetadata{}) + tds, err := regtarball.NewTarDescriptors(refs, ctlimg.NewRegistry(o.RegistryFlags.CACertPaths)) if err != nil { return fmt.Errorf("Collecting packaging metadata: %s", err) } return regtarball.NewTarWriter(tds, outputFile).Write() } - -type remoteTarDescritorsMetadata struct{} - -var _ regtarball.TarDescriptorsMetadata = remoteTarDescritorsMetadata{} - -func (m remoteTarDescritorsMetadata) Generic(ref regname.Reference) (regv1.Descriptor, error) { - desc, err := regremote.Get(ref, regremote.WithAuthFromKeychain(regauthn.DefaultKeychain)) - if err != nil { - return regv1.Descriptor{}, err - } - return desc.Descriptor, nil -} - -func (m remoteTarDescritorsMetadata) Index(ref regname.Reference) (regv1.ImageIndex, error) { - return regremote.Index(ref, regremote.WithAuthFromKeychain(regauthn.DefaultKeychain)) -} - -func (m remoteTarDescritorsMetadata) Image(ref regname.Reference) (regv1.Image, error) { - return regremote.Image(ref, regremote.WithAuthFromKeychain(regauthn.DefaultKeychain)) -} diff --git a/pkg/kbld/cmd/registry_flags.go b/pkg/kbld/cmd/registry_flags.go new file mode 100644 index 00000000..56858002 --- /dev/null +++ b/pkg/kbld/cmd/registry_flags.go @@ -0,0 +1,13 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +type RegistryFlags struct { + CACertPaths []string +} + +func (s *RegistryFlags) Set(cmd *cobra.Command) { + cmd.Flags().StringSliceVar(&s.CACertPaths, "registry-ca-cert-path", nil, "Add CA certificates for registry API (format: /tmp/foo) (can be specified multiple times)") +} diff --git a/pkg/kbld/cmd/resolve.go b/pkg/kbld/cmd/resolve.go index f4423381..1eefab01 100644 --- a/pkg/kbld/cmd/resolve.go +++ b/pkg/kbld/cmd/resolve.go @@ -23,6 +23,7 @@ type ResolveOptions struct { depsFactory cmdcore.DepsFactory FileFlags FileFlags + RegistryFlags RegistryFlags BuildConcurrency int ExportImages string @@ -41,6 +42,7 @@ func NewResolveCmd(o *ResolveOptions, flagsFactory cmdcore.FlagsFactory) *cobra. RunE: func(_ *cobra.Command, _ []string) error { return o.Run() }, } o.FileFlags.Set(cmd) + o.RegistryFlags.Set(cmd) cmd.Flags().IntVar(&o.BuildConcurrency, "build-concurrency", 4, "Set maximum number of concurrent builds") return cmd } @@ -94,7 +96,9 @@ func (o *ResolveOptions) resolveImages( }) } - queue := NewImageBuildQueue(ctlimg.NewFactory(conf, logger)) + registry := ctlimg.NewRegistry(o.RegistryFlags.CACertPaths) + factory := ctlimg.NewFactory(conf, registry, logger) + queue := NewImageBuildQueue(factory) resolvedImages, err := queue.Run(foundImages, o.BuildConcurrency) if err != nil { diff --git a/pkg/kbld/cmd/unpackage.go b/pkg/kbld/cmd/unpackage.go index 18998856..274527fe 100644 --- a/pkg/kbld/cmd/unpackage.go +++ b/pkg/kbld/cmd/unpackage.go @@ -18,9 +18,10 @@ type UnpackageOptions struct { ui ui.UI depsFactory cmdcore.DepsFactory - FileFlags FileFlags - InputPath string - Repository string + FileFlags FileFlags + RegistryFlags RegistryFlags + InputPath string + Repository string } func NewUnpackageOptions(ui ui.UI, depsFactory cmdcore.DepsFactory) *UnpackageOptions { @@ -35,6 +36,7 @@ func NewUnpackageCmd(o *UnpackageOptions, flagsFactory cmdcore.FlagsFactory) *co RunE: func(_ *cobra.Command, _ []string) error { return o.Run() }, } o.FileFlags.Set(cmd) + o.RegistryFlags.Set(cmd) cmd.Flags().StringVarP(&o.InputPath, "input", "i", "", "Input tarball path") cmd.Flags().StringVarP(&o.Repository, "repository", "r", "", "Import images into given image repository (e.g. docker.io/dkalinin/my-project)") return cmd @@ -156,15 +158,17 @@ func (o *UnpackageOptions) importImages(logger *ctlimg.LoggerPrefixWriter) (map[ logger.Write([]byte(fmt.Sprintf("importing %s -> %s...\n", existingRef.Name(), importDigestRef.Name()))) + registry := ctlimg.NewRegistry(o.RegistryFlags.CACertPaths) + switch { case item.Image != nil: - err = ctlimg.ResolvedImage{}.Write(uploadTagRef, *item.Image) + err = registry.WriteImage(uploadTagRef, *item.Image) if err != nil { return nil, fmt.Errorf("Importing image as %s: %s", importDigestRef.Name(), err) } case item.Index != nil: - err = ctlimg.ResolvedImage{}.WriteIndex(uploadTagRef, *item.Index) + err = registry.WriteIndex(uploadTagRef, *item.Index) if err != nil { return nil, fmt.Errorf("Importing image index as %s: %s", importDigestRef.Name(), err) } @@ -189,7 +193,7 @@ func (o *UnpackageOptions) importImages(logger *ctlimg.LoggerPrefixWriter) (map[ } func (o *UnpackageOptions) verifyTagDigest(uploadTagRef regname.Reference, importDigestRef regname.Digest) error { - resultURL, err := ctlimg.NewResolvedImage(uploadTagRef.Name()).URL() + resultURL, err := ctlimg.NewResolvedImage(uploadTagRef.Name(), ctlimg.NewRegistry(o.RegistryFlags.CACertPaths)).URL() if err != nil { return fmt.Errorf("Verifying imported image %s: %s", uploadTagRef.Name(), err) } diff --git a/pkg/kbld/image/factory.go b/pkg/kbld/image/factory.go index 8c27e86a..a851d157 100644 --- a/pkg/kbld/image/factory.go +++ b/pkg/kbld/image/factory.go @@ -9,12 +9,13 @@ type Image interface { } type Factory struct { - conf ctlconf.Conf - logger Logger + conf ctlconf.Conf + registry Registry + logger Logger } -func NewFactory(conf ctlconf.Conf, logger Logger) Factory { - return Factory{conf, logger} +func NewFactory(conf ctlconf.Conf, registry Registry, logger Logger) Factory { + return Factory{conf, registry, logger} } func (f Factory) New(url string) Image { @@ -37,7 +38,7 @@ func (f Factory) New(url string) Image { return digestedImage } - return ResolvedImage{url} + return NewResolvedImage(url, f.registry) } func (f Factory) shouldOverride(url string) (ctlconf.ImageOverride, bool) { diff --git a/pkg/kbld/image/registry.go b/pkg/kbld/image/registry.go new file mode 100644 index 00000000..ab31fd79 --- /dev/null +++ b/pkg/kbld/image/registry.go @@ -0,0 +1,156 @@ +package image + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net" + "net/http" + "time" + + regauthn "github.com/google/go-containerregistry/pkg/authn" + regname "github.com/google/go-containerregistry/pkg/name" + regv1 "github.com/google/go-containerregistry/pkg/v1" + regremote "github.com/google/go-containerregistry/pkg/v1/remote" +) + +type Registry struct { + caCertsPaths []string +} + +func NewRegistry(caCertsPaths []string) Registry { + return Registry{caCertsPaths} +} + +func (i Registry) Generic(ref regname.Reference) (regv1.Descriptor, error) { + opts, err := i.opts() + if err != nil { + return regv1.Descriptor{}, err + } + + desc, err := regremote.Get(ref, opts...) + if err != nil { + return regv1.Descriptor{}, err + } + + return desc.Descriptor, nil +} + +func (i Registry) Image(ref regname.Reference) (regv1.Image, error) { + opts, err := i.opts() + if err != nil { + return nil, err + } + + return regremote.Image(ref, opts...) +} + +func (i Registry) WriteImage(ref regname.Reference, img regv1.Image) error { + httpTran, err := i.newHTTPTransport() + if err != nil { + return err + } + + authz, err := regauthn.DefaultKeychain.Resolve(ref.Context().Registry) + if err != nil { + return fmt.Errorf("Getting authz details: %s", err) + } + + err = i.retry(func() error { return regremote.Write(ref, img, authz, httpTran) }) + if err != nil { + return fmt.Errorf("Writing image: %s", err) + } + + return nil +} + +func (i Registry) Index(ref regname.Reference) (regv1.ImageIndex, error) { + opts, err := i.opts() + if err != nil { + return nil, err + } + + return regremote.Index(ref, opts...) +} + +func (i Registry) WriteIndex(ref regname.Reference, idx regv1.ImageIndex) error { + httpTran, err := i.newHTTPTransport() + if err != nil { + return err + } + + authz, err := regauthn.DefaultKeychain.Resolve(ref.Context().Registry) + if err != nil { + return fmt.Errorf("Getting authz details: %s", err) + } + + err = i.retry(func() error { return regremote.WriteIndex(ref, idx, authz, httpTran) }) + if err != nil { + return fmt.Errorf("Writing image index: %s", err) + } + + return nil +} + +func (i Registry) opts() ([]regremote.ImageOption, error) { + httpTran, err := i.newHTTPTransport() + if err != nil { + return nil, err + } + + return []regremote.ImageOption{ + regremote.WithTransport(httpTran), + regremote.WithAuthFromKeychain(regauthn.DefaultKeychain), + }, nil +} + +func (i Registry) newHTTPTransport() (*http.Transport, error) { + pool, err := x509.SystemCertPool() + if err != nil { + pool = x509.NewCertPool() + } + + if len(i.caCertsPaths) > 0 { + for _, path := range i.caCertsPaths { + if certs, err := ioutil.ReadFile(path); err != nil { + return nil, fmt.Errorf("Reading CA certificates from '%s': %s", path, err) + } else if ok := pool.AppendCertsFromPEM(certs); !ok { + return nil, fmt.Errorf("Adding CA certificates from '%s': failed", path) + } + } + } + + // Copied from https://github.com/golang/go/blob/release-branch.go1.12/src/net/http/transport.go#L42-L53 + // We want to use the DefaultTransport but change its TLSClientConfig. There + // isn't a clean way to do this yet: https://github.com/golang/go/issues/26013 + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + // Use the cert pool with k8s cert bundle appended. + TLSClientConfig: &tls.Config{ + RootCAs: pool, + }, + }, nil +} + +func (i Registry) retry(doFunc func() error) error { + var lastErr error + for i := 0; i < 5; i++ { + lastErr = doFunc() + if lastErr == nil { + return nil + } + time.Sleep(1 * time.Second) + } + return fmt.Errorf("Retried 5 times: %s", lastErr) +} diff --git a/pkg/kbld/image/resolved.go b/pkg/kbld/image/resolved.go index 69b99fda..f8e20f0c 100644 --- a/pkg/kbld/image/resolved.go +++ b/pkg/kbld/image/resolved.go @@ -1,26 +1,17 @@ package image import ( - "crypto/tls" - "crypto/x509" - "fmt" - "net" - "net/http" - "time" - - regauthn "github.com/google/go-containerregistry/pkg/authn" regname "github.com/google/go-containerregistry/pkg/name" - regv1 "github.com/google/go-containerregistry/pkg/v1" - regremote "github.com/google/go-containerregistry/pkg/v1/remote" ) // ResolvedImage respresents an image that will be resolved into url+digest type ResolvedImage struct { - url string + url string + registry Registry } -func NewResolvedImage(url string) ResolvedImage { - return ResolvedImage{url} +func NewResolvedImage(url string, registry Registry) ResolvedImage { + return ResolvedImage{url, registry} } func (i ResolvedImage) URL() (string, error) { @@ -29,104 +20,10 @@ func (i ResolvedImage) URL() (string, error) { return "", err } - httpTran, err := i.newHTTPTransport() - if err != nil { - return "", err - } - - opts := []regremote.ImageOption{ - regremote.WithTransport(httpTran), - regremote.WithAuthFromKeychain(regauthn.DefaultKeychain), - } - - imgDescriptor, err := regremote.Get(tag, opts...) + imgDescriptor, err := i.registry.Generic(tag) if err != nil { return "", err } return NewDigestedImageFromParts(tag.Repository.String(), imgDescriptor.Digest.String()).URL() } - -func (ResolvedImage) newHTTPTransport() (*http.Transport, error) { - pool, err := x509.SystemCertPool() - if err != nil { - pool = x509.NewCertPool() - } - - // if crt, err := ioutil.ReadFile(path); err != nil { - // return nil, err - // } else if ok := pool.AppendCertsFromPEM(crt); !ok { - // return nil, errors.New("failed to append k8s cert bundle to cert pool") - // } - - // Copied from https://github.com/golang/go/blob/release-branch.go1.12/src/net/http/transport.go#L42-L53 - // We want to use the DefaultTransport but change its TLSClientConfig. There - // isn't a clean way to do this yet: https://github.com/golang/go/issues/26013 - return &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ResponseHeaderTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - // Use the cert pool with k8s cert bundle appended. - TLSClientConfig: &tls.Config{ - RootCAs: pool, - }, - }, nil -} - -func (i ResolvedImage) Write(ref regname.Reference, img regv1.Image) error { - httpTran, err := i.newHTTPTransport() - if err != nil { - return err - } - - authz, err := regauthn.DefaultKeychain.Resolve(ref.Context().Registry) - if err != nil { - return fmt.Errorf("Getting authz details: %s", err) - } - - err = i.retry(func() error { return regremote.Write(ref, img, authz, httpTran) }) - if err != nil { - return fmt.Errorf("Writing image: %s", err) - } - - return nil -} - -func (i ResolvedImage) WriteIndex(ref regname.Reference, idx regv1.ImageIndex) error { - httpTran, err := i.newHTTPTransport() - if err != nil { - return err - } - - authz, err := regauthn.DefaultKeychain.Resolve(ref.Context().Registry) - if err != nil { - return fmt.Errorf("Getting authz details: %s", err) - } - - err = i.retry(func() error { return regremote.WriteIndex(ref, idx, authz, httpTran) }) - if err != nil { - return fmt.Errorf("Writing image index: %s", err) - } - - return nil -} - -func (i ResolvedImage) retry(doFunc func() error) error { - var lastErr error - for i := 0; i < 5; i++ { - lastErr = doFunc() - if lastErr == nil { - return nil - } - time.Sleep(1 * time.Second) - } - return fmt.Errorf("Retried 5 times: %s", lastErr) -}