From 0b86bfc23a4383046defc26dbce4d01a18568173 Mon Sep 17 00:00:00 2001 From: Itay Donanhirsh Date: Mon, 29 Jul 2024 18:33:16 -0700 Subject: [PATCH] wip --- deploysentry/README.md | 0 deploysentry/autokitteh.yaml | 25 + deploysentry/go/Makefile | 8 + deploysentry/go/api.proto | 33 ++ deploysentry/go/buf.gen.yaml | 12 + deploysentry/go/buf.yaml | 7 + deploysentry/go/gen/api.pb.go | 514 ++++++++++++++++++ .../go/gen/apiv1connect/api.connect.go | 161 ++++++ deploysentry/go/go.mod | 12 + deploysentry/go/go.sum | 12 + deploysentry/go/handler.go | 50 ++ deploysentry/go/main.go | 61 +++ deploysentry/main.star | 64 +++ 13 files changed, 959 insertions(+) create mode 100644 deploysentry/README.md create mode 100644 deploysentry/autokitteh.yaml create mode 100644 deploysentry/go/Makefile create mode 100644 deploysentry/go/api.proto create mode 100644 deploysentry/go/buf.gen.yaml create mode 100644 deploysentry/go/buf.yaml create mode 100644 deploysentry/go/gen/api.pb.go create mode 100644 deploysentry/go/gen/apiv1connect/api.connect.go create mode 100644 deploysentry/go/go.mod create mode 100644 deploysentry/go/go.sum create mode 100644 deploysentry/go/handler.go create mode 100644 deploysentry/go/main.go create mode 100644 deploysentry/main.star diff --git a/deploysentry/README.md b/deploysentry/README.md new file mode 100644 index 00000000..e69de29b diff --git a/deploysentry/autokitteh.yaml b/deploysentry/autokitteh.yaml new file mode 100644 index 00000000..8648d7b9 --- /dev/null +++ b/deploysentry/autokitteh.yaml @@ -0,0 +1,25 @@ +version: v1 + +project: + name: deploysentry + vars: + - name: DEPLOYSENTRY_ADDR + value: "localhost:8080" + - name: INITIAL_RATIO + value: "10" + - name: RATIO_INCREMENT + value: "10" + - name: STEP_DURATION + value: "2s" + connections: + - name: grpc + integration: "grpc" + - name: slack + integration: "slack" + - name: "http" + integration: "http" + triggers: + - name: slack_app_mention + connection: slack + event_type: app_mention + entrypoint: main.star:on_slack_app_mention diff --git a/deploysentry/go/Makefile b/deploysentry/go/Makefile new file mode 100644 index 00000000..6c44db72 --- /dev/null +++ b/deploysentry/go/Makefile @@ -0,0 +1,8 @@ +.PHONY: srv +srv: gen + go build . + +.PHONY: gen +gen: + rm -fR gen + buf generate diff --git a/deploysentry/go/api.proto b/deploysentry/go/api.proto new file mode 100644 index 00000000..f4486cc9 --- /dev/null +++ b/deploysentry/go/api.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package autokitteh.deploysentry.api.v1; + +message DeployRequest { + string svc = 1; + string version = 2; +} + +message DeployResponse {} + +message SetRatioRequest { + string svc = 1; + string version = 2; + int32 ratio = 3; // 0..100 +} + +message SetRatioResponse {} + +message GetRequest { + string svc = 1; +} + +message GetResponse { + string version = 1; + int32 ratio = 2; +} + +service DeploySentryService { + rpc Deploy(DeployRequest) returns (DeployResponse); + rpc SetRatio(SetRatioRequest) returns (SetRatioResponse); + rpc Get(GetRequest) returns (GetResponse); +} diff --git a/deploysentry/go/buf.gen.yaml b/deploysentry/go/buf.gen.yaml new file mode 100644 index 00000000..38705608 --- /dev/null +++ b/deploysentry/go/buf.gen.yaml @@ -0,0 +1,12 @@ +version: v1 +managed: + enabled: true + go_package_prefix: + default: github.com/autokitteh/kittehub/deploysentry/go/gen +plugins: + - plugin: buf.build/protocolbuffers/go:v1.31.0 + out: gen + opt: paths=source_relative + - plugin: buf.build/connectrpc/go:v1.11.0 + out: gen + opt: paths=source_relative diff --git a/deploysentry/go/buf.yaml b/deploysentry/go/buf.yaml new file mode 100644 index 00000000..1a519456 --- /dev/null +++ b/deploysentry/go/buf.yaml @@ -0,0 +1,7 @@ +version: v1 +breaking: + use: + - FILE +lint: + use: + - DEFAULT diff --git a/deploysentry/go/gen/api.pb.go b/deploysentry/go/gen/api.pb.go new file mode 100644 index 00000000..180f159a --- /dev/null +++ b/deploysentry/go/gen/api.pb.go @@ -0,0 +1,514 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: api.proto + +package apiv1 + +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 DeployRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Svc string `protobuf:"bytes,1,opt,name=svc,proto3" json:"svc,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` +} + +func (x *DeployRequest) Reset() { + *x = DeployRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeployRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeployRequest) ProtoMessage() {} + +func (x *DeployRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_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 DeployRequest.ProtoReflect.Descriptor instead. +func (*DeployRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{0} +} + +func (x *DeployRequest) GetSvc() string { + if x != nil { + return x.Svc + } + return "" +} + +func (x *DeployRequest) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +type DeployResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *DeployResponse) Reset() { + *x = DeployResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeployResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeployResponse) ProtoMessage() {} + +func (x *DeployResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_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 DeployResponse.ProtoReflect.Descriptor instead. +func (*DeployResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{1} +} + +type SetRatioRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Svc string `protobuf:"bytes,1,opt,name=svc,proto3" json:"svc,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + Ratio int32 `protobuf:"varint,3,opt,name=ratio,proto3" json:"ratio,omitempty"` // 0..100 +} + +func (x *SetRatioRequest) Reset() { + *x = SetRatioRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetRatioRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetRatioRequest) ProtoMessage() {} + +func (x *SetRatioRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[2] + 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 SetRatioRequest.ProtoReflect.Descriptor instead. +func (*SetRatioRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{2} +} + +func (x *SetRatioRequest) GetSvc() string { + if x != nil { + return x.Svc + } + return "" +} + +func (x *SetRatioRequest) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *SetRatioRequest) GetRatio() int32 { + if x != nil { + return x.Ratio + } + return 0 +} + +type SetRatioResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SetRatioResponse) Reset() { + *x = SetRatioResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetRatioResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetRatioResponse) ProtoMessage() {} + +func (x *SetRatioResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[3] + 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 SetRatioResponse.ProtoReflect.Descriptor instead. +func (*SetRatioResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{3} +} + +type GetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Svc string `protobuf:"bytes,1,opt,name=svc,proto3" json:"svc,omitempty"` +} + +func (x *GetRequest) Reset() { + *x = GetRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRequest) ProtoMessage() {} + +func (x *GetRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[4] + 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 GetRequest.ProtoReflect.Descriptor instead. +func (*GetRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{4} +} + +func (x *GetRequest) GetSvc() string { + if x != nil { + return x.Svc + } + return "" +} + +type GetResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Ratio int32 `protobuf:"varint,2,opt,name=ratio,proto3" json:"ratio,omitempty"` +} + +func (x *GetResponse) Reset() { + *x = GetResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResponse) ProtoMessage() {} + +func (x *GetResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[5] + 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 GetResponse.ProtoReflect.Descriptor instead. +func (*GetResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{5} +} + +func (x *GetResponse) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *GetResponse) GetRatio() int32 { + if x != nil { + return x.Ratio + } + return 0 +} + +var File_api_proto protoreflect.FileDescriptor + +var file_api_proto_rawDesc = []byte{ + 0x0a, 0x09, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1e, 0x61, 0x75, 0x74, + 0x6f, 0x6b, 0x69, 0x74, 0x74, 0x65, 0x68, 0x2e, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x22, 0x3b, 0x0a, 0x0d, 0x44, + 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, + 0x73, 0x76, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x76, 0x63, 0x12, 0x18, + 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x10, 0x0a, 0x0e, 0x44, 0x65, 0x70, 0x6c, + 0x6f, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x53, 0x0a, 0x0f, 0x53, 0x65, + 0x74, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, + 0x03, 0x73, 0x76, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x76, 0x63, 0x12, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x22, + 0x12, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x76, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x73, 0x76, 0x63, 0x22, 0x3d, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x32, 0xcd, 0x02, 0x0a, 0x13, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x53, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x67, 0x0a, 0x06, 0x44, 0x65, + 0x70, 0x6c, 0x6f, 0x79, 0x12, 0x2d, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x69, 0x74, 0x74, 0x65, + 0x68, 0x2e, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x69, 0x74, 0x74, 0x65, 0x68, + 0x2e, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x08, 0x53, 0x65, 0x74, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x12, + 0x2f, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x69, 0x74, 0x74, 0x65, 0x68, 0x2e, 0x64, 0x65, 0x70, + 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x65, 0x74, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x30, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x69, 0x74, 0x74, 0x65, 0x68, 0x2e, 0x64, 0x65, + 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x31, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x2a, 0x2e, 0x61, 0x75, 0x74, 0x6f, + 0x6b, 0x69, 0x74, 0x74, 0x65, 0x68, 0x2e, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, + 0x74, 0x72, 0x79, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x69, 0x74, 0x74, + 0x65, 0x68, 0x2e, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0x83, 0x02, 0x0a, 0x22, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, + 0x69, 0x74, 0x74, 0x65, 0x68, 0x2e, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x42, 0x08, 0x41, 0x70, 0x69, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x69, 0x74, 0x74, 0x65, 0x68, 0x2f, 0x6b, 0x69, 0x74, + 0x74, 0x65, 0x68, 0x75, 0x62, 0x2f, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2f, 0x67, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x31, 0xa2, + 0x02, 0x03, 0x41, 0x44, 0x41, 0xaa, 0x02, 0x1e, 0x41, 0x75, 0x74, 0x6f, 0x6b, 0x69, 0x74, 0x74, + 0x65, 0x68, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, + 0x41, 0x70, 0x69, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x1e, 0x41, 0x75, 0x74, 0x6f, 0x6b, 0x69, 0x74, + 0x74, 0x65, 0x68, 0x5c, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, 0x74, 0x72, 0x79, + 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x2a, 0x41, 0x75, 0x74, 0x6f, 0x6b, 0x69, + 0x74, 0x74, 0x65, 0x68, 0x5c, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, 0x74, 0x72, + 0x79, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x21, 0x41, 0x75, 0x74, 0x6f, 0x6b, 0x69, 0x74, 0x74, 0x65, + 0x68, 0x3a, 0x3a, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x3a, + 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_api_proto_rawDescOnce sync.Once + file_api_proto_rawDescData = file_api_proto_rawDesc +) + +func file_api_proto_rawDescGZIP() []byte { + file_api_proto_rawDescOnce.Do(func() { + file_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_proto_rawDescData) + }) + return file_api_proto_rawDescData +} + +var file_api_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_api_proto_goTypes = []interface{}{ + (*DeployRequest)(nil), // 0: autokitteh.deploysentry.api.v1.DeployRequest + (*DeployResponse)(nil), // 1: autokitteh.deploysentry.api.v1.DeployResponse + (*SetRatioRequest)(nil), // 2: autokitteh.deploysentry.api.v1.SetRatioRequest + (*SetRatioResponse)(nil), // 3: autokitteh.deploysentry.api.v1.SetRatioResponse + (*GetRequest)(nil), // 4: autokitteh.deploysentry.api.v1.GetRequest + (*GetResponse)(nil), // 5: autokitteh.deploysentry.api.v1.GetResponse +} +var file_api_proto_depIdxs = []int32{ + 0, // 0: autokitteh.deploysentry.api.v1.DeploySentryService.Deploy:input_type -> autokitteh.deploysentry.api.v1.DeployRequest + 2, // 1: autokitteh.deploysentry.api.v1.DeploySentryService.SetRatio:input_type -> autokitteh.deploysentry.api.v1.SetRatioRequest + 4, // 2: autokitteh.deploysentry.api.v1.DeploySentryService.Get:input_type -> autokitteh.deploysentry.api.v1.GetRequest + 1, // 3: autokitteh.deploysentry.api.v1.DeploySentryService.Deploy:output_type -> autokitteh.deploysentry.api.v1.DeployResponse + 3, // 4: autokitteh.deploysentry.api.v1.DeploySentryService.SetRatio:output_type -> autokitteh.deploysentry.api.v1.SetRatioResponse + 5, // 5: autokitteh.deploysentry.api.v1.DeploySentryService.Get:output_type -> autokitteh.deploysentry.api.v1.GetResponse + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] 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_api_proto_init() } +func file_api_proto_init() { + if File_api_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeployRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeployResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetRatioRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetRatioResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetResponse); 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_api_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_proto_goTypes, + DependencyIndexes: file_api_proto_depIdxs, + MessageInfos: file_api_proto_msgTypes, + }.Build() + File_api_proto = out.File + file_api_proto_rawDesc = nil + file_api_proto_goTypes = nil + file_api_proto_depIdxs = nil +} diff --git a/deploysentry/go/gen/apiv1connect/api.connect.go b/deploysentry/go/gen/apiv1connect/api.connect.go new file mode 100644 index 00000000..b20fd3d8 --- /dev/null +++ b/deploysentry/go/gen/apiv1connect/api.connect.go @@ -0,0 +1,161 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: api.proto + +package apiv1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + gen "github.com/autokitteh/kittehub/deploysentry/go/gen" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion0_1_0 + +const ( + // DeploySentryServiceName is the fully-qualified name of the DeploySentryService service. + DeploySentryServiceName = "autokitteh.deploysentry.api.v1.DeploySentryService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // DeploySentryServiceDeployProcedure is the fully-qualified name of the DeploySentryService's + // Deploy RPC. + DeploySentryServiceDeployProcedure = "/autokitteh.deploysentry.api.v1.DeploySentryService/Deploy" + // DeploySentryServiceSetRatioProcedure is the fully-qualified name of the DeploySentryService's + // SetRatio RPC. + DeploySentryServiceSetRatioProcedure = "/autokitteh.deploysentry.api.v1.DeploySentryService/SetRatio" + // DeploySentryServiceGetProcedure is the fully-qualified name of the DeploySentryService's Get RPC. + DeploySentryServiceGetProcedure = "/autokitteh.deploysentry.api.v1.DeploySentryService/Get" +) + +// DeploySentryServiceClient is a client for the autokitteh.deploysentry.api.v1.DeploySentryService +// service. +type DeploySentryServiceClient interface { + Deploy(context.Context, *connect.Request[gen.DeployRequest]) (*connect.Response[gen.DeployResponse], error) + SetRatio(context.Context, *connect.Request[gen.SetRatioRequest]) (*connect.Response[gen.SetRatioResponse], error) + Get(context.Context, *connect.Request[gen.GetRequest]) (*connect.Response[gen.GetResponse], error) +} + +// NewDeploySentryServiceClient constructs a client for the +// autokitteh.deploysentry.api.v1.DeploySentryService service. By default, it uses the Connect +// protocol with the binary Protobuf Codec, asks for gzipped responses, and sends uncompressed +// requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewDeploySentryServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) DeploySentryServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &deploySentryServiceClient{ + deploy: connect.NewClient[gen.DeployRequest, gen.DeployResponse]( + httpClient, + baseURL+DeploySentryServiceDeployProcedure, + opts..., + ), + setRatio: connect.NewClient[gen.SetRatioRequest, gen.SetRatioResponse]( + httpClient, + baseURL+DeploySentryServiceSetRatioProcedure, + opts..., + ), + get: connect.NewClient[gen.GetRequest, gen.GetResponse]( + httpClient, + baseURL+DeploySentryServiceGetProcedure, + opts..., + ), + } +} + +// deploySentryServiceClient implements DeploySentryServiceClient. +type deploySentryServiceClient struct { + deploy *connect.Client[gen.DeployRequest, gen.DeployResponse] + setRatio *connect.Client[gen.SetRatioRequest, gen.SetRatioResponse] + get *connect.Client[gen.GetRequest, gen.GetResponse] +} + +// Deploy calls autokitteh.deploysentry.api.v1.DeploySentryService.Deploy. +func (c *deploySentryServiceClient) Deploy(ctx context.Context, req *connect.Request[gen.DeployRequest]) (*connect.Response[gen.DeployResponse], error) { + return c.deploy.CallUnary(ctx, req) +} + +// SetRatio calls autokitteh.deploysentry.api.v1.DeploySentryService.SetRatio. +func (c *deploySentryServiceClient) SetRatio(ctx context.Context, req *connect.Request[gen.SetRatioRequest]) (*connect.Response[gen.SetRatioResponse], error) { + return c.setRatio.CallUnary(ctx, req) +} + +// Get calls autokitteh.deploysentry.api.v1.DeploySentryService.Get. +func (c *deploySentryServiceClient) Get(ctx context.Context, req *connect.Request[gen.GetRequest]) (*connect.Response[gen.GetResponse], error) { + return c.get.CallUnary(ctx, req) +} + +// DeploySentryServiceHandler is an implementation of the +// autokitteh.deploysentry.api.v1.DeploySentryService service. +type DeploySentryServiceHandler interface { + Deploy(context.Context, *connect.Request[gen.DeployRequest]) (*connect.Response[gen.DeployResponse], error) + SetRatio(context.Context, *connect.Request[gen.SetRatioRequest]) (*connect.Response[gen.SetRatioResponse], error) + Get(context.Context, *connect.Request[gen.GetRequest]) (*connect.Response[gen.GetResponse], error) +} + +// NewDeploySentryServiceHandler builds an HTTP handler from the service implementation. It returns +// the path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewDeploySentryServiceHandler(svc DeploySentryServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + deploySentryServiceDeployHandler := connect.NewUnaryHandler( + DeploySentryServiceDeployProcedure, + svc.Deploy, + opts..., + ) + deploySentryServiceSetRatioHandler := connect.NewUnaryHandler( + DeploySentryServiceSetRatioProcedure, + svc.SetRatio, + opts..., + ) + deploySentryServiceGetHandler := connect.NewUnaryHandler( + DeploySentryServiceGetProcedure, + svc.Get, + opts..., + ) + return "/autokitteh.deploysentry.api.v1.DeploySentryService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case DeploySentryServiceDeployProcedure: + deploySentryServiceDeployHandler.ServeHTTP(w, r) + case DeploySentryServiceSetRatioProcedure: + deploySentryServiceSetRatioHandler.ServeHTTP(w, r) + case DeploySentryServiceGetProcedure: + deploySentryServiceGetHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedDeploySentryServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedDeploySentryServiceHandler struct{} + +func (UnimplementedDeploySentryServiceHandler) Deploy(context.Context, *connect.Request[gen.DeployRequest]) (*connect.Response[gen.DeployResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("autokitteh.deploysentry.api.v1.DeploySentryService.Deploy is not implemented")) +} + +func (UnimplementedDeploySentryServiceHandler) SetRatio(context.Context, *connect.Request[gen.SetRatioRequest]) (*connect.Response[gen.SetRatioResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("autokitteh.deploysentry.api.v1.DeploySentryService.SetRatio is not implemented")) +} + +func (UnimplementedDeploySentryServiceHandler) Get(context.Context, *connect.Request[gen.GetRequest]) (*connect.Response[gen.GetResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("autokitteh.deploysentry.api.v1.DeploySentryService.Get is not implemented")) +} diff --git a/deploysentry/go/go.mod b/deploysentry/go/go.mod new file mode 100644 index 00000000..04d31082 --- /dev/null +++ b/deploysentry/go/go.mod @@ -0,0 +1,12 @@ +module github.com/autokitteh/kittehub/deploysentry/go + +go 1.22.0 + +require ( + connectrpc.com/connect v1.16.2 + connectrpc.com/grpcreflect v1.2.0 + golang.org/x/net v0.27.0 + google.golang.org/protobuf v1.34.2 +) + +require golang.org/x/text v0.16.0 // indirect diff --git a/deploysentry/go/go.sum b/deploysentry/go/go.sum new file mode 100644 index 00000000..62772b61 --- /dev/null +++ b/deploysentry/go/go.sum @@ -0,0 +1,12 @@ +connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE= +connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc= +connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= +connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/deploysentry/go/handler.go b/deploysentry/go/handler.go new file mode 100644 index 00000000..7ded4aff --- /dev/null +++ b/deploysentry/go/handler.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "errors" + "log" + + "connectrpc.com/connect" + + apiv1 "github.com/autokitteh/kittehub/deploysentry/go/gen" +) + +type val struct { + v string + r int32 +} + +type handler struct{ deploys map[string]val } + +func (h *handler) Deploy(_ context.Context, req *connect.Request[apiv1.DeployRequest]) (*connect.Response[apiv1.DeployResponse], error) { + log.Printf("deploy %s#%s", req.Msg.Svc, req.Msg.Version) + + h.deploys[req.Msg.Svc] = val{v: req.Msg.Version, r: 0} + + return connect.NewResponse(&apiv1.DeployResponse{}), nil +} + +func (h *handler) SetRatio(_ context.Context, req *connect.Request[apiv1.SetRatioRequest]) (*connect.Response[apiv1.SetRatioResponse], error) { + if _, ok := h.deploys[req.Msg.Svc]; !ok { + return nil, connect.NewError(connect.CodeNotFound, errors.New("deploy not found")) + } + + log.Printf("set ratio %s#%s to %d", req.Msg.Svc, req.Msg.Version, req.Msg.Ratio) + + h.deploys[req.Msg.Svc] = val{v: req.Msg.Version, r: req.Msg.Ratio} + + return connect.NewResponse(&apiv1.SetRatioResponse{}), nil +} + +func (h *handler) get(svc string) val { + return h.deploys[svc] +} + +func (h *handler) Get(_ context.Context, req *connect.Request[apiv1.GetRequest]) (*connect.Response[apiv1.GetResponse], error) { + v := h.get(req.Msg.Svc) + + return connect.NewResponse(&apiv1.GetResponse{Version: v.v, Ratio: v.r}), nil +} + +func newHandler() *handler { return &handler{deploys: make(map[string]val)} } diff --git a/deploysentry/go/main.go b/deploysentry/go/main.go new file mode 100644 index 00000000..4d8aeb8e --- /dev/null +++ b/deploysentry/go/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + + "connectrpc.com/grpcreflect" + "github.com/autokitteh/kittehub/deploysentry/go/gen/apiv1connect" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +type key struct{ svc, version string } + +var stats = map[key]int32{ + {svc: "svc1", version: "v1"}: 0, + {svc: "svc1", version: "v2"}: 10, + {svc: "svc1", version: "v3"}: 20, +} + +func main() { + mux := http.NewServeMux() + + h := newHandler() + + mux.Handle(apiv1connect.NewDeploySentryServiceHandler(h)) + + mux.Handle(grpcreflect.NewHandlerV1( + grpcreflect.NewStaticReflector(apiv1connect.DeploySentryServiceName), + )) + + mux.Handle("/check", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + svc := r.URL.Query().Get("svc") + + v := h.get(svc) + + total := int32(float32(stats[key{svc: svc, version: v.v}]) * float32(v.r) / float32(100)) + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(map[string]int32{"error_rate": total}); err != nil { + log.Printf("json encode: %v", err) + } + + log.Printf("check %s: %d errors", svc, total) + })) + + log.Print("Starting HTTP server on port 8080") + + srv := &http.Server{ + Addr: ":8080", + Handler: h2c.NewHandler( + mux, + &http2.Server{}, + ), + } + + if err := srv.ListenAndServe(); err != nil { + log.Fatalf("HTTP listen and serve: %v", err) + } +} diff --git a/deploysentry/main.star b/deploysentry/main.star new file mode 100644 index 00000000..c6b3af75 --- /dev/null +++ b/deploysentry/main.star @@ -0,0 +1,64 @@ +load("env", "DEPLOYSENTRY_ADDR", "INITIAL_RATIO", "RATIO_INCREMENT", "STEP_DURATION") +load("@grpc", "grpc") +load("@http", "http") +load("@slack", "slack") + +MAX_ERROR_RATE = 5 + +def on_slack_app_mention(data): + channel = data.channel + + parts = data.text.split(" ") + + if parts[1] != "deploy": + return + + svc, version = parts[2], parts[3] + + print(svc, version) + + _deploy(channel, svc, version) + + for r in range(0, 100, int(RATIO_INCREMENT)): + result = http.get("http://{}/check".format(DEPLOYSENTRY_ADDR), params={"svc": svc}).body_json + rate = result['error_rate'] + slack.chat_post_message(channel, "error rate: {}".format(rate)) + + if rate > MAX_ERROR_RATE: + slack.chat_post_message(channel, "error rate too high, aborting deployment") + _set_ratio(channel, svc, version, 0) + return + + _set_ratio(channel, svc, version, r) + sleep(STEP_DURATION) + + _set_ratio(channel, svc, version, 100) + slack.chat_post_message(channel, "deployment complete, yay!") + + +def _deploy(channel, svc, version): + slack.chat_post_message(channel, "deploying {}#{}".format(svc, version)) + + grpc.call( + host=DEPLOYSENTRY_ADDR, + service="autokitteh.deploysentry.api.v1.DeploySentryService", + method="Deploy", + payload={ + "svc": svc, + "version": version, + }, + ) + +def _set_ratio(channel, svc, version, ratio): + slack.chat_post_message(channel, "setting ratio of {}#{} to {}%".format(svc, version, ratio)) + + grpc.call( + host=DEPLOYSENTRY_ADDR, + service="autokitteh.deploysentry.api.v1.DeploySentryService", + method="SetRatio", + payload={ + "svc": svc, + "version": version, + "ratio": ratio, + }, + )