Repository: openshift/cluster-baremetal-operator
Pull request #466: OCPBUGS-52483: Allow provisioningIP when network disabled
Author: login: honza
Labels: name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: jira/severity-critical
description: Referenced Jira bug's severity is critical for the branch this PR is targeting.
name: jira/valid-bug
description: Indicates that a referenced Jira bug is valid for the branch this PR is targeting.
name: jira/valid-reference
description: Indicates that this PR references a valid Jira ticket of any type.
Created: 2025-03-18T18:53:15Z | Merged: 2025-05-23T02:26:20Z

In 96b70d809bb608194407ebea334979a3e91a5a74, we decided to ignore the ProvisioningIP setting when the provisioning network is disabled.  It turns out that this has unintended consequences in specific scenarios.

Here, we are relaxing that requirement.  We pass the ProvisioningIP value to the metal3 containers.  The original bug was related to DHCP causing problems so we disable DHCP when the provisioning network is disabled.

UPDATE: We also take the opportunity to refactor the config retrieval logic around ProvisioningIP. We rename some functions to make things clearer and we no longer require network CIDR when provisioning IP is specified.



Diff:
diff --git a/api/v1alpha1/provisioning_validation.go b/api/v1alpha1/provisioning_validation.go
index d4dec790d..be5446359 100644
--- a/api/v1alpha1/provisioning_validation.go
+++ b/api/v1alpha1/provisioning_validation.go
@@ -159,6 +159,9 @@ func validateProvisioningNetworkSettings(ip string, cidr string, dhcpRange strin
 		return errs
 	}
 
+	if provisioningNetworkMode != ProvisioningNetworkManaged {
+		return errs
+	}
 	// Verify Network CIDR
 	_, provisioningCIDR, err := net.ParseCIDR(cidr)
 	if err != nil {
diff --git a/api/v1alpha1/provisioning_validation_test.go b/api/v1alpha1/provisioning_validation_test.go
index dfa8c515d..c1d385891 100644
--- a/api/v1alpha1/provisioning_validation_test.go
+++ b/api/v1alpha1/provisioning_validation_test.go
@@ -287,22 +287,6 @@ func TestValidateDisabledProvisioningConfig(t *testing.T) {
 			expectedError: false,
 			expectedMode:  ProvisioningNetworkDisabled,
 		},
-		{
-			// IP and CIDR set with bad CIDR
-			name:          "InvalidDisabledBadCIDR",
-			spec:          disabledProvisioning().ProvisioningIP("172.22.0.3").ProvisioningNetworkCIDR("172.22.0.0/33").build(),
-			expectedError: true,
-			expectedMode:  ProvisioningNetworkDisabled,
-			expectedMsg:   "could not parse provisioningNetworkCIDR",
-		},
-		{
-			// Only IP is set and not CIDR
-			name:          "InvalidDisabledOnlyIP",
-			spec:          disabledProvisioning().ProvisioningIP("172.22.0.3").ProvisioningNetworkCIDR("").build(),
-			expectedError: true,
-			expectedMode:  ProvisioningNetworkDisabled,
-			expectedMsg:   "provisioningNetworkCIDR",
-		},
 		{
 			// No Provisioning IP or Network
 			name:          "NoProvisioningIPNetwork",
diff --git a/go.mod b/go.mod
index ec8c85698..2a0945353 100644
--- a/go.mod
+++ b/go.mod
@@ -78,7 +78,7 @@ require (
 	github.com/fzipp/gocyclo v0.6.0 // indirect
 	github.com/ghostiam/protogetter v0.3.6 // indirect
 	github.com/go-critic/go-critic v0.11.4 // indirect
-	github.com/go-errors/errors v1.0.1 // indirect
+	github.com/go-errors/errors v1.5.1 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/zapr v1.3.0 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
diff --git a/go.sum b/go.sum
index 7ba62a6db..f1dfd71fb 100644
--- a/go.sum
+++ b/go.sum
@@ -188,8 +188,9 @@ github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7Y
 github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
 github.com/go-critic/go-critic v0.11.4 h1:O7kGOCx0NDIni4czrkRIXTnit0mkyKOCePh3My6OyEU=
 github.com/go-critic/go-critic v0.11.4/go.mod h1:2QAdo4iuLik5S9YG0rT4wcZ8QxwHYkrr6/2MWAiv/vc=
-github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
+github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
+github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
diff --git a/provisioning/baremetal_config.go b/provisioning/baremetal_config.go
index 26d92d8da..f4895138b 100644
--- a/provisioning/baremetal_config.go
+++ b/provisioning/baremetal_config.go
@@ -25,7 +25,7 @@ import (
 	metal3iov1alpha1 "github.com/openshift/cluster-baremetal-operator/api/v1alpha1"
 )
 
-var (
+const (
 	baremetalHttpPort              = "6180"
 	baremetalVmediaHttpsPort       = "6183"
 	baremetalWebhookPort           = "9447"
@@ -49,6 +49,9 @@ var (
 )
 
 func getDHCPRange(config *metal3iov1alpha1.ProvisioningSpec) *string {
+	if config.ProvisioningNetwork != metal3iov1alpha1.ProvisioningNetworkManaged {
+		return nil
+	}
 	var dhcpRange string
 	if config.ProvisioningDHCPRange != "" {
 		_, net, err := net.ParseCIDR(config.ProvisioningNetworkCIDR)
@@ -60,10 +63,7 @@ func getDHCPRange(config *metal3iov1alpha1.ProvisioningSpec) *string {
 	return &dhcpRange
 }
 
-func getProvisioningIPCIDR(config *metal3iov1alpha1.ProvisioningSpec) *string {
-	if config.ProvisioningNetwork == metal3iov1alpha1.ProvisioningNetworkDisabled {
-		return nil
-	}
+func getProvisioningIPWithPrefix(config *metal3iov1alpha1.ProvisioningSpec) *string {
 	if config.ProvisioningNetworkCIDR != "" && config.ProvisioningIP != "" {
 		_, net, err := net.ParseCIDR(config.ProvisioningNetworkCIDR)
 		if err == nil {
@@ -72,6 +72,18 @@ func getProvisioningIPCIDR(config *metal3iov1alpha1.ProvisioningSpec) *string {
 			return &ipCIDR
 		}
 	}
+
+	return nil
+}
+
+func getProvisioningIP(config *metal3iov1alpha1.ProvisioningSpec) *string {
+	if config.ProvisioningNetwork == metal3iov1alpha1.ProvisioningNetworkManaged {
+		return getProvisioningIPWithPrefix(config)
+	}
+
+	if config.ProvisioningIP != "" {
+		return &config.ProvisioningIP
+	}
 	return nil
 }
 
@@ -112,7 +124,7 @@ func getBootIsoSource(config *metal3iov1alpha1.ProvisioningSpec) *string {
 func getMetal3DeploymentConfig(name string, baremetalConfig *metal3iov1alpha1.ProvisioningSpec) *string {
 	switch name {
 	case provisioningIP:
-		return getProvisioningIPCIDR(baremetalConfig)
+		return getProvisioningIP(baremetalConfig)
 	case provisioningInterface:
 		return &baremetalConfig.ProvisioningInterface
 	case provisioningMacAddresses:
diff --git a/provisioning/baremetal_config_test.go b/provisioning/baremetal_config_test.go
index 5d1bff201..7b30e68cc 100644
--- a/provisioning/baremetal_config_test.go
+++ b/provisioning/baremetal_config_test.go
@@ -21,6 +21,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"k8s.io/utils/ptr"
 
 	metal3iov1alpha1 "github.com/openshift/cluster-baremetal-operator/api/v1alpha1"
 )
@@ -30,69 +31,78 @@ func TestGetMetal3DeploymentConfig(t *testing.T) {
 		name          string
 		configName    string
 		spec          *metal3iov1alpha1.ProvisioningSpec
-		expectedValue string
+		expectedValue *string
 	}{
 		{
 			name:          "Managed ProvisioningIPCIDR",
 			configName:    provisioningIP,
 			spec:          managedProvisioning().build(),
-			expectedValue: "172.30.20.3/24",
+			expectedValue: ptr.To("172.30.20.3/24"),
 		},
 		{
 			name:          "Managed ProvisioningInterface",
 			configName:    provisioningInterface,
 			spec:          managedProvisioning().build(),
-			expectedValue: "eth0",
+			expectedValue: ptr.To("eth0"),
 		},
 		{
 			name:          "Unmanaged DeployKernelUrl",
 			configName:    deployKernelUrl,
 			spec:          unmanagedProvisioning().build(),
-			expectedValue: "file:///shared/html/images/ironic-python-agent.kernel",
+			expectedValue: ptr.To("file:///shared/html/images/ironic-python-agent.kernel"),
 		},
 		{
 			name:          "Disabled DeployKernelUrl",
 			configName:    deployKernelUrl,
 			spec:          disabledProvisioning().build(),
-			expectedValue: "file:///shared/html/images/ironic-python-agent.kernel",
+			expectedValue: ptr.To("file:///shared/html/images/ironic-python-agent.kernel"),
 		},
 		{
 			name:          "Unmanaged HttpPort",
 			configName:    httpPort,
 			spec:          unmanagedProvisioning().build(),
-			expectedValue: "6180",
+			expectedValue: ptr.To("6180"),
 		},
 		{
 			name:          "Managed DHCPRange",
 			configName:    dhcpRange,
 			spec:          managedProvisioning().build(),
-			expectedValue: "172.30.20.11,172.30.20.101,24",
+			expectedValue: ptr.To("172.30.20.11,172.30.20.101,24"),
 		},
 		{
 			name:          "Managed IPv6 DHCPRange",
 			configName:    dhcpRange,
 			spec:          managedIPv6Provisioning().build(),
-			expectedValue: "fd2e:6f44:5dd8:b856::10,fd2e:6f44:5dd8:b856::ff,80",
+			expectedValue: ptr.To("fd2e:6f44:5dd8:b856::10,fd2e:6f44:5dd8:b856::ff,80"),
 		},
 		{
 			name:          "Disabled DHCPRange",
 			configName:    dhcpRange,
 			spec:          disabledProvisioning().build(),
-			expectedValue: "",
+			expectedValue: nil,
 		},
 		{
 			name:          "Disabled RhcosImageUrl",
 			configName:    machineImageUrl,
 			spec:          disabledProvisioning().build(),
-			expectedValue: "http://172.22.0.1/images/rhcos-44.81.202001171431.0-openstack.x86_64.qcow2.gz?sha256=e98f83a2b9d4043719664a2be75fe8134dc6ca1fdbde807996622f8cc7ecd234",
+			expectedValue: ptr.To("http://172.22.0.1/images/rhcos-44.81.202001171431.0-openstack.x86_64.qcow2.gz?sha256=e98f83a2b9d4043719664a2be75fe8134dc6ca1fdbde807996622f8cc7ecd234"),
 		},
 	}
 	for _, tc := range tCases {
 		t.Run(tc.name, func(t *testing.T) {
 			t.Logf("Testing tc : %s", tc.name)
 			actualValue := getMetal3DeploymentConfig(tc.configName, tc.spec)
-			assert.NotNil(t, actualValue)
-			assert.Equal(t, tc.expectedValue, *actualValue, fmt.Sprintf("%s : Expected : %s Actual : %s", tc.configName, tc.expectedValue, *actualValue))
+
+			if tc.expectedValue == nil {
+				assert.Nil(t, actualValue)
+				return
+			}
+
+			if actualValue == nil {
+				t.Fatal("actual value was nil")
+			}
+
+			assert.Equal(t, tc.expectedValue, actualValue, fmt.Sprintf("%s : Expected : %s Actual : %s", tc.configName, *tc.expectedValue, *actualValue))
 			return
 		})
 	}
diff --git a/provisioning/baremetal_pod_test.go b/provisioning/baremetal_pod_test.go
index 6c523add8..138b121fe 100644
--- a/provisioning/baremetal_pod_test.go
+++ b/provisioning/baremetal_pod_test.go
@@ -342,10 +342,10 @@ func TestNewMetal3Containers(t *testing.T) {
 			name:   "UnmanagedSpec",
 			config: unmanagedProvisioning().build(),
 			expectedContainers: []corev1.Container{
-				withEnv(containers["metal3-httpd"], envWithValue("PROVISIONING_INTERFACE", "ensp0")),
-				withEnv(containers["metal3-ironic"], envWithValue("PROVISIONING_INTERFACE", "ensp0")),
+				withEnv(containers["metal3-httpd"], envWithValue("PROVISIONING_INTERFACE", "ensp0"), envWithValue("PROVISIONING_IP", "172.30.20.3")),
+				withEnv(containers["metal3-ironic"], envWithValue("PROVISIONING_INTERFACE", "ensp0"), envWithValue("PROVISIONING_IP", "172.30.20.3")),
 				containers["metal3-ramdisk-logs"],
-				withEnv(containers["metal3-static-ip-manager"], envWithValue("PROVISIONING_INTERFACE", "ensp0")),
+				withEnv(containers["metal3-static-ip-manager"], envWithValue("PROVISIONING_INTERFACE", "ensp0"), envWithValue("PROVISIONING_IP", "172.30.20.3")),
 				withEnv(containers["metal3-dnsmasq"], envWithValue("PROVISIONING_INTERFACE", "ensp0"), envWithValue("DHCP_RANGE", "")),
 			},
 			sshkey: "",
@@ -357,14 +357,14 @@ func TestNewMetal3Containers(t *testing.T) {
 				withEnv(
 					containers["metal3-httpd"],
 					envWithValue("PROVISIONING_INTERFACE", ""),
-					envWithFieldValue("PROVISIONING_IP", "status.hostIP"),
 					envWithValue("IRONIC_LISTEN_PORT", "6388"),
+					envWithValue("PROVISIONING_IP", "172.30.20.3"),
 				),
 				withEnv(
 					containers["metal3-ironic"],
 					envWithValue("PROVISIONING_INTERFACE", ""),
-					envWithFieldValue("PROVISIONING_IP", "status.hostIP"),
 					envWithValue("IRONIC_KERNEL_PARAMS", "rd.net.timeout.carrier=30 ip=dhcp6"),
+					envWithValue("PROVISIONING_IP", "172.30.20.3"),
 				),
 				containers["metal3-ramdisk-logs"],
 			},
diff --git a/vendor/github.com/go-errors/errors/.travis.yml b/vendor/github.com/go-errors/errors/.travis.yml
index 9d00fdd5d..1dc296026 100644
--- a/vendor/github.com/go-errors/errors/.travis.yml
+++ b/vendor/github.com/go-errors/errors/.travis.yml
@@ -2,4 +2,6 @@ language: go
 
 go:
   - "1.8.x"
-  - "1.10.x"
+  - "1.11.x"
+  - "1.16.x"
+  - "1.21.x"
diff --git a/vendor/github.com/go-errors/errors/README.md b/vendor/github.com/go-errors/errors/README.md
index 5d4f1873d..558bc883e 100644
--- a/vendor/github.com/go-errors/errors/README.md
+++ b/vendor/github.com/go-errors/errors/README.md
@@ -9,7 +9,7 @@ This is particularly useful when you want to understand the state of execution
 when an error was returned unexpectedly.
 
 It provides the type \*Error which implements the standard golang error
-interface, so you can use this library interchangably with code that is
+interface, so you can use this library interchangeably with code that is
 expecting a normal error return.
 
 Usage
@@ -64,3 +64,21 @@ packages by Facebook and Dropbox, it was moved to one canonical location so
 everyone can benefit.
 
 This package is licensed under the MIT license, see LICENSE.MIT for details.
+
+
+## Changelog
+* v1.1.0 updated to use go1.13's standard-library errors.Is method instead of == in errors.Is
+* v1.2.0 added `errors.As` from the standard library.
+* v1.3.0 *BREAKING* updated error methods to return `error` instead of `*Error`.
+>  Code that needs access to the underlying `*Error` can use the new errors.AsError(e)
+> ```
+>   // before
+>   errors.New(err).ErrorStack()
+>   // after
+>.  errors.AsError(errors.Wrap(err)).ErrorStack()
+> ```
+* v1.4.0 *BREAKING* v1.4.0 reverted all changes from v1.3.0 and is identical to v1.2.0
+* v1.4.1 no code change, but now without an unnecessary cover.out file.
+* v1.4.2 performance improvement to ErrorStack() to avoid unnecessary work https://github.com/go-errors/errors/pull/40
+* v1.5.0 add errors.Join() and errors.Unwrap() copying the stdlib https://github.com/go-errors/errors/pull/40
+* v1.5.1 fix build on go1.13..go1.19 (broken by adding Join and Unwrap with wrong build constraints)
diff --git a/vendor/github.com/go-errors/errors/error.go b/vendor/github.com/go-errors/errors/error.go
index 60062a437..ccbc2e427 100644
--- a/vendor/github.com/go-errors/errors/error.go
+++ b/vendor/github.com/go-errors/errors/error.go
@@ -91,6 +91,10 @@ func New(e interface{}) *Error {
 // fmt.Errorf("%v"). The skip parameter indicates how far up the stack
 // to start the stacktrace. 0 is from the current call, 1 from its caller, etc.
 func Wrap(e interface{}, skip int) *Error {
+	if e == nil {
+		return nil
+	}
+
 	var err error
 
 	switch e := e.(type) {
@@ -117,6 +121,9 @@ func Wrap(e interface{}, skip int) *Error {
 // up the stack to start the stacktrace. 0 is from the current call,
 // 1 from its caller, etc.
 func WrapPrefix(e interface{}, prefix string, skip int) *Error {
+	if e == nil {
+		return nil
+	}
 
 	err := Wrap(e, 1+skip)
 
@@ -132,26 +139,6 @@ func WrapPrefix(e interface{}, prefix string, skip int) *Error {
 
 }
 
-// Is detects whether the error is equal to a given error. Errors
-// are considered equal by this function if they are the same object,
-// or if they both contain the same error inside an errors.Error.
-func Is(e error, original error) bool {
-
-	if e == original {
-		return true
-	}
-
-	if e, ok := e.(*Error); ok {
-		return Is(e.Err, original)
-	}
-
-	if original, ok := original.(*Error); ok {
-		return Is(e, original.Err)
-	}
-
-	return false
-}
-
 // Errorf creates a new error with the given message. You can use it
 // as a drop-in replacement for fmt.Errorf() to provide descriptive
 // errors in return values.
@@ -215,3 +202,8 @@ func (err *Error) TypeName() string {
 	}
 	return reflect.TypeOf(err.Err).String()
 }
+
+// Return the wrapped error (implements api for As function).
+func (err *Error) Unwrap() error {
+	return err.Err
+}
diff --git a/vendor/github.com/go-errors/errors/error_1_13.go b/vendor/github.com/go-errors/errors/error_1_13.go
new file mode 100644
index 000000000..34ab3e00e
--- /dev/null
+++ b/vendor/github.com/go-errors/errors/error_1_13.go
@@ -0,0 +1,35 @@
+//go:build go1.13
+// +build go1.13
+
+package errors
+
+import (
+	baseErrors "errors"
+)
+
+// As finds the first error in err's tree that matches target, and if one is found, sets
+// target to that error value and returns true. Otherwise, it returns false.
+//
+// For more information see stdlib errors.As.
+func As(err error, target interface{}) bool {
+	return baseErrors.As(err, target)
+}
+
+// Is detects whether the error is equal to a given error. Errors
+// are considered equal by this function if they are matched by errors.Is
+// or if their contained errors are matched through errors.Is.
+func Is(e error, original error) bool {
+	if baseErrors.Is(e, original) {
+		return true
+	}
+
+	if e, ok := e.(*Error); ok {
+		return Is(e.Err, original)
+	}
+
+	if original, ok := original.(*Error); ok {
+		return Is(e, original.Err)
+	}
+
+	return false
+}
diff --git a/vendor/github.com/go-errors/errors/error_backward.go b/vendor/github.com/go-errors/errors/error_backward.go
new file mode 100644
index 000000000..ff14c4bfa
--- /dev/null
+++ b/vendor/github.com/go-errors/errors/error_backward.go
@@ -0,0 +1,125 @@
+//go:build !go1.13
+// +build !go1.13
+
+package errors
+
+import (
+	"reflect"
+)
+
+type unwrapper interface {
+	Unwrap() error
+}
+
+// As assigns error or any wrapped error to the value target points
+// to. If there is no value of the target type of target As returns
+// false.
+func As(err error, target interface{}) bool {
+	targetType := reflect.TypeOf(target)
+
+	for {
+		errType := reflect.TypeOf(err)
+
+		if errType == nil {
+			return false
+		}
+
+		if reflect.PtrTo(errType) == targetType {
+			reflect.ValueOf(target).Elem().Set(reflect.ValueOf(err))
+			return true
+		}
+
+		wrapped, ok := err.(unwrapper)
+		if ok {
+			err = wrapped.Unwrap()
+		} else {
+			return false
+		}
+	}
+}
+
+// Is detects whether the error is equal to a given error. Errors
+// are considered equal by this function if they are the same object,
+// or if they both contain the same error inside an errors.Error.
+func Is(e error, original error) bool {
+	if e == original {
+		return true
+	}
+
+	if e, ok := e.(*Error); ok {
+		return Is(e.Err, original)
+	}
+
+	if original, ok := original.(*Error); ok {
+		return Is(e, original.Err)
+	}
+
+	return false
+}
+
+// Disclaimer: functions Join and Unwrap are copied from the stdlib errors
+// package v1.21.0.
+
+// Join returns an error that wraps the given errors.
+// Any nil error values are discarded.
+// Join returns nil if every value in errs is nil.
+// The error formats as the concatenation of the strings obtained
+// by calling the Error method of each element of errs, with a newline
+// between each string.
+//
+// A non-nil error returned by Join implements the Unwrap() []error method.
+func Join(errs ...error) error {
+	n := 0
+	for _, err := range errs {
+		if err != nil {
+			n++
+		}
+	}
+	if n == 0 {
+		return nil
+	}
+	e := &joinError{
+		errs: make([]error, 0, n),
+	}
+	for _, err := range errs {
+		if err != nil {
+			e.errs = append(e.errs, err)
+		}
+	}
+	return e
+}
+
+type joinError struct {
+	errs []error
+}
+
+func (e *joinError) Error() string {
+	var b []byte
+	for i, err := range e.errs {
+		if i > 0 {
+			b = append(b, '\n')
+		}
+		b = append(b, err.Error()...)
+	}
+	return string(b)
+}
+
+func (e *joinError) Unwrap() []error {
+	return e.errs
+}
+
+// Unwrap returns the result of calling the Unwrap method on err, if err's
+// type contains an Unwrap method returning error.
+// Otherwise, Unwrap returns nil.
+//
+// Unwrap only calls a method of the form "Unwrap() error".
+// In particular Unwrap does not unwrap errors returned by [Join].
+func Unwrap(err error) error {
+	u, ok := err.(interface {
+		Unwrap() error
+	})
+	if !ok {
+		return nil
+	}
+	return u.Unwrap()
+}
diff --git a/vendor/github.com/go-errors/errors/join_unwrap_1_20.go b/vendor/github.com/go-errors/errors/join_unwrap_1_20.go
new file mode 100644
index 000000000..44df35ece
--- /dev/null
+++ b/vendor/github.com/go-errors/errors/join_unwrap_1_20.go
@@ -0,0 +1,32 @@
+//go:build go1.20
+// +build go1.20
+
+package errors
+
+import baseErrors "errors"
+
+// Join returns an error that wraps the given errors.
+// Any nil error values are discarded.
+// Join returns nil if every value in errs is nil.
+// The error formats as the concatenation of the strings obtained
+// by calling the Error method of each element of errs, with a newline
+// between each string.
+//
+// A non-nil error returned by Join implements the Unwrap() []error method.
+//
+// For more information see stdlib errors.Join.
+func Join(errs ...error) error {
+	return baseErrors.Join(errs...)
+}
+
+// Unwrap returns the result of calling the Unwrap method on err, if err's
+// type contains an Unwrap method returning error.
+// Otherwise, Unwrap returns nil.
+//
+// Unwrap only calls a method of the form "Unwrap() error".
+// In particular Unwrap does not unwrap errors returned by [Join].
+//
+// For more information see stdlib errors.Unwrap.
+func Unwrap(err error) error {
+	return baseErrors.Unwrap(err)
+}
diff --git a/vendor/github.com/go-errors/errors/join_unwrap_backward.go b/vendor/github.com/go-errors/errors/join_unwrap_backward.go
new file mode 100644
index 000000000..50c766976
--- /dev/null
+++ b/vendor/github.com/go-errors/errors/join_unwrap_backward.go
@@ -0,0 +1,71 @@
+//go:build !go1.20
+// +build !go1.20
+
+package errors
+
+// Disclaimer: functions Join and Unwrap are copied from the stdlib errors
+// package v1.21.0.
+
+// Join returns an error that wraps the given errors.
+// Any nil error values are discarded.
+// Join returns nil if every value in errs is nil.
+// The error formats as the concatenation of the strings obtained
+// by calling the Error method of each element of errs, with a newline
+// between each string.
+//
+// A non-nil error returned by Join implements the Unwrap() []error method.
+func Join(errs ...error) error {
+	n := 0
+	for _, err := range errs {
+		if err != nil {
+			n++
+		}
+	}
+	if n == 0 {
+		return nil
+	}
+	e := &joinError{
+		errs: make([]error, 0, n),
+	}
+	for _, err := range errs {
+		if err != nil {
+			e.errs = append(e.errs, err)
+		}
+	}
+	return e
+}
+
+type joinError struct {
+	errs []error
+}
+
+func (e *joinError) Error() string {
+	var b []byte
+	for i, err := range e.errs {
+		if i > 0 {
+			b = append(b, '\n')
+		}
+		b = append(b, err.Error()...)
+	}
+	return string(b)
+}
+
+func (e *joinError) Unwrap() []error {
+	return e.errs
+}
+
+// Unwrap returns the result of calling the Unwrap method on err, if err's
+// type contains an Unwrap method returning error.
+// Otherwise, Unwrap returns nil.
+//
+// Unwrap only calls a method of the form "Unwrap() error".
+// In particular Unwrap does not unwrap errors returned by [Join].
+func Unwrap(err error) error {
+	u, ok := err.(interface {
+		Unwrap() error
+	})
+	if !ok {
+		return nil
+	}
+	return u.Unwrap()
+}
diff --git a/vendor/github.com/go-errors/errors/stackframe.go b/vendor/github.com/go-errors/errors/stackframe.go
index 750ab9a52..ef4a8b3f3 100644
--- a/vendor/github.com/go-errors/errors/stackframe.go
+++ b/vendor/github.com/go-errors/errors/stackframe.go
@@ -1,9 +1,10 @@
 package errors
 
 import (
+	"bufio"
 	"bytes"
 	"fmt"
-	"io/ioutil"
+	"os"
 	"runtime"
 	"strings"
 )
@@ -52,7 +53,7 @@ func (frame *StackFrame) Func() *runtime.Func {
 func (frame *StackFrame) String() string {
 	str := fmt.Sprintf("%s:%d (0x%x)\n", frame.File, frame.LineNumber, frame.ProgramCounter)
 
-	source, err := frame.SourceLine()
+	source, err := frame.sourceLine()
 	if err != nil {
 		return str
 	}
@@ -62,18 +63,37 @@ func (frame *StackFrame) String() string {
 
 // SourceLine gets the line of code (from File and Line) of the original source if possible.
 func (frame *StackFrame) SourceLine() (string, error) {
-	data, err := ioutil.ReadFile(frame.File)
-
+	source, err := frame.sourceLine()
 	if err != nil {
-		return "", New(err)
+		return source, New(err)
 	}
+	return source, err
+}
 
-	lines := bytes.Split(data, []byte{'\n'})
-	if frame.LineNumber <= 0 || frame.LineNumber >= len(lines) {
+func (frame *StackFrame) sourceLine() (string, error) {
+	if frame.LineNumber <= 0 {
 		return "???", nil
 	}
-	// -1 because line-numbers are 1 based, but our array is 0 based
-	return string(bytes.Trim(lines[frame.LineNumber-1], " \t")), nil
+
+	file, err := os.Open(frame.File)
+	if err != nil {
+		return "", err
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+	currentLine := 1
+	for scanner.Scan() {
+		if currentLine == frame.LineNumber {
+			return string(bytes.Trim(scanner.Bytes(), " \t")), nil
+		}
+		currentLine++
+	}
+	if err := scanner.Err(); err != nil {
+		return "", err
+	}
+
+	return "???", nil
 }
 
 func packageAndName(fn *runtime.Func) (string, string) {
diff --git a/vendor/modules.txt b/vendor/modules.txt
index f09f9c955..479de0038 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -194,8 +194,8 @@ github.com/go-critic/go-critic/checkers/internal/astwalk
 github.com/go-critic/go-critic/checkers/internal/lintutil
 github.com/go-critic/go-critic/checkers/rulesdata
 github.com/go-critic/go-critic/linter
-# github.com/go-errors/errors v1.0.1
-## explicit
+# github.com/go-errors/errors v1.5.1
+## explicit; go 1.14
 github.com/go-errors/errors
 # github.com/go-logr/logr v1.4.2
 ## explicit; go 1.18


Reviews:
user: login: dtantsur
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T09:55:36Z
user: login: dtantsur
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T09:57:31Z
user: login: dtantsur
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T09:58:51Z
user: login: honza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T19:39:24Z
user: login: honza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T10:32:53Z
user: login: honza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T10:34:33Z
user: login: dtantsur
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T08:20:27Z
user: login: dtantsur
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T08:27:28Z
user: login: honza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T09:22:56Z
user: login: dtantsur
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T09:59:00Z
user: login: honza
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T11:34:28Z

Review comments:
diff_hunk: @@ -49,6 +49,9 @@ var (
 )
 
 func getDHCPRange(config *metal3iov1alpha1.ProvisioningSpec) *string {
+	if config.ProvisioningNetwork == metal3iov1alpha1.ProvisioningNetworkDisabled {
path: provisioning/baremetal_config.go
user: login: dtantsur
body: I think it should be != Managed because we also don't start DHCP in the Unmanaged case
created_at: 2025-05-13T09:55:36Z
author_association: MEMBER
diff_hunk: @@ -61,9 +64,6 @@ func getDHCPRange(config *metal3iov1alpha1.ProvisioningSpec) *string {
 }
 
 func getProvisioningIPCIDR(config *metal3iov1alpha1.ProvisioningSpec) *string {
-	if config.ProvisioningNetwork == metal3iov1alpha1.ProvisioningNetworkDisabled {
-		return nil
-	}
 	if config.ProvisioningNetworkCIDR != "" && config.ProvisioningIP != "" {
path: provisioning/baremetal_config.go
user: login: dtantsur
body: I wonder if we need to split the logic around provisioning CIDR and provisioning IP. Despite very similar name, the latter is generically useful while the former is DHCP-specific.

Shameless plug: when writing IrSO, I actually split the two option groups entirely: https://github.com/metal3-io/ironic-standalone-operator/blob/main/api/v1alpha1/ironic_types.go#L61-L161
created_at: 2025-05-13T09:57:31Z
author_association: MEMBER
diff_hunk: @@ -357,13 +357,11 @@ func TestNewMetal3Containers(t *testing.T) {
 				withEnv(
 					containers["metal3-httpd"],
 					envWithValue("PROVISIONING_INTERFACE", ""),
-					envWithFieldValue("PROVISIONING_IP", "status.hostIP"),
path: provisioning/baremetal_pod_test.go
user: login: dtantsur
body: I'd like to see one more test case where the provisioning IP is not set, and we fall back to status.hostIP
created_at: 2025-05-13T09:58:51Z
author_association: MEMBER
diff_hunk: @@ -357,13 +357,11 @@ func TestNewMetal3Containers(t *testing.T) {
 				withEnv(
 					containers["metal3-httpd"],
 					envWithValue("PROVISIONING_INTERFACE", ""),
-					envWithFieldValue("PROVISIONING_IP", "status.hostIP"),
path: provisioning/baremetal_pod_test.go
user: login: honza
body: Isn't that the test just below, L374? `DisabledSpecWithoutProvisioningIP`?
created_at: 2025-05-15T19:39:24Z
author_association: MEMBER
diff_hunk: @@ -49,6 +49,9 @@ var (
 )
 
 func getDHCPRange(config *metal3iov1alpha1.ProvisioningSpec) *string {
+	if config.ProvisioningNetwork == metal3iov1alpha1.ProvisioningNetworkDisabled {
path: provisioning/baremetal_config.go
user: login: honza
body: Done
created_at: 2025-05-16T10:32:53Z
author_association: MEMBER
diff_hunk: @@ -61,9 +64,6 @@ func getDHCPRange(config *metal3iov1alpha1.ProvisioningSpec) *string {
 }
 
 func getProvisioningIPCIDR(config *metal3iov1alpha1.ProvisioningSpec) *string {
-	if config.ProvisioningNetwork == metal3iov1alpha1.ProvisioningNetworkDisabled {
-		return nil
-	}
 	if config.ProvisioningNetworkCIDR != "" && config.ProvisioningIP != "" {
path: provisioning/baremetal_config.go
user: login: honza
body: I renamed the function and simplified the logic.  Also, I made the network CIDR required only in managed mode based on the example in IrSO.
created_at: 2025-05-16T10:34:33Z
author_association: MEMBER
diff_hunk: @@ -10,9 +10,6 @@ bin
 # Test binary, build with `go test -c`
 *.test
 
-# Output of the go coverage tool, specifically when used with LiteIDE
-*.out
-
path: .gitignore
user: login: dtantsur
body: Unrelated (and potentially wrong?) change
created_at: 2025-05-22T08:20:27Z
author_association: MEMBER
diff_hunk: @@ -357,13 +357,11 @@ func TestNewMetal3Containers(t *testing.T) {
 				withEnv(
 					containers["metal3-httpd"],
 					envWithValue("PROVISIONING_INTERFACE", ""),
-					envWithFieldValue("PROVISIONING_IP", "status.hostIP"),
path: provisioning/baremetal_pod_test.go
user: login: dtantsur
body: Ah, just noticed, thank you
created_at: 2025-05-22T08:27:28Z
author_association: MEMBER
diff_hunk: @@ -10,9 +10,6 @@ bin
 # Test binary, build with `go test -c`
 *.test
 
-# Output of the go coverage tool, specifically when used with LiteIDE
-*.out
-
path: .gitignore
user: login: honza
body: The `verify-deps` job was [previously complaining about this file](https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/openshift_cluster-baremetal-operator/466/pull-ci-openshift-cluster-baremetal-operator-main-verify-deps/1923328959299194880).
created_at: 2025-05-22T09:22:56Z
author_association: MEMBER
diff_hunk: @@ -10,9 +10,6 @@ bin
 # Test binary, build with `go test -c`
 *.test
 
-# Output of the go coverage tool, specifically when used with LiteIDE
-*.out
-
path: .gitignore
user: login: dtantsur
body: Oh lovely... Can we bump go-errors to include https://github.com/go-errors/errors/commit/7db060dd91303e294f1ee2855024347369f42c11?
created_at: 2025-05-22T09:59:00Z
author_association: MEMBER
diff_hunk: @@ -10,9 +10,6 @@ bin
 # Test binary, build with `go test -c`
 *.test
 
-# Output of the go coverage tool, specifically when used with LiteIDE
-*.out
-
path: .gitignore
user: login: honza
body: Done
created_at: 2025-05-22T11:34:28Z
author_association: MEMBER

PR comments:
user: login: honza
created_at: 2025-03-18T18:53:35Z
body: /hold
author_association: MEMBER
user: login: openshift-ci-robot
created_at: 2025-04-02T19:11:00Z
body: @honza: This pull request references [Jira Issue OCPBUGS-52483](https://issues.redhat.com//browse/OCPBUGS-52483), which is invalid:
 - expected the bug to target the "4.17.z" version, but no target version was set
 - release note text must be set and not match the template OR release note type must be set to "Release Note Not Required".  For more information you can reference the [OpenShift Bug Process](https://source.redhat.com/groups/public/openshift/openshift_wiki/openshift_bugzilla_process#doc-text-for-bugs).
 - expected [Jira Issue OCPBUGS-52483](https://issues.redhat.com//browse/OCPBUGS-52483) to depend on a bug targeting a version in 4.18.0, 4.18.z and in one of the following states: VERIFIED, RELEASE PENDING, CLOSED (ERRATA), CLOSED (CURRENT RELEASE), CLOSED (DONE), CLOSED (DONE-ERRATA), but no dependents were found

Comment <code>/jira refresh</code> to re-evaluate validity if changes to the Jira bug are made, or edit the title of this pull request to link to a different bug.

The bug has been updated to refer to the pull request using the external bug tracker.

<details>

In response to [this](https://github.com/openshift/cluster-baremetal-operator/pull/466):

>


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fcluster-baremetal-operator).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-05-02T18:23:14Z
body: @honza: This pull request references [Jira Issue OCPBUGS-52483](https://issues.redhat.com//browse/OCPBUGS-52483), which is invalid:
 - expected the bug to target either version "4.20.*" or "openshift-4.20.*", but it targets "4.17.z" instead

Comment <code>/jira refresh</code> to re-evaluate validity if changes to the Jira bug are made, or edit the title of this pull request to link to a different bug.

<details>

In response to [this](https://github.com/openshift/cluster-baremetal-operator/pull/466):

>In 96b70d809bb608194407ebea334979a3e91a5a74, we decided to ignore the
>ProvisioningIP setting when the provisioning network is disabled.  It
>turns out that this has unintended consequences in specific scenarios.
>
>Here, we are relaxing that requirement.  We pass the ProvisioningIP
>value to the metal3 containers.  The original bug was related to DHCP
>causing problems so we disable DHCP when the provisioning network is
>disabled.


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fcluster-baremetal-operator).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-05-02T18:23:54Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/openshift/cluster-baremetal-operator/pull/466#" title="Author self-approved">honza</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=openshift%2Fcluster-baremetal-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/openshift/cluster-baremetal-operator/blob/main/OWNERS)~~ [honza]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: honza
created_at: 2025-05-20T12:58:28Z
body: /retest-required
author_association: MEMBER
user: login: honza
created_at: 2025-05-20T16:55:32Z
body: ```
2025-05-20T14:56:12.785478251Z 2025-05-20 14:56:12.7841
ERROR ironic.conductor.task_manager
[None req-01e32944-c0b3-454d-8c87-5c662f574d29 - - - - - -]
Node c86fc930-9b12-472b-9f1c-d8277e81831f moved to provision state "inspect failed" from state "inspecting"; target provision state is "manageable":
ironic.common.exception.HardwareInspectionFailure:
Failed to inspect hardware. Reason: unable to start inspection: Unable to connect to http://[fd2e:6f44:5dd8:c956::1]:8000/redfish/v1/Systems/fbcc7c34-ef06-48a4-962e-e15631765c50/VirtualMedia/Cd/Actions/VirtualMedia.InsertMedia.
Error: HTTPConnectionPool(host='fd2e:6f44:5dd8:c956::1', port=8000): Read timed out. (read timeout=60)
```
author_association: MEMBER
user: login: honza
created_at: 2025-05-21T20:51:35Z
body: /unhold
author_association: MEMBER
user: login: honza
created_at: 2025-05-22T09:35:53Z
body: /jira refresh
author_association: MEMBER
user: login: openshift-ci-robot
created_at: 2025-05-22T09:36:02Z
body: @honza: This pull request references [Jira Issue OCPBUGS-52483](https://issues.redhat.com//browse/OCPBUGS-52483), which is valid. The bug has been moved to the POST state.

<details><summary>3 validation(s) were run on this bug</summary>

* bug is open, matching expected state (open)
* bug target version (4.20.0) matches configured target version for branch (4.20.0)
* bug is in the state ASSIGNED, which is one of the valid states (NEW, ASSIGNED, POST)</details>

Requesting review from QA contact:
/cc @jadhaj

<details>

In response to [this](https://github.com/openshift/cluster-baremetal-operator/pull/466#issuecomment-2900553432):

>/jira refresh


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fcluster-baremetal-operator).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: dtantsur
created_at: 2025-05-22T16:14:50Z
body: /lgtm
author_association: MEMBER
user: login: openshift-ci-robot
created_at: 2025-05-22T16:52:29Z
body: /retest-required

Remaining retests: 0 against base HEAD f64a5c1d1e3a7d34d89582874ac63d70ef9c556e and 2 for PR HEAD 80eae4820de970fc47af6cf30ef2f3f035c6e4c3 in total

author_association: CONTRIBUTOR
user: login: honza
created_at: 2025-05-22T22:03:08Z
body: /test e2e-metal-ipi-serial-ipv4
author_association: MEMBER
user: login: openshift-ci[bot]
created_at: 2025-05-23T02:23:25Z
body: @honza: 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
--- | --- | --- | --- | ---
ci/prow/e2e-metal-ipi-upgrade-ovn-ipv6 | 80eae4820de970fc47af6cf30ef2f3f035c6e4c3 | [link](https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/openshift_cluster-baremetal-operator/466/pull-ci-openshift-cluster-baremetal-operator-main-e2e-metal-ipi-upgrade-ovn-ipv6/1925514737253093376) | false | `/test e2e-metal-ipi-upgrade-ovn-ipv6`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=openshift&repo=cluster-baremetal-operator&pr=466). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:honza).

<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 -->
author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-05-23T02:26:25Z
body: @honza: [Jira Issue OCPBUGS-52483](https://issues.redhat.com//browse/OCPBUGS-52483): All pull requests linked via external trackers have merged:
 * [openshift/cluster-baremetal-operator#466](https://github.com/openshift/cluster-baremetal-operator/pull/466)

[Jira Issue OCPBUGS-52483](https://issues.redhat.com//browse/OCPBUGS-52483) has been moved to the MODIFIED state.

<details>

In response to [this](https://github.com/openshift/cluster-baremetal-operator/pull/466):

>In 96b70d809bb608194407ebea334979a3e91a5a74, we decided to ignore the ProvisioningIP setting when the provisioning network is disabled.  It turns out that this has unintended consequences in specific scenarios.
>
>Here, we are relaxing that requirement.  We pass the ProvisioningIP value to the metal3 containers.  The original bug was related to DHCP causing problems so we disable DHCP when the provisioning network is disabled.
>
>UPDATE: We also take the opportunity to refactor the config retrieval logic around ProvisioningIP. We rename some functions to make things clearer and we no longer require network CIDR when provisioning IP is specified.
>


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fcluster-baremetal-operator).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: openshift-bot
created_at: 2025-05-23T04:16:37Z
body: **[ART PR BUILD NOTIFIER]**

Distgit: ose-cluster-baremetal-operator
This PR has been included in build [ose-cluster-baremetal-operator-container-v4.20.0-202505230342.p0.g2a6cf53.assembly.stream.el9](https://brewweb.engineering.redhat.com/brew/buildinfo?buildID=3660353).
All builds following this will include this PR.
author_association: NONE
user: login: honza
created_at: 2025-05-24T16:14:49Z
body: /cherry-pick release-4.19
author_association: MEMBER
user: login: openshift-cherrypick-robot
created_at: 2025-05-24T16:15:35Z
body: @honza: new pull request created: #474

<details>

In response to [this](https://github.com/openshift/cluster-baremetal-operator/pull/466#issuecomment-2906910242):

>/cherry-pick release-4.19


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.
</details>
author_association: NONE