LXD provider can create workers
The bare minimum needed code to successfully create an instance that installs and launches a runner is there.
This commit is contained in:
parent
eb28542110
commit
d68b842375
9 changed files with 438 additions and 117 deletions
|
|
@ -16,6 +16,7 @@ func NewDefaultCloudInitConfig() *CloudInit {
|
|||
PackageUpgrade: true,
|
||||
Packages: []string{
|
||||
"curl",
|
||||
"tar",
|
||||
},
|
||||
SystemInfo: &SystemInfo{
|
||||
DefaultUser: DefaultUser{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
129
config/lxd.go
Normal file
129
config/lxd.go
Normal file
|
|
@ -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
|
||||
}
|
||||
142
runner/providers/lxd/images.go
Normal file
142
runner/providers/lxd/images.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
18
testdata/config.toml
vendored
18
testdata/config.toml
vendored
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ var (
|
|||
"suse": config.Linux,
|
||||
"fedora": config.Linux,
|
||||
"flatcar": config.Linux,
|
||||
"gentoo": config.Linux,
|
||||
"windows": config.Windows,
|
||||
}
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue