Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define the Galley API #122

Closed
wants to merge 17 commits into from
Closed

Conversation

mandarjog
Copy link
Contributor

@mandarjog mandarjog commented Jun 12, 2017

This PR proposes an abstract config API service loosely based on the Kubernetes API server.
It adds a streaming watch to satisfy istio use cases efficiently, Stream watcher API follows etcdv3 watch API.

@mandarjog mandarjog requested a review from geeknoid June 12, 2017 15:52
@mandarjog
Copy link
Contributor Author

@lavalamp Can you please take a look at this?

@mandarjog mandarjog requested a review from ldemailly June 12, 2017 16:51
Copy link
Contributor

@ZackButcher ZackButcher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do worry about discoverability here: as a human I'm not confident I could ever call the API myself and get it correct. I know the primary mode of use will be via istioctl, but for the most part we're just shuttling around bytes, which is hard to use.

However, I don't know of another way we could do it and not require istioctl to know every proto type we're using (which is impossible in the case of Mixer config).


syntax = "proto3";

package istio.config;
Copy link
Contributor

@ZackButcher ZackButcher Jun 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all the api methods are v1/... then this package should be istio.config.v1 too so that the gRPC service is hosted as istio.config.v1.IstioConfig, rather than istio.config.IstioConfig. On that note, the service name should be changed, making comment below.

Including the version in the proto package should save us pain on the gRPC side later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed naming issues.

//
// /apiGroup/version/objecttype/{objectGroup}/objectname
// + etcdv3 stream watch API
service IstioConfig {
Copy link
Contributor

@ZackButcher ZackButcher Jun 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IstioConfig -> Config, or Service. We're already in the package istio.config, the repeated info is redundant. If anything, you might call this Service so it's addressed as istio.config.v1.Service.

// that is within an object group.
rpc ListObjects(ListObjectsRequest) returns (ObjectList) {
option (google.api.http) = {
get: "/v1/{meta.api_group}/{meta.object_type}" // ?meta.object_group=group1}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover comment here? // ?meta.object_group=group1}"

option (google.api.http) = {
get: "/v1/{meta.api_group}/{meta.object_type}" // ?meta.object_group=group1}"
additional_bindings: {
get: "/v1/{meta.api_group}/{meta.object_type}/meta.object_group}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a { for the object_group:

"/v1/{meta.api_group}/{meta.object_type}/{meta.object_group}"


syntax = "proto3";

package istio.v1.config;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we want istio.v1.config and not istio.config.v1? The first implies that this API surface can only change when Istio changes version, while the second implies that the config API can be versioned independently of Istio overall. I think we'll want to freedom to iterate on this API independently of Istio's overall version.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW Kubernetes buckets its apis into "groups" which are versioned separately.

// kv holds the KeyValue for the event.
// A PUT event contains current kv pair.
// A PUT event with kv.Version=1 indicates the creation of a key.
// A DELETE/EXPIRE event contains the deleted key with
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DELETE/EXPIRE -> DELETE only for now? Are we going to support expiration of configs?

ObjectMeta key = 1;

// start watching from this revision.
int64 start_revision = 2;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems no revisions are set in any of the responses. What does the revision means here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

versions are available in ObjectMeta

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it the revision of the storage? I was thinking it's per-object revision. Could you modify the comment in ObjectMeta?

It's probably better to use a single word for a concept, either version or revision, but not both.

Copy link
Contributor

@ZackButcher ZackButcher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link

@lavalamp lavalamp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have time to say as much as I would like, but here's a few thoughts.


syntax = "proto3";

package istio.v1.config;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW Kubernetes buckets its apis into "groups" which are versioned separately.

};

// Delete a single object. May return validation errors.
rpc DeleteObject(DeleteObjectRequest) returns (google.protobuf.Empty) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have to double check but I think Kubernetes might send back the last state of the object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// if true object(s) should include data.
bool data = 1;

// if true object(s) should include source_data.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the difference between data and source_data?

}
}

message ObjectFieldSelector {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kubenetes is generalizing this concept into a composable value clients can set in the Accept: header. E.g., metadata only, json, proto, and csv (for dumb clients).

//
message ObjectList {
// meta is the metadata associated with this list.
ObjectMeta meta = 1;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kubernetes has ObjectMeta and ListMeta-- the latter has many fewer things in it.

Meta meta = 1;

// select selects fields to include in the result.
ObjectFieldSelector select = 2;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Kubernetes field selector is used to find an object(s), not to select which fields to return.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed name.

I will leave it out arbitrary query selection for now. This is an optimization.


message ObjectList {
// meta is the metadata associated with this list.
Meta meta = 1;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

K8s uses separate ObjectMeta and ListMeta types, the latter is much shorter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meta could describe any part of the hierarchy. I would like to keep it this way and then specialized it later.

message Meta {
// api_group is the top level delegation.
// All messages from an api group are handled by the same delegation handler.
string api_group = 1;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear that it's useful to have proto versions of the api_group & api_version concepts, since definitionally clients must have the proto version compiled in to even talk to the endpoint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client does not need all proto definitions. It only needs struct, bytes and other basic protos. However if a client does have the proto definitions it can parse bytes into a typed proto.


// version
// version of the object.
int64 version = 5;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of version? resource version, like in k8s? something else? Comment should probably explain.

string object_group = 3;

// name of the object.
string name = 4;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added.

// For example:
// If `souce_data` contains an expressions in text form
// `data` may contain a parsed AST
bytes data = 3;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's assumed to be protobuf-encoding, should this be google.protobuf.Any?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

google.protobuf.Any has repercussions for the grpc transcoding proxy. It needs to have access to the underlying protos defined by the internal type field of any.
That defeats the purpose of having an opaque proto message that only the code that converts it needs to understand.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if grpc transcending proxy doesn't have access to underlying protos? Does it fail in a reasonable way (i.e. do nothing) or does it corrupt data, etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it fails hard, if the underlying protos are not available. This establishes an unwanted coupling.

};

// Create an object. This may result in configuration validation errors.
// Referrential integrity is not generally guranteed, however individual validators
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/guranteed/guaranteed/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed.

@@ -0,0 +1,305 @@
// Copyright 2017 Istio Authors
// vim: set expandtab ts=2 sw=2:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidentally left in vim modeline?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left it in while it is being edited.

UPDATE = 0; // ADD and UPDATE
DELETE = 1;
}
// type is the kind of event. If type is a PUT, it indicates
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/PUT/UPDATE here and lines 297 and 298

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Meta meta = 1;

// source_data is the data as it was specified in json / yaml format.
google.protobuf.Struct source_data = 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we ever want to allow users to create/update objects using the explicit proto message type vs. Struct to avoid unnecessary conversions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, I will add it here.

// For example:
// If `souce_data` contains an expressions in text form
// `data` may contain a parsed AST
bytes data = 3;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if grpc transcending proxy doesn't have access to underlying protos? Does it fail in a reasonable way (i.e. do nothing) or does it corrupt data, etc?


enum FilterType {
// filter out put event.
NOPUT = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use UPDATE instead of PUT to be consistent with EventType below. Also, can you use CAPITALIZED_NAMES_WITH_UNDERSCORES to be consistent with https://cloud.google.com/apis/design/naming_convention?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed filter types for now.

option (google.api.http) = {
get: "/{meta.api_group}/{meta.api_group_version}"
additional_bindings: {
get: "/"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried the grpc-gateway and met an error on this line. It seems grpc-gateway assumes the path not ending with /.

--grpc-gateway_out: segment neither wildcards, literal or variable: expected "{" but got "\x00": /

maybe add a special word for this section, like "/__all__"?

// Get a single object.
rpc GetObject(GetObjectRequest) returns (Object) {
option (google.api.http) = {
get: "/{meta.api_group}/{meta.api_group_version}/{meta.object_type}/{meta.object_group}/{meta.name}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't allow to specify incl, and since booleans are false by defualt, the server can't respond any resources. Possibly you might want to reconsider it from include to exclude.

@@ -0,0 +1,322 @@
// Copyright 2017 Istio Authors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file should be in galley/v1 following the pattern for mixer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

// api_group = /mixer/v1 or
// api_group = /proxy/v1
// + etcdv3 stream watch API
service Service {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service -> Galley

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

import "google/protobuf/empty.proto";
import "google/api/annotations.proto";

// The Istio Config API service follows
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Istio Config API Service -> Galley

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

import "google/api/annotations.proto";

// The Istio Config API service follows
// the kubenertes API server delegation and resource model.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kubernetes -> Kubernetes

//
// api_group = /mixer/v1 or
// api_group = /proxy/v1
// + etcdv3 stream watch API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this etcdv3 line mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed.


// revision
// revision of the repository, the last time Object was updated.
int64 revision = 7;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's 'repository'? The whole config store as a whole? Can we expand the word 'revision' to be more descriptive?

map<string, string> labels = 8;
}

// watchers follows etcdv3 watch pattern.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove reference to external stuff and document the pattern here instead. It's OK to go steal comments from k8s if you need to.


// watchers follows etcdv3 watch pattern.
message WatchRequest {
// request_union is a request to either create a new watcher or cancel an existing watcher.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"is a request to either" -> "indicates whether to"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

// watchers follows etcdv3 watch pattern.
message WatchRequest {
// request_union is a request to either create a new watcher or cancel an existing watcher.
oneof request_union {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this weird pattern? Shouldn't we just have different methods, one to create a watch and one to delete it, rather than having this union?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Create and cancel are part of the same message because the WatchRequest message
// is used in a streaming API. The client may add new watchers and remove old watchers
// on an existing stream.

Added comments.

// used to identify the watched subtree
Meta subtree = 1;

// start watching from this revision.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revision of what? The repository?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes repository. updated comment.

@geeknoid geeknoid changed the title Config API Service Defining the Galley API. Jun 15, 2017
@geeknoid
Copy link
Contributor

I'm not thrilled with this API. I think we're going too far in being generic, the API provides little semantic clarity over what parameters are expected and how they interact.

I realize we have some open-ended config stuff to deal with. But at the same time, there are various chunks of well-known config that we will handle (routing rules for example, or all of Mixer's well-known templates). I believe we should have dedicated strongly-typed APIs for those things we know, and only fallback to generic bland API for the open-ended stuff.

@ayj
Copy link
Contributor

ayj commented Jun 15, 2017

re: @geeknoid's concerns, it would be good to understand and document the motivation for a more generic API. We can probably live with some churn while the API settles down considering we'll be auto-generating the more tedious boilerplate code if that is the only motivation.

// meta is the metadata associated with the root where listing begins.
Meta meta = 1;

// incl(ude) selects fields to include in the result.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update comment.

// meta is the metadata associated with the object.
Meta meta = 1;

// oneof source_data or data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this three line comment.

// source_data is the data as it was specified in json / yaml format.
google.protobuf.Struct source_data = 2;

// data is protos encoded as bytes only specify
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix comment.

// meta is the metadata associated with the object.
Meta meta = 1;

// incl(ude) selects fields to include in the result.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix comment.

Meta meta = 1;
}

// Object
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say something useful here.

// source_data is the data as it was specified in json / yaml format.
google.protobuf.Struct source_data = 2;

// data is the binary encoded protobuf data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment here and on ObjectRequest.data probably should match.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Meta meta = 1;

// list of objects returned.
repeated Object objects = 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need pagination support for any list API.

map<string, string> labels = 8;
}

// WatchRequest creates on cancels a watch.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth putting the watcher definitions into their own proto file.

// /apiGroup/version/objecttype/{objectGroup}/objectname
//
// api_group = /mixer/v1 or
// api_group = /proxy/v1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remain unconvinced that api_group is a good thing. It encourages component-based config state. Having "core" as an api group and putting everything in there doesn't seem useful. Why have "core" then?

@geeknoid
Copy link
Contributor

As I mentioned in my previous review, I'm not a fan of this API. This is really just a totally generic "store protos in a hierarchy" API. Is this all we can or should do? Do we expect to create a usability layer on top of this which has stronger typing?

@ZackButcher
Copy link
Contributor

Do we expect to create a usability layer on top of this which has stronger typing?

I think this is the only way this API can work - as I said earlier, I'm not confident I could correctly curl it.

With respect to adding strong types for things like well-known templates in Mixer, I'd really prefer we don't do that. In my opinion we should not special case things just because we happen to ship them instead of some other third party. On the other hand, I want a usable API. But I don't want to have to rebuild everything, even my CLI tools, every time I change my Mixer deployment. It's a hard balance to strike.

Is it an option for us to have the fully generic API for the reasons that've been laid out (mostly to avoid proto dependencies everywhere), but also ship a more strongly typed API? As a strawman, consider generating a strongly typed API server from a set of inputs (the templates from Mixer, whatever is required by Envoy, etc) that acts as a wrapper dispatching to this generic API. Our CLI tools can go directly against the generic API and still provide a good experience, but we can also offer a reasonable experience for operators not using our tooling. Such a wrapper API would likely make building a UI easier as well.

@geeknoid
Copy link
Contributor

geeknoid commented Jun 16, 2017 via email

@lavalamp
Copy link

Take a look at the kubernetes command line tool. It provides CRUD against resources it doesn't have compiled in. (I will leave it to you to judge whether it is "usable" or not :) )

To make this work we are opinionated about the resource data format (must have consistent metadata). A proto-first system like the one in this PR is going to make this hard, because proto is not self-describing. Maybe you can have rules about the tag number of the metadata field. You absolutely must have some way of identifying the type of a blob of data; in kubernetes, our protos have an envelope layer that declares the type and format (i.e., compressed, encrypted, json, proto).

@lavalamp
Copy link

I am happy to get in a room with all of you and give a brief explanation of how Kubernetes solves these problems if that would be helpful, btw.

@mandarjog
Copy link
Contributor Author

mandarjog commented Jun 16, 2017

@geeknoid
Discoverability problem can be solved orthogonally.

  • Every validator (which already tightly coupled with the components) serves config description as Open API Spec at a well known endpoint. This should include adapters that were not present at build time.
  • Galley aggregates these specs to form a spec per installation.
  • Istioctl downloads the spec to provide up to date help messages.
  • As a post build step, every component produces config descriptions
    The Istio release step aggregates the descriptions and packages them with istioctl, so config descriptions known at compile time are all pre packaged.

// Object validator service validates objects before they are committed to storage.
// Galley maintains a map of object_types to validators. A single validator may validate many types.
// For example MixerValidator Service should validate all Mixer resources.
service ObjectValidator {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename this to ObjectValidatorAndConverter ?

int64 revision = 7;

// labels associated with the object.
map<string, string> labels = 8;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the case of mixer, Is this where the templateName will be passed as a key to the map ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that labels are entirely owned by the user. The user assigns and manages labels on pieces of state. So we shouldn't be using those for system-level state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct. System related information is encoded in resource URI.
As we talked about before, templateName is am implementation detail of how Mixer implements mesh Functions

/core/metrics/v1/mesh/request_count ==>
templateName = metrics
subject = mesh
constructorName = request_count 

labels are owned by operator. I also think labels may be used by UI to record metadata.


message Event {
enum EventType {
PUT = 0; // ADD and UPDATE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we use HTTP verbs here? If PUT represents an ADD or an UPDATE why don't we just have variants ADD, UPDATE, DELETE - is there a reason to constrain this set to HTTP-like things?

@geeknoid
Copy link
Contributor

geeknoid commented Jun 20, 2017 via email

@ZackButcher
Copy link
Contributor

ZackButcher commented Jun 20, 2017

Status allows us to return details per failure via it's details field. We can return a single status representing the logic result of the user's operation, and additionally return one detail per failure. We can use the existing error_details proto as the payloads, or define our own message for failures.

// key associated with the requested object.
Meta key = 1;

// oneof source_data or data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this comment.

int32 max_page_size = 3;
}

message ObjectRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should be CreateObjectRequest

// api_group is the top level delegation and extension point.
string api_group = 1;

// objectType is the type of an object.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object_type

// every object type is independently versioned.
string object_type_version = 3;

// objectGroup is an arbitrary hierarchical grouping of resources.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object_group

string object_type_version = 3;

// objectGroup is an arbitrary hierarchical grouping of resources.
// For Mixer configuration object_group == mesh, svc:svc1, svc:svc2 ...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete the mixer/proxy-specific comment.



// Meta is the metadata associated with an object.
message Meta {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a payload_type enum as suggested yesterday in our conversation with Daniel?

}


// The client should WatchResponse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what this comment is trying to say.

// its modification revision set to the revision of deletion.
ConfigObject kv = 2;

// prev_kv holds the key-value pair before the event happens.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

happens -> happened

// Create and cancel are part of the same message because the WatchRequest message
// is used in a streaming API. The client may add new watchers and remove old watchers
// on an existing stream.
message WatchRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are known issues to implement bidi streaming for HTTP1 (See RFC6202), and combines multiple watches in one streaming API adds too much complexity to both server and client implementation.

It sounds like implementing multiplexing by hand.

Since gRPC comes with multiplexing and flow-control, it is better to just use 1 stream per WatchCreateRequest, and use the stream state to handling cancel etc.

Is there any strong reason not to do so?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having multiple watch subscriptions arrive serialized thru the same pipe is the semantic that we are want here. The main consumer of this API is a grpc client.

I don't think it adds complexity at the point of use. Stream provides a good place to maintain subscription state .

Will bidi streaming simply not work with http1 ?

Copy link
Contributor

@ZackButcher ZackButcher Jun 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is multiplexing occurring here? The client initiates listening to a stream of updates by informing the server which updates its interested in. The client can subsequently inform the server that it's interested in more (or less) updates without breaking its existing connection to the server. From the perspective of both the client and server the stream of changes is in order.

If anything, I'd argue that streaming is simpler for the client. By forcing the client to handle a stream for each subset of events it cares about, the client now has to open N connections to the server rather than one, and has to handle listening to all of them. Chances are the client will wire up the same listener to each of the N connections - at that point, what's the advantage of multiple streams vs one stream? Further, gRPC supports flow control on streaming APIs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And is HTTP1 support that important? We expect the primary consumers of this API to use gRPC. We can expose a streaming REST interface over HTTP2. Is it really a huge loss to not be able to stream listening to changes over HTTP1?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even the main consumer is gRPC client, can't multiple one directional streaming API calls in same gRPC channel provide same semantics? (i.e. let gRPC library manage streams state instead of clients)

Bidi streaming rarely works well with HTTP1, it requires client stack to be stream aware.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The watch_id here is logically stream_id and you are re-implementing multiplexing here, no? I'm not saying this should be unary, but 1 stream per WatchCreateRequest sounds more reasonable.

That's a good point, interpreted that way the stream is multiplexed. However I think the hard parts of multiplexing are lining up the (request, response) pairs and handling per-message cancellations and deadlines (none of which gRPC supports). This API doesn't expose any of those semantics. Instead, I think of it as allowing the client to expand or restrict the stream of changes the server is sending to it.

Suppose instead of using an ID, the cancel request looked like:

message WatchCancelRequest {
  Meta subtree = 1;
}

That's effectively what the watch_id is a shorthand for. At that point, I'd argue that the API doesn't look nearly as much like its attempting to multiplex.

client never has to open N connections (for gRPC or HTTP2) client. And for HTTP1 it has to but still better than bidi.

Sorry, I was unclear: I mean that complexity for the client was increased because it has to manage N logical (application level) connections to the server, regardless of how gRPC chooses to transmit the data over the network.

Copy link
Contributor

@lizan lizan Jun 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However I think the hard parts of multiplexing are lining up the (request, response) pairs and handling per-message cancellations and deadlines (none of which gRPC supports). This API doesn't expose any of those semantics. Instead, I think of it as allowing the client to expand or restrict the stream of changes the server is sending to it.

Sorry I don't understand that part. Suppose we have one directional streaming API like

service Watcher { 
  rpc Watch(WatchCreateRequest) returns (stream WatchResponse) {
    option (google.api.http) = {
      post: "/events/v1:watch"
      body: "*"
    };
  };
}

message WatchResponse {
  oneof response_union {
    WatchCreated created = 3;
    WatchEvents events = 4;
    WatchProgress progress = 6; 
  };
}

So each streaming call maps to a watch_id here. Since gRPC has cancellation support, client can cancel any stream.

  • Open a new stream instead of sending WatchCreateRequest to bidi stream.
  • Cancel existing stream instead of sending WatchCancelRequest.

Isn't gRPC taking care of the hard parts you mentioned?

Sorry, I was unclear: I mean that complexity for the client was increased because it has to manage N logical (application level) connections to the server, regardless of how gRPC chooses to transmit the data over the network.

It really depends on how client are going to use the data watcher API. If you are watching multiple same kind (object_type) of ConfigObjects that maybe true, but if client are watching different kind of ConfigObjects, the client has to handle them differently.

This just reminds me when we started mixer API with bidi streaming and eventually moved to unary. I'm skeptical that the bidi would work efficiently here, given the complexity that you have to maintain watch_id in both server and client.

Copy link
Contributor

@ZackButcher ZackButcher Jun 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that the hard parts of a bidi multiplexed stream are those things, which gRPC does not handle for you in its implementation of streaming. But that that is fine because we don't use those things in this API.

This just reminds me when we started mixer API with bidi streaming and eventually moved to unary.

I think this use case is very different than the intent of the original streaming Mixer API. The goal of that API was to use streaming to save state so we could be more efficient on the wire (the lower overhead per-request of streaming vs an unary message was also a nice benefit). Here, we're not streaming for efficiency; we're streaming because we really do have a logical stream of changes.


If the intent of this API is for the client to dispatching to handlers based on the watch_id then I agree that your proposed API, where the client opens a stream per thing it cares about, is clearly better. However, I do not think that that is the intent of this API. I think the client will dispatch based on the value of the response_union, and doesn't particularly care about the watch_id.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're streaming because we really do have a logical stream of changes.

OK, then the intent should be clearly commented, whether the client should send a WatchCreateRequest to existing gRPC streaming call or start a new gRPC streaming call.

Though I'm not completely convinced that we really need the ability to change the logical stream dynamically. (How often and how many WatchCreateRequest or WatchCancelRequest we send per gRPC stream?) If you look at K8s watch API, it doesn't provide such ability but works for most cases.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be helpful to describe a concrete use case, where a client needs (or does not need) to change subscription of a watch. While useful in theory, in many cases I would expect a component to establish a watch "near the (logical) top of its hierarchy", possibly with filtering by type, so it gets all events of interest without granular control.

@kyessenov
Copy link
Contributor

kyessenov commented Jun 21, 2017 via email

@lizan
Copy link
Contributor

lizan commented Jun 21, 2017

I just meant having multiple one directional streaming let gRPC provide more fine grained flow-control (flow control are per gRPC stream).

Copy link

@jmuk jmuk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some errors I've met when compiling the PR actually.

// within an api group.
rpc ListObjectTypes(ListObjectTypesRequest) returns (ListObjectTypesResponse) {
option (google.api.http) = {
get: "/{meta.api_group}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: meta.api_group -> query.api_group

// Delete a single object. May return validation errors.
rpc DeleteObject(DeleteObjectRequest) returns (DeleteObjectResponse) {
option (google.api.http) = {
delete: "/{meta.api_group}/{meta.object_type}/{meta.object_type_version}/{meta.object_group}/{meta.name}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: meta -> key

import "google/rpc/status.proto";
import "google/api/annotations.proto";

import "service.proto";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: import line should specify the path from the root. i.e. galley/v1/service.proto

import "google/rpc/status.proto";
import "google/api/annotations.proto";

import "service.proto";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto: galley/v1/service.proto

option (google.api.http) = {
get: "/{meta.api_group}"
additional_bindings: {
get: "/"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I commented previously, grpc-gateway can't compile with this spec (see grpc-ecosystem/grpc-gateway#414). Consider removing this additional binding for the time being.

};

// Create an object. This may result in configuration validation errors.
// Referential integrity should be maximally enforced, except where not possible or desirable.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this line means. Could you elaborate more?


// kv holds the KeyValue for the event.
// A PUT event contains current kv pair.
// A PUT event with kv.Version=1 indicates the creation of a key.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfigObject does not have version property.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe comment should refer to ConfigObject.Meta.revision?

// A PUT event contains current kv pair.
// A PUT event with kv.Version=1 indicates the creation of a key.
// A DELETE event contains the deleted key with
// its modification revision set to the revision of deletion.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's hard to ensure to deliver the exact revision when the deletion happened -- some compaction or bundled event delivery would lose such information. Maybe, we can just say that the revision means that the kv doesn't exist at that point.

// WatchEvents indicates that this message contains events from the watch stream.
message WatchEvents {
// returns events for the specified watch id.
repeated Event events = 4;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it also has the current_revision too?
Otherwise the watcher client can't tell what's the exact revision right now, it needs to wait for the next WatchProgress event. That sounds like redundant.

@@ -33,22 +33,22 @@ import "google/api/annotations.proto";
// api_group = /vendor1
service Galley {
// Get a single object.
rpc GetObject(GetObjectRequest) returns (Object) {
rpc GetObject(GetObjectRequest) returns (ConfigObject) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider rename rpc to use Config instead of Object? like GetConfig, ListConfig.

//
// It generalizes the concept of namespace to an arbitrary grouping.
//
// /apiGroup/objecttype/version/{objectGroup}/objectname
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consistent with camel case

// The primary field that was in error, if available.
string field = 2;
// The line number on which error occurred, if available.
int32 line_number = 3;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the config object has no information about the original source (source_data is already converted into Struct proto), it's probably impossible to respond with line number in the original source. Maybe we should keep the original source string in the config object too?

@geeknoid
Copy link
Contributor

geeknoid commented Jun 29, 2017 via email

import "google/rpc/status.proto";

// Galley follows
// the Kubenertes API server delegation and resource model.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the Galley design seems to abandon the k8s approach:
A granular resource oriented API based on the Kubernetes resource model was considered here. However it does not provide the required CI/CD experience therefore this document proposes a hierarchical resource API.
Can this be clarified?


message DeleteObjectRequest {
// key associated with the requested object.
Meta key = 1;
Copy link

@elevran elevran Jul 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any validation that the meta.version, meta.revision, etc. matches the current state before deletion?

// object_group is an arbitrary hierarchical grouping of resources.
// For Mixer configuration object_group == mesh, svc:svc1, svc:svc2 ...
// For Proxy it can be mesh, destination.
string object_group = 4;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comment above seems to suggest that string could have a format (e.g., svc:svc1, etc.) that indicates the grouping.
Is this the intent? Is Galley aware of the grouping and/or syntax used?

// If this object was deleted.
// In this case Validator should check referential integrity.
// Only metadata about the object being deleted is sent.
Meta deleted = 2;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And deny deletion if it could result in loss of referential integrity?
What about cross-component referential integrity - needed, possible?

// Create and cancel are part of the same message because the WatchRequest message
// is used in a streaming API. The client may add new watchers and remove old watchers
// on an existing stream.
message WatchRequest {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be helpful to describe a concrete use case, where a client needs (or does not need) to change subscription of a watch. While useful in theory, in many cases I would expect a component to establish a watch "near the (logical) top of its hierarchy", possibly with filtering by type, so it gets all events of interest without granular control.


// kv holds the KeyValue for the event.
// A PUT event contains current kv pair.
// A PUT event with kv.Version=1 indicates the creation of a key.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe comment should refer to ConfigObject.Meta.revision?

@mandarjog
Copy link
Contributor Author

@elevran I am abandoning this PR in favor of https://github.com/istio/galley/pull/51

@mandarjog mandarjog closed this Jul 11, 2017
nacx pushed a commit to nacx/api that referenced this pull request Feb 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.