projects/aws/bottlerocket-bootstrap/pkg/kubeadm/utils.go (262 lines of code) (raw):
package kubeadm
import (
"context"
"encoding/base64"
"fmt"
"io/fs"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
"github.com/aws/eks-anywhere-build-tooling/bottlerocket-bootstrap/pkg/files"
"github.com/aws/eks-anywhere-build-tooling/bottlerocket-bootstrap/pkg/utils"
)
const (
apiServerManifestPath = "/.bottlerocket/rootfs/etc/kubernetes/manifests/kube-apiserver"
ebsInitMarker = "/tmp/run-ebs-init"
kubernetesCACertFile = "/etc/kubernetes/pki/ca.crt"
)
func getBootstrapToken() (string, error) {
token, err := exec.Command(kubeadmBinary, "token", "list", "-o", "jsonpath=\"{.token}\"").Output()
if err != nil {
return "", errors.Wrap(err, "Error getting token")
}
// Remove leading and ending double quotes
// tokens do not contain quotes in the middle
replacedToken := strings.ReplaceAll(string(token), "\"", "")
return replacedToken, nil
}
func getEncodedCA() (string, error) {
caFileData, err := ioutil.ReadFile(kubernetesCACertFile)
if err != nil {
return "", errors.Wrap(err, "Error reading the ca data")
}
return base64.StdEncoding.EncodeToString(caFileData), nil
}
func setHostName(filepath string) error {
fmt.Println("Setting hostname in config files")
hostname, err := os.Hostname()
if err != nil {
return errors.Wrap(err, "Error getting hostname")
}
fileData, err := ioutil.ReadFile(filepath)
if err != nil {
return errors.Wrap(err, "Error reading kubeadm file")
}
fileDataStr := string(fileData)
fileDataStr = strings.ReplaceAll(fileDataStr, "{{ ds.meta_data.hostname }}", hostname)
// Write the file back
if err := files.Write(filepath, []byte(fileDataStr), 0o640); err != nil {
return errors.Wrap(err, "Error writing file")
}
fmt.Println("Wrote config file back to kubeadm")
fmt.Println(fileDataStr)
return nil
}
func getDNS(path string) (string, error) {
dns, err := exec.Command(kubectl, "--kubeconfig", path, "get", "svc", "kube-dns", "-n", "kube-system", "-o", "jsonpath='{..clusterIP}'").Output()
if err != nil {
return "", errors.Wrap(err, "Error getting api server")
}
// Remove leading and ending double quotes
// dns ip doesnt have quotes in the middle
replacedDns := strings.ReplaceAll(string(dns), "'", "")
return replacedDns, nil
}
// TODO: ioutil.ReadFile on the kubelet config fails saying no file found,
// but awk and cat command pass. Really weird error. Must try and debug this out
// Possibly we kill the kubeadm command prematurely because we just stat the file
// while the file is being written?
func getDNSFromJoinConfig(path string) (string, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
fmt.Println(string(data))
dns, err := exec.Command("bash", "-c", "awk '/clusterDNS:/ { getline; print $2 }' "+path).CombinedOutput()
if err != nil {
return "", errors.Wrap(err, "Error getting api server")
}
return strings.TrimSuffix(string(dns), "\n"), nil
}
func getBootstrapFromJoinConfig(path string) (string, string, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return "", "", errors.Wrap(err, "Error reading kubeadm join config file")
}
joinConfig := strings.TrimPrefix(string(data), "---")
kubeadmJoinData, err := unmarshalIntoMap([]byte(joinConfig))
if err != nil {
return "", "", errors.Wrap(err, "failed unmarshalling yaml kubeadm join config to interfaces")
}
discovery := kubeadmJoinData["discovery"].(map[string]interface{})
bootstrapToken := discovery["bootstrapToken"].(map[string]interface{})
serverEndpoint := bootstrapToken["apiServerEndpoint"].(string)
token := bootstrapToken["token"].(string)
return "https://" + serverEndpoint, token, nil
}
func getLocalApiServerReadinessEndpoint() (string, error) {
data, err := ioutil.ReadFile(apiServerManifestPath)
if err != nil {
return "", errors.Wrap(err, "Error reading ApiServer manifest file")
}
podDef, err := utils.UnmarshalPodDefinition(data)
if err != nil {
return "", errors.Wrap(err, "Error parsing pod def from manifest")
}
for _, container := range podDef.Spec.Containers {
// Validate if readiness probe exists on the definition
if container.ReadinessProbe != nil {
readinessProbeHandler := container.ReadinessProbe.HTTPGet
url := fmt.Sprintf("%s://%s:%d%s", readinessProbeHandler.Scheme, readinessProbeHandler.Host, readinessProbeHandler.Port.IntVal, readinessProbeHandler.Path)
return url, nil
}
}
return "", errors.New("Cannot find readiness probe exists on pod definition")
}
func isClusterWithExternalEtcd(kubeconfigPath string) (bool, error) {
clusterConfiguration, err := getClusterConfigurationFromCluster(kubeconfigPath)
if err != nil {
return false, err
}
return isExternalEtcd(clusterConfiguration)
}
func getClusterConfigurationFromCluster(kubeconfigPath string) ([]byte, error) {
clusterConfiguration, err := exec.Command(kubectl, "--kubeconfig", kubeconfigPath, "-n", "kube-system", "get", "cm", "kubeadm-config", "-o", "jsonpath='{..data.ClusterConfiguration}'").Output()
if err != nil {
return nil, errors.Wrap(err, "failed getting kubeadm-config configMap")
}
return clusterConfiguration, nil
}
func isExternalEtcd(clusterConfiguration []byte) (bool, error) {
config, err := unmarshalIntoMap(clusterConfiguration)
if err != nil {
return false, errors.Wrap(err, "error unmarshalling ClusterConfiguration")
}
etcd := config["etcd"].(map[string]interface{})
return etcd["external"] != nil, nil
}
func unmarshalIntoMap(content []byte) (map[string]interface{}, error) {
var parsedMap map[string]interface{}
content = []byte(strings.Trim(string(content), "'"))
err := yaml.Unmarshal(content, &parsedMap)
if err != nil {
return nil, errors.Wrap(err, "error unmarshalling into map of empty interface")
}
return parsedMap, err
}
type EbsInitControl struct {
Cancel context.CancelFunc
OkChan chan bool
Timeout <-chan time.Time
}
// startEbsInit starts the ebs-init goroutine in the background
// if marker file is present. Currenlty ebs-init is best-effort
// and if it fails it won't prevent the instance from bootstrapping.
// A 2 minutes timeout is added so that instance bootstrap time is
// not inflated.
func startEbsInit() *EbsInitControl {
if _, err := os.Stat(ebsInitMarker); err == nil {
okChan := make(chan bool)
ctx, cancel := context.WithCancel(context.Background())
fmt.Printf("Starting ebs-init \n")
ebsInitControl := &EbsInitControl{
Timeout: time.After(2 * time.Minute),
Cancel: cancel,
OkChan: okChan,
}
go func(ctx context.Context, okChan chan bool) {
for {
select {
case <-ctx.Done():
return
default:
readFiles()
okChan <- true
return
}
}
}(ctx, okChan)
return ebsInitControl
} else {
fmt.Printf("Skipping ebs-init \n")
return nil
}
}
// readFiles walks the filesystem tree and reads all regular files
func readFiles() {
root := "/.bottlerocket/rootfs/"
err := filepath.WalkDir(root, walkDirFunc)
if err != nil {
message := fmt.Errorf("failed reading files %w", err)
fmt.Printf("%s\n", message)
}
fmt.Printf("All files read \n")
}
// walkDirFunc is passed to filepath.WalkDir, it does some validations
// and skips non existing (or deleted/renamed) files and dirs as well as
// some special dirs
func walkDirFunc(path string, dirEntry fs.DirEntry, err error) error {
if dirEntry == nil {
return filepath.SkipDir
}
if dirEntry.IsDir() {
// skip special dirs
if dirEntry.Name() == "proc" || dirEntry.Name() == "sys" || dirEntry.Name() == "run" {
return filepath.SkipDir
}
} else {
entryInfo, entryInfoError := dirEntry.Info()
// check if file exists and is regular
if entryInfoError == nil && entryInfo.Mode().IsRegular() {
ioutil.ReadFile(path)
}
}
return nil
}
// KubeletTlsConfig is a struct that holds cert and private key
type KubeletTlsConfig struct {
KubeletServingCert string
KubeletServingPrivateKey string
}
// readKubeletTlsConfig loads kubelet serving cert and private key from files
func readKubeletTlsConfig(reader FileReader) *KubeletTlsConfig {
kubeletServingCertFile := "/.bottlerocket/rootfs/var/lib/kubeadm/pki/kubelet-serving.crt"
kubeletServingPrivateKeyFile := "/.bottlerocket/rootfs/var/lib/kubeadm/pki/kubelet-serving.key"
kubeletTlsConfig := &KubeletTlsConfig{}
kubeletServingCertContents, err := readAndEncodeFile(kubeletServingCertFile, reader)
if err != nil {
message := fmt.Errorf("skipping Kubelet TLS configuration: %s %w", kubeletServingCertFile, err)
fmt.Printf("%s\n", message)
return nil
}
kubeletServingPrivateKeyContents, err := readAndEncodeFile(kubeletServingPrivateKeyFile, reader)
if err != nil {
message := fmt.Errorf("skipping Kubelet TLS configuration: %s %w", kubeletServingPrivateKeyFile, err)
fmt.Printf("%s\n", message)
return nil
}
kubeletTlsConfig.KubeletServingCert = kubeletServingCertContents
kubeletTlsConfig.KubeletServingPrivateKey = kubeletServingPrivateKeyContents
fmt.Println("Kubelet TLS configuration available")
return kubeletTlsConfig
}
// FileReader is an interface that wraps the ReadFile function.
type FileReader interface {
ReadFile(filename string) ([]byte, error)
}
// RealFileReader is a struct that implements the FileReader interface
// using the ioutil.ReadFile function.
type RealFileReader struct{}
// ReadFile reads the contents of a file using ioutil.ReadFile.
func (r RealFileReader) ReadFile(filename string) ([]byte, error) {
return ioutil.ReadFile(filename)
}
// readAndEncodeFile reads the contents of a file and encodes them as base64 string
func readAndEncodeFile(filePath string, reader FileReader) (string, error) {
contents, err := reader.ReadFile(filePath)
if err == nil {
if string(contents) == "" {
return "", errors.New("empty contents")
}
return base64.StdEncoding.EncodeToString(contents), nil
}
return "", err
}
// isEmpty checks if passed sting is empty
func isEmpty(str string) bool {
return len(str) == 0
}
func patchKubeVipManifest() error {
fmt.Println("Patching kube-vip static pod manifest")
kubeVipManifest := filepath.Join(staticPodManifestsPath, "kube-vip.yaml")
contents, err := ioutil.ReadFile(kubeVipManifest)
if err != nil {
return errors.Wrapf(err, "Error reading file %s", kubeVipManifest)
}
updatedContents := strings.ReplaceAll(string(contents), "path: /etc/kubernetes/admin.conf", "path: /etc/kubernetes/super-admin.conf")
updatedContents = strings.ReplaceAll(string(updatedContents), "path: /var/lib/kubeadm/admin.conf", "path: /var/lib/kubeadm/super-admin.conf")
err = os.WriteFile(kubeVipManifest, []byte(updatedContents), 0o644)
if err != nil {
return errors.Wrapf(err, "Error writing file %s", kubeVipManifest)
}
fmt.Println("-------------------------------")
fmt.Printf("Patched kube-vip manifest: \n%s\n", updatedContents)
fmt.Println("-------------------------------")
return nil
}