kubernetes: verify Kubernetes components

This commit is contained in:
Leonard Cohnen 2022-11-14 19:09:49 +01:00 committed by 3u13r
parent 2c9ddbc6e7
commit 1e98b686b6
17 changed files with 439 additions and 229 deletions

View file

@ -1,17 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package k8sapi
const (
// Paths and permissions necessary for Kubernetes installation.
cniPluginsDir = "/opt/cni/bin"
binDir = "/run/state/bin"
kubeadmPath = "/run/state/bin/kubeadm"
kubeletPath = "/run/state/bin/kubelet"
kubeletServicePath = "/usr/lib/systemd/system/kubelet.service"
executablePerm = 0o544
)

View file

@ -10,6 +10,7 @@ import (
"archive/tar"
"compress/gzip"
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
@ -20,6 +21,7 @@ import (
"time"
"github.com/edgelesssys/constellation/v2/internal/retry"
"github.com/edgelesssys/constellation/v2/internal/versions"
"github.com/spf13/afero"
"k8s.io/utils/clock"
)
@ -50,28 +52,38 @@ func newOSInstaller() *osInstaller {
}
// Install downloads a resource from a URL, applies any given text transformations and extracts the resulting file if required.
// The resulting file(s) are copied to all destinations.
func (i *osInstaller) Install(
ctx context.Context, sourceURL string, destinations []string, perm fs.FileMode, extract bool,
) error {
tempPath, err := i.retryDownloadToTempDir(ctx, sourceURL)
// The resulting file(s) are copied to the destination. It also verifies the sha256 hash of the downloaded file.
func (i *osInstaller) Install(ctx context.Context, kubernetesComponent versions.ComponentVersion) error {
tempPath, err := i.retryDownloadToTempDir(ctx, kubernetesComponent.URL)
if err != nil {
return err
}
file, err := i.fs.OpenFile(tempPath, os.O_RDONLY, 0)
if err != nil {
return fmt.Errorf("opening file %q: %w", tempPath, err)
}
sha := sha256.New()
if _, err := io.Copy(sha, file); err != nil {
return fmt.Errorf("reading file %q: %w", tempPath, err)
}
calculatedHash := fmt.Sprintf("sha256:%x", sha.Sum(nil))
if calculatedHash != kubernetesComponent.Hash {
return fmt.Errorf("hash of file %q %s does not match expected hash %s", tempPath, calculatedHash, kubernetesComponent.Hash)
}
defer func() {
_ = i.fs.Remove(tempPath)
}()
for _, destination := range destinations {
var err error
if extract {
err = i.extractArchive(tempPath, destination, perm)
} else {
err = i.copy(tempPath, destination, perm)
}
if err != nil {
return fmt.Errorf("installing from %q: copying to destination %q: %w", sourceURL, destination, err)
}
if kubernetesComponent.Extract {
err = i.extractArchive(tempPath, kubernetesComponent.InstallPath, executablePerm)
} else {
err = i.copy(tempPath, kubernetesComponent.InstallPath, executablePerm)
}
if err != nil {
return fmt.Errorf("installing from %q: copying to destination %q: %w", kubernetesComponent.URL, kubernetesComponent.InstallPath, err)
}
return nil
}

View file

@ -22,6 +22,7 @@ import (
"testing"
"time"
"github.com/edgelesssys/constellation/v2/internal/versions"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -31,29 +32,52 @@ import (
)
func TestInstall(t *testing.T) {
serverURL := "http://server/path"
testCases := map[string]struct {
server httpBufconnServer
component versions.ComponentVersion
hash string
destination string
extract bool
readonly bool
wantErr bool
wantFiles map[string][]byte
}{
"download works": {
server: newHTTPBufconnServerWithBody([]byte("file-contents")),
destination: "/destination",
wantFiles: map[string][]byte{"/destination": []byte("file-contents")},
server: newHTTPBufconnServerWithBody([]byte("file-contents")),
component: versions.ComponentVersion{
URL: serverURL,
Hash: "sha256:f03779b36bece74893fd6533a67549675e21573eb0e288d87158738f9c24594e",
InstallPath: "/destination",
},
wantFiles: map[string][]byte{"/destination": []byte("file-contents")},
},
"download with extract works": {
server: newHTTPBufconnServerWithBody(createTarGz([]byte("file-contents"), "/destination")),
destination: "/prefix",
extract: true,
wantFiles: map[string][]byte{"/prefix/destination": []byte("file-contents")},
server: newHTTPBufconnServerWithBody(createTarGz([]byte("file-contents"), "/destination")),
component: versions.ComponentVersion{
URL: serverURL,
Hash: "sha256:a52a1664ca0a6ec9790384e3d058852ab8b3a8f389a9113d150fdc6ab308d949",
InstallPath: "/prefix",
Extract: true,
},
wantFiles: map[string][]byte{"/prefix/destination": []byte("file-contents")},
},
"hash validation fails": {
server: newHTTPBufconnServerWithBody([]byte("file-contents")),
component: versions.ComponentVersion{
URL: serverURL,
Hash: "sha256:abc",
InstallPath: "/destination",
},
wantErr: true,
},
"download fails": {
server: newHTTPBufconnServer(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }),
destination: "/destination",
wantErr: true,
server: newHTTPBufconnServer(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }),
component: versions.ComponentVersion{
URL: serverURL,
Hash: "sha256:abc",
InstallPath: "/destination",
},
wantErr: true,
},
}
@ -81,7 +105,7 @@ func TestInstall(t *testing.T) {
retriable: func(err error) bool { return false },
}
err := inst.Install(context.Background(), "http://server/path", []string{tc.destination}, fs.ModePerm, tc.extract)
err := inst.Install(context.Background(), tc.component)
if tc.wantErr {
assert.Error(err)
return

View file

@ -13,7 +13,6 @@ import (
"encoding/pem"
"errors"
"fmt"
"io/fs"
"net"
"net/http"
"os"
@ -42,6 +41,10 @@ import (
const (
// kubeletStartTimeout is the maximum time given to the kubelet service to (re)start.
kubeletStartTimeout = 10 * time.Minute
// crdTimeout is the maximum time given to the CRDs to be created.
crdTimeout = 30 * time.Second
executablePerm = 0o544
kubeletServicePath = "/usr/lib/systemd/system/kubelet.service"
)
// Client provides the functions to talk to the k8s API.
@ -57,7 +60,7 @@ type Client interface {
type installer interface {
Install(
ctx context.Context, sourceURL string, destinations []string, perm fs.FileMode, extract bool,
ctx context.Context, kubernetesComponent versions.ComponentVersion,
) error
}
@ -75,34 +78,26 @@ func NewKubernetesUtil() *KubernetesUtil {
}
}
// InstallComponentsFromCLI installs the kubernetes components passed from the CLI.
func (k *KubernetesUtil) InstallComponentsFromCLI(ctx context.Context, kubernetesComponents versions.ComponentVersions) error {
for _, component := range kubernetesComponents {
if err := k.inst.Install(ctx, component); err != nil {
return fmt.Errorf("installing kubernetes component from URL %s: %w", component.URL, err)
}
}
return enableSystemdUnit(ctx, kubeletServicePath)
}
// InstallComponents installs kubernetes components in the version specified.
// TODO(AB#2543,3u13r): Remove this function once the JoinService is extended.
func (k *KubernetesUtil) InstallComponents(ctx context.Context, version versions.ValidK8sVersion) error {
versionConf := versions.VersionConfigs[version]
if err := k.inst.Install(
ctx, versionConf.CNIPlugins.URL, []string{cniPluginsDir}, executablePerm, true,
); err != nil {
return fmt.Errorf("installing cni plugins: %w", err)
}
if err := k.inst.Install(
ctx, versionConf.Crictl.URL, []string{binDir}, executablePerm, true,
); err != nil {
return fmt.Errorf("installing crictl: %w", err)
}
if err := k.inst.Install(
ctx, versionConf.Kubelet.URL, []string{kubeletPath}, executablePerm, false,
); err != nil {
return fmt.Errorf("installing kubelet: %w", err)
}
if err := k.inst.Install(
ctx, versionConf.Kubeadm.URL, []string{kubeadmPath}, executablePerm, false,
); err != nil {
return fmt.Errorf("installing kubeadm: %w", err)
}
if err := k.inst.Install(
ctx, versionConf.Kubectl.URL, []string{constants.KubectlPath}, executablePerm, false,
); err != nil {
return fmt.Errorf("installing kubectl: %w", err)
for _, component := range versionConf.KubernetesComponents {
if err := k.inst.Install(ctx, component); err != nil {
return fmt.Errorf("installing kubernetes component from URL %s: %w", component.URL, err)
}
}
return enableSystemdUnit(ctx, kubeletServicePath)
@ -132,7 +127,7 @@ func (k *KubernetesUtil) InitCluster(
// preflight
log.Infof("Running kubeadm preflight checks")
cmd := exec.CommandContext(ctx, kubeadmPath, "init", "phase", "preflight", "-v=5", "--config", initConfigFile.Name())
cmd := exec.CommandContext(ctx, constants.KubeadmPath, "init", "phase", "preflight", "-v=5", "--config", initConfigFile.Name())
out, err := cmd.CombinedOutput()
if err != nil {
var exitErr *exec.ExitError
@ -144,7 +139,7 @@ func (k *KubernetesUtil) InitCluster(
// create CA certs
log.Infof("Creating Kubernetes control-plane certificates and keys")
cmd = exec.CommandContext(ctx, kubeadmPath, "init", "phase", "certs", "all", "-v=5", "--config", initConfigFile.Name())
cmd = exec.CommandContext(ctx, constants.KubeadmPath, "init", "phase", "certs", "all", "-v=5", "--config", initConfigFile.Name())
out, err = cmd.CombinedOutput()
if err != nil {
var exitErr *exec.ExitError
@ -172,7 +167,7 @@ func (k *KubernetesUtil) InitCluster(
skipPhases += ",addon/kube-proxy"
}
cmd = exec.CommandContext(ctx, kubeadmPath, "init", "-v=5", skipPhases, "--config", initConfigFile.Name())
cmd = exec.CommandContext(ctx, constants.KubeadmPath, "init", "-v=5", skipPhases, "--config", initConfigFile.Name())
out, err = cmd.CombinedOutput()
if err != nil {
var exitErr *exec.ExitError
@ -362,7 +357,7 @@ func (k *KubernetesUtil) JoinCluster(ctx context.Context, joinConfig []byte, pee
}
// run `kubeadm join` to join a worker node to an existing Kubernetes cluster
cmd := exec.CommandContext(ctx, kubeadmPath, "join", "-v=5", "--config", joinConfigFile.Name())
cmd := exec.CommandContext(ctx, constants.KubeadmPath, "join", "-v=5", "--config", joinConfigFile.Name())
out, err := cmd.CombinedOutput()
if err != nil {
var exitErr *exec.ExitError