mirror of https://github.com/rancher/rke2.git
459 lines
16 KiB
Go
459 lines
16 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/google/go-containerregistry/pkg/authn"
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
|
helmv1 "github.com/k3s-io/helm-controller/pkg/apis/helm.cattle.io/v1"
|
|
"github.com/k3s-io/k3s/pkg/cli/cmds"
|
|
"github.com/k3s-io/k3s/pkg/daemons/agent"
|
|
daemonconfig "github.com/k3s-io/k3s/pkg/daemons/config"
|
|
"github.com/k3s-io/k3s/pkg/util"
|
|
"github.com/k3s-io/k3s/pkg/version"
|
|
pkgerrors "github.com/pkg/errors"
|
|
"github.com/rancher/rke2/pkg/images"
|
|
"github.com/rancher/wharfie/pkg/credentialprovider/plugin"
|
|
"github.com/rancher/wharfie/pkg/extract"
|
|
"github.com/rancher/wharfie/pkg/registries"
|
|
"github.com/rancher/wharfie/pkg/tarfile"
|
|
"github.com/rancher/wrangler/v3/pkg/merr"
|
|
"github.com/rancher/wrangler/v3/pkg/yaml"
|
|
"github.com/sirupsen/logrus"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
)
|
|
|
|
var (
|
|
releasePattern = regexp.MustCompile("^v[0-9]")
|
|
helmChartGVK = helmv1.SchemeGroupVersion.WithKind("HelmChart")
|
|
injectAnnotationKey = version.Program + ".cattle.io/inject-cluster-config"
|
|
injectEnvKey = version.ProgramUpper + "_INJECT_CLUSTER_CONFIG"
|
|
injectDefault = true
|
|
)
|
|
|
|
// binDirForDigest returns the path to dataDir/data/refDigest/bin.
|
|
func binDirForDigest(dataDir string, refDigest string) string {
|
|
return filepath.Join(dataDir, "data", refDigest, "bin")
|
|
}
|
|
|
|
// chartsDirForDigest returns the path to dataDir/data/refDigest/charts.
|
|
func chartsDirForDigest(dataDir string, refDigest string) string {
|
|
return filepath.Join(dataDir, "data", refDigest, "charts")
|
|
}
|
|
|
|
// manifestsDir returns the path to dataDir/server/manifests.
|
|
func manifestsDir(dataDir string) string {
|
|
return filepath.Join(dataDir, "server", "manifests")
|
|
}
|
|
|
|
// imagesDir returns the path to dataDir/agent/images.
|
|
func imagesDir(dataDir string) string {
|
|
return filepath.Join(dataDir, "agent", "images")
|
|
}
|
|
|
|
// symlinkBinDir returns the path to dataDir/bin.
|
|
// This will be symlinked to the current runtime bin dir.
|
|
func symlinkBinDir(dataDir string) string {
|
|
return filepath.Join(dataDir, "bin")
|
|
}
|
|
|
|
// dirExists returns true if a directory exists at the given path.
|
|
func dirExists(dir string) bool {
|
|
if s, err := os.Stat(dir); err == nil && s.IsDir() {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Stage extracts binaries and manifests from the runtime image specified in imageConf into the directory
|
|
// at dataDir. It attempts to load the runtime image from a tarball at dataDir/agent/images,
|
|
// falling back to a remote image pull if the image is not found within a tarball.
|
|
// Extraction is skipped if a bin directory for the specified image already exists.
|
|
// Unique image detection is accomplished by hashing the image name and tag, or the image digest,
|
|
// depending on what the runtime image reference points at.
|
|
// If the bin directory already exists, or content is successfully extracted, the bin directory path is returned.
|
|
func Stage(resolver *images.Resolver, nodeConfig *daemonconfig.Node, cfg cmds.Agent) (string, error) {
|
|
var img v1.Image
|
|
|
|
ref, err := resolver.GetReference(images.Runtime)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
refDigest, err := releaseRefDigest(ref)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
refBinDir := binDirForDigest(cfg.DataDir, refDigest)
|
|
refChartsDir := chartsDirForDigest(cfg.DataDir, refDigest)
|
|
imagesDir := imagesDir(cfg.DataDir)
|
|
|
|
if dirExists(refBinDir) && dirExists(refChartsDir) {
|
|
logrus.Infof("Runtime image %s bin and charts directories already exist; skipping extract", ref.Name())
|
|
} else {
|
|
// Try to use configured runtime image from an airgap tarball
|
|
img, err = preloadBootstrapFromRuntime(imagesDir, resolver)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// If we didn't find the requested image in a tarball, pull it from the remote registry.
|
|
// Note that this will fail (potentially after a long delay) if the registry cannot be reached.
|
|
if img == nil {
|
|
registry, err := registries.GetPrivateRegistries(cfg.PrivateRegistry)
|
|
if err != nil {
|
|
return "", pkgerrors.WithMessagef(err, "failed to load private registry configuration from %s", cfg.PrivateRegistry)
|
|
}
|
|
// Override registry config with version provided by (and potentially modified by) k3s agent setup
|
|
registry.Registry = nodeConfig.AgentConfig.Registry
|
|
|
|
// Try to enable Kubelet image credential provider plugins; fall back to legacy docker credentials
|
|
if agent.ImageCredProvAvailable(&nodeConfig.AgentConfig) {
|
|
plugins, err := plugin.RegisterCredentialProviderPlugins(nodeConfig.AgentConfig.ImageCredProvConfig, nodeConfig.AgentConfig.ImageCredProvBinDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
registry.DefaultKeychain = plugins
|
|
} else {
|
|
registry.DefaultKeychain = authn.DefaultKeychain
|
|
}
|
|
|
|
logrus.Infof("Pulling runtime image %s", ref.Name())
|
|
// Make sure that the runtime image is also loaded into containerd
|
|
images.Pull(imagesDir, images.Runtime, ref)
|
|
img, err = registry.Image(ref, remote.WithPlatform(v1.Platform{Architecture: runtime.GOARCH, OS: runtime.GOOS}))
|
|
if err != nil {
|
|
return "", pkgerrors.WithMessagef(err, "failed to get runtime image %s", ref.Name())
|
|
}
|
|
}
|
|
|
|
// Extract binaries and charts
|
|
extractPaths := map[string]string{
|
|
"/bin": refBinDir,
|
|
"/charts": refChartsDir,
|
|
}
|
|
if err := extract.ExtractDirs(img, extractPaths); err != nil {
|
|
return "", pkgerrors.WithMessage(err, "failed to extract runtime image")
|
|
}
|
|
// Ensure correct permissions on bin dir
|
|
if err := os.Chmod(refBinDir, 0755); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
// ignore errors on symlink rewrite
|
|
_ = os.RemoveAll(symlinkBinDir(cfg.DataDir))
|
|
_ = os.Symlink(refBinDir, symlinkBinDir(cfg.DataDir))
|
|
|
|
return refBinDir, nil
|
|
}
|
|
|
|
// UpdateManifests copies the staged manifests into the server's manifests dir, and applies
|
|
// cluster configuration values to any HelmChart manifests found in the manifests directory.
|
|
func UpdateManifests(resolver *images.Resolver, ingressController string, nodeConfig *daemonconfig.Node, cfg cmds.Agent) error {
|
|
ref, err := resolver.GetReference(images.Runtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
refDigest, err := releaseRefDigest(ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
refChartsDir := chartsDirForDigest(cfg.DataDir, refDigest)
|
|
manifestsDir := manifestsDir(cfg.DataDir)
|
|
|
|
// TODO - instead of copying over then rewriting the manifests, we should template them as we
|
|
// copy, only overwriting if they're different - and then make a second pass and rewrite any
|
|
// user-provided manifests that weren't just copied over. This will work better with the deploy
|
|
// controller's mtime-based change detection.
|
|
|
|
// Copy all charts into the manifests directory, since the K3s
|
|
// deploy controller will delete them if they are disabled.
|
|
if err := copyDir(manifestsDir, refChartsDir); err != nil {
|
|
return pkgerrors.WithMessage(err, "failed to copy runtime charts")
|
|
}
|
|
|
|
// Fix up HelmCharts to pass through configured values.
|
|
// This needs to be done every time in order to sync values from the CLI
|
|
if err := setChartValues(manifestsDir, ingressController, nodeConfig, cfg); err != nil {
|
|
return pkgerrors.WithMessage(err, "failed to rewrite HelmChart manifests to pass through CLI values")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// releaseRefDigest returns a unique name for an image reference.
|
|
// If the image refers to a tag that appears to be a version string, it returns the tag + the first 12 bytes of the SHA256 hash of the reference string.
|
|
// If the image refers to a digest, it returns the digest, without the alg prefix ("sha256:", etc).
|
|
// If neither of the above conditions are met (semver tag or digest), an error is raised.
|
|
func releaseRefDigest(ref name.Reference) (string, error) {
|
|
if t, ok := ref.(name.Tag); ok && releasePattern.MatchString(t.TagStr()) {
|
|
hash := sha256.Sum256([]byte(ref.String()))
|
|
return t.TagStr() + "-" + hex.EncodeToString(hash[:])[:12], nil
|
|
} else if d, ok := ref.(name.Digest); ok {
|
|
str := d.DigestStr()
|
|
parts := strings.SplitN(str, ":", 2)
|
|
if len(parts) == 2 {
|
|
return parts[1], nil
|
|
}
|
|
return parts[0], nil
|
|
}
|
|
return "", fmt.Errorf("Runtime image %s is not a not a reference to a digest or version tag matching pattern %s", ref.Name(), releasePattern)
|
|
}
|
|
|
|
// preloadBootstrapFromRuntime tries to load the runtime image from tarballs, using both the
|
|
// default registry, and the user-configured registry (on the off chance they've retagged the
|
|
// images in the tarball to match their private registry).
|
|
func preloadBootstrapFromRuntime(imagesDir string, resolver *images.Resolver) (v1.Image, error) {
|
|
var refs []name.Reference
|
|
runtimeRef, err := resolver.GetReference(images.Runtime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if runtimeRef.Context().Registry.Name() == images.DefaultRegistry {
|
|
// If the image is from the default registry, only check for that.
|
|
refs = []name.Reference{runtimeRef}
|
|
} else {
|
|
// If the image is from a different registry, check the default first, then the configured registry.
|
|
defaultRef, err := resolver.GetReference(images.Runtime, images.WithRegistry(images.DefaultRegistry))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
refs = []name.Reference{defaultRef, runtimeRef}
|
|
}
|
|
|
|
for _, ref := range refs {
|
|
img, err := tarfile.FindImage(imagesDir, ref)
|
|
if img != nil {
|
|
return img, err
|
|
}
|
|
if err != nil {
|
|
logrus.Warnf("Failed to load runtime image %s from tarball: %v", ref.Name(), err)
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// copyDir recursively copies files from source to destination. If the target
|
|
// file already exists, the current permissions, ownership, and xattrs will be
|
|
// retained, but the contents will be overwritten.
|
|
func copyDir(target, source string) error {
|
|
entries, err := os.ReadDir(source)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read %s: %w", source, err)
|
|
}
|
|
|
|
if err := os.MkdirAll(target, 0755); err != nil && !os.IsExist(err) {
|
|
return err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
src := filepath.Join(source, entry.Name())
|
|
tgt := filepath.Join(target, entry.Name())
|
|
|
|
fileInfo, err := entry.Info()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get file info for %s: %w", entry.Name(), err)
|
|
}
|
|
|
|
switch {
|
|
case entry.IsDir():
|
|
if err := copyDir(tgt, src); err != nil {
|
|
return err
|
|
}
|
|
case (fileInfo.Mode() & os.ModeType) == 0:
|
|
if err := copyFile(tgt, src); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
logrus.Warnf("Skipping file with unsupported mode: %s: %s", src, fileInfo.Mode())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// copyFile copies the the source file to the target, creating or truncating it as necessary.
|
|
func copyFile(target, source string) error {
|
|
src, err := os.Open(source)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open source %s: %w", source, err)
|
|
}
|
|
defer src.Close()
|
|
|
|
tgt, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open target %s: %w", target, err)
|
|
}
|
|
defer tgt.Close()
|
|
|
|
_, err = io.Copy(tgt, src)
|
|
return err
|
|
}
|
|
|
|
// setChartValues scans the directory at manifestDir. It attempts to load all manifests
|
|
// in that directory as HelmCharts. Any manifests that contain a HelmChart are modified to
|
|
// pass through settings to both the Helm job and the chart values.
|
|
// NOTE: This will probably fail if any manifest contains multiple documents. This should
|
|
// not matter for any of our packaged components, but may prevent this from working on user manifests.
|
|
func setChartValues(manifestsDir, ingressController string, nodeConfig *daemonconfig.Node, cfg cmds.Agent) error {
|
|
chartValues := map[string]string{
|
|
"global.clusterCIDR": util.JoinIPNets(nodeConfig.AgentConfig.ClusterCIDRs),
|
|
"global.clusterCIDRv4": util.JoinIP4Nets(nodeConfig.AgentConfig.ClusterCIDRs),
|
|
"global.clusterCIDRv6": util.JoinIP6Nets(nodeConfig.AgentConfig.ClusterCIDRs),
|
|
"global.clusterDNS": util.JoinIPs(nodeConfig.AgentConfig.ClusterDNSs),
|
|
"global.clusterDomain": nodeConfig.AgentConfig.ClusterDomain,
|
|
"global.rke2DataDir": cfg.DataDir,
|
|
"global.serviceCIDR": util.JoinIPNets(nodeConfig.AgentConfig.ServiceCIDRs),
|
|
"global.systemDefaultIngressClass": ingressController,
|
|
"global.systemDefaultRegistry": nodeConfig.AgentConfig.SystemDefaultRegistry,
|
|
"global.cattle.systemDefaultRegistry": nodeConfig.AgentConfig.SystemDefaultRegistry,
|
|
}
|
|
|
|
files := map[string]os.FileInfo{}
|
|
if err := filepath.Walk(manifestsDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch {
|
|
case info.IsDir():
|
|
return nil
|
|
case strings.HasSuffix(path, ".yml"):
|
|
case strings.HasSuffix(path, ".yaml"):
|
|
default:
|
|
return nil
|
|
}
|
|
files[path] = info
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var errs []error
|
|
for fileName, info := range files {
|
|
if err := rewriteChart(fileName, info, chartValues); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
return merr.NewErrors(errs...)
|
|
}
|
|
|
|
// rewriteChart applies cluster configuration values to the file at fileName with associated info.
|
|
func rewriteChart(fileName string, info os.FileInfo, chartValues map[string]string) error {
|
|
fh, err := os.OpenFile(fileName, os.O_RDWR, info.Mode())
|
|
if err != nil {
|
|
return pkgerrors.WithMessagef(err, "failed to open manifest %s", fileName)
|
|
}
|
|
defer fh.Close()
|
|
|
|
// Ignore manifest if it cannot be decoded
|
|
objs, err := yaml.ToObjects(fh)
|
|
if err != nil {
|
|
logrus.Warnf("Failed to decode manifest %s: %s", fileName, err)
|
|
return nil
|
|
}
|
|
|
|
var changed bool
|
|
|
|
OBJECTS:
|
|
for _, obj := range objs {
|
|
// Manipulate the HelmChart using Unstructured to avoid dropping unknown fields when rewriting the content.
|
|
// Ref: https://github.com/rancher/rke2/issues/527
|
|
unst, ok := obj.(*unstructured.Unstructured)
|
|
|
|
// Ignore object if it is not a HelmChart
|
|
if !ok || unst.GroupVersionKind() != helmChartGVK {
|
|
continue
|
|
}
|
|
|
|
// Ignore object if injection is disabled via annotation or default setting
|
|
if !isInjectEnabled(unst) {
|
|
continue
|
|
}
|
|
|
|
var contentChanged bool
|
|
content := unst.UnstructuredContent()
|
|
|
|
// Generally we should avoid using Set on HelmCharts since it cannot be overridden by HelmChartConfig,
|
|
// but in this case we need to do it in order to avoid potentially mangling the ValuesContent YAML by
|
|
// blindly appending content to it in order to set values.
|
|
for k, v := range chartValues {
|
|
cv, _, err := unstructured.NestedString(content, "spec", "set", k)
|
|
if err != nil {
|
|
logrus.Warnf("Failed to get current value from %s/%s in %s: %v", unst.GetNamespace(), unst.GetName(), fileName, err)
|
|
continue OBJECTS
|
|
}
|
|
if cv != v {
|
|
if err := unstructured.SetNestedField(content, v, "spec", "set", k); err != nil {
|
|
logrus.Warnf("Failed to write chart value to %s/%s in %s: %v", unst.GetNamespace(), unst.GetName(), fileName, err)
|
|
continue OBJECTS
|
|
}
|
|
contentChanged = true
|
|
}
|
|
}
|
|
|
|
if contentChanged {
|
|
changed = true
|
|
unst.SetUnstructuredContent(content)
|
|
}
|
|
}
|
|
|
|
if !changed {
|
|
logrus.Infof("No cluster configuration value changes necessary for manifest %s", fileName)
|
|
return nil
|
|
}
|
|
|
|
data, err := yaml.Export(objs...)
|
|
if err != nil {
|
|
return pkgerrors.WithMessagef(err, "failed to export modified manifest %s", fileName)
|
|
}
|
|
|
|
if _, err := fh.Seek(0, 0); err != nil {
|
|
return pkgerrors.WithMessagef(err, "failed to seek in manifest %s", fileName)
|
|
}
|
|
|
|
if err := fh.Truncate(0); err != nil {
|
|
return pkgerrors.WithMessagef(err, "failed to truncate manifest %s", fileName)
|
|
}
|
|
|
|
if _, err := fh.Write(data); err != nil {
|
|
return pkgerrors.WithMessagef(err, "failed to write modified manifest %s", fileName)
|
|
}
|
|
|
|
if err := fh.Sync(); err != nil {
|
|
return pkgerrors.WithMessagef(err, "failed to sync modified manifest %s", fileName)
|
|
}
|
|
|
|
logrus.Infof("Updated manifest %s to set cluster configuration values", fileName)
|
|
return nil
|
|
}
|
|
|
|
func isInjectEnabled(obj *unstructured.Unstructured) bool {
|
|
if v, ok := obj.GetAnnotations()[injectAnnotationKey]; ok {
|
|
if b, err := strconv.ParseBool(v); err == nil {
|
|
return b
|
|
}
|
|
}
|
|
return getInjectDefault()
|
|
}
|
|
|
|
func getInjectDefault() bool {
|
|
if b, err := strconv.ParseBool(os.Getenv(injectEnvKey)); err == nil {
|
|
return b
|
|
}
|
|
return injectDefault
|
|
}
|