Compare commits

...

7 Commits

Author SHA1 Message Date
c207152892 cleanup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-12-06 19:04:42 +03:00
Кирилл Горбунов
7a302ce899 #2 - add swaggerset (#8)
Co-authored-by: Gorbunov Kirill Andreevich <kgorbunov@mtsbank.ru>
Reviewed-on: #8
Reviewed-by: Василий Толстов <v.tolstov@unistack.org>
Co-authored-by: Кирилл Горбунов <kirya_gorbunov_2015@mail.ru>
Co-committed-by: Кирилл Горбунов <kirya_gorbunov_2015@mail.ru>
2024-12-06 19:01:07 +03:00
Кирилл Горбунов
ce57938ec2 #1 (#5)
Co-authored-by: Gorbunov Kirill Andreevich <kgorbunov@mtsbank.ru>
Reviewed-on: #5
Co-authored-by: Кирилл Горбунов <kirya_gorbunov_2015@mail.ru>
Co-committed-by: Кирилл Горбунов <kirya_gorbunov_2015@mail.ru>
2024-11-24 12:57:53 +03:00
c1103c714a allow to work with multiple configs
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-11-20 00:35:04 +03:00
3398ee60f3 complete nested config files
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-11-19 18:15:19 +03:00
ee4c343dee add empty graphql task
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-11-19 13:55:37 +03:00
4c32535bfa initial import for nested configs
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-11-19 12:37:17 +03:00
12 changed files with 768 additions and 73 deletions

1
.gitignore vendored
View File

@@ -25,3 +25,4 @@ config.yaml
servicechecker
*.protoset
authn.yaml
.idea

View File

@@ -7,7 +7,6 @@ import (
"os/signal"
"time"
openapi_v3 "github.com/google/gnostic/openapiv3"
"github.com/google/uuid"
grpccli "go.unistack.org/micro-client-grpc/v3"
httpcli "go.unistack.org/micro-client-http/v3"
@@ -29,11 +28,9 @@ import (
"go.unistack.org/servicechecker/pkg/config"
"go.unistack.org/servicechecker/pkg/grpcconn"
"go.unistack.org/servicechecker/pkg/httpconn"
"go.unistack.org/servicechecker/pkg/protoset"
"go.unistack.org/servicechecker/pkg/scheduler"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"go.unistack.org/servicechecker/pkg/swaggerset"
)
var (
@@ -62,14 +59,12 @@ func main() {
l.Fatal(ctx, "failed to init meter", err)
}
meters["default"] = m
f, err := os.Open("config.yaml")
if err != nil {
l.Fatal(ctx, "failed to open config", err)
}
defer f.Close()
meters[uuid.Nil.String()] = m
cfg := &config.Config{}
if err = cfg.Parse(f); err != nil {
l.Info(ctx, "try to load config")
if err := config.Load(config.Filesytem, "config.yaml", cfg); err != nil {
l.Fatal(ctx, "failed to open config", err)
}
@@ -133,7 +128,7 @@ func main() {
client.Codec("application/grpc+json", jsonpbcodec.NewCodec()),
client.ContentType("application/grpc"),
client.Retries(0),
// client.TLSConfig(&tls.Config{InsecureSkipVerify: true}),
// client.TLSConfig(&tls.Config{InsecureSkipVerify: true}),
)
if err = gcli.Init(); err != nil {
l.Fatal(ctx, "failed to init grpc client", err)
@@ -146,14 +141,14 @@ func main() {
client.Codec("application/json", jsonpbcodec.NewCodec()),
client.ContentType("application/json"),
client.Retries(0),
// client.TLSConfig(&tls.Config{InsecureSkipVerify: true}),
// client.TLSConfig(&tls.Config{InsecureSkipVerify: true}),
)
if err = hcli.Init(); err != nil {
l.Fatal(ctx, "failed to init http client", err)
}
clients["http"] = hcli
for _, check := range cfg.Checks {
for _, check := range cfg.App.Checks {
l.Info(ctx, fmt.Sprintf("check %#+v", check))
if !check.Active {
continue
@@ -161,7 +156,7 @@ func main() {
var mtr meter.Meter
if !cfg.App.MultiUser {
mtr = meters["default"]
mtr = meters[uuid.Nil.String()]
} else {
if v, ok := meters[check.User]; ok && v != nil {
mtr = v
@@ -185,6 +180,10 @@ func main() {
fn, args, err = newGRPCTask(ctx, l, mtr, check.Name, task)
case task.HTTP != nil:
fn, args, err = newHTTPTask(ctx, l, mtr, check.Name, task)
case task.GraphQL != nil:
fn, args, err = newGraphQLTask(ctx, l, mtr, check.Name, task)
default:
err = fmt.Errorf("unknown task type")
}
if err != nil {
l.Error(ctx, "failed to create task", err)
@@ -214,8 +213,10 @@ func newHTTPTask(ctx context.Context, l logger.Logger, m meter.Meter, check stri
var treq client.Request
var opts []client.CallOption
var labels []string
swaggerSet := swaggerset.NewSwaggerSet()
if task.HTTP.OpenAPI != "" {
var svc string
openapiBuf, err := os.ReadFile(task.HTTP.OpenAPI)
if err != nil {
@@ -223,12 +224,11 @@ func newHTTPTask(ctx context.Context, l logger.Logger, m meter.Meter, check stri
return nil, nil, err
}
doc, err := openapi_v3.ParseDocument(openapiBuf)
err = swaggerSet.AddSwaggerset(task.HTTP.Addr, svc, openapiBuf)
if err != nil {
l.Error(ctx, "failed to unmarshal openapi file", err)
l.Error(ctx, "failed to add openApi spec", err)
return nil, nil, err
}
_ = doc
errmap := make(map[string]interface{}, 1)
errmap["default"] = &codecpb.Frame{}
@@ -240,8 +240,17 @@ func newHTTPTask(ctx context.Context, l logger.Logger, m meter.Meter, check stri
// client.WithContentType("application/json"),
}
msg, err := swaggerSet.GetMessage(task.HTTP.Addr, svc, task.HTTP.Method)
if err != nil {
l.Error(ctx, "failed to get message from swagger spec", err)
return nil, nil, err
}
req = &codecpb.Frame{Data: []byte(task.HTTP.Data)}
rsp = &codecpb.Frame{}
if task.HTTP.Data == "" {
req = msg.Request
}
rsp = msg.Response
treq = c.NewRequest(task.Name, task.Name, req)
@@ -305,51 +314,28 @@ func newGRPCTask(ctx context.Context, l logger.Logger, m meter.Meter, check stri
var rsp interface{}
var treq client.Request
var labels []string
p := protoset.NewProtoSet()
if task.GRPC.Protoset != "" {
pkg, svc, mth := grpcconn.ServiceMethod(task.GRPC.Endpoint)
protosetBuf, err := os.ReadFile(task.GRPC.Protoset)
if err != nil {
l.Error(ctx, "failed to unmarshal protoset file", err)
return nil, nil, err
}
fdset := &descriptorpb.FileDescriptorSet{}
if err = protocodec.NewCodec().Unmarshal(protosetBuf, fdset); err != nil {
l.Error(ctx, "failed to unmarshal protoset file", err)
if err = p.AddProtoset(task.GRPC.Addr, svc, protosetBuf); err != nil {
l.Error(ctx, "failed add protoset", err)
return nil, nil, err
}
pfileoptions := protodesc.FileOptions{AllowUnresolvable: true}
pfiles, err := pfileoptions.NewFiles(fdset)
req, rsp, err = p.GetMessage(task.GRPC.Addr, pkg, svc, mth)
if err != nil {
l.Error(ctx, "failed to use protoset file", err)
l.Error(ctx, "failed get req, rsp from protoset", err)
return nil, nil, err
}
pkg, svc, mth := grpcconn.ServiceMethod(task.GRPC.Endpoint)
pdesc, err := pfiles.FindDescriptorByName(protoreflect.FullName(pkg + "." + svc))
if err != nil {
l.Error(ctx, "failed to find service "+pkg+"."+svc)
return nil, nil, err
}
sdesc, ok := pdesc.(protoreflect.ServiceDescriptor)
if !ok {
err = fmt.Errorf("failed to find service " + pkg + "." + svc)
l.Error(ctx, "unable to find service in protoset", err)
return nil, nil, err
}
mdesc := sdesc.Methods().ByName(protoreflect.Name(mth))
if mdesc == nil {
err = fmt.Errorf("unknown method " + mth)
l.Error(ctx, "failed to find method", err)
return nil, nil, err
}
req = dynamicpb.NewMessageType(mdesc.Input()).New()
rsp = dynamicpb.NewMessageType(mdesc.Output()).New()
if err = jsonpbcodec.NewCodec().Unmarshal([]byte(task.GRPC.Data), req); err != nil {
l.Error(ctx, "failed to unmarshal", err)
return nil, nil, err
@@ -401,3 +387,96 @@ func newGRPCTask(ctx context.Context, l logger.Logger, m meter.Meter, check stri
return fn, nil, nil
}
func newGraphQLTask(ctx context.Context, l logger.Logger, m meter.Meter, check string, task *config.TaskConfig) (any, []any, error) {
/*
var err error
c, ok := clients["http"]
if !ok {
err = fmt.Errorf("unknown client http")
l.Error(ctx, "failed to get client", err)
return nil, nil, err
}
var req interface{}
var rsp interface{}
var treq client.Request
var opts []client.CallOption
var labels []string
if task.HTTP.OpenAPI != "" {
openapiBuf, err := os.ReadFile(task.HTTP.OpenAPI)
if err != nil {
l.Error(ctx, "failed to unmarshal openapi file", err)
return nil, nil, err
}
doc, err := openapi_v3.ParseDocument(openapiBuf)
if err != nil {
l.Error(ctx, "failed to unmarshal openapi file", err)
return nil, nil, err
}
_ = doc
errmap := make(map[string]interface{}, 1)
errmap["default"] = &codecpb.Frame{}
opts = []client.CallOption{
httpcli.ErrorMap(errmap),
httpcli.Method(task.HTTP.Method),
httpcli.Path(task.HTTP.Endpoint),
// client.WithContentType("application/json"),
}
req = &codecpb.Frame{Data: []byte(task.HTTP.Data)}
rsp = &codecpb.Frame{}
treq = c.NewRequest(task.Name, task.Name, req)
labels = []string{"check", check, "task", task.Name, "service", task.Name, "endpoint", task.Name}
}
fn := func() {
var cerr error
metadata := make(map[string]string, len(task.HTTP.Metadata))
var rquid string
for k, v := range task.HTTP.Metadata {
if k == "x-request-id" && v == "generate" {
uid, err := uuid.NewV7()
if err != nil {
l.Error(ctx, "failed to generate x-request-id", err)
uid = uuid.Nil
} else {
v = uid.String()
}
}
metadata[k] = v
rquid = v
}
l.Info(ctx, fmt.Sprintf("call %s.%s endpoint %s", treq.Service(), treq.Method(), treq.Endpoint()), "x-request-id", rquid)
m.Counter(semconv.ClientRequestInflight, labels...).Inc()
ts := time.Now()
cerr = httpconn.Call(ctx, rquid, l, c, task.HTTP.Addr, time.Duration(task.Timeout),
treq,
rsp,
append(opts, client.WithRequestMetadata(metadata))...,
)
te := time.Since(ts)
m.Counter(semconv.ClientRequestInflight, labels...).Dec()
m.Summary(semconv.ClientRequestLatencyMicroseconds, labels...).Update(te.Seconds())
m.Histogram(semconv.ClientRequestDurationSeconds, labels...).Update(te.Seconds())
if cerr != nil {
m.Counter(semconv.ClientRequestTotal, append(labels, "status", "failure")...).Inc()
} else {
m.Counter(semconv.ClientRequestTotal, append(labels, "status", "success")...).Inc()
}
}
*/
return nil, nil, nil
}

10
go.mod
View File

@@ -4,8 +4,9 @@ go 1.23.3
require (
github.com/go-co-op/gocron/v2 v2.12.3
github.com/google/gnostic v0.7.0
github.com/google/uuid v1.6.0
github.com/ompluscator/dynamic-struct v1.4.0
github.com/stretchr/testify v1.9.0
go.unistack.org/micro-client-grpc/v3 v3.11.10
go.unistack.org/micro-client-http/v3 v3.9.14
go.unistack.org/micro-codec-json/v3 v3.10.1
@@ -20,16 +21,21 @@ require (
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/gnostic v0.7.0 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect
go.unistack.org/metrics v0.0.1 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/text v0.20.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect
google.golang.org/grpc v1.68.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

12
go.sum
View File

@@ -805,8 +805,9 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -819,6 +820,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/ompluscator/dynamic-struct v1.4.0 h1:I/Si9LZtItSwiTMe7vosEuIu2TKdOvWbE3R/lokpN4Q=
github.com/ompluscator/dynamic-struct v1.4.0/go.mod h1:ADQ1+6Ox1D+ntuNwTHyl1NvpAqY2lBXPSPbcO4CJdeA=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
@@ -840,8 +843,9 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
@@ -1202,8 +1206,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@@ -1,14 +1,33 @@
package config
import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"github.com/google/uuid"
yamlcodec "go.unistack.org/micro-codec-yaml/v3"
mtime "go.unistack.org/micro/v3/util/time"
)
var Filesytem fs.FS
func init() {
dir, _ := os.Getwd()
Filesytem = os.DirFS(dir)
}
type Config struct {
App *AppConfig `json:"app,omitempty" yaml:"app,omitempty"`
Meter *MeterConfig `json:"meter,omitempty" yaml:"meter,omitempty"`
}
type AppConfig struct {
MultiUser bool `json:"multi_user,omitempty" yaml:"multi_user,omitempty"`
ChecksFiles []string `json:"checks_files,omitempty" yaml:"checks_files,omitempty"`
Checks []*CheckConfig `json:"checks,omitempty" yaml:"checks,omitempty"`
MultiUser bool `json:"multi_user,omitempty" yaml:"multi_user,omitempty"`
}
type MeterConfig struct {
@@ -16,19 +35,14 @@ type MeterConfig struct {
Path string `json:"path,omitempty" yaml:"path,omitempty"`
}
type Config struct {
App *AppConfig `json:"app,omitempty" yaml:"app,omitempty"`
Meter *MeterConfig `json:"meter,omitempty" yaml:"meter,omitempty"`
Checks []*CheckConfig `json:"checks,omitempty" yaml:"checks,omitempty"`
}
type CheckConfig struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Tasks []*TaskConfig `json:"tasks,omitempty" yaml:"tasks,omitempty"`
Timeout mtime.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
Interval mtime.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
Active bool `json:"active,omitempty" yaml:"active,omitempty"`
User string `json:"user,omitempty" yaml:"user,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
User string `json:"user,omitempty" yaml:"user,omitempty"`
Tasks []*TaskConfig `json:"tasks,omitempty" yaml:"tasks,omitempty"`
TasksFiles []string `json:"tasks_files,omitempty" yaml:"tasks_files,omitempty"`
Timeout mtime.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
Interval mtime.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
Active bool `json:"active,omitempty" yaml:"active,omitempty"`
}
type HTTPConfig struct {
@@ -65,11 +79,84 @@ type TaskConfig struct {
Active bool `json:"active,omitempty" yaml:"active,omitempty"`
}
func (cfg *Config) Parse(r io.Reader) error {
buf, err := io.ReadAll(r)
func Load(fileSystem fs.FS, name string, cfg *Config) error {
if err := load(fileSystem, name, cfg); err != nil {
return err
}
for _, checksPatternFile := range cfg.App.ChecksFiles {
checkRoot := filepath.Dir(checksPatternFile)
checksFiles := fsWalkDir(fileSystem, checkRoot, checksPatternFile)
for _, checkFile := range checksFiles {
checks := []*CheckConfig{}
if err := load(fileSystem, checkFile, &checks); err != nil {
return err
}
for ckecksIdx := range checks {
for _, tasksPatternFile := range checks[ckecksIdx].TasksFiles {
taskRoot := filepath.Join(filepath.Dir(checksPatternFile), filepath.Dir(tasksPatternFile))
tasksFiles := fsWalkDir(fileSystem, taskRoot, filepath.Join(filepath.Dir(checksPatternFile), tasksPatternFile))
for tasksIdx := range tasksFiles {
tasks := []*TaskConfig{}
if err := load(fileSystem, tasksFiles[tasksIdx], &tasks); err != nil {
return err
}
checks[ckecksIdx].Tasks = append(checks[ckecksIdx].Tasks, tasks...)
}
}
cfg.App.Checks = append(cfg.App.Checks, checks[ckecksIdx])
}
}
}
if !cfg.App.MultiUser {
for _, check := range cfg.App.Checks {
check.User = uuid.Nil.String()
}
}
return nil
}
func fsWalkDir(fileSystem fs.FS, root string, pattern string) []string {
var files []string
fs.WalkDir(fileSystem, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || !d.Type().IsRegular() {
return nil
}
var ok bool
if ok, err = filepath.Match(pattern, path); err == nil && ok {
files = append(files, path)
} else {
return err
}
return nil
})
return files
}
func load(fileSystem fs.FS, name string, cfg interface{}) error {
f, err := fileSystem.Open(name)
if err != nil {
return err
}
return yamlcodec.NewCodec().Unmarshal(buf, cfg)
c := yamlcodec.NewCodec()
var buf []byte
if buf, err = io.ReadAll(f); err == nil {
if err = f.Close(); err == nil {
err = c.Unmarshal(buf, cfg)
}
}
if err != nil {
return fmt.Errorf("failed to load config %w", err)
}
return nil
}

View File

@@ -3,8 +3,8 @@
package grpcconn
import (
"github.com/emicklei/proto"
"github.com/jhump/protoreflect/desc"
"google.golang.org/protobuf/proto"
)
var protoSets = map[string]*desc.FileDescriptor

84
pkg/protoset/protoset.go Normal file
View File

@@ -0,0 +1,84 @@
package protoset
import (
"context"
"errors"
"fmt"
"sync"
protocodec "go.unistack.org/micro-codec-proto/v3"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
)
var errNotFound = errors.New("file descriptor not found")
type ProtoSet struct {
mu sync.Mutex
files map[string]*protoregistry.Files
}
func NewProtoSet() *ProtoSet {
return &ProtoSet{
mu: sync.Mutex{},
files: make(map[string]*protoregistry.Files, 0),
}
}
func (p *ProtoSet) GetMessage(addr, pkg, svc, mth string) (protoreflect.Message, protoreflect.Message, error) {
if addr == "" || svc == "" || mth == "" || pkg == "" {
return nil, nil, errors.New("addr or service name is empty")
}
p.mu.Lock()
pfile, ok := p.files[addr+"|"+svc]
p.mu.Unlock()
if !ok || pfile == nil {
return nil, nil, errNotFound
}
pdesc, err := pfile.FindDescriptorByName(protoreflect.FullName(pkg + "." + svc))
if err != nil {
return nil, nil, fmt.Errorf("failed to find service %s.%s, err: %w", pkg, svc, err)
}
sdesc, ok := pdesc.(protoreflect.ServiceDescriptor)
if !ok {
return nil, nil, fmt.Errorf("failed to find service " + pkg + "." + svc)
}
mdesc := sdesc.Methods().ByName(protoreflect.Name(mth))
if mdesc == nil {
return nil, nil, fmt.Errorf("unknown method " + mth)
}
req := dynamicpb.NewMessageType(mdesc.Input()).New()
rsp := dynamicpb.NewMessageType(mdesc.Output()).New()
return req, rsp, nil
}
func (p *ProtoSet) AddProtoset(addr, svc string, data []byte) error {
fdset := &descriptorpb.FileDescriptorSet{}
if err := protocodec.NewCodec().Unmarshal(data, fdset); err != nil {
return fmt.Errorf("failed to unmarshal protoset file: %w", err)
}
pfileoptions := protodesc.FileOptions{AllowUnresolvable: true}
pfiles, err := pfileoptions.NewFiles(fdset)
if err != nil {
return fmt.Errorf("failed to use protoset file, err: %w", err)
}
p.mu.Lock()
p.files[addr+"|"+svc] = pfiles
p.mu.Unlock()
return nil
}
func (p *ProtoSet) AddReflection(ctx context.Context, service string, addr string) error {
return nil
}

View File

@@ -0,0 +1,49 @@
package protoset
import (
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestProtoSet_1(t *testing.T) {
p := NewProtoSet()
data, err := os.ReadFile("path to .protoset")
assert.Nil(t, err)
err = p.AddProtoset("localhost:9090", "CardService", data)
assert.Nil(t, err)
req, rsp, err := p.GetMessage("localhost:9090", "card_proto", "CardService", "GetCardList")
assert.Nil(t, err)
assert.NotNil(t, req)
assert.NotNil(t, rsp)
fmt.Printf("req: %v, rsp: %v \n", req, rsp)
}
func TestProtoSet_2_bad(t *testing.T) {
p := NewProtoSet()
data, err := os.ReadFile("path to .protoset")
assert.Nil(t, err)
err = p.AddProtoset("localhost:9090", "CardService", data)
assert.Nil(t, err)
req, rsp, err := p.GetMessage("localhost:9090", "card_proto", "Card", "GetCardList")
assert.Error(t, err)
assert.Nil(t, req)
assert.Nil(t, rsp)
fmt.Printf("req: %v, rsp: %v \n", req, rsp)
}
func TestProtoSet_3_not_found(t *testing.T) {
p := NewProtoSet()
req, rsp, err := p.GetMessage("localhost:9090", "card_proto", "CardService", "GetCardList")
assert.ErrorIs(t, err, errNotFound)
assert.Nil(t, req)
assert.Nil(t, rsp)
fmt.Printf("req: %v, rsp: %v \n", req, rsp)
}

View File

@@ -0,0 +1,81 @@
openapi: 3.0.3
info:
title: platform/services/domain/service-proto
description: Domain Service
version: 3.6.0
paths:
/domain-service/v1/push_mail/enabled:
get:
tags:
- DomainService
description: Получение статуса подключения PUSH (глобального)
operationId: IsPushTokenEnabled
parameters:
- name: Phone
in: header
schema:
type: string
- name: app_name
in: query
schema:
type: string
- name: device_id.value
in: query
description: The string value.
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/IsPushTokenEnabledRsp'
post:
tags:
- DomainService
description: Сохранение статуса подключения PUSH (глобального)
operationId: SetPushTokenEnabled
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SetPushTokenEnabledReq'
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SetPushTokenEnabledRsp'
components:
schemas:
IsPushTokenEnabledRsp:
type: object
properties:
result:
type: boolean
setPushTokenEnabledRsp:
$ref: '#/components/schemas/SetPushTokenEnabledRsp'
SetPushTokenEnabledReq:
type: object
properties:
app_name:
type: string
enabled:
type: boolean
device_id:
$ref: '#/components/schemas/StringValue'
StringValue:
type: object
properties:
value:
type: string
description: The string value.
description: Wrapper message for `string`. The JSON representation for `StringValue` is JSON string.
SetPushTokenEnabledRsp:
type: object
properties:
result:
type: boolean

View File

@@ -0,0 +1,82 @@
package swaggerset
import (
"errors"
"net/http"
"sync"
openapi "go.unistack.org/micro-proto/v3/openapiv3"
)
var errNotFound = errors.New("file descriptor not found")
type SwaggerSet struct {
mu sync.Mutex
files map[string]*openapi.Document
}
func NewSwaggerSet() *SwaggerSet {
return &SwaggerSet{
mu: sync.Mutex{},
files: make(map[string]*openapi.Document, 0),
}
}
func (p *SwaggerSet) GetMessage(addr, svc, mth string) (*Message, error) {
if svc == "" || mth == "" || addr == "" {
return nil, errors.New("addr or service name is empty")
}
p.mu.Lock()
doc := p.files[addr+"|"+svc]
p.mu.Unlock()
var reqParam, reqBody, rsp interface{}
var typeReq string
for _, path := range doc.Paths.GetPath() {
if path.GetName() == mth {
if path.GetValue().GetGet() != nil {
typeReq = http.MethodGet
reqParam, reqBody, rsp = handleOperation(path.GetValue().GetGet(), doc)
}
if path.GetValue().GetPost() != nil {
typeReq = http.MethodPost
reqParam, reqBody, rsp = handleOperation(path.GetValue().GetPost(), doc)
}
if path.GetValue().GetDelete() != nil {
typeReq = http.MethodDelete
reqParam, reqBody, rsp = handleOperation(path.GetValue().GetDelete(), doc)
}
if path.GetValue().GetPatch() != nil {
typeReq = http.MethodPatch
reqParam, reqBody, rsp = handleOperation(path.GetValue().GetPatch(), doc)
}
if path.GetValue().GetPut() != nil {
typeReq = http.MethodPut
reqParam, reqBody, rsp = handleOperation(path.GetValue().GetPut(), doc)
}
}
}
msg := &Message{
Type: typeReq,
Request: httpRequest{
Header: reqParam,
Body: reqBody,
},
Response: rsp,
}
return msg, nil
}
func (p *SwaggerSet) AddSwaggerset(addr, svc string, data []byte) error {
doc, err := openapi.ParseDocument(data)
if err != nil {
return err
}
p.mu.Lock()
p.files[addr+"|"+svc] = doc
p.mu.Unlock()
return nil
}

View File

@@ -0,0 +1,33 @@
package swaggerset
import (
"encoding/json"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSwaggerSet_1(t *testing.T) {
s := NewSwaggerSet()
data, err := os.ReadFile("swagger.yaml")
assert.Nil(t, err)
err = s.AddSwaggerset("localhost:8080", "service", data)
assert.Nil(t, err)
msg, err := s.GetMessage(
"localhost:8080",
"service",
"/domain-service/v1/push_mail/enabled",
"GET")
assert.Nil(t, err)
assert.NotNil(t, msg)
req, err := json.Marshal(msg.Request)
assert.Nil(t, err)
rsp, err := json.Marshal(msg.Response)
assert.Nil(t, err)
fmt.Printf("JSON: type: %s, req: %s, rsp: %s \n", msg.Type, req, rsp)
fmt.Printf("Struct: type: %s, req: %+v, rsp: %+v \n", msg.Type, msg.Request, msg.Response)
}

189
pkg/swaggerset/util.go Normal file
View File

@@ -0,0 +1,189 @@
package swaggerset
import (
"fmt"
"reflect"
"strings"
dynamicstruct "github.com/ompluscator/dynamic-struct"
openapi "go.unistack.org/micro-proto/v3/openapiv3"
)
type Message struct {
Type string
Request httpRequest
Response interface{}
}
type httpRequest struct {
Header interface{}
Body interface{}
}
func handleOperation(operation *openapi.Operation, doc *openapi.Document) (reqParam, reqBody, rsp interface{}) {
// Обработка параметров (GET)
if len(operation.Parameters) > 0 {
paramsStruct := dynamicstruct.NewStruct()
for _, paramRef := range operation.Parameters {
var param *openapi.Parameter
param = paramRef.GetParameter()
fieldName := capitalize(param.Name)
jsonName := strings.ToLower(param.Name[:1]) + param.Name[1:]
goType := getGoType(doc, param.Schema)
// В зависимости от того, где параметр находится (header, query, path, etc.), добавляем соответствующий тег
switch param.In {
case "query":
paramsStruct = paramsStruct.AddField(fieldName, goType, fmt.Sprintf(`json:"%s" query:"%s"`, jsonName, jsonName))
case "header":
paramsStruct = paramsStruct.AddField(fieldName, goType, fmt.Sprintf(`json:"%s" header:"%s"`, jsonName, jsonName))
default:
paramsStruct = paramsStruct.AddField(fieldName, goType, fmt.Sprintf(`json:"%s"`, jsonName))
}
}
// Получили структуру запроса для методов, где есть параметры в header, query, path, etc., добавили теги
reqParam = paramsStruct.Build().New()
}
// Обработка тела запроса (POST)
if operation.GetRequestBody() != nil {
if operation.GetRequestBody().GetRequestBody() != nil {
bodyFields := buildDynamicStruct(doc, operation.GetRequestBody().GetRequestBody().GetContent().GetAdditionalProperties()[0].GetValue().GetSchema())
bodyStruct := reflect.StructOf(bodyFields)
bodyInstance := reflect.New(bodyStruct).Interface()
// Получили тело запроса
reqBody = bodyInstance
}
if operation.GetRequestBody().GetReference() != nil {
schemOrRef := findReference(doc, operation.GetRequestBody().GetReference().GetXRef())
bodyFields := buildDynamicStruct(doc, schemOrRef)
bodyStruct := reflect.StructOf(bodyFields)
bodyInstance := reflect.New(bodyStruct).Interface()
// Получили тело запроса
reqBody = bodyInstance
}
}
// Обработка ответов
for _, rspOrRef := range operation.Responses.ResponseOrReference {
if rspOrRef.GetValue().GetResponse() != nil {
for _, prop := range rspOrRef.Value.GetResponse().GetContent().GetAdditionalProperties() {
responseFields := buildDynamicStruct(doc, prop.GetValue().GetSchema())
responseStruct := reflect.StructOf(responseFields)
responseInstance := reflect.New(responseStruct).Interface()
// Получили структуру ответа
rsp = responseInstance
}
}
if rspOrRef.GetValue().GetReference() != nil {
schemaOrRef := findReference(doc, rspOrRef.GetValue().GetReference().GetXRef())
responseFields := buildDynamicStruct(doc, schemaOrRef)
responseStruct := reflect.StructOf(responseFields)
responseInstance := reflect.New(responseStruct).Interface()
// Получили структуру ответа
rsp = responseInstance
}
}
return
}
// Рекурсивное создание структуры из схемы с учетом $ref
func buildDynamicStruct(doc *openapi.Document, schemaOrRef *openapi.SchemaOrReference) []reflect.StructField {
var sfields []reflect.StructField
var schema *openapi.Schema
if schemaOrRef.GetSchema() != nil {
schema = schemaOrRef.GetSchema()
}
if schemaOrRef.GetReference() != nil {
name := strings.Split(schemaOrRef.GetReference().GetXRef(), "#/components/schemas/")[1]
fieldName := capitalize(name)
subBuilder := buildDynamicStruct(doc, findReference(doc, schemaOrRef.GetReference().GetXRef()))
sfield := reflect.StructField{
Name: fieldName,
Type: reflect.StructOf(subBuilder),
Tag: reflect.StructTag(fmt.Sprintf(`json:"%s"`, name)),
}
sfields = append(sfields, sfield)
}
for _, prop := range schema.GetProperties().GetAdditionalProperties() {
fieldName := capitalize(prop.GetName())
if prop.GetValue().GetReference() != nil {
subBuilder := buildDynamicStruct(doc, prop.GetValue())
sfield := reflect.StructField{
Name: fieldName,
Type: reflect.StructOf(subBuilder),
Tag: reflect.StructTag(fmt.Sprintf(`json:"%s"`, prop.GetName())),
}
sfields = append(sfields, sfield)
}
if prop.GetValue().GetSchema() != nil {
sfield := reflect.StructField{
Name: fieldName,
Type: reflect.TypeOf(getGoType(doc, prop.GetValue())),
Tag: reflect.StructTag(fmt.Sprintf(`json:"%s"`, prop.GetName())),
}
sfields = append(sfields, sfield)
}
}
return sfields
}
func findReference(doc *openapi.Document, ref string) *openapi.SchemaOrReference {
var result *openapi.SchemaOrReference
ref = strings.Split(ref, "#/components/schemas/")[1]
for _, prop := range doc.Components.Schemas.GetAdditionalProperties() {
if prop.Name == ref {
result = prop.Value
}
}
return result
}
// Преобразование типа OpenAPI в тип Go
func getGoType(doc *openapi.Document, schema *openapi.SchemaOrReference) interface{} {
switch schema.GetSchema().Type {
case "string":
return ""
case "integer":
return 0
case "boolean":
return false
case "array":
return []interface{}{}
case "object":
return buildDynamicStruct(doc, schema)
default:
return nil
}
}
func capitalize(fieldName string) string {
if fieldName == "" {
return fieldName
}
// Заменяем точки на подчеркивания для унификации
fieldName = strings.ReplaceAll(fieldName, ".", "_")
// Разделяем строку по подчеркиваниям
parts := strings.Split(fieldName, "_")
if len(parts) == 1 {
return strings.ToUpper(fieldName[:1]) + fieldName[1:]
}
// Обрабатываем каждый фрагмент
for i := 0; i < len(parts); i++ {
// Капитализируем первые буквы всех частей, кроме первой
parts[i] = strings.Title(parts[i]) // cases.Title(language.English).String(parts[i])
}
// Собираем строку обратно, соединяя части без подчеркиваний
return strings.Join(parts, "")
}