diff --git a/go.mod b/go.mod index 775a53e9..58954279 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/stretchr/testify v1.8.4 golang.org/x/sys v0.8.0 google.golang.org/grpc v1.55.0 + google.golang.org/protobuf v1.30.0 ) require ( @@ -24,6 +25,5 @@ require ( golang.org/x/text v0.8.0 // indirect golang.org/x/tools v0.6.0 // indirect google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect - google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/plugin/plugin.pb.go b/pkg/plugin/plugin.pb.go new file mode 100644 index 00000000..27dcb8ab --- /dev/null +++ b/pkg/plugin/plugin.pb.go @@ -0,0 +1,209 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.12.4 +// source: plugin.proto + +package plugin + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + 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 Empty struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Empty) Reset() { + *x = Empty{} + if protoimpl.UnsafeEnabled { + mi := &file_plugin_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Empty) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Empty) ProtoMessage() {} + +func (x *Empty) ProtoReflect() protoreflect.Message { + mi := &file_plugin_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 Empty.ProtoReflect.Descriptor instead. +func (*Empty) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{0} +} + +type ConfigsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Configs map[string]string `protobuf:"bytes,1,rep,name=configs,proto3" json:"configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *ConfigsRequest) Reset() { + *x = ConfigsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_plugin_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConfigsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigsRequest) ProtoMessage() {} + +func (x *ConfigsRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugin_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 ConfigsRequest.ProtoReflect.Descriptor instead. +func (*ConfigsRequest) Descriptor() ([]byte, []int) { + return file_plugin_proto_rawDescGZIP(), []int{1} +} + +func (x *ConfigsRequest) GetConfigs() map[string]string { + if x != nil { + return x.Configs + } + return nil +} + +var File_plugin_proto protoreflect.FileDescriptor + +var file_plugin_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0x8b, 0x01, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x3d, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x73, 0x1a, 0x3a, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, 0x46, 0x0a, + 0x0c, 0x53, 0x70, 0x69, 0x66, 0x66, 0x65, 0x48, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x12, 0x36, 0x0a, + 0x0b, 0x50, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x16, 0x2e, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x12, 0x5a, 0x10, 0x2e, 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x70, + 0x6b, 0x67, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_plugin_proto_rawDescOnce sync.Once + file_plugin_proto_rawDescData = file_plugin_proto_rawDesc +) + +func file_plugin_proto_rawDescGZIP() []byte { + file_plugin_proto_rawDescOnce.Do(func() { + file_plugin_proto_rawDescData = protoimpl.X.CompressGZIP(file_plugin_proto_rawDescData) + }) + return file_plugin_proto_rawDescData +} + +var file_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_plugin_proto_goTypes = []interface{}{ + (*Empty)(nil), // 0: plugin.Empty + (*ConfigsRequest)(nil), // 1: plugin.ConfigsRequest + nil, // 2: plugin.ConfigsRequest.ConfigsEntry +} +var file_plugin_proto_depIdxs = []int32{ + 2, // 0: plugin.ConfigsRequest.configs:type_name -> plugin.ConfigsRequest.ConfigsEntry + 1, // 1: plugin.SpiffeHelper.PostConfigs:input_type -> plugin.ConfigsRequest + 0, // 2: plugin.SpiffeHelper.PostConfigs:output_type -> plugin.Empty + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_plugin_proto_init() } +func file_plugin_proto_init() { + if File_plugin_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_plugin_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Empty); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_plugin_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConfigsRequest); 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_plugin_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_plugin_proto_goTypes, + DependencyIndexes: file_plugin_proto_depIdxs, + MessageInfos: file_plugin_proto_msgTypes, + }.Build() + File_plugin_proto = out.File + file_plugin_proto_rawDesc = nil + file_plugin_proto_goTypes = nil + file_plugin_proto_depIdxs = nil +} diff --git a/pkg/plugin/plugin_grpc.pb.go b/pkg/plugin/plugin_grpc.pb.go new file mode 100644 index 00000000..c37f64c9 --- /dev/null +++ b/pkg/plugin/plugin_grpc.pb.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v3.12.4 +// source: plugin.proto + +package plugin + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// 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 + +const ( + SpiffeHelper_PostConfigs_FullMethodName = "/plugin.SpiffeHelper/PostConfigs" +) + +// SpiffeHelperClient is the client API for SpiffeHelper 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 SpiffeHelperClient interface { + PostConfigs(ctx context.Context, in *ConfigsRequest, opts ...grpc.CallOption) (*Empty, error) +} + +type spiffeHelperClient struct { + cc grpc.ClientConnInterface +} + +func NewSpiffeHelperClient(cc grpc.ClientConnInterface) SpiffeHelperClient { + return &spiffeHelperClient{cc} +} + +func (c *spiffeHelperClient) PostConfigs(ctx context.Context, in *ConfigsRequest, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, SpiffeHelper_PostConfigs_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SpiffeHelperServer is the server API for SpiffeHelper service. +// All implementations must embed UnimplementedSpiffeHelperServer +// for forward compatibility +type SpiffeHelperServer interface { + PostConfigs(context.Context, *ConfigsRequest) (*Empty, error) + mustEmbedUnimplementedSpiffeHelperServer() +} + +// UnimplementedSpiffeHelperServer must be embedded to have forward compatible implementations. +type UnimplementedSpiffeHelperServer struct { +} + +func (UnimplementedSpiffeHelperServer) PostConfigs(context.Context, *ConfigsRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method PostConfigs not implemented") +} +func (UnimplementedSpiffeHelperServer) mustEmbedUnimplementedSpiffeHelperServer() {} + +// UnsafeSpiffeHelperServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SpiffeHelperServer will +// result in compilation errors. +type UnsafeSpiffeHelperServer interface { + mustEmbedUnimplementedSpiffeHelperServer() +} + +func RegisterSpiffeHelperServer(s grpc.ServiceRegistrar, srv SpiffeHelperServer) { + s.RegisterService(&SpiffeHelper_ServiceDesc, srv) +} + +func _SpiffeHelper_PostConfigs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ConfigsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SpiffeHelperServer).PostConfigs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SpiffeHelper_PostConfigs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SpiffeHelperServer).PostConfigs(ctx, req.(*ConfigsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SpiffeHelper_ServiceDesc is the grpc.ServiceDesc for SpiffeHelper service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SpiffeHelper_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "plugin.SpiffeHelper", + HandlerType: (*SpiffeHelperServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "PostConfigs", + Handler: _SpiffeHelper_PostConfigs_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "plugin.proto", +} diff --git a/pkg/plugin/simple-example/simple-example.go b/pkg/plugin/simple-example/simple-example.go new file mode 100644 index 00000000..05e375aa --- /dev/null +++ b/pkg/plugin/simple-example/simple-example.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "fmt" + "log" + "net" + + pb "github.com/spiffe/spiffe-helper/pkg/plugin" + "google.golang.org/grpc" +) + +type simpleExampleServer struct { + pb.SpiffeHelperServer +} + +func (s *simpleExampleServer) PostConfigs(ctx context.Context, request *pb.ConfigsRequest) (*pb.Empty, error) { + configs := request.Configs + + fmt.Printf("From: %s\n", configs["from"]) + fmt.Printf("To: %s\n", configs["to"]) + fmt.Printf("Message: %s\n", configs["message"]) + + return new(pb.Empty), nil +} + +func main() { + lis, err := net.Listen("tcp", "localhost:8081") + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + grpcServer := grpc.NewServer() + simpleExampleServer := &simpleExampleServer{} + pb.RegisterSpiffeHelperServer(grpcServer, simpleExampleServer) + log.Printf("server listening at %v", lis.Addr()) + + if err := grpcServer.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/pkg/sidecar/config.go b/pkg/sidecar/config.go index 5444e48f..844c0c73 100644 --- a/pkg/sidecar/config.go +++ b/pkg/sidecar/config.go @@ -8,6 +8,17 @@ import ( "github.com/spiffe/go-spiffe/v2/logger" ) +type Plugin struct { + Name string `hcl:"name,label"` + Hostname string `hcl:"hostname"` + Port string `hcl:"port"` + Body map[string]string `hcl:"body,remain"` +} + +type Plugins struct { + Plugin []Plugin `hcl:"plugin,block"` +} + // Config contains config variables when creating a SPIFFE Sidecar. type Config struct { AgentAddress string `hcl:"agentAddress"` @@ -22,28 +33,28 @@ type Config struct { SvidKeyFileName string `hcl:"svidKeyFileName"` SvidBundleFileName string `hcl:"svidBundleFileName"` RenewSignal string `hcl:"renewSignal"` + + Plugins map[string]interface{} `hcl:"plugins,block"` // TODO: is there a reason for this to be exposed? and inside of config? ReloadExternalProcess func() error // TODO: is there a reason for this to be exposed? and inside of config? Log logger.Logger } -// ParseConfig parses the given HCL file into a SidecarConfig struct -func ParseConfig(file string) (*Config, error) { - sidecarConfig := new(Config) - +func ParseConfig(fileName string) (*Config, error) { // Read HCL file - dat, err := os.ReadFile(file) + dat, err := os.ReadFile(fileName) if err != nil { return nil, err } // Parse HCL - if err := hcl.Decode(sidecarConfig, string(dat)); err != nil { + config := new(Config) + if err := hcl.Decode(config, string(dat)); err != nil { return nil, err } - return sidecarConfig, nil + return config, nil } func ValidateConfig(c *Config) error { diff --git a/pkg/sidecar/sidecar.go b/pkg/sidecar/sidecar.go index 8e245a2e..70776842 100644 --- a/pkg/sidecar/sidecar.go +++ b/pkg/sidecar/sidecar.go @@ -1,6 +1,7 @@ package sidecar import ( + "context" "crypto/x509" "encoding/csv" "encoding/pem" @@ -8,12 +9,16 @@ import ( "os" "os/exec" "path" + "strconv" "strings" "sync/atomic" "github.com/spiffe/go-spiffe/v2/logger" "github.com/spiffe/go-spiffe/v2/workloadapi" + pb "github.com/spiffe/spiffe-helper/pkg/plugin" + "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" ) @@ -78,6 +83,9 @@ func (s *Sidecar) updateCertificates(svidResponse *workloadapi.X509Context) { s.config.Log.Errorf("Unable to signal process: %v", err) } + s.config.Log.Infof("Updating plugins") + s.updatePlugins() + select { case s.certReadyChan <- struct{}{}: default: @@ -121,6 +129,47 @@ func (s *Sidecar) signalProcess() (err error) { return nil } +func (s *Sidecar) updatePlugins() { + for pluginName, pluginConfig := range s.config.Plugins { + // create request + request := &pb.ConfigsRequest{} + request.Configs = make(map[string]string) + for k, v := range pluginConfig.([]map[string]interface{})[0] { + request.Configs[k] = v.(string) + } + + request.Configs["certDir"] = s.config.CertDir + request.Configs["addIntermediatesToBundle"] = strconv.FormatBool(s.config.AddIntermediatesToBundle) + request.Configs["svidFileName"] = s.config.SvidFileName + request.Configs["svidKeyFileName"] = s.config.SvidKeyFileName + request.Configs["svidBundleFileName"] = s.config.SvidBundleFileName + + // try to post request + config := pluginConfig.([]map[string]interface{})[0] + hostname, hostnameExists := config["hostname"] + port, portExists := config["port"] + if !hostnameExists || hostname == "" || !portExists || port == "" { + fmt.Printf("Please provide hostname and port for plugin %s", pluginName) + continue + } + + conn, err := grpc.Dial(config["hostname"].(string)+":"+config["port"].(string), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + s.config.Log.Errorf("Failed to connect with plugin %s", pluginName) + continue + } + + client := pb.NewSpiffeHelperClient(conn) + response, err := client.PostConfigs(context.Background(), request) + if err != nil { + s.config.Log.Errorf("Failed to post configs for plugin %s", pluginName) + continue + } + + s.config.Log.Infof("Plugin %s updated: %s", pluginName, response) + } +} + func (s *Sidecar) checkProcessExit() { atomic.StoreInt32(&s.processRunning, 1) _, err := s.process.Wait() diff --git a/proto/plugin/plugin.proto b/proto/plugin/plugin.proto new file mode 100644 index 00000000..5334617f --- /dev/null +++ b/proto/plugin/plugin.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package plugin; +option go_package = "../../pkg/plugin"; + +message Empty {} + +message ConfigsRequest { + map configs = 1; +} + +service SpiffeHelper { + rpc PostConfigs(ConfigsRequest) returns (Empty) {}; +}