Skip to content

Commit f785651

Browse files
committed
add ability to wire generated clients to directories
1 parent 2a01e96 commit f785651

File tree

5 files changed

+716
-0
lines changed

5 files changed

+716
-0
lines changed

pkg/manifestclient/encoding.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package manifestclient
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
8+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9+
"k8s.io/apimachinery/pkg/runtime"
10+
"k8s.io/apimachinery/pkg/runtime/serializer"
11+
)
12+
13+
func individualFromList(objList *unstructured.UnstructuredList, name string) (*unstructured.Unstructured, error) {
14+
individualKind := strings.TrimSuffix(objList.GetKind(), "List")
15+
16+
for _, obj := range objList.Items {
17+
if obj.GetName() != name {
18+
continue
19+
}
20+
21+
ret := obj.DeepCopy()
22+
ret.SetKind(individualKind)
23+
return ret, nil
24+
}
25+
26+
return nil, fmt.Errorf("not found in this list")
27+
}
28+
29+
func readListFile(contentReader RawReader, path string) (*unstructured.UnstructuredList, error) {
30+
content, err := contentReader.ReadFile(path)
31+
if err != nil {
32+
return nil, fmt.Errorf("unable to read %q: %w", path, err)
33+
}
34+
35+
return decodeListObj(content)
36+
}
37+
38+
func readIndividualFile(contentReader RawReader, path string) (*unstructured.Unstructured, error) {
39+
content, err := contentReader.ReadFile(path)
40+
if err != nil {
41+
return nil, fmt.Errorf("unable to read %q: %w", path, err)
42+
}
43+
44+
return decodeIndividualObj(content)
45+
}
46+
47+
var localScheme = runtime.NewScheme()
48+
var codecs = serializer.NewCodecFactory(localScheme)
49+
50+
func decodeIndividualObj(content []byte) (*unstructured.Unstructured, error) {
51+
obj, _, err := codecs.UniversalDecoder().Decode(content, nil, &unstructured.Unstructured{})
52+
if err != nil {
53+
return nil, fmt.Errorf("unable to decode: %w", err)
54+
}
55+
return obj.(*unstructured.Unstructured), nil
56+
}
57+
58+
func decodeListObj(content []byte) (*unstructured.UnstructuredList, error) {
59+
obj, _, err := codecs.UniversalDecoder().Decode(content, nil, &unstructured.UnstructuredList{})
60+
if err != nil {
61+
return nil, fmt.Errorf("unable to decode: %w", err)
62+
}
63+
return obj.(*unstructured.UnstructuredList), nil
64+
}
65+
66+
func serializeIndividualObjToJSON(obj *unstructured.Unstructured) (string, error) {
67+
ret, err := json.MarshalIndent(obj.Object, "", " ")
68+
if err != nil {
69+
return "", err
70+
}
71+
return string(ret) + "\n", nil
72+
}
73+
74+
func serializeListObjToJSON(obj *unstructured.UnstructuredList) (string, error) {
75+
ret, err := json.MarshalIndent(obj, "", " ")
76+
if err != nil {
77+
return "", err
78+
}
79+
return string(ret) + "\n", nil
80+
}

pkg/manifestclient/get.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package manifestclient
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io/fs"
7+
"path/filepath"
8+
9+
apirequest "k8s.io/apiserver/pkg/endpoints/request"
10+
)
11+
12+
// must-gather has a few different ways to store resources
13+
// 1. cluster-scoped-resource/group/resource/<name>.yaml
14+
// 2. cluster-scoped-resource/group/resource.yaml
15+
// 3. namespaces/<namespace>/group/resource/<name>.yaml
16+
// 4. namespaces/<namespace>/group/resource.yaml
17+
// we have to choose which to prefer and we should always prefer the #2 if it's available.
18+
// Keep in mind that to produce a cluster-scoped list of namespaced resources, you can need to navigate many namespaces.
19+
func (mrt *manifestRoundTripper) get(requestInfo *apirequest.RequestInfo) ([]byte, error) {
20+
if len(requestInfo.Name) == 0 {
21+
return nil, fmt.Errorf("name required for GET")
22+
}
23+
if len(requestInfo.Resource) == 0 {
24+
return nil, fmt.Errorf("resource required for GET")
25+
}
26+
requiredAPIVersion := fmt.Sprintf("%s/%s", requestInfo.APIGroup, requestInfo.APIVersion)
27+
if len(requestInfo.APIGroup) == 0 {
28+
requiredAPIVersion = fmt.Sprintf("%s", requestInfo.APIVersion)
29+
}
30+
31+
individualFilePath := individualFileLocation(requestInfo)
32+
individualObj, individualErr := readIndividualFile(mrt.contentReader, individualFilePath)
33+
switch {
34+
case errors.Is(individualErr, fs.ErrNotExist):
35+
// try for the list
36+
case individualErr != nil:
37+
return nil, fmt.Errorf("unable to read file: %w", individualErr)
38+
default:
39+
if individualObj.GetAPIVersion() != requiredAPIVersion {
40+
return nil, fmt.Errorf("actual version %v does not match request %v", individualObj.GetAPIVersion(), requiredAPIVersion)
41+
}
42+
ret, err := serializeIndividualObjToJSON(individualObj)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to serialize %v: %v", individualFilePath, err)
45+
}
46+
return []byte(ret), nil
47+
}
48+
49+
listFilePath := listFileLocation(requestInfo)
50+
listObj, listErr := readListFile(mrt.contentReader, listFilePath)
51+
switch {
52+
case errors.Is(listErr, fs.ErrNotExist):
53+
// we need this to be a not-found when sent back
54+
return nil, newNotFound(requestInfo)
55+
56+
case listErr != nil:
57+
return nil, fmt.Errorf("unable to read file: %w", listErr)
58+
default:
59+
obj, err := individualFromList(listObj, requestInfo.Name)
60+
if obj == nil {
61+
return nil, newNotFound(requestInfo)
62+
}
63+
if obj.GetAPIVersion() != requiredAPIVersion {
64+
return nil, fmt.Errorf("actual version %v does not match request %v", obj.GetAPIVersion(), requiredAPIVersion)
65+
}
66+
67+
ret, err := serializeIndividualObjToJSON(obj)
68+
if err != nil {
69+
return nil, fmt.Errorf("failed to serialize %v: %v", listFilePath, err)
70+
}
71+
return []byte(ret), nil
72+
}
73+
}
74+
75+
func individualFileLocation(requestInfo *apirequest.RequestInfo) string {
76+
fileParts := []string{}
77+
78+
if len(requestInfo.Namespace) > 0 {
79+
fileParts = append(fileParts, "namespaces", requestInfo.Namespace)
80+
} else {
81+
fileParts = append(fileParts, "cluster-scoped-resources")
82+
}
83+
84+
if len(requestInfo.APIGroup) > 0 {
85+
fileParts = append(fileParts, requestInfo.APIGroup)
86+
} else {
87+
fileParts = append(fileParts, "core")
88+
}
89+
90+
fileParts = append(fileParts, requestInfo.Resource, fmt.Sprintf("%s.yaml", requestInfo.Name))
91+
92+
return filepath.Join(fileParts...)
93+
}
94+
95+
func listFileLocation(requestInfo *apirequest.RequestInfo) string {
96+
fileParts := []string{}
97+
98+
if len(requestInfo.Namespace) > 0 {
99+
fileParts = append(fileParts, "namespaces", requestInfo.Namespace)
100+
} else {
101+
fileParts = append(fileParts, "cluster-scoped-resources")
102+
}
103+
104+
if len(requestInfo.APIGroup) > 0 {
105+
fileParts = append(fileParts, requestInfo.APIGroup)
106+
} else {
107+
fileParts = append(fileParts, "core")
108+
}
109+
110+
fileParts = append(fileParts, fmt.Sprintf("%s.yaml", requestInfo.Resource))
111+
112+
return filepath.Join(fileParts...)
113+
}

pkg/manifestclient/list.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package manifestclient
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io/fs"
7+
"path/filepath"
8+
9+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
10+
apirequest "k8s.io/apiserver/pkg/endpoints/request"
11+
)
12+
13+
// must-gather has a few different ways to store resources
14+
// 1. cluster-scoped-resource/group/resource/<name>.yaml
15+
// 2. cluster-scoped-resource/group/resource.yaml
16+
// 3. namespaces/<namespace>/group/resource/<name>.yaml
17+
// 4. namespaces/<namespace>/group/resource.yaml
18+
// we have to choose which to prefer and we should always prefer the #2 if it's available.
19+
// Keep in mind that to produce a cluster-scoped list of namespaced resources, you can need to navigate many namespaces.
20+
func (mrt *manifestRoundTripper) list(requestInfo *apirequest.RequestInfo) ([]byte, error) {
21+
var retList *unstructured.UnstructuredList
22+
possibleListFiles, err := allPossibleListFileLocations(mrt.contentReader, requestInfo)
23+
if err != nil {
24+
return nil, fmt.Errorf("unable to determine list file locations: %w", err)
25+
}
26+
for _, listFile := range possibleListFiles {
27+
currList, err := readListFile(mrt.contentReader, listFile)
28+
switch {
29+
case errors.Is(err, fs.ErrNotExist):
30+
// do nothing, it's possible, not guaranteed
31+
continue
32+
case err != nil:
33+
return nil, fmt.Errorf("unable to determine read list file %v: %w", listFile, err)
34+
}
35+
36+
if retList == nil {
37+
retList = currList
38+
continue
39+
}
40+
for i := range currList.Items {
41+
retList.Items = append(retList.Items, currList.Items[i])
42+
}
43+
}
44+
if retList != nil {
45+
ret, err := serializeListObjToJSON(retList)
46+
if err != nil {
47+
return nil, fmt.Errorf("failed to serialize: %v", err)
48+
}
49+
return []byte(ret), nil
50+
}
51+
52+
retList = &unstructured.UnstructuredList{
53+
Object: map[string]interface{}{},
54+
Items: nil,
55+
}
56+
individualFiles, err := allIndividualFileLocations(mrt.contentReader, requestInfo)
57+
if err != nil {
58+
return nil, fmt.Errorf("unable to determine individual file locations: %w", err)
59+
}
60+
for _, individualFile := range individualFiles {
61+
currInstance, err := readIndividualFile(mrt.contentReader, individualFile)
62+
switch {
63+
case errors.Is(err, fs.ErrNotExist):
64+
// do nothing, it's possible, not guaranteed
65+
continue
66+
case err != nil:
67+
return nil, fmt.Errorf("unable to determine read list file %v: %w", individualFile, err)
68+
}
69+
70+
retList.Items = append(retList.Items, *currInstance)
71+
}
72+
if len(retList.Items) > 0 {
73+
retList.SetKind(retList.Items[0].GetKind() + "List")
74+
retList.SetAPIVersion(retList.Items[0].GetAPIVersion())
75+
76+
ret, err := serializeListObjToJSON(retList)
77+
if err != nil {
78+
return nil, fmt.Errorf("failed to serialize: %v", err)
79+
}
80+
return []byte(ret), nil
81+
}
82+
83+
return nil, fmt.Errorf("unable to read any file so we have no Kind")
84+
}
85+
86+
func allIndividualFileLocations(contentReader RawReader, requestInfo *apirequest.RequestInfo) ([]string, error) {
87+
resourceDirectoryParts := []string{}
88+
if len(requestInfo.APIGroup) > 0 {
89+
resourceDirectoryParts = append(resourceDirectoryParts, requestInfo.APIGroup)
90+
} else {
91+
resourceDirectoryParts = append(resourceDirectoryParts, "core")
92+
}
93+
resourceDirectoryParts = append(resourceDirectoryParts, requestInfo.Resource)
94+
95+
resourceDirectoriesToCheckForIndividualFiles := []string{}
96+
if len(requestInfo.Namespace) > 0 {
97+
parts := append([]string{"namespaces", requestInfo.Namespace}, resourceDirectoryParts...)
98+
resourceDirectoriesToCheckForIndividualFiles = append(resourceDirectoriesToCheckForIndividualFiles, filepath.Join(parts...))
99+
100+
} else {
101+
clusterParts := append([]string{"cluster-scoped-resources"}, resourceDirectoryParts...)
102+
resourceDirectoriesToCheckForIndividualFiles = append(resourceDirectoriesToCheckForIndividualFiles, filepath.Join(clusterParts...))
103+
104+
namespaces, err := allNamespacesWithData(contentReader)
105+
if err != nil {
106+
return nil, fmt.Errorf("unable to read namespaces")
107+
}
108+
for _, ns := range namespaces {
109+
nsParts := append([]string{"namespaces", ns}, resourceDirectoryParts...)
110+
resourceDirectoriesToCheckForIndividualFiles = append(resourceDirectoriesToCheckForIndividualFiles, filepath.Join(nsParts...))
111+
}
112+
}
113+
114+
allIndividualFilePaths := []string{}
115+
for _, resourceDirectory := range resourceDirectoriesToCheckForIndividualFiles {
116+
individualFiles, err := contentReader.ReadDir(resourceDirectory)
117+
switch {
118+
case errors.Is(err, fs.ErrNotExist):
119+
continue
120+
case err != nil:
121+
return nil, fmt.Errorf("unable to read resourceDir")
122+
}
123+
124+
for _, curr := range individualFiles {
125+
allIndividualFilePaths = append(allIndividualFilePaths, filepath.Join(resourceDirectory, curr.Name()))
126+
}
127+
}
128+
129+
return allIndividualFilePaths, nil
130+
}
131+
132+
func allPossibleListFileLocations(contentReader RawReader, requestInfo *apirequest.RequestInfo) ([]string, error) {
133+
resourceListFileParts := []string{}
134+
if len(requestInfo.APIGroup) > 0 {
135+
resourceListFileParts = append(resourceListFileParts, requestInfo.APIGroup)
136+
} else {
137+
resourceListFileParts = append(resourceListFileParts, "core")
138+
}
139+
resourceListFileParts = append(resourceListFileParts, fmt.Sprintf("%s.yaml", requestInfo.Resource))
140+
141+
allPossibleListFileLocations := []string{}
142+
if len(requestInfo.Namespace) > 0 {
143+
parts := append([]string{"namespaces", requestInfo.Namespace}, resourceListFileParts...)
144+
allPossibleListFileLocations = append(allPossibleListFileLocations, filepath.Join(parts...))
145+
146+
} else {
147+
clusterParts := append([]string{"cluster-scoped-resources"}, resourceListFileParts...)
148+
allPossibleListFileLocations = append(allPossibleListFileLocations, filepath.Join(clusterParts...))
149+
150+
namespaces, err := allNamespacesWithData(contentReader)
151+
if err != nil {
152+
return nil, fmt.Errorf("unable to read namespaces")
153+
}
154+
for _, ns := range namespaces {
155+
nsParts := append([]string{"namespaces", ns}, resourceListFileParts...)
156+
allPossibleListFileLocations = append(allPossibleListFileLocations, filepath.Join(nsParts...))
157+
}
158+
}
159+
160+
return allPossibleListFileLocations, nil
161+
}
162+
163+
func allNamespacesWithData(contentReader RawReader) ([]string, error) {
164+
nsDirs, err := contentReader.ReadDir("namespaces")
165+
if err != nil {
166+
return nil, fmt.Errorf("failed to read allNamespacesWithData: %w", err)
167+
}
168+
169+
ret := []string{}
170+
for _, curr := range nsDirs {
171+
ret = append(ret, curr.Name())
172+
}
173+
174+
return ret, nil
175+
}

0 commit comments

Comments
 (0)