diff --git a/go.mod b/go.mod index 3cfeb2202b..2957ed56eb 100644 --- a/go.mod +++ b/go.mod @@ -128,3 +128,5 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace github.com/openshift/library-go => github.com/vrutkovs/library-go v0.0.0-20241122091102-4d0a3e9ced24 diff --git a/go.sum b/go.sum index 808510b03d..a2d41a1514 100644 --- a/go.sum +++ b/go.sum @@ -167,8 +167,6 @@ github.com/openshift/build-machinery-go v0.0.0-20240613134303-8359781da660 h1:F0 github.com/openshift/build-machinery-go v0.0.0-20240613134303-8359781da660/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= github.com/openshift/client-go v0.0.0-20241001162912-da6d55e4611f h1:FRc0bVNWprihWS0GqQWzb3dY4dkCwpOP3mDw5NwSoR4= github.com/openshift/client-go v0.0.0-20241001162912-da6d55e4611f/go.mod h1:KiZi2mJRH1TOJ3FtBDYS6YvUL30s/iIXaGSUrSa36mo= -github.com/openshift/library-go v0.0.0-20241119162247-f466fdd82330 h1:nK16PhWNPIjnv7Vux1jBlzEPgA9q/o/CwqsbRpQJrpY= -github.com/openshift/library-go v0.0.0-20241119162247-f466fdd82330/go.mod h1:9B1MYPoLtP9tqjWxcbUNVpwxy68zOH/3EIP6c31dAM0= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -215,6 +213,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= +github.com/vrutkovs/library-go v0.0.0-20241122091102-4d0a3e9ced24 h1:OMTPL8M+Wba2xfon/rctp0SDp8JdIJwSKv8ponsKgp8= +github.com/vrutkovs/library-go v0.0.0-20241122091102-4d0a3e9ced24/go.mod h1:9B1MYPoLtP9tqjWxcbUNVpwxy68zOH/3EIP6c31dAM0= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= diff --git a/pkg/operator/nodekubeconfigcontroller/nodekubeconfigcontroller.go b/pkg/operator/nodekubeconfigcontroller/nodekubeconfigcontroller.go index a98106ff58..82f36b2549 100644 --- a/pkg/operator/nodekubeconfigcontroller/nodekubeconfigcontroller.go +++ b/pkg/operator/nodekubeconfigcontroller/nodekubeconfigcontroller.go @@ -2,7 +2,10 @@ package nodekubeconfigcontroller import ( "context" + "crypto/tls" + "crypto/x509" "encoding/base64" + "encoding/pem" "fmt" "strings" "time" @@ -14,6 +17,7 @@ import ( "github.com/openshift/cluster-kube-apiserver-operator/bindata" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/operatorclient" "github.com/openshift/library-go/pkg/controller/factory" + "github.com/openshift/library-go/pkg/operator/certrotation" "github.com/openshift/library-go/pkg/operator/events" "github.com/openshift/library-go/pkg/operator/resource/resourceapply" "github.com/openshift/library-go/pkg/operator/resource/resourceread" @@ -112,6 +116,22 @@ func ensureNodeKubeconfigs(ctx context.Context, client coreclientv1.CoreV1Interf return fmt.Errorf("system:admin client private key missing from secret %s/node-system-admin-client", operatorclient.OperatorNamespace) } + // Ensure secret key matches the certificate + _, err = tls.X509KeyPair(systemAdminClientCert, systemAdminClientKey) + if err != nil { + return fmt.Errorf("system:admin client private key doesn't match the certificate from secret %s/node-system-admin-client", operatorclient.OperatorNamespace) + } + // extract not-before/not-after timestamps valid x509 certificate + var block *pem.Block + block, _ = pem.Decode(systemAdminClientCert) + if block == nil || block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + return fmt.Errorf("invalid first block found for certificate from secret %s/node-system-admin-client", operatorclient.OperatorNamespace) + } + parsedCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse the certificate from secret %s/node-system-admin-client", operatorclient.OperatorNamespace) + } + servingCABundleCM, err := configmapLister.ConfigMaps(operatorclient.TargetNamespace).Get("kube-apiserver-server-ca") if err != nil { return err @@ -152,6 +172,8 @@ func ensureNodeKubeconfigs(ctx context.Context, client coreclientv1.CoreV1Interf requiredSecret.Annotations = map[string]string{} } requiredSecret.Annotations[annotations.OpenShiftComponent] = "kube-apiserver" + requiredSecret.Annotations[certrotation.CertificateNotBeforeAnnotation] = parsedCert.NotBefore.Format(time.RFC3339) + requiredSecret.Annotations[certrotation.CertificateNotAfterAnnotation] = parsedCert.NotAfter.Format(time.RFC3339) _, _, err = resourceapply.ApplySecret(ctx, client, recorder, requiredSecret) if err != nil { diff --git a/pkg/operator/nodekubeconfigcontroller/nodekubeconfigcontroller_test.go b/pkg/operator/nodekubeconfigcontroller/nodekubeconfigcontroller_test.go index cdca5acbd5..97a91b086e 100644 --- a/pkg/operator/nodekubeconfigcontroller/nodekubeconfigcontroller_test.go +++ b/pkg/operator/nodekubeconfigcontroller/nodekubeconfigcontroller_test.go @@ -2,12 +2,15 @@ package nodekubeconfigcontroller import ( "context" + "encoding/base64" + "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/openshift/api/annotations" configv1 "github.com/openshift/api/config/v1" configlistersv1 "github.com/openshift/client-go/config/listers/config/v1" + "github.com/openshift/library-go/pkg/operator/certrotation" "github.com/openshift/library-go/pkg/operator/events" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" @@ -85,7 +88,34 @@ func (l *secretLister) Get(name string) (*corev1.Secret, error) { return l.client.CoreV1().Secrets(l.namespace).Get(context.Background(), name, metav1.GetOptions{}) } +const privateKey = ` +-----BEGIN PRIVATE KEY----- +MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEArvkpSCWaStPfbYr4 +cCJyv8pXWnJ4K22emSrYDNcp7Dm6qjtN/lsVNuGDyWyR4cUaJYXkaD2OrZiXDzzk +BZlS3QIDAQABAkA9BZhoGPUec5XQVk8ejGUIjkC4woM2YhyVvmNq1v8/6q6V+uPw +yDEfBMapuLVY+QhyVELXFOCHA5iKxrlFHZThAiEA1XA5mlbHtrJqEZ7yI5m6+Szj +7YVzSkdSgfDZ//heAh8CIQDR3VbN9QmJRIM1yhIkP9BoWSxvXdH6QMXdC2X7Tkwj +gwIgcpbSxjLK/CIjYhx0oXpacIaSRCX+dKV//XVChPNh/T8CIQCSFscXZez2fhfs +eLb6PuXfzbuN5ryFvVM/VXDvaIi96wIgcHjUpONghaoA51XejMAxWanDiwAgRV5H +XNdFkBi4q7o= +-----END PRIVATE KEY-----` +const publicKey = `-----BEGIN CERTIFICATE----- +MIIBfzCCASmgAwIBAgIUEEUHu1PzqJCGQ63vxVokwBxGPYwwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MTEyNjA4NTA0NloXDTM0MTEy +NDA4NTA0NlowFDESMBAGA1UEAwwJbG9jYWxob3N0MFwwDQYJKoZIhvcNAQEBBQAD +SwAwSAJBAK75KUglmkrT322K+HAicr/KV1pyeCttnpkq2AzXKew5uqo7Tf5bFTbh +g8lskeHFGiWF5Gg9jq2Ylw885AWZUt0CAwEAAaNTMFEwHQYDVR0OBBYEFJna5Io+ +idLKO73zypGl2itp92JUMB8GA1UdIwQYMBaAFJna5Io+idLKO73zypGl2itp92JU +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADQQB71tlkWNFDvMRxtz+a +NYMU1thAVfVFciNXPS07tUduFSwVvYORUxx2w+5JfUdKu69hLpBFVPqvHQjPoQgc +vUBI +-----END CERTIFICATE-----` +const certNotBefore = "2024-11-26T08:50:46Z" +const certNotAfter = "2034-11-24T08:50:46Z" + func TestEnsureNodeKubeconfigs(t *testing.T) { + publicKeyBase64 := base64.StdEncoding.EncodeToString([]byte(publicKey)) + privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(privateKey)) tt := []struct { name string existingObjects []runtime.Object @@ -111,8 +141,8 @@ func TestEnsureNodeKubeconfigs(t *testing.T) { Name: "node-system-admin-client", }, Data: map[string][]byte{ - "tls.crt": []byte("system:admin certificate"), - "tls.key": []byte("system:admin key"), + "tls.crt": []byte(publicKey), + "tls.key": []byte(privateKey), }, }, }, @@ -143,11 +173,13 @@ func TestEnsureNodeKubeconfigs(t *testing.T) { Namespace: "openshift-kube-apiserver", Name: "node-kubeconfigs", Annotations: map[string]string{ - annotations.OpenShiftComponent: "kube-apiserver", + annotations.OpenShiftComponent: "kube-apiserver", + certrotation.CertificateNotBeforeAnnotation: certNotBefore, + certrotation.CertificateNotAfterAnnotation: certNotAfter, }, }, Data: map[string][]byte{ - "localhost.kubeconfig": []byte(`apiVersion: v1 + "localhost.kubeconfig": []byte(fmt.Sprintf(`apiVersion: v1 kind: Config clusters: - cluster: @@ -163,10 +195,10 @@ current-context: system:admin users: - name: system:admin user: - client-certificate-data: c3lzdGVtOmFkbWluIGNlcnRpZmljYXRl - client-key-data: c3lzdGVtOmFkbWluIGtleQ== -`), - "localhost-recovery.kubeconfig": []byte(`apiVersion: v1 + client-certificate-data: %s + client-key-data: %s +`, publicKeyBase64, privateKeyBase64)), + "localhost-recovery.kubeconfig": []byte(fmt.Sprintf(`apiVersion: v1 kind: Config clusters: - cluster: @@ -183,10 +215,10 @@ current-context: system:admin users: - name: system:admin user: - client-certificate-data: c3lzdGVtOmFkbWluIGNlcnRpZmljYXRl - client-key-data: c3lzdGVtOmFkbWluIGtleQ== -`), - "lb-ext.kubeconfig": []byte(`apiVersion: v1 + client-certificate-data: %s + client-key-data: %s +`, publicKeyBase64, privateKeyBase64)), + "lb-ext.kubeconfig": []byte(fmt.Sprintf(`apiVersion: v1 kind: Config clusters: - cluster: @@ -202,10 +234,10 @@ current-context: system:admin users: - name: system:admin user: - client-certificate-data: c3lzdGVtOmFkbWluIGNlcnRpZmljYXRl - client-key-data: c3lzdGVtOmFkbWluIGtleQ== -`), - "lb-int.kubeconfig": []byte(`apiVersion: v1 + client-certificate-data: %s + client-key-data: %s +`, publicKeyBase64, privateKeyBase64)), + "lb-int.kubeconfig": []byte(fmt.Sprintf(`apiVersion: v1 kind: Config clusters: - cluster: @@ -221,9 +253,9 @@ current-context: system:admin users: - name: system:admin user: - client-certificate-data: c3lzdGVtOmFkbWluIGNlcnRpZmljYXRl - client-key-data: c3lzdGVtOmFkbWluIGtleQ== -`), + client-certificate-data: %s + client-key-data: %s +`, publicKeyBase64, privateKeyBase64)), }, }, }, diff --git a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/annotations.go b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/annotations.go index 5ce9fa2934..c4c81b53e1 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/annotations.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/annotations.go @@ -6,6 +6,16 @@ import ( ) const ( + // CertificateNotBeforeAnnotation contains the certificate expiration date in RFC3339 format. + CertificateNotBeforeAnnotation = "auth.openshift.io/certificate-not-before" + // CertificateNotAfterAnnotation contains the certificate expiration date in RFC3339 format. + CertificateNotAfterAnnotation = "auth.openshift.io/certificate-not-after" + // CertificateIssuer contains the common name of the certificate that signed another certificate. + CertificateIssuer = "auth.openshift.io/certificate-issuer" + // CertificateHostnames contains the hostnames used by a signer. + CertificateHostnames = "auth.openshift.io/certificate-hostnames" + // AutoRegenerateAfterOfflineExpiryAnnotation contains a link to PR and an e2e test name which verifies + // that TLS artifact is correctly regenerated after it has expired AutoRegenerateAfterOfflineExpiryAnnotation string = "certificates.openshift.io/auto-regenerate-after-offline-expiry" ) @@ -17,6 +27,10 @@ type AdditionalAnnotations struct { // AutoRegenerateAfterOfflineExpiry contains a link to PR and an e2e test name which verifies // that TLS artifact is correctly regenerated after it has expired AutoRegenerateAfterOfflineExpiry string + // NotBefore contains certificate the certificate creation date in RFC3339 format. + NotBefore string + // NotAfter contains certificate the certificate validity date in RFC3339 format. + NotAfter string } func (a AdditionalAnnotations) EnsureTLSMetadataUpdate(meta *metav1.ObjectMeta) bool { @@ -36,6 +50,14 @@ func (a AdditionalAnnotations) EnsureTLSMetadataUpdate(meta *metav1.ObjectMeta) meta.Annotations[AutoRegenerateAfterOfflineExpiryAnnotation] = a.AutoRegenerateAfterOfflineExpiry modified = true } + if len(a.NotBefore) > 0 && meta.Annotations[CertificateNotBeforeAnnotation] != a.NotBefore { + meta.Annotations[CertificateNotBeforeAnnotation] = a.NotBefore + modified = true + } + if len(a.NotAfter) > 0 && meta.Annotations[CertificateNotAfterAnnotation] != a.NotAfter { + meta.Annotations[CertificateNotAfterAnnotation] = a.NotAfter + modified = true + } return modified } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/client_cert_rotation_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/client_cert_rotation_controller.go index d8569f2c8d..4b7fb1fda9 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/client_cert_rotation_controller.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/client_cert_rotation_controller.go @@ -15,14 +15,6 @@ import ( ) const ( - // CertificateNotBeforeAnnotation contains the certificate expiration date in RFC3339 format. - CertificateNotBeforeAnnotation = "auth.openshift.io/certificate-not-before" - // CertificateNotAfterAnnotation contains the certificate expiration date in RFC3339 format. - CertificateNotAfterAnnotation = "auth.openshift.io/certificate-not-after" - // CertificateIssuer contains the common name of the certificate that signed another certificate. - CertificateIssuer = "auth.openshift.io/certificate-issuer" - // CertificateHostnames contains the hostnames used by a signer. - CertificateHostnames = "auth.openshift.io/certificate-hostnames" // RunOnceContextKey is a context value key that can be used to call the controller Sync() and make it only run the syncWorker once and report error. RunOnceContextKey = "cert-rotation-controller.openshift.io/run-once" ) diff --git a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go index 2eb761bbb3..540114949c 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go @@ -90,7 +90,7 @@ func (c RotatedSigningCASecret) EnsureSigningCertKeyPair(ctx context.Context) (* reason = "secret doesn't exist" } c.EventRecorder.Eventf("SignerUpdateRequired", "%q in %q requires a new signing cert/key pair: %v", c.Name, c.Namespace, reason) - if err := setSigningCertKeyPairSecret(signingCertKeyPairSecret, c.Validity); err != nil { + if err := setSigningCertKeyPairSecret(signingCertKeyPairSecret, c.Validity, c.AdditionalAnnotations); err != nil { return nil, false, err } @@ -194,7 +194,7 @@ func getValidityFromAnnotations(annotations map[string]string) (notBefore time.T } // setSigningCertKeyPairSecret creates a new signing cert/key pair and sets them in the secret -func setSigningCertKeyPairSecret(signingCertKeyPairSecret *corev1.Secret, validity time.Duration) error { +func setSigningCertKeyPairSecret(signingCertKeyPairSecret *corev1.Secret, validity time.Duration, annotations AdditionalAnnotations) error { signerName := fmt.Sprintf("%s_%s@%d", signingCertKeyPairSecret.Namespace, signingCertKeyPairSecret.Name, time.Now().Unix()) ca, err := crypto.MakeSelfSignedCAConfigForDuration(signerName, validity) if err != nil { @@ -215,9 +215,11 @@ func setSigningCertKeyPairSecret(signingCertKeyPairSecret *corev1.Secret, validi } signingCertKeyPairSecret.Data["tls.crt"] = certBytes.Bytes() signingCertKeyPairSecret.Data["tls.key"] = keyBytes.Bytes() - signingCertKeyPairSecret.Annotations[CertificateNotAfterAnnotation] = ca.Certs[0].NotAfter.Format(time.RFC3339) - signingCertKeyPairSecret.Annotations[CertificateNotBeforeAnnotation] = ca.Certs[0].NotBefore.Format(time.RFC3339) + annotations.NotBefore = ca.Certs[0].NotBefore.Format(time.RFC3339) + annotations.NotAfter = ca.Certs[0].NotAfter.Format(time.RFC3339) signingCertKeyPairSecret.Annotations[CertificateIssuer] = ca.Certs[0].Issuer.CommonName + _ = annotations.EnsureTLSMetadataUpdate(&signingCertKeyPairSecret.ObjectMeta) + return nil } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/target.go b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/target.go index b68aea1633..42b7559551 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/target.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/target.go @@ -251,8 +251,8 @@ func setTargetCertKeyPairSecret(targetCertKeyPairSecret *corev1.Secret, validity if err != nil { return err } - targetCertKeyPairSecret.Annotations[CertificateNotAfterAnnotation] = certKeyPair.Certs[0].NotAfter.Format(time.RFC3339) - targetCertKeyPairSecret.Annotations[CertificateNotBeforeAnnotation] = certKeyPair.Certs[0].NotBefore.Format(time.RFC3339) + annotations.NotBefore = certKeyPair.Certs[0].NotBefore.Format(time.RFC3339) + annotations.NotAfter = certKeyPair.Certs[0].NotAfter.Format(time.RFC3339) targetCertKeyPairSecret.Annotations[CertificateIssuer] = certKeyPair.Certs[0].Issuer.CommonName _ = annotations.EnsureTLSMetadataUpdate(&targetCertKeyPairSecret.ObjectMeta) diff --git a/vendor/github.com/openshift/library-go/pkg/operator/events/recorder.go b/vendor/github.com/openshift/library-go/pkg/operator/events/recorder.go index d4341a5904..2918012ff5 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/events/recorder.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/events/recorder.go @@ -2,6 +2,7 @@ package events import ( "context" + "crypto/sha256" "errors" "fmt" "k8s.io/client-go/kubernetes" @@ -224,7 +225,7 @@ func makeEvent(clock clock.PassiveClock, involvedObjRef *corev1.ObjectReference, event := &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ // TODO this is always used to create a unique event. Perhaps we should hash the message to be unique enough for apply-configuration - Name: fmt.Sprintf("%v.%x", involvedObjRef.Name, currentTime.UnixNano()), + Name: fmt.Sprintf("%v.%x.%s", involvedObjRef.Name, currentTime.UnixNano(), hashForEventNameSuffix(eventType, reason, message)), Namespace: involvedObjRef.Namespace, }, InvolvedObject: *involvedObjRef, @@ -238,3 +239,20 @@ func makeEvent(clock clock.PassiveClock, involvedObjRef *corev1.ObjectReference, event.Source.Component = sourceComponent return event } + +func hashForEventNameSuffix(in ...string) string { + data := []byte{} + for _, curr := range in { + data = append(data, []byte(curr)...) + } + if len(data) == 0 { + return "MISSING" + } + + hash := sha256.New() + hash.Write(data) + hashBytes := hash.Sum(nil) + + // we're looking to deconflict names, not protect the crown jewels + return fmt.Sprintf("%x", hashBytes[len(hashBytes)-4:]) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c2ebff9214..904e3c7c6a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -349,7 +349,7 @@ github.com/openshift/client-go/security/informers/externalversions/internalinter github.com/openshift/client-go/security/informers/externalversions/security github.com/openshift/client-go/security/informers/externalversions/security/v1 github.com/openshift/client-go/security/listers/security/v1 -# github.com/openshift/library-go v0.0.0-20241119162247-f466fdd82330 +# github.com/openshift/library-go v0.0.0-20241119162247-f466fdd82330 => github.com/vrutkovs/library-go v0.0.0-20241122091102-4d0a3e9ced24 ## explicit; go 1.22.0 github.com/openshift/library-go/pkg/apiserver/jsonpatch github.com/openshift/library-go/pkg/assets @@ -1542,3 +1542,4 @@ sigs.k8s.io/structured-merge-diff/v4/value ## explicit; go 1.12 sigs.k8s.io/yaml sigs.k8s.io/yaml/goyaml.v2 +# github.com/openshift/library-go => github.com/vrutkovs/library-go v0.0.0-20241122091102-4d0a3e9ced24