Skip to content

Commit

Permalink
Merge pull request #1811 from deads2k/mutation-detection
Browse files Browse the repository at this point in the history
API-1835: add mutation tracking to the fake client
  • Loading branch information
openshift-merge-bot[bot] authored Oct 7, 2024
2 parents 636f95d + 2d51c5a commit 695e391
Show file tree
Hide file tree
Showing 10 changed files with 825 additions and 74 deletions.
168 changes: 168 additions & 0 deletions pkg/manifestclient/mutation_tracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package manifestclient

import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"sync"
)

type AllActionsTracker struct {
lock sync.RWMutex

ActionToTracker map[Action]*ActionTracker
}

type Action string

const (
// this is really a subset of patch, but we treat it separately because it is useful to do so
ActionApply Action = "server-side-apply"
ActionApplyStatus Action = "server-side-apply-status"
ActionUpdate Action = "update"
ActionUpdateStatus Action = "update-status"
ActionCreate Action = "create"
ActionDelete Action = "delete"
)

type ActionMetadata struct {
Action Action
GVR schema.GroupVersionResource
Namespace string
Name string
}

type ActionTracker struct {
Action Action
ResourceToTracker map[schema.GroupVersionResource]*ResourceTracker
}

type ResourceTracker struct {
GVR schema.GroupVersionResource
NamespaceToTracker map[string]*NamespaceTracker
}

type NamespaceTracker struct {
Namespace string
NameToTracker map[string]*NameTracker
}

type NameTracker struct {
Name string
SerializedRequests []SerializedRequest
}

type SerializedRequest struct {
Options []byte
Body []byte
}

func (a *AllActionsTracker) AddRequest(metadata ActionMetadata, request SerializedRequest) {
a.lock.Lock()
defer a.lock.Unlock()

if a.ActionToTracker == nil {
a.ActionToTracker = map[Action]*ActionTracker{}
}
if _, ok := a.ActionToTracker[metadata.Action]; !ok {
a.ActionToTracker[metadata.Action] = &ActionTracker{Action: metadata.Action}
}
a.ActionToTracker[metadata.Action].AddRequest(metadata, request)
}

func (a *AllActionsTracker) ListActions() []Action {
a.lock.Lock()
defer a.lock.Unlock()

return sets.KeySet(a.ActionToTracker).UnsortedList()
}

func (a *AllActionsTracker) MutationsForAction(action Action) *ActionTracker {
a.lock.RLock()
defer a.lock.RUnlock()

return a.ActionToTracker[action]
}

func (a *AllActionsTracker) MutationsForMetadata(metadata ActionMetadata) []SerializedRequest {
a.lock.RLock()
defer a.lock.RUnlock()

actionTracker := a.MutationsForAction(metadata.Action)
if actionTracker == nil {
return nil
}
resourceTracker := actionTracker.MutationsForResource(metadata.GVR)
if resourceTracker == nil {
return nil
}
namespaceTracker := resourceTracker.MutationsForNamespace(metadata.Namespace)
if namespaceTracker == nil {
return nil
}
nameTracker := namespaceTracker.MutationsForName(metadata.Name)
if nameTracker == nil {
return nil
}
return nameTracker.SerializedRequests
}

func (a *ActionTracker) AddRequest(metadata ActionMetadata, request SerializedRequest) {
if a.ResourceToTracker == nil {
a.ResourceToTracker = map[schema.GroupVersionResource]*ResourceTracker{}
}
if _, ok := a.ResourceToTracker[metadata.GVR]; !ok {
a.ResourceToTracker[metadata.GVR] = &ResourceTracker{GVR: metadata.GVR}
}
a.ResourceToTracker[metadata.GVR].AddRequest(metadata, request)
}

func (a *ActionTracker) ListResources() []schema.GroupVersionResource {
return sets.KeySet(a.ResourceToTracker).UnsortedList()
}

func (a *ActionTracker) MutationsForResource(gvr schema.GroupVersionResource) *ResourceTracker {
return a.ResourceToTracker[gvr]
}

func (a *ResourceTracker) AddRequest(metadata ActionMetadata, request SerializedRequest) {
if a.NamespaceToTracker == nil {
a.NamespaceToTracker = map[string]*NamespaceTracker{}
}
if _, ok := a.NamespaceToTracker[metadata.Namespace]; !ok {
a.NamespaceToTracker[metadata.Namespace] = &NamespaceTracker{Namespace: metadata.Namespace}
}
a.NamespaceToTracker[metadata.Namespace].AddRequest(metadata, request)
}

func (a *ResourceTracker) ListNamespaces() []string {
return sets.KeySet(a.NamespaceToTracker).UnsortedList()
}

func (a *ResourceTracker) MutationsForNamespace(namespace string) *NamespaceTracker {
return a.NamespaceToTracker[namespace]
}

func (a *NamespaceTracker) AddRequest(metadata ActionMetadata, request SerializedRequest) {
if a.NameToTracker == nil {
a.NameToTracker = map[string]*NameTracker{}
}
if _, ok := a.NameToTracker[metadata.Name]; !ok {
a.NameToTracker[metadata.Name] = &NameTracker{Name: metadata.Name}
}
a.NameToTracker[metadata.Name].AddRequest(request)
}

func (a *NamespaceTracker) ListNames() []string {
return sets.KeySet(a.NameToTracker).UnsortedList()
}

func (a *NamespaceTracker) MutationsForName(name string) *NameTracker {
return a.NameToTracker[name]
}

func (a *NameTracker) AddRequest(request SerializedRequest) {
if a.SerializedRequests == nil {
a.SerializedRequests = []SerializedRequest{}
}
a.SerializedRequests = append(a.SerializedRequests, request)
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,13 @@ type RawReader interface {
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) {
func newReadRoundTripper(contentReader RawReader) *manifestRoundTripper {
return &manifestRoundTripper{
contentReader: contentReader,
requestInfoResolver: server.NewRequestInfoResolver(&server.Config{
LegacyAPIGroupPrefixes: sets.NewString(server.DefaultLegacyAPIPrefix),
}),
}, nil
}
}

type prefixedContentReader struct {
Expand Down
94 changes: 94 additions & 0 deletions pkg/manifestclient/readwrite_roundtripper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package manifestclient

import (
"bytes"
"embed"
"fmt"
"io"
"net/http"
)

// Enter here and call `NewForConfigAndClient(&rest.Config{}, httpClient)`
func NewHTTPClient(mustGatherDir string) MutationTrackingClient {
mutationTrackingRoundTripper := newReadWriteRoundTripper(newMustGatherReader(mustGatherDir))
return &mutationTrackingClient{
httpClient: &http.Client{
Transport: mutationTrackingRoundTripper,
},
mutationTrackingRoundTripper: mutationTrackingRoundTripper,
}
}

// Enter here and call `NewForConfigAndClient(&rest.Config{}, httpClient)`
func NewTestingHTTPClient(embedFS embed.FS, prefix string) MutationTrackingClient {
mutationTrackingRoundTripper := newReadWriteRoundTripper(newPrefixedReader(embedFS, prefix))
return &mutationTrackingClient{
httpClient: &http.Client{
Transport: mutationTrackingRoundTripper,
},
mutationTrackingRoundTripper: mutationTrackingRoundTripper,
}
}

func NewTestingRoundTripper(embedFS embed.FS, prefix string) *readWriteRoundTripper {
return newReadWriteRoundTripper(newPrefixedReader(embedFS, prefix))
}

func NewRoundTripper(mustGatherDir string) *readWriteRoundTripper {
return newReadWriteRoundTripper(newMustGatherReader(mustGatherDir))
}

func newReadWriteRoundTripper(contentReader RawReader) *readWriteRoundTripper {
return &readWriteRoundTripper{
readDelegate: newReadRoundTripper(contentReader),
writeDelegate: newWriteRoundTripper(),
}
}

type readWriteRoundTripper struct {
readDelegate *manifestRoundTripper
writeDelegate *writeTrackingRoundTripper
}

type MutationTrackingRoundTripper interface {
http.RoundTripper
GetMutations() *AllActionsTracker
}

type mutationTrackingClient struct {
httpClient *http.Client

mutationTrackingRoundTripper MutationTrackingRoundTripper
}

func (m mutationTrackingClient) GetHTTPClient() *http.Client {
return m.httpClient
}

func (m mutationTrackingClient) GetMutations() *AllActionsTracker {
return m.mutationTrackingRoundTripper.GetMutations()
}

type MutationTrackingClient interface {
GetHTTPClient() *http.Client
GetMutations() *AllActionsTracker
}

func (rt *readWriteRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
switch req.Method {
case "GET", "HEAD":
return rt.readDelegate.RoundTrip(req)
case "POST", "PUT", "PATCH", "DELETE":
return rt.writeDelegate.RoundTrip(req)
default:
resp := &http.Response{}
resp.StatusCode = http.StatusInternalServerError
resp.Status = http.StatusText(resp.StatusCode)
resp.Body = io.NopCloser(bytes.NewBufferString(fmt.Sprintf("unhandled verb: %q", req.Method)))
return resp, nil
}
}

func (rt *readWriteRoundTripper) GetMutations() *AllActionsTracker {
return rt.writeDelegate.actionTracker
}
Loading

0 comments on commit 695e391

Please sign in to comment.