mirror of
https://github.com/GoogleContainerTools/kaniko
synced 2026-09-30 19:23:45 +02:00
* Add mTLS (client cert) support Add support for Mutual TLS (mTLS) client certificate authentication. The expected format of the new --registry-client-cert flag is the same as the existing --registry-certificate flag, which will allow different client certificates for different registries: --registry-client-cert my.registry.url=/path/to/cert.crt,/path/to/key.key * tidy: Rename mTLS (Client Cert) flag to be in line with others This flag didn't describe that it was for the client certs uses with the registry. Although this should be reasonably obvious, I like the consistency with the other registry flag. * test: Added unit tests for mTLS (Client Cert) loading * test: Add 2 more tests for comma split formatting since the comma splitting is a new portion of code let's make sure that that format works well too in other cases * tidy: Fix formatting of flag help text * tidy: Made invalid cert format error consistent I was running the tests and saw the message: Failed to load client certificate/key '/path/to/client/certificate.cert' for my.registry.name, format is my.registry.name=/path/to/cert,/path/to/key I then realized that it'd be a lot nicer if this showed the user what they input, and how they should change it (rather than decomposing it: Failed to load client certificate/key 'my.registry.name=/path/to/client/certificate.cert', expected format: my.registry.name=/path/to/cert,/path/to/key * test: Fixed incorrect test argument This didn't fail the test before because it's only attempting to show that certs only get loaded and used for their associated registry but it's important to keep this correct. This case is covered by the test below, "RegistriesClientCertificates incorrect cert format" * doc: Add new flag to README.md * mod: Fail to push if there was a problem loading client certs Rather than warning that there was an issue, we should fail if the requested client certificates were not found or failed to load. This feels a lot better than waiting for the build to finish then failing later. * mod: Return an error if the certificate authority fails to load, just like client certs The MakeTransport function was changed in the previous commit to allow returning errors if there was a problem loading certificates, rather than just print warnings. This feels a lot better as you get the error immediately that there's a problem to fix, rather than getting a warning, then later an error that the server's certificate could not be verified. * tidy: fix golint issues
257 lines
7.1 KiB
Go
257 lines
7.1 KiB
Go
/*
|
|
Copyright 2018 Google LLC
|
|
|
|
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 cache
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/GoogleContainerTools/kaniko/pkg/config"
|
|
"github.com/GoogleContainerTools/kaniko/pkg/creds"
|
|
"github.com/GoogleContainerTools/kaniko/pkg/util"
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"github.com/google/go-containerregistry/pkg/v1/layout"
|
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
|
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// LayerCache is the layer cache
|
|
type LayerCache interface {
|
|
RetrieveLayer(string) (v1.Image, error)
|
|
}
|
|
|
|
// RegistryCache is the registry cache
|
|
type RegistryCache struct {
|
|
Opts *config.KanikoOptions
|
|
}
|
|
|
|
// RetrieveLayer retrieves a layer from the cache given the cache key ck.
|
|
func (rc *RegistryCache) RetrieveLayer(ck string) (v1.Image, error) {
|
|
cache, err := Destination(rc.Opts, ck)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting cache destination")
|
|
}
|
|
logrus.Infof("Checking for cached layer %s...", cache)
|
|
|
|
cacheRef, err := name.NewTag(cache, name.WeakValidation)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, fmt.Sprintf("getting reference for %s", cache))
|
|
}
|
|
|
|
registryName := cacheRef.Repository.Registry.Name()
|
|
if rc.Opts.Insecure || rc.Opts.InsecureRegistries.Contains(registryName) {
|
|
newReg, err := name.NewRegistry(registryName, name.WeakValidation, name.Insecure)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cacheRef.Repository.Registry = newReg
|
|
}
|
|
|
|
tr, err := util.MakeTransport(rc.Opts.RegistryOptions, registryName)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "making transport for registry %q", registryName)
|
|
}
|
|
|
|
img, err := remote.Image(cacheRef, remote.WithTransport(tr), remote.WithAuthFromKeychain(creds.GetKeychain()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = verifyImage(img, rc.Opts.CacheTTL, cache); err != nil {
|
|
return nil, err
|
|
}
|
|
return img, nil
|
|
}
|
|
|
|
func verifyImage(img v1.Image, cacheTTL time.Duration, cache string) error {
|
|
cf, err := img.ConfigFile()
|
|
if err != nil {
|
|
return errors.Wrap(err, fmt.Sprintf("retrieving config file for %s", cache))
|
|
}
|
|
|
|
expiry := cf.Created.Add(cacheTTL)
|
|
// Layer is stale, rebuild it.
|
|
if expiry.Before(time.Now()) {
|
|
logrus.Infof("Cache entry expired: %s", cache)
|
|
return fmt.Errorf("Cache entry expired: %s", cache)
|
|
}
|
|
|
|
// Force the manifest to be populated
|
|
if _, err := img.RawManifest(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LayoutCache is the OCI image layout cache
|
|
type LayoutCache struct {
|
|
Opts *config.KanikoOptions
|
|
}
|
|
|
|
func (lc *LayoutCache) RetrieveLayer(ck string) (v1.Image, error) {
|
|
cache, err := Destination(lc.Opts, ck)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting cache destination")
|
|
}
|
|
logrus.Infof("Checking for cached layer %s...", cache)
|
|
|
|
var img v1.Image
|
|
if img, err = locateImage(strings.TrimPrefix(cache, "oci:")); err != nil {
|
|
return nil, errors.Wrap(err, "locating cache image")
|
|
}
|
|
|
|
if err = verifyImage(img, lc.Opts.CacheTTL, cache); err != nil {
|
|
return nil, err
|
|
}
|
|
return img, nil
|
|
}
|
|
|
|
func locateImage(path string) (v1.Image, error) {
|
|
var img v1.Image
|
|
layoutPath, err := layout.FromPath(path)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, fmt.Sprintf("constructing layout path from %s", path))
|
|
}
|
|
index, err := layoutPath.ImageIndex()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, fmt.Sprintf("retrieving index file for %s", layoutPath))
|
|
}
|
|
manifest, err := index.IndexManifest()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, fmt.Sprintf("retrieving manifest file for %s", layoutPath))
|
|
}
|
|
for _, m := range manifest.Manifests {
|
|
// assume there is only one image
|
|
img, err = layoutPath.Image(m.Digest)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, fmt.Sprintf("initializing image with digest %s", m.Digest.String()))
|
|
}
|
|
}
|
|
if img == nil {
|
|
return nil, fmt.Errorf("path contains no images")
|
|
}
|
|
return img, nil
|
|
}
|
|
|
|
// Destination returns the repo where the layer should be stored
|
|
// If no cache is specified, one is inferred from the destination provided
|
|
func Destination(opts *config.KanikoOptions, cacheKey string) (string, error) {
|
|
cache := opts.CacheRepo
|
|
if cache == "" {
|
|
destination := opts.Destinations[0]
|
|
destRef, err := name.NewTag(destination, name.WeakValidation)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "getting tag for destination")
|
|
}
|
|
return fmt.Sprintf("%s/cache:%s", destRef.Context(), cacheKey), nil
|
|
}
|
|
return fmt.Sprintf("%s:%s", cache, cacheKey), nil
|
|
}
|
|
|
|
// LocalSource retrieves a source image from a local cache given cacheKey
|
|
func LocalSource(opts *config.CacheOptions, cacheKey string) (v1.Image, error) {
|
|
cache := opts.CacheDir
|
|
if cache == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
path := path.Join(cache, cacheKey)
|
|
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("No file found for cache key %v %v", cacheKey, err)
|
|
logrus.Debug(msg)
|
|
return nil, NotFoundErr{msg: msg}
|
|
}
|
|
|
|
// A stale cache is a bad cache
|
|
expiry := fi.ModTime().Add(opts.CacheTTL)
|
|
if expiry.Before(time.Now()) {
|
|
msg := fmt.Sprintf("Cached image is too old: %v", fi.ModTime())
|
|
logrus.Debug(msg)
|
|
return nil, ExpiredErr{msg: msg}
|
|
}
|
|
|
|
logrus.Infof("Found %s in local cache", cacheKey)
|
|
return cachedImageFromPath(path)
|
|
}
|
|
|
|
// cachedImage represents a v1.Tarball that is cached locally in a CAS.
|
|
// Computing the digest for a v1.Tarball is very expensive. If the tarball
|
|
// is named with the digest we can store this and return it directly rather
|
|
// than recompute it.
|
|
type cachedImage struct {
|
|
digest string
|
|
v1.Image
|
|
mfst *v1.Manifest
|
|
}
|
|
|
|
func (c *cachedImage) Digest() (v1.Hash, error) {
|
|
return v1.NewHash(c.digest)
|
|
}
|
|
|
|
func (c *cachedImage) Manifest() (*v1.Manifest, error) {
|
|
if c.mfst == nil {
|
|
return c.Image.Manifest()
|
|
}
|
|
return c.mfst, nil
|
|
}
|
|
|
|
func mfstFromPath(p string) (*v1.Manifest, error) {
|
|
f, err := os.Open(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
return v1.ParseManifest(f)
|
|
}
|
|
|
|
func cachedImageFromPath(p string) (v1.Image, error) {
|
|
imgTar, err := tarball.ImageFromPath(p, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting image from path")
|
|
}
|
|
|
|
// Manifests may be present next to the tar, named with a ".json" suffix
|
|
mfstPath := p + ".json"
|
|
|
|
var mfst *v1.Manifest
|
|
if _, err := os.Stat(mfstPath); err != nil {
|
|
logrus.Debugf("Manifest does not exist at file: %s", mfstPath)
|
|
} else {
|
|
mfst, err = mfstFromPath(mfstPath)
|
|
if err != nil {
|
|
logrus.Debugf("Error parsing manifest from file: %s", mfstPath)
|
|
} else {
|
|
logrus.Infof("Found manifest at %s", mfstPath)
|
|
}
|
|
}
|
|
|
|
return &cachedImage{
|
|
digest: filepath.Base(p),
|
|
Image: imgTar,
|
|
mfst: mfst,
|
|
}, nil
|
|
}
|