diff --git a/cmd/proxy-server/server/server.go b/cmd/proxy-server/server/server.go index 5e59336a..af0b0784 100644 --- a/cmd/proxy-server/server/server.go +++ b/cmd/proxy-server/server/server.go @@ -40,6 +40,7 @@ import ( _ "github.com/Snowflake-Labs/sansshell/services/localfile" _ "github.com/Snowflake-Labs/sansshell/services/packages" _ "github.com/Snowflake-Labs/sansshell/services/process" + ss "github.com/Snowflake-Labs/sansshell/services/sansshell/server" _ "github.com/Snowflake-Labs/sansshell/services/service" ) @@ -112,12 +113,18 @@ func Run(ctx context.Context, rs RunState, hooks ...rpcauth.RPCAuthzHook) { serverOpts := []grpc.ServerOption{ grpc.Creds(serverCreds), + // Even though the proxy RPC is streaming we have unary RPCs (logging, reflection) we + // also need to properly auth and log. + grpc.ChainUnaryInterceptor(telemetry.UnaryServerLogInterceptor(rs.Logger), authz.Authorize), grpc.ChainStreamInterceptor(telemetry.StreamServerLogInterceptor(rs.Logger), authz.AuthorizeStream), } g := grpc.NewServer(serverOpts...) server.Register(g) reflection.Register(g) + // Create a an instance of logging for the proxy server itself. + s := &ss.Server{} + s.Register(g) rs.Logger.Info("initialized proxy service", "credsource", rs.CredSource) rs.Logger.Info("serving..") diff --git a/cmd/sanssh/client/client.go b/cmd/sanssh/client/client.go index 6e2a1e14..defc5b45 100644 --- a/cmd/sanssh/client/client.go +++ b/cmd/sanssh/client/client.go @@ -38,6 +38,7 @@ import ( _ "github.com/Snowflake-Labs/sansshell/services/localfile/client" _ "github.com/Snowflake-Labs/sansshell/services/packages/client" _ "github.com/Snowflake-Labs/sansshell/services/process/client" + _ "github.com/Snowflake-Labs/sansshell/services/sansshell/client" _ "github.com/Snowflake-Labs/sansshell/services/service/client" "github.com/Snowflake-Labs/sansshell/services/util" ) diff --git a/cmd/sansshell-server/server/server.go b/cmd/sansshell-server/server/server.go index c58b781a..c905cd7a 100644 --- a/cmd/sansshell-server/server/server.go +++ b/cmd/sansshell-server/server/server.go @@ -35,6 +35,7 @@ import ( _ "github.com/Snowflake-Labs/sansshell/services/localfile/server" _ "github.com/Snowflake-Labs/sansshell/services/packages/server" _ "github.com/Snowflake-Labs/sansshell/services/process/server" + _ "github.com/Snowflake-Labs/sansshell/services/sansshell/server" _ "github.com/Snowflake-Labs/sansshell/services/service/server" ) diff --git a/proxy/proxy/proxy.go b/proxy/proxy/proxy.go index 78a70996..ddf9f31b 100644 --- a/proxy/proxy/proxy.go +++ b/proxy/proxy/proxy.go @@ -62,6 +62,13 @@ func (p *Conn) Direct() bool { return p.direct } +// Proxy will return the ClientConn which connects directly to the proxy rather +// than wrapped as proxy.Conn normally does. This allows callers to invoke direct RPCs +// against the proxy as needed (such as services/logging). +func (p *Conn) Proxy() *grpc.ClientConn { + return p.cc +} + // proxyStream provides all the context for send/receive in a grpc stream sense then translated to the streaming connection // we hold to the proxy. It also implements a fully functional grpc.ClientStream interface. type proxyStream struct { diff --git a/services/sansshell/client/client.go b/services/sansshell/client/client.go new file mode 100644 index 00000000..d84898e9 --- /dev/null +++ b/services/sansshell/client/client.go @@ -0,0 +1,214 @@ +/* Copyright (c) 2019 Snowflake Inc. All rights reserved. + + 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 client provides the client interface for 'Logging' +package client + +import ( + "context" + "flag" + "fmt" + "os" + "time" + + "github.com/google/subcommands" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/Snowflake-Labs/sansshell/client" + pb "github.com/Snowflake-Labs/sansshell/services/sansshell" + "github.com/Snowflake-Labs/sansshell/services/util" +) + +const subPackage = "sansshell" + +func init() { + subcommands.Register(&sansshellCmd{}, subPackage) +} + +func setup(f *flag.FlagSet) *subcommands.Commander { + c := client.SetupSubpackage(subPackage, f) + c.Register(&setVerbosityCmd{}, "") + c.Register(&getVerbosityCmd{}, "") + c.Register(&setProxyVerbosityCmd{}, "") + c.Register(&getProxyVerbosityCmd{}, "") + return c +} + +type sansshellCmd struct{} + +func (*sansshellCmd) Name() string { return subPackage } +func (p *sansshellCmd) Synopsis() string { + return client.GenerateSynopsis(setup(flag.NewFlagSet("", flag.ContinueOnError))) +} +func (p *sansshellCmd) Usage() string { + return client.GenerateUsage(subPackage, p.Synopsis()) +} +func (*sansshellCmd) SetFlags(f *flag.FlagSet) {} + +func (p *sansshellCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + c := setup(f) + return c.Execute(ctx, args...) +} + +type setVerbosityCmd struct { + level int +} + +func (*setVerbosityCmd) Name() string { return "set-verbosity" } +func (*setVerbosityCmd) Synopsis() string { return "Set the logging verbosity level." } +func (*setVerbosityCmd) Usage() string { + return `set-verbosity: + Sends an integer logging level and returns the previous integer logging level. +` +} + +func (s *setVerbosityCmd) SetFlags(f *flag.FlagSet) { + f.IntVar(&s.level, "verbosity", 0, "The logging verbosity level to set") +} + +func (s *setVerbosityCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + state := args[0].(*util.ExecuteState) + c := pb.NewLoggingClientProxy(state.Conn) + + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + resp, err := c.SetVerbosityOneMany(ctx, &pb.SetVerbosityRequest{Level: int32(s.level)}) + if err != nil { + // Emit this to every error file as it's not specific to a given target. + for _, e := range state.Err { + fmt.Fprintf(e, "Could not set logging: %v\n", err) + } + return subcommands.ExitFailure + } + + retCode := subcommands.ExitSuccess + for r := range resp { + if r.Error != nil { + fmt.Fprintf(state.Err[r.Index], "Setting logging verbosity for target %s (%d) returned error: %v\n", r.Target, r.Index, r.Error) + retCode = subcommands.ExitFailure + continue + } + fmt.Fprintf(state.Out[r.Index], "Target %s (%d) previous logging level %d\n", r.Target, r.Index, r.Resp.Level) + } + return retCode +} + +type getVerbosityCmd struct { +} + +func (*getVerbosityCmd) Name() string { return "get-verbosity" } +func (*getVerbosityCmd) Synopsis() string { return "Get logging level verbosity" } +func (*getVerbosityCmd) Usage() string { + return `get-verbosity: + Sends an empty request and expects to get back an integer level for the current logging verbosity. +` +} + +func (*getVerbosityCmd) SetFlags(f *flag.FlagSet) {} + +func (g *getVerbosityCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + state := args[0].(*util.ExecuteState) + c := pb.NewLoggingClientProxy(state.Conn) + + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + resp, err := c.GetVerbosityOneMany(ctx, &emptypb.Empty{}) + if err != nil { + // Emit this to every error file as it's not specific to a given target. + for _, e := range state.Err { + fmt.Fprintf(e, "Could not set logging: %v\n", err) + } + return subcommands.ExitFailure + } + + retCode := subcommands.ExitSuccess + for r := range resp { + if r.Error != nil { + fmt.Fprintf(state.Err[r.Index], "Getting logging verbosity for target %s (%d) returned error: %v\n", r.Target, r.Index, r.Error) + retCode = subcommands.ExitFailure + continue + } + fmt.Fprintf(state.Out[r.Index], "Target %s (%d) current logging level %d\n", r.Target, r.Index, r.Resp.Level) + } + return retCode +} + +type setProxyVerbosityCmd struct { + level int +} + +func (*setProxyVerbosityCmd) Name() string { return "set-proxy-verbosity" } +func (*setProxyVerbosityCmd) Synopsis() string { return "Set the proxy logging verbosity level." } +func (*setProxyVerbosityCmd) Usage() string { + return `set-proxy-verbosity: + Sends an integer logging level for the proxy server and returns the previous integer logging level. +` +} + +func (s *setProxyVerbosityCmd) SetFlags(f *flag.FlagSet) { + f.IntVar(&s.level, "verbosity", 0, "The logging verbosity level to set") +} + +func (s *setProxyVerbosityCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + state := args[0].(*util.ExecuteState) + if len(state.Out) > 1 { + fmt.Fprintf(os.Stderr, "can't call proxy logging with multiple targets") + } + // Get a real connection to the proxy + c := pb.NewLoggingClient(state.Conn.Proxy()) + + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + resp, err := c.SetVerbosity(ctx, &pb.SetVerbosityRequest{Level: int32(s.level)}) + if err != nil { + fmt.Fprintf(state.Err[0], "Could not set proxy logging: %v\n", err) + return subcommands.ExitFailure + } + fmt.Fprintf(state.Out[0], "Proxy previous logging level %d\n", resp.Level) + return subcommands.ExitSuccess +} + +type getProxyVerbosityCmd struct { +} + +func (*getProxyVerbosityCmd) Name() string { return "get-proxy-verbosity" } +func (*getProxyVerbosityCmd) Synopsis() string { return "Get the proxy logging level verbosity" } +func (*getProxyVerbosityCmd) Usage() string { + return `get-proxy-verbosity: + Sends an empty request and expects to get back an integer level for the current proxy logging verbosity. +` +} + +func (*getProxyVerbosityCmd) SetFlags(f *flag.FlagSet) {} + +func (g *getProxyVerbosityCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + state := args[0].(*util.ExecuteState) + if len(state.Out) > 1 { + fmt.Fprintf(os.Stderr, "can't call proxy logging with multiple targets") + } + // Get a real connection to the proxy + c := pb.NewLoggingClient(state.Conn.Proxy()) + + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + resp, err := c.GetVerbosity(ctx, &emptypb.Empty{}) + if err != nil { + fmt.Fprintf(state.Err[0], "Could not get proxy logging: %v\n", err) + return subcommands.ExitFailure + } + fmt.Fprintf(state.Out[0], "Proxy current logging level %d\n", resp.Level) + return subcommands.ExitSuccess +} diff --git a/services/sansshell/sansshell.go b/services/sansshell/sansshell.go new file mode 100644 index 00000000..5c84c20c --- /dev/null +++ b/services/sansshell/sansshell.go @@ -0,0 +1,24 @@ +/* Copyright (c) 2019 Snowflake Inc. All rights reserved. + + 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 sansshell defines the RPC interface for internal Sansshell operations. +// This differs from other sansshell services as it is only changing internal state and not +// otherwise interacting with the host OS. +package sansshell + +// To regenerate the proto headers if the .proto changes, just run go generate +// and this encodes the necessary magic: +//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=require_unimplemented_servers=false:. --go-grpc_opt=paths=source_relative --go-grpcproxy_out=. --go-grpcproxy_opt=paths=source_relative sansshell.proto diff --git a/services/sansshell/sansshell.pb.go b/services/sansshell/sansshell.pb.go new file mode 100644 index 00000000..56888fc0 --- /dev/null +++ b/services/sansshell/sansshell.pb.go @@ -0,0 +1,240 @@ +// Copyright (c) 2019 Snowflake Inc. All rights reserved. +// +//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. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: sansshell.proto + +package sansshell + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SetVerbosityRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Level int32 `protobuf:"varint,1,opt,name=Level,proto3" json:"Level,omitempty"` +} + +func (x *SetVerbosityRequest) Reset() { + *x = SetVerbosityRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_sansshell_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetVerbosityRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetVerbosityRequest) ProtoMessage() {} + +func (x *SetVerbosityRequest) ProtoReflect() protoreflect.Message { + mi := &file_sansshell_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetVerbosityRequest.ProtoReflect.Descriptor instead. +func (*SetVerbosityRequest) Descriptor() ([]byte, []int) { + return file_sansshell_proto_rawDescGZIP(), []int{0} +} + +func (x *SetVerbosityRequest) GetLevel() int32 { + if x != nil { + return x.Level + } + return 0 +} + +type VerbosityReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Level int32 `protobuf:"varint,1,opt,name=Level,proto3" json:"Level,omitempty"` +} + +func (x *VerbosityReply) Reset() { + *x = VerbosityReply{} + if protoimpl.UnsafeEnabled { + mi := &file_sansshell_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VerbosityReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerbosityReply) ProtoMessage() {} + +func (x *VerbosityReply) ProtoReflect() protoreflect.Message { + mi := &file_sansshell_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VerbosityReply.ProtoReflect.Descriptor instead. +func (*VerbosityReply) Descriptor() ([]byte, []int) { + return file_sansshell_proto_rawDescGZIP(), []int{1} +} + +func (x *VerbosityReply) GetLevel() int32 { + if x != nil { + return x.Level + } + return 0 +} + +var File_sansshell_proto protoreflect.FileDescriptor + +var file_sansshell_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x73, 0x61, 0x6e, 0x73, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x09, 0x53, 0x61, 0x6e, 0x73, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x1a, 0x1b, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, + 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2b, 0x0a, 0x13, 0x53, 0x65, 0x74, + 0x56, 0x65, 0x72, 0x62, 0x6f, 0x73, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x05, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x26, 0x0a, 0x0e, 0x56, 0x65, 0x72, 0x62, 0x6f, 0x73, + 0x69, 0x74, 0x79, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x32, 0x9b, + 0x01, 0x0a, 0x07, 0x4c, 0x6f, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x12, 0x4b, 0x0a, 0x0c, 0x53, 0x65, + 0x74, 0x56, 0x65, 0x72, 0x62, 0x6f, 0x73, 0x69, 0x74, 0x79, 0x12, 0x1e, 0x2e, 0x53, 0x61, 0x6e, + 0x73, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x2e, 0x53, 0x65, 0x74, 0x56, 0x65, 0x72, 0x62, 0x6f, 0x73, + 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x53, 0x61, 0x6e, + 0x73, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x2e, 0x56, 0x65, 0x72, 0x62, 0x6f, 0x73, 0x69, 0x74, 0x79, + 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x56, 0x65, + 0x72, 0x62, 0x6f, 0x73, 0x69, 0x74, 0x79, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x19, 0x2e, 0x53, 0x61, 0x6e, 0x73, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x2e, 0x56, 0x65, 0x72, 0x62, + 0x6f, 0x73, 0x69, 0x74, 0x79, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x2f, 0x5a, 0x2d, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x53, 0x6e, 0x6f, 0x77, 0x66, + 0x6c, 0x61, 0x6b, 0x65, 0x2d, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x73, 0x61, 0x6e, 0x73, 0x73, 0x68, + 0x65, 0x6c, 0x6c, 0x2f, 0x73, 0x61, 0x6e, 0x73, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_sansshell_proto_rawDescOnce sync.Once + file_sansshell_proto_rawDescData = file_sansshell_proto_rawDesc +) + +func file_sansshell_proto_rawDescGZIP() []byte { + file_sansshell_proto_rawDescOnce.Do(func() { + file_sansshell_proto_rawDescData = protoimpl.X.CompressGZIP(file_sansshell_proto_rawDescData) + }) + return file_sansshell_proto_rawDescData +} + +var file_sansshell_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_sansshell_proto_goTypes = []interface{}{ + (*SetVerbosityRequest)(nil), // 0: Sansshell.SetVerbosityRequest + (*VerbosityReply)(nil), // 1: Sansshell.VerbosityReply + (*emptypb.Empty)(nil), // 2: google.protobuf.Empty +} +var file_sansshell_proto_depIdxs = []int32{ + 0, // 0: Sansshell.Logging.SetVerbosity:input_type -> Sansshell.SetVerbosityRequest + 2, // 1: Sansshell.Logging.GetVerbosity:input_type -> google.protobuf.Empty + 1, // 2: Sansshell.Logging.SetVerbosity:output_type -> Sansshell.VerbosityReply + 1, // 3: Sansshell.Logging.GetVerbosity:output_type -> Sansshell.VerbosityReply + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_sansshell_proto_init() } +func file_sansshell_proto_init() { + if File_sansshell_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_sansshell_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetVerbosityRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sansshell_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VerbosityReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_sansshell_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sansshell_proto_goTypes, + DependencyIndexes: file_sansshell_proto_depIdxs, + MessageInfos: file_sansshell_proto_msgTypes, + }.Build() + File_sansshell_proto = out.File + file_sansshell_proto_rawDesc = nil + file_sansshell_proto_goTypes = nil + file_sansshell_proto_depIdxs = nil +} diff --git a/services/sansshell/sansshell.proto b/services/sansshell/sansshell.proto new file mode 100644 index 00000000..8b6561a9 --- /dev/null +++ b/services/sansshell/sansshell.proto @@ -0,0 +1,38 @@ +/* Copyright (c) 2019 Snowflake Inc. All rights reserved. + + 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"; + +package Sansshell; + +option go_package = "github.com/Snowflake-Labs/sansshell/sansshell"; + +import "google/protobuf/empty.proto"; + +service Logging { + // SetVerbosity will change the logging level of the stdr logger package. + // This can be called concurrently with no guarentees on ordering so the + // final level set is the last RPC processed. This will return the previous + // verbosity setting that was in effect before setting. + rpc SetVerbosity(SetVerbosityRequest) returns (VerbosityReply) {} + // GetVerbosity returns the latest verbosity level based on the most + // recently processed SetVerbosity RPC. + rpc GetVerbosity(google.protobuf.Empty) returns (VerbosityReply) {} +} + +message SetVerbosityRequest { int32 Level = 1; } + +message VerbosityReply { int32 Level = 1; } \ No newline at end of file diff --git a/services/sansshell/sansshell_grpc.pb.go b/services/sansshell/sansshell_grpc.pb.go new file mode 100644 index 00000000..7cbb72c8 --- /dev/null +++ b/services/sansshell/sansshell_grpc.pb.go @@ -0,0 +1,148 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package sansshell + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// LoggingClient is the client API for Logging service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type LoggingClient interface { + // SetVerbosity will change the logging level of the stdr logger package. + // This can be called concurrently with no guarentees on ordering so the + // final level set is the last RPC processed. This will return the previous + // verbosity setting that was in effect before setting. + SetVerbosity(ctx context.Context, in *SetVerbosityRequest, opts ...grpc.CallOption) (*VerbosityReply, error) + // GetVerbosity returns the latest verbosity level based on the most + // recently processed SetVerbosity RPC. + GetVerbosity(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VerbosityReply, error) +} + +type loggingClient struct { + cc grpc.ClientConnInterface +} + +func NewLoggingClient(cc grpc.ClientConnInterface) LoggingClient { + return &loggingClient{cc} +} + +func (c *loggingClient) SetVerbosity(ctx context.Context, in *SetVerbosityRequest, opts ...grpc.CallOption) (*VerbosityReply, error) { + out := new(VerbosityReply) + err := c.cc.Invoke(ctx, "/Sansshell.Logging/SetVerbosity", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *loggingClient) GetVerbosity(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VerbosityReply, error) { + out := new(VerbosityReply) + err := c.cc.Invoke(ctx, "/Sansshell.Logging/GetVerbosity", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// LoggingServer is the server API for Logging service. +// All implementations should embed UnimplementedLoggingServer +// for forward compatibility +type LoggingServer interface { + // SetVerbosity will change the logging level of the stdr logger package. + // This can be called concurrently with no guarentees on ordering so the + // final level set is the last RPC processed. This will return the previous + // verbosity setting that was in effect before setting. + SetVerbosity(context.Context, *SetVerbosityRequest) (*VerbosityReply, error) + // GetVerbosity returns the latest verbosity level based on the most + // recently processed SetVerbosity RPC. + GetVerbosity(context.Context, *emptypb.Empty) (*VerbosityReply, error) +} + +// UnimplementedLoggingServer should be embedded to have forward compatible implementations. +type UnimplementedLoggingServer struct { +} + +func (UnimplementedLoggingServer) SetVerbosity(context.Context, *SetVerbosityRequest) (*VerbosityReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetVerbosity not implemented") +} +func (UnimplementedLoggingServer) GetVerbosity(context.Context, *emptypb.Empty) (*VerbosityReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetVerbosity not implemented") +} + +// UnsafeLoggingServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to LoggingServer will +// result in compilation errors. +type UnsafeLoggingServer interface { + mustEmbedUnimplementedLoggingServer() +} + +func RegisterLoggingServer(s grpc.ServiceRegistrar, srv LoggingServer) { + s.RegisterService(&Logging_ServiceDesc, srv) +} + +func _Logging_SetVerbosity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetVerbosityRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LoggingServer).SetVerbosity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/Sansshell.Logging/SetVerbosity", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LoggingServer).SetVerbosity(ctx, req.(*SetVerbosityRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Logging_GetVerbosity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LoggingServer).GetVerbosity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/Sansshell.Logging/GetVerbosity", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LoggingServer).GetVerbosity(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// Logging_ServiceDesc is the grpc.ServiceDesc for Logging service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Logging_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Sansshell.Logging", + HandlerType: (*LoggingServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SetVerbosity", + Handler: _Logging_SetVerbosity_Handler, + }, + { + MethodName: "GetVerbosity", + Handler: _Logging_GetVerbosity_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sansshell.proto", +} diff --git a/services/sansshell/sansshell_grpcproxy.pb.go b/services/sansshell/sansshell_grpcproxy.pb.go new file mode 100644 index 00000000..d96b6453 --- /dev/null +++ b/services/sansshell/sansshell_grpcproxy.pb.go @@ -0,0 +1,169 @@ +// Auto generated code by protoc-gen-go-grpcproxy +// DO NOT EDIT + +// Adds OneMany versions of RPC methods for use by proxy clients + +package sansshell + +import ( + context "context" + proxy "github.com/Snowflake-Labs/sansshell/proxy/proxy" + grpc "google.golang.org/grpc" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +import ( + "fmt" +) + +// LoggingClientProxy is the superset of LoggingClient which additionally includes the OneMany proxy methods +type LoggingClientProxy interface { + LoggingClient + SetVerbosityOneMany(ctx context.Context, in *SetVerbosityRequest, opts ...grpc.CallOption) (<-chan *SetVerbosityManyResponse, error) + GetVerbosityOneMany(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (<-chan *GetVerbosityManyResponse, error) +} + +// Embed the original client inside of this so we get the other generated methods automatically. +type loggingClientProxy struct { + *loggingClient +} + +// NewLoggingClientProxy creates a LoggingClientProxy for use in proxied connections. +// NOTE: This takes a proxy.Conn instead of a generic ClientConnInterface as the methods here are only valid in proxy.Conn contexts. +func NewLoggingClientProxy(cc *proxy.Conn) LoggingClientProxy { + return &loggingClientProxy{NewLoggingClient(cc).(*loggingClient)} +} + +// SetVerbosityManyResponse encapsulates a proxy data packet. +// It includes the target, index, response and possible error returned. +type SetVerbosityManyResponse struct { + Target string + // As targets can be duplicated this is the index into the slice passed to proxy.Conn. + Index int + Resp *VerbosityReply + Error error +} + +// SetVerbosityOneMany provides the same API as SetVerbosity but sends the same request to N destinations at once. +// N can be a single destination. +// +// NOTE: The returned channel must be read until it closes in order to avoid leaking goroutines. +func (c *loggingClientProxy) SetVerbosityOneMany(ctx context.Context, in *SetVerbosityRequest, opts ...grpc.CallOption) (<-chan *SetVerbosityManyResponse, error) { + conn := c.cc.(*proxy.Conn) + ret := make(chan *SetVerbosityManyResponse) + // If this is a single case we can just use Invoke and marshal it onto the channel once and be done. + if len(conn.Targets) == 1 { + go func() { + out := &SetVerbosityManyResponse{ + Target: conn.Targets[0], + Index: 0, + Resp: &VerbosityReply{}, + } + err := conn.Invoke(ctx, "/Sansshell.Logging/SetVerbosity", in, out.Resp, opts...) + if err != nil { + out.Error = err + } + // Send and close. + ret <- out + close(ret) + }() + return ret, nil + } + manyRet, err := conn.InvokeOneMany(ctx, "/Sansshell.Logging/SetVerbosity", in, opts...) + if err != nil { + return nil, err + } + // A goroutine to retrive untyped responses and convert them to typed ones. + go func() { + for { + typedResp := &SetVerbosityManyResponse{ + Resp: &VerbosityReply{}, + } + + resp, ok := <-manyRet + if !ok { + // All done so we can shut down. + close(ret) + return + } + typedResp.Target = resp.Target + typedResp.Index = resp.Index + typedResp.Error = resp.Error + if resp.Error == nil { + if err := resp.Resp.UnmarshalTo(typedResp.Resp); err != nil { + typedResp.Error = fmt.Errorf("can't decode any response - %v. Original Error - %v", err, resp.Error) + } + } + ret <- typedResp + } + }() + + return ret, nil +} + +// GetVerbosityManyResponse encapsulates a proxy data packet. +// It includes the target, index, response and possible error returned. +type GetVerbosityManyResponse struct { + Target string + // As targets can be duplicated this is the index into the slice passed to proxy.Conn. + Index int + Resp *VerbosityReply + Error error +} + +// GetVerbosityOneMany provides the same API as GetVerbosity but sends the same request to N destinations at once. +// N can be a single destination. +// +// NOTE: The returned channel must be read until it closes in order to avoid leaking goroutines. +func (c *loggingClientProxy) GetVerbosityOneMany(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (<-chan *GetVerbosityManyResponse, error) { + conn := c.cc.(*proxy.Conn) + ret := make(chan *GetVerbosityManyResponse) + // If this is a single case we can just use Invoke and marshal it onto the channel once and be done. + if len(conn.Targets) == 1 { + go func() { + out := &GetVerbosityManyResponse{ + Target: conn.Targets[0], + Index: 0, + Resp: &VerbosityReply{}, + } + err := conn.Invoke(ctx, "/Sansshell.Logging/GetVerbosity", in, out.Resp, opts...) + if err != nil { + out.Error = err + } + // Send and close. + ret <- out + close(ret) + }() + return ret, nil + } + manyRet, err := conn.InvokeOneMany(ctx, "/Sansshell.Logging/GetVerbosity", in, opts...) + if err != nil { + return nil, err + } + // A goroutine to retrive untyped responses and convert them to typed ones. + go func() { + for { + typedResp := &GetVerbosityManyResponse{ + Resp: &VerbosityReply{}, + } + + resp, ok := <-manyRet + if !ok { + // All done so we can shut down. + close(ret) + return + } + typedResp.Target = resp.Target + typedResp.Index = resp.Index + typedResp.Error = resp.Error + if resp.Error == nil { + if err := resp.Resp.UnmarshalTo(typedResp.Resp); err != nil { + typedResp.Error = fmt.Errorf("can't decode any response - %v. Original Error - %v", err, resp.Error) + } + } + ret <- typedResp + } + }() + + return ret, nil +} diff --git a/services/sansshell/server/logging.go b/services/sansshell/server/logging.go new file mode 100644 index 00000000..350ced68 --- /dev/null +++ b/services/sansshell/server/logging.go @@ -0,0 +1,66 @@ +/* Copyright (c) 2019 Snowflake Inc. All rights reserved. + + 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 server implements the sansshell 'Logging' service. +package server + +import ( + "context" + "sync" + + "github.com/Snowflake-Labs/sansshell/services" + pb "github.com/Snowflake-Labs/sansshell/services/sansshell" + "github.com/go-logr/logr" + "github.com/go-logr/stdr" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" +) + +// Server is used to implement the gRPC Server +type Server struct { + mu sync.RWMutex + lastVal int32 +} + +// SetVerbosity sets the logging level and returns the last value before this was called. +func (s *Server) SetVerbosity(ctx context.Context, req *pb.SetVerbosityRequest) (*pb.VerbosityReply, error) { + s.mu.Lock() + defer s.mu.Unlock() + logger := logr.FromContextOrDiscard(ctx) + old := int32(stdr.SetVerbosity(int(req.Level))) + logger.Info("set-verbosity", "new level", req.Level, "old level", old) + reply := &pb.VerbosityReply{ + Level: old, + } + s.lastVal = req.Level + return reply, nil +} + +// GetVerbosity returns the last set value (or 0 if it's never been set). +func (s *Server) GetVerbosity(ctx context.Context, req *emptypb.Empty) (*pb.VerbosityReply, error) { + s.mu.RLock() + defer s.mu.RUnlock() + return &pb.VerbosityReply{Level: s.lastVal}, nil +} + +// Register is called to expose this handler to the gRPC server +func (s *Server) Register(gs *grpc.Server) { + pb.RegisterLoggingServer(gs, s) +} + +func init() { + services.RegisterSansShellService(&Server{}) +} diff --git a/services/sansshell/server/logging_test.go b/services/sansshell/server/logging_test.go new file mode 100644 index 00000000..67573b49 --- /dev/null +++ b/services/sansshell/server/logging_test.go @@ -0,0 +1,75 @@ +/* Copyright (c) 2019 Snowflake Inc. All rights reserved. + + 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 server + +import ( + "context" + "log" + "net" + "os" + "testing" + + pb "github.com/Snowflake-Labs/sansshell/services/sansshell" + "github.com/Snowflake-Labs/sansshell/testing/testutil" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/types/known/emptypb" +) + +var ( + bufSize = 1024 * 1024 + lis *bufconn.Listener + conn *grpc.ClientConn +) + +func bufDialer(context.Context, string) (net.Conn, error) { + return lis.Dial() +} + +func TestMain(m *testing.M) { + lis = bufconn.Listen(bufSize) + s := grpc.NewServer() + lfs := &Server{} + lfs.Register(s) + go func() { + if err := s.Serve(lis); err != nil { + log.Fatalf("Server exited with error: %v", err) + } + }() + defer s.GracefulStop() + + os.Exit(m.Run()) +} + +func TestGetSetVerbosity(t *testing.T) { + var err error + ctx := context.Background() + conn, err = grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithTransportCredentials(insecure.NewCredentials())) + testutil.FatalOnErr("Failed to dial bufnet", err, t) + t.Cleanup(func() { conn.Close() }) + + client := pb.NewLoggingClient(conn) + lvl := int32(42) + _, err = client.SetVerbosity(ctx, &pb.SetVerbosityRequest{Level: lvl}) + testutil.FatalOnErr("SetVerbosity", err, t) + resp, err := client.GetVerbosity(ctx, &emptypb.Empty{}) + testutil.FatalOnErr("GetVerbosity", err, t) + if got, want := resp.Level, lvl; got != want { + t.Fatalf("Didn't get expected value back from GetVerbosity. Got %d want %d", got, want) + } +} diff --git a/testing/integrate.sh b/testing/integrate.sh index ec754796..2d052977 100755 --- a/testing/integrate.sh +++ b/testing/integrate.sh @@ -50,6 +50,7 @@ function check_status { function shutdown { echo "Shutting down" + echo "Logs in ${LOGS}" if [ -n "${PROXY_PID}" ]; then kill -KILL ${PROXY_PID} fi @@ -409,6 +410,8 @@ for i in \ github.com/Snowflake-Labs/sansshell/services/packages/client \ github.com/Snowflake-Labs/sansshell/services/process \ github.com/Snowflake-Labs/sansshell/services/process/client \ + github.com/Snowflake-Labs/sansshell/services/sansshell \ + github.com/Snowflake-Labs/sansshell/services/sansshell/client \ github.com/Snowflake-Labs/sansshell/services/service \ github.com/Snowflake-Labs/sansshell/services/service/client \ github.com/Snowflake-Labs/sansshell/testing/testutil; do @@ -472,13 +475,13 @@ check_status $? /dev/null policy check failed for server echo echo "Starting servers. Logs in ${LOGS}" -./bin/proxy-server -v=1 --justification --root-ca=./auth/mtls/testdata/root.pem --server-cert=./auth/mtls/testdata/leaf.pem --server-key=./auth/mtls/testdata/leaf.key --client-cert=./auth/mtls/testdata/client.pem --client-key=./auth/mtls/testdata/client.key --policy-file=${LOGS}/policy --hostport=localhost:50043 >& ${LOGS}/proxy.log & +./bin/proxy-server --justification --root-ca=./auth/mtls/testdata/root.pem --server-cert=./auth/mtls/testdata/leaf.pem --server-key=./auth/mtls/testdata/leaf.key --client-cert=./auth/mtls/testdata/client.pem --client-key=./auth/mtls/testdata/client.key --policy-file=${LOGS}/policy --hostport=localhost:50043 >& ${LOGS}/proxy.log & PROXY_PID=$! # Since we're controlling lifetime the shell can ignore this (avoids useless termination messages). disown %% # The server needs to be root in order for package installation tests (and the nodes run this as root). -sudo --preserve-env=AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY -b ./bin/sansshell-server -v=1 --justification --root-ca=./auth/mtls/testdata/root.pem --server-cert=./auth/mtls/testdata/leaf.pem --server-key=./auth/mtls/testdata/leaf.key --policy-file=${LOGS}/policy --hostport=localhost:50042 >& ${LOGS}/server.log +sudo --preserve-env=AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY -b ./bin/sansshell-server --justification --root-ca=./auth/mtls/testdata/root.pem --server-cert=./auth/mtls/testdata/leaf.pem --server-key=./auth/mtls/testdata/leaf.key --policy-file=${LOGS}/policy --hostport=localhost:50042 >& ${LOGS}/server.log # Skip if on github if [ -z "${ON_GITHUB}" ]; then @@ -537,6 +540,21 @@ if [ $? != 1 ]; then check_status 1 /dev/null missing justification failed fi +# Now set logging to v=1 and validate we saw that in the logs +echo "Setting logging level higher" +${SANSSH_PROXY} ${MULTI_TARGETS} sansshell set-verbosity --verbosity=1 +egrep -q -e '"msg"="set-verbosity".*"new level"=1 "old level"=0' ${LOGS}/server.log +check_status $? /dev/null cant find log entry for changing levels + +echo "Setting proxy logging level higher" +${SANSSH_PROXY} sansshell set-proxy-verbosity --verbosity=1 +egrep -q -e '"msg"="set-verbosity".*"new level"=1 "old level"=0' ${LOGS}/proxy.log +check_status $? /dev/null cant find log entry in proxy for changing levels + +${SANSSH_PROXY} sansshell get-proxy-verbosity +check_status $? /dev/null cant get proxy verbosity + +run_a_test false 1 sansshell get-verbosity run_a_test false 50 ansible playbook --playbook=$PWD/services/ansible/server/testdata/test.yml --vars=path=/tmp,path2=/ @@ -815,5 +833,5 @@ fi # TODO(jchacon): Provide a java binary for test{s echo -echo "All tests pass. Logs in ${LOGS}" +echo "All tests pass." echo