diff --git a/config/lxd.go b/config/lxd.go index 2b4e0b36..8b8b1f7e 100644 --- a/config/lxd.go +++ b/config/lxd.go @@ -99,7 +99,19 @@ type LXD struct { ImageRemotes map[string]LXDImageRemote `toml:"image_remotes" json:"image-remotes"` // SecureBoot enables secure boot for VMs spun up using this provider. - SecureBoot bool `yaml:"secure_boot" json:"secure-boot"` + SecureBoot bool `toml:"secure_boot" json:"secure-boot"` + + // InstanceType allows you to choose between a virtual machine and a container + InstanceType LXDImageType `toml:"instance_type" json:"instance-type"` +} + +func (l *LXD) GetInstanceType() LXDImageType { + switch l.InstanceType { + case LXDImageVirtualMachine, LXDImageContainer: + return l.InstanceType + default: + return LXDImageVirtualMachine + } } func (l *LXD) Validate() error { diff --git a/runner/providers/lxd/images.go b/runner/providers/lxd/images.go index 3a3fa4e2..259d8d4e 100644 --- a/runner/providers/lxd/images.go +++ b/runner/providers/lxd/images.go @@ -25,13 +25,10 @@ import ( lxd "github.com/lxc/lxd/client" "github.com/lxc/lxd/shared/api" "github.com/pkg/errors" - "gopkg.in/yaml.v3" ) type image struct { remotes map[string]config.LXDImageRemote - - cli lxd.InstanceServer } // parseImageName parses the image name that comes in from the config and returns a @@ -50,8 +47,8 @@ func (i *image) parseImageName(imageName string) (config.LXDImageRemote, string, return config.LXDImageRemote{}, "", runnerErrors.ErrNotFound } -func (i *image) getLocalImageByAlias(imageName string, imageType config.LXDImageType, arch string) (*api.Image, error) { - aliases, err := i.cli.GetImageAliasArchitectures(imageType.String(), imageName) +func (i *image) getLocalImageByAlias(imageName string, imageType config.LXDImageType, arch string, cli lxd.InstanceServer) (*api.Image, error) { + aliases, err := cli.GetImageAliasArchitectures(imageType.String(), imageName) if err != nil { return nil, errors.Wrapf(err, "resolving alias: %s", imageName) } @@ -61,7 +58,7 @@ func (i *image) getLocalImageByAlias(imageName string, imageType config.LXDImage return nil, fmt.Errorf("no image found for arch %s and image type %s with name %s", arch, imageType, imageName) } - image, _, err := i.cli.GetImage(alias.Target) + image, _, err := cli.GetImage(alias.Target) if err != nil { return nil, errors.Wrap(err, "fetching image details") } @@ -79,7 +76,7 @@ func (i *image) clientFromRemoteArgs(remote config.LXDImageRemote) (lxd.ImageSer return d, nil } -func (i *image) copyImageFromRemote(remote config.LXDImageRemote, imageName string, imageType config.LXDImageType, arch string) (*api.Image, error) { +func (i *image) copyImageFromRemote(remote config.LXDImageRemote, imageName string, imageType config.LXDImageType, arch string, cli lxd.InstanceServer) (*api.Image, error) { imgCli, err := i.clientFromRemoteArgs(remote) if err != nil { return nil, errors.Wrap(err, "fetching image server client") @@ -91,13 +88,6 @@ func (i *image) copyImageFromRemote(remote config.LXDImageRemote, imageName stri return nil, errors.Wrapf(err, "resolving alias: %s", imageName) } - yml, err := yaml.Marshal(aliases) - if err != nil { - return nil, err - } - - fmt.Println(string(yml)) - alias, ok := aliases[arch] if !ok { return nil, fmt.Errorf("no image found for arch %s and image type %s with name %s", arch, imageType, imageName) @@ -108,25 +98,11 @@ func (i *image) copyImageFromRemote(remote config.LXDImageRemote, imageName stri return nil, errors.Wrap(err, "fetching image details") } - // Ask LXD to copy the image from the remote server - imgAliases := []api.ImageAlias{} - found := false - for _, alias := range image.Aliases { - if alias.Name == imageName { - found = true - break - } - } - if !found { - imgAliases = append(imgAliases, api.ImageAlias{Name: imageName}) + imgCopyArgs := &lxd.ImageCopyArgs{ + AutoUpdate: true, } - imgCopyArgs := &lxd.ImageCopyArgs{ - AutoUpdate: true, - CopyAliases: true, - Aliases: imgAliases, - } - op, err := i.cli.CopyImage(imgCli, *image, imgCopyArgs) + op, err := cli.CopyImage(imgCli, *image, imgCopyArgs) if err != nil { return nil, errors.Wrapf(err, "copying image %s from %s", imageName, remote.Address) } @@ -139,18 +115,18 @@ func (i *image) copyImageFromRemote(remote config.LXDImageRemote, imageName stri // We should now have the image locally. Force another query. This probably makes no sense, // but this is done only once. - return i.getLocalImageByAlias(imageName, imageType, arch) + return image, nil } // EnsureImage will look for an image locally, then attempt to download it from a remote // server, if the name contains a remote. Allowed formats are: // remote_name:image_name // image_name -func (i *image) EnsureImage(imageName string, imageType config.LXDImageType, arch string) (*api.Image, error) { +func (i *image) EnsureImage(imageName string, imageType config.LXDImageType, arch string, cli lxd.InstanceServer) (*api.Image, error) { if !strings.Contains(imageName, ":") { // A remote was not specified, try to find an image using the imageName as // an alias. - return i.getLocalImageByAlias(imageName, imageType, arch) + return i.getLocalImageByAlias(imageName, imageType, arch, cli) } remote, parsedName, err := i.parseImageName(imageName) @@ -158,13 +134,13 @@ func (i *image) EnsureImage(imageName string, imageType config.LXDImageType, arc return nil, errors.Wrap(err, "parsing image name") } - if img, err := i.getLocalImageByAlias(parsedName, imageType, arch); err == nil { + if img, err := i.getLocalImageByAlias(parsedName, imageType, arch, cli); err == nil { return img, nil } else { log.Printf("failed to fetch local image of type %v with name %s and arch %s: %s", imageType, parsedName, arch, err) } - img, err := i.copyImageFromRemote(remote, parsedName, imageType, arch) + img, err := i.copyImageFromRemote(remote, parsedName, imageType, arch, cli) if err != nil { return nil, errors.Wrap(err, "fetching image") } diff --git a/runner/providers/lxd/lxd.go b/runner/providers/lxd/lxd.go index 082b760d..d92b180b 100644 --- a/runner/providers/lxd/lxd.go +++ b/runner/providers/lxd/lxd.go @@ -17,6 +17,7 @@ package lxd import ( "context" "fmt" + "sync" "garm/config" runnerErrors "garm/errors" @@ -80,24 +81,11 @@ func NewProvider(ctx context.Context, cfg *config.Provider, controllerID string) return nil, fmt.Errorf("invalid provider type %s, expected %s", cfg.ProviderType, config.LXDProvider) } - cli, err := getClientFromConfig(ctx, &cfg.LXD) - if err != nil { - return nil, errors.Wrap(err, "creating LXD client") - } - - _, _, err = cli.GetProject(projectName(cfg.LXD)) - if err != nil { - return nil, errors.Wrapf(err, "fetching project name: %s", projectName(cfg.LXD)) - } - cli = cli.UseProject(projectName(cfg.LXD)) - provider := &LXD{ ctx: ctx, cfg: cfg, - cli: cli, controllerID: controllerID, imageManager: &image{ - cli: cli, remotes: cfg.LXD.ImageRemotes, }, } @@ -116,6 +104,30 @@ type LXD struct { imageManager *image // controllerID is the ID of this controller controllerID string + + mux sync.Mutex +} + +func (l *LXD) getCLI() (lxd.InstanceServer, error) { + l.mux.Lock() + defer l.mux.Unlock() + + if l.cli != nil { + return l.cli, nil + } + cli, err := getClientFromConfig(l.ctx, &l.cfg.LXD) + if err != nil { + return nil, errors.Wrap(err, "creating LXD client") + } + + _, _, err = cli.GetProject(projectName(l.cfg.LXD)) + if err != nil { + return nil, errors.Wrapf(err, "fetching project name: %s", projectName(l.cfg.LXD)) + } + cli = cli.UseProject(projectName(l.cfg.LXD)) + l.cli = cli + + return cli, nil } func (l *LXD) getProfiles(flavor string) ([]string, error) { @@ -126,7 +138,12 @@ func (l *LXD) getProfiles(flavor string) ([]string, error) { set := map[string]struct{}{} - profiles, err := l.cli.GetProfileNames() + cli, err := l.getCLI() + if err != nil { + return nil, errors.Wrap(err, "fetching client") + } + + profiles, err := cli.GetProfileNames() if err != nil { return nil, errors.Wrap(err, "fetching profile names") } @@ -208,7 +225,9 @@ func (l *LXD) getCreateInstanceArgs(bootstrapParams params.BootstrapInstance) (a return api.InstancesPost{}, errors.Wrap(err, "fetching archictecture") } - image, err := l.imageManager.EnsureImage(bootstrapParams.Image, config.LXDImageVirtualMachine, arch) + instanceType := l.cfg.LXD.GetInstanceType() + + image, err := l.imageManager.EnsureImage(bootstrapParams.Image, instanceType, arch, l.cli) if err != nil { return api.InstancesPost{}, errors.Wrap(err, "getting image details") } @@ -223,24 +242,29 @@ func (l *LXD) getCreateInstanceArgs(bootstrapParams params.BootstrapInstance) (a return api.InstancesPost{}, errors.Wrap(err, "generating cloud-config") } + configMap := map[string]string{ + "user.user-data": cloudCfg, + controllerIDKeyName: l.controllerID, + poolIDKey: bootstrapParams.PoolID, + } + + if instanceType == config.LXDImageVirtualMachine { + configMap["security.secureboot"] = l.secureBootEnabled() + } + args := api.InstancesPost{ InstancePut: api.InstancePut{ Architecture: image.Architecture, Profiles: profiles, Description: "Github runner provisioned by garm", - Config: map[string]string{ - "user.user-data": cloudCfg, - "security.secureboot": l.secureBootEnabled(), - controllerIDKeyName: l.controllerID, - poolIDKey: bootstrapParams.PoolID, - }, + Config: configMap, }, Source: api.InstanceSource{ Type: "image", Fingerprint: image.Fingerprint, }, Name: bootstrapParams.Name, - Type: api.InstanceTypeVM, + Type: api.InstanceType(instanceType), } return args, nil } @@ -254,8 +278,12 @@ func (l *LXD) AsParams() params.Provider { } func (l *LXD) launchInstance(createArgs api.InstancesPost) error { + cli, err := l.getCLI() + if err != nil { + return errors.Wrap(err, "fetching client") + } // Get LXD to create the instance (background operation) - op, err := l.cli.CreateInstance(createArgs) + op, err := cli.CreateInstance(createArgs) if err != nil { return errors.Wrap(err, "creating instance") } @@ -272,7 +300,7 @@ func (l *LXD) launchInstance(createArgs api.InstancesPost) error { Timeout: -1, } - op, err = l.cli.UpdateInstanceState(createArgs.Name, reqState, "") + op, err = cli.UpdateInstanceState(createArgs.Name, reqState, "") if err != nil { return errors.Wrap(err, "starting instance") } @@ -292,8 +320,6 @@ func (l *LXD) CreateInstance(ctx context.Context, bootstrapParams params.Bootstr return params.Instance{}, errors.Wrap(err, "fetching create args") } - // asJs, err := yaml.Marshal(args) - // fmt.Println(string(asJs), err) if err := l.launchInstance(args); err != nil { return params.Instance{}, errors.Wrap(err, "creating instance") } @@ -308,7 +334,11 @@ func (l *LXD) CreateInstance(ctx context.Context, bootstrapParams params.Bootstr // GetInstance will return details about one instance. func (l *LXD) GetInstance(ctx context.Context, instanceName string) (params.Instance, error) { - instance, _, err := l.cli.GetInstanceFull(instanceName) + cli, err := l.getCLI() + if err != nil { + return params.Instance{}, errors.Wrap(err, "fetching client") + } + instance, _, err := cli.GetInstanceFull(instanceName) if err != nil { if isNotFoundError(err) { return params.Instance{}, errors.Wrapf(runnerErrors.ErrNotFound, "fetching instance: %q", err) @@ -321,6 +351,11 @@ func (l *LXD) GetInstance(ctx context.Context, instanceName string) (params.Inst // Delete instance will delete the instance in a provider. func (l *LXD) DeleteInstance(ctx context.Context, instance string) error { + cli, err := l.getCLI() + if err != nil { + return errors.Wrap(err, "fetching client") + } + if err := l.setState(instance, "stop", true); err != nil { if isNotFoundError(err) { return nil @@ -333,7 +368,7 @@ func (l *LXD) DeleteInstance(ctx context.Context, instance string) error { } } - op, err := l.cli.DeleteInstance(instance) + op, err := cli.DeleteInstance(instance) if err != nil { return errors.Wrap(err, "removing instance") } @@ -347,7 +382,12 @@ func (l *LXD) DeleteInstance(ctx context.Context, instance string) error { // ListInstances will list all instances for a provider. func (l *LXD) ListInstances(ctx context.Context, poolID string) ([]params.Instance, error) { - instances, err := l.cli.GetInstancesFull(api.InstanceTypeAny) + cli, err := l.getCLI() + if err != nil { + return []params.Instance{}, errors.Wrap(err, "fetching client") + } + + instances, err := cli.GetInstancesFull(api.InstanceTypeAny) if err != nil { return []params.Instance{}, errors.Wrap(err, "fetching instances") } @@ -394,7 +434,12 @@ func (l *LXD) setState(instance, state string, force bool) error { Force: force, } - op, err := l.cli.UpdateInstanceState(instance, reqState, "") + cli, err := l.getCLI() + if err != nil { + return errors.Wrap(err, "fetching client") + } + + op, err := cli.UpdateInstanceState(instance, reqState, "") if err != nil { return errors.Wrapf(err, "setting state to %s", state) } diff --git a/runner/providers/lxd/util.go b/runner/providers/lxd/util.go index 475ab00a..941e5107 100644 --- a/runner/providers/lxd/util.go +++ b/runner/providers/lxd/util.go @@ -161,7 +161,13 @@ func getClientFromConfig(ctx context.Context, cfg *config.LXD) (cli lxd.Instance TLSClientCert: string(clientCertContents), TLSClientKey: string(clientKeyContents), } - return lxd.ConnectLXD(cfg.URL, &connectArgs) + + lxdCLI, err := lxd.ConnectLXD(cfg.URL, &connectArgs) + if err != nil { + return nil, errors.Wrap(err, "connecting to LXD") + } + + return lxdCLI, nil } func projectName(cfg config.LXD) string {