Repository: metal3-io/baremetal-operator
Pull request #2559 (2025-07-02 15:36:55)

✨ Stop requiring DEPLOY_KERNEL/RAMDISK
Ironic has global configuration that allows specifying them, even
depending on the architecture. Our ironic-image supports that when
IPA downloader is used (and should start supporting explicit variables
too).

Fixes: #2100 
Signed-off-by: Dmitry Tantsur <dtantsur@protonmail.com>


metal3-io-bot: @dtantsur: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
generate | 5bf26563e7d0b0d58c9f8e94f97b22ce9584acb2 | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/2559/generate/1940434533887250432) | true | `/test generate`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=2559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:dtantsur).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
diff --git a/.golangci.yaml b/.golangci.yaml
index 33892735..b22d8759 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -65,7 +65,7 @@ linters:
   - noctx
   - nolintlint
   - nosprintfhostport
-  - perfsprint
+  #- perfsprint
   - prealloc
   - predeclared
   - reassign
diff --git a/apis/metal3.io/v1alpha1/firmwareschema_types.go b/apis/metal3.io/v1alpha1/firmwareschema_types.go
index 036e00e8..d9d020c4 100644
--- a/apis/metal3.io/v1alpha1/firmwareschema_types.go
+++ b/apis/metal3.io/v1alpha1/firmwareschema_types.go
@@ -88,7 +88,7 @@ func (schema *SettingSchema) Validate(name string, value intstr.IntOrString) err
 				return nil
 			}
 		}
-		return SchemaSettingError{name: name, message: "unknown enumeration value - " + value.String()}
+		return SchemaSettingError{name: name, message: fmt.Sprintf("unknown enumeration value - %s", value.String())}
 
 	case "Integer":
 		if value.Type == intstr.String {
@@ -118,7 +118,7 @@ func (schema *SettingSchema) Validate(name string, value intstr.IntOrString) err
 		if value.String() == "true" || value.String() == "false" {
 			return nil
 		}
-		return SchemaSettingError{name: name, message: value.String() + " is not a boolean"}
+		return SchemaSettingError{name: name, message: fmt.Sprintf("%s is not a boolean", value.String())}
 
 	case "Password":
 		// Prevent sets of password types
@@ -130,7 +130,7 @@ func (schema *SettingSchema) Validate(name string, value intstr.IntOrString) err
 
 	default:
 		// Unexpected attribute type
-		return SchemaSettingError{name: name, message: "unexpected attribute type " + schema.AttributeType}
+		return SchemaSettingError{name: name, message: fmt.Sprintf("unexpected attribute type %s", schema.AttributeType)}
 	}
 }
 
diff --git a/config/default/ironic.env b/config/default/ironic.env
index e72cb3c3..3fe36d25 100644
--- a/config/default/ironic.env
+++ b/config/default/ironic.env
@@ -1,7 +1,5 @@
 HTTP_PORT=6180
 PROVISIONING_INTERFACE=eth2
 DHCP_RANGE=172.22.0.10,172.22.0.100
-DEPLOY_KERNEL_URL=http://172.22.0.2:6180/images/ironic-python-agent.kernel
-DEPLOY_RAMDISK_URL=http://172.22.0.2:6180/images/ironic-python-agent.initramfs
 IRONIC_ENDPOINT=http://172.22.0.2:6385/v1/
 CACHEURL=http://172.22.0.1/images
diff --git a/config/render/capm3.yaml b/config/render/capm3.yaml
index 42283193..7568288f 100644
--- a/config/render/capm3.yaml
+++ b/config/render/capm3.yaml
@@ -2510,8 +2510,6 @@ subjects:
 apiVersion: v1
 data:
   CACHEURL: http://172.22.0.1/images
-  DEPLOY_KERNEL_URL: http://172.22.0.2:6180/images/ironic-python-agent.kernel
-  DEPLOY_RAMDISK_URL: http://172.22.0.2:6180/images/ironic-python-agent.initramfs
   DHCP_RANGE: 172.22.0.10,172.22.0.100
   HTTP_PORT: "6180"
   IRONIC_ENDPOINT: http://172.22.0.2:6385/v1/
diff --git a/internal/controller/metal3.io/baremetalhost_controller.go b/internal/controller/metal3.io/baremetalhost_controller.go
index 979911e4..d04bb618 100644
--- a/internal/controller/metal3.io/baremetalhost_controller.go
+++ b/internal/controller/metal3.io/baremetalhost_controller.go
@@ -906,7 +906,7 @@ func (r *BareMetalHostReconciler) registerHost(prov provisioner.Provisioner, inf
 	// Create the hostFirmwareComponents resource with same host name/namespace if it doesn't exist
 	if info.host.Name != "" {
 		if !info.host.DeletionTimestamp.IsZero() {
-			info.log.Info("will not attempt to create new hostFirmwareSettings and hostFirmwareComponents in " + info.host.Namespace)
+			info.log.Info(fmt.Sprintf("will not attempt to create new hostFirmwareSettings and hostFirmwareComponents in %s", info.host.Namespace))
 		} else {
 			if err = r.createHostFirmwareSettings(info); err != nil {
 				info.log.Info("failed creating hostfirmwaresettings")
@@ -1119,7 +1119,7 @@ func (r *BareMetalHostReconciler) matchProfile(info *reconcileInfo) (dirty bool,
 	if info.host.SetHardwareProfile(hardwareProfile) {
 		dirty = true
 		info.log.Info("updating hardware profile", "profile", hardwareProfile)
-		info.publishEvent("ProfileSet", "Hardware profile set: "+hardwareProfile)
+		info.publishEvent("ProfileSet", fmt.Sprintf("Hardware profile set: %s", hardwareProfile))
 	}
 
 	hintsDirty, err := updateRootDeviceHints(info.host, info)
diff --git a/internal/controller/metal3.io/errors.go b/internal/controller/metal3.io/errors.go
index fffa064b..713e384a 100644
--- a/internal/controller/metal3.io/errors.go
+++ b/internal/controller/metal3.io/errors.go
@@ -11,7 +11,8 @@ type EmptyBMCAddressError struct {
 }
 
 func (e EmptyBMCAddressError) Error() string {
-	return "Empty BMC address " + e.message
+	return fmt.Sprintf("Empty BMC address %s",
+		e.message)
 }
 
 // EmptyBMCSecretError is returned when the BMC secret
@@ -21,7 +22,8 @@ type EmptyBMCSecretError struct {
 }
 
 func (e EmptyBMCSecretError) Error() string {
-	return "No BMC CredentialsName defined " + e.message
+	return fmt.Sprintf("No BMC CredentialsName defined %s",
+		e.message)
 }
 
 // ResolveBMCSecretRefError is returned when the BMC secret
@@ -31,7 +33,8 @@ type ResolveBMCSecretRefError struct {
 }
 
 func (e ResolveBMCSecretRefError) Error() string {
-	return "BMC CredentialsName secret doesn't exist " + e.message
+	return fmt.Sprintf("BMC CredentialsName secret doesn't exist %s",
+		e.message)
 }
 
 // NoDataInSecretError is returned when host configuration
diff --git a/internal/controller/metal3.io/hostfirmwaresettings_controller.go b/internal/controller/metal3.io/hostfirmwaresettings_controller.go
index 78bc88da..2f3fd9f3 100644
--- a/internal/controller/metal3.io/hostfirmwaresettings_controller.go
+++ b/internal/controller/metal3.io/hostfirmwaresettings_controller.go
@@ -19,8 +19,6 @@ package controllers
 import (
 	"context"
 	"crypto/sha256"
-	"encoding/hex"
-	"errors"
 	"fmt"
 	"reflect"
 	"sort"
@@ -383,31 +381,31 @@ func (r *HostFirmwareSettingsReconciler) updateEventHandler(e event.UpdateEvent)
 
 // Validate the HostFirmwareSetting Spec against the schema.
 func (r *HostFirmwareSettingsReconciler) validateHostFirmwareSettings(info *rInfo, status *metal3api.HostFirmwareSettingsStatus, schema *metal3api.FirmwareSchema) []error {
-	var errs []error
+	var errors []error
 
 	for name, val := range info.hfs.Spec.Settings {
 		// Prohibit any Spec settings with "Password"
 		if strings.Contains(name, "Password") {
-			errs = append(errs, errors.New("cannot set Password field"))
+			errors = append(errors, fmt.Errorf("cannot set Password field"))
 			continue
 		}
 
 		// The setting must be in the Status
 		if _, ok := status.Settings[name]; !ok {
-			errs = append(errs, fmt.Errorf("setting %s is not in the Status field", name))
+			errors = append(errors, fmt.Errorf("setting %s is not in the Status field", name))
 			continue
 		}
 
 		// check validity of updated value
 		if schema != nil {
 			if err := schema.ValidateSetting(name, val, schema.Spec.Schema); err != nil {
-				errs = append(errs, err)
+				errors = append(errors, err)
 			}
 		}
 	}
 
-	if len(errs) > 0 {
-		return errs
+	if len(errors) > 0 {
+		return errors
 	}
 
 	return nil
@@ -470,7 +468,7 @@ func GetSchemaName(schema map[string]metal3api.SettingSchema) string {
 
 	h := sha256.New()
 	fmt.Fprintf(h, "%v", hashkeys)
-	hash := hex.EncodeToString(h.Sum(nil))[:8]
+	hash := fmt.Sprintf("%x", h.Sum(nil))[:8]
 
 	return "schema-" + hash
 }
diff --git a/internal/webhooks/metal3.io/v1alpha1/baremetalhost_validation.go b/internal/webhooks/metal3.io/v1alpha1/baremetalhost_validation.go
index e51350d8..42317923 100644
--- a/internal/webhooks/metal3.io/v1alpha1/baremetalhost_validation.go
+++ b/internal/webhooks/metal3.io/v1alpha1/baremetalhost_validation.go
@@ -376,7 +376,7 @@ func (webhook *BareMetalHost) validateCrossNamespaceSecretReferences(host *metal
 
 func validatePowerStatus(host *metal3api.BareMetalHost) error {
 	if host.Spec.DisablePowerOff && !host.Spec.Online {
-		return errors.New("node can't simultaneously have online set to false and have power off disabled")
+		return fmt.Errorf("node can't simultaneously have online set to false and have power off disabled")
 	}
 	return nil
 }
diff --git a/internal/webhooks/metal3.io/v1alpha1/bmceventsubscription_webhook.go b/internal/webhooks/metal3.io/v1alpha1/bmceventsubscription_webhook.go
index 587dbec1..4788b358 100644
--- a/internal/webhooks/metal3.io/v1alpha1/bmceventsubscription_webhook.go
+++ b/internal/webhooks/metal3.io/v1alpha1/bmceventsubscription_webhook.go
@@ -16,7 +16,6 @@ package webhooks
 
 import (
 	"context"
-	"errors"
 	"fmt"
 
 	metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
@@ -72,7 +71,7 @@ func (webhook *BMCEventSubscription) ValidateUpdate(_ context.Context, oldObj, n
 	}
 
 	if newBMCES.Spec != oldBMCES.Spec {
-		return nil, errors.New("subscriptions cannot be updated, please recreate it")
+		return nil, fmt.Errorf("subscriptions cannot be updated, please recreate it")
 	}
 
 	return nil, nil
diff --git a/main.go b/main.go
index ccdb60bf..b8e519c0 100644
--- a/main.go
+++ b/main.go
@@ -78,11 +78,11 @@ func init() {
 }
 
 func printVersion() {
-	setupLog.Info("Go Version: " + runtime.Version())
+	setupLog.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
 	setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH))
-	setupLog.Info("Git commit: " + version.Commit)
-	setupLog.Info("Build time: " + version.BuildTime)
-	setupLog.Info("Component: " + version.String)
+	setupLog.Info(fmt.Sprintf("Git commit: %s", version.Commit))
+	setupLog.Info(fmt.Sprintf("Build time: %s", version.BuildTime))
+	setupLog.Info(fmt.Sprintf("Component: %s", version.String))
 }
 
 func setupChecks(mgr ctrl.Manager) {
diff --git a/pkg/hardwareutils/bmc/errors.go b/pkg/hardwareutils/bmc/errors.go
index 7fa1b99d..a1d96ff9 100644
--- a/pkg/hardwareutils/bmc/errors.go
+++ b/pkg/hardwareutils/bmc/errors.go
@@ -23,5 +23,6 @@ type CredentialsValidationError struct {
 }
 
 func (e CredentialsValidationError) Error() string {
-	return "Validation error with BMC credentials: " + e.message
+	return fmt.Sprintf("Validation error with BMC credentials: %s",
+		e.message)
 }
diff --git a/pkg/imageprovider/invalid.go b/pkg/imageprovider/invalid.go
index 9f016bc4..de837918 100644
--- a/pkg/imageprovider/invalid.go
+++ b/pkg/imageprovider/invalid.go
@@ -1,11 +1,15 @@
 package imageprovider
 
+import (
+	"fmt"
+)
+
 type ImageBuildInvalid struct {
 	err error
 }
 
 func (ibf ImageBuildInvalid) Error() string {
-	return "Cannot generate image: " + ibf.err.Error()
+	return fmt.Sprintf("Cannot generate image: %s", ibf.err.Error())
 }
 
 func (ibf ImageBuildInvalid) Unwrap() error {
diff --git a/pkg/provisioner/ironic/clients/auth.go b/pkg/provisioner/ironic/clients/auth.go
index 2ff63e12..2b3c5e6f 100644
--- a/pkg/provisioner/ironic/clients/auth.go
+++ b/pkg/provisioner/ironic/clients/auth.go
@@ -1,7 +1,7 @@
 package clients
 
 import (
-	"errors"
+	"fmt"
 	"net/url"
 	"os"
 	"path"
@@ -63,9 +63,9 @@ func LoadAuth() (auth AuthConfig, err error) {
 	}
 
 	if auth.Username == "" {
-		err = errors.New("empty HTTP Basic Auth username")
+		err = fmt.Errorf("empty HTTP Basic Auth username")
 	} else if auth.Password == "" {
-		err = errors.New("empty HTTP Basic Auth password")
+		err = fmt.Errorf("empty HTTP Basic Auth password")
 	}
 	return
 }
@@ -84,7 +84,7 @@ func ConfigFromEndpointURL(endpointURL string) (endpoint string, auth AuthConfig
 		auth.Username = parsedURL.User.Username()
 		auth.Password, hasPasswd = parsedURL.User.Password()
 		if !hasPasswd {
-			err = errors.New("no password supplied for HTTP Basic Auth")
+			err = fmt.Errorf("no password supplied for HTTP Basic Auth")
 		}
 		parsedURL.User = nil
 	} else {
diff --git a/pkg/provisioner/ironic/devicehints/devicehints.go b/pkg/provisioner/ironic/devicehints/devicehints.go
index f058d57f..db575c2d 100644
--- a/pkg/provisioner/ironic/devicehints/devicehints.go
+++ b/pkg/provisioner/ironic/devicehints/devicehints.go
@@ -18,34 +18,34 @@ func MakeHintMap(source *metal3api.RootDeviceHints) map[string]string {
 
 	if source.DeviceName != "" {
 		if strings.HasPrefix(source.DeviceName, "/dev/disk/by-path/") {
-			hints["by_path"] = "s== " + source.DeviceName
+			hints["by_path"] = fmt.Sprintf("s== %s", source.DeviceName)
 		} else {
-			hints["name"] = "s== " + source.DeviceName
+			hints["name"] = fmt.Sprintf("s== %s", source.DeviceName)
 		}
 	}
 	if source.HCTL != "" {
-		hints["hctl"] = "s== " + source.HCTL
+		hints["hctl"] = fmt.Sprintf("s== %s", source.HCTL)
 	}
 	if source.Model != "" {
-		hints["model"] = "<in> " + source.Model
+		hints["model"] = fmt.Sprintf("<in> %s", source.Model)
 	}
 	if source.Vendor != "" {
-		hints["vendor"] = "<in> " + source.Vendor
+		hints["vendor"] = fmt.Sprintf("<in> %s", source.Vendor)
 	}
 	if source.SerialNumber != "" {
-		hints["serial"] = "s== " + source.SerialNumber
+		hints["serial"] = fmt.Sprintf("s== %s", source.SerialNumber)
 	}
 	if source.MinSizeGigabytes != 0 {
 		hints["size"] = fmt.Sprintf(">= %d", source.MinSizeGigabytes)
 	}
 	if source.WWN != "" {
-		hints["wwn"] = "s== " + source.WWN
+		hints["wwn"] = fmt.Sprintf("s== %s", source.WWN)
 	}
 	if source.WWNWithExtension != "" {
-		hints["wwn_with_extension"] = "s== " + source.WWNWithExtension
+		hints["wwn_with_extension"] = fmt.Sprintf("s== %s", source.WWNWithExtension)
 	}
 	if source.WWNVendorExtension != "" {
-		hints["wwn_vendor_extension"] = "s== " + source.WWNVendorExtension
+		hints["wwn_vendor_extension"] = fmt.Sprintf("s== %s", source.WWNVendorExtension)
 	}
 	switch {
 	case source.Rotational == nil:
diff --git a/pkg/provisioner/ironic/factory.go b/pkg/provisioner/ironic/factory.go
index 30c1e5c4..eb0cf5c6 100644
--- a/pkg/provisioner/ironic/factory.go
+++ b/pkg/provisioner/ironic/factory.go
@@ -114,10 +114,8 @@ func loadConfigFromEnv(havePreprovImgBuilder bool) (ironicConfig, error) {
 	c.deployRamdiskURL = os.Getenv("DEPLOY_RAMDISK_URL")
 	c.deployISOURL = os.Getenv("DEPLOY_ISO_URL")
 	if !havePreprovImgBuilder {
-		if c.deployISOURL == "" &&
-			(c.deployKernelURL == "" || c.deployRamdiskURL == "") {
-			return c, errors.New("either DEPLOY_KERNEL_URL and DEPLOY_RAMDISK_URL or DEPLOY_ISO_URL must be set")
-		}
+		// NOTE(dtantsur): with a PreprovisioningImage controller, it makes sense to set only the kernel.
+		// Without it, either both or neither must be set.
 		if (c.deployKernelURL == "" && c.deployRamdiskURL != "") ||
 			(c.deployKernelURL != "" && c.deployRamdiskURL == "") {
 			return c, errors.New("DEPLOY_KERNEL_URL and DEPLOY_RAMDISK_URL can only be set together")
@@ -138,7 +136,7 @@ func loadConfigFromEnv(havePreprovImgBuilder bool) (ironicConfig, error) {
 
 	if forcePersistentBootDevice := os.Getenv("LIVE_ISO_FORCE_PERSISTENT_BOOT_DEVICE"); forcePersistentBootDevice != "" {
 		if forcePersistentBootDevice != "Default" && forcePersistentBootDevice != "Always" && forcePersistentBootDevice != "Never" {
-			return c, errors.New("invalid value for variable LIVE_ISO_FORCE_PERSISTENT_BOOT_DEVICE, must be one of Default, Always or Never")
+			return c, fmt.Errorf("invalid value for variable LIVE_ISO_FORCE_PERSISTENT_BOOT_DEVICE, must be one of Default, Always or Never")
 		}
 		c.liveISOForcePersistentBootDevice = forcePersistentBootDevice
 	}
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index e5f9941f..82f9bfc2 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -138,7 +138,8 @@ func (p *ironicProvisioner) validateNode(ironicNode *nodes.Node) (errorMessage s
 		// We expect to see errors of this nature sometimes, so rather
 		// than reporting it as a reconcile error we record the error
 		// status on the host and return.
-		errorMessage = "host validation error: " + strings.Join(validationErrors, "; ")
+		errorMessage = fmt.Sprintf("host validation error: %s",
+			strings.Join(validationErrors, "; "))
 		return errorMessage, nil
 	}
 	return "", nil
@@ -351,14 +352,10 @@ func (p *ironicProvisioner) configureImages(data provisioner.ManagementAccessDat
 	switch data.State {
 	case metal3api.StateInspecting,
 		metal3api.StatePreparing:
-		if deployImageInfo == nil {
-			if p.config.havePreprovImgBuilder {
-				result, err = transientError(provisioner.ErrNeedsPreprovisioningImage)
-			} else {
-				result, err = operationFailed("no preprovisioning image available")
-			}
-			return result, err
+		if deployImageInfo == nil && p.config.havePreprovImgBuilder {
+			result, err = transientError(provisioner.ErrNeedsPreprovisioningImage)
 		}
+		return result, err
 	default:
 	}
 
@@ -457,7 +454,7 @@ func setDeployImage(config ironicConfig, accessDetails bmc.AccessDetails, hostIm
 			deployImageInfo[deployRamdiskKey] = hostImage.ImageURL
 			if hostImage.ExtraKernelParams != "" {
 				// Using %default% prevents overriding the config in ironic-image
-				deployImageInfo[kernelParamsKey] = "%default% " + hostImage.ExtraKernelParams
+				deployImageInfo[kernelParamsKey] = fmt.Sprintf("%%default%% %s", hostImage.ExtraKernelParams)
 			}
 			return deployImageInfo
 		}
@@ -754,7 +751,7 @@ func (p *ironicProvisioner) GetFirmwareComponents() ([]metal3api.FirmwareCompone
 	}
 
 	if !p.availableFeatures.HasFirmwareUpdates() {
-		return nil, errors.New("current ironic version does not support firmware updates")
+		return nil, fmt.Errorf("current ironic version does not support firmware updates")
 	}
 
 	// Setting to 2 since we only support bmc and bios
@@ -824,7 +821,7 @@ func (p *ironicProvisioner) setUpForProvisioning(ironicNode *nodes.Node, data pr
 		"deploy step", ironicNode.DeployStep,
 	)
 	p.publisher("ProvisioningStarted",
-		"Image provisioning started for "+data.Image.URL)
+		fmt.Sprintf("Image provisioning started for %s", data.Image.URL))
 	return result, nil
 }
 
@@ -877,8 +874,8 @@ func (p *ironicProvisioner) Adopt(data provisioner.AdoptData, restartOnFailure b
 				},
 			)
 		}
-		return operationFailed("Host adoption failed: " +
-			ironicNode.LastError)
+		return operationFailed(fmt.Sprintf("Host adoption failed: %s",
+			ironicNode.LastError))
 	case nodes.Active:
 		// Empty Fault means that maintenance was set manually, not by Ironic
 		if ironicNode.Maintenance && ironicNode.Fault == "" && data.State != metal3api.StateDeleting {
@@ -1251,7 +1248,8 @@ func (p *ironicProvisioner) Provision(data provisioner.ProvisionData, forceReboo
 				return retryAfterDelay(0)
 			}
 			p.log.Info("found error", "msg", ironicNode.LastError)
-			return operationFailed("Image provisioning failed: " + ironicNode.LastError)
+			return operationFailed(fmt.Sprintf("Image provisioning failed: %s",
+				ironicNode.LastError))
 		}
 		p.log.Info("recovering from previous failure")
 		if provResult, err = p.setUpForProvisioning(ironicNode, data); err != nil || provResult.Dirty || provResult.ErrorMessage != "" {
@@ -1314,7 +1312,7 @@ func (p *ironicProvisioner) Provision(data provisioner.ProvisionData, forceReboo
 	case nodes.Active:
 		// provisioning is done
 		p.publisher("ProvisioningComplete",
-			"Image provisioning completed for "+data.Image.URL)
+			fmt.Sprintf("Image provisioning completed for %s", data.Image.URL))
 		p.log.Info("finished provisioning")
 		return operationComplete()
 
@@ -1400,7 +1398,7 @@ func (p *ironicProvisioner) Deprovision(restartOnFailure bool) (result provision
 	case nodes.CleanFail:
 		if !restartOnFailure {
 			p.log.Info("cleaning failed", "lastError", ironicNode.LastError)
-			return operationFailed("Cleaning failed: " + ironicNode.LastError)
+			return operationFailed(fmt.Sprintf("Cleaning failed: %s", ironicNode.LastError))
 		}
 		p.log.Info("retrying cleaning")
 		if ironicNode.Maintenance {
@@ -1619,8 +1617,8 @@ func (p *ironicProvisioner) PowerOn(force bool) (result provisioner.Result, err
 		}
 		if ironicNode.LastError != "" && !force {
 			p.log.Info("PowerOn operation failed", "msg", ironicNode.LastError)
-			return operationFailed("PowerOn operation failed: " +
-				ironicNode.LastError)
+			return operationFailed(fmt.Sprintf("PowerOn operation failed: %s",
+				ironicNode.LastError))
 		}
 		return p.changePower(ironicNode, nodes.PowerOn)
 	}
diff --git a/pkg/version/version.go b/pkg/version/version.go
index 114e3f6f..b2b69007 100644
--- a/pkg/version/version.go
+++ b/pkg/version/version.go
@@ -1,6 +1,8 @@
 // Package version includes the version information.
 package version
 
+import "fmt"
+
 var (
 	// Raw is the string representation of the version. This will be replaced
 	// with the calculated version at build time.
@@ -8,7 +10,7 @@ var (
 	Raw = "was not built with version info"
 
 	// String is the human-friendly representation of the version.
-	String = "metal3-io/baremetal-operator " + Raw
+	String = fmt.Sprintf("metal3-io/baremetal-operator %s", Raw)
 
 	// Commit is the commit hash from which the software was built.
 	// Set via LDFLAGS in Makefile.


Test patch:
diff --git a/config/overlays/e2e/ironic.env b/config/overlays/e2e/ironic.env
index 44147ae0..6f200720 100644
--- a/config/overlays/e2e/ironic.env
+++ b/config/overlays/e2e/ironic.env
@@ -1,3 +1,1 @@
-DEPLOY_KERNEL_URL=http://192.168.222.1:6180/images/ironic-python-agent.kernel
-DEPLOY_RAMDISK_URL=http://192.168.222.1:6180/images/ironic-python-agent.initramfs
 IRONIC_ENDPOINT=https://192.168.222.1:6385/v1/
diff --git a/pkg/provisioner/ironic/clients/updateopts_test.go b/pkg/provisioner/ironic/clients/updateopts_test.go
index fbe9fe3d..8193f71c 100644
--- a/pkg/provisioner/ironic/clients/updateopts_test.go
+++ b/pkg/provisioner/ironic/clients/updateopts_test.go
@@ -1,6 +1,7 @@
 package clients
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/go-logr/logr"
@@ -357,7 +358,7 @@ func TestGetUpdateOperation(t *testing.T) {
 
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
-			path := "test/" + c.Field
+			path := fmt.Sprintf("test/%s", c.Field)
 			updateOp := getUpdateOperation(
 				c.Field, existingData,
 				c.NewValue,
diff --git a/pkg/provisioner/ironic/factory_test.go b/pkg/provisioner/ironic/factory_test.go
index db47d8b2..0d32eccb 100644
--- a/pkg/provisioner/ironic/factory_test.go
+++ b/pkg/provisioner/ironic/factory_test.go
@@ -98,24 +98,19 @@ func TestLoadConfigFromEnv(t *testing.T) {
 				ramdiskURL: "http://ramdisk",
 			},
 		},
-		{
-			name:          "no deploy info",
-			env:           EnvFixture{},
-			expectedError: "either DEPLOY_KERNEL_URL and DEPLOY_RAMDISK_URL or DEPLOY_ISO_URL must be set",
-		},
 		{
 			name: "only kernel",
 			env: EnvFixture{
 				kernelURL: "http://kernel",
 			},
-			expectedError: "either DEPLOY_KERNEL_URL and DEPLOY_RAMDISK_URL or DEPLOY_ISO_URL must be set",
+			expectedError: "DEPLOY_KERNEL_URL and DEPLOY_RAMDISK_URL can only be set together",
 		},
 		{
 			name: "only ramdisk",
 			env: EnvFixture{
 				ramdiskURL: "http://ramdisk",
 			},
-			expectedError:         "either DEPLOY_KERNEL_URL and DEPLOY_RAMDISK_URL or DEPLOY_ISO_URL must be set",
+			expectedError:         "DEPLOY_KERNEL_URL and DEPLOY_RAMDISK_URL can only be set together",
 			expectedImgBuildError: "DEPLOY_RAMDISK_URL requires DEPLOY_KERNEL_URL to be set also",
 		},
 		{
diff --git a/pkg/provisioner/ironic/testserver/ironic.go b/pkg/provisioner/ironic/testserver/ironic.go
index 0241fe41..d9864e52 100644
--- a/pkg/provisioner/ironic/testserver/ironic.go
+++ b/pkg/provisioner/ironic/testserver/ironic.go
@@ -225,7 +225,7 @@ func (m *IronicMock) NoNode(name string) *IronicMock {
 
 // NodeError configures the server to return the specified error code for /v1/nodes/name.
 func (m *IronicMock) NodeError(name string, errorCode int) *IronicMock {
-	m.ErrorResponse("/v1/nodes/"+name, errorCode)
+	m.ErrorResponse(fmt.Sprintf("/v1/nodes/%s", name), errorCode)
 	return m
 }
 
diff --git a/pkg/provisioner/ironic/testserver/server.go b/pkg/provisioner/ironic/testserver/server.go
index 9ba08877..c6502af4 100644
--- a/pkg/provisioner/ironic/testserver/server.go
+++ b/pkg/provisioner/ironic/testserver/server.go
@@ -7,7 +7,6 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"regexp"
-	"strconv"
 	"strings"
 	"testing"
 )
@@ -153,7 +152,7 @@ func (m *MockServer) ResponseJSON(pattern string, payload interface{}) *MockServ
 func (m *MockServer) ErrorResponse(pattern string, errorCode int) *MockServer {
 	m.t.Logf("%s: adding error response handler for %s", m.name, pattern)
 	m.mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
-		m.logRequest(r, strconv.Itoa(errorCode))
+		m.logRequest(r, fmt.Sprintf("%d", errorCode))
 		http.Error(w, "An error", errorCode)
 	})
 	return m
diff --git a/test/e2e/e2e_config.go b/test/e2e/e2e_config.go
index 21d481ef..6600d8ba 100644
--- a/test/e2e/e2e_config.go
+++ b/test/e2e/e2e_config.go
@@ -178,7 +178,7 @@ func (c *Config) Validate() error {
 func (c *Config) GetIntervals(spec, key string) []interface{} {
 	intervals, ok := c.Intervals[fmt.Sprintf("%s/%s", spec, key)]
 	if !ok {
-		if intervals, ok = c.Intervals["default/"+key]; !ok {
+		if intervals, ok = c.Intervals[fmt.Sprintf("default/%s", key)]; !ok {
 			return nil
 		}
 	}
diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go
index 217b1beb..f189d619 100644
--- a/test/e2e/e2e_suite_test.go
+++ b/test/e2e/e2e_suite_test.go
@@ -99,7 +99,7 @@ func serviceAccountToken() (string, error) {
 	}`
 
 	// Temporary file to store the token request
-	secretName := serviceAccountName + "-token-request"
+	secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
 	tokenRequestFile := filepath.Join("/tmp", secretName) //nolint: gocritic
 	err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644))
 	if err != nil {
@@ -271,7 +271,7 @@ var _ = SynchronizedBeforeSuite(func() []byte {
 			"--namespace", namespace,
 			"--image=curlimages/curl:7.87.0",
 			"--command",
-			"--", "curl", "-v", "--tlsv1.3", "-k", "-H", "Authorization:Bearer "+token,
+			"--", "curl", "-v", "--tlsv1.3", "-k", "-H", fmt.Sprintf("Authorization:Bearer %s", token),
 			fmt.Sprintf("https://%s.%s.svc.cluster.local:8443/metrics", metricsServiceName, namespace))
 		_, err = cmd.CombinedOutput()
 		Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")
diff --git a/test/e2e/live_iso_test.go b/test/e2e/live_iso_test.go
index da1fdb29..0ed61792 100644
--- a/test/e2e/live_iso_test.go
+++ b/test/e2e/live_iso_test.go
@@ -37,7 +37,7 @@ var _ = Describe("Live-ISO", Label("required", "live-iso"), func() {
 		accessDetails, err := metal3bmc.NewAccessDetails(bmc.Address, false)
 		Expect(err).NotTo(HaveOccurred())
 		if !accessDetails.SupportsISOPreprovisioningImage() {
-			Skip("BMC does not support ISO images. It does not make sense to test live-ISO here. BMC address: " + bmc.Address)
+			Skip(fmt.Sprintf("BMC does not support ISO images. It does not make sense to test live-ISO here. BMC address: %s", bmc.Address))
 		}
 
 		imageURL = e2eConfig.GetVariable("ISO_IMAGE_URL")
diff --git a/test/e2e/upgrade_test.go b/test/e2e/upgrade_test.go
index 1f69c433..428af9cf 100644
--- a/test/e2e/upgrade_test.go
+++ b/test/e2e/upgrade_test.go
@@ -372,7 +372,7 @@ var _ = Describe("Upgrade", Label("optional", "upgrade"), func() {
 			Expect(upgradeClusterProvider).ToNot(BeNil(), "Failed to create a cluster")
 			kubeconfigPath = upgradeClusterProvider.GetKubeconfigPath()
 			DeferCleanup(func() {
-				By("Disposing the kind cluster " + upgradeClusterName)
+				By(fmt.Sprintf("Disposing the kind cluster %s", upgradeClusterName))
 				upgradeClusterProvider.Dispose(ctx)
 			})
 		}