Skip to content

Commit

Permalink
k8s: clean up serializers so that they never print status objects (#1690
Browse files Browse the repository at this point in the history
)

Fixes #1667
  • Loading branch information
nicks authored May 28, 2019
1 parent 6bf7355 commit 344dc1d
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 41 deletions.
2 changes: 1 addition & 1 deletion internal/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ func (k K8sClient) actOnEntities(ctx context.Context, cmdArgs []string, entities
args := append([]string{}, cmdArgs...)
args = append(args, "-f", "-")

rawYAML, err := SerializeYAML(entities)
rawYAML, err := SerializeSpecYAML(entities)
if err != nil {
return "", "", errors.Wrapf(err, "serializeYaml for kubectl %s", cmdArgs)
}
Expand Down
126 changes: 126 additions & 0 deletions internal/k8s/encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package k8s

import (
"unsafe"

jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
)

var defaultJSONIterator = createDefaultJSONIterator()
var specJSONIterator = createSpecJSONIterator()

func createDefaultJSONConfig() jsoniter.Config {
return jsoniter.Config{
EscapeHTML: true,
SortMapKeys: true,
ValidateJsonRawMessage: true,
CaseSensitive: true,
}
}

func createDefaultJSONIterator() jsoniter.API {
return createDefaultJSONConfig().Froze()
}

// Create a JSON iterator that:
// - encodes "zero" metav1.Time values as empty instead of nil
// - encodes all status values as empty
func createSpecJSONIterator() jsoniter.API {
config := createDefaultJSONConfig().Froze()
config.RegisterExtension(newTimeExtension())
config.RegisterExtension(alwaysEmptyExtension{
typeIndex: createTypeIndex(allStatusTypes()),
})
return config
}

func allStatusTypes() []reflect2.Type {
result := []reflect2.Type{}
for _, typ := range scheme.Scheme.AllKnownTypes() {
typ2 := reflect2.Type2(typ)

sTyp2, ok := typ2.(reflect2.StructType)
if !ok {
continue
}

statusField := sTyp2.FieldByName("Status")
if statusField == nil {
continue
}

result = append(result, statusField.Type())
}
return result
}

type TypeIndex map[reflect2.Type]bool

func (idx TypeIndex) Contains(typ reflect2.Type) bool {
_, ok := idx[typ]
return ok
}

func createTypeIndex(ts []reflect2.Type) TypeIndex {
result := make(map[reflect2.Type]bool)
for _, t := range ts {
result[t] = true
}
return TypeIndex(result)
}

// Any type that matches this extension is considered empty,
// and skipped during json serialization.
type alwaysEmptyExtension struct {
*jsoniter.DummyExtension
typeIndex TypeIndex
}

func (e alwaysEmptyExtension) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
if e.typeIndex.Contains(typ) {
return alwaysEmptyEncoder{}
}
return nil
}

type alwaysEmptyEncoder struct {
}

func (alwaysEmptyEncoder) IsEmpty(ptr unsafe.Pointer) bool { return true }
func (alwaysEmptyEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {}

type timeExtension struct {
*jsoniter.DummyExtension
timeType reflect2.Type
}

func newTimeExtension() timeExtension {
return timeExtension{
// memoize the type lookup
timeType: reflect2.TypeOf(metav1.Time{}),
}
}

func (e timeExtension) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
if e.timeType == typ {
return timeEncoder{delegate: defaultJSONIterator.EncoderOf(typ)}
}
return nil
}

type timeEncoder struct {
delegate jsoniter.ValEncoder
}

// Returns true if the time value is the zero value.
func (e timeEncoder) IsEmpty(ptr unsafe.Pointer) bool {
t := *((*metav1.Time)(ptr))
return t == metav1.Time{} || e.delegate.IsEmpty(ptr)
}

func (e timeEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
e.delegate.Encode(ptr, stream)
}
6 changes: 3 additions & 3 deletions internal/k8s/entity_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func BenchmarkParseUnparseSingle(b *testing.B) {
if err != nil {
b.Fatal(err)
}
_, err = SerializeYAML(entities)
_, err = SerializeSpecYAML(entities)
if err != nil {
b.Fatal(err)
}
Expand All @@ -30,7 +30,7 @@ func BenchmarkParseUnparseLonger(b *testing.B) {
if err != nil {
b.Fatal(err)
}
_, err = SerializeYAML(entities)
_, err = SerializeSpecYAML(entities)
if err != nil {
b.Fatal(err)
}
Expand All @@ -48,7 +48,7 @@ func BenchmarkParseUnparseLongest(b *testing.B) {
if err != nil {
b.Fatal(err)
}
_, err = SerializeYAML(entities)
_, err = SerializeSpecYAML(entities)
if err != nil {
b.Fatal(err)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/k8s/fake_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (c *FakeK8sClient) Upsert(ctx context.Context, entities []K8sEntity) error
if c.UpsertError != nil {
return c.UpsertError
}
yaml, err := SerializeYAML(entities)
yaml, err := SerializeSpecYAML(entities)
if err != nil {
return errors.Wrap(err, "kubectl apply")
}
Expand All @@ -167,7 +167,7 @@ func (c *FakeK8sClient) Upsert(ctx context.Context, entities []K8sEntity) error
}

func (c *FakeK8sClient) Delete(ctx context.Context, entities []K8sEntity) error {
yaml, err := SerializeYAML(entities)
yaml, err := SerializeSpecYAML(entities)
if err != nil {
return errors.Wrap(err, "kubectl delete")
}
Expand Down
18 changes: 9 additions & 9 deletions internal/k8s/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestInjectDigestSanchoYAML(t *testing.T) {
t.Errorf("Expected replaced: true. Actual: %v", replaced)
}

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -69,7 +69,7 @@ func TestInjectDigestDoesNotMutateOriginal(t *testing.T) {
t.Errorf("Expected replaced: true. Actual: %v", replaced)
}

result, err := SerializeYAML([]K8sEntity{entity})
result, err := SerializeSpecYAML([]K8sEntity{entity})
if err != nil {
t.Fatal(err)
}
Expand All @@ -91,7 +91,7 @@ func TestInjectImagePullPolicy(t *testing.T) {
t.Fatal(err)
}

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand All @@ -100,7 +100,7 @@ func TestInjectImagePullPolicy(t *testing.T) {
t.Errorf("image does not have correct pull policy: %s", result)
}

serializedOrigEntity, err := SerializeYAML([]K8sEntity{entity})
serializedOrigEntity, err := SerializeSpecYAML([]K8sEntity{entity})
if err != nil {
t.Fatal(err)
}
Expand All @@ -122,7 +122,7 @@ func TestInjectImagePullPolicyDoesNotMutateOriginal(t *testing.T) {
t.Fatal(err)
}

result, err := SerializeYAML([]K8sEntity{entity})
result, err := SerializeSpecYAML([]K8sEntity{entity})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -173,7 +173,7 @@ func TestInjectDigestBlorgBackendYAML(t *testing.T) {
t.Errorf("Expected replaced: true. Actual: %v", replaced)
}

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -224,7 +224,7 @@ func TestInjectSyncletImage(t *testing.T) {
t.Errorf("Expected replacement in:\n%s", testyaml.SyncletYAML)
}

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -330,7 +330,7 @@ func TestInjectDigestEnvVar(t *testing.T) {
t.Errorf("Expected replaced: true. Actual: %v", replaced)
}

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -366,7 +366,7 @@ func testInjectDigestCRD(t *testing.T, yaml string, expectedDigestPrefix string)
t.Errorf("Expected replaced: true. Actual: %v", replaced)
}

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand Down
12 changes: 6 additions & 6 deletions internal/k8s/label_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestInjectLabelPod(t *testing.T) {
t.Fatal(err)
}

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand All @@ -50,7 +50,7 @@ func TestInjectLabelDeployment(t *testing.T) {
t.Fatal(err)
}

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand All @@ -72,7 +72,7 @@ func TestInjectLabelDeploymentBeta1(t *testing.T) {
t.Fatal(err)
}

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand All @@ -97,7 +97,7 @@ func TestInjectLabelDeploymentBeta2(t *testing.T) {
t.Fatal(err)
}

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand All @@ -122,7 +122,7 @@ func TestInjectLabelExtDeploymentBeta1(t *testing.T) {
t.Fatal(err)
}

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -153,7 +153,7 @@ func TestInjectStatefulSet(t *testing.T) {
assert.Equal(t, "deadbeef", podTmpl.ObjectMeta.Labels["tilt-runid"])
assert.Equal(t, "", vcTmpl.ObjectMeta.Labels["tilt-runid"])

result, err := SerializeYAML([]K8sEntity{newEntity})
result, err := SerializeSpecYAML([]K8sEntity{newEntity})
if err != nil {
t.Fatal(err)
}
Expand Down
32 changes: 26 additions & 6 deletions internal/k8s/serialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/util/yaml"
yamlDecoder "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/kubernetes/scheme"
yamlEncoder "sigs.k8s.io/yaml"
)

func ParseYAMLFromString(yaml string) ([]K8sEntity, error) {
Expand All @@ -22,7 +22,7 @@ func ParseYAMLFromString(yaml string) ([]K8sEntity, error) {
// https://github.com/kubernetes/cli-runtime/blob/d6a36215b15f83b94578f2ffce5d00447972e8ae/pkg/genericclioptions/resource/visitor.go#L583
func ParseYAML(k8sYaml io.Reader) ([]K8sEntity, error) {
reader := bufio.NewReader(k8sYaml)
decoder := yaml.NewYAMLOrJSONDecoder(reader, 4096)
decoder := yamlDecoder.NewYAMLOrJSONDecoder(reader, 4096)

result := make([]K8sEntity, 0)
for {
Expand Down Expand Up @@ -71,14 +71,34 @@ func ParseYAML(k8sYaml io.Reader) ([]K8sEntity, error) {
return result, nil
}

func SerializeYAML(decoded []K8sEntity) (string, error) {
yamlSerializer := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
// Serializes the provided K8s object as YAML to the given writer.
//
// By convention, all K8s objects contain ObjectMetadata, Spec, and Status.
// This only serializes the metadata and spec, skipping the status.
func serializeSpec(obj runtime.Object, w io.Writer) error {
json, err := specJSONIterator.Marshal(obj)
if err != nil {
return err
}
data, err := yamlEncoder.JSONToYAML(json)
if err != nil {
return err
}
_, err = w.Write(data)
return err
}

// Serializes the provided K8s objects as YAML.
//
// By convention, all K8s objects contain ObjectMetadata, Spec, and Status.
// This only serializes the metadata and spec, skipping the status.
func SerializeSpecYAML(decoded []K8sEntity) (string, error) {
buf := bytes.NewBuffer(nil)
for i, obj := range decoded {
if i != 0 {
buf.Write([]byte("\n---\n"))
}
err := yamlSerializer.Encode(obj.Obj, buf)
err := serializeSpec(obj.Obj, buf)
if err != nil {
return "", err
}
Expand Down
Loading

0 comments on commit 344dc1d

Please sign in to comment.