mirror of https://github.com/kubernetes/kops.git
				
				
				
			
		
			
				
	
	
		
			356 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			356 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
/*
 | 
						|
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 assets
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"net/url"
 | 
						|
	"os"
 | 
						|
	"path"
 | 
						|
	"regexp"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/blang/semver/v4"
 | 
						|
	"k8s.io/apimachinery/pkg/util/wait"
 | 
						|
	"k8s.io/klog/v2"
 | 
						|
	"k8s.io/kops/pkg/apis/kops"
 | 
						|
	"k8s.io/kops/pkg/apis/kops/util"
 | 
						|
	"k8s.io/kops/pkg/kubemanifest"
 | 
						|
	"k8s.io/kops/pkg/values"
 | 
						|
	"k8s.io/kops/util/pkg/hashing"
 | 
						|
	"k8s.io/kops/util/pkg/mirrors"
 | 
						|
	"k8s.io/kops/util/pkg/vfs"
 | 
						|
)
 | 
						|
 | 
						|
// AssetBuilder discovers and remaps assets.
 | 
						|
type AssetBuilder struct {
 | 
						|
	ContainerAssets []*ContainerAsset
 | 
						|
	FileAssets      []*FileAsset
 | 
						|
	AssetsLocation  *kops.Assets
 | 
						|
	// TODO we'd like to use cloudup.Phase here, but that introduces a go cyclic dependency
 | 
						|
	Phase string
 | 
						|
 | 
						|
	// KubernetesVersion is the version of kubernetes we are installing
 | 
						|
	KubernetesVersion semver.Version
 | 
						|
 | 
						|
	// StaticManifests records static manifests
 | 
						|
	StaticManifests []*StaticManifest
 | 
						|
}
 | 
						|
 | 
						|
type StaticManifest struct {
 | 
						|
	// Key is the unique identifier of the manifest
 | 
						|
	Key string
 | 
						|
 | 
						|
	// Path is the path to the manifest
 | 
						|
	Path string
 | 
						|
 | 
						|
	// The static manifest will only be applied to instances matching the specified role
 | 
						|
	Roles []kops.InstanceGroupRole
 | 
						|
}
 | 
						|
 | 
						|
// ContainerAsset models a container's location.
 | 
						|
type ContainerAsset struct {
 | 
						|
	// DockerImage will be the name of the container we should run.
 | 
						|
	// This is used to copy a container to a ContainerRegistry.
 | 
						|
	DockerImage string
 | 
						|
	// CanonicalLocation will be the source location of the container.
 | 
						|
	CanonicalLocation string
 | 
						|
}
 | 
						|
 | 
						|
// FileAsset models a file's location.
 | 
						|
type FileAsset struct {
 | 
						|
	// DownloadURL is the URL from which the cluster should download the asset.
 | 
						|
	DownloadURL *url.URL
 | 
						|
	// CanonicalURL is the canonical location of the asset, for example as distributed by the kops project
 | 
						|
	CanonicalURL *url.URL
 | 
						|
	// SHAValue is the SHA hash of the FileAsset.
 | 
						|
	SHAValue string
 | 
						|
}
 | 
						|
 | 
						|
// NewAssetBuilder creates a new AssetBuilder.
 | 
						|
func NewAssetBuilder(cluster *kops.Cluster, phase string) *AssetBuilder {
 | 
						|
	a := &AssetBuilder{
 | 
						|
		AssetsLocation: cluster.Spec.Assets,
 | 
						|
		Phase:          phase,
 | 
						|
	}
 | 
						|
 | 
						|
	version, err := util.ParseKubernetesVersion(cluster.Spec.KubernetesVersion)
 | 
						|
	if err != nil {
 | 
						|
		// This should have already been validated
 | 
						|
		klog.Fatalf("unexpected error from ParseKubernetesVersion %s: %v", cluster.Spec.KubernetesVersion, err)
 | 
						|
	}
 | 
						|
	a.KubernetesVersion = *version
 | 
						|
 | 
						|
	return a
 | 
						|
}
 | 
						|
 | 
						|
// RemapManifest transforms a kubernetes manifest.
 | 
						|
// Whenever we are building a Task that includes a manifest, we should pass it through RemapManifest first.
 | 
						|
// This will:
 | 
						|
// * rewrite the images if they are being redirected to a mirror, and ensure the image is uploaded
 | 
						|
func (a *AssetBuilder) RemapManifest(data []byte) ([]byte, error) {
 | 
						|
	objects, err := kubemanifest.LoadObjectsFrom(data)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	for _, object := range objects {
 | 
						|
		if err := object.RemapImages(a.RemapImage); err != nil {
 | 
						|
			return nil, fmt.Errorf("error remapping images: %v", err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return objects.ToYAML()
 | 
						|
}
 | 
						|
 | 
						|
// RemapImage normalizes a containers location if a user sets the AssetsLocation ContainerRegistry location.
 | 
						|
func (a *AssetBuilder) RemapImage(image string) (string, error) {
 | 
						|
	asset := &ContainerAsset{}
 | 
						|
 | 
						|
	asset.DockerImage = image
 | 
						|
 | 
						|
	if strings.HasPrefix(image, "k8s.gcr.io/kops/dns-controller:") {
 | 
						|
		// To use user-defined DNS Controller:
 | 
						|
		// 1. DOCKER_REGISTRY=[your docker hub repo] make dns-controller-push
 | 
						|
		// 2. export DNSCONTROLLER_IMAGE=[your docker hub repo]
 | 
						|
		// 3. make kops and create/apply cluster
 | 
						|
		override := os.Getenv("DNSCONTROLLER_IMAGE")
 | 
						|
		if override != "" {
 | 
						|
			image = override
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if strings.HasPrefix(image, "k8s.gcr.io/kops/kops-controller:") {
 | 
						|
		// To use user-defined DNS Controller:
 | 
						|
		// 1. DOCKER_REGISTRY=[your docker hub repo] make kops-controller-push
 | 
						|
		// 2. export KOPSCONTROLLER_IMAGE=[your docker hub repo]
 | 
						|
		// 3. make kops and create/apply cluster
 | 
						|
		override := os.Getenv("KOPSCONTROLLER_IMAGE")
 | 
						|
		if override != "" {
 | 
						|
			image = override
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if strings.HasPrefix(image, "k8s.gcr.io/kops/kube-apiserver-healthcheck:") {
 | 
						|
		override := os.Getenv("KUBE_APISERVER_HEALTHCHECK_IMAGE")
 | 
						|
		if override != "" {
 | 
						|
			image = override
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if a.AssetsLocation != nil && a.AssetsLocation.ContainerProxy != nil {
 | 
						|
		containerProxy := strings.TrimSuffix(*a.AssetsLocation.ContainerProxy, "/")
 | 
						|
		normalized := image
 | 
						|
 | 
						|
		// If the image name contains only a single / we need to determine if the image is located on docker-hub or if it's using a convenient URL like k8s.gcr.io/<image-name>
 | 
						|
		// In case of a hub image it should be sufficient to just prepend the proxy url, producing eg docker-proxy.example.com/weaveworks/weave-kube
 | 
						|
		if strings.Count(normalized, "/") <= 1 && !strings.ContainsAny(strings.Split(normalized, "/")[0], ".:") {
 | 
						|
			normalized = containerProxy + "/" + normalized
 | 
						|
		} else {
 | 
						|
			var re = regexp.MustCompile(`^[^/]+`)
 | 
						|
			normalized = re.ReplaceAllString(normalized, containerProxy)
 | 
						|
		}
 | 
						|
 | 
						|
		asset.DockerImage = normalized
 | 
						|
		asset.CanonicalLocation = image
 | 
						|
 | 
						|
		// Run the new image
 | 
						|
		image = asset.DockerImage
 | 
						|
	}
 | 
						|
 | 
						|
	if a.AssetsLocation != nil && a.AssetsLocation.ContainerRegistry != nil {
 | 
						|
		registryMirror := *a.AssetsLocation.ContainerRegistry
 | 
						|
		normalized := image
 | 
						|
 | 
						|
		// Remove the 'standard' kubernetes image prefix, just for sanity
 | 
						|
		normalized = strings.TrimPrefix(normalized, "k8s.gcr.io/")
 | 
						|
 | 
						|
		// When assembling the cluster spec, kops may call the option more then once until the config converges
 | 
						|
		// This means that this function may me called more than once on the same image
 | 
						|
		// It this is pass is the second one, the image will already have been normalized with the containerRegistry settings
 | 
						|
		// If this is the case, passing though the process again will re-prepend the container registry again
 | 
						|
		// and again, causing the spec to never converge and the config build to fail.
 | 
						|
		if !strings.HasPrefix(normalized, registryMirror+"/") {
 | 
						|
			// We can't nest arbitrarily
 | 
						|
			// Some risk of collisions, but also -- and __ in the names appear to be blocked by docker hub
 | 
						|
			normalized = strings.Replace(normalized, "/", "-", -1)
 | 
						|
			asset.DockerImage = registryMirror + "/" + normalized
 | 
						|
		}
 | 
						|
 | 
						|
		asset.CanonicalLocation = image
 | 
						|
 | 
						|
		// Run the new image
 | 
						|
		image = asset.DockerImage
 | 
						|
	}
 | 
						|
 | 
						|
	a.ContainerAssets = append(a.ContainerAssets, asset)
 | 
						|
	return image, nil
 | 
						|
}
 | 
						|
 | 
						|
// RemapFileAndSHA returns a remapped URL for the file, if AssetsLocation is defined.
 | 
						|
// It also returns the SHA hash of the file.
 | 
						|
func (a *AssetBuilder) RemapFileAndSHA(fileURL *url.URL) (*url.URL, *hashing.Hash, error) {
 | 
						|
	if fileURL == nil {
 | 
						|
		return nil, nil, fmt.Errorf("unable to remap a nil URL")
 | 
						|
	}
 | 
						|
 | 
						|
	fileAsset := &FileAsset{
 | 
						|
		DownloadURL: fileURL,
 | 
						|
	}
 | 
						|
 | 
						|
	if a.AssetsLocation != nil && a.AssetsLocation.FileRepository != nil {
 | 
						|
		fileAsset.CanonicalURL = fileURL
 | 
						|
 | 
						|
		normalizedFileURL, err := a.remapURL(fileURL)
 | 
						|
		if err != nil {
 | 
						|
			return nil, nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		fileAsset.DownloadURL = normalizedFileURL
 | 
						|
 | 
						|
		klog.V(4).Infof("adding remapped file: %+v", fileAsset)
 | 
						|
	}
 | 
						|
 | 
						|
	h, err := a.findHash(fileAsset)
 | 
						|
	if err != nil {
 | 
						|
		return nil, nil, err
 | 
						|
	}
 | 
						|
	fileAsset.SHAValue = h.Hex()
 | 
						|
 | 
						|
	klog.V(8).Infof("adding file: %+v", fileAsset)
 | 
						|
	a.FileAssets = append(a.FileAssets, fileAsset)
 | 
						|
 | 
						|
	return fileAsset.DownloadURL, h, nil
 | 
						|
}
 | 
						|
 | 
						|
// RemapFileAndSHAValue returns a remapped URL for the file without a SHA file in object storage, if AssetsLocation is defined.
 | 
						|
func (a *AssetBuilder) RemapFileAndSHAValue(fileURL *url.URL, shaValue string) (*url.URL, error) {
 | 
						|
	if fileURL == nil {
 | 
						|
		return nil, fmt.Errorf("unable to remap a nil URL")
 | 
						|
	}
 | 
						|
 | 
						|
	fileAsset := &FileAsset{
 | 
						|
		DownloadURL: fileURL,
 | 
						|
		SHAValue:    shaValue,
 | 
						|
	}
 | 
						|
 | 
						|
	if a.AssetsLocation != nil && a.AssetsLocation.FileRepository != nil {
 | 
						|
		fileAsset.CanonicalURL = fileURL
 | 
						|
 | 
						|
		normalizedFile, err := a.remapURL(fileURL)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		fileAsset.DownloadURL = normalizedFile
 | 
						|
		klog.V(4).Infof("adding remapped file: %q", fileAsset.DownloadURL.String())
 | 
						|
	}
 | 
						|
 | 
						|
	klog.V(8).Infof("adding file: %+v", fileAsset)
 | 
						|
	a.FileAssets = append(a.FileAssets, fileAsset)
 | 
						|
 | 
						|
	return fileAsset.DownloadURL, nil
 | 
						|
}
 | 
						|
 | 
						|
// FindHash returns the hash value of a FileAsset.
 | 
						|
func (a *AssetBuilder) findHash(file *FileAsset) (*hashing.Hash, error) {
 | 
						|
 | 
						|
	// If the phase is "assets" we use the CanonicalFileURL,
 | 
						|
	// but during other phases we use the hash from the FileRepository or the base kops path.
 | 
						|
	// We do not want to just test for CanonicalFileURL as it is defined in
 | 
						|
	// other phases, but is not used to test for the SHA.
 | 
						|
	// This prevents a chicken and egg problem where the file is not yet in the FileRepository.
 | 
						|
	//
 | 
						|
	// assets phase -> get the sha file from the source / CanonicalFileURL
 | 
						|
	// any other phase -> get the sha file from the kops base location or the FileRepository
 | 
						|
	//
 | 
						|
	// TLDR; we use the file.CanonicalFileURL during assets phase, and use file.FileUrl the
 | 
						|
	// rest of the time. If not we get a chicken and the egg problem where we are reading the sha file
 | 
						|
	// before it exists.
 | 
						|
	u := file.DownloadURL
 | 
						|
	if a.Phase == "assets" && file.CanonicalURL != nil {
 | 
						|
		u = file.CanonicalURL
 | 
						|
	}
 | 
						|
 | 
						|
	if u == nil {
 | 
						|
		return nil, fmt.Errorf("file url is not defined")
 | 
						|
	}
 | 
						|
 | 
						|
	// We now prefer sha256 hashes
 | 
						|
	for backoffSteps := 1; backoffSteps <= 3; backoffSteps++ {
 | 
						|
		// We try first with a short backoff, so we don't
 | 
						|
		// waste too much time looking for files that don't
 | 
						|
		// exist before trying the next one
 | 
						|
		backoff := wait.Backoff{
 | 
						|
			Duration: 500 * time.Millisecond,
 | 
						|
			Factor:   2,
 | 
						|
			Steps:    backoffSteps,
 | 
						|
		}
 | 
						|
 | 
						|
		for _, ext := range []string{".sha256", ".sha1"} {
 | 
						|
			for _, mirror := range mirrors.FindUrlMirrors(u.String()) {
 | 
						|
				hashURL := mirror + ext
 | 
						|
				klog.V(3).Infof("Trying to read hash fie: %q", hashURL)
 | 
						|
				b, err := vfs.Context.ReadFile(hashURL, vfs.WithBackoff(backoff))
 | 
						|
				if err != nil {
 | 
						|
					// Try to log without being too alarming - issue #7550
 | 
						|
					klog.V(2).Infof("Unable to read hash file %q: %v", hashURL, err)
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				hashString := strings.TrimSpace(string(b))
 | 
						|
				klog.V(2).Infof("Found hash %q for %q", hashString, u)
 | 
						|
 | 
						|
				// Accept a hash string that is `<hash> <filename>`
 | 
						|
				fields := strings.Fields(hashString)
 | 
						|
				if len(fields) == 0 {
 | 
						|
					klog.Infof("Hash file was empty %q", hashURL)
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				return hashing.FromString(fields[0])
 | 
						|
			}
 | 
						|
			if ext == ".sha256" {
 | 
						|
				klog.V(2).Infof("Unable to read new sha256 hash file (is this an older/unsupported kubernetes release?)")
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if a.AssetsLocation != nil && a.AssetsLocation.FileRepository != nil {
 | 
						|
		return nil, fmt.Errorf("you may have not staged your files correctly, please execute kops update cluster using the assets phase")
 | 
						|
	}
 | 
						|
	return nil, fmt.Errorf("cannot determine hash for %q (have you specified a valid file location?)", u)
 | 
						|
}
 | 
						|
 | 
						|
func (a *AssetBuilder) remapURL(canonicalURL *url.URL) (*url.URL, error) {
 | 
						|
	f := ""
 | 
						|
	if a.AssetsLocation != nil {
 | 
						|
		f = values.StringValue(a.AssetsLocation.FileRepository)
 | 
						|
	}
 | 
						|
	if f == "" {
 | 
						|
		return nil, fmt.Errorf("assetsLocation.fileRepository must be set to remap asset %v", canonicalURL)
 | 
						|
	}
 | 
						|
 | 
						|
	fileRepo, err := url.Parse(f)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("unable to parse assetsLocation.fileRepository %q: %v", f, err)
 | 
						|
	}
 | 
						|
 | 
						|
	fileRepo.Path = path.Join(fileRepo.Path, canonicalURL.Path)
 | 
						|
 | 
						|
	return fileRepo, nil
 | 
						|
}
 |