From f75ce3bf573b666948a126b2af95c23441d78aab Mon Sep 17 00:00:00 2001 From: Mathieu Tortuyaux Date: Thu, 28 Nov 2024 11:36:44 +0100 Subject: [PATCH] platform: implement the kola elements for Akamai Signed-off-by: Mathieu Tortuyaux --- cmd/kola/options.go | 9 ++- kola/harness.go | 5 ++ platform/machine/akamai/cluster.go | 92 ++++++++++++++++++++++++++++ platform/machine/akamai/flight.go | 69 +++++++++++++++++++++ platform/machine/akamai/machine.go | 96 ++++++++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 platform/machine/akamai/cluster.go create mode 100644 platform/machine/akamai/flight.go create mode 100644 platform/machine/akamai/machine.go diff --git a/cmd/kola/options.go b/cmd/kola/options.go index 5f94bb5bd..c863028d0 100644 --- a/cmd/kola/options.go +++ b/cmd/kola/options.go @@ -40,7 +40,7 @@ var ( kolaOffering string defaultTargetBoard = sdk.DefaultBoard() kolaArchitectures = []string{"amd64"} - kolaPlatforms = []string{"aws", "azure", "brightbox", "do", "esx", "external", "gce", "hetzner", "openstack", "equinixmetal", "qemu", "qemu-unpriv", "scaleway"} + kolaPlatforms = []string{"akamai", "aws", "azure", "brightbox", "do", "esx", "external", "gce", "hetzner", "openstack", "equinixmetal", "qemu", "qemu-unpriv", "scaleway"} kolaDistros = []string{"cl", "fcos", "rhcos"} kolaChannels = []string{"alpha", "beta", "stable", "edge", "lts"} kolaOfferings = []string{"basic", "pro"} @@ -253,6 +253,12 @@ func init() { sv(&kola.HetznerOptions.Location, "hetzner-location", "fsn1", "Hetzner location name") sv(&kola.HetznerOptions.Image, "hetzner-image", "", "Hetzner image ID") sv(&kola.HetznerOptions.ServerType, "hetzner-server-type", "cx22", "Hetzner instance type") + + // Akamai specific options + sv(&kola.AkamaiOptions.Token, "akamai-token", "", "Akamai access token") + sv(&kola.AkamaiOptions.Image, "akamai-image", "", "Akamai image ID") + sv(&kola.AkamaiOptions.Region, "akamai-region", "", "Akamai region") + sv(&kola.AkamaiOptions.Type, "akamai-type", "g6-nanode-1", "Akamai instance type") } // Sync up the command line options if there is dependency @@ -274,6 +280,7 @@ func syncOptions() error { kola.BrightboxOptions.Board = board kola.ScalewayOptions.Board = board kola.HetznerOptions.Board = board + kola.AkamaiOptions.Board = board validateOption := func(name, item string, valid []string) error { for _, v := range valid { diff --git a/kola/harness.go b/kola/harness.go index d8df8a5de..9a015d88d 100644 --- a/kola/harness.go +++ b/kola/harness.go @@ -38,6 +38,7 @@ import ( "github.com/flatcar/mantle/kola/register" "github.com/flatcar/mantle/kola/torcx" "github.com/flatcar/mantle/platform" + akamaiapi "github.com/flatcar/mantle/platform/api/akamai" awsapi "github.com/flatcar/mantle/platform/api/aws" azureapi "github.com/flatcar/mantle/platform/api/azure" brightboxapi "github.com/flatcar/mantle/platform/api/brightbox" @@ -49,6 +50,7 @@ import ( openstackapi "github.com/flatcar/mantle/platform/api/openstack" scalewayapi "github.com/flatcar/mantle/platform/api/scaleway" "github.com/flatcar/mantle/platform/conf" + "github.com/flatcar/mantle/platform/machine/akamai" "github.com/flatcar/mantle/platform/machine/aws" "github.com/flatcar/mantle/platform/machine/azure" "github.com/flatcar/mantle/platform/machine/brightbox" @@ -69,6 +71,7 @@ var ( plog = capnslog.NewPackageLogger("github.com/flatcar/mantle", "kola") Options = platform.Options{} + AkamaiOptions = akamaiapi.Options{Options: &Options} // glue to set platform options from main AWSOptions = awsapi.Options{Options: &Options} // glue to set platform options from main AzureOptions = azureapi.Options{Options: &Options} // glue to set platform options from main BrightboxOptions = brightboxapi.Options{Options: &Options} // glue to set platform options from main @@ -234,6 +237,8 @@ type NativeRunner func(funcName string, m platform.Machine) error func NewFlight(pltfrm string) (flight platform.Flight, err error) { switch pltfrm { + case "akamai": + flight, err = akamai.NewFlight(&AkamaiOptions) case "aws": flight, err = aws.NewFlight(&AWSOptions) case "azure": diff --git a/platform/machine/akamai/cluster.go b/platform/machine/akamai/cluster.go new file mode 100644 index 000000000..f5b02719e --- /dev/null +++ b/platform/machine/akamai/cluster.go @@ -0,0 +1,92 @@ +// Copyright The Mantle Authors. +// SPDX-License-Identifier: Apache-2.0 + +package akamai + +import ( + "context" + "crypto/rand" + "fmt" + "os" + "path/filepath" + + "github.com/flatcar/mantle/platform" + "github.com/flatcar/mantle/platform/conf" +) + +type cluster struct { + *platform.BaseCluster + flight *flight +} + +func (bc *cluster) NewMachine(userdata *conf.UserData) (platform.Machine, error) { + conf, err := bc.RenderUserData(userdata, map[string]string{ + "$public_ipv4": "${COREOS_CUSTOM_PUBLIC_IPV4}", + "$private_ipv4": "${COREOS_CUSTOM_PRIVATE_IPV4}", + }) + if err != nil { + return nil, err + } + + // Hack to workaround CT inheritance. + // Can be dropped once we remove CT dependency. + // https://github.com/flatcar/Flatcar/issues/1386 + conf.AddSystemdUnitDropin("coreos-metadata.service", "00-custom-metadata.conf", `[Service] +ExecStartPost=/usr/bin/sed -i "s/AKAMAI/CUSTOM/" /run/metadata/flatcar +ExecStartPost=/usr/bin/sed -i "s/PRIVATE_IPV4_0/PRIVATE_IPV4/" /run/metadata/flatcar +ExecStartPost=/usr/bin/sed -i "s/PUBLIC_IPV4_0/PUBLIC_IPV4/" /run/metadata/flatcar +ExecStartPost=/usr/bin/sed -i "s#/32##" /run/metadata/flatcar +`) + + instance, err := bc.flight.api.CreateServer(context.TODO(), bc.vmname(), conf.String()) + if err != nil { + return nil, err + } + + mach := &machine{ + cluster: bc, + mach: instance, + } + + // machine to destroy + m := mach + defer func() { + if m != nil { + m.Destroy() + } + }() + + mach.dir = filepath.Join(bc.RuntimeConf().OutputDir, mach.ID()) + if err := os.Mkdir(mach.dir, 0777); err != nil { + return nil, err + } + + confPath := filepath.Join(mach.dir, "ignition.json") + if err := conf.WriteFile(confPath); err != nil { + return nil, err + } + + if mach.journal, err = platform.NewJournal(mach.dir); err != nil { + return nil, err + } + + if err := platform.StartMachine(mach, mach.journal); err != nil { + return nil, err + } + + m = nil + bc.AddMach(mach) + + return mach, nil +} + +func (bc *cluster) vmname() string { + b := make([]byte, 5) + rand.Read(b) + return fmt.Sprintf("%s-%x", bc.Name()[0:13], b) +} + +func (bc *cluster) Destroy() { + bc.BaseCluster.Destroy() + bc.flight.DelCluster(bc) +} diff --git a/platform/machine/akamai/flight.go b/platform/machine/akamai/flight.go new file mode 100644 index 000000000..ac9ac858a --- /dev/null +++ b/platform/machine/akamai/flight.go @@ -0,0 +1,69 @@ +// Copyright The Mantle Authors. +// SPDX-License-Identifier: Apache-2.0 + +package akamai + +import ( + "fmt" + + "github.com/coreos/pkg/capnslog" + ctplatform "github.com/flatcar/container-linux-config-transpiler/config/platform" + + "github.com/flatcar/mantle/platform" + "github.com/flatcar/mantle/platform/api/akamai" +) + +const ( + Platform platform.Name = "akamai" +) + +var ( + plog = capnslog.NewPackageLogger("github.com/flatcar/mantle", "platform/machine/akamai") +) + +type flight struct { + *platform.BaseFlight + api *akamai.API +} + +func NewFlight(opts *akamai.Options) (platform.Flight, error) { + api, err := akamai.New(opts) + if err != nil { + return nil, fmt.Errorf("creating akamai API client: %w", err) + } + + // TODO: Rework the Base Flight to remove the CT dependency. + base, err := platform.NewBaseFlight(opts.Options, Platform, ctplatform.Custom) + if err != nil { + return nil, fmt.Errorf("creating base flight: %w", err) + } + + bf := &flight{ + BaseFlight: base, + api: api, + } + + return bf, nil +} + +// NewCluster creates an instance of a Cluster suitable for spawning +// instances on the Akamai platform. +func (bf *flight) NewCluster(rconf *platform.RuntimeConfig) (platform.Cluster, error) { + bc, err := platform.NewBaseCluster(bf.BaseFlight, rconf) + if err != nil { + return nil, fmt.Errorf("creating akamai base cluster: %w", err) + } + + c := &cluster{ + BaseCluster: bc, + flight: bf, + } + + bf.AddCluster(c) + + return c, nil +} + +func (bf *flight) Destroy() { + bf.BaseFlight.Destroy() +} diff --git a/platform/machine/akamai/machine.go b/platform/machine/akamai/machine.go new file mode 100644 index 000000000..fc3c9983b --- /dev/null +++ b/platform/machine/akamai/machine.go @@ -0,0 +1,96 @@ +// Copyright The Mantle Authors. +// SPDX-License-Identifier: Apache-2.0 + +package akamai + +import ( + "context" + "strconv" + + "golang.org/x/crypto/ssh" + + "github.com/flatcar/mantle/platform" + "github.com/flatcar/mantle/platform/api/akamai" +) + +type machine struct { + cluster *cluster + mach *akamai.Server + dir string + journal *platform.Journal + console string +} + +// ID returns the ID of the machine. +func (bm *machine) ID() string { + return strconv.Itoa(bm.mach.Instance.ID) +} + +// IP returns the IP of the machine. +func (bm *machine) IP() string { + if len(bm.mach.Instance.IPv4) > 0 { + return bm.mach.Instance.IPv4[0].String() + } + + return "" +} + +// IP returns the private IP of the machine. +func (bm *machine) PrivateIP() string { + return "" +} + +// RuntimeConf returns the runtime configuration of the cluster. +func (bm *machine) RuntimeConf() *platform.RuntimeConfig { + return bm.cluster.RuntimeConf() +} + +func (bm *machine) SSHClient() (*ssh.Client, error) { + return bm.cluster.SSHClient(bm.IP()) +} + +func (bm *machine) PasswordSSHClient(user string, password string) (*ssh.Client, error) { + return bm.cluster.PasswordSSHClient(bm.IP(), user, password) +} + +func (bm *machine) SSH(cmd string) ([]byte, []byte, error) { + return bm.cluster.SSH(bm, cmd) +} + +func (bm *machine) Reboot() error { + return platform.RebootMachine(bm, bm.journal) +} + +func (bm *machine) Destroy() { + // TODO: Add "saveConsole" logic here when Akamai API will support fetching the console output. + + if err := bm.cluster.flight.api.DeleteServer(context.TODO(), bm.ID()); err != nil { + plog.Errorf("deleting server %v: %v", bm.ID(), err) + } + + if bm.journal != nil { + bm.journal.Destroy() + } + + bm.cluster.DelMach(bm) +} + +func (bm *machine) ConsoleOutput() string { + return bm.console +} + +func (bm *machine) JournalOutput() string { + if bm.journal == nil { + return "" + } + + data, err := bm.journal.Read() + if err != nil { + plog.Errorf("Reading journal for instance %v: %v", bm.ID(), err) + } + return string(data) +} + +func (bm *machine) Board() string { + return bm.cluster.flight.Options().Board +}