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:
Gabriel Adrian Samfira 2022-04-19 14:42:10 +00:00
parent eb28542110
commit d68b842375
9 changed files with 438 additions and 117 deletions

View file

@ -16,6 +16,7 @@ func NewDefaultCloudInitConfig() *CloudInit {
PackageUpgrade: true,
Packages: []string{
"curl",
"tar",
},
SystemInfo: &SystemInfo{
DefaultUser: DefaultUser{

View file

@ -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 {

View file

@ -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)

View file

@ -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
View 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
}

View 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
}

View file

@ -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
View file

@ -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"

View file

@ -30,6 +30,7 @@ var (
"suse": config.Linux,
"fedora": config.Linux,
"flatcar": config.Linux,
"gentoo": config.Linux,
"windows": config.Windows,
}
)