Skip to content

Commit

Permalink
add ability to wire generated clients to directories
Browse files Browse the repository at this point in the history
  • Loading branch information
deads2k committed Apr 29, 2024
1 parent 9059903 commit e1a3b51
Show file tree
Hide file tree
Showing 5 changed files with 470 additions and 0 deletions.
72 changes: 72 additions & 0 deletions pkg/manifestclient/encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package manifestclient

import (
"encoding/json"
"fmt"
"strings"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)

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
}
99 changes: 99 additions & 0 deletions pkg/manifestclient/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package manifestclient

import (
"errors"
"fmt"
"io/fs"
"path/filepath"

apirequest "k8s.io/apiserver/pkg/endpoints/request"
)

// 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
}
}
48 changes: 48 additions & 0 deletions pkg/manifestclient/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package manifestclient

import (
"fmt"

apirequest "k8s.io/apiserver/pkg/endpoints/request"
)

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) 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
}
153 changes: 153 additions & 0 deletions pkg/manifestclient/roundtripper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package manifestclient

import (
"bytes"
"embed"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"

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 newNotFound(requestInfo *apirequest.RequestInfo) error {
return apierrors.NewNotFound(schema.GroupResource{
Group: requestInfo.APIGroup,
Resource: requestInfo.Resource,
}, requestInfo.Name)
}
Loading

0 comments on commit e1a3b51

Please sign in to comment.