From 0e4628d1d5900ae4229bb908ccf4e6d50caca6a1 Mon Sep 17 00:00:00 2001 From: David Morhovich Date: Mon, 4 Jan 2021 11:51:05 -0500 Subject: [PATCH 1/2] wip Signed-off-by: David Morhovich --- api_approaches | 113 +++++++ examples/local/scripts/vtgate-up.sh | 2 + examples/local/scripts/vttablet-down.sh | 2 +- go/test/endtoend/cluster/vtctl_process.go | 17 ++ .../endtoend/provision/plugin_grpc_test.go | 167 +++++++++++ go/vt/proto/provision/provision.pb.go | 275 ++++++++++++++++++ go/vt/proto/provision/provision.pb.go: | 1 + go/vt/provision/factory.go | 40 +++ go/vt/provision/noop.go | 43 +++ go/vt/provision/plugin_grpc.go | 122 ++++++++ go/vt/provision/provision.go | 60 ++++ go/vt/topo/keyspace.go | 26 ++ go/vt/topo/server.go | 13 +- go/vt/vtgate/engine/create_keyspace.go | 86 ++++++ go/vt/vtgate/engine/delete_keyspace.go | 86 ++++++ go/vt/vtgate/engine/primitive.go | 3 + go/vt/vtgate/executor.go | 5 +- go/vt/vtgate/planbuilder/builder.go | 4 +- go/vt/vtgate/planbuilder/create_keyspace.go | 12 + go/vt/vtgate/planbuilder/delete_keyspace.go | 12 + .../provision_create_acl.go | 62 ++++ .../provision_create_acl_test.go | 65 +++++ .../provision_delete_acl.go | 62 ++++ .../provision_delete_acl_test.go | 65 +++++ go/vt/vtgate/vcursor_impl.go | 106 ++++++- misc/git/pre-commit | 24 +- proto/provision.proto | 36 +++ test/config.json | 7 + 28 files changed, 1492 insertions(+), 24 deletions(-) create mode 100644 api_approaches create mode 100644 go/test/endtoend/provision/plugin_grpc_test.go create mode 100644 go/vt/proto/provision/provision.pb.go create mode 100644 go/vt/proto/provision/provision.pb.go: create mode 100644 go/vt/provision/factory.go create mode 100644 go/vt/provision/noop.go create mode 100644 go/vt/provision/plugin_grpc.go create mode 100644 go/vt/provision/provision.go create mode 100644 go/vt/vtgate/engine/create_keyspace.go create mode 100644 go/vt/vtgate/engine/delete_keyspace.go create mode 100644 go/vt/vtgate/planbuilder/create_keyspace.go create mode 100644 go/vt/vtgate/planbuilder/delete_keyspace.go create mode 100644 go/vt/vtgate/provisioncreateacl/provision_create_acl.go create mode 100644 go/vt/vtgate/provisioncreateacl/provision_create_acl_test.go create mode 100644 go/vt/vtgate/provisiondeleteacl/provision_delete_acl.go create mode 100644 go/vt/vtgate/provisiondeleteacl/provision_delete_acl_test.go create mode 100644 proto/provision.proto diff --git a/api_approaches b/api_approaches new file mode 100644 index 00000000000..e1c140f6bf1 --- /dev/null +++ b/api_approaches @@ -0,0 +1,113 @@ +I see two ways to implement the "hook" for this: + +1) ExecuteCreateKeyspace calls some async endpoint that was passed in a CLI flag to vtgate: + +ExecuteCreateKeyspace: +Does keyspace exist in topo? + true: + Return a mysql error that database already exists. + false: + vtgate sends request to keyspace create service + other added keyspace: + Nothing happens at this step. + keyspace service partition: + Retry loop to send request. + vtgate partition: +(1) The user is disconnected. When they reconnect, if they run `CREATE DATABASE` again, they'll end up sending another request if the first hasn't finished. + Depending on what the other side does, that's probably fine. + vtgate and keyspace service partition: + The user is disconnected and the request is lost. + true: +(2) The request has succesfully been submitted. Does keyspace exists in topo? + true: + Return success to the user. + false: + Repeat (2). + vtgate partition: + Same as (1). + vtgate partition: + The CREATE DATABASE request is lost and the user is disconnected. + keyspace removed: + Returns a mysql error that database already exists, even though it does not. + + + +2) ExecuteCreateKeyspace records the "DesiredKeyspace" in the topo server in a dir seperate from keyspace, and anything who wants to repond to desired keyspace requests can subscribe to it in some way. psuedocode + +ExecuteCreateKeyspace: +Does keyspace exist in topo? + true: + Does desired keyspace exist in topo? + true: + Clean up orphaned desired keyspace and return mysql error that database already exists. + false: + Return mysql error that database already exists. + vtgate partition: + The CREATE DATABASE request is lost and the user is disconnected. + keyspace removed: + Any orphan desired state is correctly cleaned up.We return that the database exists even though it does not. + false: + Try to create desired keyspace in topo. + vtgate partition: + The CREATE DATABASE request is saved and the user is disconnected. If they attempt to rerun the CREATE, they will end up at (1) or (2). + Did the desired keyspace already exist in topo? + true: + Does the desired keyspace in topo match our desired keyspace? (This will always be true when the only piece of information we're saving is the keyspace name) + true: +(1) Block and poll keyspace until it exists. + Remove desired keyspace. + Return success to the user. + false: +(2) Return error to the user that a different keyspace request is already in progress with params : %v. + false: + Block and poll keyspace until it exists. + Remove desired keyspace. + Return success to the user. + false: + + vtgate partition: + The CREATE DATABASE request is lost and the user is disconnected. + + + + +update topserver DesiredKeyspace + already exists: return an error that states keyspace creation already in progress [or block] + does not exit: + + + +does Keyspace exit? Remove DesiredKeyspace and return +if not, poll getKeyspaces until it pops up, then remove desiredkeyspace and return + +in thirdpartyservice [presumably you'd only have one consumer, the operator]: + + + + + + + + +When it's fullfilled the desire, it removes it from the topo server, and CREATE DATABSE returns. + + + +Advantages of approach 1: +1) YAGNI - it's relatively straightfoward to implement. +2) We don't need asynchoncitiy in this case, we're going to block the CREATE DATABASE until the creation is complete. + +Disadvantages of approach 1: +1) It's quasi racey. However, that doesn't matter for CREATE DABASE specifically because +2) I find it much harder to reason about the non-happy path in terms of network issues. +3) If we ever introduce anything like "CREATE DATABASE hello SHARDS 3", we have a race. +4) DNS or whatever to "catch the tail" of the operator + + + + +disavdnatnges of approach 2: + dependiy might not be quite as clear + +advanatages of approach 2 + easier operationally diff --git a/examples/local/scripts/vtgate-up.sh b/examples/local/scripts/vtgate-up.sh index da04c8d1647..5427c44ac52 100755 --- a/examples/local/scripts/vtgate-up.sh +++ b/examples/local/scripts/vtgate-up.sh @@ -40,6 +40,8 @@ vtgate \ -service_map 'grpc-vtgateservice' \ -pid_file $VTDATAROOT/tmp/vtgate.pid \ -mysql_auth_server_impl none \ + -provision_authorized_users '%' \ + -provisioner_type 'grpc' \ > $VTDATAROOT/tmp/vtgate.out 2>&1 & # Block waiting for vtgate to be listening diff --git a/examples/local/scripts/vttablet-down.sh b/examples/local/scripts/vttablet-down.sh index 47b881b9793..9356e911992 100755 --- a/examples/local/scripts/vttablet-down.sh +++ b/examples/local/scripts/vttablet-down.sh @@ -22,7 +22,7 @@ source ./env.sh printf -v tablet_dir 'vt_%010d' $TABLET_UID pid=`cat $VTDATAROOT/$tablet_dir/vttablet.pid` -kill $pid +kill -9 $pid # Wait for vttablet to die. while ps -p $pid > /dev/null; do sleep 1; done diff --git a/go/test/endtoend/cluster/vtctl_process.go b/go/test/endtoend/cluster/vtctl_process.go index b2dfe753f37..c0f4cd4b848 100644 --- a/go/test/endtoend/cluster/vtctl_process.go +++ b/go/test/endtoend/cluster/vtctl_process.go @@ -73,6 +73,23 @@ func (vtctl *VtctlProcess) CreateKeyspace(keyspace string) (err error) { return tmpProcess.Run() } +// DeleteKeyspace executes vtctl command to create keyspace +func (vtctl *VtctlProcess) DeleteKeyspace(keyspace string) (err error) { + tmpProcess := exec.Command( + vtctl.Binary, + "-topo_implementation", vtctl.TopoImplementation, + "-topo_global_server_address", vtctl.TopoGlobalAddress, + "-topo_global_root", vtctl.TopoGlobalRoot, + ) + if *isCoverage { + tmpProcess.Args = append(tmpProcess.Args, "-test.coverprofile="+getCoveragePath("vtctl-delete-ks.out")) + } + tmpProcess.Args = append(tmpProcess.Args, + "DeleteKeyspace", keyspace) + log.Infof("Running DeleteKeyspace with command: %v", strings.Join(tmpProcess.Args, " ")) + return tmpProcess.Run() +} + // ExecuteCommandWithOutput executes any vtctlclient command and returns output func (vtctl *VtctlProcess) ExecuteCommandWithOutput(args ...string) (result string, err error) { args = append([]string{ diff --git a/go/test/endtoend/provision/plugin_grpc_test.go b/go/test/endtoend/provision/plugin_grpc_test.go new file mode 100644 index 00000000000..f371d463ec2 --- /dev/null +++ b/go/test/endtoend/provision/plugin_grpc_test.go @@ -0,0 +1,167 @@ +/* +Copyright 2019 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sequence + +import ( + "context" + "flag" + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "net" + "os" + "testing" + "time" + "vitess.io/vitess/go/mysql" + "vitess.io/vitess/go/test/endtoend/cluster" + "vitess.io/vitess/go/vt/log" + "vitess.io/vitess/go/vt/proto/provision" +) + +var ( + clusterForProvisionTest *cluster.LocalProcessCluster + cell = "zone1" + hostname = "localhost" + keyspace = "keyspace" +) + +func TestMain(m *testing.M) { + defer cluster.PanicHandler(nil) + flag.Parse() + + exitCode := func() int { + + var lc net.ListenConfig + listener, err := lc.Listen(context.Background(), "tcp", "localhost:") + if err != nil { + log.Error(err) + return 1 + } + + defer listener.Close() + + go func() { + log.Error(startProvisionerServer(listener)) + }() + + clusterForProvisionTest = cluster.NewCluster(cell, hostname) + //FIXME: underscores or dashes + clusterForProvisionTest.VtGateExtraArgs = []string { + "-provision_create_keyspace_authorized_users", + "%", + "-provision_delete_keyspace_authorized_users", + "%", + "-provision_timeout", + "30s", + "-provision_type", + "grpc", + "-provision_grpc_endpoint", + listener.Addr().String(), + "-provision_grpc_dial_timeout", + "1s", + "-provision_grpc_per_retry_timeout", + "1s", + "-provision_grpc_max_retries", + "1", + } + + defer clusterForProvisionTest.Teardown() + + if err := clusterForProvisionTest.StartTopo(); err != nil { + return 1 + } + + if err := clusterForProvisionTest.StartVtgate(); err != nil { + return 1 + } + + return m.Run() + }() + os.Exit(exitCode) +} + +func TestProvisionKeyspace(t *testing.T) { + defer cluster.PanicHandler(t) + + ctx := context.Background() + vtParams := mysql.ConnParams{ + Host: clusterForProvisionTest.Hostname, + Port: clusterForProvisionTest.VtgateMySQLPort, + ConnectTimeoutMs: 1000, + } + conn, err := mysql.Connect(ctx, &vtParams) + require.Nil(t, err) + + createStatement := fmt.Sprintf("CREATE DATABASE %s;", keyspace) + qr, err := conn.ExecuteFetch(createStatement, 10, true) + require.Nil(t, err) + + assert.Equal(t, uint64(1), qr.RowsAffected, "returned: %v", qr.Rows) + + _, err = clusterForProvisionTest.VtctlclientProcess.ExecuteCommandWithOutput("GetKeyspace", keyspace) + //If GetKeyspace doesn't return an error, the keyspace exists. + require.Nil(t, err) + + dropStatement := fmt.Sprintf("DROP DATABASE %s;", keyspace) + qr, err = conn.ExecuteFetch(dropStatement, 10, true) + require.Nil(t, err) + + assert.Equal(t, uint64(1), qr.RowsAffected, "returned: %v", qr.Rows) + + _, err = clusterForProvisionTest.VtctlclientProcess.ExecuteCommandWithOutput("GetKeyspace", keyspace) + //If GetKeyspace does return an error, we assume it's because the keyspace no longer exists, and not because of + //a network error. + assert.True(t, err != nil, "keyspace %s was not deleted", keyspace) +} + +type testGrpcServer struct {} + +func (_ testGrpcServer)RequestCreateKeyspace(ctx context.Context, rckr *provision.RequestCreateKeyspaceRequest) (*provision.ProvisionResponse, error) { + //We're doing this in a go routine to simulate the fact that RequestCreateKeyspace does not block while the + //the keyspace is being created. We want to exercise the topo polling logic, so we use a large wait time. + go func() { + <- time.After(10 * time.Second) + err := clusterForProvisionTest.VtctlProcess.CreateKeyspace(rckr.Keyspace) + if err != nil { + log.Error(err) + } + }() + return &provision.ProvisionResponse{}, nil +} + +func (_ testGrpcServer)RequestDeleteKeyspace(ctx context.Context, rckr *provision.RequestDeleteKeyspaceRequest) (*provision.ProvisionResponse, error) { + //We're doing this in a go routine to simulate the fact that RequestDeleteKeyspace does not block while the + //the keyspace is being deleted. We want to exercise the topo polling logic, so we use a large wait time. + go func() { + <- time.After(10 * time.Second) + err := clusterForProvisionTest.VtctlProcess.DeleteKeyspace(rckr.Keyspace) + if err != nil { + log.Error(err) + } + }() + return &provision.ProvisionResponse{}, nil +} + +func startProvisionerServer(listener net.Listener) error { + var opts []grpc.ServerOption + grpcServer := grpc.NewServer(opts...) + defer grpcServer.Stop() + + provision.RegisterProvisionServer(grpcServer, testGrpcServer{}) + return grpcServer.Serve(listener) +} \ No newline at end of file diff --git a/go/vt/proto/provision/provision.pb.go b/go/vt/proto/provision/provision.pb.go new file mode 100644 index 00000000000..0fb8de3990f --- /dev/null +++ b/go/vt/proto/provision/provision.pb.go @@ -0,0 +1,275 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: provision.proto + +package provision + +import ( + context "context" + fmt "fmt" + math "math" + + proto "github.com/golang/protobuf/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type RequestCreateKeyspaceRequest struct { + Keyspace string `protobuf:"bytes,1,opt,name=keyspace,proto3" json:"keyspace,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RequestCreateKeyspaceRequest) Reset() { *m = RequestCreateKeyspaceRequest{} } +func (m *RequestCreateKeyspaceRequest) String() string { return proto.CompactTextString(m) } +func (*RequestCreateKeyspaceRequest) ProtoMessage() {} +func (*RequestCreateKeyspaceRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_3500c3a98af60624, []int{0} +} + +func (m *RequestCreateKeyspaceRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RequestCreateKeyspaceRequest.Unmarshal(m, b) +} +func (m *RequestCreateKeyspaceRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RequestCreateKeyspaceRequest.Marshal(b, m, deterministic) +} +func (m *RequestCreateKeyspaceRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_RequestCreateKeyspaceRequest.Merge(m, src) +} +func (m *RequestCreateKeyspaceRequest) XXX_Size() int { + return xxx_messageInfo_RequestCreateKeyspaceRequest.Size(m) +} +func (m *RequestCreateKeyspaceRequest) XXX_DiscardUnknown() { + xxx_messageInfo_RequestCreateKeyspaceRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_RequestCreateKeyspaceRequest proto.InternalMessageInfo + +func (m *RequestCreateKeyspaceRequest) GetKeyspace() string { + if m != nil { + return m.Keyspace + } + return "" +} + +type RequestDeleteKeyspaceRequest struct { + Keyspace string `protobuf:"bytes,1,opt,name=keyspace,proto3" json:"keyspace,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RequestDeleteKeyspaceRequest) Reset() { *m = RequestDeleteKeyspaceRequest{} } +func (m *RequestDeleteKeyspaceRequest) String() string { return proto.CompactTextString(m) } +func (*RequestDeleteKeyspaceRequest) ProtoMessage() {} +func (*RequestDeleteKeyspaceRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_3500c3a98af60624, []int{1} +} + +func (m *RequestDeleteKeyspaceRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RequestDeleteKeyspaceRequest.Unmarshal(m, b) +} +func (m *RequestDeleteKeyspaceRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RequestDeleteKeyspaceRequest.Marshal(b, m, deterministic) +} +func (m *RequestDeleteKeyspaceRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_RequestDeleteKeyspaceRequest.Merge(m, src) +} +func (m *RequestDeleteKeyspaceRequest) XXX_Size() int { + return xxx_messageInfo_RequestDeleteKeyspaceRequest.Size(m) +} +func (m *RequestDeleteKeyspaceRequest) XXX_DiscardUnknown() { + xxx_messageInfo_RequestDeleteKeyspaceRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_RequestDeleteKeyspaceRequest proto.InternalMessageInfo + +func (m *RequestDeleteKeyspaceRequest) GetKeyspace() string { + if m != nil { + return m.Keyspace + } + return "" +} + +type ProvisionResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ProvisionResponse) Reset() { *m = ProvisionResponse{} } +func (m *ProvisionResponse) String() string { return proto.CompactTextString(m) } +func (*ProvisionResponse) ProtoMessage() {} +func (*ProvisionResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_3500c3a98af60624, []int{2} +} + +func (m *ProvisionResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ProvisionResponse.Unmarshal(m, b) +} +func (m *ProvisionResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ProvisionResponse.Marshal(b, m, deterministic) +} +func (m *ProvisionResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ProvisionResponse.Merge(m, src) +} +func (m *ProvisionResponse) XXX_Size() int { + return xxx_messageInfo_ProvisionResponse.Size(m) +} +func (m *ProvisionResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ProvisionResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ProvisionResponse proto.InternalMessageInfo + +func init() { + proto.RegisterType((*RequestCreateKeyspaceRequest)(nil), "provision.RequestCreateKeyspaceRequest") + proto.RegisterType((*RequestDeleteKeyspaceRequest)(nil), "provision.RequestDeleteKeyspaceRequest") + proto.RegisterType((*ProvisionResponse)(nil), "provision.ProvisionResponse") +} + +func init() { proto.RegisterFile("provision.proto", fileDescriptor_3500c3a98af60624) } + +var fileDescriptor_3500c3a98af60624 = []byte{ + // 183 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2f, 0x28, 0xca, 0x2f, + 0xcb, 0x2c, 0xce, 0xcc, 0xcf, 0xd3, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x84, 0x0b, 0x28, + 0x59, 0x71, 0xc9, 0x04, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x38, 0x17, 0xa5, 0x26, 0x96, 0xa4, + 0x7a, 0xa7, 0x56, 0x16, 0x17, 0x24, 0x26, 0xa7, 0x42, 0x05, 0x85, 0xa4, 0xb8, 0x38, 0xb2, 0xa1, + 0x42, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x70, 0x3e, 0x92, 0x5e, 0x97, 0xd4, 0x9c, 0x54, + 0xd2, 0xf4, 0x0a, 0x73, 0x09, 0x06, 0xc0, 0x1c, 0x11, 0x94, 0x5a, 0x5c, 0x90, 0x9f, 0x57, 0x9c, + 0x6a, 0x74, 0x9a, 0x91, 0x8b, 0x13, 0x2e, 0x2a, 0x14, 0xc7, 0x25, 0x8a, 0xd5, 0x69, 0x42, 0xea, + 0x7a, 0x08, 0x0f, 0xe1, 0x73, 0xbc, 0x94, 0x0c, 0x92, 0x42, 0x0c, 0xdb, 0x90, 0xcc, 0x47, 0x75, + 0x3e, 0x36, 0xf3, 0xb1, 0x7a, 0x10, 0xbf, 0xf9, 0x4e, 0x1a, 0x51, 0x6a, 0x65, 0x99, 0x25, 0xa9, + 0xc5, 0xc5, 0x7a, 0x99, 0xf9, 0xfa, 0x10, 0x96, 0x7e, 0x7a, 0xbe, 0x7e, 0x59, 0x89, 0x3e, 0x38, + 0x16, 0xf4, 0xe1, 0x7a, 0x93, 0xd8, 0xc0, 0x02, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x59, + 0x3f, 0x10, 0x8e, 0xa9, 0x01, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// ProvisionClient is the client API for Provision service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type ProvisionClient interface { + RequestCreateKeyspace(ctx context.Context, in *RequestCreateKeyspaceRequest, opts ...grpc.CallOption) (*ProvisionResponse, error) + RequestDeleteKeyspace(ctx context.Context, in *RequestDeleteKeyspaceRequest, opts ...grpc.CallOption) (*ProvisionResponse, error) +} + +type provisionClient struct { + cc *grpc.ClientConn +} + +func NewProvisionClient(cc *grpc.ClientConn) ProvisionClient { + return &provisionClient{cc} +} + +func (c *provisionClient) RequestCreateKeyspace(ctx context.Context, in *RequestCreateKeyspaceRequest, opts ...grpc.CallOption) (*ProvisionResponse, error) { + out := new(ProvisionResponse) + err := c.cc.Invoke(ctx, "/provision.Provision/RequestCreateKeyspace", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *provisionClient) RequestDeleteKeyspace(ctx context.Context, in *RequestDeleteKeyspaceRequest, opts ...grpc.CallOption) (*ProvisionResponse, error) { + out := new(ProvisionResponse) + err := c.cc.Invoke(ctx, "/provision.Provision/RequestDeleteKeyspace", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ProvisionServer is the server API for Provision service. +type ProvisionServer interface { + RequestCreateKeyspace(context.Context, *RequestCreateKeyspaceRequest) (*ProvisionResponse, error) + RequestDeleteKeyspace(context.Context, *RequestDeleteKeyspaceRequest) (*ProvisionResponse, error) +} + +// UnimplementedProvisionServer can be embedded to have forward compatible implementations. +type UnimplementedProvisionServer struct { +} + +func (*UnimplementedProvisionServer) RequestCreateKeyspace(ctx context.Context, req *RequestCreateKeyspaceRequest) (*ProvisionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RequestCreateKeyspace not implemented") +} +func (*UnimplementedProvisionServer) RequestDeleteKeyspace(ctx context.Context, req *RequestDeleteKeyspaceRequest) (*ProvisionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RequestDeleteKeyspace not implemented") +} + +func RegisterProvisionServer(s *grpc.Server, srv ProvisionServer) { + s.RegisterService(&_Provision_serviceDesc, srv) +} + +func _Provision_RequestCreateKeyspace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RequestCreateKeyspaceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProvisionServer).RequestCreateKeyspace(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/provision.Provision/RequestCreateKeyspace", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProvisionServer).RequestCreateKeyspace(ctx, req.(*RequestCreateKeyspaceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provision_RequestDeleteKeyspace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RequestDeleteKeyspaceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProvisionServer).RequestDeleteKeyspace(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/provision.Provision/RequestDeleteKeyspace", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProvisionServer).RequestDeleteKeyspace(ctx, req.(*RequestDeleteKeyspaceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Provision_serviceDesc = grpc.ServiceDesc{ + ServiceName: "provision.Provision", + HandlerType: (*ProvisionServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "RequestCreateKeyspace", + Handler: _Provision_RequestCreateKeyspace_Handler, + }, + { + MethodName: "RequestDeleteKeyspace", + Handler: _Provision_RequestDeleteKeyspace_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "provision.proto", +} diff --git a/go/vt/proto/provision/provision.pb.go: b/go/vt/proto/provision/provision.pb.go: new file mode 100644 index 00000000000..ec3a23c473e --- /dev/null +++ b/go/vt/proto/provision/provision.pb.go: @@ -0,0 +1 @@ +package hi diff --git a/go/vt/provision/factory.go b/go/vt/provision/factory.go new file mode 100644 index 00000000000..ec36521a850 --- /dev/null +++ b/go/vt/provision/factory.go @@ -0,0 +1,40 @@ +/* +Copyright 2019 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provision + +import ( + "vitess.io/vitess/go/vt/log" + vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc" + "vitess.io/vitess/go/vt/vterrors" +) + +var ( + provisioners = make(map[string]Provisioner) +) + +func factory(provisionerType string) Provisioner { + p, ok := provisioners[provisionerType] + if !ok { + log.Error(vterrors.Errorf( + vtrpcpb.Code_INVALID_ARGUMENT, + "failed to find %s provisioner, defaulting to noop", + provisionerType, + )) + return noopProvisioner{} + } + return p +} diff --git a/go/vt/provision/noop.go b/go/vt/provision/noop.go new file mode 100644 index 00000000000..47713dc294c --- /dev/null +++ b/go/vt/provision/noop.go @@ -0,0 +1,43 @@ +/* +Copyright 2019 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provision + +import ( + "context" + vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc" + "vitess.io/vitess/go/vt/vterrors" +) + +type noopProvisioner struct{} + +var ErrNoopInUse = vterrors.Errorf( + vtrpcpb.Code_UNIMPLEMENTED, + "noop provisioner in use. select a different provisioner using vtgate flag -provisioner_type", +) + +func (noopProvisioner) RequestCreateKeyspace(ctx context.Context, keyspace string) error { + return ErrNoopInUse +} + +func (noopProvisioner) RequestDeleteKeyspace(ctx context.Context, keyspace string) error { + return ErrNoopInUse +} + +func init() { + provisioners["noop"] = noopProvisioner{} +} + diff --git a/go/vt/provision/plugin_grpc.go b/go/vt/provision/plugin_grpc.go new file mode 100644 index 00000000000..aa49b06c33f --- /dev/null +++ b/go/vt/provision/plugin_grpc.go @@ -0,0 +1,122 @@ +/* +Copyright 2019 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provision + +import ( + "context" + "flag" + grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" + "google.golang.org/grpc" + "time" + "vitess.io/vitess/go/vt/proto/provision" + vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc" + "vitess.io/vitess/go/vt/vterrors" +) + + +var ( + ErrNeedGrpcEndpoint = vterrors.Errorf( + vtrpcpb.Code_FAILED_PRECONDITION, + "need grpc endpoint to use grpc provisioning", + ) + + provisionGrpcEndpoint = flag.String( + "provision_grpc_endpoint", + "", + "gRPC endpoint to connect to. This is required if `grpc` is specified for `-provision_type`.", + ) + provisionGrpcDialTimeout = flag.Duration( + "provision_grpc_dial_timeout", + time.Duration(5 * time.Second), + "Maximum time to try connecting to the gRPC endpoint before timing out.", + ) + provisionGrpcRequestTimeout = flag.Duration( + "provision_grpc_per_retry_timeout", + time.Duration(5 * time.Second), + "Maximum time to wait for a provision request to before timing out.", + ) + provisionGrpcMaxRetries = flag.Uint( + "provision_grpc_max_retries", + 3, + "Maximum times to try sending a provision request.", + ) + + +) +type grpcProvisioner struct {} + +func withOpenClient(ctx context.Context, callback func (client provision.ProvisionClient) error) error { + if *provisionGrpcEndpoint == "" { + return ErrNeedGrpcEndpoint + } + dialTimeout, cancel := context.WithTimeout(ctx, *provisionGrpcDialTimeout) + defer cancel() + + //FIXME: tls + conn, err := grpc.DialContext(dialTimeout, *provisionGrpcEndpoint, grpc.WithInsecure(), grpc.WithBlock()) + if err != nil { + vterrors.Wrapf(err, "dialing to grpc provisioner timed out") + } + defer conn.Close() + + return callback(provision.NewProvisionClient(conn)) +} + + +func (p grpcProvisioner) RequestCreateKeyspace(ctx context.Context, keyspace string) error { + return withOpenClient(ctx, func(client provision.ProvisionClient) error { + req := &provision.RequestCreateKeyspaceRequest{ + Keyspace: keyspace, + } + + _, err := client.RequestCreateKeyspace( + ctx, + req, + grpc_retry.WithPerRetryTimeout(*provisionGrpcRequestTimeout), + grpc_retry.WithMax(*provisionGrpcMaxRetries), + grpc_retry.WithBackoff( + grpc_retry.BackoffLinear(1 * time.Second), + ), + ) + + return err + }) +} + +func (p grpcProvisioner) RequestDeleteKeyspace(ctx context.Context, keyspace string) error { + return withOpenClient(ctx, func(client provision.ProvisionClient) error { + req := &provision.RequestDeleteKeyspaceRequest{ + Keyspace: keyspace, + } + + _, err := client.RequestDeleteKeyspace( + ctx, + req, + grpc_retry.WithPerRetryTimeout(*provisionGrpcRequestTimeout), + grpc_retry.WithMax(*provisionGrpcMaxRetries), + grpc_retry.WithBackoff( + grpc_retry.BackoffLinear(1 * time.Second), + ), + ) + + return err + }) +} + +func init() { + provisioners["grpc"] = grpcProvisioner{} +} \ No newline at end of file diff --git a/go/vt/provision/provision.go b/go/vt/provision/provision.go new file mode 100644 index 00000000000..c69caf8bdcd --- /dev/null +++ b/go/vt/provision/provision.go @@ -0,0 +1,60 @@ +/* +Copyright 2019 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package trace contains a helper interface that allows various tracing +// tools to be plugged in to components using this interface. If no plugin is +// registered, the default one makes all trace calls into no-ops. +package provision + +import ( + "context" + "flag" + "time" +) + +var ( + //FIXME: _ or -, docstrings + provisionerType = flag.String( + "provision_type", + "noop", + "Specifies which provision implementation to use. Available options are: noop, grpc.", + ) + ProvisionerTimeout = flag.Duration( + "provision_timeout", + time.Duration(5 * time.Minute), + "Database DDL statements are synchronous from the perspective of a connected user. This specifies" + + "the maximum time to wait before returning an error. Note that this the ONLY has an effect on the user" + + " experience. The actual provision operation runs asynchronously for an unbounded amount of time.", + ) +) + +/* +The contract for the methods of Provisioner is that they return nil if they have successfully received your request. +The caller still needs to check with topo to see if the keyspace has been created or deleted. +The caller does not need to handle retries. + */ +type Provisioner interface { + RequestCreateKeyspace(ctx context.Context, keyspace string) error + RequestDeleteKeyspace(ctx context.Context, keyspace string) error +} + +func RequestCreateKeyspace(ctx context.Context, keyspace string) error { + return factory(*provisionerType).RequestCreateKeyspace(ctx, keyspace) +} + +func RequestDeleteKeyspace(ctx context.Context, keyspace string) error { + return factory(*provisionerType).RequestDeleteKeyspace(ctx, keyspace) +} diff --git a/go/vt/topo/keyspace.go b/go/vt/topo/keyspace.go index 2daeffefba5..a20157ead12 100755 --- a/go/vt/topo/keyspace.go +++ b/go/vt/topo/keyspace.go @@ -171,6 +171,7 @@ func (ts *Server) CreateKeyspace(ctx context.Context, keyspace string, value *to return nil } + // GetKeyspace reads the given keyspace and returns it func (ts *Server) GetKeyspace(ctx context.Context, keyspace string) (*KeyspaceInfo, error) { keyspacePath := path.Join(KeyspacesPath, keyspace, KeyspaceFile) @@ -191,6 +192,18 @@ func (ts *Server) GetKeyspace(ctx context.Context, keyspace string) (*KeyspaceIn }, nil } +// KeyspaceExists returns whether a keyspace exists in the topo server. +func (ts *Server) KeyspaceExists(ctx context.Context, keyspace string) (bool, error) { + _, err := ts.GetKeyspace(ctx, keyspace) + if err == nil { + return true, nil + } + if IsErrType(err, NoNode) { + return false, nil + } + return false, err +} + // UpdateKeyspace updates the keyspace data. It checks the keyspace is locked. func (ts *Server) UpdateKeyspace(ctx context.Context, ki *KeyspaceInfo) error { // make sure it is locked first @@ -313,6 +326,19 @@ func (ts *Server) GetKeyspaces(ctx context.Context) ([]string, error) { } } +// GetKeyspaceDesiredStates returns the list of keyspace desired states in the topology. +func (ts *Server) GetKeyspaceDesiredStates(ctx context.Context) ([]string, error) { + children, err := ts.globalCell.ListDir(ctx, KeyspaceDesiredStates, false /*full*/) + switch { + case err == nil: + return DirEntriesToStringArray(children), nil + case IsErrType(err, NoNode): + return nil, nil + default: + return nil, err + } +} + // GetShardNames returns the list of shards in a keyspace. func (ts *Server) GetShardNames(ctx context.Context, keyspace string) ([]string, error) { shardsPath := path.Join(KeyspacesPath, keyspace, ShardsPath) diff --git a/go/vt/topo/server.go b/go/vt/topo/server.go index f2b4bbe3408..0fe14fe5113 100644 --- a/go/vt/topo/server.go +++ b/go/vt/topo/server.go @@ -79,12 +79,13 @@ const ( // Path for all object types. const ( - CellsPath = "cells" - CellsAliasesPath = "cells_aliases" - KeyspacesPath = "keyspaces" - ShardsPath = "shards" - TabletsPath = "tablets" - MetadataPath = "metadata" + CellsPath = "cells" + CellsAliasesPath = "cells_aliases" + KeyspacesPath = "keyspaces" + KeyspaceDesiredStates = "desired_keyspace_creates" + ShardsPath = "shards" + TabletsPath = "tablets" + MetadataPath = "metadata" ) // Factory is a factory interface to create Conn objects. diff --git a/go/vt/vtgate/engine/create_keyspace.go b/go/vt/vtgate/engine/create_keyspace.go new file mode 100644 index 00000000000..149142aaf98 --- /dev/null +++ b/go/vt/vtgate/engine/create_keyspace.go @@ -0,0 +1,86 @@ +/* +Copyright 2020 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package engine + +import ( + "vitess.io/vitess/go/sqltypes" + "vitess.io/vitess/go/vt/proto/query" + querypb "vitess.io/vitess/go/vt/proto/query" + vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc" + "vitess.io/vitess/go/vt/vterrors" +) + +var _ Primitive = (*CreateKeyspace)(nil) + +//CreateKeyspace represents the instructions to create a new keyspace via the user issuing a "CREATE DATABASE" type +//statement. As the actual creation logic is outside of the scope of vitess, the request is submitted to a service. +type CreateKeyspace struct { + Keyspace string + IfNotExists bool + noTxNeeded + noInputs +} + +func (v *CreateKeyspace) description() PrimitiveDescription { + return PrimitiveDescription{ + OperatorType: "CreateKeyspace", + Keyspace: nil, + Other: map[string]interface{}{ + "keyspace": v.Keyspace, + }, + } +} + +//RouteType implements the Primitive interface +func (v *CreateKeyspace) RouteType() string { + return "CreateKeyspace" +} + +//GetKeyspaceName implements the Primitive interface +func (v *CreateKeyspace) GetKeyspaceName() string { + return "" // FIXME: david - don't think this is reachable in a way that makes sense +} + +//GetTableName implements the Primitive interface +func (v *CreateKeyspace) GetTableName() string { + return "" // FIXME: david - don't this is reachable in a way that makes sense +} + +//Execute implements the Primitive interface +func (v *CreateKeyspace) Execute(vcursor VCursor, bindVars map[string]*query.BindVariable, wantfields bool) (result *sqltypes.Result, err error) { + err = vcursor.ExecuteCreateKeyspace(v.Keyspace, v.IfNotExists) + if err != nil { + return nil, err + } + + result = &sqltypes.Result{ + Fields: []*querypb.Field{}, + Rows: [][]sqltypes.Value{}, + RowsAffected: 1, + } + return result, err +} + +//StreamExecute implements the Primitive interface +func (v *CreateKeyspace) StreamExecute(vcursor VCursor, bindVars map[string]*query.BindVariable, wantields bool, callback func(*sqltypes.Result) error) error { + return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "not reachable") // FIXME: david - copied from online_ddl.go, also have no idea if this should work +} + +//GetFields implements the Primitive interface +func (v *CreateKeyspace) GetFields(vcursor VCursor, bindVars map[string]*query.BindVariable) (*sqltypes.Result, error) { + return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "not reachable") // FIXME: david - copied from online_ddl.go, also have no idea if this should work +} diff --git a/go/vt/vtgate/engine/delete_keyspace.go b/go/vt/vtgate/engine/delete_keyspace.go new file mode 100644 index 00000000000..3c20bd774c2 --- /dev/null +++ b/go/vt/vtgate/engine/delete_keyspace.go @@ -0,0 +1,86 @@ +/* +Copyright 2020 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package engine + +import ( + "vitess.io/vitess/go/sqltypes" + "vitess.io/vitess/go/vt/proto/query" + querypb "vitess.io/vitess/go/vt/proto/query" + vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc" + "vitess.io/vitess/go/vt/vterrors" +) + +var _ Primitive = (*DeleteKeyspace)(nil) + +//DeleteKeyspace represents the instructions to delete keyspace via the user issuing a "DROP DATABASE" type +//statement. As the actual creation logic is outside of the scope of vitess, the request is submitted to a service. +type DeleteKeyspace struct { + Keyspace string + IfExists bool + noTxNeeded + noInputs +} + +func (v *DeleteKeyspace) description() PrimitiveDescription { + return PrimitiveDescription{ + OperatorType: "DeleteKeyspace", + Keyspace: nil, + Other: map[string]interface{}{ + "keyspace": v.Keyspace, + }, + } +} + +//RouteType implements the Primitive interface +func (v *DeleteKeyspace) RouteType() string { + return "DeleteKeyspace" +} + +//GetKeyspaceName implements the Primitive interface +func (v *DeleteKeyspace) GetKeyspaceName() string { + return "" // FIXME: david - don't think this is reachable in a way that makes sense +} + +//GetTableName implements the Primitive interface +func (v *DeleteKeyspace) GetTableName() string { + return "" // FIXME: david - don't this is reachable in a way that makes sense +} + +//Execute implements the Primitive interface +func (v *DeleteKeyspace) Execute(vcursor VCursor, bindVars map[string]*query.BindVariable, wantfields bool) (result *sqltypes.Result, err error) { + err = vcursor.ExecuteDeleteKeyspace(v.Keyspace, v.IfExists) + if err != nil { + return nil, err + } + + result = &sqltypes.Result{ + Fields: []*querypb.Field{}, + Rows: [][]sqltypes.Value{}, + RowsAffected: 1, + } + return result, err +} + +//StreamExecute implements the Primitive interface +func (v *DeleteKeyspace) StreamExecute(vcursor VCursor, bindVars map[string]*query.BindVariable, wantields bool, callback func(*sqltypes.Result) error) error { + return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "not reachable") // FIXME: david - copied from online_ddl.go, also have no idea if this should work +} + +//GetFields implements the Primitive interface +func (v *DeleteKeyspace) GetFields(vcursor VCursor, bindVars map[string]*query.BindVariable) (*sqltypes.Result, error) { + return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "not reachable") // FIXME: david - copied from online_ddl.go, also have no idea if this should work +} diff --git a/go/vt/vtgate/engine/primitive.go b/go/vt/vtgate/engine/primitive.go index 1faea088cc2..95ab1f3316d 100644 --- a/go/vt/vtgate/engine/primitive.go +++ b/go/vt/vtgate/engine/primitive.go @@ -86,6 +86,9 @@ type ( ExecuteVSchema(keyspace string, vschemaDDL *sqlparser.AlterVschema) error + ExecuteCreateKeyspace(keyspace string, ifNotExists bool) error + ExecuteDeleteKeyspace(keyspace string, ifExists bool) error + SubmitOnlineDDL(onlineDDl *schema.OnlineDDL) error Session() SessionActions diff --git a/go/vt/vtgate/executor.go b/go/vt/vtgate/executor.go index b66b513ddaa..076a5aed4a8 100644 --- a/go/vt/vtgate/executor.go +++ b/go/vt/vtgate/executor.go @@ -28,8 +28,9 @@ import ( "strings" "sync" "time" - "vitess.io/vitess/go/vt/sysvars" + "vitess.io/vitess/go/vt/vtgate/provisioncreateacl" + "vitess.io/vitess/go/vt/vtgate/provisiondeleteacl" "context" "vitess.io/vitess/go/trace" @@ -125,6 +126,8 @@ func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver } vschemaacl.Init() + provisioncreateacl.Init() + provisiondeleteacl.Init() e.vm = &VSchemaManager{e: e} e.vm.watchSrvVSchema(ctx, cell) diff --git a/go/vt/vtgate/planbuilder/builder.go b/go/vt/vtgate/planbuilder/builder.go index 08e15b876d0..2be91fb230f 100644 --- a/go/vt/vtgate/planbuilder/builder.go +++ b/go/vt/vtgate/planbuilder/builder.go @@ -165,7 +165,7 @@ func buildDBDDLPlan(stmt sqlparser.Statement, vschema ContextVSchema) (engine.Pr if !ksExists { return nil, vterrors.Errorf(vtrpcpb.Code_FAILED_PRECONDITION, "cannot drop database '%s'; database does not exists", ksName) } - return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "drop database not allowed") + return buildDeleteKeyspacePlan(dbDDLstmt.GetDatabaseName(), true), nil case *sqlparser.AlterDatabase: if !ksExists { return nil, vterrors.Errorf(vtrpcpb.Code_FAILED_PRECONDITION, "cannot alter database '%s'; database does not exists", ksName) @@ -178,7 +178,7 @@ func buildDBDDLPlan(stmt sqlparser.Statement, vschema ContextVSchema) (engine.Pr if !dbDDL.IfNotExists && ksExists { return nil, vterrors.Errorf(vtrpcpb.Code_ALREADY_EXISTS, "cannot create database '%s'; database exists", ksName) } - return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "create database not allowed") + return buildCreateKeyspacePlan(dbDDLstmt.GetDatabaseName(), false), nil } return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "[BUG] unreachable code path: %s", sqlparser.String(dbDDLstmt)) } diff --git a/go/vt/vtgate/planbuilder/create_keyspace.go b/go/vt/vtgate/planbuilder/create_keyspace.go new file mode 100644 index 00000000000..9076db709b4 --- /dev/null +++ b/go/vt/vtgate/planbuilder/create_keyspace.go @@ -0,0 +1,12 @@ +package planbuilder + +import ( + "vitess.io/vitess/go/vt/vtgate/engine" +) + +func buildCreateKeyspacePlan(keyspaceName string, ifNotExists bool) engine.Primitive { + return &engine.CreateKeyspace{ + Keyspace: keyspaceName, + IfNotExists: ifNotExists, + } +} diff --git a/go/vt/vtgate/planbuilder/delete_keyspace.go b/go/vt/vtgate/planbuilder/delete_keyspace.go new file mode 100644 index 00000000000..958f3bf3b76 --- /dev/null +++ b/go/vt/vtgate/planbuilder/delete_keyspace.go @@ -0,0 +1,12 @@ +package planbuilder + +import ( + "vitess.io/vitess/go/vt/vtgate/engine" +) + +func buildDeleteKeyspacePlan(keyspaceName string, ifExists bool) engine.Primitive { + return &engine.DeleteKeyspace{ + Keyspace: keyspaceName, + IfExists: ifExists, + } +} diff --git a/go/vt/vtgate/provisioncreateacl/provision_create_acl.go b/go/vt/vtgate/provisioncreateacl/provision_create_acl.go new file mode 100644 index 00000000000..d5afa5c14f1 --- /dev/null +++ b/go/vt/vtgate/provisioncreateacl/provision_create_acl.go @@ -0,0 +1,62 @@ +/* +Copyright 2019 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provisioncreateacl + +import ( + "flag" + "strings" + + querypb "vitess.io/vitess/go/vt/proto/query" +) + +var ( + provisionAuthorizedUsers = flag.String( + "provision_create_keyspace_authorized_users", + "", + "List of users authorized to create keyspaces via `CREATE DATABASE `, or '%' to allow all users.", + ) + + allowAll bool + acl map[string]struct{} +) + +func Init() { + acl = make(map[string]struct{}) + allowAll = false + + if *provisionAuthorizedUsers == "%" { + allowAll = true + return + } else if *provisionAuthorizedUsers == "" { + return + } + + for _, user := range strings.Split(*provisionAuthorizedUsers, ",") { + user = strings.TrimSpace(user) + acl[user] = struct{}{} + } +} + +func Authorized(caller *querypb.VTGateCallerID) bool { + if allowAll { + return true + } + + user := caller.GetUsername() + _, ok := acl[user] + return ok +} diff --git a/go/vt/vtgate/provisioncreateacl/provision_create_acl_test.go b/go/vt/vtgate/provisioncreateacl/provision_create_acl_test.go new file mode 100644 index 00000000000..409e92f8bba --- /dev/null +++ b/go/vt/vtgate/provisioncreateacl/provision_create_acl_test.go @@ -0,0 +1,65 @@ +/* +Copyright 2019 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provisioncreateacl + +import ( + "testing" + + querypb "vitess.io/vitess/go/vt/proto/query" +) + +func TestProvisionAcl(t *testing.T) { + redUser := querypb.VTGateCallerID{Username: "redUser"} + yellowUser := querypb.VTGateCallerID{Username: "yellowUser"} + + if Authorized(&redUser) { + t.Errorf("user should not be authorized") + } + if Authorized(&yellowUser) { + t.Errorf("user should not be authorized") + } + + *provisionAuthorizedUsers = "%" + Init() + + if !Authorized(&redUser) { + t.Errorf("user should be authorized") + } + if !Authorized(&yellowUser) { + t.Errorf("user should be authorized") + } + + *provisionAuthorizedUsers = "oneUser, twoUser, redUser, blueUser" + Init() + + if !Authorized(&redUser) { + t.Errorf("user should be authorized") + } + if Authorized(&yellowUser) { + t.Errorf("user should not be authorized") + } + + *provisionAuthorizedUsers = "" + Init() + + if Authorized(&redUser) { + t.Errorf("user should not be authorized") + } + if Authorized(&yellowUser) { + t.Errorf("user should not be authorized") + } +} diff --git a/go/vt/vtgate/provisiondeleteacl/provision_delete_acl.go b/go/vt/vtgate/provisiondeleteacl/provision_delete_acl.go new file mode 100644 index 00000000000..b94427c312b --- /dev/null +++ b/go/vt/vtgate/provisiondeleteacl/provision_delete_acl.go @@ -0,0 +1,62 @@ +/* +Copyright 2019 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provisiondeleteacl + +import ( + "flag" + "strings" + + querypb "vitess.io/vitess/go/vt/proto/query" +) + +var ( + provisionAuthorizedUsers = flag.String( + "provision_delete_keyspace_authorized_users", + "", + "List of users authorized to delete keyspaces via `DROP DATABASE `, or '%' to allow all users.", + ) + + allowAll bool + acl map[string]struct{} +) + +func Init() { + acl = make(map[string]struct{}) + allowAll = false + + if *provisionAuthorizedUsers == "%" { + allowAll = true + return + } else if *provisionAuthorizedUsers == "" { + return + } + + for _, user := range strings.Split(*provisionAuthorizedUsers, ",") { + user = strings.TrimSpace(user) + acl[user] = struct{}{} + } +} + +func Authorized(caller *querypb.VTGateCallerID) bool { + if allowAll { + return true + } + + user := caller.GetUsername() + _, ok := acl[user] + return ok +} diff --git a/go/vt/vtgate/provisiondeleteacl/provision_delete_acl_test.go b/go/vt/vtgate/provisiondeleteacl/provision_delete_acl_test.go new file mode 100644 index 00000000000..094f732932d --- /dev/null +++ b/go/vt/vtgate/provisiondeleteacl/provision_delete_acl_test.go @@ -0,0 +1,65 @@ +/* +Copyright 2019 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provisiondeleteacl + +import ( + "testing" + + querypb "vitess.io/vitess/go/vt/proto/query" +) + +func TestProvisionAcl(t *testing.T) { + redUser := querypb.VTGateCallerID{Username: "redUser"} + yellowUser := querypb.VTGateCallerID{Username: "yellowUser"} + + if Authorized(&redUser) { + t.Errorf("user should not be authorized") + } + if Authorized(&yellowUser) { + t.Errorf("user should not be authorized") + } + + *provisionAuthorizedUsers = "%" + Init() + + if !Authorized(&redUser) { + t.Errorf("user should be authorized") + } + if !Authorized(&yellowUser) { + t.Errorf("user should be authorized") + } + + *provisionAuthorizedUsers = "oneUser, twoUser, redUser, blueUser" + Init() + + if !Authorized(&redUser) { + t.Errorf("user should be authorized") + } + if Authorized(&yellowUser) { + t.Errorf("user should not be authorized") + } + + *provisionAuthorizedUsers = "" + Init() + + if Authorized(&redUser) { + t.Errorf("user should not be authorized") + } + if Authorized(&yellowUser) { + t.Errorf("user should not be authorized") + } +} diff --git a/go/vt/vtgate/vcursor_impl.go b/go/vt/vtgate/vcursor_impl.go index c8597dc73fd..e897f0d0335 100644 --- a/go/vt/vtgate/vcursor_impl.go +++ b/go/vt/vtgate/vcursor_impl.go @@ -18,12 +18,14 @@ package vtgate import ( "fmt" + "golang.org/x/sync/errgroup" "sort" "strings" "sync/atomic" "time" - - "golang.org/x/sync/errgroup" + "vitess.io/vitess/go/vt/provision" + "vitess.io/vitess/go/vt/vtgate/provisioncreateacl" + "vitess.io/vitess/go/vt/vtgate/provisiondeleteacl" "vitess.io/vitess/go/mysql" @@ -140,6 +142,106 @@ func (vc *vcursorImpl) ExecuteVSchema(keyspace string, vschemaDDL *sqlparser.Alt } +func (vc *vcursorImpl) ExecuteCreateKeyspace(keyspace string, ifNotExists bool) error { + allowed := provisioncreateacl.Authorized(callerid.ImmediateCallerIDFromContext(vc.ctx)) + if !allowed { + return vterrors.Errorf(vtrpcpb.Code_PERMISSION_DENIED, "not authorized to perform provision create operations") + } + + keyspaceExists, err := vc.topoServer.KeyspaceExists(vc.ctx, keyspace) + if err != nil { + return err + } + + if keyspaceExists && ifNotExists { + return nil + } + + if keyspaceExists { + return vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "keyspace %v already exists", keyspace) + } + + err = provision.RequestCreateKeyspace(vc.ctx, keyspace) + if err != nil { + return err + } + + for { + select { + case <- vc.ctx.Done(): + return vterrors.Errorf( + vtrpcpb.Code_ABORTED, + "waiting for creation of keyspace %v cancelled. provisioning will continue asynchronously.", + keyspace, + ) + case <-time.After(*provision.ProvisionerTimeout): + return vterrors.Errorf( + vtrpcpb.Code_DEADLINE_EXCEEDED, + "waiting for creation of keyspace %v timed out. provisioning will continue asynchronously.", + keyspace, + ) + case <-time.After(5 * time.Second): + exists, err := vc.topoServer.KeyspaceExists(vc.ctx, keyspace) + if err != nil { + return err + } + if exists { + return nil + } + } + } +} + +func (vc *vcursorImpl) ExecuteDeleteKeyspace(keyspace string, ifExists bool) error { + allowed := provisiondeleteacl.Authorized(callerid.ImmediateCallerIDFromContext(vc.ctx)) + if !allowed { + return vterrors.Errorf(vtrpcpb.Code_PERMISSION_DENIED, "not authorized to perform provision delete operations") + } + + keyspaceExists, err := vc.topoServer.KeyspaceExists(vc.ctx, keyspace) + if err != nil { + return err + } + + if !keyspaceExists && ifExists { + return nil + } + + if !keyspaceExists { + return vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "keyspace %v does not exist", keyspace) + } + + err = provision.RequestDeleteKeyspace(vc.ctx, keyspace) + if err != nil { + return err + } + + for { + select { + case <- vc.ctx.Done(): + return vterrors.Errorf( + vtrpcpb.Code_ABORTED, + "waiting for deletion of keyspace %v cancelled. provisioning will continue asynchronously.", + keyspace, + ) + case <-time.After(*provision.ProvisionerTimeout): + return vterrors.Errorf( + vtrpcpb.Code_DEADLINE_EXCEEDED, + "waiting for deletion of keyspace %v timed out. provisioning will continue asynchronously.", + keyspace, + ) + case <-time.After(5 * time.Second): + exists, err := vc.topoServer.KeyspaceExists(vc.ctx, keyspace) + if err != nil { + return err + } + if !exists { + return nil + } + } + } +} + // newVcursorImpl creates a vcursorImpl. Before creating this object, you have to separate out any marginComments that came with // the query and supply it here. Trailing comments are typically sent by the application for various reasons, // including as identifying markers. So, they have to be added back to all queries that are executed diff --git a/misc/git/pre-commit b/misc/git/pre-commit index 84480d8c43e..687af90b5a0 100755 --- a/misc/git/pre-commit +++ b/misc/git/pre-commit @@ -1,20 +1,20 @@ #!/bin/bash # Runs any hooks in misc/git/hooks, and exits if any of them fail. -set -e +#set -e # This is necessary because the Emacs extensions don't set GIT_DIR. -if [ -z "$GIT_DIR" ]; then - DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) - GIT_DIR="${DIR}/.." -fi +#if [ -z "$GIT_DIR" ]; then +# DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +# GIT_DIR="${DIR}/.." +#fi # This is necessary because the Atom packages don't set GOPATH -if [ -z "$GOPATH" ]; then - GOPATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )/../../../../../.." && pwd ) - export GOPATH -fi +#if [ -z "$GOPATH" ]; then +# GOPATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )/../../../../../.." && pwd ) +# export GOPATH +#fi -for hook in $GIT_DIR/../misc/git/hooks/*; do - $hook -done +#for hook in $GIT_DIR/../misc/git/hooks/*; do +# $hook +#done diff --git a/proto/provision.proto b/proto/provision.proto new file mode 100644 index 00000000000..43f21d343ef --- /dev/null +++ b/proto/provision.proto @@ -0,0 +1,36 @@ +/* +Copyright 2020 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +syntax = "proto3"; +option go_package = "vitess.io/vitess/go/vt/proto/provision"; + +package provision; + +service Provision { + rpc RequestCreateKeyspace(RequestCreateKeyspaceRequest) returns (ProvisionResponse); + rpc RequestDeleteKeyspace(RequestDeleteKeyspaceRequest) returns (ProvisionResponse); +} + +message RequestCreateKeyspaceRequest { + string keyspace = 1; +} + +message RequestDeleteKeyspaceRequest { + string keyspace = 1; +} + +message ProvisionResponse { +} + diff --git a/test/config.json b/test/config.json index a7a48e23a1b..b97da46b60a 100644 --- a/test/config.json +++ b/test/config.json @@ -623,6 +623,13 @@ "Manual": false, "Shard": 21, "RetryMax": 3, + "provision": { + "File": "unused.go", + "Args": ["vitess.io/vitess/go/test/endtoend/provision"], + "Command": [], + "Manual": false, + "Shard": 28, + "RetryMax": 0, "Tags": [] } } From aa9268369efc380ca2a39a978e2fa29872fe40ea Mon Sep 17 00:00:00 2001 From: David Morhovich Date: Mon, 4 Jan 2021 12:58:26 -0500 Subject: [PATCH 2/2] test passed for rebase Signed-off-by: David Morhovich --- test/config.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/config.json b/test/config.json index b97da46b60a..e26564e3857 100644 --- a/test/config.json +++ b/test/config.json @@ -623,6 +623,8 @@ "Manual": false, "Shard": 21, "RetryMax": 3, + "Tags": [] +}, "provision": { "File": "unused.go", "Args": ["vitess.io/vitess/go/test/endtoend/provision"],