-
Notifications
You must be signed in to change notification settings - Fork 217
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add ability to wire generated clients to directories
- Loading branch information
Showing
2 changed files
with
450 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,352 @@ | ||
package manifestclient | ||
|
||
import ( | ||
"bytes" | ||
"embed" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"io/fs" | ||
"net/http" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
"k8s.io/apimachinery/pkg/runtime/schema" | ||
|
||
"k8s.io/apimachinery/pkg/runtime" | ||
"k8s.io/apimachinery/pkg/runtime/serializer" | ||
|
||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
|
||
apirequest "k8s.io/apiserver/pkg/endpoints/request" | ||
|
||
"k8s.io/apimachinery/pkg/util/sets" | ||
"k8s.io/apiserver/pkg/server" | ||
) | ||
|
||
type manifestRoundTripper struct { | ||
contentReader RawReader | ||
|
||
// requestInfoResolver is the same type constructed the same way as the kube-apiserver | ||
requestInfoResolver *apirequest.RequestInfoFactory | ||
} | ||
|
||
type RawReader interface { | ||
fs.FS | ||
fs.ReadFileFS | ||
fs.ReadDirFS | ||
} | ||
|
||
func NewTestingRoundTripper(embedFS embed.FS, prefix string) (*manifestRoundTripper, error) { | ||
return newRoundTripper(newPrefixedReader(embedFS, prefix)) | ||
} | ||
|
||
func NewRoundTripper(mustGatherDir string) (*manifestRoundTripper, error) { | ||
return newRoundTripper(newMustGatherReader(mustGatherDir)) | ||
} | ||
|
||
func newRoundTripper(contentReader RawReader) (*manifestRoundTripper, error) { | ||
return &manifestRoundTripper{ | ||
contentReader: contentReader, | ||
requestInfoResolver: server.NewRequestInfoResolver(&server.Config{ | ||
LegacyAPIGroupPrefixes: sets.NewString(server.DefaultLegacyAPIPrefix), | ||
}), | ||
}, nil | ||
} | ||
|
||
type prefixedContentReader struct { | ||
embedFS embed.FS | ||
prefix string | ||
} | ||
|
||
func newPrefixedReader(embedFS embed.FS, prefix string) RawReader { | ||
return &prefixedContentReader{ | ||
embedFS: embedFS, | ||
prefix: prefix, | ||
} | ||
} | ||
|
||
func (r *prefixedContentReader) Open(name string) (fs.File, error) { | ||
return r.embedFS.Open(filepath.Join(r.prefix, name)) | ||
} | ||
|
||
func (r *prefixedContentReader) ReadFile(name string) ([]byte, error) { | ||
return fs.ReadFile(r.embedFS, filepath.Join(r.prefix, name)) | ||
} | ||
|
||
func (r *prefixedContentReader) ReadDir(name string) ([]fs.DirEntry, error) { | ||
return fs.ReadDir(r.embedFS, filepath.Join(r.prefix, name)) | ||
} | ||
|
||
type mustGatherReader struct { | ||
filesystem fs.FS | ||
mustGatherDir string | ||
} | ||
|
||
func newMustGatherReader(mustGatherDir string) RawReader { | ||
return &mustGatherReader{ | ||
filesystem: os.DirFS(mustGatherDir), | ||
mustGatherDir: mustGatherDir, | ||
} | ||
} | ||
|
||
func (r *mustGatherReader) Open(name string) (fs.File, error) { | ||
return r.filesystem.Open(name) | ||
} | ||
|
||
func (r *mustGatherReader) ReadFile(name string) ([]byte, error) { | ||
return fs.ReadFile(r.filesystem, name) | ||
} | ||
|
||
func (r *mustGatherReader) ReadDir(name string) ([]fs.DirEntry, error) { | ||
return fs.ReadDir(r.filesystem, name) | ||
} | ||
|
||
// RoundTrip will allow performing read requests very similar to a kube-apiserver against a must-gather style directory. | ||
// Only GETs. | ||
// no watches. (maybe add watches | ||
func (mrt *manifestRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { | ||
requestInfo, err := mrt.requestInfoResolver.NewRequestInfo(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed reading requestInfo: %w", err) | ||
} | ||
if !requestInfo.IsResourceRequest { | ||
return nil, fmt.Errorf("non-resource requests are not supported by this implementation") | ||
} | ||
if len(requestInfo.Subresource) != 0 { | ||
return nil, fmt.Errorf("subresource %v is not supported by this implementation", requestInfo.Subresource) | ||
} | ||
|
||
var returnBody []byte | ||
var returnErr error | ||
switch requestInfo.Verb { | ||
case "get": | ||
// TODO handle label and field selectors because single item lists are GETs | ||
returnBody, returnErr = mrt.get(requestInfo) | ||
|
||
case "list": | ||
// TODO handle label and field selectors | ||
|
||
default: | ||
return nil, fmt.Errorf("verb %v is not supported by this implementation", requestInfo.Verb) | ||
} | ||
|
||
resp := &http.Response{} | ||
switch { | ||
case apierrors.IsNotFound(returnErr): | ||
resp.StatusCode = http.StatusNotFound | ||
resp.Status = http.StatusText(resp.StatusCode) | ||
resp.Body = io.NopCloser(bytes.NewBufferString(returnErr.Error())) | ||
case returnErr != nil: | ||
resp.StatusCode = http.StatusInternalServerError | ||
resp.Status = http.StatusText(resp.StatusCode) | ||
resp.Body = io.NopCloser(bytes.NewBufferString(returnErr.Error())) | ||
default: | ||
resp.StatusCode = http.StatusOK | ||
resp.Status = http.StatusText(resp.StatusCode) | ||
resp.Body = io.NopCloser(bytes.NewReader(returnBody)) | ||
} | ||
|
||
return resp, nil | ||
} | ||
|
||
func (mrt *manifestRoundTripper) allNamespacesWithData() ([]string, error) { | ||
nsDirs, err := mrt.contentReader.ReadDir("namespaces") | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to read allNamespacesWithData: %w", err) | ||
} | ||
|
||
ret := []string{} | ||
for _, curr := range nsDirs { | ||
ret = append(ret, curr.Name()) | ||
} | ||
|
||
return ret, nil | ||
} | ||
|
||
// must-gather has a few different ways to store resources | ||
// 1. cluster-scoped-resource/group/resource/<name>.yaml | ||
// 2. cluster-scoped-resource/group/resource.yaml | ||
// 3. namespaces/<namespace>/group/resource/<name>.yaml | ||
// 4. namespaces/<namespace>/group/resource.yaml | ||
// we have to choose which to prefer and we should always prefer the #2 if it's available. | ||
// Keep in mind that to produce a cluster-scoped list of namespaced resources, you can need to navigate many namespaces. | ||
func (mrt *manifestRoundTripper) get(requestInfo *apirequest.RequestInfo) ([]byte, error) { | ||
if len(requestInfo.Name) == 0 { | ||
return nil, fmt.Errorf("name required for GET") | ||
} | ||
if len(requestInfo.Resource) == 0 { | ||
return nil, fmt.Errorf("resource required for GET") | ||
} | ||
requiredAPIVersion := fmt.Sprintf("%s/%s", requestInfo.APIGroup, requestInfo.APIVersion) | ||
if len(requestInfo.APIGroup) == 0 { | ||
requiredAPIVersion = fmt.Sprintf("%s", requestInfo.APIVersion) | ||
} | ||
|
||
individualFileParts := []string{} | ||
if len(requestInfo.Namespace) > 0 { | ||
individualFileParts = append(individualFileParts, "namespaces", requestInfo.Namespace) | ||
} else { | ||
individualFileParts = append(individualFileParts, "cluster-scoped-resources") | ||
} | ||
if len(requestInfo.APIGroup) > 0 { | ||
individualFileParts = append(individualFileParts, requestInfo.APIGroup) | ||
} else { | ||
individualFileParts = append(individualFileParts, "core") | ||
} | ||
individualFileParts = append(individualFileParts, requestInfo.Resource, fmt.Sprintf("%s.yaml", requestInfo.Name)) | ||
individualFilePath := filepath.Join(individualFileParts...) | ||
|
||
individualObj, individualErr := readIndividualFile(mrt.contentReader, individualFilePath) | ||
switch { | ||
case errors.Is(individualErr, fs.ErrNotExist): | ||
// try for the list | ||
case individualErr != nil: | ||
return nil, fmt.Errorf("unable to read file: %w", individualErr) | ||
default: | ||
if individualObj.GetAPIVersion() != requiredAPIVersion { | ||
return nil, fmt.Errorf("actual version %v does not match request %v", individualObj.GetAPIVersion(), requiredAPIVersion) | ||
} | ||
ret, err := serializeIndividualObjToJSON(individualObj) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to serialize %v: %v", individualFilePath, err) | ||
} | ||
return []byte(ret), nil | ||
} | ||
|
||
listFileParts := []string{} | ||
if len(requestInfo.Namespace) > 0 { | ||
listFileParts = append(listFileParts, "namespaces", requestInfo.Namespace) | ||
} else { | ||
listFileParts = append(listFileParts, "cluster-scoped-resources") | ||
} | ||
if len(requestInfo.APIGroup) > 0 { | ||
listFileParts = append(listFileParts, requestInfo.APIGroup) | ||
} else { | ||
listFileParts = append(listFileParts, "core") | ||
} | ||
listFileParts = append(listFileParts, fmt.Sprintf("%s.yaml", requestInfo.Resource)) | ||
listFilePath := filepath.Join(listFileParts...) | ||
|
||
listObj, listErr := readListFile(mrt.contentReader, listFilePath) | ||
switch { | ||
case errors.Is(individualErr, fs.ErrNotExist): | ||
// we need this to be a not-found when sent back | ||
return nil, newNotFound(requestInfo) | ||
|
||
case listErr != nil: | ||
return nil, fmt.Errorf("unable to read file: %w", listErr) | ||
default: | ||
obj, err := individualFromList(listObj, requestInfo.Name) | ||
if obj == nil { | ||
return nil, newNotFound(requestInfo) | ||
} | ||
if obj.GetAPIVersion() != requiredAPIVersion { | ||
return nil, fmt.Errorf("actual version %v does not match request %v", obj.GetAPIVersion(), requiredAPIVersion) | ||
} | ||
|
||
ret, err := serializeIndividualObjToJSON(obj) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to serialize %v: %v", listFilePath, err) | ||
} | ||
return []byte(ret), nil | ||
} | ||
} | ||
|
||
// must-gather has a few different ways to store resources | ||
// 1. cluster-scoped-resource/group/resource/<name>.yaml | ||
// 2. cluster-scoped-resource/group/resource.yaml | ||
// 3. namespaces/<namespace>/group/resource/<name>.yaml | ||
// 4. namespaces/<namespace>/group/resource.yaml | ||
// we have to choose which to prefer and we should always prefer the #2 if it's available. | ||
// Keep in mind that to produce a cluster-scoped list of namespaced resources, you can need to navigate many namespaces. | ||
func (mrt *manifestRoundTripper) list(requestInfo *apirequest.RequestInfo) ([]byte, error) { | ||
panic("missing") | ||
// easiest first: namespaced, with known namespace | ||
if len(requestInfo.Namespace) > 0 { | ||
} | ||
// | ||
//clusterScopedDir := []string{"cluster-scoped-resources", requestInfo.APIGroup} | ||
//namespaceScopedDir := []string{"namespaces", requestInfo.APIGroup} | ||
//clusterScopedFile := []string{"cluster-scoped-resources", requestInfo.APIGroup} | ||
//namespaceScopedFile := []string{"namespaces", requestInfo.APIGroup} | ||
//onlyUseNamespaced := false | ||
// | ||
//if len(requestInfo.Namespace) > 0 { | ||
// onlyUseNamespaced = true | ||
//} | ||
// | ||
//clusterScopedPath = append(clusterScopedPath) | ||
return nil, nil | ||
} | ||
|
||
func newNotFound(requestInfo *apirequest.RequestInfo) error { | ||
return apierrors.NewNotFound(schema.GroupResource{ | ||
Group: requestInfo.APIGroup, | ||
Resource: requestInfo.Resource, | ||
}, requestInfo.Name) | ||
} | ||
|
||
func individualFromList(objList *unstructured.UnstructuredList, name string) (*unstructured.Unstructured, error) { | ||
individualKind := strings.TrimSuffix(objList.GetKind(), "List") | ||
|
||
for _, obj := range objList.Items { | ||
if obj.GetName() != name { | ||
continue | ||
} | ||
|
||
ret := obj.DeepCopy() | ||
ret.SetKind(individualKind) | ||
return ret, nil | ||
} | ||
|
||
return nil, fmt.Errorf("not found in this list") | ||
} | ||
|
||
func readListFile(contentReader RawReader, path string) (*unstructured.UnstructuredList, error) { | ||
content, err := contentReader.ReadFile(path) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to read %q: %w", err) | ||
} | ||
|
||
return decodeListObj(content) | ||
} | ||
|
||
func readIndividualFile(contentReader RawReader, path string) (*unstructured.Unstructured, error) { | ||
content, err := contentReader.ReadFile(path) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to read %q: %w", path, err) | ||
} | ||
|
||
return decodeIndividualObj(content) | ||
} | ||
|
||
var localScheme = runtime.NewScheme() | ||
var codecs = serializer.NewCodecFactory(localScheme) | ||
|
||
func decodeIndividualObj(content []byte) (*unstructured.Unstructured, error) { | ||
obj, _, err := codecs.UniversalDecoder().Decode(content, nil, &unstructured.Unstructured{}) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to decode: %w", err) | ||
} | ||
return obj.(*unstructured.Unstructured), nil | ||
} | ||
|
||
func decodeListObj(content []byte) (*unstructured.UnstructuredList, error) { | ||
obj, _, err := codecs.UniversalDecoder().Decode(content, nil, &unstructured.UnstructuredList{}) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to decode: %w", err) | ||
} | ||
return obj.(*unstructured.UnstructuredList), nil | ||
} | ||
|
||
func serializeIndividualObjToJSON(obj *unstructured.Unstructured) (string, error) { | ||
ret, err := json.MarshalIndent(obj.Object, "", " ") | ||
if err != nil { | ||
return "", err | ||
} | ||
return string(ret) + "\n", nil | ||
} |
Oops, something went wrong.