From d68b842375724a705278e549d2773c4e9e1758b6 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Tue, 19 Apr 2022 14:42:10 +0000 Subject: [PATCH] LXD provider can create workers The bare minimum needed code to successfully create an instance that installs and launches a runner is there. --- cloudconfig/cloudconfig.go | 1 + cloudconfig/templates.go | 17 ++-- cmd/runner-manager/main.go | 8 +- config/config.go | 70 +------------- config/lxd.go | 129 +++++++++++++++++++++++++ runner/providers/lxd/images.go | 142 +++++++++++++++++++++++++++ runner/providers/lxd/lxd.go | 169 +++++++++++++++++++++++++-------- testdata/config.toml | 18 ++++ util/util.go | 1 + 9 files changed, 438 insertions(+), 117 deletions(-) create mode 100644 config/lxd.go create mode 100644 runner/providers/lxd/images.go diff --git a/cloudconfig/cloudconfig.go b/cloudconfig/cloudconfig.go index 2f4aff26..17a4e0a2 100644 --- a/cloudconfig/cloudconfig.go +++ b/cloudconfig/cloudconfig.go @@ -16,6 +16,7 @@ func NewDefaultCloudInitConfig() *CloudInit { PackageUpgrade: true, Packages: []string{ "curl", + "tar", }, SystemInfo: &SystemInfo{ DefaultUser: DefaultUser{ diff --git a/cloudconfig/templates.go b/cloudconfig/templates.go index 2c643fde..f0b2d195 100644 --- a/cloudconfig/templates.go +++ b/cloudconfig/templates.go @@ -7,19 +7,20 @@ import ( "github.com/pkg/errors" ) -var CloudConfigTemplate = ` -#!/bin/bash +var CloudConfigTemplate = `#!/bin/bash set -ex +set -o pipefail -curl -o "/home/runner/{{ .FileName }}" "{{ .DownloadURL }}" -mkdir -p /home/runner/action-runner -tar xf "/home/runner/{{ .FileName }}" -C /home/runner/action-runner/ -chown {{ .RunnerUsername }}:{{ .RunnerGroup }} -R /home/{{ .RunnerUsername }}/action-runner/ +curl -L -o "/home/runner/{{ .FileName }}" "{{ .DownloadURL }}" +mkdir -p /home/runner/actions-runner +tar xf "/home/runner/{{ .FileName }}" -C /home/runner/actions-runner/ +chown {{ .RunnerUsername }}:{{ .RunnerGroup }} -R /home/{{ .RunnerUsername }}/actions-runner/ sudo /home/{{ .RunnerUsername }}/actions-runner/bin/installdependencies.sh sudo -u {{ .RunnerUsername }} -- /home/{{ .RunnerUsername }}/actions-runner/config.sh --unattended --url "{{ .RepoURL }}" --token "{{ .GithubToken }}" --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral -/home/{{ .RunnerUsername }}/actions-runner/svc.sh install -/home/{{ .RunnerUsername }}/actions-runner/svc.sh start +cd /home/{{ .RunnerUsername }}/actions-runner +./svc.sh install {{ .RunnerUsername }} +./svc.sh start ` type InstallRunnerParams struct { diff --git a/cmd/runner-manager/main.go b/cmd/runner-manager/main.go index 0f30877c..d2f6d94b 100644 --- a/cmd/runner-manager/main.go +++ b/cmd/runner-manager/main.go @@ -16,6 +16,7 @@ import ( "github.com/google/go-github/v43/github" "golang.org/x/oauth2" + "gopkg.in/yaml.v3" ) var ( @@ -94,7 +95,12 @@ func main() { if err != nil { log.Fatal(err) } - log.Printf("got tools: %v", tools) + + toolsAsYaml, err := yaml.Marshal(tools) + if err != nil { + log.Fatal(err) + } + log.Printf("got tools:\n%s\n", string(toolsAsYaml)) log.Print("fetching runner token") ghRunnerToken, _, err := ghClient.Actions.CreateRegistrationToken(ctx, cfg.Repositories[0].Owner, cfg.Repositories[0].Name) diff --git a/config/config.go b/config/config.go index 5d477cfd..f6e14cff 100644 --- a/config/config.go +++ b/config/config.go @@ -6,7 +6,6 @@ import ( "fmt" "io/ioutil" "net" - "net/url" "os" "path/filepath" "time" @@ -63,6 +62,7 @@ const ( Amd64 OSArch = "amd64" I386 OSArch = "i386" Arm64 OSArch = "arm64" + Arm OSArch = "arm" ) // NewConfig returns a new Config @@ -150,74 +150,6 @@ func (g *Github) Validate() error { return nil } -// LXD holds connection information for an LXD cluster. -type LXD struct { - // UnixSocket is the path on disk to the LXD unix socket. If defined, - // this is prefered over connecting via HTTPs. - UnixSocket string `toml:"unix_socket_path" json:"unix-socket-path"` - - // Project name is the name of the project in which this runner will create - // instances. If this option is not set, the default project will be used. - // The project used here, must have all required profiles created by you - // beforehand. For LXD, the "flavor" used in the runner definition for a pool - // equates to a profile in the desired project. - ProjectName string `toml:"project_name" json:"project-name"` - - // IncludeDefaultProfile specifies whether or not this provider will always add - // the "default" profile to any newly created instance. - IncludeDefaultProfile bool `toml:"include_default_profile" json:"include-default-profile"` - - // URL holds the IP address. - URL string `toml:"address" json:"address"` - // ClientCertificate is the x509 client certificate path used for authentication. - ClientCertificate string `toml:"client_certificate" json:"client_certificate"` - // ClientKey is the key used for client certificate authentication. - ClientKey string `toml:"client_key" json:"client-key"` - // TLS certificate of the remote server. If not specified, the system CA is used. - TLSServerCert string `toml:"tls_server_certificate" json:"tls-server-certificate"` - // TLSCA is the TLS CA certificate when running LXD in PKI mode. - TLSCA string `toml:"tls_ca" json:"tls-ca"` - - // TODO: add simplestreams sources -} - -func (l *LXD) Validate() error { - if l.UnixSocket != "" { - if _, err := os.Stat(l.UnixSocket); err != nil { - return fmt.Errorf("could not access unix socket %s: %q", l.UnixSocket, err) - } - - return nil - } - - if l.URL == "" { - return fmt.Errorf("unix_socket or address must be specified") - } - - if _, err := url.Parse(l.URL); err != nil { - return fmt.Errorf("invalid LXD URL") - } - - if l.ClientCertificate == "" || l.ClientKey == "" { - return fmt.Errorf("client_certificate and client_key are mandatory when connecting via HTTPs") - } - - if _, err := os.Stat(l.ClientCertificate); err != nil { - return fmt.Errorf("failed to access client certificate %s: %q", l.ClientCertificate, err) - } - - if _, err := os.Stat(l.ClientKey); err != nil { - return fmt.Errorf("failed to access client key %s: %q", l.ClientKey, err) - } - - if l.TLSServerCert != "" { - if _, err := os.Stat(l.TLSServerCert); err != nil { - return fmt.Errorf("failed to access tls_server_certificate %s: %q", l.TLSServerCert, err) - } - } - return nil -} - // Provider holds access information for a particular provider. // A provider offers compute resources on which we spin up self hosted runners. type Provider struct { diff --git a/config/lxd.go b/config/lxd.go new file mode 100644 index 00000000..27f00fc9 --- /dev/null +++ b/config/lxd.go @@ -0,0 +1,129 @@ +package config + +import ( + "fmt" + "net/url" + "os" + + "github.com/pkg/errors" +) + +type LXDRemoteProtocol string +type LXDImageType string + +func (l LXDImageType) String() string { + return string(l) +} + +const ( + SimpleStreams LXDRemoteProtocol = "simplestreams" + LXDImageVirtualMachine LXDImageType = "virtual-machine" + LXDImageContainer LXDImageType = "container" +) + +type LXDRemote struct { + Address string `toml:"addr" json:"addr"` + Public bool `toml:"public" json:"public"` + Protocol LXDRemoteProtocol `toml:"protocol" json:"protocol"` + InsecureSkipVerify bool `toml:"skip_verify" json:"skip-verify"` +} + +func (l *LXDRemote) Validate() error { + if l.Protocol != SimpleStreams { + // Only supports simplestreams for now. + return fmt.Errorf("invalid remote protocol %s. Supported protocols: %s", l.Protocol, SimpleStreams) + } + if l.Address == "" { + return fmt.Errorf("missing address") + } + + url, err := url.ParseRequestURI(l.Address) + if err != nil { + return errors.Wrap(err, "validating address") + } + + if url.Scheme != "http" && url.Scheme != "https" { + return fmt.Errorf("address must be http or https") + } + + return nil +} + +// LXD holds connection information for an LXD cluster. +type LXD struct { + // UnixSocket is the path on disk to the LXD unix socket. If defined, + // this is prefered over connecting via HTTPs. + UnixSocket string `toml:"unix_socket_path" json:"unix-socket-path"` + + // Project name is the name of the project in which this runner will create + // instances. If this option is not set, the default project will be used. + // The project used here, must have all required profiles created by you + // beforehand. For LXD, the "flavor" used in the runner definition for a pool + // equates to a profile in the desired project. + ProjectName string `toml:"project_name" json:"project-name"` + + // IncludeDefaultProfile specifies whether or not this provider will always add + // the "default" profile to any newly created instance. + IncludeDefaultProfile bool `toml:"include_default_profile" json:"include-default-profile"` + + // URL holds the IP address. + URL string `toml:"address" json:"address"` + // ClientCertificate is the x509 client certificate path used for authentication. + ClientCertificate string `toml:"client_certificate" json:"client_certificate"` + // ClientKey is the key used for client certificate authentication. + ClientKey string `toml:"client_key" json:"client-key"` + // TLS certificate of the remote server. If not specified, the system CA is used. + TLSServerCert string `toml:"tls_server_certificate" json:"tls-server-certificate"` + // TLSCA is the TLS CA certificate when running LXD in PKI mode. + TLSCA string `toml:"tls_ca" json:"tls-ca"` + + // ImageRemotes is a map to a set of remote image repositories we can use to + // download images. + ImageRemotes map[string]LXDRemote `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"` +} + +func (l *LXD) Validate() error { + if l.UnixSocket != "" { + if _, err := os.Stat(l.UnixSocket); err != nil { + return fmt.Errorf("could not access unix socket %s: %q", l.UnixSocket, err) + } + + return nil + } + + if l.URL == "" { + return fmt.Errorf("unix_socket or address must be specified") + } + + if _, err := url.Parse(l.URL); err != nil { + return fmt.Errorf("invalid LXD URL") + } + + if l.ClientCertificate == "" || l.ClientKey == "" { + return fmt.Errorf("client_certificate and client_key are mandatory when connecting via HTTPs") + } + + if _, err := os.Stat(l.ClientCertificate); err != nil { + return fmt.Errorf("failed to access client certificate %s: %q", l.ClientCertificate, err) + } + + if _, err := os.Stat(l.ClientKey); err != nil { + return fmt.Errorf("failed to access client key %s: %q", l.ClientKey, err) + } + + if l.TLSServerCert != "" { + if _, err := os.Stat(l.TLSServerCert); err != nil { + return fmt.Errorf("failed to access tls_server_certificate %s: %q", l.TLSServerCert, err) + } + } + + for name, val := range l.ImageRemotes { + if err := val.Validate(); err != nil { + return fmt.Errorf("remote %s is invalid: %s", name, err) + } + } + return nil +} diff --git a/runner/providers/lxd/images.go b/runner/providers/lxd/images.go new file mode 100644 index 00000000..9a8ebb71 --- /dev/null +++ b/runner/providers/lxd/images.go @@ -0,0 +1,142 @@ +package lxd + +import ( + "fmt" + "strings" + + "runner-manager/config" + runnerErrors "runner-manager/errors" + + 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.LXDRemote + + cli lxd.InstanceServer +} + +// parseImageName parses the image name that comes in from the config and returns a +// remote. If no remote is configured with the given name, an error is returned. +func (i *image) parseImageName(imageName string) (config.LXDRemote, string, error) { + if !strings.Contains(imageName, ":") { + return config.LXDRemote{}, "", fmt.Errorf("image does not include a remote") + } + + details := strings.SplitN(imageName, ":", 2) + for remoteName, val := range i.remotes { + if remoteName == details[0] { + return val, details[1], nil + } + } + return config.LXDRemote{}, "", runnerErrors.ErrNotFound +} + +func (i *image) getLocalImageByAlias(imageName string, imageType config.LXDImageType, arch string) (*api.Image, error) { + aliases, err := i.cli.GetImageAliasArchitectures(imageType.String(), imageName) + if err != nil { + return nil, errors.Wrapf(err, "resolving alias: %s", imageName) + } + + 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) + } + + image, _, err := i.cli.GetImage(alias.Target) + if err != nil { + return nil, errors.Wrap(err, "fetching image details") + } + return image, nil +} + +func (i *image) clientFromRemoteArgs(remote config.LXDRemote) (lxd.ImageServer, error) { + connectArgs := &lxd.ConnectionArgs{ + InsecureSkipVerify: remote.InsecureSkipVerify, + } + d, err := lxd.ConnectSimpleStreams(remote.Address, connectArgs) + if err != nil { + return nil, errors.Wrapf(err, "connecting to image server %s", remote.Address) + } + return d, nil +} + +func (i *image) copyImageFromRemote(remote config.LXDRemote, imageName string, imageType config.LXDImageType, arch string) (*api.Image, error) { + imgCli, err := i.clientFromRemoteArgs(remote) + if err != nil { + return nil, errors.Wrap(err, "fetching image server client") + } + defer imgCli.Disconnect() + + aliases, err := imgCli.GetImageAliasArchitectures(imageType.String(), imageName) + if err != nil { + 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) + } + + image, _, err := imgCli.GetImage(alias.Target) + if err != nil { + return nil, errors.Wrap(err, "fetching image details") + } + + // Ask LXD to copy the image from the remote server + imgCopyArgs := &lxd.ImageCopyArgs{ + AutoUpdate: true, + CopyAliases: true, + } + op, err := i.cli.CopyImage(imgCli, *image, imgCopyArgs) + if err != nil { + return nil, errors.Wrapf(err, "copying image %s from %s", imageName, remote.Address) + } + + // And wait for it to finish + err = op.Wait() + if err != nil { + return nil, errors.Wrap(err, "waiting for image copy operation") + } + + // 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) +} + +// 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) { + 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) + } + + remote, parsedName, err := i.parseImageName(imageName) + if err != nil { + return nil, errors.Wrap(err, "parsing image name") + } + + if img, err := i.getLocalImageByAlias(parsedName, imageType, arch); err == nil { + return img, nil + } + + img, err := i.copyImageFromRemote(remote, parsedName, imageType, arch) + if err != nil { + return nil, errors.Wrap(err, "fetching image") + } + return img, nil +} diff --git a/runner/providers/lxd/lxd.go b/runner/providers/lxd/lxd.go index 94dc8482..6e2247dc 100644 --- a/runner/providers/lxd/lxd.go +++ b/runner/providers/lxd/lxd.go @@ -2,8 +2,8 @@ package lxd import ( "context" - "encoding/json" "fmt" + "io/ioutil" "strings" "runner-manager/cloudconfig" @@ -18,14 +18,29 @@ import ( "github.com/lxc/lxd/shared/api" "github.com/pborman/uuid" "github.com/pkg/errors" + "gopkg.in/yaml.v3" ) var _ common.Provider = &LXD{} var ( - archMap map[string]string = map[string]string{ - "x86_64": "x64", - "amd64": "x64", + // lxdToGithubArchMap translates LXD architectures to Github tools architectures. + // TODO: move this in a separate package. This will most likely be used + // by any other provider. + lxdToGithubArchMap map[string]string = map[string]string{ + "x86_64": "x64", + "amd64": "x64", + "armv7l": "arm", + "aarch64": "arm64", + "x64": "x64", + "arm": "arm", + "arm64": "arm64", + } + + configToLXDArchMap map[config.OSArch]string = map[config.OSArch]string{ + config.Amd64: "x86_64", + config.Arm64: "aarch64", + config.Arm: "armv7l", } ) @@ -36,22 +51,46 @@ const ( func getClientFromConfig(ctx context.Context, cfg *config.LXD) (cli lxd.InstanceServer, err error) { if cfg.UnixSocket != "" { - cli, err = lxd.ConnectLXDUnixWithContext(ctx, cfg.UnixSocket, nil) - } else { - connectArgs := lxd.ConnectionArgs{ - TLSServerCert: cfg.TLSServerCert, - TLSCA: cfg.TLSCA, - TLSClientCert: cfg.ClientCertificate, - TLSClientKey: cfg.ClientKey, + return lxd.ConnectLXDUnixWithContext(ctx, cfg.UnixSocket, nil) + } + + var srvCrtContents, tlsCAContents, clientCertContents, clientKeyContents []byte + + if cfg.TLSServerCert != "" { + srvCrtContents, err = ioutil.ReadFile(cfg.TLSServerCert) + if err != nil { + return nil, errors.Wrap(err, "reading TLSServerCert") } - cli, err = lxd.ConnectLXD(cfg.URL, &connectArgs) } - if err != nil { - return nil, errors.Wrap(err, "connecting to LXD") + if cfg.TLSCA != "" { + tlsCAContents, err = ioutil.ReadFile(cfg.TLSCA) + if err != nil { + return nil, errors.Wrap(err, "reading TLSCA") + } } - return cli, nil + if cfg.ClientCertificate != "" { + clientCertContents, err = ioutil.ReadFile(cfg.ClientCertificate) + if err != nil { + return nil, errors.Wrap(err, "reading ClientCertificate") + } + } + + if cfg.ClientKey != "" { + clientKeyContents, err = ioutil.ReadFile(cfg.ClientKey) + if err != nil { + return nil, errors.Wrap(err, "reading ClientKey") + } + } + + connectArgs := lxd.ConnectionArgs{ + TLSServerCert: string(srvCrtContents), + TLSCA: string(tlsCAContents), + TLSClientCert: string(clientCertContents), + TLSClientKey: string(clientKeyContents), + } + return lxd.ConnectLXD(cfg.URL, &connectArgs) } func projectName(cfg config.LXD) string { @@ -93,6 +132,10 @@ func NewProvider(ctx context.Context, cfg *config.Provider, pool *config.Pool) ( cfg: cfg, pool: pool, cli: cli, + imageManager: &image{ + cli: cli, + remotes: cfg.LXD.ImageRemotes, + }, } return provider, nil @@ -108,6 +151,8 @@ type LXD struct { ctx context.Context // cli is the LXD client. cli lxd.InstanceServer + // imageManager downloads images from remotes + imageManager *image } func (l *LXD) getProfiles(runner config.Runner) ([]string, error) { @@ -134,20 +179,6 @@ func (l *LXD) getProfiles(runner config.Runner) ([]string, error) { return ret, nil } -// TODO: Add image details cache. Avoid doing a request if not necessary. -func (l *LXD) getImageDetails(runner config.Runner) (*api.Image, error) { - alias, _, err := l.cli.GetImageAlias(runner.Image) - if err != nil { - return nil, errors.Wrapf(err, "resolving alias: %s", runner.Image) - } - - image, _, err := l.cli.GetImage(alias.Target) - if err != nil { - return nil, errors.Wrap(err, "fetching image details") - } - return image, nil -} - func (l *LXD) getCloudConfig(runner config.Runner, bootstrapParams params.BootstrapInstance, tools github.RunnerApplicationDownload, runnerName string) (string, error) { cloudCfg := cloudconfig.NewDefaultCloudInitConfig() @@ -168,8 +199,9 @@ func (l *LXD) getCloudConfig(runner config.Runner, bootstrapParams params.Bootst } cloudCfg.AddSSHKey(bootstrapParams.SSHKeys...) - cloudCfg.AddFile(installScript, "/var/run/install_runner.sh", "root:root", "755") - cloudCfg.AddRunCmd("/var/run/install_runner.sh") + cloudCfg.AddFile(installScript, "/install_runner.sh", "root:root", "755") + cloudCfg.AddRunCmd("/install_runner.sh") + cloudCfg.AddRunCmd("rm -f /install_runner.sh") asStr, err := cloudCfg.Serialize() if err != nil { @@ -192,12 +224,14 @@ func (l *LXD) getTools(image *api.Image, tools []*github.RunnerApplicationDownlo return github.RunnerApplicationDownload{}, errors.Wrap(err, "fetching OS type") } + // Validate image OS. Linux only for now. switch osType { case config.Linux: default: return github.RunnerApplicationDownload{}, fmt.Errorf("this provider does not support OS type: %s", osType) } + // Find tools for OS/Arch. for _, tool := range tools { if tool == nil { continue @@ -212,14 +246,33 @@ func (l *LXD) getTools(image *api.Image, tools []*github.RunnerApplicationDownlo return *tool, nil } - arch, ok := archMap[image.Architecture] - if ok && arch == *tool.Architecture { + arch, ok := lxdToGithubArchMap[image.Architecture] + if ok && arch == *tool.Architecture && *tool.OS == string(osType) { return *tool, nil } } return github.RunnerApplicationDownload{}, fmt.Errorf("failed to find tools for OS %s and arch %s", osType, image.Architecture) } +func (l *LXD) resolveArchitecture(runner config.Runner) (string, error) { + if string(runner.OSArch) == "" { + return configToLXDArchMap[config.Amd64], nil + } + arch, ok := configToLXDArchMap[runner.OSArch] + if !ok { + return "", fmt.Errorf("architecture %s is not supported", runner.OSArch) + } + return arch, nil +} + +// sadly, the security.secureboot flag is a string encoded boolean. +func (l *LXD) secureBootEnabled() string { + if l.cfg.LXD.SecureBoot { + return "true" + } + return "false" +} + func (l *LXD) getCreateInstanceArgs(bootstrapParams params.BootstrapInstance) (api.InstancesPost, error) { name := fmt.Sprintf("runner-manager-%s", uuid.New()) runner, err := util.FindRunnerType(bootstrapParams.RunnerType, l.pool.Runners) @@ -233,7 +286,12 @@ func (l *LXD) getCreateInstanceArgs(bootstrapParams params.BootstrapInstance) (a return api.InstancesPost{}, errors.Wrap(err, "fetching profiles") } - image, err := l.getImageDetails(runner) + arch, err := l.resolveArchitecture(runner) + if err != nil { + return api.InstancesPost{}, errors.Wrap(err, "fetching archictecture") + } + + image, err := l.imageManager.EnsureImage(runner.Image, config.LXDImageVirtualMachine, arch) if err != nil { return api.InstancesPost{}, errors.Wrap(err, "getting image details") } @@ -254,12 +312,13 @@ func (l *LXD) getCreateInstanceArgs(bootstrapParams params.BootstrapInstance) (a Profiles: profiles, Description: "Github runner provisioned by runner-manager", Config: map[string]string{ - "user.user-data": cloudCfg, + "user.user-data": cloudCfg, + "security.secureboot": l.secureBootEnabled(), }, }, Source: api.InstanceSource{ - Type: "image", - Alias: runner.Image, + Type: "image", + Fingerprint: image.Fingerprint, }, Name: name, Type: api.InstanceTypeVM, @@ -267,6 +326,38 @@ func (l *LXD) getCreateInstanceArgs(bootstrapParams params.BootstrapInstance) (a return args, nil } +func (l *LXD) launchInstance(createArgs api.InstancesPost) error { + // Get LXD to create the instance (background operation) + op, err := l.cli.CreateInstance(createArgs) + if err != nil { + return errors.Wrap(err, "creating instance") + } + + // Wait for the operation to complete + err = op.Wait() + if err != nil { + return errors.Wrap(err, "waiting for instance creation") + } + + // Get LXD to start the instance (background operation) + reqState := api.InstanceStatePut{ + Action: "start", + Timeout: -1, + } + + op, err = l.cli.UpdateInstanceState(createArgs.Name, reqState, "") + if err != nil { + return errors.Wrap(err, "starting instance") + } + + // Wait for the operation to complete + err = op.Wait() + if err != nil { + return errors.Wrap(err, "waiting for instance to start") + } + return nil +} + // CreateInstance creates a new compute instance in the provider. func (l *LXD) CreateInstance(ctx context.Context, bootstrapParams params.BootstrapInstance) error { args, err := l.getCreateInstanceArgs(bootstrapParams) @@ -274,9 +365,9 @@ func (l *LXD) CreateInstance(ctx context.Context, bootstrapParams params.Bootstr return errors.Wrap(err, "fetching create args") } - asJs, err := json.MarshalIndent(args, "", " ") + asJs, err := yaml.Marshal(args) fmt.Println(string(asJs), err) - return nil + return l.launchInstance(args) } // Delete instance will delete the instance in a provider. diff --git a/testdata/config.toml b/testdata/config.toml index 0994c10d..3fdb2f8e 100644 --- a/testdata/config.toml +++ b/testdata/config.toml @@ -43,11 +43,29 @@ [provider.lxd] unix_socket_path = "/var/snap/lxd/common/lxd/unix.socket" include_default_profile = false + secure_boot = false project_name = "github" address = "" client_certificate = "" client_key = "" tls_server_certificate = "" + [provider.lxd.image_remotes] + [provider.lxd.image_remotes.ubuntu] + addr = "https://cloud-images.ubuntu.com/releases" + public = true + protocol = "simplestreams" + skip_verify = false + [provider.lxd.image_remotes.ubuntu_daily] + addr = "https://cloud-images.ubuntu.com/daily" + public = true + protocol = "simplestreams" + skip_verify = false + [provider.lxd.image_remotes.images] + addr = "https://images.linuxcontainers.org" + public = true + protocol = "simplestreams" + skip_verify = false + [github] oauth2_token = "super secret" diff --git a/util/util.go b/util/util.go index ec05d203..df7643b8 100644 --- a/util/util.go +++ b/util/util.go @@ -30,6 +30,7 @@ var ( "suse": config.Linux, "fedora": config.Linux, "flatcar": config.Linux, + "gentoo": config.Linux, "windows": config.Windows, } )