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 68ab19e
Show file tree
Hide file tree
Showing 5 changed files with 716 additions and 0 deletions.
80 changes: 80 additions & 0 deletions pkg/manifestclient/encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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", path, 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
}

func serializeListObjToJSON(obj *unstructured.UnstructuredList) (string, error) {
ret, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return "", err
}
return string(ret) + "\n", nil
}
113 changes: 113 additions & 0 deletions pkg/manifestclient/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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)
}

individualFilePath := individualFileLocation(requestInfo)
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
}

listFilePath := listFileLocation(requestInfo)
listObj, listErr := readListFile(mrt.contentReader, listFilePath)
switch {
case errors.Is(listErr, 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
}
}

func individualFileLocation(requestInfo *apirequest.RequestInfo) string {
fileParts := []string{}

if len(requestInfo.Namespace) > 0 {
fileParts = append(fileParts, "namespaces", requestInfo.Namespace)
} else {
fileParts = append(fileParts, "cluster-scoped-resources")
}

if len(requestInfo.APIGroup) > 0 {
fileParts = append(fileParts, requestInfo.APIGroup)
} else {
fileParts = append(fileParts, "core")
}

fileParts = append(fileParts, requestInfo.Resource, fmt.Sprintf("%s.yaml", requestInfo.Name))

return filepath.Join(fileParts...)
}

func listFileLocation(requestInfo *apirequest.RequestInfo) string {
fileParts := []string{}

if len(requestInfo.Namespace) > 0 {
fileParts = append(fileParts, "namespaces", requestInfo.Namespace)
} else {
fileParts = append(fileParts, "cluster-scoped-resources")
}

if len(requestInfo.APIGroup) > 0 {
fileParts = append(fileParts, requestInfo.APIGroup)
} else {
fileParts = append(fileParts, "core")
}

fileParts = append(fileParts, fmt.Sprintf("%s.yaml", requestInfo.Resource))

return filepath.Join(fileParts...)
}
175 changes: 175 additions & 0 deletions pkg/manifestclient/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package manifestclient

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

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
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) list(requestInfo *apirequest.RequestInfo) ([]byte, error) {
var retList *unstructured.UnstructuredList
possibleListFiles, err := allPossibleListFileLocations(mrt.contentReader, requestInfo)
if err != nil {
return nil, fmt.Errorf("unable to determine list file locations: %w", err)
}
for _, listFile := range possibleListFiles {
currList, err := readListFile(mrt.contentReader, listFile)
switch {
case errors.Is(err, fs.ErrNotExist):
// do nothing, it's possible, not guaranteed
continue
case err != nil:
return nil, fmt.Errorf("unable to determine read list file %v: %w", listFile, err)
}

if retList == nil {
retList = currList
continue
}
for i := range currList.Items {
retList.Items = append(retList.Items, currList.Items[i])
}
}
if retList != nil {
ret, err := serializeListObjToJSON(retList)
if err != nil {
return nil, fmt.Errorf("failed to serialize: %v", err)
}
return []byte(ret), nil
}

retList = &unstructured.UnstructuredList{
Object: map[string]interface{}{},
Items: nil,
}
individualFiles, err := allIndividualFileLocations(mrt.contentReader, requestInfo)
if err != nil {
return nil, fmt.Errorf("unable to determine individual file locations: %w", err)
}
for _, individualFile := range individualFiles {
currInstance, err := readIndividualFile(mrt.contentReader, individualFile)
switch {
case errors.Is(err, fs.ErrNotExist):
// do nothing, it's possible, not guaranteed
continue
case err != nil:
return nil, fmt.Errorf("unable to determine read list file %v: %w", individualFile, err)
}

retList.Items = append(retList.Items, *currInstance)
}
if len(retList.Items) > 0 {
retList.SetKind(retList.Items[0].GetKind() + "List")
retList.SetAPIVersion(retList.Items[0].GetAPIVersion())

ret, err := serializeListObjToJSON(retList)
if err != nil {
return nil, fmt.Errorf("failed to serialize: %v", err)
}
return []byte(ret), nil
}

return nil, fmt.Errorf("unable to read any file so we have no Kind")
}

func allIndividualFileLocations(contentReader RawReader, requestInfo *apirequest.RequestInfo) ([]string, error) {
resourceDirectoryParts := []string{}
if len(requestInfo.APIGroup) > 0 {
resourceDirectoryParts = append(resourceDirectoryParts, requestInfo.APIGroup)
} else {
resourceDirectoryParts = append(resourceDirectoryParts, "core")
}
resourceDirectoryParts = append(resourceDirectoryParts, requestInfo.Resource)

resourceDirectoriesToCheckForIndividualFiles := []string{}
if len(requestInfo.Namespace) > 0 {
parts := append([]string{"namespaces", requestInfo.Namespace}, resourceDirectoryParts...)
resourceDirectoriesToCheckForIndividualFiles = append(resourceDirectoriesToCheckForIndividualFiles, filepath.Join(parts...))

} else {
clusterParts := append([]string{"cluster-scoped-resources"}, resourceDirectoryParts...)
resourceDirectoriesToCheckForIndividualFiles = append(resourceDirectoriesToCheckForIndividualFiles, filepath.Join(clusterParts...))

namespaces, err := allNamespacesWithData(contentReader)
if err != nil {
return nil, fmt.Errorf("unable to read namespaces")
}
for _, ns := range namespaces {
nsParts := append([]string{"namespaces", ns}, resourceDirectoryParts...)
resourceDirectoriesToCheckForIndividualFiles = append(resourceDirectoriesToCheckForIndividualFiles, filepath.Join(nsParts...))
}
}

allIndividualFilePaths := []string{}
for _, resourceDirectory := range resourceDirectoriesToCheckForIndividualFiles {
individualFiles, err := contentReader.ReadDir(resourceDirectory)
switch {
case errors.Is(err, fs.ErrNotExist):
continue
case err != nil:
return nil, fmt.Errorf("unable to read resourceDir")
}

for _, curr := range individualFiles {
allIndividualFilePaths = append(allIndividualFilePaths, filepath.Join(resourceDirectory, curr.Name()))
}
}

return allIndividualFilePaths, nil
}

func allPossibleListFileLocations(contentReader RawReader, requestInfo *apirequest.RequestInfo) ([]string, error) {
resourceListFileParts := []string{}
if len(requestInfo.APIGroup) > 0 {
resourceListFileParts = append(resourceListFileParts, requestInfo.APIGroup)
} else {
resourceListFileParts = append(resourceListFileParts, "core")
}
resourceListFileParts = append(resourceListFileParts, fmt.Sprintf("%s.yaml", requestInfo.Resource))

allPossibleListFileLocations := []string{}
if len(requestInfo.Namespace) > 0 {
parts := append([]string{"namespaces", requestInfo.Namespace}, resourceListFileParts...)
allPossibleListFileLocations = append(allPossibleListFileLocations, filepath.Join(parts...))

} else {
clusterParts := append([]string{"cluster-scoped-resources"}, resourceListFileParts...)
allPossibleListFileLocations = append(allPossibleListFileLocations, filepath.Join(clusterParts...))

namespaces, err := allNamespacesWithData(contentReader)
if err != nil {
return nil, fmt.Errorf("unable to read namespaces")
}
for _, ns := range namespaces {
nsParts := append([]string{"namespaces", ns}, resourceListFileParts...)
allPossibleListFileLocations = append(allPossibleListFileLocations, filepath.Join(nsParts...))
}
}

return allPossibleListFileLocations, nil
}

func allNamespacesWithData(contentReader RawReader) ([]string, error) {
nsDirs, err := 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
}
Loading

0 comments on commit 68ab19e

Please sign in to comment.