From e5cfd978931d3a44988be8791ac586b3ee5bee1c Mon Sep 17 00:00:00 2001 From: Andreas Fritzler Date: Mon, 9 Dec 2024 14:57:46 +0100 Subject: [PATCH] Add `metalctl console` subcommand - Add a `metalctl console` subcommand with which you are able to connect to a `Servers` serial console - Factor out `bmcutils` into an own package --- cmd/metalctl/app/app.go | 1 + cmd/metalctl/app/console.go | 174 ++++++++++++++++++ go.mod | 3 +- go.sum | 6 +- internal/{controller => bmcutils}/bmcutils.go | 65 +++++-- internal/console/console.go | 63 +++++++ internal/console/console_test.go | 111 +++++++++++ internal/console/suite_test.go | 142 ++++++++++++++ internal/controller/bmc_controller.go | 7 +- internal/controller/bmc_controller_test.go | 5 +- internal/controller/endpoint_controller.go | 3 +- internal/controller/server_controller.go | 16 +- 12 files changed, 565 insertions(+), 31 deletions(-) create mode 100644 cmd/metalctl/app/console.go rename internal/{controller => bmcutils}/bmcutils.go (79%) create mode 100644 internal/console/console.go create mode 100644 internal/console/console_test.go create mode 100644 internal/console/suite_test.go diff --git a/cmd/metalctl/app/app.go b/cmd/metalctl/app/app.go index 46d1ce3..f3bc3fa 100644 --- a/cmd/metalctl/app/app.go +++ b/cmd/metalctl/app/app.go @@ -30,5 +30,6 @@ func NewCommand() *cobra.Command { Args: cobra.NoArgs, } root.AddCommand(NewMoveCommand()) + root.AddCommand(NewConsoleCommand()) return root } diff --git a/cmd/metalctl/app/console.go b/cmd/metalctl/app/console.go new file mode 100644 index 0000000..2c3dbd9 --- /dev/null +++ b/cmd/metalctl/app/console.go @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package app + +import ( + "context" + "fmt" + "io" + "log" + "log/slog" + "os" + + "github.com/ironcore-dev/metal-operator/internal/console" + + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +var ( + kubeconfigPath string + kubeconfig string + serialConsoleNumber int +) + +func NewConsoleCommand() *cobra.Command { + consoleCmd := &cobra.Command{ + Use: "console", + Short: "Access the serial console of a Server", + RunE: runConsole, + } + + consoleCmd.Flags().StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig.") + consoleCmd.Flags().IntVar(&serialConsoleNumber, "serial-console-number", 1, "Serial console number.") + + if verbose { + slog.SetLogLoggerLevel(slog.LevelDebug) + } + return consoleCmd +} + +func runConsole(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("server name is required") + } + var serverName string + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + serverName = args[0] + + k8sClient, err := createClient() + if err != nil { + return err + } + + if err := openConsoleStream(cmd.Context(), k8sClient, serverName); err != nil { + return err + } + + return nil +} + +func openConsoleStream(ctx context.Context, k8sClient client.Client, serverName string) error { + consoleConfig, err := console.GetConfigForServerName(ctx, k8sClient, serverName) + if err != nil { + return fmt.Errorf("failed to get console config: %w", err) + } + if consoleConfig == nil { + return fmt.Errorf("console config is nil") + } + + // Create SSH client configuration + sshConfig := &ssh.ClientConfig{ + User: consoleConfig.Username, + Auth: []ssh.AuthMethod{ + ssh.Password(consoleConfig.Password), + }, + // TODO: use proper key verification + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + // Connect to the BMC + conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:22", consoleConfig.BMCAddress), sshConfig) + if err != nil { + return fmt.Errorf("failed to connect to BMC: %w", err) + } + defer func(conn *ssh.Client) { + if err = conn.Close(); err != nil { + log.Printf("failed to close SSH connection: %v", err) + } + }(conn) + + // Start a session + session, err := conn.NewSession() + if err != nil { + return fmt.Errorf("failed to create SSH session: %w", err) + } + defer func(session *ssh.Session) { + if err = session.Close(); err != nil { + log.Printf("failed to close SSH session: %v", err) + } + }(session) + + // Request a pseudo-terminal for interactive sessions + if err = session.RequestPty("xterm", 80, 40, ssh.TerminalModes{ + ssh.ECHO: 0, // Disable echo + ssh.TTY_OP_ISPEED: 14400, // Input speed + ssh.TTY_OP_OSPEED: 14400, // Output speed + }); err != nil { + return fmt.Errorf("failed to request pseudo-terminal failed: %w", err) + } + + // Start the SOL session + stdin, err := session.StdinPipe() + if err != nil { + return fmt.Errorf("could not get stdin pipe: %w", err) + } + stdout, err := session.StdoutPipe() + if err != nil { + return fmt.Errorf("could not get stdout pipe: %w", err) + } + + go func() { + _, err = io.Copy(os.Stdout, stdout) + if err != nil { + log.Printf("failed to copy stdout: %s", err) + } + }() // Stream the SOL output to the terminal + + if err = session.Start(fmt.Sprintf("console %d", serialConsoleNumber)); err != nil { + return fmt.Errorf("failed to start SOL command: %w", err) + } + + log.Println("Serial-over-LAN session active. Press Ctrl+C to exit.") + go func() { + // Allow sending input to the session + _, err = io.Copy(stdin, os.Stdin) + if err != nil { + log.Printf("failed to copy stdin: %s", err) + } + }() + + // Wait for the session to end + if err := session.Wait(); err != nil { + return fmt.Errorf("error during SOL session: %v", err) + } + return nil +} + +func createClient() (client.Client, error) { + if kubeconfig != "" { + kubeconfigPath = kubeconfig + } else { + kubeconfigPath = os.Getenv("KUBECONFIG") + if kubeconfigPath == "" { + fmt.Println("Error: --kubeconfig flag or KUBECONFIG environment variable must be set") + os.Exit(1) + } + } + + clientConfig, err := config.GetConfigWithContext("") + if err != nil { + return nil, fmt.Errorf("failed getting client config: %w", err) + } + + k8sClient, err := client.New(clientConfig, client.Options{Scheme: scheme}) + if err != nil { + return nil, fmt.Errorf("failed creating controller-runtime client: %w", err) + } + return k8sClient, nil +} diff --git a/go.mod b/go.mod index 3b3f72a..8acbbc6 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,9 @@ require ( github.com/ironcore-dev/controller-utils v0.9.5 github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.0 - github.com/stmcginnis/gofish v0.20.0 github.com/spf13/cobra v1.8.1 + github.com/stmcginnis/gofish v0.20.0 + golang.org/x/crypto v0.28.0 k8s.io/api v0.31.1 k8s.io/apiextensions-apiserver v0.31.0 k8s.io/apimachinery v0.31.1 diff --git a/go.sum b/go.sum index 08db354..a2ab0d7 100644 --- a/go.sum +++ b/go.sum @@ -51,10 +51,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/ironcore-dev/controller-utils v0.9.5 h1:vdUnCNolC0uDMZy5d8Noib5kdqdK9YpelVSgPc30qTc= -github.com/ironcore-dev/controller-utils v0.9.5/go.mod h1:pzrmJmc6LXtn48cTAgKHm5i8ry6q1Qw7SUh0XaYfIP4= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/ironcore-dev/controller-utils v0.9.5 h1:vdUnCNolC0uDMZy5d8Noib5kdqdK9YpelVSgPc30qTc= +github.com/ironcore-dev/controller-utils v0.9.5/go.mod h1:pzrmJmc6LXtn48cTAgKHm5i8ry6q1Qw7SUh0XaYfIP4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -129,6 +129,8 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/internal/controller/bmcutils.go b/internal/bmcutils/bmcutils.go similarity index 79% rename from internal/controller/bmcutils.go rename to internal/bmcutils/bmcutils.go index aef8c9d..2949a0e 100644 --- a/internal/controller/bmcutils.go +++ b/internal/bmcutils/bmcutils.go @@ -1,18 +1,66 @@ // SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors // SPDX-License-Identifier: Apache-2.0 -package controller +package bmcutils import ( "context" "fmt" "net" - metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" "github.com/ironcore-dev/metal-operator/bmc" + + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/client" ) +func GetBMCCredentialsFromSecret(secret *metalv1alpha1.BMCSecret) (string, string, error) { + // TODO: use constants for secret keys + username, ok := secret.Data["username"] + if !ok { + return "", "", fmt.Errorf("no username found in the BMC secret") + } + password, ok := secret.Data["password"] + if !ok { + return "", "", fmt.Errorf("no password found in the BMC secret") + } + return string(username), string(password), nil +} + +func GetBMCFromBMCName(ctx context.Context, c client.Client, bmcName string) (*metalv1alpha1.BMC, error) { + bmcObj := &metalv1alpha1.BMC{} + if err := c.Get(ctx, client.ObjectKey{Name: bmcName}, bmcObj); err != nil { + return nil, fmt.Errorf("failed to get bmc %q: %w", bmcName, err) + } + return bmcObj, nil +} + +func GetBMCCredentialsForBMCSecretName(ctx context.Context, c client.Client, bmcSecretName string) (string, string, error) { + bmcSecret := &metalv1alpha1.BMCSecret{} + if err := c.Get(ctx, client.ObjectKey{Name: bmcSecretName}, bmcSecret); err != nil { + return "", "", fmt.Errorf("failed to get bmc secret: %w", err) + } + return GetBMCCredentialsFromSecret(bmcSecret) +} + +func GetBMCAddressForBMC(ctx context.Context, c client.Client, bmcObj *metalv1alpha1.BMC) (string, error) { + var address string + + if bmcObj.Spec.EndpointRef != nil { + endpoint := &metalv1alpha1.Endpoint{} + if err := c.Get(ctx, client.ObjectKey{Name: bmcObj.Spec.EndpointRef.Name}, endpoint); err != nil { + return "", fmt.Errorf("failed to get Endpoints for BMC: %w", err) + } + return endpoint.Spec.IP.String(), nil + } + + if bmcObj.Spec.Endpoint != nil { + return bmcObj.Spec.Endpoint.IP.String(), nil + } + + return address, nil +} + const DefaultKubeNamespace = "default" func GetBMCClientForServer(ctx context.Context, c client.Client, server *metalv1alpha1.Server, insecure bool, options bmc.BMCOptions) (bmc.BMC, error) { @@ -124,19 +172,6 @@ func CreateBMCClient( return bmcClient, nil } -func GetBMCCredentialsFromSecret(secret *metalv1alpha1.BMCSecret) (string, string, error) { - // TODO: use constants for secret keys - username, ok := secret.Data["username"] - if !ok { - return "", "", fmt.Errorf("no username found in the BMC secret") - } - password, ok := secret.Data["password"] - if !ok { - return "", "", fmt.Errorf("no password found in the BMC secret") - } - return string(username), string(password), nil -} - func GetServerNameFromBMCandIndex(index int, bmc *metalv1alpha1.BMC) string { return fmt.Sprintf("%s-%s-%d", bmc.Name, "system", index) } diff --git a/internal/console/console.go b/internal/console/console.go new file mode 100644 index 0000000..a13307a --- /dev/null +++ b/internal/console/console.go @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package console + +import ( + "context" + "fmt" + + "github.com/ironcore-dev/metal-operator/api/v1alpha1" + "github.com/ironcore-dev/metal-operator/internal/bmcutils" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Config struct { + BMCAddress string + Username string + Password string +} + +func GetConfigForServerName(ctx context.Context, c client.Client, serverName string) (*Config, error) { + server := &v1alpha1.Server{} + if err := c.Get(ctx, client.ObjectKey{Name: serverName}, server); err != nil { + return nil, fmt.Errorf("failed to get server %q: %w", serverName, err) + } + + // Inline BMC configuration + if server.Spec.BMC != nil { + username, password, err := bmcutils.GetBMCCredentialsForBMCSecretName(ctx, c, server.Spec.BMC.BMCSecretRef.Name) + if err != nil { + return nil, err + } + return &Config{ + BMCAddress: server.Spec.BMC.Address, + Username: username, + Password: password, + }, nil + } + + // BMC by reference + if server.Spec.BMCRef != nil { + bmc, err := bmcutils.GetBMCFromBMCName(ctx, c, server.Spec.BMCRef.Name) + if err != nil { + return nil, err + } + username, password, err := bmcutils.GetBMCCredentialsForBMCSecretName(ctx, c, bmc.Spec.BMCSecretRef.Name) + if err != nil { + return nil, err + } + address, err := bmcutils.GetBMCAddressForBMC(ctx, c, bmc) + if err != nil { + return nil, err + } + + return &Config{ + BMCAddress: address, + Username: username, + Password: password, + }, nil + } + + return nil, nil +} diff --git a/internal/console/console_test.go b/internal/console/console_test.go new file mode 100644 index 0000000..b459a16 --- /dev/null +++ b/internal/console/console_test.go @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package console + +import ( + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = FDescribe("Console Access", func() { + _ = SetupTest() + + It("Should successfully construct console config for Server with inline configuration", func(ctx SpecContext) { + By("Creating a BMCSecret") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Data: map[string][]byte{ + "username": []byte("foo"), + "password": []byte("bar"), + }, + } + Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmcSecret) + + By("Creating a Server object") + server := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Spec: metalv1alpha1.ServerSpec{ + BMC: &metalv1alpha1.BMCAccess{ + Protocol: metalv1alpha1.Protocol{}, + Address: "10.0.0.1", + BMCSecretRef: corev1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, server)).To(Succeed()) + DeferCleanup(k8sClient.Delete, server) + + config, err := GetConfigForServerName(ctx, k8sClient, server.Name) + Expect(err).NotTo(HaveOccurred()) + Expect(config).To(Equal(&Config{ + BMCAddress: "10.0.0.1", + Username: "foo", + Password: "bar", + })) + }) + + It("Should successfully construct console config for Server with a BMC ref", func(ctx SpecContext) { + By("Creating a BMCSecret") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Data: map[string][]byte{ + "username": []byte("foo"), + "password": []byte("bar"), + }, + } + Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmcSecret) + + bmc := &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Spec: metalv1alpha1.BMCSpec{ + BMCSecretRef: corev1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + Endpoint: &metalv1alpha1.InlineEndpoint{ + MACAddress: "aa:bb:cc:dd", + IP: metalv1alpha1.MustParseIP("10.0.0.1"), + }, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + DeferCleanup(k8sClient.Delete, bmc) + + By("Creating a Server object") + server := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + Spec: metalv1alpha1.ServerSpec{ + BMCRef: &corev1.LocalObjectReference{ + Name: bmc.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, server)).To(Succeed()) + DeferCleanup(k8sClient.Delete, server) + + config, err := GetConfigForServerName(ctx, k8sClient, server.Name) + Expect(err).NotTo(HaveOccurred()) + Expect(config).To(Equal(&Config{ + BMCAddress: "10.0.0.1", + Username: "foo", + Password: "bar", + })) + }) +}) diff --git a/internal/console/suite_test.go b/internal/console/suite_test.go new file mode 100644 index 0000000..7b18bd5 --- /dev/null +++ b/internal/console/suite_test.go @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package console + +import ( + "context" + "fmt" + "path/filepath" + "runtime" + "testing" + "time" + + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" + "github.com/ironcore-dev/metal-operator/internal/registry" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/envtest" + . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + //+kubebuilder:scaffold:imports +) + +const ( + pollingInterval = 50 * time.Millisecond + eventuallyTimeout = 3 * time.Second + consistentlyDuration = 1 * time.Second +) + +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment +) + +func TestControllers(t *testing.T) { + SetDefaultConsistentlyPollingInterval(pollingInterval) + SetDefaultEventuallyPollingInterval(pollingInterval) + SetDefaultEventuallyTimeout(eventuallyTimeout) + SetDefaultConsistentlyDuration(consistentlyDuration) + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + DeferCleanup(testEnv.Stop) + + Expect(metalv1alpha1.AddToScheme(scheme.Scheme)).NotTo(HaveOccurred()) + + err = metalv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // set komega client + SetClient(k8sClient) + + By("Starting the registry server") + var mgrCtx context.Context + mgrCtx, cancel := context.WithCancel(context.Background()) + DeferCleanup(cancel) + registryServer := registry.NewServer(":30000") + go func() { + defer GinkgoRecover() + Expect(registryServer.Start(mgrCtx)).To(Succeed(), "failed to start registry server") + }() +}) + +func SetupTest() *corev1.Namespace { + ns := &corev1.Namespace{} + + BeforeEach(func(ctx SpecContext) { + var mgrCtx context.Context + mgrCtx, cancel := context.WithCancel(context.Background()) + DeferCleanup(cancel) + + *ns = corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-", + }, + } + Expect(k8sClient.Create(ctx, ns)).To(Succeed(), "failed to create test namespace") + DeferCleanup(k8sClient.Delete, ns) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + Controller: config.Controller{ + // need to skip unique controller name validation + // since all tests need a dedicated controller + SkipNameValidation: ptr.To(true), + }, + Metrics: metricsserver.Options{ + BindAddress: "0", + }, + }) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + Expect(k8sManager.Start(mgrCtx)).To(Succeed(), "failed to start manager") + }() + }) + + return ns +} diff --git a/internal/controller/bmc_controller.go b/internal/controller/bmc_controller.go index 8880d86..2731dbe 100644 --- a/internal/controller/bmc_controller.go +++ b/internal/controller/bmc_controller.go @@ -14,6 +14,7 @@ import ( "github.com/ironcore-dev/controller-utils/clientutils" metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" "github.com/ironcore-dev/metal-operator/bmc" + "github.com/ironcore-dev/metal-operator/internal/bmcutils" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -119,7 +120,7 @@ func (r *BMCReconciler) updateBMCStatusDetails(ctx context.Context, log logr.Log return fmt.Errorf("failed to patch IP and MAC address status: %w", err) } - bmcClient, err := GetBMCClientFromBMC(ctx, r.Client, bmcObj, r.Insecure, r.BMCPollingOptions) + bmcClient, err := bmcutils.GetBMCClientFromBMC(ctx, r.Client, bmcObj, r.Insecure, r.BMCPollingOptions) if err != nil { return fmt.Errorf("failed to create BMC client: %w", err) } @@ -149,7 +150,7 @@ func (r *BMCReconciler) updateBMCStatusDetails(ctx context.Context, log logr.Log } func (r *BMCReconciler) discoverServers(ctx context.Context, log logr.Logger, bmcObj *metalv1alpha1.BMC) error { - bmcClient, err := GetBMCClientFromBMC(ctx, r.Client, bmcObj, r.Insecure, r.BMCPollingOptions) + bmcClient, err := bmcutils.GetBMCClientFromBMC(ctx, r.Client, bmcObj, r.Insecure, r.BMCPollingOptions) if err != nil { return fmt.Errorf("failed to create BMC client: %w", err) } @@ -166,7 +167,7 @@ func (r *BMCReconciler) discoverServers(ctx context.Context, log logr.Logger, bm Kind: "Server", }, ObjectMeta: metav1.ObjectMeta{ - Name: GetServerNameFromBMCandIndex(i, bmcObj), + Name: bmcutils.GetServerNameFromBMCandIndex(i, bmcObj), }, Spec: metalv1alpha1.ServerSpec{ UUID: strings.ToLower(s.UUID), // always use lower-case uuids diff --git a/internal/controller/bmc_controller_test.go b/internal/controller/bmc_controller_test.go index 6c5c03d..75b40bc 100644 --- a/internal/controller/bmc_controller_test.go +++ b/internal/controller/bmc_controller_test.go @@ -5,6 +5,7 @@ package controller import ( metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" + "github.com/ironcore-dev/metal-operator/internal/bmcutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" @@ -51,7 +52,7 @@ var _ = Describe("BMC Controller", func() { By("Ensuring that the Server resource will be removed") server := &metalv1alpha1.Server{ ObjectMeta: metav1.ObjectMeta{ - Name: GetServerNameFromBMCandIndex(0, bmc), + Name: bmcutils.GetServerNameFromBMCandIndex(0, bmc), }, } DeferCleanup(k8sClient.Delete, server) @@ -138,7 +139,7 @@ var _ = Describe("BMC Controller", func() { By("Ensuring that the Server resource has been created") server := &metalv1alpha1.Server{ ObjectMeta: metav1.ObjectMeta{ - Name: GetServerNameFromBMCandIndex(0, bmc), + Name: bmcutils.GetServerNameFromBMCandIndex(0, bmc), }, } Eventually(Object(server)).Should(SatisfyAll( diff --git a/internal/controller/endpoint_controller.go b/internal/controller/endpoint_controller.go index e8b32a8..897c4da 100644 --- a/internal/controller/endpoint_controller.go +++ b/internal/controller/endpoint_controller.go @@ -14,6 +14,7 @@ import ( metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" "github.com/ironcore-dev/metal-operator/bmc" "github.com/ironcore-dev/metal-operator/internal/api/macdb" + "github.com/ironcore-dev/metal-operator/internal/bmcutils" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -135,7 +136,7 @@ func (r *EndpointReconciler) reconcile(ctx context.Context, log logr.Logger, end bmcClient, err := bmc.NewRedfishKubeBMCClient( ctx, bmcOptions, - r.Client, DefaultKubeNamespace) + r.Client, bmcutils.DefaultKubeNamespace) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to create BMC client: %w", err) } diff --git a/internal/controller/server_controller.go b/internal/controller/server_controller.go index 288fa87..dac5733 100644 --- a/internal/controller/server_controller.go +++ b/internal/controller/server_controller.go @@ -12,6 +12,8 @@ import ( "sort" "time" + "github.com/ironcore-dev/metal-operator/internal/bmcutils" + "github.com/go-logr/logr" "github.com/ironcore-dev/controller-utils/clientutils" metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" @@ -272,7 +274,7 @@ func (r *ServerReconciler) handleDiscoveryState(ctx context.Context, log logr.Lo } log.V(1).Info("Server state set to power on") - bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) + bmcClient, err := bmcutils.GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) if err != nil { return false, fmt.Errorf("failed to create BMC client: %w", err) } @@ -416,7 +418,7 @@ func (r *ServerReconciler) updateServerStatus(ctx context.Context, log logr.Logg log.V(1).Info("Server has no BMC connection configured") return nil } - bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) + bmcClient, err := bmcutils.GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) if err != nil { return fmt.Errorf("failed to create BMC client: %w", err) } @@ -615,7 +617,7 @@ func (r *ServerReconciler) pxeBootServer(ctx context.Context, log logr.Logger, s return fmt.Errorf("can only PXE boot server with valid BMC ref or inline BMC configuration") } - bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) + bmcClient, err := bmcutils.GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) defer func() { if bmcClient != nil { bmcClient.Logout() @@ -706,7 +708,7 @@ func (r *ServerReconciler) ensureServerPowerState(ctx context.Context, log logr. return nil } - bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) + bmcClient, err := bmcutils.GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) defer func() { if bmcClient != nil { bmcClient.Logout() @@ -816,7 +818,7 @@ func (r *ServerReconciler) applyBootOrder(ctx context.Context, log logr.Logger, log.V(1).Info("Server has no BMC connection configured") return nil } - bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) + bmcClient, err := bmcutils.GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) if err != nil { return fmt.Errorf("failed to create BMC client: %w", err) } @@ -850,7 +852,7 @@ func (r *ServerReconciler) applyBiosSettings(ctx context.Context, log logr.Logge log.V(1).Info("Server has no BMC connection configured") return nil } - bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) + bmcClient, err := bmcutils.GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) if err != nil { return fmt.Errorf("failed to create BMC client: %w", err) } @@ -902,7 +904,7 @@ func (r *ServerReconciler) handleAnnotionOperations(ctx context.Context, log log if !ok { return false, nil } - bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) + bmcClient, err := bmcutils.GetBMCClientForServer(ctx, r.Client, server, r.Insecure, r.BMCOptions) if err != nil { return false, fmt.Errorf("failed to create BMC client: %w", err) }