diff --git a/cmd/prune-junit-xml/prunexml.go b/cmd/prune-junit-xml/prunexml.go
new file mode 100644
index 00000000000..f4a5421736b
--- /dev/null
+++ b/cmd/prune-junit-xml/prunexml.go
@@ -0,0 +1,150 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+ "encoding/xml"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+)
+
+// JUnitTestSuites is a collection of JUnit test suites.
+type JUnitTestSuites struct {
+ XMLName xml.Name `xml:"testsuites"`
+ Suites []JUnitTestSuite `xml:"testsuite"`
+}
+
+// JUnitTestSuite is a single JUnit test suite which may contain many
+// testcases.
+type JUnitTestSuite struct {
+ XMLName xml.Name `xml:"testsuite"`
+ Tests int `xml:"tests,attr"`
+ Failures int `xml:"failures,attr"`
+ Time string `xml:"time,attr"`
+ Name string `xml:"name,attr"`
+ Properties []JUnitProperty `xml:"properties>property,omitempty"`
+ TestCases []JUnitTestCase `xml:"testcase"`
+ Timestamp string `xml:"timestamp,attr"`
+}
+
+// JUnitTestCase is a single test case with its result.
+type JUnitTestCase struct {
+ XMLName xml.Name `xml:"testcase"`
+ Classname string `xml:"classname,attr"`
+ Name string `xml:"name,attr"`
+ Time string `xml:"time,attr"`
+ SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"`
+ Failure *JUnitFailure `xml:"failure,omitempty"`
+}
+
+// JUnitSkipMessage contains the reason why a testcase was skipped.
+type JUnitSkipMessage struct {
+ Message string `xml:"message,attr"`
+}
+
+// JUnitProperty represents a key/value pair used to define properties.
+type JUnitProperty struct {
+ Name string `xml:"name,attr"`
+ Value string `xml:"value,attr"`
+}
+
+// JUnitFailure contains data related to a failed test.
+type JUnitFailure struct {
+ Message string `xml:"message,attr"`
+ Type string `xml:"type,attr"`
+ Contents string `xml:",chardata"`
+}
+
+func main() {
+ maxTextSize := flag.Int("max-text-size", 1, "maximum size of attribute or text (in MB)")
+ flag.Parse()
+
+ if flag.NArg() > 0 {
+ for _, path := range flag.Args() {
+ fmt.Printf("processing junit xml file : %s\n", path)
+ xmlReader, err := os.Open(path)
+ if err != nil {
+ panic(err)
+ }
+ defer xmlReader.Close()
+ suites, err := fetchXML(xmlReader) // convert MB into bytes (roughly!)
+ if err != nil {
+ panic(err)
+ }
+
+ pruneXML(suites, *maxTextSize*1e6) // convert MB into bytes (roughly!)
+
+ xmlWriter, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+ if err != nil {
+ panic(err)
+ }
+ defer xmlWriter.Close()
+ err = streamXML(xmlWriter, suites)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println("done.")
+ }
+ }
+}
+
+func pruneXML(suites *JUnitTestSuites, maxBytes int) {
+ for _, suite := range suites.Suites {
+ for _, testcase := range suite.TestCases {
+ if testcase.SkipMessage != nil {
+ if len(testcase.SkipMessage.Message) > maxBytes {
+ fmt.Printf("clipping skip message in test case : %s\n", testcase.Name)
+ testcase.SkipMessage.Message = "[... clipped...]" +
+ testcase.SkipMessage.Message[len(testcase.SkipMessage.Message)-maxBytes:]
+ }
+ }
+ if testcase.Failure != nil {
+ if len(testcase.Failure.Contents) > maxBytes {
+ fmt.Printf("clipping failure message in test case : %s\n", testcase.Name)
+ testcase.Failure.Contents = "[... clipped...]" +
+ testcase.Failure.Contents[len(testcase.Failure.Contents)-maxBytes:]
+ }
+ }
+ }
+ }
+}
+
+func fetchXML(xmlReader io.Reader) (*JUnitTestSuites, error) {
+ decoder := xml.NewDecoder(xmlReader)
+ var suites JUnitTestSuites
+ err := decoder.Decode(&suites)
+ if err != nil {
+ return nil, err
+ }
+ return &suites, nil
+}
+
+func streamXML(writer io.Writer, in *JUnitTestSuites) error {
+ _, err := writer.Write([]byte("\n"))
+ if err != nil {
+ return err
+ }
+ encoder := xml.NewEncoder(writer)
+ encoder.Indent("", "\t")
+ err = encoder.Encode(in)
+ if err != nil {
+ return err
+ }
+ return encoder.Flush()
+}
diff --git a/cmd/prune-junit-xml/prunexml_test.go b/cmd/prune-junit-xml/prunexml_test.go
new file mode 100644
index 00000000000..6f9aef6ee7b
--- /dev/null
+++ b/cmd/prune-junit-xml/prunexml_test.go
@@ -0,0 +1,66 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "github.com/stretchr/testify/assert"
+ "strings"
+ "testing"
+)
+
+func TestPruneXML(t *testing.T) {
+ sourceXML := `
+
+
+
+
+
+
+
+
+
+
+
/home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/internal/transport/transport.go:169 +0x147
k8s.io/kubernetes/vendor/google.golang.org/grpc/internal/transport.(*transportReader).Read(0xc0e5f8edb0, {0xc0efe16f88?, 0xc1169d3a88?, 0x1804787?})
/home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/internal/transport/transport.go:483 +0x32
io.ReadAtLeast({0x55c5720, 0xc0e5f8edb0}, {0xc0efe16f88, 0x5, 0x5}, 0x5)
/usr/local/go/src/io/io.go:331 +0x9a
io.ReadFull(...)
/usr/local/go/src/io/io.go:350
k8s.io/kubernetes/vendor/google.golang.org/grpc/internal/transport.(*Stream).Read(0xc0f3cd67e0, {0xc0efe16f88, 0x5, 0x5})
/home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/internal/transport/transport.go:467 +0xa5
k8s.io/kubernetes/vendor/google.golang.org/grpc.(*parser).recvMsg(0xc0efe16f78, 0x7fffffff)
/home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/rpc_util.go:559 +0x47
k8s.io/kubernetes/vendor/google.golang.org/grpc.recvAndDecompress(0xc1169d3c58?, 0xc0f3cd67e0, {0x0, 0x0}, 0x7fffffff, 0x0, {0x0, 0x0})
/home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/rpc_util.go:690 +0x66
k8s.io/kubernetes/vendor/google.golang.org/grpc.recv(0x172b28f?, {0x7f837c291d58, 0x7f84350}, 0x6f5a274d6e8f284c?, {0x0?, 0x0?}, {0x4be7d40, 0xc0f8c01d50}, 0x0?, 0x0, ...)
/home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/rpc_util.go:758 +0x6e
k8s.io/kubernetes/vendor/google.golang.org/grpc.(*csAttempt).recvMsg(0xc0eb72d800, {0x4be7d40?, 0xc0f8c01d50}, 0x2?)
/home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/stream.go:970 +0x2b0
k8s.io/kubernetes/vendor/google.golang.org/grpc.(*clientStream).RecvMsg.func1(0x4be7d40?)
/home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/stream.go:821 +0x25
k8s.io/kubernetes/vendor/google.golang.org/grpc.(*clientStream).withRetry(0xc0f3cd65a0, 0xc1169d3e78, 0xc1169d3e48)
/home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/stream.go:675 +0x2f6
k8s.io/kubernetes/vendor/google.golang.org/grpc.(*clientStream).RecvMsg(0xc0f3cd65a0, {0x4be7d40?, 0xc0f8c01d50?})
/home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/stream.go:820 +0x11f
k8s.io/kubernetes/vendor/github.com/grpc-ecosystem/go-grpc-prometheus.(*monitoredClientStream).RecvMsg(0xc0efe16f90, {0x4be7d40?, 0xc0f8c01d50?})
/home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/github.com/grpc-ecosystem/go-grpc-prometheus/client_metrics.go:160
+
+
+`
+
+ outputXML := `
+
+
+
+
+
+
+
+
+
+
+ [... clipped...]prometheus/client_metrics.go:160
+
+
+`
+ suites, _ := fetchXML(strings.NewReader(sourceXML))
+ pruneXML(suites, 32)
+ var output bytes.Buffer
+ writer := bufio.NewWriter(&output)
+ _ = streamXML(writer, suites)
+ _ = writer.Flush()
+ assert.Equal(t, outputXML, string(output.Bytes()), "xml was not pruned correctly")
+}
diff --git a/hack/make-rules/test.sh b/hack/make-rules/test.sh
index 4c0015ebd62..33e8c311dc7 100755
--- a/hack/make-rules/test.sh
+++ b/hack/make-rules/test.sh
@@ -240,6 +240,14 @@ produceJUnitXMLReport() {
rm "${junit_filename_prefix}"*.stdout
fi
+ if ! command -v prune-junit-xml >/dev/null 2>&1; then
+ kube::log::status "prune-junit-xml not found; installing from hack/tools"
+ pushd "${KUBE_ROOT}/cmd/prune-junit-xml" >/dev/null
+ GO111MODULE=on go install .
+ popd >/dev/null
+ fi
+ prune-junit-xml "${junit_xml_filename}"
+
kube::log::status "Saved JUnit XML test report to ${junit_xml_filename}"
}