Handle the new output of kubectl explain
which indents differently
This commit is contained in:
parent
484f134a98
commit
187ee80ee3
@ -414,6 +414,11 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr
|
|||||||
t.Run("every API can show its docs to the user via kubectl explain, including aggregated APIs, and everything has a description", func(t *testing.T) {
|
t.Run("every API can show its docs to the user via kubectl explain, including aggregated APIs, and everything has a description", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
// Log the version of kubectl to make it appear in CI output for easier debugging.
|
||||||
|
runKubectlVersion(t)
|
||||||
|
|
||||||
|
foundFieldNames := 0
|
||||||
|
|
||||||
for _, r := range resources {
|
for _, r := range resources {
|
||||||
if !strings.Contains(r.GroupVersion, env.APIGroupSuffix) {
|
if !strings.Contains(r.GroupVersion, env.APIGroupSuffix) {
|
||||||
continue
|
continue
|
||||||
@ -428,9 +433,18 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr
|
|||||||
// Note that this test might indirectly depend on the kubectl discovery cache, found in $HOME/.kube/cache/discovery.
|
// Note that this test might indirectly depend on the kubectl discovery cache, found in $HOME/.kube/cache/discovery.
|
||||||
// If you are working on changing API type struct comments, then you may need to clear your discovery cache
|
// If you are working on changing API type struct comments, then you may need to clear your discovery cache
|
||||||
// (or wait ~10 minutes for the cache to expire) for the new comments to appear in the `kubectl explain` results.
|
// (or wait ~10 minutes for the cache to expire) for the new comments to appear in the `kubectl explain` results.
|
||||||
requireKubectlExplainShowsDescriptionForResource(t, a.Name, a.Kind, r.GroupVersion)
|
foundFieldNames += requireKubectlExplainShowsDescriptionForResource(t, a.Name, a.Kind, r.GroupVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Because we are parsing text from `kubectl explain` and because the format of that text can change
|
||||||
|
// over time, make a rudimentary assertion that this test exercised the whole tree of all fields of all
|
||||||
|
// Pinniped API resources. Without this, the test could accidentally skip parts of the tree if the
|
||||||
|
// format has changed.
|
||||||
|
require.Equal(t, 225, foundFieldNames,
|
||||||
|
"Expected to find all known fields of all Pinniped API resources. "+
|
||||||
|
"You may will need to update this expectation if you added new fields to the API types.",
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Pinniped resources do not have short names", func(t *testing.T) {
|
t.Run("Pinniped resources do not have short names", func(t *testing.T) {
|
||||||
@ -599,13 +613,13 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) {
|
|||||||
"did not find expected number of Pinniped CRDs to check for additionalPrinterColumns")
|
"did not find expected number of Pinniped CRDs to check for additionalPrinterColumns")
|
||||||
}
|
}
|
||||||
|
|
||||||
func requireKubectlExplainShowsDescriptionForResource(t *testing.T, resourceName string, resourceKind string, resourceGroupVersion string) {
|
func requireKubectlExplainShowsDescriptionForResource(t *testing.T, resourceName string, resourceKind string, resourceGroupVersion string) int {
|
||||||
// Run kubectl explain on the resource.
|
// Run kubectl explain on the resource.
|
||||||
output := runKubectlExplain(t, resourceName, resourceGroupVersion)
|
output := runKubectlExplain(t, resourceName, resourceGroupVersion)
|
||||||
|
|
||||||
// Check that the output is as expected.
|
// Check that the output is as expected.
|
||||||
if strings.Contains(output, "GROUP: ") {
|
if strings.Contains(output, "GROUP: ") {
|
||||||
// At some point kubectl split the group and version into two separate fields in the output.
|
// Starting in kubectl v1.27, kubectl split the group and version into two separate fields in the output.
|
||||||
splitGroupAndVersion := strings.Split(resourceGroupVersion, "/")
|
splitGroupAndVersion := strings.Split(resourceGroupVersion, "/")
|
||||||
require.Len(t, splitGroupAndVersion, 2)
|
require.Len(t, splitGroupAndVersion, 2)
|
||||||
require.Regexp(t, `(?m)^GROUP:\s+`+regexp.QuoteMeta(splitGroupAndVersion[0])+`$`, output)
|
require.Regexp(t, `(?m)^GROUP:\s+`+regexp.QuoteMeta(splitGroupAndVersion[0])+`$`, output)
|
||||||
@ -620,45 +634,49 @@ func requireKubectlExplainShowsDescriptionForResource(t *testing.T, resourceName
|
|||||||
// Use assert here so that the test keeps running when a description is empty, so we can find all the empty descriptions.
|
// Use assert here so that the test keeps running when a description is empty, so we can find all the empty descriptions.
|
||||||
assert.NotRegexp(t, `(?m)^\s*<empty>\s*$`, output, "resource or field should not have an empty description in kubectl explain")
|
assert.NotRegexp(t, `(?m)^\s*<empty>\s*$`, output, "resource or field should not have an empty description in kubectl explain")
|
||||||
|
|
||||||
if strings.Contains(output, "\nFIELD: ") {
|
|
||||||
// We must have explained a leaf field, which has no children fields.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if resourceName == "whoamirequests.spec" {
|
if resourceName == "whoamirequests.spec" {
|
||||||
// This is an exception because this field is declared to be an empty struct in its type definition. It is
|
// This is an exception because this field is declared to be an empty struct in its type definition. It is
|
||||||
// not a leaf field because it is a struct, but it also has no children because the struct contains no fields.
|
// not a leaf field because it is a struct, but it also has no children because the struct contains no fields.
|
||||||
// So it has neither the `FIELD:` section nor the `FIELDS:` section in the output.
|
// So it has neither the `FIELD:` section nor the `FIELDS:` section in the output.
|
||||||
return
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(output, "\nFIELDS:\n") {
|
||||||
|
// We must have explained a leaf field, which has no children fields.
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, we must have explained a resource or field which has children fields, so it should have a fields list.
|
// Otherwise, we must have explained a resource or field which has children fields, so it should have a fields list.
|
||||||
require.Contains(t, output, "\nFIELDS:\n")
|
|
||||||
|
|
||||||
// Grab everything after the line that says `FIELDS:`.
|
// Grab everything after the line that says `FIELDS:`.
|
||||||
fieldsSectionMatches := regexp.MustCompile(`(?s).+\nFIELDS:\n(.+)`).FindStringSubmatch(output)
|
fieldsSectionMatches := regexp.MustCompile(`(?s).+\nFIELDS:\n(.+)`).FindStringSubmatch(output)
|
||||||
require.Len(t, fieldsSectionMatches, 2)
|
require.Len(t, fieldsSectionMatches, 2)
|
||||||
allFieldsDescribedText := fieldsSectionMatches[1]
|
allFieldsDescribedText := fieldsSectionMatches[1]
|
||||||
|
|
||||||
// Grab the names of all the fields from the fields description.
|
// Grab the names of all the fields from the fields description.
|
||||||
|
foundFieldNames := 0
|
||||||
fieldNames := []string{}
|
fieldNames := []string{}
|
||||||
for _, line := range strings.Split(allFieldsDescribedText, "\n") {
|
for _, line := range strings.Split(allFieldsDescribedText, "\n") {
|
||||||
if strings.HasPrefix(line, " ") {
|
if strings.HasPrefix(line, " ") {
|
||||||
// Field names are indented by exactly three spaces.
|
// Field names are indented by exactly 2 or 3 spaces (depending on the version of kubectl).
|
||||||
// Skip lines that are indented deeper, which are field descriptions.
|
// Skip lines that are indented deeper (by at least 4 spaces), which are field descriptions.
|
||||||
|
// Starting in kubectl v1.27, field names became indented by 3 spaces.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(strings.TrimSpace(line)) == 0 {
|
if len(strings.TrimSpace(line)) == 0 {
|
||||||
// Ignore empty lines.
|
// Ignore empty lines.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Field name lines start with 3 spaces, then the field name, then some tabs/spaces, then the field type.
|
// Field name lines start with exactly 2 or 3 spaces (depending on the version of kubectl), then the field name,
|
||||||
// Grab just the field name.
|
// then some tabs/spaces, then the field type. Grab just the field name.
|
||||||
fieldsNameMatches := regexp.MustCompile(`^ {3}(\S+)\s+`).FindStringSubmatch(line)
|
// Starting in kubectl v1.27, field names became indented by 3 spaces.
|
||||||
require.Len(t, fieldsNameMatches, 2, fmt.Sprintf("field name line which did not match: %s", line))
|
fieldsNameMatches := regexp.MustCompile(`^ {2,3}(\S+)\s+`).FindStringSubmatch(line)
|
||||||
fieldNames = append(fieldNames, fieldsNameMatches[1])
|
require.Len(t, fieldsNameMatches, 2, fmt.Sprintf("field name line which did not match: %s\nwhole actual value:\n%s", line, output))
|
||||||
|
fieldName := fieldsNameMatches[1]
|
||||||
|
fieldNames = append(fieldNames, fieldName)
|
||||||
|
t.Logf(" Found field: %s.%s", resourceName, fieldName)
|
||||||
}
|
}
|
||||||
require.Greater(t, len(fieldNames), 0, "should have found some field names in the kubectl explain output, but didn't find any")
|
require.Greater(t, len(fieldNames), 0, "should have found some field names in the kubectl explain output, but didn't find any")
|
||||||
|
foundFieldNames += len(fieldNames)
|
||||||
|
|
||||||
// For each field, check to see that docs were provided for that field by making a recursive call to this function.
|
// For each field, check to see that docs were provided for that field by making a recursive call to this function.
|
||||||
for _, fieldName := range fieldNames {
|
for _, fieldName := range fieldNames {
|
||||||
@ -666,8 +684,18 @@ func requireKubectlExplainShowsDescriptionForResource(t *testing.T, resourceName
|
|||||||
// Skip these since the docs are implemented by k8s packages, so we can assume that they are correct.
|
// Skip these since the docs are implemented by k8s packages, so we can assume that they are correct.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
requireKubectlExplainShowsDescriptionForResource(t, fmt.Sprintf("%s.%s", resourceName, fieldName), resourceKind, resourceGroupVersion)
|
foundFieldNames += requireKubectlExplainShowsDescriptionForResource(t, fmt.Sprintf("%s.%s", resourceName, fieldName), resourceKind, resourceGroupVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return foundFieldNames
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKubectlVersion(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
t.Log("Running: kubectl version")
|
||||||
|
out, err := exec.Command("kubectl", "version").CombinedOutput()
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Log(string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKubectlExplain(t *testing.T, resourceName string, apiVersion string) string {
|
func runKubectlExplain(t *testing.T, resourceName string, apiVersion string) string {
|
||||||
|
Loading…
Reference in New Issue
Block a user