AB#2426 Mini Constellation (#198)

* Mini Constellation commands to quickly deploy a local Constellation cluster

* Download libvirt container image if not present locally

* Fix libvirt KVM permission issues by creating kvm group using host GID inside container

* Remove QEMU specific values from state file

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
Co-authored-by: Nils Hanke <nils.hanke@outlook.com>
This commit is contained in:
Daniel Weiße 2022-10-07 09:38:43 +02:00 committed by GitHub
parent 0c651c55dd
commit 0edae36e43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 469 additions and 139 deletions

View file

@ -44,6 +44,7 @@ func NewRootCmd() *cobra.Command {
rootCmd.AddCommand(cmd.NewConfigCmd())
rootCmd.AddCommand(cmd.NewCreateCmd())
rootCmd.AddCommand(cmd.NewInitCmd())
rootCmd.AddCommand(cmd.NewMiniCmd())
rootCmd.AddCommand(cmd.NewVerifyCmd())
rootCmd.AddCommand(cmd.NewUpgradeCmd())
rootCmd.AddCommand(cmd.NewRecoverCmd())

View file

@ -176,9 +176,7 @@ func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirt
if err := lv.Start(ctx, name, config.Provider.QEMU.LibvirtContainerImage); err != nil {
return state.ConstellationState{}, err
}
// non standard port to avoid conflict with host libvirt
// changes here should also be reflected in the Dockerfile in "cli/internal/libvirt/Dockerfile"
libvirtURI = "qemu+tcp://localhost:16599/system"
libvirtURI = libvirt.LibvirtTCPConnectURI
// socket for system URI should be in /var/run/libvirt/libvirt-sock
case libvirtURI == "qemu:///system":

View file

@ -14,6 +14,7 @@ import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/state"
@ -23,11 +24,17 @@ import (
)
func TestCreate(t *testing.T) {
fsWithDefaultConfig := func(require *require.Assertions, provider cloudprovider.Provider) afero.Fs {
fs := afero.NewMemMapFs()
file := file.NewHandler(fs)
require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider)))
return fs
}
testState := state.ConstellationState{Name: "test", LoadBalancerIP: "192.0.2.1"}
someErr := errors.New("failed")
testCases := map[string]struct {
setupFs func(*require.Assertions) afero.Fs
setupFs func(*require.Assertions, cloudprovider.Provider) afero.Fs
creator *stubCloudCreator
provider cloudprovider.Provider
yesFlag bool
@ -40,7 +47,7 @@ func TestCreate(t *testing.T) {
wantAbbort bool
}{
"create": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
setupFs: fsWithDefaultConfig,
creator: &stubCloudCreator{state: testState},
provider: cloudprovider.GCP,
controllerCountFlag: intPtr(1),
@ -48,7 +55,7 @@ func TestCreate(t *testing.T) {
yesFlag: true,
},
"interactive": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
setupFs: fsWithDefaultConfig,
creator: &stubCloudCreator{state: testState},
provider: cloudprovider.Azure,
controllerCountFlag: intPtr(2),
@ -56,7 +63,7 @@ func TestCreate(t *testing.T) {
stdin: "yes\n",
},
"interactive abort": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
setupFs: fsWithDefaultConfig,
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
controllerCountFlag: intPtr(1),
@ -65,7 +72,7 @@ func TestCreate(t *testing.T) {
wantAbbort: true,
},
"interactive error": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
setupFs: fsWithDefaultConfig,
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
controllerCountFlag: intPtr(1),
@ -74,7 +81,7 @@ func TestCreate(t *testing.T) {
wantErr: true,
},
"flag name to long": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
setupFs: fsWithDefaultConfig,
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
controllerCountFlag: intPtr(1),
@ -83,7 +90,7 @@ func TestCreate(t *testing.T) {
wantErr: true,
},
"flag control-plane-count invalid": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
setupFs: fsWithDefaultConfig,
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
controllerCountFlag: intPtr(0),
@ -91,7 +98,7 @@ func TestCreate(t *testing.T) {
wantErr: true,
},
"flag worker-count invalid": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
setupFs: fsWithDefaultConfig,
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
controllerCountFlag: intPtr(3),
@ -99,24 +106,25 @@ func TestCreate(t *testing.T) {
wantErr: true,
},
"flag control-plane-count missing": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
setupFs: fsWithDefaultConfig,
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
workerCountFlag: intPtr(3),
wantErr: true,
},
"flag worker-count missing": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
setupFs: fsWithDefaultConfig,
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
controllerCountFlag: intPtr(3),
wantErr: true,
},
"old state in directory": {
setupFs: func(require *require.Assertions) afero.Fs {
setupFs: func(require *require.Assertions, csp cloudprovider.Provider) afero.Fs {
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
require.NoError(fileHandler.Write(constants.StateFilename, []byte{1}, file.OptNone))
require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), csp)))
return fs
},
creator: &stubCloudCreator{},
@ -127,10 +135,11 @@ func TestCreate(t *testing.T) {
wantErr: true,
},
"old adminConf in directory": {
setupFs: func(require *require.Assertions) afero.Fs {
setupFs: func(require *require.Assertions, csp cloudprovider.Provider) afero.Fs {
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
require.NoError(fileHandler.Write(constants.AdminConfFilename, []byte{1}, file.OptNone))
require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), csp)))
return fs
},
creator: &stubCloudCreator{},
@ -141,10 +150,11 @@ func TestCreate(t *testing.T) {
wantErr: true,
},
"old masterSecret in directory": {
setupFs: func(require *require.Assertions) afero.Fs {
setupFs: func(require *require.Assertions, csp cloudprovider.Provider) afero.Fs {
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
require.NoError(fileHandler.Write(constants.MasterSecretFilename, []byte{1}, file.OptNone))
require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), csp)))
return fs
},
creator: &stubCloudCreator{},
@ -155,17 +165,17 @@ func TestCreate(t *testing.T) {
wantErr: true,
},
"config does not exist": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
setupFs: fsWithDefaultConfig,
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
controllerCountFlag: intPtr(1),
workerCountFlag: intPtr(1),
yesFlag: true,
configFlag: constants.ConfigFilename,
configFlag: "/does/not/exist",
wantErr: true,
},
"create error": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
setupFs: fsWithDefaultConfig,
creator: &stubCloudCreator{createErr: someErr},
provider: cloudprovider.GCP,
controllerCountFlag: intPtr(1),
@ -174,8 +184,10 @@ func TestCreate(t *testing.T) {
wantErr: true,
},
"write state error": {
setupFs: func(require *require.Assertions) afero.Fs {
setupFs: func(require *require.Assertions, csp cloudprovider.Provider) afero.Fs {
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), csp)))
return afero.NewReadOnlyFs(fs)
},
creator: &stubCloudCreator{},
@ -196,7 +208,7 @@ func TestCreate(t *testing.T) {
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetIn(bytes.NewBufferString(tc.stdin))
cmd.Flags().String("config", "", "") // register persisten flag manually
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
if tc.yesFlag {
require.NoError(cmd.Flags().Set("yes", "true"))
}
@ -213,7 +225,7 @@ func TestCreate(t *testing.T) {
require.NoError(cmd.Flags().Set("worker-nodes", strconv.Itoa(*tc.workerCountFlag)))
}
fileHandler := file.NewHandler(tc.setupFs(require))
fileHandler := file.NewHandler(tc.setupFs(require, tc.provider))
err := create(cmd, tc.creator, fileHandler)

View file

@ -6,14 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only
package cmd
import "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
type helmLoader interface {
Load(csp string, conformanceMode bool) ([]byte, error)
Load(csp cloudprovider.Provider, conformanceMode bool) ([]byte, error)
}
type stubHelmLoader struct {
loadErr error
}
func (d *stubHelmLoader) Load(csp string, conformanceMode bool) ([]byte, error) {
func (d *stubHelmLoader) Load(csp cloudprovider.Provider, conformanceMode bool) ([]byte, error) {
return nil, d.loadErr
}

View file

@ -114,7 +114,7 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
return err
}
helmDeployments, err := helmLoader.Load(provider.String(), flags.conformance)
helmDeployments, err := helmLoader.Load(provider, flags.conformance)
if err != nil {
return fmt.Errorf("loading Helm charts: %w", err)
}

View file

@ -60,8 +60,7 @@ func TestInitialize(t *testing.T) {
AzureResourceGroup: "test",
}
testQemuState := &state.ConstellationState{
CloudProvider: "QEMU",
QEMUWorkerInstances: cloudtypes.Instances{"id-0": {}, "id-1": {}},
CloudProvider: "QEMU",
}
testInitResp := &initproto.InitResponse{
Kubeconfig: []byte("kubeconfig"),

24
cli/internal/cmd/mini.go Normal file
View file

@ -0,0 +1,24 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import "github.com/spf13/cobra"
// NewMiniCmd creates a new cobra.Command for managing mini Constellation clusters.
func NewMiniCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "mini",
Short: "Manage mini Constellation clusters",
Long: "Manage mini Constellation clusters.",
Args: cobra.ExactArgs(0),
}
cmd.AddCommand(newMiniUpCmd())
cmd.AddCommand(newMiniDownCmd())
return cmd
}

View file

@ -0,0 +1,60 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"errors"
"fmt"
"os"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/state"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"go.uber.org/multierr"
)
func newMiniDownCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "down",
Short: "Destroy a mini Constellation cluster",
Long: "Destroy a mini Constellation cluster.",
Args: cobra.ExactArgs(0),
RunE: runDown,
}
return cmd
}
func runDown(cmd *cobra.Command, args []string) error {
if err := checkForMiniCluster(file.NewHandler(afero.NewOsFs())); err != nil {
return fmt.Errorf("failed to destroy cluster: %w. Are you in the correct working directory?", err)
}
err := runTerminate(cmd, args)
if removeErr := os.Remove(constants.MasterSecretFilename); removeErr != nil && !os.IsNotExist(removeErr) {
err = multierr.Append(err, removeErr)
}
return err
}
func checkForMiniCluster(fileHandler file.Handler) error {
var state state.ConstellationState
if err := fileHandler.ReadJSON(constants.StateFilename, &state); err != nil {
return err
}
if cloudprovider.FromString(state.CloudProvider) != cloudprovider.QEMU {
return errors.New("cluster is not a QEMU based Constellation")
}
if state.Name != "mini" {
return errors.New("cluster is not a mini Constellation cluster")
}
return nil
}

279
cli/internal/cmd/miniup.go Normal file
View file

@ -0,0 +1,279 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"runtime"
"strings"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/cli/internal/helm"
"github.com/edgelesssys/constellation/v2/cli/internal/libvirt"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/grpc/dialer"
"github.com/edgelesssys/constellation/v2/internal/license"
"github.com/edgelesssys/constellation/v2/internal/versions"
"github.com/schollz/progressbar/v3"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"golang.org/x/sys/unix"
)
func newMiniUpCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "up",
Short: "Create and initialize a new mini Constellation cluster",
Long: "Create and initialize a new mini Constellation cluster.\n" +
"A mini cluster consists of a single control-plane and worker node, hosted using QEMU/KVM.\n",
Args: cobra.ExactArgs(0),
RunE: runUp,
}
// override global flag so we don't have a default value for the config
cmd.Flags().String("config", "", "path to the config file to use for the cluster")
return cmd
}
func runUp(cmd *cobra.Command, args []string) error {
if err := checkSystemRequirements(cmd.OutOrStdout()); err != nil {
return fmt.Errorf("system requirements not met: %w", err)
}
fileHandler := file.NewHandler(afero.NewOsFs())
// create config if not passed as flag and set default values
config, err := prepareConfig(cmd, fileHandler)
if err != nil {
return fmt.Errorf("preparing config: %w", err)
}
// create cluster
spinner := newSpinner(cmd, "Creating cluster in QEMU ", false)
spinner.Start()
err = createMiniCluster(cmd.Context(), fileHandler, cloudcmd.NewCreator(cmd.OutOrStdout()), config)
spinner.Stop()
if err != nil {
return fmt.Errorf("creating cluster: %w", err)
}
cmd.Println("Cluster successfully created.")
connectURI := config.Provider.QEMU.LibvirtURI
if connectURI == "" {
connectURI = libvirt.LibvirtTCPConnectURI
}
cmd.Println("Connect to the VMs by executing:")
cmd.Printf("\tvirsh -c %s\n\n", connectURI)
// initialize cluster
if err := initializeMiniCluster(cmd, fileHandler); err != nil {
return fmt.Errorf("initializing cluster: %w", err)
}
return nil
}
// checkSystemRequirements checks if the system meets the requirements for running a mini Constellation cluster.
// We do so by verifying that the host:
// - arch/os is linux/amd64.
// - has access to /dev/kvm.
// - has at least 4 CPU cores.
// - has at least 4GB of memory.
// - has at least 20GB of free disk space.
func checkSystemRequirements(out io.Writer) error {
// check arch/os
if runtime.GOARCH != "amd64" || runtime.GOOS != "linux" {
return fmt.Errorf("creation of a QEMU based Constellation is not supported for %s/%s", runtime.GOOS, runtime.GOARCH)
}
// check if /dev/kvm exists
if _, err := os.Stat("/dev/kvm"); err != nil {
return fmt.Errorf("unable to access KVM device: %w", err)
}
// check CPU cores
if runtime.NumCPU() < 4 {
return fmt.Errorf("insufficient CPU cores: %d, at least 4 cores are required by mini Constellation", runtime.NumCPU())
}
if runtime.NumCPU() < 6 {
fmt.Fprintf(out, "WARNING: Only %d CPU cores available. This may cause performance issues.\n", runtime.NumCPU())
}
// check memory
f, err := os.Open("/proc/meminfo")
if err != nil {
return fmt.Errorf("determining available memory: failed to open /proc/meminfo: %w", err)
}
defer f.Close()
var memKB int
scanner := bufio.NewScanner(f)
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "MemTotal:") {
_, err = fmt.Sscanf(scanner.Text(), "MemTotal:%d", &memKB)
if err != nil {
return fmt.Errorf("determining available memory: failed to parse /proc/meminfo: %w", err)
}
}
}
memGB := memKB / 1024 / 1024
if memGB < 4 {
return fmt.Errorf("insufficient memory: %dGB, at least 4GB of memory are required by mini Constellation", memGB)
}
if memGB < 6 {
fmt.Fprintln(out, "WARNING: Less than 6GB of memory available. This may cause performance issues.")
}
var stat unix.Statfs_t
if err := unix.Statfs(".", &stat); err != nil {
return err
}
freeSpaceGB := stat.Bavail * uint64(stat.Bsize) / 1024 / 1024 / 1024
if freeSpaceGB < 20 {
return fmt.Errorf("insufficient disk space: %dGB, at least 20GB of disk space are required by mini Constellation", freeSpaceGB)
}
return nil
}
// prepareConfig reads a given config, or creates a new minimal QEMU config.
func prepareConfig(cmd *cobra.Command, fileHandler file.Handler) (*config.Config, error) {
configPath, err := cmd.Flags().GetString("config")
if err != nil {
return nil, err
}
// check for existing config
if configPath != "" {
config, err := readConfig(cmd.OutOrStdout(), fileHandler, configPath)
if err != nil {
return nil, err
}
if config.GetProvider() != cloudprovider.QEMU {
return nil, errors.New("invalid provider for mini constellation cluster")
}
return config, nil
}
// download image to current directory if it doesn't exist
const imagePath = "./constellation.qcow2"
if _, err := os.Stat(imagePath); err == nil {
cmd.Printf("Using existing image at %s\n\n", imagePath)
} else if errors.Is(err, os.ErrNotExist) {
cmd.Printf("Downloading image to %s\n", imagePath)
if err := installImage(cmd.Context(), cmd.OutOrStdout(), versions.ConstellationQEMUImageURL, imagePath); err != nil {
return nil, fmt.Errorf("downloading image to %s: %w", imagePath, err)
}
} else {
return nil, fmt.Errorf("checking if image exists: %w", err)
}
if err := cmd.Flags().Set("config", constants.ConfigFilename); err != nil {
return nil, err
}
config := config.Default()
config.RemoveProviderExcept(cloudprovider.QEMU)
config.StateDiskSizeGB = 8
config.Provider.QEMU.Image = imagePath
return config, fileHandler.WriteYAML(constants.ConfigFilename, config, file.OptOverwrite)
}
// createMiniCluster creates a new cluster using the given config.
func createMiniCluster(ctx context.Context, fileHandler file.Handler, creator cloudCreator, config *config.Config) error {
state, err := creator.Create(ctx, cloudprovider.QEMU, config, "mini", "", 1, 1)
if err != nil {
return err
}
if err := fileHandler.WriteJSON(constants.StateFilename, state); err != nil {
return err
}
return writeIPtoIDFile(fileHandler, state)
}
// initializeMiniCluster initializes a QEMU cluster.
func initializeMiniCluster(cmd *cobra.Command, fileHandler file.Handler) (retErr error) {
// clean up cluster resources if initialization fails
defer func() {
if retErr != nil {
cmd.Printf("An error occurred: %s\n", retErr)
cmd.Println("Attempting to roll back.")
_ = runDown(cmd, []string{})
cmd.Printf("Rollback succeeded.\n\n")
}
}()
newDialer := func(validator *cloudcmd.Validator) *dialer.Dialer {
return dialer.New(nil, validator.V(cmd), &net.Dialer{})
}
helmLoader := &helm.ChartLoader{}
cmd.Flags().String("master-secret", "", "")
cmd.Flags().String("endpoint", "", "")
cmd.Flags().Bool("conformance", false, "")
if err := initialize(cmd, newDialer, fileHandler, helmLoader, license.NewClient()); err != nil {
return err
}
return nil
}
// installImage downloads the image from sourceURL to the destination.
func installImage(ctx context.Context, out io.Writer, sourceURL, destination string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("downloading image: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("downloading image: %s", resp.Status)
}
f, err := os.OpenFile(destination, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
bar := progressbar.NewOptions64(
resp.ContentLength,
progressbar.OptionSetWriter(out),
progressbar.OptionShowBytes(true),
progressbar.OptionSetPredictTime(true),
progressbar.OptionFullWidth(),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "=",
SaucerHead: ">",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
progressbar.OptionClearOnFinish(),
progressbar.OptionOnCompletion(func() { fmt.Fprintf(out, "Done.\n\n") }),
)
defer bar.Close()
_, err = io.Copy(io.MultiWriter(f, bar), resp.Body)
if err != nil {
return err
}
return nil
}

View file

@ -16,10 +16,6 @@ import (
)
func readConfig(out io.Writer, fileHandler file.Handler, name string) (*config.Config, error) {
if name == "" {
return config.Default(), nil
}
cnf, err := config.FromFile(fileHandler, name)
if err != nil {
return nil, err

View file

@ -31,7 +31,7 @@ var HelmFS embed.FS
type ChartLoader struct{}
func (i *ChartLoader) Load(csp string, conformanceMode bool) ([]byte, error) {
func (i *ChartLoader) Load(csp cloudprovider.Provider, conformanceMode bool) ([]byte, error) {
ciliumDeployment, err := i.loadCilium(csp, conformanceMode)
if err != nil {
return nil, err
@ -44,13 +44,13 @@ func (i *ChartLoader) Load(csp string, conformanceMode bool) ([]byte, error) {
return depl, nil
}
func (i *ChartLoader) loadCilium(csp string, conformanceMode bool) (helm.Deployment, error) {
func (i *ChartLoader) loadCilium(csp cloudprovider.Provider, conformanceMode bool) (helm.Deployment, error) {
chart, err := loadChartsDir(HelmFS, "charts/cilium")
if err != nil {
return helm.Deployment{}, err
}
var ciliumVals map[string]interface{}
switch cloudprovider.FromString(csp) {
switch csp {
case cloudprovider.GCP:
ciliumVals = gcpVals
case cloudprovider.Azure:

View file

@ -6,7 +6,6 @@ RUN dnf -y update && \
qemu-kvm \
swtpm \
swtpm-tools \
xsltproc \
libvirt-client && \
dnf clean all

View file

@ -9,14 +9,21 @@ package libvirt
import (
"context"
"errors"
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
docker "github.com/docker/docker/client"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
)
// LibvirtTCPConnectURI is the default URI to connect to containerized libvirt.
// Non standard port to avoid conflict with host libvirt.
// Changes here should also be reflected in the Dockerfile in "cli/internal/libvirt/Dockerfile".
const LibvirtTCPConnectURI = "qemu+tcp://localhost:16599/system"
// Runner handles starting and stopping of containerized libvirt instances.
type Runner struct {
nameFile string
@ -40,6 +47,32 @@ func (r *Runner) Start(ctx context.Context, name, imageName string) error {
defer docker.Close()
containerName := name + "-libvirt"
// check if image exists locally, if not pull it
// this allows us to use a custom image without having to push it to a registry
images, err := docker.ImageList(ctx, types.ImageListOptions{
Filters: filters.NewArgs(
filters.KeyValuePair{
Key: "reference",
Value: imageName,
},
),
})
if err != nil {
return err
}
if len(images) == 0 {
reader, err := docker.ImagePull(ctx, imageName, types.ImagePullOptions{})
if err != nil {
return err
}
defer reader.Close()
if _, err := io.Copy(io.Discard, reader); err != nil {
return err
}
}
// create and start the libvirt container
if _, err := docker.ContainerCreate(ctx,
&container.Config{
Image: imageName,
@ -61,13 +94,12 @@ func (r *Runner) Start(ctx context.Context, name, imageName string) error {
return err
}
// write the name of the container to a file so we can remove it later
if err := r.file.Write(r.nameFile, []byte(containerName)); err != nil {
_ = docker.ContainerRemove(ctx, containerName, types.ContainerRemoveOptions{Force: true})
return err
}
// time.Sleep(15 * time.Second)
return nil
}

View file

@ -1,5 +1,10 @@
#!/bin/bash
# Assign qemu the GID of the host system's 'kvm' group to avoid permission issues for environments defaulting to 660 for /dev/kvm (e.g. Debian-based distros)
KVM_HOST_GID="$(stat -c '%g' /dev/kvm)"
groupadd -o -g "$KVM_HOST_GID" host-kvm
usermod -a -G host-kvm qemu
# Start libvirt daemon
libvirtd --daemon --listen
virtlogd --daemon

View file

@ -89,6 +89,7 @@ func (c *Client) CreateCluster(ctx context.Context, name string, vars Variables)
return errors.New("invalid type in IP output: not a string")
}
c.state = state.ConstellationState{
Name: name,
CloudProvider: c.provider.String(),
LoadBalancerIP: ip,
}