Skip to content

Commit f5dbb58

Browse files
JoelSpeeddeads2k
authored andcommitted
Allow resource discovery from must-gather
1 parent 84a3e6e commit f5dbb58

File tree

3 files changed

+233
-4
lines changed

3 files changed

+233
-4
lines changed

pkg/manifestclient/encoding.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"strings"
77

8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
89
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
910
"k8s.io/apimachinery/pkg/runtime"
1011
"k8s.io/apimachinery/pkg/runtime/serializer"
@@ -78,3 +79,11 @@ func serializeListObjToJSON(obj *unstructured.UnstructuredList) (string, error)
7879
}
7980
return string(ret) + "\n", nil
8081
}
82+
83+
func serializeAPIResourceListToJSON(obj *metav1.APIResourceList) (string, error) {
84+
ret, err := json.MarshalIndent(obj, "", " ")
85+
if err != nil {
86+
return "", err
87+
}
88+
return string(ret) + "\n", nil
89+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package manifestclient
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io/fs"
7+
"path/filepath"
8+
"strings"
9+
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
apirequest "k8s.io/apiserver/pkg/endpoints/request"
12+
)
13+
14+
func (mrt *manifestRoundTripper) getGroupResourceDiscovery(requestInfo *apirequest.RequestInfo) ([]byte, error) {
15+
if len(requestInfo.Path) == 0 {
16+
return nil, fmt.Errorf("path required for group resource discovery")
17+
}
18+
19+
apiResourceList := &metav1.APIResourceList{}
20+
21+
group, version, err := splitGroupVersionFromRequestPath(requestInfo.Path)
22+
if err != nil {
23+
return nil, fmt.Errorf("unable to split group/version from path: %w", err)
24+
}
25+
26+
apiResourceList.GroupVersion = fmt.Sprintf("%s/%s", group, version)
27+
if group == "core" {
28+
apiResourceList.GroupVersion = version
29+
}
30+
31+
// Map of resource name to APIResource.
32+
apiResources := map[string]metav1.APIResource{}
33+
34+
clusterGroupPath := filepath.Join("cluster-scoped-resources", group)
35+
clusterGroupDirEntries, err := mrt.contentReader.ReadDir(clusterGroupPath)
36+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
37+
return nil, fmt.Errorf("unable to read directory: %w", err)
38+
}
39+
40+
apiResourcesForClusterScope, err := getAPIResourcesFromNamespaceDirEntries(clusterGroupDirEntries, mrt.contentReader, group, version, clusterGroupPath, false /* cluster-scoped */)
41+
if err != nil {
42+
return nil, fmt.Errorf("unable to get resources from cluster-scoped directory: %w", err)
43+
}
44+
for resourceName, apiResource := range apiResourcesForClusterScope {
45+
apiResources[resourceName] = apiResource
46+
}
47+
48+
namespaceDirEntries, err := mrt.contentReader.ReadDir("namespaces")
49+
if err != nil {
50+
return nil, fmt.Errorf("unable to read directory: %w", err)
51+
}
52+
for _, namespaceDirEntry := range namespaceDirEntries {
53+
if !namespaceDirEntry.IsDir() {
54+
continue
55+
}
56+
57+
namespaceGroupPath := filepath.Join("namespaces", namespaceDirEntry.Name(), group)
58+
namespaceGroupDirEntries, err := mrt.contentReader.ReadDir(namespaceGroupPath)
59+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
60+
return nil, fmt.Errorf("unable to read directory: %w", err)
61+
} else if errors.Is(err, fs.ErrNotExist) {
62+
// No resources for this namespace.
63+
continue
64+
}
65+
66+
apiResourcesForNamespace, err := getAPIResourcesFromNamespaceDirEntries(namespaceGroupDirEntries, mrt.contentReader, group, version, namespaceGroupPath, true /* namespaced */)
67+
if err != nil {
68+
return nil, fmt.Errorf("unable to get resources from namespace directory: %w", err)
69+
}
70+
71+
for resourceName, apiResource := range apiResourcesForNamespace {
72+
apiResources[resourceName] = apiResource
73+
}
74+
}
75+
76+
for _, apiResource := range apiResources {
77+
apiResourceList.APIResources = append(apiResourceList.APIResources, apiResource)
78+
}
79+
80+
ret, err := serializeAPIResourceListToJSON(apiResourceList)
81+
if err != nil {
82+
return nil, fmt.Errorf("failed to serialize group resource discovery: %v", err)
83+
}
84+
return []byte(ret), nil
85+
}
86+
87+
func splitGroupVersionFromRequestPath(path string) (string, string, error) {
88+
if path == "/api/v1" {
89+
return "core", "v1", nil
90+
}
91+
92+
parts := strings.Split(path, "/")
93+
if len(parts) != 4 {
94+
return "", "", fmt.Errorf("invalid path: %s", path)
95+
}
96+
97+
return parts[2], parts[3], nil
98+
}
99+
100+
func getResourceDirAPIServerListEntry(contentReader RawReader, groupPath, resourceName, group, version string, namespaced bool) (*metav1.APIResource, error) {
101+
resourceDirEntries, err := contentReader.ReadDir(filepath.Join(groupPath, resourceName))
102+
if err != nil {
103+
return nil, fmt.Errorf("unable to read directory: %w", err)
104+
}
105+
for _, fileEntry := range resourceDirEntries {
106+
if !strings.HasSuffix(fileEntry.Name(), ".yaml") {
107+
// There shouldn't be anything that hits this, but ignore it if there is.
108+
continue
109+
}
110+
111+
individualObj, individualErr := readIndividualFile(contentReader, filepath.Join(groupPath, resourceName, fileEntry.Name()))
112+
if individualErr != nil {
113+
return nil, fmt.Errorf("unable to read file: %w", individualErr)
114+
}
115+
116+
groupVersion := fmt.Sprintf("%s/%s", group, version)
117+
if group == "core" {
118+
group = ""
119+
groupVersion = version
120+
}
121+
122+
if individualObj.GetAPIVersion() != groupVersion {
123+
continue
124+
}
125+
126+
// No point checking further, all files should produce the same APIResource.
127+
return &metav1.APIResource{
128+
Name: resourceName,
129+
Kind: individualObj.GetKind(),
130+
Group: group,
131+
Version: version,
132+
Namespaced: namespaced,
133+
Verbs: []string{"get", "list", "watch"},
134+
}, nil
135+
}
136+
137+
return nil, nil
138+
}
139+
140+
func getAPIResourcesFromNamespaceDirEntries(dirEntries []fs.DirEntry, contentReader RawReader, group, version string, basePath string, namespaced bool) (map[string]metav1.APIResource, error) {
141+
apiResources := map[string]metav1.APIResource{}
142+
for _, dirEntry := range dirEntries {
143+
// Directories are named after the resource and contain individual resources.
144+
if dirEntry.IsDir() {
145+
apiResource, err := getResourceDirAPIServerListEntry(contentReader, basePath, dirEntry.Name(), group, version, namespaced)
146+
if err != nil {
147+
return nil, fmt.Errorf("unable to get resource from directory: %w", err)
148+
}
149+
if apiResource != nil {
150+
apiResources[dirEntry.Name()] = *apiResource
151+
}
152+
}
153+
154+
if !strings.HasSuffix(dirEntry.Name(), ".yaml") {
155+
// There shouldn't be anything that hits this, but ignore it if there is.
156+
continue
157+
}
158+
159+
resourceName := strings.TrimSuffix(dirEntry.Name(), ".yaml")
160+
if _, ok := apiResources[resourceName]; ok {
161+
// We already have this resource.
162+
continue
163+
}
164+
165+
// Files are named after the resource and contain a list of resources.
166+
listObj, err := readListFile(contentReader, filepath.Join(basePath, dirEntry.Name()))
167+
if err != nil {
168+
return nil, fmt.Errorf("unable to read list file: %w", err)
169+
}
170+
171+
for _, obj := range listObj.Items {
172+
if obj.GetAPIVersion() != fmt.Sprintf("%s/%s", group, version) {
173+
continue
174+
}
175+
176+
apiResources[resourceName] = metav1.APIResource{
177+
Name: resourceName,
178+
Kind: obj.GetKind(),
179+
Group: group,
180+
Version: version,
181+
Namespaced: namespaced,
182+
Verbs: []string{"get", "list", "watch"},
183+
}
184+
185+
// Once we find a resource in the expected group/version, we can break.
186+
// Anything else would produce the same APIResource.
187+
break
188+
}
189+
}
190+
191+
return apiResources, nil
192+
}

pkg/manifestclient/roundtripper.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"path/filepath"
1212
"strconv"
13+
"strings"
1314
"time"
1415

1516
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -107,20 +108,29 @@ func (mrt *manifestRoundTripper) RoundTrip(req *http.Request) (*http.Response, e
107108
if err != nil {
108109
return nil, fmt.Errorf("failed reading requestInfo: %w", err)
109110
}
110-
if !requestInfo.IsResourceRequest {
111+
112+
isDiscovery := isServerGroupResourceDiscovery(requestInfo.Path)
113+
if !requestInfo.IsResourceRequest && !isDiscovery {
111114
return nil, fmt.Errorf("non-resource requests are not supported by this implementation")
112115
}
113116
if len(requestInfo.Subresource) != 0 {
114117
return nil, fmt.Errorf("subresource %v is not supported by this implementation", requestInfo.Subresource)
115118
}
119+
if isDiscovery && requestInfo.Verb != "get" {
120+
// TODO handle group resource discovery
121+
return nil, fmt.Errorf("group resource discovery is not supported unless it is a GET request")
122+
}
116123

117124
var returnBody []byte
118125
var returnErr error
119126
switch requestInfo.Verb {
120127
case "get":
121-
// TODO handle label and field selectors because single item lists are GETs
122-
returnBody, returnErr = mrt.get(requestInfo)
123-
128+
if isDiscovery {
129+
returnBody, returnErr = mrt.getGroupResourceDiscovery(requestInfo)
130+
} else {
131+
// TODO handle label and field selectors because single item lists are GETs
132+
returnBody, returnErr = mrt.get(requestInfo)
133+
}
124134
case "list":
125135
// TODO handle label and field selectors
126136
returnBody, returnErr = mrt.list(requestInfo)
@@ -161,6 +171,9 @@ func (mrt *manifestRoundTripper) RoundTrip(req *http.Request) (*http.Response, e
161171
resp.StatusCode = http.StatusOK
162172
resp.Status = http.StatusText(resp.StatusCode)
163173
resp.Body = io.NopCloser(bytes.NewReader(returnBody))
174+
// We always return application/json. Avoid clients expecting proto for built-ins.
175+
resp.Header = make(http.Header)
176+
resp.Header.Set("Content-Type", "application/json")
164177
}
165178

166179
return resp, nil
@@ -172,3 +185,18 @@ func newNotFound(requestInfo *apirequest.RequestInfo) error {
172185
Resource: requestInfo.Resource,
173186
}, requestInfo.Name)
174187
}
188+
189+
// checking for /apis/<group>/<version>
190+
// In this case we will return the list of resources for the group.
191+
func isServerGroupResourceDiscovery(path string) bool {
192+
// Corev1 is a special case.
193+
if path == "/api/v1" {
194+
return true
195+
}
196+
197+
parts := strings.Split(path, "/")
198+
if len(parts) != 4 {
199+
return false
200+
}
201+
return parts[0] == "" && parts[1] == "apis"
202+
}

0 commit comments

Comments
 (0)