Skip to content

Commit

Permalink
feat: add resource rewriter component
Browse files Browse the repository at this point in the history
  • Loading branch information
mhrabovcin committed Apr 11, 2023
1 parent 735f851 commit 23051f0
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 44 deletions.
3 changes: 2 additions & 1 deletion cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/mhrabovcin/troubleshoot-live/pkg/importer"
"github.com/mhrabovcin/troubleshoot-live/pkg/kubernetes"
"github.com/mhrabovcin/troubleshoot-live/pkg/proxy"
"github.com/mhrabovcin/troubleshoot-live/pkg/rewriter"
)

type serveOptions struct {
Expand Down Expand Up @@ -95,7 +96,7 @@ func runServe(bundlePath string, o *serveOptions, out output.Output) error {
out.Infof("Running HTTPs proxy service on: %s", proxyHTTPAddress)
out.Infof("Kubeconfig path: %s", kubeconfigPath)

http.Handle("/", proxy.New(testEnv.Config, supportBundle))
http.Handle("/", proxy.New(testEnv.Config, supportBundle, rewriter.GeneratedValues()))
return http.ListenAndServe(o.proxyAddress, nil) //nolint:gosec // not a production server
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/importer/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (

// AnnotationForOriginalValue creates annotation key for given value.
func AnnotationForOriginalValue(name string) string {
return fmt.Sprintf("support-bundle-live/%s", name)
return fmt.Sprintf("troubleshoot-live/%s", name)
}

// ImportBundle creates resources in provided API server.
Expand Down
26 changes: 8 additions & 18 deletions pkg/importer/prepare.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,21 @@
package importer

import (
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"k8s.io/apimachinery/pkg/api/meta"
"github.com/mhrabovcin/troubleshoot-live/pkg/rewriter"
)

// prepareForImport modifies object loaded from support bundle file in a way
// that can be imported.
func prepareForImport(in any) error {
obj, err := meta.Accessor(in)
if err != nil {
return err
}

annotations := obj.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
// TODO(mh): inject
rr := rewriter.GeneratedValues()

if obj.GetResourceVersion() != "" {
annotations[AnnotationForOriginalValue("resourceVersion")] = obj.GetResourceVersion()
obj.SetResourceVersion("")
u, ok := in.(*unstructured.Unstructured)
if !ok {
panic("non unstructured obj")
}

annotations[AnnotationForOriginalValue("creationTimestamp")] = obj.GetCreationTimestamp().Format(time.RFC3339)
obj.SetAnnotations(annotations)

return nil
return rr.BeforeImport(u)
}
62 changes: 40 additions & 22 deletions pkg/proxy/rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,28 @@ import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"time"

"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"

"github.com/mhrabovcin/troubleshoot-live/pkg/importer"
"github.com/mhrabovcin/troubleshoot-live/pkg/rewriter"
)

func rewriteResponseResourceFields(r *http.Response) (returnErr error) {
func proxyModifyResponse(rr rewriter.ResourceRewriter) func(*http.Response) error {
r := &resourceRewriter{rewriter: rr}
return r.rewriteResponseResourceFields
}

type resourceRewriter struct {
rewriter rewriter.ResourceRewriter
}

func (rr *resourceRewriter) rewriteResponseResourceFields(r *http.Response) (returnErr error) {
if r.StatusCode != http.StatusOK {
return nil
}
Expand All @@ -41,7 +48,7 @@ func rewriteResponseResourceFields(r *http.Response) (returnErr error) {
// list requests.
if err := json.Unmarshal(data, &list); err == nil && len(list.Items) > 0 {
err := list.EachListItem(func(o runtime.Object) error {
if err := remapFields(o); err != nil {
if err := remapFields(o, rr.rewriter); err != nil {
log.Println(err)
}
return nil
Expand All @@ -63,7 +70,7 @@ func rewriteResponseResourceFields(r *http.Response) (returnErr error) {

u := &unstructured.Unstructured{}
if err := json.Unmarshal(data, &u); err == nil {
if err := remapFields(u); err != nil {
if err := remapFields(u, rr.rewriter); err != nil {
log.Println(err)
return nil
}
Expand Down Expand Up @@ -109,22 +116,33 @@ func writeResponseBody(r *http.Response, data []byte) error {
return nil
}

func remapFields(in runtime.Object) error {
o, err := meta.Accessor(in)
if err != nil {
return err
func remapFields(in runtime.Object, rr rewriter.ResourceRewriter) error {
if rr == nil {
return fmt.Errorf("resource rewriter missing")
}
annotations := o.GetAnnotations()
if originalTime, ok := annotations[importer.AnnotationForOriginalValue("creationTimestamp")]; ok {
parsedTime, err := time.Parse(time.RFC3339, originalTime)
if err != nil {
return nil
}
o.SetCreationTimestamp(metav1.NewTime(parsedTime))
delete(annotations, importer.AnnotationForOriginalValue("creationTimestamp"))
log.Printf("[%s] %s/%s: resource creationTimestamp modified\n", in.GetObjectKind().GroupVersionKind(), o.GetNamespace(), o.GetName())

u, ok := in.(*unstructured.Unstructured)
if !ok {
// TODO(mh):
return nil
}
o.SetAnnotations(annotations)

return nil
return rr.BeforeServing(u)
// o, err := meta.Accessor(in)
// if err != nil {
// return err
// }
// annotations := o.GetAnnotations()
// if originalTime, ok := annotations[importer.AnnotationForOriginalValue("creationTimestamp")]; ok {
// parsedTime, err := time.Parse(time.RFC3339, originalTime)
// if err != nil {
// return nil
// }
// o.SetCreationTimestamp(metav1.NewTime(parsedTime))
// delete(annotations, importer.AnnotationForOriginalValue("creationTimestamp"))
// log.Printf("[%s] %s/%s: resource creationTimestamp modified\n", in.GetObjectKind().GroupVersionKind(), o.GetNamespace(), o.GetName())
// }
// o.SetAnnotations(annotations)

// return nil
}
5 changes: 3 additions & 2 deletions pkg/proxy/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import (
"k8s.io/client-go/rest"

"github.com/mhrabovcin/troubleshoot-live/pkg/bundle"
"github.com/mhrabovcin/troubleshoot-live/pkg/rewriter"
)

// New create new proxy handler that can be used by HTTP library.
func New(cfg *rest.Config, b bundle.Bundle) http.Handler {
func New(cfg *rest.Config, b bundle.Bundle, rr rewriter.ResourceRewriter) http.Handler {
proxyHandler, err := ReverseProxyForAPIServerHandler(cfg)
if err != nil {
log.Fatalln(err)
}
proxyHandler.ModifyResponse = rewriteResponseResourceFields
proxyHandler.ModifyResponse = proxyModifyResponse(rr)

r := mux.NewRouter()
r.Handle("/api/v1/namespaces/{namespace}/pods/{pod}/log", LogsHandler(b))
Expand Down
13 changes: 13 additions & 0 deletions pkg/rewriter/generated.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package rewriter

// GeneratedValues removes generated values.
// See: https://kubernetes.io/docs/reference/using-api/api-concepts/#generated-values
func GeneratedValues() ResourceRewriter {
return Multi(
RemoveField("metadata", "generateName"),
RemoveField("metadata", "creationTimestamp"),
RemoveField("metadata", "deletionTimestamp"),
RemoveField("metadata", "uid"),
RemoveField("metadata", "resourceVersion"),
)
}
53 changes: 53 additions & 0 deletions pkg/rewriter/generated_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package rewriter

import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

func TestGeneratedValues(t *testing.T) {
r := GeneratedValues()
timestamp := metav1.NewTime(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC))
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "generated-",
CreationTimestamp: timestamp,
DeletionTimestamp: &timestamp,
UID: types.UID("1000"),
ResourceVersion: "2000",
},
}
pod = testRewriterBeforeImport(t, r, pod)
assert.Empty(t, pod.GetGenerateName())
assert.Empty(t, pod.GetCreationTimestamp())
assert.Empty(t, pod.GetDeletionTimestamp())
assert.Empty(t, pod.GetUID())
assert.Empty(t, pod.GetResourceVersion())

expectedFields := map[string]string{
"generateName": "generated-",
"creationTimestamp": "2023-01-01T00:00:00Z",
"deletionTimestamp": "2023-01-01T00:00:00Z",
"uid": "1000",
"resourceVersion": "2000",
}
for k, v := range expectedFields {
fieldName := fmt.Sprintf("metadata.%s", k)
annotation := annotationForOriginalValue(fieldName)
assert.Contains(t, pod.GetAnnotations(), annotation, "annotation %q missing", fieldName)
assert.Equal(t, v, pod.GetAnnotations()[annotation], "wrong value stored in %q", fieldName)
}

pod = testRewriterBeforeServing(t, r, pod)
assert.Equal(t, "generated-", pod.GetGenerateName())
assert.Equal(t, timestamp.Format(time.RFC3339), pod.GetCreationTimestamp().In(time.UTC).Format(time.RFC3339))
assert.Equal(t, timestamp.Format(time.RFC3339), pod.GetDeletionTimestamp().In(time.UTC).Format(time.RFC3339))
assert.Equal(t, types.UID("1000"), pod.GetUID())
assert.Equal(t, "2000", pod.GetResourceVersion())
}
34 changes: 34 additions & 0 deletions pkg/rewriter/multi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package rewriter

import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

type multiRewriter struct {
rewriters []ResourceRewriter
}

var _ ResourceRewriter = (*multiRewriter)(nil)

// Multi executes serially multiple rewriters.
func Multi(rewriters ...ResourceRewriter) ResourceRewriter {
return &multiRewriter{
rewriters: rewriters,
}
}

func (r *multiRewriter) BeforeImport(u *unstructured.Unstructured) error {
for _, rewriter := range r.rewriters {
if err := rewriter.BeforeImport(u); err != nil {
return err
}
}
return nil
}

func (r *multiRewriter) BeforeServing(u *unstructured.Unstructured) error {
for _, rewriter := range r.rewriters {
if err := rewriter.BeforeServing(u); err != nil {
return err
}
}
return nil
}
85 changes: 85 additions & 0 deletions pkg/rewriter/rewriter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package rewriter

import (
"fmt"
"strings"

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

// annotationForOriginalValue creates annotation key for given value.
func annotationForOriginalValue(name string) string {
return fmt.Sprintf("troubleshoot-live/%s", name)
}

// ResourceRewriter prepares object for saving on import and rewrites the object
// before its returned back from proxy server.
type ResourceRewriter interface {
// BeforeImport is invoked when object is created in API server.
BeforeImport(u *unstructured.Unstructured) error

// BeforeServing is applied when object passes proxy (via List or Get request).
BeforeServing(u *unstructured.Unstructured) error
}

var _ ResourceRewriter = (*removeField)(nil)

// RemoveField removes a field from original object. This should be used for metadata
// fields that are generated by API server on write.
func RemoveField(path ...string) ResourceRewriter {
return &removeField{
fieldPath: path,
}
}

type removeField struct {
fieldPath []string
}

func (r *removeField) annotationName() string {
return annotationForOriginalValue(strings.Join(r.fieldPath, "."))
}

func (r *removeField) BeforeImport(u *unstructured.Unstructured) error {
s, ok, err := unstructured.NestedString(u.Object, r.fieldPath...)
if err != nil {
return err
}

if !ok {
return nil
}

unstructured.RemoveNestedField(u.Object, r.fieldPath...)
return addAnnotation(u, r.annotationName(), s)
}

func (r *removeField) BeforeServing(u *unstructured.Unstructured) error {
value, ok, err := unstructured.NestedString(u.Object, "metadata", "annotations", r.annotationName())
if err != nil {
return err
}
if !ok {
return nil
}

if err := unstructured.SetNestedField(u.Object, value, r.fieldPath...); err != nil {
return err
}
unstructured.RemoveNestedField(u.Object, "metadata", "annotations", r.annotationName())
return nil
}

func addAnnotation(u *unstructured.Unstructured, key, value string) error {
annotations, ok, err := unstructured.NestedStringMap(u.Object, "metadata", "annotations")
if err != nil {
return err
}

if !ok {
annotations = map[string]string{}
}

annotations[key] = value
return unstructured.SetNestedStringMap(u.Object, annotations, "metadata", "annotations")
}
Loading

0 comments on commit 23051f0

Please sign in to comment.