From 3568add38e3240957459990894f2356df9dceb15 Mon Sep 17 00:00:00 2001 From: Billy Lynch Date: Tue, 18 May 2021 12:08:13 -0400 Subject: [PATCH] TEP-0067: Results: JSON Serialized Records [proposed] This proposal outlines changes we would like to make to the Record type to better support more types and allow us to use Tekton types directly without needing to generate a separate proto type. --- teps/0072-results-json-serialized-records.md | 465 +++++++++++++++++++ teps/README.md | 1 + 2 files changed, 466 insertions(+) create mode 100644 teps/0072-results-json-serialized-records.md diff --git a/teps/0072-results-json-serialized-records.md b/teps/0072-results-json-serialized-records.md new file mode 100644 index 000000000..2a41d0c11 --- /dev/null +++ b/teps/0072-results-json-serialized-records.md @@ -0,0 +1,465 @@ +--- +status: proposed +title: "Results: JSON Serialized Records" +creation-date: "2021-05-11" +last-updated: "2021-05-11" +authors: ["wlynch@google.com"] +--- + +# TEP-0072: Results: JSON Serialized Records + + + + + + + + + +- [Summary](#summary) +- [Motivation](#motivation) + - [Goals](#goals) + - [Non-Goals](#non-goals) + - [Use Cases (optional)](#use-cases-optional) +- [Requirements](#requirements) +- [Proposal](#proposal) + - [Notes/Caveats (optional)](#notescaveats-optional) + - [Risks and Mitigations](#risks-and-mitigations) + - [User Experience (optional)](#user-experience-optional) + - [Performance (optional)](#performance-optional) +- [Design Details](#design-details) +- [Test Plan](#test-plan) +- [Design Evaluation](#design-evaluation) +- [Drawbacks](#drawbacks) +- [Alternatives](#alternatives) +- [Infrastructure Needed (optional)](#infrastructure-needed-optional) +- [Upgrade & Migration Strategy (optional)](#upgrade--migration-strategy-optional) +- [References (optional)](#references-optional) + + +## Summary / Motivation + + + + + +Currently we use the well-known +[`Any`](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto) proto +to represent arbitrary Record data: + +```proto +message Any { + string type_url = 1; + bytes value = 2; +} +``` + +While this has worked so far, this has some challenges that we would like to address: + +1. [Handcrafted proto types are prone to errors](https://github.com/tektoncd/results/issues/101). + We ideally need to generate or use the underlying types directly. +2. [We have had difficulty](https://github.com/tektoncd/results/issues/101#issuecomment-823604704) + generating Tekton Pipeline proto types from the underlying Go source due to + complications with `knative.dev/pkg` and the Kubernetes protobuf code + generator (https://github.com/knative/pkg/issues/2099). +3. This requires all integrations to generate protobuf types for data they would + like to store, which may be a roadblock for integrations if they do not have + protobuf definitions already made. + +### Goals + + + +- Remove handcrafted Pipeline types (and other project) types in favor of + autogenerated/directly defined types. + +### Non-Goals + + + +- Backwards compatibility - the Results API is in alpha, and we intend to lean + on this to make breaking changes quickly. We will provide migration tools to + help users convert between old formats and new formats. + +## Requirements + + + +- Record types should be defined by upstream sources, with little to no + involvement of Tekton Results. + +## Proposal + + + +We will create our own any message that looks very similar to the well-known Any +protobuf, but with slightly different semantics - + +```proto +message Any { + // Identifier to help users identity the underlying data type of value. + // e.g. `v1beta1.pipelines.tekton.dev/PipelineRun` + string type = 1; + + // JSON-serialized data corresponding to the above type. + bytes value = 2; +} +``` + +With this message, we will store a serialized JSON blob instead of a serialized +proto blob. This bypasses the protobuf generation issues described above, since +clients / integrations no longer need to have a proto type in order to store +data - they can simply encode it as JSON. + +### Notes/Caveats (optional) + + + +### Risks and Mitigations + + + +### User Experience (optional) + + + +### Performance (optional) + + + +- A benefit of moving to pure-JSON types is we can take advantage of JSON-aware + features in databases such as + [jsonb](https://github.com/tektoncd/results/issues/99). While this is not a + primary goal, it's an added benefit that would allow us to support indexed queries in the future. + +## Design Details + + + +### cel-go compatibility + +Currently the Results API relies on [cel-go](https://github.com/google/cel-go) +for List filtering. Moving to an opaque byte string breaks some of the dynamic +query behavior that we have today. + +To work around this, we will model the opaque byte string as a +[google.protobuf.Struct](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#struct) +internally for filtering. This will allow us to preserve a similar filtering +behavior with out requiring users to adopt the `Struct` type. + +## Test Plan + + + +This is primarily a backend change so our existing test infrastructure should be +sufficient. We likely want to add additional tests to test the transition. + +## Design Evaluation + + + +Our hope is that this change will make it simpler for integrations to bring +their own types to Tekton Results. + +## Drawbacks + + + +### cel-go macros + +Because we are moving away from the well-known Any proto, some of the built-in +features in CEL will no longer work out of the box - + +- The `type()` macro will no longer work with the underlying type (since it's + now an opaque byte string) - users will need to reference the `type` field: + + | Before | After | + | ---------------------------------------------------- | ----------------------------------------------------- | + | type(record.data) == tekton.pipeline.v1beta1.TaskRun | record.data.type == "tekton.pipeline.v1beta1.TaskRun" | + +- Underlying data cannot be directly referenced - users will need to use the + `value` field: + + | Before | After | + | -------------------------------- | -------------------------------------- | + | record.data.name.contains("foo") | record.data.value.name.contains("foo") | + +It is possible that we might be able to replicate some of the well-known Any +behavior by implementing our own +[TypeProvider](https://pkg.go.dev/github.com/google/cel-go@v0.7.3/common/types/ref#TypeProvider), +though we are considering this out of scope for now: + +- We're not sure if this will work / what amount of work would be required to + make this work. +- We're not sure that CEL is the filtering mechanism that we want to use long + term - e.g. will CEL be a good fit for indexed storage mechanisms like + [jsonb](https://www.postgresql.org/docs/9.4/datatype-json.html)? It's unclear + whether CEL will be too complex for us to model into a structured index query. + +## Alternatives + + + +### Embed fields directly in Record + +A small change would be to embed the `Any` fields directly in the Record +definition - + +```proto +message Record { + // Resource name, must be rooted in parent result. + string name = 1 [(google.api.resource_reference) = { + child_type: "tekton.results.v1alpha2/Record" + }]; + + // Server assigned identifier of the Result. + string id = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Identifier to help users identity the underlying data type of value. + // e.g. `v1beta1.pipelines.tekton.dev/PipelineRun` + string type = 7; + + // JSON-serialized data corresponding to the above type. + byte value = 8; + + // The etag for this record. + // If this is provided on update, it must match the server's etag. + string etag = 4; + + // Server assigned timestamp for when the result was created. + google.protobuf.Timestamp created_time = 5 + [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Server assigned timestamp for when the results was updated. + google.protobuf.Timestamp updated_time = 6 + [(google.api.field_behavior) = OUTPUT_ONLY]; +} +``` + +This would simplify some of the querying (e.g. you could use `record.data` +instead of `record.data.value`), though this would prevent us from implementing +TypeProvider support for the embedded type later on. + +OPEN DISCUSSION: Would like feedback on this approach. Seems like it simplifies +things a little, but we're not fully sure of the consequences that might come of +this. + +### Remove Knative Types from Pipeline status + +If we remove the problematic knative.dev/pkg imports, this would make it +possible for us to easily generate proto types, similar to how Kubernetes does +for all core types +([example](https://github.com/kubernetes/api/blob/master/core/v1/generated.proto)). +We are avoiding this for 2 reasons: + +1. This will be an invasive change - Tekton is fairly tied to knative.dev/pkg's + types so divorcing the 2 libraries will be a large change, likely requiring + us to remove all usage of knative.dev/pkg since + [controller conditions behavior](https://github.com/tektoncd/pipeline/blob/1f5980f8c8a05b106687cfa3e5b3193c213cb66e/pkg/apis/pipeline/v1beta1/taskrun_types.go#L143-L175) + is tied to knative.dev/pkg Conditions types. +2. This still requires other integrations to generate their own proto types, + which might run into similar issues. + +### Handcraft Tekton Pipeline protos + +We could continue to handcraft message types, with some sort of conformance test +to make sure the types match. This will likely be a ton of manual effort, and be +prone to errors. We'd prefer to infer definitions from the base types. + +### Use well-known Struct field + +Instead of using a byte string, we could use the `google.protobuf.Struct` type +directly. + +```proto +message Any { + // Identifier to help users identity the underlying data type of value. + // e.g. `v1beta1.pipelines.tekton.dev/PipelineRun` + string type = 1; + + // JSON-serialized data corresponding to the above type. + google.protobuf.Struct value = 2; +} +``` + +We are choosing not do to this because the Struct type is only really needed for +the List filtering, so it doesn't make sense to pass the complexity of dealing +with the Struct type down to clients. e.g. if we this is how we implemented this, a query would look like: + +1. (server) Data fetched from DB (jsonb) +2. (server) jsonb data unmarshalled to Struct +3. (client) Struct marshalled to JSON +4. (client) Unmarshal JSON to underlying type (e.g. TaskRun). + +Instead we will treat this as an implementation detail of server-side filtering. + +## Upgrade & Migration Strategy (optional) + + + +While this change will be breaking, we plan on providing a migration tool to +convert old-style records to new style records. + +The Result watcher will already attempt to reconcile existing objects, so the +only thing this script needs to do is backfill old deleted results. + +## References (optional) + + diff --git a/teps/README.md b/teps/README.md index 1a2444ec3..c65b894a5 100644 --- a/teps/README.md +++ b/teps/README.md @@ -217,3 +217,4 @@ This is the complete list of Tekton teps: |[TEP-0067](0067-tekton-catalog-pipeline-organization.md) | Tekton Catalog Pipeline Organization | implementable | 2021-02-22 | |[TEP-0070](0070-tekton-catalog-task-platform-support.md) | Platform support in Tekton catalog | proposed | 2021-06-02 | |[TEP-0071](0071-custom-task-sdk.md) | Custom Task SDK | proposed | 2021-06-15 | +|[TEP-0072](0072-results-json-serialized-records.md) | Results: JSON Serialized Records | proposed | 2021-05-11 |