├── manager
├── api
│ ├── api_test.go
│ └── api.go
├── executor
│ ├── executor.go
│ ├── mock_PluginClient.go
│ ├── general_executor.go
│ └── executor_test.go
├── healthcheck
│ ├── healthcheck.go
│ ├── healthcheck_test.go
│ └── general_healthcheck.go
├── dispatcher
│ ├── mock_Dispatcher.go
│ ├── dispatcher_test.go
│ ├── pagerduty.go
│ ├── slack.go
│ ├── telegram.go
│ ├── dispatcher.go
│ └── discord.go
├── plugin
│ ├── db_test.go
│ ├── plugin_test.go
│ ├── plugin.go
│ └── db.go
└── config
│ ├── fixtures.go
│ └── config.go
├── sdk
├── Makefile
├── log.go
├── bootstrap
│ └── main.template
├── go.mod
├── grpc_test.go
├── README.md
├── grpc.go
├── sdk.go
└── sdk_test.go
├── .gitignore
├── utils
├── version_test.go
├── version.go
├── monitoring.go
├── cmd.go
├── logs.go
├── utils_test.go
└── utils.go
├── docs
├── api.md
├── community_plugins.md
├── workflow.md
├── contributing.md
└── design.md
├── types
├── cmd.go
├── monitoring.go
└── manager.go
├── .github
├── pull_request_template.md
├── ISSUE_TEMPLATE
│ ├── ci_cd_implementation.md
│ ├── bug_report.md
│ ├── feature_request.md
│ └── feature_development.md
└── workflows
│ ├── ci_coverage.yml
│ ├── golangci-lint.yml
│ ├── ci_pr_notifier.yaml
│ ├── ci_reamin_pr_reminder.yaml
│ └── create-vatz-bi-weekly.yml
├── Makefile
├── main.go
├── monitoring
├── prometheus
│ ├── metric.go
│ ├── metric_test.go
│ ├── prometheus_test.go
│ └── prometheus.go
└── gcp
│ ├── gcp_client.go
│ └── gcp.go
├── cmd
├── plugin_test.go
├── cmd_test.go
├── version.go
├── cmd.go
├── stop.go
├── init.go
└── start.go
├── Dockerfile
├── rpc
├── grpc.go
├── rpc_test.go
└── rpc.go
├── mocks
├── mock_PluginClient.go
└── HealthCheck.go
├── script
└── simple_start_guide
│ ├── install_vatz&official-plugins.sh
│ ├── vatz_start.sh
│ ├── readme.md
│ ├── vatz_stop.sh
│ └── default_config.yaml
├── go.mod
├── README.md
└── LICENSE.LESSER
/manager/api/api_test.go:
--------------------------------------------------------------------------------
1 | package api
2 |
--------------------------------------------------------------------------------
/sdk/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | go test -v
3 |
4 | coverage:
5 | go test -v -coverprofile=cover.out
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swo
2 | *.swp
3 |
4 | cover.out
5 | vatz
6 | default.yaml
7 |
8 | *.log
9 |
10 | /.idea/modules.xml
11 | /.idea/vatz.iml
12 | /.idea/vcs.xml
13 | /.idea/workspace.xml
14 |
--------------------------------------------------------------------------------
/utils/version_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestVersion(t *testing.T) {
10 | ver := GetVersion()
11 |
12 | assert.Equal(t, "dev-N/A", ver)
13 | }
14 |
--------------------------------------------------------------------------------
/sdk/log.go:
--------------------------------------------------------------------------------
1 | package sdk
2 |
3 | import (
4 | "os"
5 | "time"
6 |
7 | "github.com/rs/zerolog"
8 | "github.com/rs/zerolog/log"
9 | )
10 |
11 | func init() {
12 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
13 | }
14 |
--------------------------------------------------------------------------------
/utils/version.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "fmt"
4 |
5 | // Version strings
6 | var (
7 | Version = "dev"
8 | Commit = "N/A"
9 | )
10 |
11 | // GetVersion returns version information of VATZ
12 | func GetVersion() string {
13 | return fmt.Sprintf("%s-%s", Version, Commit)
14 | }
15 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # VATZ API Specs
2 |
3 | RPC service is available for managing VATZ. For now, this RPC service is only available on local.
4 |
5 | ## Get Plugin's status
6 |
7 | Querying plugin's status.
8 |
9 | ```
10 | ~$ curl localhost:19091/v1/plugin_status
11 | {"status":"OK","pluginStatus":[{"status":"FAIL","pluginName":"up check"}]}
12 | ```
13 |
--------------------------------------------------------------------------------
/types/cmd.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type Initializer string
4 |
5 | const (
6 | TEST Initializer = "TEST"
7 | LIVE Initializer = "LIVE"
8 | )
9 |
10 | type PluginState struct {
11 | Status string `json:"status"`
12 | PluginStatus []struct {
13 | Status string `json:"status"`
14 | PluginName string `json:"pluginName"`
15 | } `json:"pluginStatus"`
16 | }
17 |
--------------------------------------------------------------------------------
/types/monitoring.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | /*
4 | * gcp Types
5 | *
6 | */
7 |
8 | type CredentialOption string
9 |
10 | const (
11 | MonitoringIdentifier = "vatz"
12 | ApplicationDefaultCredentials CredentialOption = "ADC"
13 | ServiceAccountCredentials CredentialOption = "SAC"
14 | APIKey CredentialOption = "APIKey"
15 | OAuth2 CredentialOption = "OAuth"
16 | )
17 |
--------------------------------------------------------------------------------
/docs/community_plugins.md:
--------------------------------------------------------------------------------
1 | # Community Plugins
2 |
3 | Share your own plugins with everyone by adding to the list at [vatz-community-plugins](https://github.com/dsrvlabs/vatz-community-plugins)
4 | Other contribution policies are available at [contribution guide](https://github.com/dsrvlabs/vatz/blob/main/docs/contributing.md).
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ### 1. Type of change
2 |
3 | Please delete options that are not relevant.
4 |
5 | - [ ] New feature
6 | - [ ] Enhancement
7 | - [ ] Bug/fix (non-breaking change which fixes an issue)
8 | - [ ] others (anything other than above)
9 |
10 | ---
11 |
12 | ### 2. Summary
13 | > Please include a summary of the changes and which issue is fixed or solved.
14 |
15 | **Related:** # (issue)
16 |
17 | **Summary**
18 |
19 |
20 | ---
21 |
22 | ### 3. Comments
23 | > Please, leave a comments if there's further action that requires.
--------------------------------------------------------------------------------
/sdk/bootstrap/main.template:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/dsrvlabs/vatz/sdk"
7 | "golang.org/x/net/context"
8 | "google.golang.org/protobuf/types/known/structpb"
9 | )
10 |
11 | const (
12 | addr = "0.0.0.0"
13 | port = 9091
14 | )
15 |
16 | func main() {
17 | p := sdk.NewPlugin()
18 | p.Register(pluginFeature)
19 |
20 | ctx := context.Background()
21 | if err := p.Start(ctx, addr, port); err != nil {
22 | fmt.Println("exit")
23 | }
24 | }
25 |
26 | func pluginFeature(info, opt map[string]*structpb.Value) error {
27 | // TODO: Fill here.
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | VERSION := $(shell git describe --tags)
2 | BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
3 | COMMIT_HASH := $(shell git rev-parse --short HEAD)
4 | LDFLAGS := -ldflags="-X 'github.com/dsrvlabs/vatz/utils.Version=$(BRANCH)' -X 'github.com/dsrvlabs/vatz/utils.Commit=$(COMMIT_HASH)'"
5 |
6 | .PHONY: test build coverage clean lint
7 |
8 | all: test coverage build
9 |
10 | test:
11 | @go fmt
12 | @go test ./... -v
13 |
14 | coverage:
15 | @go test -coverprofile cover.out ./...
16 |
17 | build:
18 | go build $(LDFLAGS) -v
19 |
20 | clean:
21 | go clean
22 |
23 | lint:
24 | golangci-lint run --timeout 5m
25 |
--------------------------------------------------------------------------------
/manager/executor/executor.go:
--------------------------------------------------------------------------------
1 | package executor
2 |
3 | import (
4 | "context"
5 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
6 | "github.com/dsrvlabs/vatz/manager/config"
7 | dp "github.com/dsrvlabs/vatz/manager/dispatcher"
8 | "sync"
9 | )
10 |
11 | // Executor provides interfaces to execute plugin features.
12 | type Executor interface {
13 | Execute(ctx context.Context, gClient pluginpb.PluginClient, plugin config.Plugin, dispatchers []dp.Dispatcher) error
14 | }
15 |
16 | // NewExecutor create new executor instance.
17 | func NewExecutor() Executor {
18 | return &executor{
19 | status: sync.Map{},
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/ci_cd_implementation.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: CI/CD implementation
3 | about: Create/enhance any feature that boosts Vatz Devops Process
4 | title: ''
5 | labels: Vatz, area:ci/cd
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### **What does it require to on Vatz**
11 | > A clear and concise description of what you expected to happen through Ci/CD
12 |
13 |
14 | ### **Expected behavior**
15 | > A clear and concise description of results after CI/CD implementation.
16 |
17 |
18 | ### **How to run**
19 | > Please, Update and give simple guide to run CI/CD process with this issue.
20 |
21 |
22 | ### **Screenshots**
23 | > If applicable, add screenshots to help explain your problem
24 |
25 |
--------------------------------------------------------------------------------
/sdk/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/dsrvlabs/vatz/sdk
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/dsrvlabs/vatz-proto v0.0.0-20220420191920-c7decada518f
7 | github.com/rs/zerolog v1.26.1
8 | github.com/stretchr/testify v1.7.1
9 | golang.org/x/net v0.23.0
10 | google.golang.org/grpc v1.45.0
11 | google.golang.org/protobuf v1.27.1
12 | )
13 |
14 | require (
15 | github.com/davecgh/go-spew v1.1.1 // indirect
16 | github.com/golang/protobuf v1.5.2 // indirect
17 | github.com/pmezard/go-difflib v1.0.0 // indirect
18 | github.com/stretchr/objx v0.3.0 // indirect
19 | golang.org/x/sys v0.18.0 // indirect
20 | golang.org/x/text v0.14.0 // indirect
21 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
22 | gopkg.in/yaml.v3 v3.0.0 // indirect
23 | )
24 |
--------------------------------------------------------------------------------
/.github/workflows/ci_coverage.yml:
--------------------------------------------------------------------------------
1 | name: Coverage Test
2 |
3 | on:
4 | pull_request_target:
5 | types: [opened, synchronize]
6 | push:
7 | branches:
8 | - main
9 |
10 | jobs:
11 | coverage:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v3
17 | - name: Set up Python
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: 3.11.5
21 | - name: Build Code
22 | run: |
23 | make build
24 | - name: Run coverage test
25 | run: |
26 | make coverage
27 | - name: Deny PR on test failure
28 | if: failure()
29 | run: |
30 | echo "Coverage test failed. This PR cannot be merged."
31 | exit 1
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report/Fix
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: Vatz, type:bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Checklist
11 | - [ ] Bug from Service (Vatz)
12 | - [ ] Bug from Plugin (Vatz)
13 | - [ ] Malfunction on CI/CD
14 | - [ ] etc(e.g, Hardware version doesn't match, )
15 |
16 | ---
17 |
18 | ### **Describe the bug**
19 | > A clear and concise description of what the bug is.
20 |
21 |
22 | ### **How to Reproduce**
23 | > Steps to reproduce the behavior:
24 |
25 | 1. Go to '...'
26 | 2. '...'
27 |
28 |
29 | ### **Expected behavior**
30 | > A clear and concise description of what you expected to happen.
31 |
32 |
33 |
34 | ### **Screenshots**
35 | > If applicable, add screenshots to help explain your problem.
36 |
37 |
--------------------------------------------------------------------------------
/utils/monitoring.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/url"
7 | "os/exec"
8 | )
9 |
10 | func DownloadFileWithWgetOrCurl(url string) ([]byte, error) {
11 | // First, try to download using wget
12 | cmd := exec.Command("wget", "-qO-", url)
13 | var out bytes.Buffer
14 | cmd.Stdout = &out
15 | err := cmd.Run()
16 | if err == nil {
17 | return out.Bytes(), nil
18 | }
19 |
20 | // If wget fails, try using curl
21 | cmd = exec.Command("curl", "-s", url)
22 | out.Reset()
23 | cmd.Stdout = &out
24 | err = cmd.Run()
25 | if err != nil {
26 | return nil, fmt.Errorf("failed to download file using wget or curl: %v", err)
27 | }
28 |
29 | return out.Bytes(), nil
30 | }
31 |
32 | func IsURL(str string) bool {
33 | u, err := url.Parse(str)
34 | return err == nil && u.Scheme != "" && u.Host != ""
35 | }
36 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/dsrvlabs/vatz/cmd"
5 | "github.com/dsrvlabs/vatz/utils"
6 | "github.com/rs/zerolog/log"
7 | "github.com/spf13/cobra"
8 | "strings"
9 | )
10 |
11 | var rootCmd *cobra.Command
12 |
13 | func init() {
14 | //Set to Log level to Info which reduce log that doesn't be recorded and save log volumes.
15 | utils.InitializeLogger()
16 | rootCmd = cmd.GetRootCommand()
17 | }
18 |
19 | func main() {
20 | if err := rootCmd.Execute(); err != nil {
21 | if strings.Contains(err.Error(), "open default.yaml") {
22 | msg := "Please, Check config file default.yaml path or initialize VATZ with command `vatz init` to create config file `default.yaml`."
23 | log.Error().Str("module", "config").Msg(msg)
24 | } else {
25 | log.Error().Msgf("VATZ CLI command Error: %s", err)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/monitoring/prometheus/metric.go:
--------------------------------------------------------------------------------
1 | package prometheus
2 |
3 | import (
4 | "context"
5 | "github.com/dsrvlabs/vatz/utils"
6 | "google.golang.org/protobuf/types/known/emptypb"
7 | )
8 |
9 | func (c *prometheusManager) getPluginUp(hostName string, grpcClientAndPluginInfo []utils.GClientWithPlugin) (pluginUp map[int]*prometheusValue) {
10 | pluginUp = make(map[int]*prometheusValue)
11 |
12 | for _, info := range grpcClientAndPluginInfo {
13 | pluginInfo := info.PluginInfo
14 | pluginUp[pluginInfo.Port] = &prometheusValue{
15 | Up: 1,
16 | PluginName: pluginInfo.Name,
17 | HostName: hostName,
18 | }
19 | grpcClient := info.GRPCClient
20 | verify, err := grpcClient.Verify(context.Background(), new(emptypb.Empty))
21 | if err != nil || verify == nil {
22 | pluginUp[pluginInfo.Port].Up = 0
23 | }
24 | }
25 |
26 | return
27 | }
28 |
--------------------------------------------------------------------------------
/cmd/plugin_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "os"
5 | "testing"
6 |
7 | "github.com/dsrvlabs/vatz/manager/config"
8 | "github.com/spf13/cobra"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestPluginInstall(t *testing.T) {
13 | defer os.Remove("cosmos-status")
14 | defer os.Remove("./vatz-test.db")
15 |
16 | _, err := config.InitConfig("../default.yaml")
17 | assert.Nil(t, err)
18 |
19 | cfg := config.GetConfig()
20 | cfg.Vatz.HomePath = os.Getenv("PWD")
21 | // pluginDir = os.Getenv("PWD")
22 |
23 | root := cobra.Command{}
24 | root.AddCommand(createPluginCommand())
25 | root.SetArgs([]string{
26 | "plugin",
27 | "install",
28 | "github.com/dsrvlabs/vatz-plugin-cosmoshub/plugins/node_active_status",
29 | "cosmos-status"})
30 |
31 | err = root.Execute()
32 | assert.Nil(t, err)
33 | }
34 |
35 | // TODO: Test Start.
36 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Start from the official Golang image based on Debian
2 | FROM golang:1.20-buster as builder
3 |
4 | # Set the Current Working Directory inside the container
5 | WORKDIR /app/vatz
6 |
7 | # Copy the entire project into the container
8 | COPY . .
9 |
10 | # Run the Makefile to build the application
11 | RUN make build
12 |
13 | # Run vatz init to initialize the application (this should generate the binary)
14 | RUN ./vatz init
15 |
16 | # Start a new stage from scratch
17 | FROM alpine:latest
18 |
19 | WORKDIR /root/
20 |
21 | # Copy the Pre-built binary file from the previous stage
22 | COPY --from=builder /app/vatz/vatz ./vatz
23 |
24 | # Expose port 8080 (or whatever port your application uses)
25 | EXPOSE 8080
26 |
27 | # Command to start the application, expecting a config.yaml file to be provided at runtime
28 | CMD ["./vatz", "start", "--config", "/config/config.yaml"]
--------------------------------------------------------------------------------
/monitoring/prometheus/metric_test.go:
--------------------------------------------------------------------------------
1 | package prometheus
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/dsrvlabs/vatz/utils"
8 | )
9 |
10 | func Test_prometheusManager_getPluginUp(t *testing.T) {
11 | type fields struct {
12 | Protocol string
13 | }
14 | type args struct {
15 | hostName string
16 | }
17 | var tests []struct {
18 | name string
19 | fields fields
20 | args args
21 | wantPluginUp map[int]*prometheusValue
22 | }
23 | var grpcClientWithPlugins []utils.GClientWithPlugin
24 | for _, tt := range tests {
25 | t.Run(tt.name, func(t *testing.T) {
26 | c := &prometheusManager{
27 | Protocol: tt.fields.Protocol,
28 | }
29 | if gotPluginUp := c.getPluginUp(tt.args.hostName, grpcClientWithPlugins); !reflect.DeepEqual(gotPluginUp, tt.wantPluginUp) {
30 | t.Errorf("getPluginUp() = %v, want %v", gotPluginUp, tt.wantPluginUp)
31 | }
32 | })
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest any idea for Vatz project
4 | title: ''
5 | labels: Vatz, type:feature-request
6 | assignees: ''
7 | ---
8 |
9 | ## Checklist
10 | - [ ] New Feature for the SVC/Plugin
11 | - [ ] New Feature for CI/CD
12 | - [ ] Enhancement on Vatz
13 | - [ ] Others(etc. e.g, documentation,...)
14 | ---
15 |
16 | ### **Please describe, what it's about or related Problem with**
17 | > A clear description of what feature need to be developed or enhanced in Vatz
18 |
19 |
20 |
21 | ### **Describe the output that you are expecting for above**
22 | > A clear and concise description of what you expect to happen.
23 |
24 |
25 |
26 | ### **Describe alternatives you've considered**
27 | > A clear and concise description of any alternative solutions or features you've considered.
28 |
29 |
30 |
31 | ### **Additional context**
32 | > Add any other context or screenshots about the feature request here.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_development.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Development
3 | about: Specify feature is going to be developed & through certain request.
4 | title: ''
5 | labels: Vatz, type:feature-development
6 | assignees: ''
7 | ---
8 |
9 | ## Checklist
10 |
11 | - [ ] New Feature for the Service/Plugin (Vatz)
12 | - [ ] Enhancement (Vatz)
13 | - [ ] others(etc. e.g, documentation, project policy management)
14 |
15 | ---
16 | ### **Is your feature request related to a problem? Please describe.**
17 | > A clear and concise description of what the problem is. such as Ex. I'm always frustrated
18 |
19 |
20 | ### **Describe the solution you'd like| Ex**
21 | > A clear and concise description of what you want to happen
22 |
23 |
24 | ### **Describe alternatives you've considered**
25 | > A clear and concise description of any alternative solutions or features you have considered.
26 |
27 |
28 | ### **Additional context or comment**
29 | > Add any other context or screenshots about the feature request here.
--------------------------------------------------------------------------------
/manager/executor/mock_PluginClient.go:
--------------------------------------------------------------------------------
1 | package executor
2 |
3 | import (
4 | "context"
5 |
6 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
7 | "github.com/stretchr/testify/mock"
8 | "google.golang.org/grpc"
9 | "google.golang.org/protobuf/types/known/emptypb"
10 | )
11 |
12 | type mockPluginClient struct {
13 | mock.Mock
14 | }
15 |
16 | func (c *mockPluginClient) Verify(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pluginpb.VerifyInfo, error) {
17 | // TODO: Not using yet.
18 | return nil, nil
19 | }
20 |
21 | func (c *mockPluginClient) Execute(ctx context.Context, in *pluginpb.ExecuteRequest, opts ...grpc.CallOption) (*pluginpb.ExecuteResponse, error) {
22 | ret := c.Called(ctx, in, opts)
23 |
24 | var r0 *pluginpb.ExecuteResponse
25 | var r1 error
26 | if rf, ok := ret.Get(0).(func(context.Context, *pluginpb.ExecuteRequest, []grpc.CallOption) (*pluginpb.ExecuteResponse, error)); ok {
27 | r0, r1 = rf(ctx, in, opts)
28 | } else {
29 | r0 = ret.Get(0).(*pluginpb.ExecuteResponse)
30 | r1 = ret.Error(1)
31 | }
32 | return r0, r1
33 | }
34 |
--------------------------------------------------------------------------------
/rpc/grpc.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "context"
5 |
6 | pb "github.com/dsrvlabs/vatz-proto/rpc/v1"
7 | emptypb "google.golang.org/protobuf/types/known/emptypb"
8 |
9 | "github.com/dsrvlabs/vatz/manager/healthcheck"
10 | tp "github.com/dsrvlabs/vatz/types"
11 | )
12 |
13 | type grpcService struct {
14 | pb.UnimplementedVatzRPCServer
15 |
16 | healthChecker healthcheck.HealthCheck
17 | }
18 |
19 | func (s *grpcService) PluginStatus(ctx context.Context, in *emptypb.Empty) (*pb.PluginStatusResponse, error) {
20 | pluginStatus := s.healthChecker.PluginStatus(ctx)
21 |
22 | respStatus := pb.PluginStatusResponse{
23 | Status: pb.Status_OK,
24 | PluginStatus: make([]*pb.PluginStatus, len(pluginStatus)),
25 | }
26 |
27 | for i, status := range pluginStatus {
28 | newStatus := pb.PluginStatus{PluginName: status.Plugin.Name}
29 |
30 | if status.IsAlive == tp.AliveStatusUp {
31 | newStatus.Status = pb.Status_OK
32 | } else {
33 | newStatus.Status = pb.Status_FAIL
34 | }
35 |
36 | respStatus.PluginStatus[i] = &newStatus
37 | }
38 |
39 | return &respStatus, nil
40 | }
41 |
--------------------------------------------------------------------------------
/cmd/cmd_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | tp "github.com/dsrvlabs/vatz/types"
5 | "os"
6 | "testing"
7 |
8 | "github.com/spf13/cobra"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestInitCmd(t *testing.T) {
13 | tests := []struct {
14 | Desc string
15 | Args []string
16 | ExpectFile string
17 | }{
18 | // TODO: Testing default config could corrupt real environment.
19 | // So, remove it intensionally.
20 | //{
21 | // Desc: "Init with default",
22 | // Args: []string{"init"},
23 | // ExpectFile: "default.yaml",
24 | //},
25 | {
26 | Desc: "Init with selected filename",
27 | Args: []string{"init", "--output", "hello.yaml", "--home", "./"},
28 | ExpectFile: "hello.yaml",
29 | },
30 | }
31 |
32 | for _, test := range tests {
33 | root := cobra.Command{}
34 | root.AddCommand(createInitCommand(tp.TEST))
35 | root.SetArgs(test.Args)
36 |
37 | err := root.Execute()
38 | defer os.Remove(test.ExpectFile)
39 |
40 | assert.Nil(t, err)
41 |
42 | _, err = os.ReadFile(test.ExpectFile)
43 | assert.Nil(t, err)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/manager/api/api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 |
6 | managerpb "github.com/dsrvlabs/vatz-proto/manager/v1"
7 | )
8 |
9 | var (
10 | ExecutableRPC GrpcService
11 | )
12 |
13 | type GrpcService struct {
14 | managerpb.UnimplementedManagerServer
15 | }
16 |
17 | func (s *GrpcService) Execute(ctx context.Context, in *managerpb.ExecuteRequest) (*managerpb.ExecuteResponse, error) {
18 | return nil, nil
19 | }
20 |
21 | func (s *GrpcService) Init(ctx context.Context, in *managerpb.InitRequest) (*managerpb.InitResponse, error) {
22 |
23 | return &managerpb.InitResponse{Result: managerpb.CommandStatus_SUCCESS}, nil
24 | }
25 |
26 | func (s *GrpcService) End(ctx context.Context, in *managerpb.EndRequest) (*managerpb.EndResponse, error) {
27 | return &managerpb.EndResponse{Result: managerpb.CommandStatus_SUCCESS}, nil
28 | }
29 |
30 | func (s *GrpcService) Verify(ctx context.Context, in *managerpb.VerifyRequest) (*managerpb.VerifyInfo, error) {
31 | return nil, nil
32 | }
33 |
34 | func (s *GrpcService) UpdateConfig(ctx context.Context, in *managerpb.UpdateRequest) (*managerpb.UpdateResponse, error) {
35 | return nil, nil
36 | }
37 |
--------------------------------------------------------------------------------
/utils/cmd.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | tp "github.com/dsrvlabs/vatz/types"
7 | "github.com/rs/zerolog/log"
8 | "io"
9 | "net/http"
10 | )
11 |
12 | func GetPluginStatus(vatzRPC string) (tp.PluginState, error) {
13 | var newPluginStatus tp.PluginState
14 | statusRequest := fmt.Sprintf("%s/v1/plugin_status", vatzRPC)
15 |
16 | req, err := http.NewRequest(http.MethodGet, statusRequest, nil)
17 | if err != nil {
18 | return newPluginStatus, err
19 | }
20 |
21 | cli := http.Client{}
22 | resp, err := cli.Do(req)
23 | if err != nil {
24 | log.Error().Str("module", "plugin").Err(err)
25 | return newPluginStatus, err
26 | }
27 |
28 | log.Debug().Str("module", "plugin").Msgf("Plugin(s) status is requested to %s.", statusRequest)
29 |
30 | respData, err := io.ReadAll(resp.Body)
31 | if err != nil {
32 | log.Error().Str("module", "plugin").Err(err)
33 | return newPluginStatus, err
34 | }
35 |
36 | err = json.Unmarshal(respData, &newPluginStatus)
37 | if err != nil {
38 | log.Error().Str("module", "plugin").Err(err)
39 | return newPluginStatus, err
40 | }
41 |
42 | return newPluginStatus, nil
43 | }
44 |
--------------------------------------------------------------------------------
/.github/workflows/golangci-lint.yml:
--------------------------------------------------------------------------------
1 | name: Golang Linter
2 | # Lint runs golangci-lint over the entire vatz repository.
3 | #
4 | # This workflow is run on every pull request and push to main.
5 | #
6 | # The `golangci` job will pass without running if no *.{go, mod, sum}
7 | # files have been modified.
8 | #
9 | # To run this locally, simply run `golangci-lint run` from the root of the repo.
10 |
11 | on:
12 | pull_request:
13 | push:
14 | branches:
15 | - main
16 | jobs:
17 | golangci:
18 | name: golangci-lint
19 | runs-on: ubuntu-latest
20 | timeout-minutes: 8
21 | steps:
22 | - uses: actions/checkout@v3
23 | - uses: actions/setup-go@v3
24 | with:
25 | go-version: '1.18'
26 | - uses: technote-space/get-diff-action@v6
27 | with:
28 | PATTERNS: |
29 | **/**.go
30 | go.mod
31 | go.sum
32 | - uses: golangci/golangci-lint-action@v3
33 | with:
34 | # Required: the version of golangci-lint is required and
35 | # must be specified without patch version: we always use the
36 | # latest patch version.
37 | version: v1.49.0
38 | args: --timeout 10m
39 | if: env.GIT_DIFF
40 |
--------------------------------------------------------------------------------
/manager/healthcheck/healthcheck.go:
--------------------------------------------------------------------------------
1 | package healthcheck
2 |
3 | import (
4 | "context"
5 | "github.com/dsrvlabs/vatz/types"
6 | "sync"
7 |
8 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
9 | "github.com/dsrvlabs/vatz/manager/config"
10 | dp "github.com/dsrvlabs/vatz/manager/dispatcher"
11 | )
12 |
13 | // HealthCheck provides interfaces to check health.
14 | type HealthCheck interface {
15 | PluginHealthCheck(ctx context.Context, gClient pluginpb.PluginClient, plugin config.Plugin, dispatcher []dp.Dispatcher) (types.AliveStatus, error)
16 | VATZHealthCheck(schedule []string, dispatcher []dp.Dispatcher) error
17 | PluginStatus(ctx context.Context) []types.PluginStatus
18 | }
19 |
20 | // GetHealthChecker creates instance of HealthCheck
21 | func GetHealthChecker() HealthCheck {
22 | healthCheckerOnce.Do(func() {
23 | option := map[string]interface{}{"pUnique": "VATZHealthChecker"}
24 | healthCheckerSingle = healthChecker{
25 | healthMSG: types.ReqMsg{
26 | FuncName: "VATZHealthCheck",
27 | State: pluginpb.STATE_SUCCESS,
28 | Msg: "VATZ is Alive!!",
29 | Severity: pluginpb.SEVERITY_INFO,
30 | ResourceType: "VATZ",
31 | Options: option,
32 | },
33 | pluginStatus: sync.Map{},
34 | }
35 | })
36 |
37 | return &healthCheckerSingle
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/ci_pr_notifier.yaml:
--------------------------------------------------------------------------------
1 | name: New PR Notifications
2 | on:
3 | pull_request_target:
4 | types: [opened]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Get pull request details
10 | id: pr
11 | run: |
12 | curl -s \
13 | -H "Accept: application/vnd.github+json" \
14 | -H "X-GitHub-Api-Version: 2022-11-28" \
15 | https://api.github.com/repos/dsrvlabs/vatz/pulls/${{ github.event.number }} >> new_pull.json
16 | echo -e "\n\n"
17 | echo $(PRS=cat new_pull.json)
18 | echo $PRS
19 | echo PR_TITLE=$(cat new_pull.json | jq -r '.title') >> $GITHUB_OUTPUT
20 | echo PR_URL=$(cat new_pull.json | jq -r '.html_url') >> $GITHUB_OUTPUT
21 | echo PR_USER=$(cat new_pull.json | jq -r '.user.login') >> $GITHUB_OUTPUT
22 | - name: Notify
23 | uses: Ilshidur/action-discord@master
24 | env:
25 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
26 | with:
27 | args: "\n🚀A new Pull Request has just opened by ${{ steps.pr.outputs.PR_USER }} at VATZ🚀:
28 | \n--------------------------------------------------------------
29 | \n- [${{ steps.pr.outputs.PR_TITLE }}](${{ steps.pr.outputs.PR_URL }})
30 | \n\n‼️ Please, Check & Review a new PR."
--------------------------------------------------------------------------------
/manager/dispatcher/mock_Dispatcher.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.14.0. DO NOT EDIT.
2 |
3 | package dispatcher
4 |
5 | import (
6 | "github.com/dsrvlabs/vatz/types"
7 | mock "github.com/stretchr/testify/mock"
8 | )
9 |
10 | // Dispatcher is an autogenerated mock type for the Dispatcher type
11 | type MockDispatcher struct {
12 | mock.Mock
13 | }
14 |
15 | // SendNotification provides a mock function with given fields: request
16 | func (_m *MockDispatcher) SendNotification(request types.ReqMsg) error {
17 | ret := _m.Called(request)
18 |
19 | var r0 error
20 | if rf, ok := ret.Get(0).(func(types.ReqMsg) error); ok {
21 | r0 = rf(request)
22 | } else {
23 | r0 = ret.Error(0)
24 | }
25 |
26 | return r0
27 | }
28 |
29 | type mockConstructorTestingTNewDispatcher interface {
30 | mock.TestingT
31 | Cleanup(func())
32 | }
33 |
34 | // NewDispatcher creates a new instance of Dispatcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
35 | func NewMockDispatcher(t mockConstructorTestingTNewDispatcher) *MockDispatcher {
36 | mock := &MockDispatcher{}
37 | mock.Mock.Test(t)
38 |
39 | t.Cleanup(func() { mock.AssertExpectations(t) })
40 |
41 | return mock
42 | }
43 |
44 | func NewMockDispatchers(t mockConstructorTestingTNewDispatcher) []MockDispatcher {
45 | mock := &MockDispatcher{}
46 | mock.Mock.Test(t)
47 |
48 | t.Cleanup(func() { mock.AssertExpectations(t) })
49 |
50 | return []MockDispatcher{}
51 | }
52 |
--------------------------------------------------------------------------------
/mocks/mock_PluginClient.go:
--------------------------------------------------------------------------------
1 | package mocks
2 |
3 | import (
4 | "context"
5 |
6 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
7 | "github.com/stretchr/testify/mock"
8 | "google.golang.org/grpc"
9 | "google.golang.org/protobuf/types/known/emptypb"
10 | )
11 |
12 | // MockPluginClient is mock object for grpc client of VATZ.
13 | type MockPluginClient struct {
14 | mock.Mock
15 | }
16 |
17 | func (c *MockPluginClient) Verify(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pluginpb.VerifyInfo, error) {
18 | // TODO: Not using yet.
19 | ret := c.Called(ctx, in, opts)
20 |
21 | var r0 *pluginpb.VerifyInfo
22 | var r1 error
23 |
24 | if rf, ok := ret.Get(0).(func(context.Context, *emptypb.Empty, []grpc.CallOption) (*pluginpb.VerifyInfo, error)); ok {
25 | r0, r1 = rf(ctx, in, opts)
26 | } else {
27 | r0 = ret.Get(0).(*pluginpb.VerifyInfo)
28 | r1 = ret.Error(1)
29 | }
30 |
31 | return r0, r1
32 | }
33 |
34 | func (c *MockPluginClient) Execute(ctx context.Context, in *pluginpb.ExecuteRequest, opts ...grpc.CallOption) (*pluginpb.ExecuteResponse, error) {
35 | ret := c.Called(ctx, in, opts)
36 |
37 | var r0 *pluginpb.ExecuteResponse
38 | var r1 error
39 | if rf, ok := ret.Get(0).(func(context.Context, *pluginpb.ExecuteRequest, []grpc.CallOption) (*pluginpb.ExecuteResponse, error)); ok {
40 | r0, r1 = rf(ctx, in, opts)
41 | } else {
42 | r0 = ret.Get(0).(*pluginpb.ExecuteResponse)
43 | r1 = ret.Error(1)
44 | }
45 | return r0, r1
46 | }
47 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/dsrvlabs/vatz/utils"
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | func createVersionCommand() *cobra.Command {
11 | cmd := &cobra.Command{
12 | Use: "version",
13 | Short: "VATZ Version",
14 | RunE: func(cmd *cobra.Command, args []string) error {
15 | verStr := utils.GetVersion()
16 | fmt.Println(" \n @@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@ \n #@@@@@@@@@@@@@@@ @@@@@@@@@@@@@. \n @@@@@@@@@@@@@@@ &@@@@@@@ \n @@@@@@@@@@@@@@@& ,@@@@@. \n @@@@@@@@@@@@@@@ ,@@@@ \n @@@@@@@@@@@@@@@( ,@@/ \n .@@@@@@@@@@@@@@@ ,@ \n @@@@@@@@@@@@@@@. . \n *@@@@@@@@@@@@@@@ \n @@@@@@@@@@@@@@@. \n &@@@@@@@@@@@@@@ \n @@@@@@@@@@@ \n @@@@@@@@% \n @@@@@ \n @@* VATZ dsrv labs Co., Ltd. ")
17 | fmt.Println("")
18 | fmt.Println("VATZ Version: " + verStr)
19 | return nil
20 | },
21 | }
22 |
23 | return cmd
24 | }
25 |
--------------------------------------------------------------------------------
/script/simple_start_guide/install_vatz&official-plugins.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | set -v
4 |
5 | DEFAULT_LOG_PATH=/var/log/vatz
6 | DEFAULT_VATZ_PATH=/root/vatz
7 |
8 | # Create vatz log folder
9 | ## Check if $LOG_PATH exists
10 | if [ -d "$DEFAULT_LOG_PATH" ]; then
11 | echo "$DEFAULT_LOG_PATH already exists. Skipping creation."
12 | else
13 | ## Create $LOG_PATH if it doesn't exist
14 | mkdir $DEFAULT_LOG_PATH
15 | fi
16 |
17 | # Compile VATZ
18 | cd $DEFAULT_VATZ_PATH
19 | make
20 |
21 | ## You will see binary named vatz
22 |
23 | # Initialize VATZ
24 | ./vatz init
25 |
26 | # Install vatz-plugin-sysutil
27 | ./vatz plugin install github.com/dsrvlabs/vatz-plugin-sysutil/plugins/cpu_monitor cpu_monitor
28 | ./vatz plugin install github.com/dsrvlabs/vatz-plugin-sysutil/plugins/mem_monitor mem_monitor
29 | ./vatz plugin install github.com/dsrvlabs/vatz-plugin-sysutil/plugins/disk_monitor disk_monitor
30 |
31 | # Install vatz-plugin-cosmoshub
32 | ./vatz plugin install github.com/dsrvlabs/vatz-plugin-cosmoshub/plugins/node_block_sync node_block_sync
33 | ./vatz plugin install github.com/dsrvlabs/vatz-plugin-cosmoshub/plugins/node_is_alived node_is_alived
34 | ./vatz plugin install github.com/dsrvlabs/vatz-plugin-cosmoshub/plugins/node_peer_count node_peer_count
35 | ./vatz plugin install github.com/dsrvlabs/vatz-plugin-cosmoshub/plugins/node_active_status node_active_status
36 | ./vatz plugin install github.com/dsrvlabs/vatz-plugin-cosmoshub/plugins/node_governance_alarm node_governance_alarm
37 |
38 | # Check plugin list
39 | ./vatz plugin list
40 |
--------------------------------------------------------------------------------
/script/simple_start_guide/vatz_start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | set -v
4 |
5 | VATZ_PATH=/root/vatz
6 | LOG_PATH=/var/log/vatz
7 |
8 | CPU_MONITOR_PORT=9001
9 | MEM_MONITOR_PORT=9002
10 | DISK_MONITOR_PORT=9003
11 |
12 | NODE_BLOCK_SYNC_PORT=10001
13 | NODE_IS_ALIVE_PORT=10002
14 | NODE_PEER_COUNT_PORT=10003
15 | NODE_ACTIVE_STATUS_PORT=10004
16 | NODE_GOVERNANCE_ALARM_PORT=10005
17 | VALOPER_ADDRESS=valoper_address
18 | VOTER_ADDRESS=voter_address
19 |
20 |
21 | cd $VATZ_PATH
22 |
23 | ./vatz plugin start --plugin cpu_monitor --args "-port $CPU_MONITOR_PORT" --log $LOG_PATH/cpu_monitor.logs
24 | ./vatz plugin start --plugin mem_monitor --args "-port $MEM_MONITOR_PORT" --log $LOG_PATH/mem_monitor.logs
25 | ./vatz plugin start --plugin disk_monitor --args "-port $DISK_MONITOR_PORT" --log $LOG_PATH/disk_monitor.logs
26 |
27 | ./vatz plugin start --plugin node_block_sync --args "-port $NODE_BLOCK_SYNC_PORT" --log $LOG_PATH/node_block_sync.logs
28 | ./vatz plugin start --plugin node_is_alived --args "-port $NODE_IS_ALIVE_PORT" --log $LOG_PATH/node_is_alived.logs
29 | ./vatz plugin start --plugin node_peer_count --args "-port $NODE_PEER_COUNT_PORT" --log $LOG_PATH/node_peer_count.logs
30 | ./vatz plugin start --plugin node_active_status --args "-port $NODE_ACTIVE_STATUS_PORT -valoperAddr $VALOPER_ADDRESS" --log $LOG_PATH/node_active_status.logs
31 | ./vatz plugin start --plugin node_governance_alarm --args "-port $NODE_GOVERNANCE_ALARM_PORT -voterAddr $VOTER_ADDRESS" --log $LOG_PATH/node_governance_alarm.logs
32 |
33 | ./vatz start --config default.yaml >> /var/log/vatz/vatz.log 2>&1 &
34 |
35 | echo "true"
36 |
--------------------------------------------------------------------------------
/script/simple_start_guide/readme.md:
--------------------------------------------------------------------------------
1 | # How to install VATZ & Vatz Official plugins with scripts
2 | > Those following scripts helps user install VATZ in simple way to get started fast.
3 | > Please, be advised that you need to update several variable in scripts to match with your current setting such as cloned VATZ path.
4 |
5 | ## Here's simple instruction to get started
6 | ### 1. Go to script folder
7 | - You can simply setup and run VATZ with following scripts.
8 |
9 | ### 2. install_vatz&official-plugins.sh
10 | - Please, update to DEFAULT_VATZ_PATH to your vatz path and run this script to install vatz and vatz-official plugin.
11 | ```
12 | bash install_vatz&official-plugins.sh
13 | ```
14 |
15 | ### 3. default_config.yaml
16 | > Please, refer to [installation guide](../../docs/installation.md) for more detailed configs.
17 | - Replace default.yaml with this file after execute `install_vatz&official-plugins.sh`.
18 | - You must enter hostname and webhook or add more dispatchers such as telegram, pagerduty
19 | - Change the port if necessary.
20 | ```
21 | cp default_config.yaml //default.yaml
22 | ```
23 |
24 | ### 4. vatz_start.sh
25 | - Running this script will run vatz and vatz-plugin-sei.
26 | - The log is stored in `/var/log/vatz`.
27 | - You can change the log path if necessary.
28 | - Enter VALOPER_ADDRESS and VOTER_ADDRESS.
29 | - Modify home path to your current setting.
30 | ```
31 | bash vatz_start.sh
32 | ```
33 |
34 | ### 5. vatz_stop.sh
35 | - Running this script will stop both vatz and vatz-official-plugin overall that is currently running.
36 | ```
37 | bash vatz_stop.sh
38 | ```
--------------------------------------------------------------------------------
/utils/logs.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/rs/zerolog"
5 | "github.com/rs/zerolog/log"
6 | "os"
7 | "time"
8 | )
9 |
10 | var (
11 | cmdConsoleWriter = zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
12 | )
13 |
14 | func InitializeLogger() {
15 | SetGlobalLogLevel(zerolog.InfoLevel)
16 | log.Logger = log.Output(cmdConsoleWriter)
17 | }
18 |
19 | func SetGlobalLogLevel(level zerolog.Level) {
20 | /*
21 | zerolog allows for logging at the following levels (from highest to lowest):
22 | panic (zerolog.PanicLevel, 5)
23 | fatal (zerolog.FatalLevel, 4)
24 | error (zerolog.ErrorLevel, 3)
25 | warn (zerolog.WarnLevel, 2)
26 | info (zerolog.InfoLevel, 1)
27 | debug (zerolog.DebugLevel, 0)
28 | trace (zerolog.TraceLevel, -1)
29 | VATZ's global loglevel is Info, which hide debug and trace and ignores all other cases except
30 | Info, Debug, and Trace.
31 | */
32 | switch {
33 | case level == zerolog.DebugLevel:
34 | zerolog.SetGlobalLevel(zerolog.DebugLevel)
35 | case level == zerolog.TraceLevel:
36 | zerolog.SetGlobalLevel(zerolog.TraceLevel)
37 | default:
38 | zerolog.SetGlobalLevel(zerolog.InfoLevel)
39 | }
40 | }
41 |
42 | func SetLog(logfile string, defaultFlagLog string) error {
43 | if logfile == defaultFlagLog {
44 | log.Logger = log.Output(cmdConsoleWriter)
45 | } else {
46 | f, err := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
47 | if err != nil {
48 | return err
49 | }
50 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: f, TimeFormat: time.RFC3339})
51 | }
52 | return nil
53 | }
54 |
--------------------------------------------------------------------------------
/.github/workflows/ci_reamin_pr_reminder.yaml:
--------------------------------------------------------------------------------
1 | name: PR Weekdays Reminder in [open,reopened] status.
2 | on:
3 | schedule:
4 | - cron: "0 1,14 * * 1-5"
5 | workflow_dispatch:
6 | env:
7 | NUM_OF_PRS: 0
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Get list of open or reopened pull requests from github
13 | id: prs
14 | run: |
15 | curl -s \
16 | https://api.github.com/repos/dsrvlabs/vatz/pulls | jq -r '[.[] | select(.state == "open" or .state == "reopened")]' >> pullRequest.json
17 | echo -e "\n\n"
18 | echo "NUM_OF_PRS=$(cat pullRequest.json | jq -r 'length')" >> "$GITHUB_ENV"
19 | - name: Get list of open or reopened pull requests
20 | id: contents
21 | if: env.NUM_OF_PRS != 0
22 | run: |
23 | cat pullRequest.json | jq -r 'map("- [\(.title)](\(.html_url))") | join("\n")' >> list
24 | PR_LIST=$(cat list)
25 | echo -e "text<> $GITHUB_OUTPUT
26 | - name: Notify
27 | if: env.NUM_OF_PRS != 0
28 | uses: Ilshidur/action-discord@master
29 | env:
30 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
31 | with:
32 | args: "There are currently ${{ env.NUM_OF_PRS }} open or reopened pull requests at VATZ :
33 | \n==================================================================================\n
34 | ${{join(steps.contents.outputs.*, '\n')}}
35 | \n==================================================================================\n
36 | \n‼️ Please, Review and Close Pull Request."
37 |
--------------------------------------------------------------------------------
/utils/utils_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
6 | "github.com/dsrvlabs/vatz/manager/config"
7 | "github.com/stretchr/testify/assert"
8 | "syscall"
9 | "testing"
10 | "time"
11 | )
12 |
13 | func TestGetClients(t *testing.T) {
14 | type args struct {
15 | plugins []config.Plugin
16 | }
17 | var tests []struct {
18 | name string
19 | args args
20 | want []pluginpb.PluginClient
21 | }
22 | for _, tt := range tests {
23 | t.Run(tt.name, func(t *testing.T) {
24 | assert.Equalf(t, tt.want, GetClients(tt.args.plugins), "GetClients(%v)", tt.args.plugins)
25 | })
26 | }
27 | }
28 |
29 | func TestMakeUniqueValue(t *testing.T) {
30 | var testUnique1 = MakeUniqueValue("aa", "bb", 8080)
31 | var testUnique2 = MakeUniqueValue("GetCPU", "localhost", 9090)
32 | var testUnique3 = MakeUniqueValue("GetCPU", "128.97.26.11", 9090)
33 |
34 | assert.Equal(t, "aabb8080", testUnique1)
35 | assert.Equal(t, "GetCPUlocalhost9090", testUnique2)
36 | assert.Equal(t, "GetCPU128.97.26.119090", testUnique3)
37 | }
38 |
39 | func TestInitializeChannel(t *testing.T) {
40 | sigs := InitializeChannel()
41 | done := make(chan bool)
42 | go func() {
43 | time.Sleep(time.Millisecond * 100) // Wait a bit before sending the signal
44 | if err := syscall.Kill(syscall.Getpid(), syscall.SIGINT); err != nil {
45 | fmt.Printf("Failed to kill process: %v\n", err)
46 | }
47 | }()
48 |
49 | select {
50 | case <-sigs:
51 | assert.True(t, true, "Signal received as expected")
52 | case <-time.After(time.Second):
53 | assert.Fail(t, "Expected to receive signal, but did not")
54 | case <-done:
55 | // This case ensures the goroutine has completed its execution
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/docs/workflow.md:
--------------------------------------------------------------------------------
1 | # VATZ workflow
2 |
3 | ## 1. Contribution guide.
4 | Please adhere to the guidelines outlined in the [Contribution guide](contributing.md), if you have a pull request (PR) that you wish to contribute
5 |
6 | ## 2. Pull request
7 | Upon submission of the pull request (PR), it is imperative to successfully complete two preliminary steps before the review process commences.
8 |
9 |
10 |
11 | ### 2.1 Go lint test
12 |
13 |
14 | ### 2.2 Coverage test
15 |
16 |
17 | ### 2.3 Notifications
18 | VATZ maintainers will receive immediately after you have submitted the pull request.
19 |
20 |
21 |
22 | ## 3. Review
23 | > The maintainers will conduct a review of the code in the submitted pull request. It is required to obtain approval from at least two reviewers.
24 | > Please, ensure that you select to all of the following criteria
25 | > - Reviewers
26 | > - Assignees
27 | > - Project (You can leave it blank if you don't know)
28 | > - Development (issue that associate with raised PR)
29 |
30 |
31 |
32 | ## 4. Merge
33 | Pull request will be merged and subsequently closed upon receiving the requisite number of approvals from the reviewers.
34 |
35 | > Example
36 | >
37 | >
--------------------------------------------------------------------------------
/script/simple_start_guide/vatz_stop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | set -v
4 |
5 | pid0=`pidof cpu_monitor`
6 | pid1=`pidof mem_monitor`
7 | pid2=`pidof disk_monitor`
8 | pid3=`pidof node_block_sync`
9 | pid4=`pidof node_is_alived`
10 | pid5=`pidof node_peer_count`
11 | pid6=`pidof node_active_status`
12 | pid7=`pidof node_governance_alarm`
13 | pid8=`pidof vatz`
14 |
15 | # Kill process
16 |
17 | kill -15 $pid6
18 |
19 | # wait Kill process
20 | sleep 1
21 |
22 | while (( `lsof -p $pid0 | wc -l` > 0 ))
23 | do
24 | echo `lsof -p $pid0 | wc -l`
25 | sleep 1
26 | done
27 | while (( `lsof -p $pid1 | wc -l` > 0 ))
28 | do
29 | echo `lsof -p $pid1 | wc -l`
30 | sleep 1
31 | done
32 | while (( `lsof -p $pid2 | wc -l` > 0 ))
33 | do
34 | echo `lsof -p $pid2 | wc -l`
35 | sleep 1
36 | done
37 | while (( `lsof -p $pid3 | wc -l` > 0 ))
38 | do
39 | echo `lsof -p $pid3 | wc -l`
40 | sleep 1
41 | done
42 | while (( `lsof -p $pid4 | wc -l` > 0 ))
43 | do
44 | echo `lsof -p $pid4 | wc -l`
45 | sleep 1
46 | done
47 | while (( `lsof -p $pid5 | wc -l` > 0 ))
48 | do
49 | echo `lsof -p $pid5 | wc -l`
50 | sleep 1
51 | done
52 | while (( `lsof -p $pid6 | wc -l` > 0 ))
53 | do
54 | echo `lsof -p $pid6 | wc -l`
55 | sleep 1
56 | done
57 | while (( `lsof -p $pid7 | wc -l` > 0 ))
58 | do
59 | echo `lsof -p $pid7 | wc -l`
60 | sleep 1
61 | done
62 | while (( `lsof -p $pid8 | wc -l` > 0 ))
63 | do
64 | echo `lsof -p $pid8 | wc -l`
65 | sleep 1
66 | done
67 | echo "seid $pid0 is killed."
68 | echo "seid $pid1 is killed."
69 | echo "seid $pid2 is killed."
70 | echo "seid $pid3 is killed."
71 | echo "seid $pid4 is killed."
72 | echo "seid $pid5 is killed."
73 | echo "seid $pid6 is killed."
74 | echo "seid $pid7 is killed."
75 | echo "seid $pid8 is killed."
--------------------------------------------------------------------------------
/cmd/cmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | dp "github.com/dsrvlabs/vatz/manager/dispatcher"
5 | ex "github.com/dsrvlabs/vatz/manager/executor"
6 | health "github.com/dsrvlabs/vatz/manager/healthcheck"
7 | tp "github.com/dsrvlabs/vatz/types"
8 | "github.com/dsrvlabs/vatz/utils"
9 | "github.com/rs/zerolog"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | const (
14 | defaultFlagConfig = "default.yaml"
15 | defaultFlagLog = ""
16 | defaultRPC = "http://localhost:19091"
17 | defaultPromPort = "18080"
18 | defaultHomePath = "~/.vatz"
19 | )
20 |
21 | var (
22 | healthChecker = health.GetHealthChecker()
23 | executor = ex.NewExecutor()
24 | sigs = utils.InitializeChannel()
25 | dispatchers []dp.Dispatcher
26 | isDebugLevel bool
27 | isTraceLevel bool
28 | configFile string
29 | logfile string
30 | vatzRPC string
31 | promPort string
32 | )
33 |
34 | // GetRootCommand is Return Cobra Root command include all subcommands .
35 | func GetRootCommand() *cobra.Command {
36 | rootCmd := CreateRootCommand()
37 | rootCmd.AddCommand(createInitCommand(tp.LIVE))
38 | rootCmd.AddCommand(createStartCommand())
39 | rootCmd.AddCommand(createStopCommand())
40 | rootCmd.AddCommand(createPluginCommand())
41 | rootCmd.AddCommand(createVersionCommand())
42 | return rootCmd
43 | }
44 |
45 | // CreateRootCommand is Create Root command which initialize root command and global flags.
46 | func CreateRootCommand() *cobra.Command {
47 | rootCmd := &cobra.Command{PersistentPreRun: func(cmd *cobra.Command, args []string) {
48 | if isDebugLevel {
49 | utils.SetGlobalLogLevel(zerolog.DebugLevel)
50 | } else if isTraceLevel {
51 | utils.SetGlobalLogLevel(zerolog.TraceLevel)
52 | }
53 | }}
54 | rootCmd.PersistentFlags().BoolVarP(&isDebugLevel, "debug", "", false, "Enable debug mode on Log")
55 | rootCmd.PersistentFlags().BoolVarP(&isTraceLevel, "trace", "", false, "Enable trace mode on Log")
56 | return rootCmd
57 | }
58 |
--------------------------------------------------------------------------------
/sdk/grpc_test.go:
--------------------------------------------------------------------------------
1 | package sdk
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | "github.com/stretchr/testify/mock"
8 | structpb "google.golang.org/protobuf/types/known/structpb"
9 | )
10 |
11 | type mockFuncs struct {
12 | mock.Mock
13 | }
14 |
15 | func (m *mockFuncs) DummyCall1(info, option map[string]*structpb.Value) (CallResponse, error) {
16 | ret := m.Called(info, option)
17 |
18 | var r0 CallResponse
19 | if rf, ok := ret.Get(0).(func(map[string]*structpb.Value, map[string]*structpb.Value) CallResponse); ok {
20 | r0 = rf(info, option)
21 | } else {
22 | r0 = ret.Get(0).(CallResponse)
23 | }
24 |
25 | var r1 error
26 | if rf, ok := ret.Get(1).(func(map[string]*structpb.Value, map[string]*structpb.Value) error); ok {
27 | r1 = rf(info, option)
28 | } else {
29 | r1 = ret.Error(1)
30 | }
31 |
32 | return r0, r1
33 | }
34 |
35 | func TestRegister(t *testing.T) {
36 | tests := []struct {
37 | Funcs []func(in, opt map[string]*structpb.Value) (CallResponse, error)
38 | ExpectErr error
39 | }{
40 | {
41 | Funcs: []func(in, opt map[string]*structpb.Value) (CallResponse, error){
42 | callbackFunc,
43 | },
44 | ExpectErr: nil,
45 | },
46 |
47 | {
48 | Funcs: []func(in, opt map[string]*structpb.Value) (CallResponse, error){
49 | callbackFunc,
50 | callbackFunc,
51 | callbackFunc,
52 | callbackFunc,
53 | callbackFunc,
54 | callbackFunc,
55 | },
56 | ExpectErr: ErrRegisterMaxLimit,
57 | },
58 | }
59 |
60 | for _, test := range tests {
61 | p := plugin{}
62 |
63 | var err error
64 | for _, f := range test.Funcs {
65 | err = p.Register(f)
66 | }
67 |
68 | if test.ExpectErr == nil {
69 | assert.Equal(t, len(test.Funcs), len(p.grpc.callbacks))
70 | assert.Nil(t, err)
71 | } else {
72 | assert.Equal(t, test.ExpectErr, err)
73 | }
74 |
75 | }
76 |
77 | }
78 |
79 | func callbackFunc(in, opt map[string]*structpb.Value) (CallResponse, error) {
80 | return CallResponse{}, nil
81 | }
82 |
--------------------------------------------------------------------------------
/sdk/README.md:
--------------------------------------------------------------------------------
1 | # Vatz SDK for plugin
2 |
3 | **For now, `vatz` repository is private so many part of starting new project should be done by manually.**
4 |
5 | ### Step 1: Create new project
6 |
7 | First, create new directory to store.
8 |
9 | ```
10 | ~$ mkdir
11 | ~$ cd
12 | ```
13 |
14 | Initialize go.mod file.
15 |
16 | ```
17 | ~$ go mod init
18 | ```
19 |
20 | ### Step 2: Start main
21 |
22 | Create `main.go` file with below contents.
23 |
24 | ```
25 | ~$ touch main.go
26 | ```
27 |
28 | ```
29 | package main
30 |
31 | import (
32 | "flag"
33 | "fmt"
34 |
35 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
36 | "github.com/dsrvlabs/vatz/sdk"
37 | "golang.org/x/net/context"
38 | "google.golang.org/protobuf/types/known/structpb"
39 | )
40 |
41 | const (
42 | // Default values.
43 | defaultAddr = "127.0.0.1"
44 | defaultPort = 9091
45 |
46 | pluginName = "YOUR_PLUGIN_NAME"
47 | )
48 |
49 | var (
50 | addr string
51 | port int
52 | )
53 |
54 | func init() {
55 | flag.StringVar(&addr, "addr", defaultAddr, "IP Address(e.g. 0.0.0.0, 127.0.0.1)")
56 | flag.IntVar(&port, "port", defaultPort, "Port number, default 9091")
57 |
58 | flag.Parse()
59 | }
60 |
61 | func main() {
62 | p := sdk.NewPlugin(pluginName)
63 | p.Register(pluginFeature)
64 |
65 | ctx := context.Background()
66 | if err := p.Start(ctx, addr, port); err != nil {
67 | fmt.Println("exit")
68 | }
69 | }
70 |
71 | func pluginFeature(info, option map[string]*structpb.Value) (sdk.CallResponse, error) {
72 | // TODO: Fill here.
73 | ret := sdk.CallResponse{
74 | FuncName: "YOUR_FUNCTION_NAME",
75 | Message: "YOUR_MESSAGE_CONTENTS",
76 | Severity: pluginpb.SEVERITY_UNKNOWN,
77 | State: pluginpb.STATE_NONE
78 | }
79 |
80 | return ret, nil
81 | }
82 | ```
83 |
84 | Then build source code.
85 |
86 | ```
87 | ~$ go mod tidy
88 | ~$ go build
89 | ```
90 |
--------------------------------------------------------------------------------
/monitoring/prometheus/prometheus_test.go:
--------------------------------------------------------------------------------
1 | package prometheus
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/prometheus/client_golang/prometheus"
8 | )
9 |
10 | func TestInitPrometheusServer(t *testing.T) {
11 | type args struct {
12 | addr string
13 | port string
14 | protocol string
15 | }
16 | var tests []struct {
17 | name string
18 | args args
19 | wantErr bool
20 | }
21 | for _, tt := range tests {
22 | t.Run(tt.name, func(t *testing.T) {
23 | if err := InitPrometheusServer(tt.args.addr, tt.args.port, tt.args.protocol); (err != nil) != tt.wantErr {
24 | t.Errorf("InitPrometheusServer() error = %v, wantErr %v", err, tt.wantErr)
25 | }
26 | })
27 | }
28 | }
29 |
30 | func Test_newPrometheusManager(t *testing.T) {
31 | type args struct {
32 | protocol string
33 | reg prometheus.Registerer
34 | }
35 | var tests []struct {
36 | name string
37 | args args
38 | want *prometheusManager
39 | }
40 |
41 | for _, tt := range tests {
42 | t.Run(tt.name, func(t *testing.T) {
43 | if got := newPrometheusManager(tt.args.protocol, tt.args.reg); !reflect.DeepEqual(got, tt.want) {
44 | t.Errorf("newPrometheusManager() = %v, want %v", got, tt.want)
45 | }
46 | })
47 | }
48 | }
49 |
50 | func Test_prometheusManagerCollector_Collect(t *testing.T) {
51 | type fields struct {
52 | prometheusManager *prometheusManager
53 | }
54 | type args struct {
55 | prometheus.Metric
56 | }
57 | var tests []struct {
58 | name string
59 | fields fields
60 | args args
61 | }
62 | for _, tt := range tests {
63 | t.Run(tt.name, func(t *testing.T) {
64 | _ = prometheusManagerCollector{
65 | prometheusManager: tt.fields.prometheusManager,
66 | }
67 | })
68 | }
69 | }
70 |
71 | func Test_prometheusManagerCollector_Describe(t *testing.T) {
72 | type fields struct {
73 | prometheusManager *prometheusManager
74 | }
75 | type args struct {
76 | *prometheus.Desc
77 | }
78 | var tests []struct {
79 | name string
80 | fields fields
81 | args args
82 | }
83 | for _, tt := range tests {
84 | t.Run(tt.name, func(t *testing.T) {
85 | _ = prometheusManagerCollector{
86 | prometheusManager: tt.fields.prometheusManager,
87 | }
88 | })
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/script/simple_start_guide/default_config.yaml:
--------------------------------------------------------------------------------
1 | vatz_protocol_info:
2 | home_path: "~/.vatz"
3 | protocol_identifier: "Validator Node:"
4 | port: 9090
5 | health_checker_schedule:
6 | - "0 1 * * *"
7 | notification_info:
8 | host_name: "hostname"
9 | default_reminder_schedule:
10 | - "*/30 * * * *"
11 | dispatch_channels:
12 | - channel: "discord"
13 | secret: "webhook"
14 | rpc_info:
15 | enabled: true
16 | address: "127.0.0.1"
17 | grpc_port: 19090
18 | http_port: 19091
19 | monitoring_info:
20 | prometheus:
21 | enabled: true
22 | address: "127.0.0.1"
23 | port: 18080
24 | plugins_infos:
25 | default_verify_interval: 15
26 | default_execute_interval: 30
27 | default_plugin_name: "vatz-plugin"
28 | plugins:
29 | - plugin_name: "cpu_monitor"
30 | plugin_address: "localhost"
31 | plugin_port: 9001
32 | executable_methods:
33 | - method_name: "cpu_monitor"
34 | - plugin_name: "mem_monitor"
35 | plugin_address: "localhost"
36 | plugin_port: 9002
37 | executable_methods:
38 | - method_name: "mem_monitor"
39 | - plugin_name: "disk_monitor"
40 | plugin_address: "localhost"
41 | plugin_port: 9003
42 | executable_methods:
43 | - method_name: "disk_monitor"
44 | - plugin_name: "node_block_sync"
45 | plugin_address: "localhost"
46 | plugin_port: 10001
47 | executable_methods:
48 | - method_name: "node_block_sync"
49 | - plugin_name: "node_is_alived"
50 | plugin_address: "localhost"
51 | plugin_port: 10002
52 | executable_methods:
53 | - method_name: "node_is_alived"
54 | - plugin_name: "node_peer_count"
55 | plugin_address: "localhost"
56 | plugin_port: 10003
57 | executable_methods:
58 | - method_name: "node_peer_count"
59 | - plugin_name: "node_active_status"
60 | plugin_address: "localhost"
61 | plugin_port: 10004
62 | executable_methods:
63 | - method_name: "node_active_status"
64 | - plugin_name: "node_governance_alarm"
65 | plugin_address: "localhost"
66 | plugin_port: 10005
67 | executable_methods:
68 | - method_name: "node_governance_alarm"
--------------------------------------------------------------------------------
/rpc/rpc_test.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | tp "github.com/dsrvlabs/vatz/types"
7 | "io"
8 | "net/http"
9 | "testing"
10 | "time"
11 |
12 | "github.com/stretchr/testify/assert"
13 | "golang.org/x/net/context"
14 | emptypb "google.golang.org/protobuf/types/known/emptypb"
15 |
16 | pb "github.com/dsrvlabs/vatz-proto/rpc/v1"
17 | "github.com/dsrvlabs/vatz/manager/config"
18 | "github.com/dsrvlabs/vatz/mocks"
19 | )
20 |
21 | func TestRPCs(t *testing.T) {
22 | rpc := NewRPCService()
23 |
24 | go func() {
25 | err := rpc.Start("127.0.0.1", 19090, 19091)
26 | assert.Nil(t, err)
27 | }()
28 |
29 | time.Sleep(time.Second * 1) // Wait ready
30 |
31 | go func() {
32 | time.Sleep(time.Millisecond * 100)
33 |
34 | fmt.Println("Call Stop")
35 | rpc.Stop()
36 | }()
37 |
38 | req, err := http.NewRequest(http.MethodGet, "http://localhost:19091/v1/plugin_status", nil)
39 | assert.Nil(t, err)
40 |
41 | cli := http.Client{}
42 | resp, err := cli.Do(req)
43 |
44 | assert.Nil(t, err)
45 | assert.Equal(t, http.StatusOK, resp.StatusCode)
46 |
47 | data, err := io.ReadAll(resp.Body)
48 |
49 | assert.Nil(t, err)
50 |
51 | respData := map[string]any{}
52 | err = json.Unmarshal(data, &respData)
53 |
54 | assert.Nil(t, err)
55 | assert.Contains(t, respData, "status")
56 | assert.Contains(t, respData, "pluginStatus")
57 | }
58 |
59 | func TestPluginStatus(t *testing.T) {
60 | ctx := context.Background()
61 |
62 | // Mocks
63 | mockHealthCheck := mocks.HealthCheck{}
64 |
65 | mockHealthCheck.
66 | On("PluginStatus", ctx).
67 | Return([]tp.PluginStatus{
68 | {
69 | Plugin: config.Plugin{Name: "plugin1"},
70 | IsAlive: tp.AliveStatusUp,
71 | },
72 | {
73 | Plugin: config.Plugin{Name: "plugin2"},
74 | IsAlive: tp.AliveStatusDown,
75 | },
76 | })
77 |
78 | // Tests
79 | srv := grpcService{healthChecker: &mockHealthCheck}
80 | resp, err := srv.PluginStatus(ctx, &emptypb.Empty{})
81 |
82 | // Asserts
83 | assert.Nil(t, err)
84 | assert.Equal(t, 2, len(resp.PluginStatus))
85 |
86 | assert.Equal(t, "plugin1", resp.PluginStatus[0].PluginName)
87 | assert.Equal(t, pb.Status_OK, resp.PluginStatus[0].GetStatus())
88 |
89 | assert.Equal(t, "plugin2", resp.PluginStatus[1].PluginName)
90 | assert.Equal(t, pb.Status_FAIL, resp.PluginStatus[1].GetStatus())
91 |
92 | mockHealthCheck.AssertExpectations(t)
93 | }
94 |
--------------------------------------------------------------------------------
/manager/dispatcher/dispatcher_test.go:
--------------------------------------------------------------------------------
1 | package dispatcher
2 |
3 | import (
4 | pb "github.com/dsrvlabs/vatz-proto/plugin/v1"
5 | tp "github.com/dsrvlabs/vatz/types"
6 | "github.com/stretchr/testify/assert"
7 | "testing"
8 | )
9 |
10 | func TestMessageHandler(t *testing.T) {
11 | preStat := tp.StateFlag{
12 | State: pb.STATE_FAILURE,
13 | Severity: pb.SEVERITY_CRITICAL,
14 | }
15 | notifyInfoTPOn := tp.NotifyInfo{
16 | Plugin: "SamplePlugin",
17 | Method: "IsSuccess",
18 | State: pb.STATE_SUCCESS,
19 | Severity: pb.SEVERITY_WARNING,
20 | ExecuteMsg: "ExecuteMsg",
21 | }
22 |
23 | notifyInfoTPOff := tp.NotifyInfo{
24 | Plugin: "SamplePlugin",
25 | Method: "IsSuccess",
26 | State: pb.STATE_SUCCESS,
27 | Severity: pb.SEVERITY_INFO,
28 | ExecuteMsg: "ExecuteMsg",
29 | }
30 |
31 | notifyInfoTPHang := tp.NotifyInfo{
32 | Plugin: "SamplePlugin",
33 | Method: "IsSuccess",
34 | State: pb.STATE_FAILURE,
35 | Severity: pb.SEVERITY_CRITICAL,
36 | ExecuteMsg: "ExecuteMsg",
37 | }
38 | dummyOptions := map[string]interface{}{"pUnique": "SamplePlugin0"}
39 | no1, reminderSt1, deliverMessage1 := messageHandler(true, preStat, notifyInfoTPOn)
40 | no2, reminderSt2, deliverMessage2 := messageHandler(false, preStat, notifyInfoTPOff)
41 | no3, reminderSt3, deliverMessage3 := messageHandler(false, preStat, notifyInfoTPHang)
42 |
43 | assert.True(t, true == no1)
44 | assert.Equal(t, tp.ON, reminderSt1)
45 | assert.Equal(t, tp.ReqMsg{
46 | FuncName: "IsSuccess",
47 | State: pb.STATE_SUCCESS,
48 | Msg: "ExecuteMsg",
49 | Severity: pb.SEVERITY_WARNING,
50 | ResourceType: "SamplePlugin",
51 | Options: dummyOptions,
52 | }, deliverMessage1)
53 |
54 | assert.False(t, false == no2)
55 | assert.Equal(t, tp.OFF, reminderSt2)
56 | assert.Equal(t, tp.ReqMsg{
57 | FuncName: "IsSuccess",
58 | State: pb.STATE_SUCCESS,
59 | Msg: "ExecuteMsg",
60 | Severity: pb.SEVERITY_INFO,
61 | ResourceType: "SamplePlugin",
62 | Options: dummyOptions,
63 | }, deliverMessage2)
64 |
65 | assert.False(t, no3)
66 | assert.Equal(t, tp.HANG, reminderSt3)
67 | assert.Equal(t, tp.ReqMsg{
68 | FuncName: "IsSuccess",
69 | State: pb.STATE_FAILURE,
70 | Msg: "ExecuteMsg",
71 | Severity: pb.SEVERITY_CRITICAL,
72 | ResourceType: "SamplePlugin",
73 | Options: dummyOptions,
74 | }, deliverMessage3)
75 | }
76 |
--------------------------------------------------------------------------------
/sdk/grpc.go:
--------------------------------------------------------------------------------
1 | package sdk
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 |
8 | pb "github.com/dsrvlabs/vatz-proto/plugin/v1"
9 | "github.com/rs/zerolog/log"
10 | "google.golang.org/grpc"
11 | "google.golang.org/grpc/reflection"
12 | "google.golang.org/protobuf/types/known/emptypb"
13 | structpb "google.golang.org/protobuf/types/known/structpb"
14 | )
15 |
16 | type grpcServer struct {
17 | pb.UnimplementedPluginServer
18 |
19 | srv *grpc.Server
20 | callbacks []func(map[string]*structpb.Value, map[string]*structpb.Value) (CallResponse, error)
21 | }
22 |
23 | // Verify returns liveness.
24 | func (s *grpcServer) Verify(context.Context, *emptypb.Empty) (*pb.VerifyInfo, error) {
25 | return &pb.VerifyInfo{
26 | VerifyMsg: "OK",
27 | }, nil
28 | }
29 |
30 | // Execute runs plugin features.
31 | func (s *grpcServer) Execute(ctx context.Context, req *pb.ExecuteRequest) (*pb.ExecuteResponse, error) {
32 | log.Info().Str("module", "grpc").Msg("Execute")
33 |
34 | resp := &pb.ExecuteResponse{
35 | ResourceType: PluginName,
36 | }
37 |
38 | for _, f := range s.callbacks {
39 | var (
40 | executeInfo map[string]*structpb.Value
41 | option map[string]*structpb.Value
42 | )
43 |
44 | if req.GetExecuteInfo() != nil {
45 | executeInfo = req.GetExecuteInfo().GetFields()
46 | }
47 |
48 | if req.GetOptions() != nil {
49 | option = req.GetOptions().GetFields()
50 | }
51 |
52 | callResp, err := f(executeInfo, option)
53 |
54 | if err != nil {
55 | resp.Severity = pb.SEVERITY_ERROR
56 | resp.State = pb.STATE_FAILURE
57 | resp.Message = err.Error()
58 | } else {
59 | resp.Severity = callResp.Severity
60 | resp.State = callResp.State
61 | resp.Message = callResp.Message
62 | }
63 | }
64 |
65 | return resp, nil
66 | }
67 |
68 | // Start starts gRPC service.
69 | func (s *grpcServer) Start(ctx context.Context, address string, port int) error {
70 | log.Info().Str("module", "grpc").Msg("Start")
71 |
72 | c, err := net.Listen("tcp", fmt.Sprintf("%s:%d", address, port))
73 | if err != nil {
74 | log.Error().Str("module", "grpc").Msg(err.Error())
75 | return err
76 | }
77 |
78 | s.srv = grpc.NewServer()
79 |
80 | pb.RegisterPluginServer(s.srv, s)
81 | reflection.Register(s.srv)
82 |
83 | return s.srv.Serve(c)
84 | }
85 |
86 | func (s *grpcServer) Stop() {
87 | log.Info().Str("module", "grpc").Msg("Stop")
88 |
89 | if s.srv != nil {
90 | s.srv.GracefulStop()
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/sdk/sdk.go:
--------------------------------------------------------------------------------
1 | package sdk
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "os"
8 | "os/signal"
9 | "syscall"
10 |
11 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
12 | "github.com/rs/zerolog/log"
13 | structpb "google.golang.org/protobuf/types/known/structpb"
14 | )
15 |
16 | const (
17 | registerFuncLimit = 5
18 | )
19 |
20 | // Errors
21 | var (
22 | ErrRegisterMaxLimit = errors.New("too many register functions")
23 |
24 | PluginName string
25 | )
26 |
27 | // Plugin provides interfaces to manage plugin.
28 | type Plugin interface {
29 | Start(ctx context.Context, address string, port int) error
30 | Stop()
31 | Register(cb func(info, option map[string]*structpb.Value) (CallResponse, error)) error
32 | }
33 |
34 | // CallResponse represents return value of callback function.
35 | type CallResponse struct {
36 | FuncName string `json:"func_name"`
37 | Message string `json:"msg"`
38 | Severity pluginpb.SEVERITY `json:"severity"`
39 | State pluginpb.STATE `json:"state"`
40 | }
41 |
42 | type plugin struct {
43 | grpc grpcServer
44 | ch chan os.Signal
45 |
46 | Name string
47 | }
48 |
49 | func (p *plugin) Start(ctx context.Context, address string, port int) error {
50 | log.Info().Str("module", "sdk").Msg(fmt.Sprintf("Start %s %d", address, port))
51 |
52 | p.ch = make(chan os.Signal, 1)
53 | signal.Notify(p.ch, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGINT)
54 |
55 | go func() {
56 | _ = <-p.ch
57 |
58 | log.Info().Str("module", "grpc").Msg("Shutting down")
59 |
60 | p.grpc.Stop()
61 | }()
62 |
63 | return p.grpc.Start(ctx, address, port)
64 | }
65 |
66 | func (p *plugin) Stop() {
67 | log.Info().Str("module", "grpc").Msg("Stop")
68 |
69 | p.ch <- syscall.SIGTERM
70 | }
71 |
72 | func (p *plugin) Register(cb func(info, option map[string]*structpb.Value) (CallResponse, error)) error {
73 | log.Info().Str("module", "grpc").Msg("Register")
74 |
75 | if p.grpc.callbacks == nil {
76 | p.grpc.callbacks = make([]func(map[string]*structpb.Value, map[string]*structpb.Value) (CallResponse, error), 0)
77 | }
78 |
79 | if len(p.grpc.callbacks) == registerFuncLimit {
80 | return ErrRegisterMaxLimit
81 | }
82 |
83 | p.grpc.callbacks = append(p.grpc.callbacks, cb)
84 |
85 | return nil
86 | }
87 |
88 | // NewPlugin creates new plugin service instance.
89 | func NewPlugin(name string) Plugin {
90 | PluginName = name
91 |
92 | return &plugin{
93 | grpc: grpcServer{},
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/sdk/sdk_test.go:
--------------------------------------------------------------------------------
1 | package sdk
2 |
3 | import (
4 | "errors"
5 | "log"
6 | "sync"
7 | "testing"
8 | "time"
9 |
10 | pb "github.com/dsrvlabs/vatz-proto/plugin/v1"
11 | "github.com/stretchr/testify/assert"
12 | "golang.org/x/net/context"
13 | "google.golang.org/protobuf/types/known/emptypb"
14 | structpb "google.golang.org/protobuf/types/known/structpb"
15 | )
16 |
17 | func TestStartStop(t *testing.T) {
18 | ctx := context.Background()
19 |
20 | p := NewPlugin("unittest")
21 |
22 | wg := sync.WaitGroup{}
23 | wg.Add(1)
24 |
25 | go func() {
26 | log.Println("Start")
27 | _ = p.Start(ctx, "0.0.0.0", 9091)
28 |
29 | log.Println("Bye")
30 | wg.Done()
31 | }()
32 |
33 | time.Sleep(time.Second * 1)
34 |
35 | p.Stop()
36 |
37 | wg.Wait()
38 | }
39 |
40 | func TestVerify(t *testing.T) {
41 | p := plugin{}
42 |
43 | ctx := context.Background()
44 | info, err := p.grpc.Verify(ctx, &emptypb.Empty{})
45 |
46 | assert.Nil(t, err)
47 | assert.Equal(t, "OK", info.VerifyMsg)
48 | }
49 |
50 | func TestInvokeCallback(t *testing.T) {
51 | tests := []struct {
52 | MockResp CallResponse
53 | MockErr error
54 | }{
55 | {
56 | MockResp: CallResponse{
57 | State: pb.STATE_SUCCESS,
58 | Message: "hello world",
59 | },
60 | MockErr: nil,
61 | },
62 | {
63 | MockResp: CallResponse{
64 | State: pb.STATE_FAILURE,
65 | },
66 | MockErr: errors.New("dummy error"),
67 | },
68 | }
69 |
70 | ctx := context.Background()
71 |
72 | p := plugin{}
73 |
74 | for _, test := range tests {
75 | req := pb.ExecuteRequest{
76 | ExecuteInfo: &structpb.Struct{
77 | Fields: map[string]*structpb.Value{
78 | "function": structpb.NewStringValue("testfunc"),
79 | },
80 | },
81 | }
82 |
83 | mockCallback := mockFuncs{}
84 |
85 | mockCallback.
86 | On("DummyCall1", req.GetExecuteInfo().GetFields(), req.GetOptions().GetFields()).
87 | Return(test.MockResp, test.MockErr)
88 |
89 | // Test
90 | err := p.Register(mockCallback.DummyCall1)
91 | resp, err := p.grpc.Execute(ctx, &req)
92 |
93 | // Asserts
94 | assert.Nil(t, err)
95 |
96 | if test.MockErr == nil {
97 | assert.Equal(t, test.MockResp.State, resp.State)
98 | assert.Equal(t, test.MockResp.Message, resp.Message)
99 | } else {
100 | assert.Equal(t, test.MockResp.State, resp.State)
101 | assert.Equal(t, test.MockErr.Error(), resp.Message)
102 | }
103 |
104 | mockCallback.AssertExpectations(t)
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/monitoring/gcp/gcp_client.go:
--------------------------------------------------------------------------------
1 | package gcp
2 |
3 | import (
4 | "cloud.google.com/go/logging"
5 | "github.com/dsrvlabs/vatz/manager/config"
6 | tp "github.com/dsrvlabs/vatz/types"
7 | "github.com/dsrvlabs/vatz/utils"
8 | "github.com/robfig/cron/v3"
9 | "github.com/rs/zerolog/log"
10 | "time"
11 | )
12 |
13 | const defaultRPCAddr = "http://localhost:19091"
14 |
15 | var periodicCloudLog *gcpCloudLoggingEntry
16 |
17 | type gcpCloudLoggingEntry struct {
18 | Protocol string `json:"protocol"`
19 | HostName string `json:"host_name"`
20 | PluginState tp.PluginState `json:"plugin_state"`
21 | Timestamp string `json:"timestamp"`
22 | }
23 |
24 | type cloudLogging struct {
25 | client *logging.Client
26 | reminderSchedule []string
27 | reminderCron *cron.Cron
28 | }
29 |
30 | func (gl *cloudLogging) Prep(cfg *config.Config) error {
31 | periodicCloudLog = setLogMessage(cfg.Vatz.ProtocolIdentifier, cfg.Vatz.NotificationInfo.HostName, tp.PluginState{})
32 | return nil
33 | }
34 |
35 | func (gl *cloudLogging) Process() error {
36 | for _, schedule := range gl.reminderSchedule {
37 | _, err := gl.reminderCron.AddFunc(schedule, func() {
38 | pluginStatus, pluginStatusErr := utils.GetPluginStatus(defaultRPCAddr)
39 | periodicCloudLog.PluginState = pluginStatus
40 | if pluginStatusErr != nil {
41 | log.Error().Str("module", "monitoring > gcp > cloud_logging").Msgf(" Execute(GetPluginStatus) error: %v", pluginStatusErr)
42 | return // Log the error and continue the function execution
43 | }
44 | err := gl.storeLog(periodicCloudLog)
45 | if err != nil {
46 | log.Error().Str("module", "monitoring > gcp > cloud_logging").Msgf(" Execute(Storing Log) error: %v", err)
47 | }
48 | })
49 | if err != nil {
50 | log.Error().Str("module", "monitoring > gcp > cloud_logging").Msgf("failed to add function to cron: %v", err)
51 | return err
52 | }
53 | }
54 | gl.reminderCron.Start()
55 | return nil
56 | }
57 |
58 | func (gl *cloudLogging) storeLog(logEntry *gcpCloudLoggingEntry) error {
59 | gcpLogger := gl.client.Logger(tp.MonitoringIdentifier)
60 | messageToSend := logging.Entry{
61 | Payload: logEntry,
62 | Severity: logging.Info,
63 | Labels: map[string]string{"module": "vatz"},
64 | }
65 |
66 | gcpLogger.Log(messageToSend)
67 |
68 | log.Info().Str("module", "monitoring").Msgf("Store Logs into Cloud logging for %s, %s", logEntry.Protocol, logEntry.HostName)
69 | return nil
70 | }
71 |
72 | func setLogMessage(protocol string, hostName string, ps tp.PluginState) *gcpCloudLoggingEntry {
73 | return &gcpCloudLoggingEntry{
74 | Timestamp: time.Now().Format(time.RFC3339),
75 | Protocol: protocol,
76 | HostName: hostName,
77 | PluginState: ps,
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/monitoring/prometheus/prometheus.go:
--------------------------------------------------------------------------------
1 | package prometheus
2 |
3 | import (
4 | "net/http"
5 | "strconv"
6 | "sync"
7 |
8 | "github.com/dsrvlabs/vatz/manager/config"
9 | "github.com/dsrvlabs/vatz/utils"
10 | "github.com/prometheus/client_golang/prometheus"
11 | "github.com/prometheus/client_golang/prometheus/collectors"
12 | "github.com/prometheus/client_golang/prometheus/promhttp"
13 | "github.com/rs/zerolog/log"
14 | )
15 |
16 | type prometheusManager struct {
17 | Protocol string
18 | // Contains many more fields not listed in this example.
19 | }
20 |
21 | type prometheusManagerCollector struct {
22 | prometheusManager *prometheusManager
23 | }
24 |
25 | type prometheusValue struct {
26 | Up int
27 | PluginName string
28 | HostName string
29 | }
30 |
31 | func newPrometheusManager(protocol string, reg prometheus.Registerer) *prometheusManager {
32 | c := &prometheusManager{
33 | Protocol: protocol,
34 | }
35 | cc := prometheusManagerCollector{prometheusManager: c}
36 | prometheus.WrapRegistererWith(prometheus.Labels{"protocol": protocol}, reg).MustRegister(cc)
37 | return c
38 | }
39 |
40 | func (cc prometheusManagerCollector) Describe(ch chan<- *prometheus.Desc) {
41 | prometheus.DescribeByCollect(cc, ch)
42 | }
43 |
44 | func (cc prometheusManagerCollector) Collect(ch chan<- prometheus.Metric) {
45 | var (
46 | pluginUpDesc = prometheus.NewDesc(
47 | "plugin_up",
48 | "Plugin liveness checks.",
49 | []string{"plugin", "port", "host_name"}, nil,
50 | )
51 | )
52 | gClientInfos := utils.GetClients(config.GetConfig().PluginInfos.Plugins)
53 | upByPlugin := cc.prometheusManager.getPluginUp(config.GetConfig().Vatz.NotificationInfo.HostName, gClientInfos)
54 |
55 | for port, value := range upByPlugin {
56 | ch <- prometheus.MustNewConstMetric(
57 | pluginUpDesc,
58 | prometheus.GaugeValue,
59 | float64(value.Up),
60 | value.PluginName,
61 | strconv.Itoa(port),
62 | value.HostName,
63 | )
64 | }
65 | }
66 |
67 | // InitPrometheusServer initializes prometheus server
68 | func InitPrometheusServer(addr, port, protocol string) error {
69 | log.Info().Str("module", "main").Msgf("start metric server: %s:%s", addr, port)
70 |
71 | reg := prometheus.NewPedanticRegistry()
72 |
73 | var prometheusOnce sync.Once
74 | prometheusOnce.Do(func() {
75 | newPrometheusManager(protocol, reg)
76 | })
77 |
78 | reg.MustRegister(
79 |
80 | collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
81 | )
82 |
83 | http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
84 |
85 | err := http.ListenAndServe(addr+":"+port, nil) //nolint:gosec
86 |
87 | if err != nil {
88 | log.Error().Str("module", "main").Msgf("Prometheus Error: %s", err)
89 | }
90 |
91 | return nil
92 | }
93 |
--------------------------------------------------------------------------------
/mocks/HealthCheck.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v2.14.0. DO NOT EDIT.
2 |
3 | package mocks
4 |
5 | import (
6 | context "context"
7 | "github.com/dsrvlabs/vatz/types"
8 |
9 | config "github.com/dsrvlabs/vatz/manager/config"
10 |
11 | dispatcher "github.com/dsrvlabs/vatz/manager/dispatcher"
12 |
13 | mock "github.com/stretchr/testify/mock"
14 |
15 | v1 "github.com/dsrvlabs/vatz-proto/plugin/v1"
16 | )
17 |
18 | // HealthCheck is an autogenerated mock type for the HealthCheck type
19 | type HealthCheck struct {
20 | mock.Mock
21 | }
22 |
23 | // PluginHealthCheck provides a mock function with given fields: ctx, gClient, plugin, _a3
24 | func (_m *HealthCheck) PluginHealthCheck(ctx context.Context, gClient v1.PluginClient, plugin config.Plugin, _a3 []dispatcher.Dispatcher) (types.AliveStatus, error) {
25 | ret := _m.Called(ctx, gClient, plugin, _a3)
26 |
27 | var r0 types.AliveStatus
28 | if rf, ok := ret.Get(0).(func(context.Context, v1.PluginClient, config.Plugin, []dispatcher.Dispatcher) types.AliveStatus); ok {
29 | r0 = rf(ctx, gClient, plugin, _a3)
30 | } else {
31 | r0 = ret.Get(0).(types.AliveStatus)
32 | }
33 |
34 | var r1 error
35 | if rf, ok := ret.Get(1).(func(context.Context, v1.PluginClient, config.Plugin, []dispatcher.Dispatcher) error); ok {
36 | r1 = rf(ctx, gClient, plugin, _a3)
37 | } else {
38 | r1 = ret.Error(1)
39 | }
40 |
41 | return r0, r1
42 | }
43 |
44 | // PluginStatus provides a mock function with given fields: ctx
45 | func (_m *HealthCheck) PluginStatus(ctx context.Context) []types.PluginStatus {
46 | ret := _m.Called(ctx)
47 |
48 | var r0 []types.PluginStatus
49 | if rf, ok := ret.Get(0).(func(context.Context) []types.PluginStatus); ok {
50 | r0 = rf(ctx)
51 | } else {
52 | if ret.Get(0) != nil {
53 | r0 = ret.Get(0).([]types.PluginStatus)
54 | }
55 | }
56 |
57 | return r0
58 | }
59 |
60 | // VATZHealthCheck provides a mock function with given fields: schedule, _a1
61 | func (_m *HealthCheck) VATZHealthCheck(schedule []string, _a1 []dispatcher.Dispatcher) error {
62 | ret := _m.Called(schedule, _a1)
63 |
64 | var r0 error
65 | if rf, ok := ret.Get(0).(func([]string, []dispatcher.Dispatcher) error); ok {
66 | r0 = rf(schedule, _a1)
67 | } else {
68 | r0 = ret.Error(0)
69 | }
70 |
71 | return r0
72 | }
73 |
74 | type mockConstructorTestingTNewHealthCheck interface {
75 | mock.TestingT
76 | Cleanup(func())
77 | }
78 |
79 | // NewHealthCheck creates a new instance of HealthCheck. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
80 | func NewHealthCheck(t mockConstructorTestingTNewHealthCheck) *HealthCheck {
81 | mock := &HealthCheck{}
82 | mock.Mock.Test(t)
83 |
84 | t.Cleanup(func() { mock.AssertExpectations(t) })
85 |
86 | return mock
87 | }
88 |
--------------------------------------------------------------------------------
/.github/workflows/create-vatz-bi-weekly.yml:
--------------------------------------------------------------------------------
1 | name: Create a next vatz discussion & milestone
2 | on:
3 | workflow_dispatch:
4 |
5 | jobs:
6 | create-bi-weekly:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Check creation date # Step to determine if this is the second week because you have to create a 'Discussion' every two weeks
10 | id: check_week
11 | run: |
12 | WEEK_NUM=`date -d ${{ vars.START_DATE }} +%U`
13 | THIS_WEEK_NUM=`date +%U`
14 | DIFF_WEEK_NUM=$( expr $THIS_WEEK_NUM - $WEEK_NUM )
15 |
16 | if [ $(( DIFF_WEEK_NUM % 2)) -eq 0 ]; then
17 | echo "This is the correct week"
18 | echo "is_week=true" >> $GITHUB_OUTPUT
19 | else
20 | echo "This is not the correct week"
21 | echo "is_week=false" >> $GITHUB_OUTPUT
22 | fi
23 | shell: bash
24 |
25 | - name: Get meeting date
26 | if: steps.check_week.outputs.is_week == 'true'
27 | id: check_meeting_date
28 | run: |
29 | meeting=`date -d "wed" +"%Y-%m-%d"`
30 | echo "meet_date=$meeting" >> $GITHUB_OUTPUT
31 | shell: bash
32 |
33 | - name: Get next discussion number
34 | if: steps.check_week.outputs.is_week == 'true'
35 | id: check_latest_discussion
36 | run: |
37 | # GitHub GraphQL API Endpoint
38 | URL="https://api.github.com/graphql"
39 |
40 | # GraphQL Query
41 | QUERY='{
42 | "query": "query { repository(owner: \"dsrvlabs\", name: \"vatz\") { discussions(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { title url createdAt author { login } } } } }"
43 | }'
44 |
45 | # Sending the request using curl
46 | response=$(curl -s -X POST -H "Authorization: bearer ${{ secrets.GITHUB_TOKEN }}" -H "Content-Type: application/json" -d "$QUERY" $URL)
47 |
48 | LASTEST=`echo $response | jq .data.repository.discussions.nodes[0].title | awk '{print $1 }' | sed 's/\"//g' | sed 's/\.//g'`
49 |
50 | NEXT_NUM=$( expr $LASTEST + 1 )
51 |
52 | echo "dis_num=$NEXT_NUM" >> $GITHUB_OUTPUT
53 | shell: bash
54 |
55 | - name: Generate new discussion
56 | if: steps.check_latest_discussion.outputs.dis_num != ''
57 | id: create_discussion
58 | uses: abirismyname/create-discussion@v1.1.0
59 | env:
60 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61 | with:
62 | title: "${{ steps.check_latest_discussion.outputs.dis_num }}. VATZ biweekly meeting at ${{ steps.check_meeting_date.outputs.meet_date }}"
63 | body: |
64 | ### 1. Overall
65 | ### 2. Statistic Rate
66 |
67 | Sprint | Issue fulfillment | progress rate(%)
68 | --: | :--: | :--:
69 |
70 | repository-id: "${{ secrets.REPOSITORY_ID }}"
71 | category-id: "${{ secrets.CATEGORY_ID }}"
72 |
73 |
--------------------------------------------------------------------------------
/manager/plugin/db_test.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "database/sql"
5 | "os"
6 | "sync"
7 | "testing"
8 | "time"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestDBWrite(t *testing.T) {
14 | err := initDB(pluginDBName)
15 | assert.Nil(t, err)
16 |
17 | defer os.Remove(pluginDBName)
18 |
19 | wr, err := newWriter(pluginDBName)
20 | assert.Nil(t, err)
21 |
22 | rd, err := newReader(pluginDBName)
23 | assert.Nil(t, err)
24 |
25 | defer func() {
26 | db.conn.Close()
27 | once = sync.Once{}
28 | }()
29 |
30 | installedAt := time.Now()
31 |
32 | // Test insert.
33 | err = wr.AddPlugin(pluginEntry{
34 | Name: "test",
35 | Repository: "dummy",
36 | BinaryLocation: "home/status",
37 | Version: "latest",
38 | InstalledAt: installedAt,
39 | })
40 | assert.Nil(t, err)
41 |
42 | // Confirm insertion.
43 | plugin, err := rd.Get("test")
44 |
45 | assert.Nil(t, err)
46 | assert.Equal(t, "test", plugin.Name)
47 | assert.Equal(t, "dummy", plugin.Repository)
48 | assert.Equal(t, "home/status", plugin.BinaryLocation)
49 | assert.Equal(t, "latest", plugin.Version)
50 | assert.Equal(t, installedAt.UnixMilli(), plugin.InstalledAt.UnixMilli())
51 |
52 | // Test delete.
53 | err = wr.DeletePlugin("test delete")
54 | assert.Nil(t, err)
55 |
56 | // Confirm deleted.
57 | plugin, err = rd.Get("test confirm delete")
58 |
59 | assert.Nil(t, plugin)
60 | assert.Equal(t, sql.ErrNoRows, err)
61 | }
62 |
63 | // TODO: Handle already exist.
64 |
65 | func TestDBList(t *testing.T) {
66 | err := initDB(pluginDBName)
67 | assert.Nil(t, err)
68 |
69 | defer os.Remove(pluginDBName)
70 |
71 | wr, err := newWriter(pluginDBName)
72 | assert.Nil(t, err)
73 |
74 | rd, err := newReader(pluginDBName)
75 | assert.Nil(t, err)
76 |
77 | defer func() {
78 | db.conn.Close()
79 | once = sync.Once{}
80 | }()
81 |
82 | installedAt := time.Now()
83 |
84 | // Add dummy plugins
85 | testPlugins := []pluginEntry{
86 | {
87 | Name: "test",
88 | Repository: "dummy",
89 | BinaryLocation: "home/status",
90 | Version: "latest",
91 | InstalledAt: installedAt,
92 | },
93 | {
94 | Name: "test2",
95 | Repository: "dummy",
96 | BinaryLocation: "home/status",
97 | Version: "latest",
98 | InstalledAt: installedAt,
99 | },
100 | }
101 |
102 | // insert.
103 | for _, p := range testPlugins {
104 | err = wr.AddPlugin(p)
105 | assert.Nil(t, err)
106 | }
107 |
108 | plugins, err := rd.List()
109 |
110 | assert.Nil(t, err)
111 | assert.Equal(t, len(testPlugins), len(plugins))
112 |
113 | for i, p := range plugins {
114 | assert.Equal(t, testPlugins[i].Name, p.Name)
115 | assert.Equal(t, testPlugins[i].Repository, p.Repository)
116 | assert.Equal(t, testPlugins[i].BinaryLocation, p.BinaryLocation)
117 | assert.Equal(t, testPlugins[i].Version, p.Version)
118 | assert.Equal(t, testPlugins[i].InstalledAt.Unix(), p.InstalledAt.Unix())
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/monitoring/gcp/gcp.go:
--------------------------------------------------------------------------------
1 | package gcp
2 |
3 | import (
4 | "cloud.google.com/go/logging"
5 | "context"
6 | "errors"
7 | "fmt"
8 | "github.com/dsrvlabs/vatz/manager/config"
9 | tp "github.com/dsrvlabs/vatz/types"
10 | "github.com/dsrvlabs/vatz/utils"
11 | "github.com/robfig/cron/v3"
12 | "github.com/rs/zerolog/log"
13 | "golang.org/x/oauth2/google"
14 | "google.golang.org/api/option"
15 | "sync"
16 | "time"
17 | )
18 |
19 | type GCP interface {
20 | Prep(cfg *config.Config) error
21 | Process() error
22 | }
23 |
24 | var (
25 | gcpSingletons []GCP
26 | gcpOnce sync.Once
27 | validCredentialOptions = map[tp.CredentialOption]bool{
28 | tp.ApplicationDefaultCredentials: true,
29 | tp.ServiceAccountCredentials: true,
30 | tp.APIKey: true,
31 | tp.OAuth2: true,
32 | }
33 | )
34 |
35 | func GetGCP(cfg config.MonitoringInfo) []GCP {
36 | gcpOnce.Do(func() {
37 | loggPrepInfo := cfg.GCP.GCPCloudLogging
38 | if loggPrepInfo.Enabled && isValidCredentialOption(loggPrepInfo.GCPCredentialInfo.CredentialsType) {
39 | gcpClient, err := getClient(context.Background(), loggPrepInfo.GCPCredentialInfo.ProjectID, tp.CredentialOption(loggPrepInfo.GCPCredentialInfo.CredentialsType), loggPrepInfo.GCPCredentialInfo.Credentials)
40 | if err != nil {
41 | log.Error().Str("module", "monitoring > Init").Msgf("get GCP client for Logging Error: %s", err)
42 | return
43 | }
44 | gcpSingletons = append(gcpSingletons, &cloudLogging{
45 | client: gcpClient,
46 | reminderCron: cron.New(cron.WithLocation(time.UTC)),
47 | reminderSchedule: loggPrepInfo.GCPCredentialInfo.CheckerSchedule,
48 | })
49 | }
50 | })
51 | return gcpSingletons
52 | }
53 |
54 | func getClient(ctx context.Context, projectID string, credType tp.CredentialOption, credentials string) (*logging.Client, error) {
55 | var client *logging.Client
56 | var err error
57 | switch credType {
58 | case tp.ApplicationDefaultCredentials:
59 | client, err = logging.NewClient(ctx, projectID)
60 | case tp.ServiceAccountCredentials:
61 | var serviceAccountJSON []byte
62 | if utils.IsURL(credentials) {
63 | serviceAccountJSON, err = utils.DownloadFileWithWgetOrCurl(credentials)
64 | if err != nil {
65 | return nil, fmt.Errorf("failed to download service account JSON: %v", err)
66 | }
67 | client, err = logging.NewClient(ctx, projectID, option.WithCredentialsJSON(serviceAccountJSON))
68 | } else {
69 | client, err = logging.NewClient(ctx, projectID, option.WithCredentialsFile(credentials))
70 | }
71 | case tp.APIKey:
72 | client, err = logging.NewClient(ctx, projectID, option.WithAPIKey(credentials))
73 | case tp.OAuth2:
74 | tokenSource, err := google.DefaultTokenSource(ctx, logging.WriteScope)
75 | if err != nil {
76 | return nil, err
77 | }
78 | client, err = logging.NewClient(ctx, projectID, option.WithTokenSource(tokenSource))
79 | if err != nil {
80 | return nil, err
81 | }
82 | default:
83 | err = errors.New("invalid credential type")
84 | }
85 | return client, err
86 | }
87 |
88 | func isValidCredentialOption(option string) bool {
89 | credOption := tp.CredentialOption(option)
90 | _, isValid := validCredentialOptions[credOption]
91 | return isValid
92 | }
93 |
--------------------------------------------------------------------------------
/manager/plugin/plugin_test.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "sync"
7 | "testing"
8 | "time"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestPluginManager(t *testing.T) {
14 | err := initDB(pluginDBName)
15 | assert.Nil(t, err)
16 |
17 | defer func() {
18 | os.Remove("./active_status")
19 | os.Remove("./cosmos-active")
20 | os.Remove(pluginDBName)
21 | once = sync.Once{}
22 | db = nil
23 | }()
24 |
25 | // TODO: could be better using mocks.
26 | repo := "github.com/dsrvlabs/vatz-plugin-cosmoshub/plugins/node_active_status"
27 |
28 | binName := "cosmos-active"
29 |
30 | mgr := NewManager(os.Getenv("PWD"))
31 | err = mgr.Install(repo, binName, "latest")
32 | assert.Nil(t, err)
33 |
34 | _, err = os.Open("./active_status")
35 | assert.True(t, errors.Is(err, os.ErrNotExist))
36 |
37 | _, err = os.Open(binName)
38 | assert.Nil(t, err)
39 |
40 | // Test Execute
41 | logfile, err := os.OpenFile("./vatz.log", os.O_RDWR|os.O_CREATE, 0644)
42 | assert.Nil(t, err)
43 |
44 | defer func() {
45 | os.Remove("./vatz.log")
46 | }()
47 |
48 | err = mgr.Start(binName, "-valoperAddr=dummy -port 9999", logfile)
49 | assert.Nil(t, err)
50 |
51 | vatzMgr := mgr.(*vatzPluginManager)
52 | ps, err := vatzMgr.findProcessByName(binName)
53 |
54 | assert.Nil(t, err)
55 | assert.NotNil(t, ps)
56 |
57 | pName, err := ps.Name()
58 | assert.Nil(t, err)
59 | assert.Equal(t, binName, pName)
60 |
61 | isRunning, err := ps.IsRunning()
62 | assert.Nil(t, err)
63 | assert.True(t, isRunning)
64 |
65 | // Test Stop
66 | err = mgr.Stop(binName)
67 | assert.Nil(t, err)
68 |
69 | // Test DB.
70 | rd, err := newReader("./" + pluginDBName)
71 | assert.Nil(t, err)
72 |
73 | e, err := rd.Get(binName)
74 | assert.Nil(t, err)
75 |
76 | assert.Equal(t, binName, e.Name)
77 | assert.Equal(t, repo, e.Repository)
78 | }
79 |
80 | func TestPluginList(t *testing.T) {
81 | err := initDB(pluginDBName)
82 | assert.Nil(t, err)
83 |
84 | defer func() {
85 | os.Remove(pluginDBName)
86 | once = sync.Once{}
87 | db = nil
88 | }()
89 |
90 | wr, err := newWriter("./" + pluginDBName)
91 | assert.Nil(t, err)
92 |
93 | // Add dummy plugins
94 | testPlugins := []pluginEntry{
95 | {
96 | Name: "test",
97 | Repository: "dummy",
98 | BinaryLocation: "home/status",
99 | Version: "latest",
100 | InstalledAt: time.Now(),
101 | },
102 | {
103 | Name: "test2",
104 | Repository: "dummy",
105 | BinaryLocation: "home/status",
106 | Version: "latest",
107 | InstalledAt: time.Now(),
108 | },
109 | }
110 |
111 | // Insert.
112 | for _, p := range testPlugins {
113 | err = wr.AddPlugin(p)
114 | assert.Nil(t, err)
115 | }
116 |
117 | pluginManager := NewManager(os.Getenv("PWD"))
118 | plugins, err := pluginManager.List()
119 |
120 | assert.Nil(t, err)
121 | assert.Equal(t, 2, len(plugins))
122 |
123 | for i, p := range plugins {
124 | assert.Equal(t, testPlugins[i].Name, p.Name)
125 | assert.Equal(t, testPlugins[i].Repository, p.Repository)
126 | assert.Equal(t, testPlugins[i].BinaryLocation, p.Location)
127 | assert.Equal(t, testPlugins[i].Version, p.Version)
128 | assert.Equal(t, testPlugins[i].InstalledAt.Unix(), p.InstalledAt.Unix())
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/cmd/stop.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/dsrvlabs/vatz/manager/config"
7 | "github.com/rs/zerolog/log"
8 | "github.com/spf13/cobra"
9 | "os"
10 | "strconv"
11 | "syscall"
12 | "time"
13 | )
14 |
15 | func createStopCommand() *cobra.Command {
16 | log.Debug().Str("module", "cmd > stop").Msg("Stop command")
17 | cmd := &cobra.Command{
18 | Use: "stop",
19 | Short: "Stop VATZ",
20 | PreRunE: func(cmd *cobra.Command, args []string) error {
21 | _, err := config.InitConfig(configFile)
22 | return err
23 | },
24 | RunE: func(cmd *cobra.Command, args []string) error {
25 | var (
26 | maxAttempts = 3
27 | sc = syscall.SIGINT
28 | )
29 | path, err := config.GetConfig().Vatz.AbsoluteHomePath()
30 | if err != nil {
31 | return err
32 | }
33 | filPath := fmt.Sprintf("%s/vatz.pid", path)
34 | /*
35 | As mentioned on start.go
36 | Stored process id into a separated file to avoid terminating the wrong vatz process
37 | if there are two multiple running vatz processes on the single machine.
38 | */
39 | pidData, err := os.ReadFile(filPath)
40 | if err != nil {
41 | log.Error().Str("module", "cmd > stop").Msgf("There's no File vatz.pid at %s", path)
42 | return errors.New("Can't specify VATZ process id from vatz.pid, Please, Check VATZ is running.")
43 | }
44 |
45 | pid, err := strconv.Atoi(string(pidData))
46 | log.Debug().Str("module", "stop").Msgf("pid is %d", pid)
47 | if err != nil {
48 | log.Error().Str("module", "cmd > stop").Msgf("Please, check vatz process is running with pid %d", pid)
49 | return errors.New("Can't specify VATZ process id, Please, Check VATZ is running.")
50 | }
51 |
52 | if err := os.Remove(filPath); err != nil {
53 | log.Error().Str("module", "cmd > stop").Msgf("file deletion failed due to %s", err)
54 | } else {
55 | log.Debug().Str("module", "cmd > stop").Msgf("File successfully deleted at %s", filPath)
56 | }
57 |
58 | process, err := os.FindProcess(pid)
59 | if err != nil {
60 | log.Error().Str("module", "cmd > stop").Msgf("Failed to find process with pid %d", pid)
61 | log.Error().Str("module", "cmd > stop").Msg("Please, check vatz process is running")
62 | return err
63 | }
64 |
65 | ticker := time.NewTicker(3 * time.Second)
66 | defer ticker.Stop()
67 |
68 | for attempts := 0; attempts < maxAttempts; attempts++ {
69 | if attempts == 0 {
70 | //Show vatz stop message only once to the user
71 | log.Info().Msg("Sent termination signal to VATZ process, terminating ...")
72 | }
73 | if attempts == maxAttempts-1 {
74 | // Just kill process if terminating process at last attempt.
75 | sc = syscall.SIGTERM
76 | }
77 |
78 | log.Debug().Str("module", "cmd > stop").Msgf("syscall: %d", sc)
79 | if err := process.Signal(sc); err != nil {
80 | log.Error().Err(err).Msg("Failed to send termination signal")
81 | fmt.Printf("Failed to send SIGINT: %v\n", err)
82 | }
83 |
84 | <-ticker.C
85 | if err := process.Signal(syscall.Signal(0)); err != nil {
86 | log.Debug().Str("module", "cmd > stop").Msg("Process has exited.")
87 | return nil
88 | } else {
89 | log.Debug().Str("module", "cmd > stop").Msg("Process is still running. Attempting to send SIGINT again.")
90 | }
91 | }
92 | return nil
93 | },
94 | }
95 | return cmd
96 | }
97 |
--------------------------------------------------------------------------------
/manager/config/fixtures.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | // Default contents
4 | const configDefaultContents = `
5 | vatz_protocol_info:
6 | protocol_identifier: "vatz"
7 | port: 9090
8 | notification_info:
9 | discord_secret: "XXXXX"
10 | pager_duty_secret: "YYYYY"
11 | host_name: "xxx-xxxx-xxxx"
12 | dispatch_channels:
13 | - channel: "discord"
14 | secret: "https://xxxxx.xxxxxx"
15 | - channel: "telegram"
16 | secret: "https://yyyyy.yyyyyy"
17 | - channel: "pagerduty"
18 | secret: "https://zzzzz.zzzzzz"
19 | health_checker_schedule:
20 | - "* 1 * * *"
21 | rpc_info:
22 | enabled: true
23 | address: "127.0.0.1"
24 | grpc_port: 19090
25 | http_port: 19091
26 | home_path: ~/.vatz
27 |
28 | plugins_infos:
29 | default_verify_interval: 15
30 | default_execute_interval: 30
31 | default_plugin_name: "vatz-plugin"
32 | plugins:
33 | - plugin_name: "vatz-plugin-node-checker"
34 | plugin_address: "localhost"
35 | verify_interval: 7
36 | execute_interval: 9
37 | plugin_port: 9091
38 | executable_methods:
39 | - method_name: "isUp"
40 | - method_name: "getBlockHeight"
41 | - method_name: "getNumberOfPeers"
42 | - plugin_name: "vatz-plugin-machine-checker"
43 | plugin_address: "localhost"
44 | verify_interval: 8
45 | execute_interval: 10
46 | plugin_port: 9092
47 | executable_methods:
48 | - method_name: "getMemory"
49 | - method_name: "getDiscSize"
50 | - method_name: "getCPUInfo"
51 | `
52 |
53 | // "verify_interval", "execute_interval" and "plugin_name" on "plugins" are removed.
54 | const configNoIntervalContents = `
55 | vatz_protocol_info:
56 | protocol_identifier: "vatz"
57 | port: 9090
58 | notification_info:
59 | discord_secret: "hello"
60 | pager_duty_secret: "world"
61 | host_name: "dummy0"
62 | dispatch_channels:
63 | - channel: "discord"
64 | secret: "dummy1"
65 | - channel: "telegram"
66 | secret: "dummy2"
67 | - channel: "pagerduty"
68 | secret: "dummy3"
69 |
70 | plugins_infos:
71 | default_verify_interval: 15
72 | default_execute_interval: 30
73 | default_plugin_name: "vatz-plugin"
74 | plugins:
75 | - plugin_address: "localhost"
76 | plugin_port: 9091
77 | executable_methods:
78 | - method_name: "isUp"
79 | - method_name: "getBlockHeight"
80 | - method_name: "getNumberOfPeers"
81 | `
82 |
83 | // Intentionally ruin file contents.
84 | const configInvalidYAMLContents = `
85 | vatz_protocol_info
86 | protocol_identifier: "vatz"
87 | port: 9090
88 | notification_info:
89 | discord_secret: "hello"
90 | pager_duty_secret: "world"
91 | host_name: "dummy0"
92 | dispatch_channels:
93 | - channel: "discord"
94 | secret: "dummy1"
95 | - channel: "telegram"
96 | secret: "dummy2"
97 | - channel: "pagerduty"
98 | secret: "dummy3"
99 |
100 | plugins_infos:
101 | default_verify_interval: 15
102 | default_execute_interval: 30
103 | default_plugin_name: "vatz-plugin"
104 | plugins:
105 | - plugin_address: "localhost"
106 | plugin_port: 9091
107 | executable_methods:
108 | - method_name: "isUp"
109 | - method_name: "getBlockHeight"
110 | - method_name: "getNumberOfPeers"
111 | `
112 |
--------------------------------------------------------------------------------
/manager/healthcheck/healthcheck_test.go:
--------------------------------------------------------------------------------
1 | package healthcheck
2 |
3 | import (
4 | "errors"
5 | "sync"
6 | "testing"
7 |
8 | dp "github.com/dsrvlabs/vatz/manager/dispatcher"
9 |
10 | tp "github.com/dsrvlabs/vatz/types"
11 |
12 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
13 | "github.com/dsrvlabs/vatz/manager/config"
14 | "github.com/stretchr/testify/assert"
15 | "golang.org/x/net/context"
16 | "google.golang.org/grpc"
17 | emptypb "google.golang.org/protobuf/types/known/emptypb"
18 |
19 | "github.com/dsrvlabs/vatz/mocks"
20 | )
21 |
22 | func TestPluginHealthCheckSuccess(t *testing.T) {
23 | h := healthChecker{
24 | pluginStatus: sync.Map{},
25 | }
26 | ctx := context.Background()
27 |
28 | // Mock
29 | mockPluginCli := mocks.MockPluginClient{}
30 | mockPluginCli.
31 | On("Verify", ctx, new(emptypb.Empty), []grpc.CallOption(nil)).
32 | Return(&pluginpb.VerifyInfo{VerifyMsg: "test"}, nil)
33 |
34 | var mockDispatchers = []dp.Dispatcher{}
35 | status, err := h.PluginHealthCheck(ctx, &mockPluginCli, config.Plugin{Name: "dummy"}, mockDispatchers)
36 |
37 | // Asserts
38 | assert.Nil(t, err)
39 | assert.Equal(t, tp.AliveStatusUp, status)
40 |
41 | statuses := h.PluginStatus(ctx)
42 |
43 | assert.Equal(t, 1, len(statuses))
44 | assert.Equal(t, "dummy", statuses[0].Plugin.Name)
45 | assert.Equal(t, tp.AliveStatusUp, statuses[0].IsAlive)
46 |
47 | mockPluginCli.AssertExpectations(t)
48 | }
49 |
50 | func TestPluginHealthCheckFailed(t *testing.T) {
51 | tests := []struct {
52 | Desc string
53 | MockVerifyInfo *pluginpb.VerifyInfo
54 | MockVerifyErr error
55 | }{
56 | {
57 | Desc: "VerifyInfo is nil",
58 | MockVerifyInfo: nil,
59 | MockVerifyErr: nil,
60 | },
61 | {
62 | Desc: "Error VerifyInfo",
63 | MockVerifyInfo: &pluginpb.VerifyInfo{},
64 | MockVerifyErr: errors.New("temporal error occurred"),
65 | },
66 | }
67 |
68 | for _, test := range tests {
69 | h := healthChecker{
70 | pluginStatus: sync.Map{},
71 | }
72 | ctx := context.Background()
73 |
74 | // Mock
75 | mockPluginCli := mocks.MockPluginClient{}
76 | mockPluginCli.
77 | On("Verify", ctx, new(emptypb.Empty), []grpc.CallOption(nil)).
78 | Return(test.MockVerifyInfo, test.MockVerifyErr)
79 |
80 | mockDispatcher := dp.MockDispatcher{}
81 | var mockDispatchers []dp.Dispatcher
82 |
83 | mockJSONMsg := tp.ReqMsg{
84 | FuncName: "isPluginUp",
85 | State: pluginpb.STATE_FAILURE,
86 | Msg: "Plugin is DOWN!!",
87 | Severity: pluginpb.SEVERITY_CRITICAL,
88 | ResourceType: "test",
89 | }
90 | mockDispatcher.On("SendNotification", mockJSONMsg).Return(nil)
91 |
92 | // Test
93 | status, err := h.PluginHealthCheck(ctx, &mockPluginCli, config.Plugin{Name: "test"}, mockDispatchers)
94 |
95 | // Asserts
96 | assert.Error(t, err)
97 | assert.Equal(t, tp.AliveStatusDown, status)
98 |
99 | statuses := h.PluginStatus(ctx)
100 |
101 | assert.Equal(t, 0, len(statuses))
102 |
103 | /*
104 | Both cases are not available since there's error when no appropriate configs for notification
105 | assert.Equal(t, "test", statuses[0].Plugin.Name)
106 | assert.Equal(t, tp.AliveStatusDown, statuses[0].IsAlive)
107 | */
108 |
109 | mockPluginCli.AssertExpectations(t)
110 | }
111 | }
112 |
113 | func TestVatzHealthCheck(t *testing.T) {
114 | // TODO: TBD
115 | }
116 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/dsrvlabs/vatz
2 |
3 | go 1.19
4 |
5 | require (
6 | cloud.google.com/go/logging v1.7.0
7 | github.com/PagerDuty/go-pagerduty v1.6.0
8 | github.com/dsrvlabs/vatz-proto v1.0.1-0.20231013005328-0ce1b6797611
9 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3
10 | github.com/jarcoal/httpmock v1.2.0
11 | github.com/jedib0t/go-pretty/v6 v6.4.3
12 | github.com/mattn/go-sqlite3 v1.14.16
13 | github.com/prometheus/client_golang v1.14.0
14 | github.com/rs/zerolog v1.28.0
15 | github.com/shirou/gopsutil v3.21.11+incompatible
16 | github.com/spf13/cobra v1.5.0
17 | github.com/spf13/viper v1.14.0
18 | github.com/stretchr/testify v1.8.1
19 | google.golang.org/api v0.114.0
20 | google.golang.org/grpc v1.56.3
21 | google.golang.org/protobuf v1.30.0
22 | gopkg.in/yaml.v2 v2.4.0
23 | )
24 |
25 | require (
26 | cloud.google.com/go v0.110.0 // indirect
27 | cloud.google.com/go/compute v1.19.1 // indirect
28 | cloud.google.com/go/compute/metadata v0.2.3 // indirect
29 | cloud.google.com/go/longrunning v0.4.1 // indirect
30 | github.com/beorn7/perks v1.0.1 // indirect
31 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
32 | github.com/fsnotify/fsnotify v1.6.0 // indirect
33 | github.com/go-ole/go-ole v1.2.6 // indirect
34 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
35 | github.com/google/go-cmp v0.5.9 // indirect
36 | github.com/google/go-querystring v1.0.0 // indirect
37 | github.com/google/uuid v1.3.0 // indirect
38 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
39 | github.com/googleapis/gax-go/v2 v2.7.1 // indirect
40 | github.com/hashicorp/hcl v1.0.0 // indirect
41 | github.com/inconshreveable/mousetrap v1.0.1 // indirect
42 | github.com/magiconair/properties v1.8.6 // indirect
43 | github.com/mattn/go-colorable v0.1.13 // indirect
44 | github.com/mattn/go-isatty v0.0.16 // indirect
45 | github.com/mattn/go-runewidth v0.0.13 // indirect
46 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
47 | github.com/mitchellh/mapstructure v1.5.0 // indirect
48 | github.com/pelletier/go-toml v1.9.5 // indirect
49 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect
50 | github.com/prometheus/client_model v0.3.0 // indirect
51 | github.com/prometheus/common v0.37.0 // indirect
52 | github.com/prometheus/procfs v0.8.0 // indirect
53 | github.com/rivo/uniseg v0.2.0 // indirect
54 | github.com/spf13/afero v1.9.2 // indirect
55 | github.com/spf13/cast v1.5.0 // indirect
56 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
57 | github.com/spf13/pflag v1.0.5 // indirect
58 | github.com/subosito/gotenv v1.4.1 // indirect
59 | github.com/tklauser/go-sysconf v0.3.11 // indirect
60 | github.com/tklauser/numcpus v0.6.0 // indirect
61 | github.com/yusufpapurcu/wmi v1.2.2 // indirect
62 | go.opencensus.io v0.24.0 // indirect
63 | golang.org/x/oauth2 v0.7.0 // indirect
64 | golang.org/x/sync v0.1.0 // indirect
65 | google.golang.org/appengine v1.6.7 // indirect
66 | gopkg.in/ini.v1 v1.67.0 // indirect
67 | )
68 |
69 | require (
70 | github.com/davecgh/go-spew v1.1.1 // indirect
71 | github.com/golang/protobuf v1.5.3 // indirect
72 | github.com/pmezard/go-difflib v1.0.0 // indirect
73 | github.com/robfig/cron/v3 v3.0.1
74 | github.com/stretchr/objx v0.5.0 // indirect
75 | golang.org/x/net v0.9.0
76 | golang.org/x/sys v0.7.0 // indirect
77 | golang.org/x/text v0.9.0 // indirect
78 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
79 | gopkg.in/yaml.v3 v3.0.1 // indirect
80 | )
81 |
--------------------------------------------------------------------------------
/rpc/rpc.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "net/http"
8 |
9 | vatzpb "github.com/dsrvlabs/vatz-proto/rpc/v1"
10 | "github.com/dsrvlabs/vatz/manager/healthcheck"
11 | "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
12 | "github.com/rs/zerolog/log"
13 | "google.golang.org/grpc"
14 | "google.golang.org/grpc/credentials/insecure"
15 | "google.golang.org/grpc/reflection"
16 | )
17 |
18 | // VatzRPC provides RPC interfaces.
19 | type VatzRPC interface {
20 | Start(string, int, int) error
21 | Stop()
22 | }
23 |
24 | type rpcService struct {
25 | ctx context.Context
26 | cancel context.CancelFunc
27 |
28 | vatzRPCService vatzpb.VatzRPCServer
29 | grpcServer *grpc.Server
30 | httpServer *http.Server
31 | }
32 |
33 | func (s *rpcService) Start(address string, grpcPort int, httpPort int) error {
34 | log.Info().Str("module", "rpc").Msg("start rpc server")
35 |
36 | errChan := make(chan error, 2)
37 |
38 | go func(errChan chan<- error) {
39 | listenAddr := fmt.Sprintf("%s:%d", address, grpcPort)
40 | log.Info().Str("module", "rpc").Msgf("start gRPC server %s", listenAddr)
41 |
42 | l, err := net.Listen("tcp", listenAddr)
43 | if err != nil {
44 | log.Info().Str("module", "rpc").Err(err)
45 | errChan <- err
46 | return
47 | }
48 |
49 | s.grpcServer = grpc.NewServer()
50 | s.vatzRPCService = &grpcService{
51 | healthChecker: healthcheck.GetHealthChecker(),
52 | }
53 |
54 | vatzpb.RegisterVatzRPCServer(s.grpcServer, s.vatzRPCService)
55 | reflection.Register(s.grpcServer)
56 |
57 | err = s.grpcServer.Serve(l)
58 | if err != nil {
59 | log.Info().Str("module", "rpc").Err(err)
60 | errChan <- err
61 | return
62 | }
63 | }(errChan)
64 |
65 | go func(errChan chan<- error) {
66 | httpAddr := fmt.Sprintf("%s:%d", address, httpPort)
67 | log.Info().Str("module", "rpc").Msgf("start gRPC gateway server %s", httpAddr)
68 |
69 | mux := runtime.NewServeMux()
70 |
71 | opts := []grpc.DialOption{
72 | grpc.WithTransportCredentials(insecure.NewCredentials()),
73 | }
74 |
75 | grpcAddr := fmt.Sprintf("%s:%d", address, grpcPort)
76 | err := vatzpb.RegisterVatzRPCHandlerFromEndpoint(s.ctx, mux, grpcAddr, opts)
77 | if err != nil {
78 | log.Info().Str("module", "rpc").Err(err)
79 | errChan <- err
80 | return
81 | }
82 |
83 | s.httpServer = &http.Server{
84 | Addr: httpAddr,
85 | Handler: mux,
86 | }
87 |
88 | err = s.httpServer.ListenAndServe()
89 | if err != nil {
90 | log.Info().Str("module", "rpc").Err(err)
91 | errChan <- err
92 | return
93 | }
94 | }(errChan)
95 |
96 | err := <-errChan
97 |
98 | log.Info().Str("module", "rpc").Err(err)
99 |
100 | return err
101 | }
102 |
103 | func (s *rpcService) Stop() {
104 | log.Info().Str("module", "rpc").Msg("cancel")
105 | defer s.cancel()
106 |
107 | if s.httpServer != nil {
108 | err := s.httpServer.Shutdown(s.ctx)
109 | if err != nil {
110 | log.Error().Str("module", "rpc").Err(err)
111 | }
112 |
113 | }
114 |
115 | log.Info().Str("module", "rpc").Msg("stop")
116 | if s.grpcServer != nil {
117 | s.grpcServer.Stop()
118 | }
119 | }
120 |
121 | // NewRPCService creates new rpc server instance.
122 | func NewRPCService() VatzRPC {
123 | ctx := context.Background()
124 | ctx, cancel := context.WithCancel(ctx)
125 |
126 | return &rpcService{
127 | ctx: ctx,
128 | cancel: cancel,
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/manager/dispatcher/pagerduty.go:
--------------------------------------------------------------------------------
1 | package dispatcher
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | tp "github.com/dsrvlabs/vatz/types"
7 | "github.com/dsrvlabs/vatz/utils"
8 | "sync"
9 | "time"
10 |
11 | pd "github.com/PagerDuty/go-pagerduty"
12 | pb "github.com/dsrvlabs/vatz-proto/plugin/v1"
13 | "github.com/rs/zerolog/log"
14 | )
15 |
16 | // SUCCESS is string for delivering success.
17 | const SUCCESS = "success"
18 |
19 | type pagerdutyMSGEvent struct {
20 | flag string
21 | deKey string
22 | }
23 |
24 | type pagerduty struct {
25 | host string
26 | channel tp.Channel
27 | secret string
28 | subscriptions []string
29 | pagerEntry sync.Map
30 | }
31 |
32 | func (p *pagerduty) SetDispatcher(firstRunMsg bool, preStat tp.StateFlag, notifyInfo tp.NotifyInfo) error {
33 | reqToNotify, _, deliverMessage := messageHandler(firstRunMsg, preStat, notifyInfo)
34 | subscriptionEnabled, isSubscriptionIncluded := utils.IsSubscribeSpecific(p.subscriptions, notifyInfo.Plugin)
35 | if !subscriptionEnabled || subscriptionEnabled && isSubscriptionIncluded {
36 | if reqToNotify {
37 | err := p.SendNotification(deliverMessage)
38 | if err != nil {
39 | log.Error().Str("module", "dispatcher").Msgf("Channel(Pagerduty): Send notification error: %s", err)
40 | return err
41 | }
42 | }
43 | }
44 | return nil
45 | }
46 |
47 | func (p *pagerduty) SendNotification(msg tp.ReqMsg) error {
48 |
49 | var (
50 | pagerdutySeverity string
51 | emoji string
52 | methodName = msg.Options["pUnique"].(string)
53 | )
54 | /*
55 | Pagerduty severity Allowed values:
56 | - critical
57 | - warning
58 | - error
59 | - info
60 | */
61 | switch {
62 | case msg.Severity == pb.SEVERITY_INFO:
63 | pagerdutySeverity = "info"
64 | emoji = emojiCheck
65 | case msg.Severity == pb.SEVERITY_CRITICAL:
66 | pagerdutySeverity = "critical"
67 | emoji = "‼️"
68 | case msg.Severity == pb.SEVERITY_WARNING:
69 | pagerdutySeverity = "warning"
70 | emoji = "❗"
71 | default:
72 | emoji = emojiER
73 | pagerdutySeverity = "error"
74 | }
75 |
76 | v2EventPayload := &pd.V2Payload{
77 | Source: fmt.Sprintf(`(%s)`, p.host),
78 | Component: msg.ResourceType,
79 | Severity: pagerdutySeverity,
80 | Summary: fmt.Sprintf(`
81 | %s %s %s
82 | (%s)
83 | Plugin Name: %s
84 | %s`, emoji, msg.Severity.String(), emoji, p.host, msg.ResourceType, msg.Msg)}
85 |
86 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
87 | defer cancel()
88 |
89 | if pb.STATE_SUCCESS != msg.State || pb.SEVERITY_INFO != msg.Severity {
90 | resp, err := pd.ManageEventWithContext(ctx, pd.V2Event{RoutingKey: p.secret, Action: "trigger", Payload: v2EventPayload})
91 | if err != nil {
92 | log.Error().Str("module", "dispatcher").Msgf("Channel(Pagerduty): Connection failed due to Error: %s", err)
93 | return err
94 | }
95 | if resp.Status == SUCCESS {
96 | preps := make([]pagerdutyMSGEvent, 0)
97 | if prepTriggers, ok := p.pagerEntry.Load(methodName); ok {
98 | preps = prepTriggers.([]pagerdutyMSGEvent)
99 | }
100 | preps = append(preps, pagerdutyMSGEvent{flag: pagerdutySeverity, deKey: resp.DedupKey})
101 | p.pagerEntry.Store(methodName, preps)
102 | }
103 | } else if pdResolver, ok := p.pagerEntry.Load(methodName); ok {
104 | resolver := pdResolver.([]pagerdutyMSGEvent)
105 | for _, prepFlags := range resolver {
106 | _, err := pd.ManageEventWithContext(ctx, pd.V2Event{
107 | RoutingKey: p.secret,
108 | Action: "resolve",
109 | DedupKey: prepFlags.deKey,
110 | Payload: v2EventPayload,
111 | })
112 | if err != nil {
113 | log.Error().Str("module", "dispatcher").Msgf("Channel(Pagerduty): Connection failed due to Error: %s", err)
114 | return err
115 | }
116 | }
117 | p.pagerEntry.Store(methodName, make([]pagerdutyMSGEvent, 0))
118 | }
119 |
120 | return nil
121 | }
122 |
--------------------------------------------------------------------------------
/manager/dispatcher/slack.go:
--------------------------------------------------------------------------------
1 | package dispatcher
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | tp "github.com/dsrvlabs/vatz/types"
8 | "github.com/dsrvlabs/vatz/utils"
9 | "net/http"
10 | "sync"
11 |
12 | pb "github.com/dsrvlabs/vatz-proto/plugin/v1"
13 | "github.com/robfig/cron/v3"
14 | "github.com/rs/zerolog/log"
15 | )
16 |
17 | // slack: This is a sample code
18 | // that helps to multi methods for notification.
19 | type slack struct {
20 | host string
21 | channel tp.Channel
22 | secret string
23 | subscriptions []string
24 | reminderSchedule []string
25 | reminderCron *cron.Cron
26 | entry sync.Map
27 | }
28 |
29 | type SlackRequestBody struct {
30 | Text string `json:"text"`
31 | }
32 |
33 | func (s *slack) SetDispatcher(firstRunMsg bool, preStat tp.StateFlag, notifyInfo tp.NotifyInfo) error {
34 | reqToNotify, reminderState, deliverMessage := messageHandler(firstRunMsg, preStat, notifyInfo)
35 | pUnique := deliverMessage.Options["pUnique"].(string)
36 | subscriptionEnabled, isSubscriptionIncluded := utils.IsSubscribeSpecific(s.subscriptions, notifyInfo.Plugin)
37 | if !subscriptionEnabled || subscriptionEnabled && isSubscriptionIncluded {
38 | if reqToNotify {
39 | err := s.SendNotification(deliverMessage)
40 | if err != nil {
41 | log.Error().Str("module", "dispatcher").Msgf("Channel(Slack): Send notification error: %s", err)
42 | return err
43 | }
44 |
45 | }
46 | }
47 |
48 | if reminderState == tp.ON {
49 | newEntries := []cron.EntryID{}
50 | /*
51 | In case of reminder has to keep but stateFlag has changed,
52 | e.g.) CRITICAL -> WARNING
53 | e.g.) ERROR -> INFO -> ERROR
54 | */
55 | if entries, ok := s.entry.Load(pUnique); ok {
56 | for _, entry := range entries.([]cron.EntryID) {
57 | s.reminderCron.Remove(entry)
58 | }
59 | s.reminderCron.Stop()
60 | }
61 | for _, schedule := range s.reminderSchedule {
62 | id, _ := s.reminderCron.AddFunc(schedule, func() {
63 | err := s.SendNotification(deliverMessage)
64 | if err != nil {
65 | log.Error().Str("module", "dispatcher").Msgf("Channel(Slack): Send notification error: %s", err)
66 | }
67 | })
68 | newEntries = append(newEntries, id)
69 | }
70 | s.entry.Store(pUnique, newEntries)
71 | s.reminderCron.Start()
72 | } else if reminderState == tp.OFF {
73 | entries, _ := s.entry.Load(pUnique)
74 | if _, ok := entries.([]cron.EntryID); ok {
75 | for _, entity := range entries.([]cron.EntryID) {
76 | s.reminderCron.Remove(entity)
77 | }
78 | s.reminderCron.Stop()
79 | }
80 | }
81 | return nil
82 | }
83 |
84 | func (s *slack) SendNotification(msg tp.ReqMsg) error {
85 | var (
86 | err error
87 | emoji = emojiER
88 | )
89 |
90 | if msg.State == pb.STATE_SUCCESS {
91 | switch {
92 | case msg.Severity == pb.SEVERITY_CRITICAL:
93 | emoji = emojiDoubleEX
94 | case msg.Severity == pb.SEVERITY_WARNING:
95 | emoji = emojiSingleEx
96 | case msg.Severity == pb.SEVERITY_INFO:
97 | emoji = emojiCheck
98 | }
99 | }
100 | sendingText := fmt.Sprintf(`
101 | %s *%s* %s
102 | >
103 | Host: *%s*
104 | Plugin Name: _%s_
105 | %s`, emoji, msg.Severity.String(), emoji, s.host, msg.ResourceType, msg.Msg)
106 | slackBody, _ := json.Marshal(SlackRequestBody{Text: sendingText})
107 | req, err := http.NewRequest(http.MethodPost, s.secret, bytes.NewBuffer(slackBody))
108 | if err != nil {
109 | return err
110 | }
111 |
112 | req.Header.Add("Content-Type", "application/json")
113 |
114 | client := &http.Client{}
115 | resp, err := client.Do(req)
116 | if err != nil {
117 | return err
118 | }
119 |
120 | buf := new(bytes.Buffer)
121 | _, err = buf.ReadFrom(resp.Body)
122 | if err != nil {
123 | return err
124 | }
125 | if buf.String() != "ok" {
126 | return fmt.Errorf("non-ok response returned from Slack")
127 | }
128 |
129 | return nil
130 | }
131 |
--------------------------------------------------------------------------------
/manager/healthcheck/general_healthcheck.go:
--------------------------------------------------------------------------------
1 | package healthcheck
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/dsrvlabs/vatz/types"
7 | "sync"
8 | "time"
9 |
10 | pb "github.com/dsrvlabs/vatz-proto/plugin/v1"
11 | "github.com/dsrvlabs/vatz/manager/config"
12 | dp "github.com/dsrvlabs/vatz/manager/dispatcher"
13 | "github.com/dsrvlabs/vatz/utils"
14 | "github.com/robfig/cron/v3"
15 | "github.com/rs/zerolog/log"
16 | "google.golang.org/protobuf/types/known/emptypb"
17 | )
18 |
19 | var (
20 | healthCheckerOnce = sync.Once{}
21 | healthCheckerSingle = healthChecker{}
22 | )
23 |
24 | type healthChecker struct {
25 | healthMSG types.ReqMsg
26 | pluginStatus sync.Map
27 | }
28 |
29 | func (h *healthChecker) PluginHealthCheck(ctx context.Context, gClient pb.PluginClient, plugin config.Plugin, dispatchers []dp.Dispatcher) (types.AliveStatus, error) {
30 | isAlive := types.AliveStatusUp
31 | sendMSG := false
32 | pUnique := utils.MakeUniqueValue(plugin.Name, plugin.Address, plugin.Port)
33 | verify, err := gClient.Verify(ctx, new(emptypb.Empty))
34 |
35 | option := map[string]interface{}{"pUnique": pUnique}
36 |
37 | deliverMSG := types.ReqMsg{
38 | FuncName: "isPluginUp",
39 | State: pb.STATE_FAILURE,
40 | Msg: "Plugin is DOWN!!",
41 | Severity: pb.SEVERITY_CRITICAL,
42 | ResourceType: plugin.Name,
43 | Options: option,
44 | }
45 |
46 | if _, ok := h.pluginStatus.Load(pUnique); !ok {
47 | if err != nil || verify == nil {
48 | isAlive = types.AliveStatusDown
49 | sendMSG = true
50 | }
51 | } else {
52 | plStat, _ := h.pluginStatus.Load(pUnique)
53 | pStruct := plStat.(*types.PluginStatus)
54 | if err != nil || verify == nil {
55 | isAlive = types.AliveStatusDown
56 | if pStruct.IsAlive == types.AliveStatusUp {
57 | sendMSG = true
58 | }
59 | } else {
60 | if pStruct.IsAlive == types.AliveStatusDown {
61 | sendMSG = true
62 | deliverMSG.UpdateSeverity(pb.SEVERITY_INFO)
63 | deliverMSG.UpdateState(pb.STATE_SUCCESS)
64 | deliverMSG.UpdateMSG("Plugin is Alive.")
65 | }
66 | }
67 | }
68 |
69 | if sendMSG {
70 | errorCount := 0
71 | for _, dispatcher := range dispatchers {
72 | sendNotificationError := dispatcher.SendNotification(deliverMSG)
73 | if sendNotificationError != nil {
74 | log.Error().Str("module", "healthcheck").Msgf("failed to send notification: %v", err)
75 | errorCount = errorCount + 1
76 | }
77 | }
78 |
79 | if len(dispatchers) == errorCount {
80 | log.Error().Str("module", "healthcheck").Msg("All Dispatchers failed to send a notifications, Please, Check your dispatcher configs.")
81 | return isAlive, fmt.Errorf("Failed to send all configured notifications. ")
82 | }
83 | }
84 |
85 | h.pluginStatus.Store(pUnique, &types.PluginStatus{
86 | Plugin: plugin,
87 | IsAlive: isAlive,
88 | LastCheck: time.Now(),
89 | })
90 |
91 | return isAlive, nil
92 | }
93 |
94 | // VATZHealthCheck send a notification at a specific time that the vatz is alive.
95 | func (h *healthChecker) VATZHealthCheck(healthCheckerSchedule []string, dispatchers []dp.Dispatcher) error {
96 | c := cron.New(cron.WithLocation(time.UTC))
97 | for i := 0; i < len(healthCheckerSchedule); i++ {
98 | _, err := c.AddFunc(healthCheckerSchedule[i], func() {
99 | for _, dispatcher := range dispatchers {
100 | err := dispatcher.SendNotification(h.healthMSG)
101 | if err != nil {
102 | log.Error().Str("module", "dispatcher").Msgf("failed to send notification: %v", err)
103 | }
104 | }
105 | })
106 | if err != nil {
107 | log.Error().Str("module", "healthcheck").Msgf("failed to add function to cron: %v", err)
108 | }
109 | }
110 | c.Start()
111 | return nil
112 | }
113 |
114 | func (h *healthChecker) PluginStatus(ctx context.Context) []types.PluginStatus {
115 | status := make([]types.PluginStatus, 0)
116 |
117 | h.pluginStatus.Range(func(k, value any) bool {
118 | curStatus := value.(*types.PluginStatus)
119 | status = append(status, *curStatus)
120 | return true
121 | })
122 |
123 | return status
124 | }
125 |
--------------------------------------------------------------------------------
/docs/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing to Vatz
2 |
3 | > You can contribute to Vatz with issues and PRs.
4 | > Simply filing issues for problems you encounter is a great way to contribute.
5 | > Contributing implementations are greatly appreciated.
6 |
7 | ## 1. Register Issue
8 |
9 | There are 4 types of issue templates in [dsrvlabs/vatz](https://github.com/dsrvlabs)
10 | >
11 |
12 | ### Step 1. Feature Request (optional)
13 | > Please use a `Feature Request` template if you prefer.
14 |
15 | Anyone can register an issue to request a new feature that enhances our system.
16 | `Feature Request` step isn't mandatory for all feature development but good to start with.
17 | The code owner(@xellos00) can set the priority of the issue or close it. if the issue is registered without this step for discussion.
18 |
19 | ### Step 2. Register Issue
20 |
21 | - `Bug Report`:
22 | This template is used for raising an issue to report a bug or bug fix that's been reported.
23 |
24 | - `Feature Development`:
25 | This template is used to set the next step of development once the discussion is over in the previous step(`Feature request`).
26 | You can specify the purpose when you create an issue for feature development.
27 | - New Feature for the Service/Plugin (Vatz)
28 | - Enhancement (Vatz)
29 | - others(etc. e.g, documentation, project policy management)
30 |
31 | - `CI/CD Implementation`:
32 | Please use this template for the purpose of setting a CI/CD pipeline for Vatz (i.e, labeling, and discussion for new policies.)
33 |
34 | ### step 3. Fill out the info
35 |
36 | Please, fill out all the following info as much as possible when you register the issue.
37 | - Assignee
38 | - Labels
39 | - Projects
40 | - Milestone
41 |
42 |
43 |
44 |
45 | ## 2. Assign assignee
46 | **Voluntary**
47 |
48 | Anyone can assign themselves if they want to do certain tasks.
49 |
50 | **Mandatory**
51 |
52 | The code owner(@xellos00) sets an assignee if there's an appropriate assignee available in the team for certain tasks. We may have a discussion prior to setting an assignee if needed.
53 |
54 | Please, let the code owner(@xellos00) know if there are difficulties with the assigned task.
55 | - Go over with everyone on Vatz bi-weekly meeting
56 | - Switch the assignee through a meeting or request.
57 |
58 | ## 3. PR policy
59 | > These are rules for PR for VATZ project.
60 |
61 | - PR - First Review approved / First Merge.
62 | - You must delete the branch that has been merged.
63 | - If you raise the PR, you must track their PR status until their PR is closed.
64 | - Anyone who comments on PR while reviewing the process has an obligation to resolve/close their comment when the assignee has fixed a comment or suggestion.
65 | - You must include one of the keywords in `close` or `related` as below.
66 | - Put `close` keyword when you would like to close an issue.
67 | - Put `related` keyword when you would like to comment related to an issue.
68 |
69 |
70 | ---
71 |
72 |
73 | ## DSRV Validator Team's development process
74 | (Note: This section is only for DSRV's internal development process)
75 |
76 | >The ultimate goal of the validator team is to:
77 | >- Maximizing uptime
78 | >- Following up managed schedule per protocols (binary updates, new spork, epoch, vote, etc)
79 | >- Contributing to boost protocol if there are any further improvements are available.
80 |
81 | **Processing assigned issues during the Sprints**
82 |
83 | 1. At least 1 to 2 issues have to be addressed within the Sprint(two weeks).
84 | 2. Assignee has to finish the assigned or selected issue as much as possible, leave a comment if it can't be finished within the sprint.
85 | 3. As the validator team's main task is node operation and therefore leave issue handling to each member's own will.
86 | Close the issue, re-register and change to another assignee if it is difficult to proceed during the 2 sprints(4 weeks).
87 |
--------------------------------------------------------------------------------
/manager/executor/general_executor.go:
--------------------------------------------------------------------------------
1 | package executor
2 |
3 | import (
4 | "context"
5 | tp "github.com/dsrvlabs/vatz/types"
6 | "os"
7 | "sync"
8 |
9 | "github.com/rs/zerolog"
10 |
11 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
12 | "github.com/dsrvlabs/vatz/manager/config"
13 | dp "github.com/dsrvlabs/vatz/manager/dispatcher"
14 | "github.com/dsrvlabs/vatz/utils"
15 | "github.com/rs/zerolog/log"
16 | "google.golang.org/protobuf/types/known/structpb"
17 | )
18 |
19 | type executor struct {
20 | status sync.Map
21 | }
22 |
23 | func (s *executor) Execute(ctx context.Context, gClient pluginpb.PluginClient, plugin config.Plugin, dispatchers []dp.Dispatcher) error {
24 | executeMethods := plugin.ExecutableMethods
25 | log.Debug().Str("module", "executor").Msg("Execute is called.")
26 | for _, method := range executeMethods {
27 | optionMap := map[string]interface{}{
28 | "plugin_name": plugin.Name,
29 | }
30 |
31 | options, err := structpb.NewStruct(optionMap)
32 | if err != nil {
33 | log.Error().Str("module", "executor").Msgf("failed to check target structpb: %v", err)
34 | os.Exit(1)
35 | }
36 |
37 | //TODO: Please, add new logic to add param into Map.
38 |
39 | methodMap := map[string]interface{}{
40 | "execute_method": method.Name,
41 | }
42 |
43 | executeInfo, err := structpb.NewStruct(methodMap)
44 | if err != nil {
45 | log.Error().Str("module", "executor").Msgf("failed to check target structpb: %v", err)
46 | os.Exit(1)
47 | }
48 |
49 | req := &pluginpb.ExecuteRequest{
50 | ExecuteInfo: executeInfo,
51 | Options: options,
52 | }
53 |
54 | if zerolog.GlobalLevel() == zerolog.DebugLevel {
55 | log.Debug().Str("module", "executor").Msgf("request (Plugin Name: %s, Method Name: %s)", plugin.Name, method.Name)
56 | } else {
57 | log.Info().Str("module", "executor").Msgf("Executor send request to %s", plugin.Name)
58 | }
59 |
60 | resp, err := s.execute(ctx, gClient, req)
61 | if err != nil {
62 | return err
63 | }
64 |
65 | pUnique := utils.MakeUniqueValue(plugin.Name, plugin.Address, plugin.Port)
66 | firstExe, preStatus := s.updateState(pUnique, resp)
67 |
68 | for _, dpSingle := range dispatchers {
69 | err = dpSingle.SetDispatcher(firstExe, preStatus, tp.NotifyInfo{
70 | Plugin: plugin.Name,
71 | Method: method.Name,
72 | Address: plugin.Address,
73 | Port: plugin.Port,
74 | Severity: resp.GetSeverity(),
75 | State: resp.GetState(),
76 | ExecuteMsg: resp.GetMessage(),
77 | })
78 | if err != nil {
79 | log.Error().Str("module", "dispatcher").Msgf("failed to set dispatcher: %v", err)
80 | }
81 | }
82 | }
83 |
84 | return nil
85 | }
86 |
87 | func (s *executor) execute(ctx context.Context, gClient pluginpb.PluginClient, in *pluginpb.ExecuteRequest) (*pluginpb.ExecuteResponse, error) {
88 | log.Debug().Str("module", "executor").Msgf("func execute")
89 | resp, err := gClient.Execute(ctx, in)
90 | if err != nil || resp == nil {
91 | return &pluginpb.ExecuteResponse{
92 | State: pluginpb.STATE_FAILURE,
93 | Message: "API Execution Failed",
94 | Severity: pluginpb.SEVERITY_ERROR,
95 | ResourceType: "ResourceType Unknown",
96 | }, nil
97 | }
98 |
99 | if zerolog.GlobalLevel() == zerolog.DebugLevel {
100 | log.Debug().Str("module", "executor").Msgf("response (res message:%s, res State: %s) ", resp.Message, resp.State)
101 | } else {
102 | log.Info().Str("module", "executor").Msgf("response: %s", resp.State)
103 | }
104 |
105 | return resp, err
106 | }
107 |
108 | func (s *executor) updateState(unique string, resp *pluginpb.ExecuteResponse) (bool, tp.StateFlag) {
109 | log.Debug().Str("module", "executor").Msgf("func updateState")
110 | isFirstRun := false
111 | exeResp := tp.StateFlag{State: resp.GetState(), Severity: resp.GetSeverity()}
112 | if _, ok := s.status.Load(unique); !ok {
113 | isFirstRun = true
114 | s.status.Store(unique, exeResp)
115 | } else {
116 | preStatus, _ := s.status.Load(unique)
117 | preVal := preStatus.(tp.StateFlag)
118 | if preVal.State != resp.State || preVal.Severity != resp.Severity {
119 | s.status.Store(unique, exeResp)
120 | exeResp = tp.StateFlag{State: preVal.State, Severity: preVal.Severity}
121 | } else {
122 | exeResp = tp.StateFlag{State: preVal.State, Severity: preVal.Severity}
123 | }
124 | }
125 | return isFirstRun, exeResp
126 | }
127 |
--------------------------------------------------------------------------------
/types/manager.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
5 | "github.com/robfig/cron/v3"
6 | "time"
7 |
8 | "github.com/dsrvlabs/vatz/manager/config"
9 | )
10 |
11 | /*
12 | * Discord Types
13 | *
14 | */
15 | type DiscordColor int
16 |
17 | // ReqMsg is Setup message into GRPC Type.
18 | type ReqMsg struct {
19 | FuncName string `json:"func_name"`
20 | State pluginpb.STATE `json:"state"`
21 | Msg string `json:"msg"`
22 | Severity pluginpb.SEVERITY `json:"severity"`
23 | ResourceType string `json:"resource_type"`
24 | Options map[string]interface{} `json:"options"`
25 | }
26 |
27 | // UpdateState is to uptade the state of pluginpb.
28 | func (r *ReqMsg) UpdateState(stat pluginpb.STATE) {
29 | r.State = stat
30 | }
31 |
32 | // UpdateSeverity is to uptade the severity of pluginpb.
33 | func (r *ReqMsg) UpdateSeverity(sev pluginpb.SEVERITY) {
34 | r.Severity = sev
35 | }
36 |
37 | // UpdateMSG is to update message.
38 | func (r *ReqMsg) UpdateMSG(message string) {
39 | r.Msg = message
40 | }
41 |
42 | // DiscordMsg is type for sending messages to a discord.
43 | type DiscordMsg struct {
44 | Username string `json:"username,omitempty"`
45 | AvatarURL string `json:"avatar_url,omitempty"`
46 | Content string `json:"content,omitempty"`
47 | Embeds []Embed `json:"embeds"`
48 | }
49 |
50 | // Embed is information for a detailed message.
51 | type Embed struct {
52 | Author struct {
53 | Name string `json:"name,omitempty"`
54 | URL string `json:"url,omitempty"`
55 | IconURL string `json:"icon_url,omitempty"`
56 | } `json:"author,omitempty"`
57 | Title string `json:"title"`
58 | URL string `json:"url,omitempty"`
59 | Timestamp time.Time `json:"timestamp"`
60 | Description string `json:"description"`
61 | Color DiscordColor `json:"color"`
62 | Fields []Field `json:"fields,omitempty"`
63 | Thumbnail struct {
64 | URL string `json:"url,omitempty"`
65 | } `json:"thumbnail,omitempty"`
66 | Image struct {
67 | URL string `json:"url,omitempty"`
68 | } `json:"image,omitempty"`
69 | Footer struct {
70 | Text string `json:"text,omitempty"`
71 | IconURL string `json:"icon_url,omitempty"`
72 | } `json:"footer,omitempty"`
73 | }
74 |
75 | // StateFlag is type that indicates the status of the plugins.
76 | type StateFlag struct {
77 | State pluginpb.STATE `json:"state"`
78 | Severity pluginpb.SEVERITY `json:"severity"`
79 | }
80 |
81 | // CronTabSt is crontab structure.
82 | type CronTabSt struct {
83 | Crontab *cron.Cron `json:"crontab"`
84 | EntityID int `json:"entity_id"`
85 | }
86 |
87 | // Update is to update CronTabSt.
88 | func (in *CronTabSt) Update(entity int) {
89 | in.EntityID = entity
90 | }
91 |
92 | // Field is a structure for embeds that can be omitted.
93 | type Field struct {
94 | Name string `json:"name,omitempty"`
95 | Value string `json:"value,omitempty"`
96 | Inline bool `json:"inline,omitempty"`
97 | }
98 |
99 | // NotifyInfo contains detail dispatcher configs.
100 | type NotifyInfo struct {
101 | Plugin string `json:"plugin"`
102 | Method string `json:"method"`
103 | Address string `json:"address"`
104 | Port int `json:"port"`
105 | Severity pluginpb.SEVERITY `json:"severity"`
106 | State pluginpb.STATE `json:"state"`
107 | ExecuteMsg string `json:"execute_msg"`
108 | }
109 |
110 | // Channel types for dispatchers.
111 | type Channel string
112 |
113 | // the type of channel.
114 | const (
115 | Discord Channel = "DISCORD"
116 | Telegram Channel = "TELEGRAM"
117 | PagerDuty Channel = "PAGERDUTY"
118 | Slack Channel = "SLACK"
119 | )
120 |
121 | // Reminder is for reminnig alert.
122 | type Reminder string
123 |
124 | // The type of Reminder.
125 | const (
126 | ON Reminder = "ON"
127 | HANG Reminder = "HANG"
128 | OFF Reminder = "OFF"
129 | )
130 |
131 | /*
132 | * HealthCheck Types
133 | *
134 | */
135 | // AliveStatus is aliveness of plugin.
136 |
137 | type AliveStatus string
138 |
139 | // AliveStatus is type that describes aliveness flags.
140 | const (
141 | AliveStatusUp AliveStatus = "UP"
142 | AliveStatusDown AliveStatus = "DOWN"
143 | )
144 |
145 | // PluginStatus describes detail status of plugin.
146 | type PluginStatus struct {
147 | Plugin config.Plugin `json:"plugin"`
148 | IsAlive AliveStatus `json:"is_alive"`
149 | LastCheck time.Time `json:"last_check"`
150 | }
151 |
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "context"
5 | "crypto/sha256"
6 | "encoding/hex"
7 | "errors"
8 | "fmt"
9 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
10 | "github.com/dsrvlabs/vatz/manager/config"
11 | "github.com/rs/zerolog/log"
12 | "google.golang.org/grpc"
13 | "google.golang.org/grpc/connectivity"
14 | "google.golang.org/grpc/credentials/insecure"
15 | "os"
16 | "os/signal"
17 | "strconv"
18 | "sync"
19 | "syscall"
20 | "time"
21 | )
22 |
23 | type GClientWithPlugin struct {
24 | GRPCClient pluginpb.PluginClient
25 | PluginInfo config.Plugin
26 | }
27 |
28 | func MakeUniqueValue(pName, pAddr string, pPort int) string {
29 | return pName + pAddr + strconv.Itoa(pPort)
30 | }
31 |
32 | func ParseBool(str string) bool {
33 | switch str {
34 | case "true", "1", "on":
35 | return true
36 | case "false", "0", "off":
37 | return false
38 | default:
39 | return false
40 | }
41 | }
42 |
43 | /*
44 | Those funcs are internal purpose only.
45 |
46 | func: ConvertHashToInput
47 | func: UniqueHashValue
48 | */
49 | func ConvertHashToInput(hashValue string) string {
50 | // Decode the hash value from a hexadecimal string to a byte array
51 | hashBytes, _ := hex.DecodeString(hashValue)
52 |
53 | // Convert the byte array to a string
54 | originalString := string(hashBytes)
55 |
56 | return originalString
57 | }
58 |
59 | func UniqueHashValue(inputString string) string {
60 | // Create a SHA-256 hash object
61 | h := sha256.New()
62 | // Write the input string to the hash object
63 | h.Write([]byte(inputString))
64 | // Get the 256-bit hash value as a byte array
65 | hashBytes := h.Sum(nil)
66 | // Encode the hash value as a hexadecimal string
67 | hashString := hex.EncodeToString(hashBytes)
68 | // Truncate the string to 16 characters
69 | hashString = hashString[:16]
70 | return hashString
71 | }
72 |
73 | func GetClients(plugins []config.Plugin) []GClientWithPlugin {
74 | var (
75 | grpcClientWithPlugins []GClientWithPlugin
76 | wg sync.WaitGroup
77 | connectionCancel = 10
78 | )
79 |
80 | for _, plugin := range plugins {
81 | wg.Add(1)
82 | pluginAddress := fmt.Sprintf("%s:%d", plugin.Address, plugin.Port)
83 |
84 | go func(addr string, configPlugin config.Plugin) {
85 | defer wg.Done()
86 | conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
87 | if err != nil {
88 | log.Fatal().Str("module", "main").Msgf("gRPC Dial Error(%s): %s", configPlugin.Name, err)
89 | }
90 | // Create a context for the connection check.
91 |
92 | grpcClientWithPlugins = append(grpcClientWithPlugins, GClientWithPlugin{GRPCClient: pluginpb.NewPluginClient(conn),
93 | PluginInfo: configPlugin})
94 |
95 | executeTicker := time.Duration(connectionCancel) * time.Second
96 | ctx, cancel := context.WithTimeout(context.Background(), executeTicker)
97 | defer cancel()
98 |
99 | // Block until the connection is ready or until the context times out.
100 | if err := waitForConnection(ctx, conn); err != nil {
101 | log.Error().Str("module", "util").Msgf("Connection to %s (plugin:%s) failed: %v\n", addr, configPlugin.Name, err)
102 | return
103 | }
104 |
105 | if conn.GetState() == connectivity.Ready {
106 | log.Info().Str("module", "util").Msgf("Client successfully connected to %s (plugin:%s).", addr, configPlugin.Name)
107 | }
108 | }(pluginAddress, plugin)
109 | }
110 | wg.Wait()
111 |
112 | return grpcClientWithPlugins
113 | }
114 |
115 | // waitForConnection blocks until the gRPC connection is ready or the context times out.
116 | func waitForConnection(ctx context.Context, conn *grpc.ClientConn) error {
117 | for {
118 | state := conn.GetState()
119 | if state == connectivity.Ready {
120 | return nil // Connection is ready
121 | }
122 |
123 | select {
124 | case <-ctx.Done():
125 | log.Error().Str("module", "util").Msg("Connection is timed out. Please Check your plugins' status. ")
126 | return errors.New("")
127 | default:
128 | // Wait a short period before checking the connection state again.
129 | time.Sleep(100 * time.Millisecond)
130 | }
131 | }
132 | }
133 |
134 | func InitializeChannel() chan os.Signal {
135 | sigs := make(chan os.Signal, 1)
136 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
137 | return sigs
138 | }
139 |
140 | func IsSubscribeSpecific(subscriptionList []string, PluginName string) (bool, bool) {
141 | doesSpecificSubscribe := false
142 | if len(subscriptionList) > 0 {
143 | doesSpecificSubscribe = true
144 | }
145 | isSameFlagExists := false
146 | for _, v := range subscriptionList {
147 | if v == PluginName {
148 | isSameFlagExists = true
149 | break
150 | }
151 | }
152 | return doesSpecificSubscribe, isSameFlagExists
153 | }
154 |
--------------------------------------------------------------------------------
/manager/dispatcher/telegram.go:
--------------------------------------------------------------------------------
1 | package dispatcher
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | tp "github.com/dsrvlabs/vatz/types"
8 | "github.com/dsrvlabs/vatz/utils"
9 | "io"
10 | "net/http"
11 | "sync"
12 |
13 | pb "github.com/dsrvlabs/vatz-proto/plugin/v1"
14 | "github.com/robfig/cron/v3"
15 | "github.com/rs/zerolog/log"
16 | )
17 |
18 | // telegram: This is a sample code
19 | // that helps to multi methods for notification.
20 | type telegram struct {
21 | host string
22 | channel tp.Channel
23 | secret string
24 | chatID string
25 | subscriptions []string
26 | reminderSchedule []string
27 | reminderCron *cron.Cron
28 | entry sync.Map
29 | }
30 |
31 | func (t *telegram) SetDispatcher(firstRunMsg bool, preStat tp.StateFlag, notifyInfo tp.NotifyInfo) error {
32 | reqToNotify, reminderState, deliverMessage := messageHandler(firstRunMsg, preStat, notifyInfo)
33 | pUnique := deliverMessage.Options["pUnique"].(string)
34 | subscriptionEnabled, isSubscriptionIncluded := utils.IsSubscribeSpecific(t.subscriptions, notifyInfo.Plugin)
35 | if !subscriptionEnabled || subscriptionEnabled && isSubscriptionIncluded {
36 | if reqToNotify {
37 | err := t.SendNotification(deliverMessage)
38 | if err != nil {
39 | log.Error().Str("module", "dispatcher").Msgf("Channel(Telegram): Send notification error: %s", err)
40 | return err
41 | }
42 |
43 | }
44 | }
45 | if reminderState == tp.ON {
46 | newEntries := []cron.EntryID{}
47 | /*
48 | In case of reminder has to keep but stateFlag has changed,
49 | e.g.) CRITICAL -> WARNING
50 | e.g.) ERROR -> INFO -> ERROR
51 | */
52 | if entries, ok := t.entry.Load(pUnique); ok {
53 | for _, entry := range entries.([]cron.EntryID) {
54 | t.reminderCron.Remove(entry)
55 | }
56 | t.reminderCron.Stop()
57 | }
58 | for _, schedule := range t.reminderSchedule {
59 | id, _ := t.reminderCron.AddFunc(schedule, func() {
60 | err := t.SendNotification(deliverMessage)
61 | if err != nil {
62 | log.Error().Str("module", "dispatcher").Msgf("Channel(Telegram): Send notification error: %s", err)
63 | }
64 | })
65 | newEntries = append(newEntries, id)
66 | }
67 | t.entry.Store(pUnique, newEntries)
68 | t.reminderCron.Start()
69 | } else if reminderState == tp.OFF {
70 | entries, _ := t.entry.Load(pUnique)
71 | if _, ok := entries.([]cron.EntryID); ok {
72 | for _, entity := range entries.([]cron.EntryID) {
73 | t.reminderCron.Remove(entity)
74 | }
75 | t.reminderCron.Stop()
76 | }
77 | }
78 | return nil
79 | }
80 |
81 | func (t *telegram) SendNotification(msg tp.ReqMsg) error {
82 | var (
83 | err error
84 | response *http.Response
85 | emoji = emojiER
86 | )
87 |
88 | if msg.State == pb.STATE_SUCCESS {
89 | switch {
90 | case msg.Severity == pb.SEVERITY_CRITICAL:
91 | emoji = emojiDoubleEX
92 | case msg.Severity == pb.SEVERITY_WARNING:
93 | emoji = emojiSingleEx
94 | case msg.Severity == pb.SEVERITY_INFO:
95 | emoji = emojiCheck
96 | }
97 | }
98 |
99 | url := fmt.Sprintf("%s/sendMessage", getURL(t.secret))
100 | sendingText := fmt.Sprintf(`
101 | %s%s%s
102 | Host: %s
103 | Plugin Name: %s
104 | %s`, emoji, msg.Severity.String(), emoji, t.host, msg.ResourceType, msg.Msg)
105 |
106 | body, _ := json.Marshal(map[string]string{
107 | "chat_id": t.chatID,
108 | "text": sendingText,
109 | "parse_mode": "html",
110 | })
111 |
112 | response, err = http.Post(url, "application/json", bytes.NewBuffer(body))
113 | if err != nil {
114 | log.Error().Str("module", "dispatcher").Msgf("dispatcher telegram Error: %s", err)
115 | return err
116 | }
117 | defer response.Body.Close()
118 |
119 | if response.StatusCode < 200 || response.StatusCode > 299 {
120 | log.Error().Str("module", "dispatcher").Msgf("Channel(Telegram): Error in Response with Error code: %d", response.StatusCode)
121 | return fmt.Errorf("REST API Error with HTTP response status code: %d", response.StatusCode)
122 | }
123 |
124 | body, err = io.ReadAll(response.Body)
125 | if err != nil {
126 | log.Error().Str("module", "dispatcher").Msgf("Channel(Telegram): body parsing Error: %s", err)
127 | return err
128 | }
129 | respJSON := make(map[string]interface{})
130 | err = json.Unmarshal(body, &respJSON)
131 | if err != nil {
132 | log.Error().Str("module", "dispatcher").Msgf("Channel(Telegram): Unmarshalling JSON Error: %s", err)
133 | return err
134 | }
135 | if !respJSON["ok"].(bool) {
136 | log.Error().Str("module", "dispatcher").Msg("Channel(Telegram): Connection failed due to Invalid telegram token.")
137 | return fmt.Errorf("Invalid telegram token. ")
138 | }
139 |
140 | return nil
141 | }
142 |
143 | func getURL(token string) string {
144 | return fmt.Sprintf("https://api.telegram.org/bot%s", token)
145 | }
146 |
--------------------------------------------------------------------------------
/manager/dispatcher/dispatcher.go:
--------------------------------------------------------------------------------
1 | package dispatcher
2 |
3 | import (
4 | "errors"
5 | tp "github.com/dsrvlabs/vatz/types"
6 | "strings"
7 | "sync"
8 | "time"
9 |
10 | pb "github.com/dsrvlabs/vatz-proto/plugin/v1"
11 | "github.com/dsrvlabs/vatz/manager/config"
12 | "github.com/dsrvlabs/vatz/utils"
13 | "github.com/robfig/cron/v3"
14 | "github.com/rs/zerolog/log"
15 | )
16 |
17 | /* TODO: Discussion.
18 | We need to discuss about notificatino module.
19 | As I see this code, dispatcher itself is described is dispatcher
20 | but dispatcher and dispatcher module should be splitted into two part.
21 | */
22 |
23 | const (
24 | emojiER string = "🚨"
25 | emojiDoubleEX string = "‼️"
26 | emojiSingleEx string = "❗"
27 | emojiCheck string = "✅"
28 | )
29 |
30 | var (
31 | dispatcherSingletons []Dispatcher
32 | dispatcherOnce sync.Once
33 | )
34 |
35 | // Dispatcher Notification provides interfaces to send alert dispatcher message with variable channel.
36 | type Dispatcher interface {
37 | SetDispatcher(firstExecution bool, previousFlag tp.StateFlag, notifyInfo tp.NotifyInfo) error
38 | SendNotification(request tp.ReqMsg) error
39 | }
40 |
41 | // GetDispatchers gets the registered alert channel.
42 | func GetDispatchers(cfg config.NotificationInfo) []Dispatcher {
43 | if len(cfg.DispatchChannels) == 0 {
44 | dpError := errors.New("error: No Dispatcher has set")
45 | log.Error().Str("module", "dispatcher").Msg("Please, Set at least a single channel for dispatcher, e.g.) Discord or Telegram")
46 | panic(dpError)
47 | }
48 |
49 | dispatcherOnce.Do(func() {
50 | for _, chainInfo := range cfg.DispatchChannels {
51 | var chainNotificationFlag []string
52 | if len(chainInfo.ReminderSchedule) == 0 {
53 | chainInfo.ReminderSchedule = cfg.DefaultReminderSchedule
54 | }
55 |
56 | if len(chainInfo.Subscriptions) > 0 {
57 | log.Debug().Str("module", "dispatcher").Msgf("SubscribingPlugins %s", chainInfo.Subscriptions)
58 | chainNotificationFlag = chainInfo.Subscriptions
59 | }
60 |
61 | switch channel := chainInfo.Channel; {
62 | case strings.EqualFold(channel, string(tp.Discord)):
63 | dispatcherSingletons = append(dispatcherSingletons, &discord{
64 | host: cfg.HostName,
65 | channel: tp.Discord,
66 | secret: chainInfo.Secret,
67 | subscriptions: chainNotificationFlag,
68 | reminderCron: cron.New(cron.WithLocation(time.UTC)),
69 | reminderSchedule: chainInfo.ReminderSchedule,
70 | entry: sync.Map{},
71 | })
72 | case strings.EqualFold(channel, string(tp.Telegram)):
73 | dispatcherSingletons = append(dispatcherSingletons, &telegram{
74 | host: cfg.HostName,
75 | channel: tp.Telegram,
76 | secret: chainInfo.Secret,
77 | chatID: chainInfo.ChatID,
78 | subscriptions: chainNotificationFlag,
79 | reminderCron: cron.New(cron.WithLocation(time.UTC)),
80 | reminderSchedule: chainInfo.ReminderSchedule,
81 | entry: sync.Map{},
82 | })
83 | case strings.EqualFold(channel, string(tp.Slack)):
84 | dispatcherSingletons = append(dispatcherSingletons, &slack{
85 | host: cfg.HostName,
86 | channel: tp.Slack,
87 | secret: chainInfo.Secret,
88 | subscriptions: chainNotificationFlag,
89 | reminderCron: cron.New(cron.WithLocation(time.UTC)),
90 | reminderSchedule: chainInfo.ReminderSchedule,
91 | entry: sync.Map{},
92 | })
93 | case strings.EqualFold(channel, string(tp.PagerDuty)):
94 | dispatcherSingletons = append(dispatcherSingletons, &pagerduty{
95 | host: cfg.HostName,
96 | channel: tp.PagerDuty,
97 | secret: chainInfo.Secret,
98 | subscriptions: chainNotificationFlag,
99 | pagerEntry: sync.Map{},
100 | })
101 | }
102 | }
103 | })
104 | return dispatcherSingletons
105 | }
106 |
107 | func messageHandler(isFirst bool, preStat tp.StateFlag, info tp.NotifyInfo) (bool, tp.Reminder, tp.ReqMsg) {
108 | notifyOn := false
109 | reminderState := tp.HANG
110 | isFlagStateChanged := false
111 |
112 | pUnique := utils.MakeUniqueValue(info.Plugin, info.Address, info.Port)
113 |
114 | if preStat.State != info.State || preStat.Severity != info.Severity {
115 | isFlagStateChanged = true
116 | }
117 |
118 | if info.State == pb.STATE_FAILURE ||
119 | (info.State == pb.STATE_SUCCESS && info.Severity == pb.SEVERITY_WARNING) ||
120 | (info.State == pb.STATE_SUCCESS && info.Severity == pb.SEVERITY_CRITICAL) {
121 | if isFirst || isFlagStateChanged {
122 | notifyOn = true
123 | reminderState = tp.ON
124 | }
125 | } else if info.State == pb.STATE_SUCCESS && info.Severity == pb.SEVERITY_INFO && isFlagStateChanged {
126 | notifyOn = true
127 | reminderState = tp.OFF
128 | }
129 |
130 | return notifyOn, reminderState, tp.ReqMsg{
131 | FuncName: info.Method,
132 | State: info.State,
133 | Msg: info.ExecuteMsg,
134 | Severity: info.Severity,
135 | ResourceType: info.Plugin,
136 | Options: map[string]interface{}{"pUnique": pUnique},
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/manager/dispatcher/discord.go:
--------------------------------------------------------------------------------
1 | package dispatcher
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | tp "github.com/dsrvlabs/vatz/types"
9 | "github.com/dsrvlabs/vatz/utils"
10 | "net/http"
11 | "strings"
12 | "sync"
13 | "time"
14 |
15 | pb "github.com/dsrvlabs/vatz-proto/plugin/v1"
16 | "github.com/robfig/cron/v3"
17 | "github.com/rs/zerolog/log"
18 | )
19 |
20 | // DiscordColor is type for discord message color.
21 | type DiscordColor int
22 |
23 | const (
24 | discordRed tp.DiscordColor = 15548997
25 | discordYellow tp.DiscordColor = 16705372
26 | discordGreen tp.DiscordColor = 65340
27 | discordGray tp.DiscordColor = 9807270
28 | )
29 |
30 | var discordWebhookFormats = []string{
31 | "https://discord.com/api/webhooks/",
32 | "https://discordapp.com/api/webhooks/",
33 | }
34 |
35 | type discord struct {
36 | host string
37 | channel tp.Channel
38 | secret string
39 | subscriptions []string
40 | reminderSchedule []string
41 | reminderCron *cron.Cron
42 | entry sync.Map
43 | }
44 |
45 | func containsAny(s string, substrings []string) bool {
46 | for _, substr := range substrings {
47 | if strings.Contains(s, substr) {
48 | return true
49 | }
50 | }
51 | return false
52 | }
53 |
54 | func (d *discord) SetDispatcher(firstRunMsg bool, preStat tp.StateFlag, notifyInfo tp.NotifyInfo) error {
55 | reqToNotify, reminderState, deliverMessage := messageHandler(firstRunMsg, preStat, notifyInfo)
56 | pUnique := deliverMessage.Options["pUnique"].(string)
57 | subscriptionEnabled, isSubscriptionIncluded := utils.IsSubscribeSpecific(d.subscriptions, notifyInfo.Plugin)
58 | if !subscriptionEnabled || subscriptionEnabled && isSubscriptionIncluded {
59 | if reqToNotify {
60 | err := d.SendNotification(deliverMessage)
61 | if err != nil {
62 | log.Error().Str("module", "dispatcher").Msgf("Channel(Discord): Send notification error: %s", err)
63 | return err
64 | }
65 | }
66 | }
67 | if reminderState == tp.ON {
68 | newEntries := []cron.EntryID{}
69 | /*
70 | In case of reminder has to keep but stateFlag has changed,
71 | e.g.) CRITICAL -> WARNING
72 | e.g.) ERROR -> INFO -> ERROR
73 | */
74 | if entries, ok := d.entry.Load(pUnique); ok {
75 | for _, entry := range entries.([]cron.EntryID) {
76 | d.reminderCron.Remove(entry)
77 | }
78 | d.reminderCron.Stop()
79 | }
80 | for _, schedule := range d.reminderSchedule {
81 | id, _ := d.reminderCron.AddFunc(schedule, func() {
82 | err := d.SendNotification(deliverMessage)
83 | if err != nil {
84 | log.Error().Str("module", "dispatcher").Msgf("Channel(Discord): Send notification error: %s", err)
85 | }
86 | })
87 | newEntries = append(newEntries, id)
88 | }
89 | d.entry.Store(pUnique, newEntries)
90 | d.reminderCron.Start()
91 | } else if reminderState == tp.OFF {
92 | entries, _ := d.entry.Load(pUnique)
93 | if _, ok := entries.([]cron.EntryID); ok {
94 | for _, entity := range entries.([]cron.EntryID) {
95 | d.reminderCron.Remove(entity)
96 | }
97 | d.reminderCron.Stop()
98 | }
99 | }
100 |
101 | return nil
102 | }
103 |
104 | func (d *discord) SendNotification(msg tp.ReqMsg) error {
105 | if containsAny(d.secret, discordWebhookFormats) {
106 | sendingMsg := tp.DiscordMsg{Embeds: make([]tp.Embed, 1)}
107 | sendingMsg.Embeds[0].Color = discordGray
108 | emoji := emojiER
109 |
110 | if msg.ResourceType == "" {
111 | msg.ResourceType = "No Resource Type"
112 | }
113 |
114 | if msg.Msg == "" {
115 | msg.Msg = "No Message"
116 | }
117 |
118 | if msg.State == pb.STATE_SUCCESS {
119 | switch {
120 | case msg.Severity == pb.SEVERITY_CRITICAL:
121 | sendingMsg.Embeds[0].Color = discordRed
122 | emoji = emojiDoubleEX
123 | case msg.Severity == pb.SEVERITY_WARNING:
124 | sendingMsg.Embeds[0].Color = discordYellow
125 | emoji = emojiSingleEx
126 | case msg.Severity == pb.SEVERITY_INFO:
127 | sendingMsg.Embeds[0].Color = discordGreen
128 | emoji = emojiCheck
129 | }
130 | }
131 |
132 | sendingMsg.Embeds[0].Title = fmt.Sprintf(`%s %s`, emoji, msg.Severity.String())
133 | sendingMsg.Embeds[0].Fields = []tp.Field{{Name: "(" + d.host + ") " + msg.ResourceType, Value: msg.Msg, Inline: false}}
134 | sendingMsg.Embeds[0].Timestamp = time.Now()
135 |
136 | message, err := json.Marshal(sendingMsg)
137 | if err != nil {
138 | return err
139 | }
140 |
141 | req, _ := http.NewRequest("POST", d.secret, bytes.NewReader(message))
142 | req.Header.Set("Content-Type", "application/json")
143 | c := &http.Client{}
144 | _, err = c.Do(req)
145 | if err != nil {
146 | log.Error().Str("module", "dispatcher").Msgf("Channel(Discord): Send notification error: %s", err)
147 | errorMessage := fmt.Sprintf("Channel(Discord) Error: %s", err)
148 | return errors.New(errorMessage)
149 | }
150 | // TODO: Should handle response status.
151 | } else {
152 | log.Error().Str("module", "dispatcher").Msg("Channel(Discord): Connection failed due to Invalid discord webhook address")
153 | return errors.New("Channel(Discord) Error: Invalid discord webhook address. ")
154 | }
155 | return nil
156 | }
157 |
--------------------------------------------------------------------------------
/docs/design.md:
--------------------------------------------------------------------------------
1 | # VATZ Project Design (v1)
2 |
3 | 
4 |
5 | > **VATZ** is mainly designed to check the node status in real time and get the alert notification of all blockchain protocols, including metrics that doesn't supported by the protocol itself. Features for helping node operators such as automation that enable node manage orchestration and controlling VATZ over CLI commands are going to be available in near future.
6 |
7 | ### **VATZ** Project consists of 3 major components for followings:
8 | (Will be upgraded or added for the future)
9 | 1. [VATZ proto](https://github.com/dsrvlabs/vatz-proto)
10 | 2. [VATZ Service](https://github.com/dsrvlabs/vatz)
11 | 3. VATZ Plugins (Official)
12 | - [vatz-plugin-sysutil](https://github.com/dsrvlabs/vatz-plugin-sysutil)
13 | - [vatz-plugin-cosmoshub](https://github.com/dsrvlabs/vatz-plugin-cosmoshub)
14 | ### **VATZ** service supports extension to 3rd party apps for alert notifications & metric analysis.
15 | 4. Dispatchers
16 | 5. Monitoring (Metric Exporter)
17 |
18 | ---
19 |
20 | ### 1. VATZ-Proto Repository (gRPC protocols)
21 |
22 | **VATZ** is a total node management tool that is designed to be customizable and expandable through plug-in and gRPC protocol from the initial design stage. End-users can develop their own plugins to add new features with their own needs regardless of the programming language by using gRPC protocol.
23 |
24 | ### 2. VATZ Service
25 |
26 | - This is a main service of **VATZ** that executes plugin APIs based on configs.
27 |
28 | ```
29 | SAMPLE DEFAULT YAML
30 | ---
31 | vatz_protocol_info:
32 | protocol_identifier: "Put Your Protocol here"
33 | port: 9090
34 | health_checker_schedule:
35 | - "0 1 * * *"
36 | notification_info:
37 | host_name: "Put your machine's host name"
38 | default_reminder_schedule:
39 | - "*/15 * * * *"
40 | dispatch_channels:
41 | - channel: "discord"
42 | secret: "Put your Discord Webhook"
43 | - channel: "pagerduty"
44 | secret: "Put your PagerDuty's Integration Key (Events API v2)"
45 | - channel: "slack"
46 | secret: "Put Your Slack Webhook url"
47 | subscriptions:
48 | - "Please, put Plugin Name that you specifically subscribe to send notification."
49 | - channel: "telegram"
50 | secret: "Put Your Bot's Token"
51 | chat_id: "Put Your Chat's chat_id"
52 | reminder_schedule:
53 | - "*/5 * * * *"
54 | rpc_info:
55 | enabled: true
56 | address: "127.0.0.1"
57 | grpc_port: 19090
58 | http_port: 19091
59 | monitoring_info:
60 | gcp:
61 | gcp_cloud_logging_info:
62 | enabled: true
63 | cloud_logging_credential_info:
64 | project_id: "Please, Set your GCP Project id"
65 | credentials_type: "Check the Credential Type: ADC: Application, SAC: Default Credentials, Service Account Credentials, APIKey: API Key, OAuth: OAuth2"
66 | credentials: "Put your credential Info"
67 | checker_schedule:
68 | - "* * * * *"
69 | prometheus:
70 | enabled: true
71 | address: "127.0.0.1"
72 | port: 18080
73 |
74 | plugins_infos:
75 | default_verify_interval: 15
76 | default_execute_interval: 30
77 | default_plugin_name: "vatz-plugin"
78 | plugins:
79 | - plugin_name: "samplePlugin1"
80 | plugin_address: "localhost"
81 | plugin_port: 9001
82 | executable_methods:
83 | - method_name: "sampleMethod1"
84 | - plugin_name: "samplePlugin2"
85 | plugin_address: "localhost"
86 | verify_interval: 7
87 | execute_interval: 9
88 | plugin_port: 10002
89 | executable_methods:
90 | - method_name: "sampleMethod2"
91 |
92 | ```
93 |
94 | `vatz_protocol_info` & `plugins_infos` must be declared in default.yaml to get started with **VATZ** properly.
95 |
96 | ### 3. Plugins
97 | **VATZ** Plugins are designed with maximum flexibility and expandability to perform following actions for Blockchain protocols nodes
98 | - `Check`: Node & Machine status
99 | - `Collect`: Node's metric + more
100 | - `Execute`: Command on machine for certain behaviors (e.g, Restart Node)
101 | - `Automation`: Node operation orchestration
102 | - `more` : You can develop your own plugins to manage your own nodes.
103 |
104 |
105 | ### 4. Dispatchers(Notification)
106 | **VATZ** Supports 3rd party apps for notification to alert users when there's any trouble on hardware or blockchain metrics.
107 | - [Discord](https://discord.com/)
108 | - [Pagerduty](https://www.pagerduty.com/)
109 | - [Telegram Messenger](https://telegram.org/)
110 |
111 | ### 5. Monitoring
112 | The blockchain protocols have so many unique logs, and it brings a lot of data which causes difficulties in finding meaningful data by standardizing it to make it easier to view
113 | and most of the validator teams have trouble managing logs from running nodes due to log's varieties.
114 | **VATZ**'s monitoring service is designed to find a way to manage all logs from nodes efficiently with minimum efforts and cost.
115 |
116 | - `Available Now`
117 | - [Prometheus - Grafana](https://prometheus.io/docs/visualization/grafana/)
118 | - `Upcoming soon`
119 | - Elastic - Kibana
120 | - Google drive - Big Query
121 |
122 |
123 | #### - **AS-IS**
124 | 
125 |
126 | `VATZ` currently supports sending metrics for followings for Prometheus:
127 | (Note: More metrics will be available in the future)
128 | - VATZ:`service` Liveness
129 | - VATZ:`plugins` Liveness
130 |
131 | #### - **TO-BE**
132 | 
133 |
134 | **VATZ** will support to more monitoring and analysis 3rd party apps tool as shown in the diagram above.
135 |
136 | ---
137 |
138 | # VATZ Project Design (v2)
139 | The comprehensive design of VATZ v2 is presently in the development stage, with an anticipated release scheduled for 2024.
140 | Should you have any inquiries or feedback regarding VATZ v2, do not hesitate to contact us.
--------------------------------------------------------------------------------
/cmd/init.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/rs/zerolog/log"
8 | "github.com/spf13/cobra"
9 |
10 | "github.com/dsrvlabs/vatz/manager/config"
11 | "github.com/dsrvlabs/vatz/manager/plugin"
12 | tp "github.com/dsrvlabs/vatz/types"
13 | )
14 |
15 | func createInitCommand(initializer tp.Initializer) *cobra.Command {
16 | cmd := &cobra.Command{
17 | Use: "init",
18 | Short: "Initialize VATZ",
19 | RunE: func(cmd *cobra.Command, args []string) error {
20 | log.Debug().Str("module", "main").Msg("init")
21 |
22 | template := `vatz_protocol_info:
23 | home_path: "%s"
24 | protocol_identifier: "Put Your Protocol here"
25 | port: 9090
26 | health_checker_schedule:
27 | - "0 1 * * *"
28 | notification_info:
29 | host_name: "Put your machine's host name"
30 | default_reminder_schedule:
31 | - "*/30 * * * *"
32 | dispatch_channels:
33 | - channel: "discord"
34 | secret: "Put your Discord Webhook"
35 | - channel: "pagerduty"
36 | secret: "Put your PagerDuty's Integration Key (Events API v2)"
37 | - channel: "slack"
38 | secret: "Put Your Slack Webhook url"
39 | subscriptions:
40 | - "Please, put Plugin Name that you specifically subscribe to send notification."
41 | - channel: "telegram"
42 | secret: "Put Your Bot's Token"
43 | chat_id: "Put Your Chat's chat_id"
44 | reminder_schedule:
45 | - "*/5 * * * *"
46 | rpc_info:
47 | enabled: true
48 | address: "127.0.0.1"
49 | grpc_port: 19090
50 | http_port: 19091
51 | monitoring_info:
52 | gcp:
53 | gcp_cloud_logging_info:
54 | enabled: true
55 | cloud_logging_credential_info:
56 | project_id: "Please, Set your GCP Project id"
57 | credentials_type: "Check the Credential Type: ADC: Application, SAC: Default Credentials, Service Account Credentials, APIKey: API Key, OAuth: OAuth2"
58 | credentials: "Put your credential Info"
59 | checker_schedule:
60 | - "* * * * *"
61 | prometheus:
62 | enabled: true
63 | address: "127.0.0.1"
64 | port: 18080
65 | `
66 | samplePluginOptionTemplate := `plugins_infos:
67 | default_verify_interval: 15
68 | default_execute_interval: 30
69 | default_plugin_name: "vatz-plugin"
70 | plugins:
71 | - plugin_name: "samplePlugin1"
72 | plugin_address: "localhost"
73 | plugin_port: 9001
74 | executable_methods:
75 | - method_name: "sampleMethod1"
76 | - plugin_name: "samplePlugin2"
77 | plugin_address: "localhost"
78 | verify_interval: 7
79 | execute_interval: 9
80 | plugin_port: 10002
81 | executable_methods:
82 | - method_name: "sampleMethod2"
83 | `
84 |
85 | defaultPluginOptionTemplate := `plugins_infos:
86 | default_verify_interval: 15
87 | default_execute_interval: 30
88 | default_plugin_name: "vatz-plugin"
89 | plugins:
90 | - plugin_name: "vatz_cpu_monitor"
91 | plugin_address: "localhost"
92 | plugin_port: 9001
93 | executable_methods:
94 | - method_name: "cpu_monitor"
95 | - plugin_name: "vatz_mem_monitor"
96 | plugin_address: "localhost"
97 | plugin_port: 9002
98 | executable_methods:
99 | - method_name: "mem_monitor"
100 | - plugin_name: "vatz_disk_monitor"
101 | plugin_address: "localhost"
102 | plugin_port: 9003
103 | executable_methods:
104 | - method_name: "disk_monitor"
105 | - plugin_name: "vatz_net_monitor"
106 | plugin_address: "localhost"
107 | plugin_port: 9004
108 | executable_methods:
109 | - method_name: "net_monitor"
110 | - plugin_name: "vatz_block_sync"
111 | plugin_address: "localhost"
112 | plugin_port: 10001
113 | executable_methods:
114 | - method_name: "node_block_sync"
115 | - plugin_name: "vatz_node_is_alived"
116 | plugin_address: "localhost"
117 | plugin_port: 10002
118 | executable_methods:
119 | - method_name: "node_is_alived"
120 | - plugin_name: "vatz_peer_count"
121 | plugin_address: "localhost"
122 | plugin_port: 10003
123 | executable_methods:
124 | - method_name: "node_peer_count"
125 | - plugin_name: "vatz_active_status"
126 | plugin_address: "localhost"
127 | plugin_port: 10004
128 | executable_methods:
129 | - method_name: "node_active_status"
130 | - plugin_name: "vatz_gov_alarm"
131 | plugin_address: "localhost"
132 | plugin_port: 10005
133 | executable_methods:
134 | - method_name: "node_governance_alarm"`
135 |
136 | filename, err := cmd.Flags().GetString("output")
137 | if err != nil {
138 | return err
139 | }
140 |
141 | homePath, err := cmd.Flags().GetString("home")
142 | if err != nil {
143 | return err
144 | }
145 |
146 | template = fmt.Sprintf(template, homePath)
147 | log.Debug().Str("module", "main").Msgf("home path %s", homePath)
148 | log.Debug().Str("module", "main").Msgf("create file %s", filename)
149 |
150 | f, err := os.Create(filename)
151 | if err != nil {
152 | return err
153 | }
154 |
155 | configOption, err := cmd.Flags().GetBool("all")
156 | if err != nil {
157 | return err
158 | }
159 | if configOption {
160 | template = template + defaultPluginOptionTemplate
161 | } else {
162 | template = template + samplePluginOptionTemplate
163 | }
164 | _, err = f.WriteString(template)
165 | if err != nil {
166 | return err
167 | }
168 |
169 | _, err = config.InitConfig(filename)
170 | if err != nil {
171 | return err
172 | }
173 |
174 | pluginDir, err := config.GetConfig().Vatz.AbsoluteHomePath()
175 | if err != nil {
176 | return err
177 | }
178 |
179 | log.Debug().Str("module", "main").Msgf("Plugin dir %s", pluginDir)
180 | mgr := plugin.NewManager(pluginDir)
181 | return mgr.Init(initializer)
182 | },
183 | }
184 |
185 | _ = cmd.PersistentFlags().StringP("output", "o", defaultFlagConfig, "New config file to create")
186 | _ = cmd.PersistentFlags().StringP("home", "p", defaultHomePath, "Home directory of VATZ")
187 | _ = cmd.PersistentFlags().BoolP("all", "a", false, "Create config yaml with all default setting of official plugins.")
188 |
189 | return cmd
190 | }
191 |
--------------------------------------------------------------------------------
/manager/executor/executor_test.go:
--------------------------------------------------------------------------------
1 | package executor
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | tp "github.com/dsrvlabs/vatz/types"
7 | "sync"
8 | "testing"
9 |
10 | dp "github.com/dsrvlabs/vatz/manager/dispatcher"
11 | "github.com/dsrvlabs/vatz/utils"
12 |
13 | pluginpb "github.com/dsrvlabs/vatz-proto/plugin/v1"
14 | "github.com/dsrvlabs/vatz/manager/config"
15 | "github.com/stretchr/testify/assert"
16 | "google.golang.org/grpc"
17 | "google.golang.org/protobuf/types/known/structpb"
18 | )
19 |
20 | func TestExecutorSuccess(t *testing.T) {
21 | const (
22 | testMethodName = "is_up"
23 | testPluginName = "unittest_plugin"
24 | testPluginAddress = "127.0.0.1"
25 | testPluginPort = 10002
26 | )
27 |
28 | tests := []struct {
29 | Desc string
30 | TestExecResp *pluginpb.ExecuteResponse
31 | TestNotifInfo tp.NotifyInfo
32 | }{
33 | {
34 | Desc: "No Alert",
35 | TestExecResp: &pluginpb.ExecuteResponse{
36 | State: pluginpb.STATE_SUCCESS,
37 | Severity: pluginpb.SEVERITY_UNKNOWN,
38 | },
39 | TestNotifInfo: tp.NotifyInfo{
40 | Plugin: testPluginName,
41 | Method: testMethodName,
42 | Address: testPluginAddress,
43 | Port: testPluginPort,
44 | State: pluginpb.STATE_SUCCESS,
45 | Severity: pluginpb.SEVERITY_UNKNOWN,
46 | },
47 | },
48 | }
49 |
50 | for _, test := range tests {
51 | ctx := context.Background()
52 | cfgPlugin := config.Plugin{
53 | Name: testPluginName,
54 | Address: testPluginAddress,
55 | Port: testPluginPort,
56 | ExecutableMethods: []struct {
57 | Name string `yaml:"method_name"`
58 | }{
59 | {testMethodName},
60 | },
61 | }
62 |
63 | // Mocks.
64 | mockExeInfo, err := structpb.NewStruct(map[string]interface{}{
65 | "execute_method": testMethodName,
66 | })
67 | assert.Nil(t, err)
68 |
69 | mockOpts, err := structpb.NewStruct(map[string]interface{}{
70 | "plugin_name": testPluginName,
71 | })
72 | assert.Nil(t, err)
73 |
74 | exeReq := pluginpb.ExecuteRequest{
75 | ExecuteInfo: mockExeInfo,
76 | Options: mockOpts,
77 | }
78 |
79 | mockClient := mockPluginClient{}
80 | mockClient.On("Execute", ctx, &exeReq, []grpc.CallOption(nil)).Return(test.TestExecResp, nil)
81 |
82 | mockNotif := dp.MockDispatcher{}
83 | var mockNotifs []dp.Dispatcher
84 |
85 | if test.TestNotifInfo.State != pluginpb.STATE_SUCCESS {
86 | dummyMsg := tp.ReqMsg{
87 | FuncName: testMethodName,
88 | State: pluginpb.STATE_FAILURE,
89 | Msg: "No response from Plugin",
90 | Severity: pluginpb.SEVERITY_CRITICAL,
91 | ResourceType: testPluginName,
92 | }
93 | mockNotif.On("SendNotification", dummyMsg).Return(nil)
94 | }
95 |
96 | // Test
97 | e := executor{
98 | status: sync.Map{},
99 | }
100 |
101 | err = e.Execute(ctx, &mockClient, cfgPlugin, mockNotifs)
102 |
103 | fmt.Println("Status", &e.status)
104 |
105 | // Asserts
106 | mockClient.AssertExpectations(t)
107 | mockNotif.AssertExpectations(t)
108 |
109 | assert.Nil(t, err)
110 | pUnique := utils.MakeUniqueValue(testPluginName, testPluginAddress, testPluginPort)
111 | mockStatus, _ := e.status.Load(pUnique)
112 | assert.True(t, mockStatus.(tp.StateFlag) == tp.StateFlag{State: pluginpb.STATE_SUCCESS, Severity: pluginpb.SEVERITY_UNKNOWN})
113 | }
114 | }
115 |
116 | func TestExecutorFailure(t *testing.T) {
117 | const (
118 | testMethodName = "is_up"
119 | testPluginName = "unittest_plugin"
120 | )
121 |
122 | tests := []struct {
123 | Desc string
124 | MockPrevStatus bool
125 | TestExecResp *pluginpb.ExecuteResponse
126 | TestNotifInfo tp.NotifyInfo
127 | ExpectReqMsg tp.ReqMsg
128 | }{
129 | {
130 | Desc: "Alert ERROR",
131 | MockPrevStatus: false,
132 | TestExecResp: &pluginpb.ExecuteResponse{
133 | State: pluginpb.STATE_FAILURE,
134 | Severity: pluginpb.SEVERITY_ERROR,
135 | },
136 | TestNotifInfo: tp.NotifyInfo{
137 | Plugin: testPluginName,
138 | Method: testMethodName,
139 | State: pluginpb.STATE_FAILURE,
140 | Severity: pluginpb.SEVERITY_ERROR,
141 | },
142 | ExpectReqMsg: tp.ReqMsg{
143 | FuncName: testMethodName,
144 | State: pluginpb.STATE_FAILURE,
145 | Msg: "No response from Plugin",
146 | Severity: pluginpb.SEVERITY_CRITICAL,
147 | ResourceType: testPluginName,
148 | },
149 | },
150 | {
151 | Desc: "Alert Critical",
152 | MockPrevStatus: false,
153 | TestExecResp: &pluginpb.ExecuteResponse{
154 | State: pluginpb.STATE_FAILURE,
155 | Severity: pluginpb.SEVERITY_CRITICAL,
156 | Message: "test execute msg",
157 | },
158 | TestNotifInfo: tp.NotifyInfo{
159 | Plugin: testPluginName,
160 | Method: testMethodName,
161 | State: pluginpb.STATE_FAILURE,
162 | Severity: pluginpb.SEVERITY_CRITICAL,
163 | ExecuteMsg: "test execute msg",
164 | },
165 | ExpectReqMsg: tp.ReqMsg{
166 | FuncName: testMethodName,
167 | State: pluginpb.STATE_FAILURE,
168 | Msg: "test execute msg",
169 | Severity: pluginpb.SEVERITY_CRITICAL,
170 | ResourceType: testPluginName,
171 | },
172 | },
173 | }
174 |
175 | for _, test := range tests {
176 | ctx := context.Background()
177 | cfgPlugin := config.Plugin{
178 | Name: testPluginName,
179 | ExecutableMethods: []struct {
180 | Name string `yaml:"method_name"`
181 | }{
182 | {testMethodName},
183 | },
184 | }
185 |
186 | // Mocks.
187 | mockExeInfo, err := structpb.NewStruct(map[string]interface{}{
188 | "execute_method": testMethodName,
189 | })
190 | assert.Nil(t, err)
191 |
192 | mockOpts, err := structpb.NewStruct(map[string]interface{}{
193 | "plugin_name": testPluginName,
194 | })
195 | assert.Nil(t, err)
196 |
197 | exeReq := pluginpb.ExecuteRequest{
198 | ExecuteInfo: mockExeInfo,
199 | Options: mockOpts,
200 | }
201 |
202 | mockClient := mockPluginClient{}
203 | mockClient.On("Execute", ctx, &exeReq, []grpc.CallOption(nil)).Return(test.TestExecResp, nil)
204 |
205 | mockNotif := dp.MockDispatcher{}
206 | var mockNotifs []dp.Dispatcher
207 |
208 | mockNotif.On("SendNotification", test.ExpectReqMsg).Return(nil)
209 |
210 | // Test
211 | e := executor{
212 | status: sync.Map{},
213 | }
214 |
215 | err = e.Execute(ctx, &mockClient, cfgPlugin, mockNotifs)
216 |
217 | fmt.Println("Status", &e.status)
218 |
219 | // Asserts
220 | mockClient.AssertExpectations(t)
221 |
222 | assert.Nil(t, err)
223 | mockStatus, _ := e.status.Load(testMethodName)
224 | assert.False(t, mockStatus == true)
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | VATZ (Validators' A To Z)
2 |
3 |
4 |
5 |

6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | # What is VATZ?
15 |
16 | **VATZ** is a tool for building, analyzing, and managing blockchain node infrastructure safely and efficiently. You can set up **VATZ** to manage existing or new blockchain nodes and integrate with popular services like PagerDuty, Discord, or Telegram as well as custom in-house solutions.
17 |
18 | ## How does VATZ Project work?
19 |
20 | This project is primarily designed to check node states in real time and receive alert notifications for all blockchain protocols, including metrics that the protocol itself might not support.
21 |
22 | To this end, it consists of 3 components:
23 |
24 | 1. **VATZ-Proto**: API specification for VATZ (SVC) and VATZ Plugins that implements protobuf allowing users to develop in the language they wish.
25 | 2. **VATZ (SVC)**: Service that executes plugin APIs based on configs, checks plugin health, and sends notifications to configured channels.
26 | 3. **VATZ** **Plugins (with SDK)**: Plugins that integrate with VATZ to support features like node status checks, metric collection, and command execution.
27 |
28 | For further information, check [VATZ Project Design](docs/design.md)
29 |
30 |
31 | ## What is the key feature of **VATZ Project**?
32 |
33 | ### Multi-Protocol Support
34 | Any protocol can be managed through **VATZ** with plugins. Even unsupported protocols can be integrated through simple plugin development.
35 |
36 | ### Infrastructure as Code
37 | **VATZ** is described using a high-level configuration syntax. You can divide your plugins into modular components that can then be combined in different ways to behave through automation.
38 |
39 | ### Monitoring
40 | Various logs and node data are collected and exported by a node exporter, then monitored through the 3rd party applications like Grafana.
41 |
42 | ### Data Analysis
43 | **VATZ** helps build datasets for your protocols, and transfer your data into popular services like Prometheus, Kafka, Google BigQuery, and more. In this way, **VATZ** aims to optimize your node infrastructure, and operators get insight into dependencies in their infrastructure. (Exp. 2023-Q3)
44 |
45 | ### Change Automation
46 | Complex node operation tasks can be executed through **VATZ** with minimal human interaction. (Exp. 2023-Q4)
47 |
48 | # Our Mission
49 | We're on a mission to both transform the way people experience blockchain technology, and help them shape it.
50 | As validators, we provide tools for low-cost and low-effort node management for anyone wanting to onboard next-generation blockchain technology.
51 |
52 | ---
53 | # Usage of VATZ
54 |
55 | ## How to get started with VATZ
56 | Check out the [Installation Guide](docs/installation.md) to install and start using VATZ.
57 | - You can get started with simple scripts, Please check [install scripts instructions](script/simple_start_guide/readme.md)
58 |
59 | ## How to use **VATZ** CLIs
60 | Refer to the [VATZ CLIs guide](docs/cli.md) to find available CLI arguments.
61 |
62 | ## Official Plugins
63 | > Our team is developing official plugins for easier operation, including basic monitoring metrics.
64 |
65 |
66 | ### 1. [vatz-plugin-sysutil](https://github.com/dsrvlabs/vatz-plugin-sysutil)
67 | vatz-plugin-sysutil is **VATZ** plugin for system utilization monitoring, i.e.:
68 | - CPU
69 | - DISK
70 | - Memory
71 | - Network Traffic
72 |
73 | ### 2. [vatz-plugin-comoshub](https://github.com/dsrvlabs/vatz-plugin-cosmoshub)
74 | vatz-plugin-comoshub is **VATZ** plugin for cosmoshub node monitoring for followings:
75 | - Node Block Sync
76 | - Node Liveness
77 | - Peer Count
78 | - Active Status
79 | - Node Governance Alarm
80 |
81 | ## Community Plugins
82 | > We encourage everyone to share their plugins to make node operating easier.
83 |
84 | - Please, share your own VATZ plugins on [Community Plugins](docs/community_plugins.md)!
85 |
86 |
87 | ---
88 | # Release Note
89 |
90 | Please check the Release Note for details of the latest releases.
91 | - [VATZ](https://github.com/dsrvlabs/vatz/releases)
92 | - [vatz-plugin-comoshub](https://github.com/dsrvlabs/vatz-plugin-cosmoshub/releases)
93 | - [vatz-plugin-sysutil](https://github.com/dsrvlabs/vatz-plugin-sysutil/releases)
94 |
95 | ---
96 |
97 | # We welcome your feedback
98 | We're constantly striving to enhance and build on open-source resources.
99 | Feel free to share your thoughts or feedback with us regarding VATZ.
100 | You can start by registering any [issues](https://github.com/dsrvlabs/vatz/issues) you might find!
101 | Let’s continue building VATZ together!
102 |
103 | ## Contributing
104 |
105 | **VATZ** welcomes contributions! If you are looking to contribute, please check the following documents.
106 | - [Contributing](docs/contributing.md) explains what kinds of contributions we look for and how to contribute.
107 | - [Project Workflow Instructions](docs/workflow.md) explains how to build and test.
108 |
109 | ## License
110 |
111 | The `VATZ` library (i.e. all code outside of the `cmd` directory) is licensed under the
112 | [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html), also
113 | included in our repository in the `LICENSE.LESSER` file.
114 |
115 | The `VATZ` binaries (i.e. all code inside of the `cmd` directory) are licensed under the
116 | [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html), also
117 | included in our repository in the `LICENSE` file.
118 |
119 |
120 | # Contact us
121 | Please don’t hesitate to contact [us](mailto:validator@dsrvlabs.com) if you need any further information about **VATZ**.
122 |
123 | ## Who are we
124 |
125 | A leading blockchain technology company, **[DSRV](https://www.dsrvlabs.com/)** validates for 40+ global networks and provides infrastructure solutions for next-level building. This includes All That Node (enterprise-grade NaaS supporting 24+ protocols) and WELLDONE Studio (multi-chain product suite for developers and retail users alike).
126 |
127 | Our ethos is to adapt to what the market and community need; our mission to advance the next internet and enable every player to build what they envision.
128 |
129 | [
](https://www.dsrvlabs.com/)
130 | [
](https://medium.com/dsrv)
131 | [
](https://github.com/dsrvlabs)
132 | [
](https://www.youtube.com/channel/UCWhv8Kd430cEMpEYBPtSPjA/featured)
133 | [
](https://twitter.com/dsrvlabs)
134 |
--------------------------------------------------------------------------------
/cmd/start.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "net"
8 | "os"
9 | "strconv"
10 | "strings"
11 | "time"
12 |
13 | managerPb "github.com/dsrvlabs/vatz-proto/manager/v1"
14 | pluginPb "github.com/dsrvlabs/vatz-proto/plugin/v1"
15 | "github.com/dsrvlabs/vatz/manager/api"
16 | config "github.com/dsrvlabs/vatz/manager/config"
17 | dp "github.com/dsrvlabs/vatz/manager/dispatcher"
18 | pl "github.com/dsrvlabs/vatz/manager/plugin"
19 | "github.com/dsrvlabs/vatz/monitoring/gcp"
20 | "github.com/dsrvlabs/vatz/monitoring/prometheus"
21 | "github.com/dsrvlabs/vatz/rpc"
22 | tp "github.com/dsrvlabs/vatz/types"
23 | "github.com/dsrvlabs/vatz/utils"
24 | "github.com/rs/zerolog/log"
25 | "github.com/spf13/cobra"
26 | "google.golang.org/grpc"
27 | grpchealth "google.golang.org/grpc/health"
28 | healthpb "google.golang.org/grpc/health/grpc_health_v1"
29 | "google.golang.org/grpc/reflection"
30 | )
31 |
32 | func createStartCommand() *cobra.Command {
33 | log.Debug().Str("module", "cmd > start").Msg("start command")
34 | cmd := &cobra.Command{
35 | Use: "start",
36 | Short: "Start VATZ",
37 | PreRunE: func(cmd *cobra.Command, args []string) error {
38 | log.Debug().Str("module", "cmd start").Msgf("Set logfile %s", logfile)
39 | return utils.SetLog(logfile, defaultFlagLog)
40 | },
41 | RunE: func(cmd *cobra.Command, args []string) error {
42 | log.Debug().Str("module", "main").Msgf("load config %s", configFile)
43 | _, err := config.InitConfig(configFile)
44 | if err != nil {
45 | log.Error().Str("module", "config").Msgf("loadConfig Error: %s", err)
46 | if errors.Is(err, os.ErrNotExist) {
47 | msg := "Please, initialize VATZ with command `./vatz init` to create config file `default.yaml` first or set appropriate path for config file default.yaml."
48 | log.Error().Str("module", "config").Msg(msg)
49 | }
50 | return nil
51 | }
52 | /*
53 | Stored process id into a separated file to avoid terminating the wrong vatz process
54 | if there are two multiple running vatz processes on the single machine.
55 | */
56 | path, err := config.GetConfig().Vatz.AbsoluteHomePath()
57 | if err != nil {
58 | return err
59 | }
60 | processID := os.Getpid()
61 | log.Debug().Str("module", "cmd > start").Msgf("pid is %d", processID)
62 | if err := os.WriteFile(fmt.Sprintf("%s/vatz.pid", path), []byte(strconv.Itoa(processID)), 0644); err != nil {
63 | log.Error().Err(err).Msg("Failed to write PID file")
64 | return err
65 | }
66 | return initiateServer(sigs)
67 | },
68 | }
69 |
70 | cmd.PersistentFlags().StringVar(&configFile, "config", defaultFlagConfig, "VATZ config file.")
71 | cmd.PersistentFlags().StringVar(&logfile, "log", defaultFlagLog, "log file export to.")
72 | cmd.PersistentFlags().StringVar(&promPort, "prometheus", defaultPromPort, "prometheus port number.")
73 |
74 | return cmd
75 | }
76 |
77 | func initiateServer(ch <-chan os.Signal) error {
78 | log.Info().Str("module", "main").Msg("Initialize Server")
79 | _, cancel := context.WithCancel(context.Background())
80 | defer cancel()
81 | cfg := config.GetConfig()
82 | dispatchers = dp.GetDispatchers(cfg.Vatz.NotificationInfo)
83 |
84 | // Health Check
85 | s := grpc.NewServer()
86 | serv := api.GrpcService{}
87 | managerPb.RegisterManagerServer(s, &serv)
88 | reflection.Register(s)
89 |
90 | // Health Check
91 | addr := fmt.Sprintf(":%d", cfg.Vatz.Port)
92 | err := healthChecker.VATZHealthCheck(cfg.Vatz.HealthCheckerSchedule, dispatchers)
93 | if err != nil {
94 | log.Error().Str("module", "cmd > start").Msgf("VATZHealthCheck Error: %s", err)
95 | }
96 |
97 | listener, err := net.Listen("tcp", addr)
98 | if err != nil {
99 | log.Error().Str("module", "cmd > start").Msgf("VATZ Listener Error: %s", err)
100 | }
101 |
102 | log.Info().Str("module", "main").Msgf("Start VATZ Server on Listening Port: %s", addr)
103 | grpcClients := utils.GetClients(cfg.PluginInfos.Plugins)
104 | startExecutor(grpcClients, ch)
105 |
106 | rpcServ := rpc.NewRPCService()
107 | go func() {
108 | if err := rpcServ.Start(cfg.Vatz.RPCInfo.Address, cfg.Vatz.RPCInfo.GRPCPort, cfg.Vatz.RPCInfo.HTTPPort); err != nil {
109 | log.Error().Str("module", "rpc").Msgf("RPC Service Starting Error: %s", err)
110 | }
111 | }()
112 |
113 | monitoringInfo := cfg.Vatz.MonitoringInfo
114 | if monitoringInfo.GCP.GCPCloudLogging.Enabled {
115 | gcpServices := gcp.GetGCP(monitoringInfo)
116 | for _, svc := range gcpServices {
117 | err := svc.Prep(cfg)
118 | if err != nil {
119 | log.Error().Str("module", "gcp").Msgf("Fail to set Log Message: %s", err)
120 | }
121 | err = svc.Process()
122 | if err != nil {
123 | log.Error().Str("module", "gcp").Msgf("Fail to store Message in gcp Cloud logging : %s", err)
124 | }
125 | }
126 | }
127 | if monitoringInfo.Prometheus.Enabled {
128 | var prometheusPort string
129 | if defaultPromPort == promPort {
130 | prometheusPort = strconv.Itoa(monitoringInfo.Prometheus.Port)
131 | } else {
132 | prometheusPort = promPort
133 | }
134 | err := prometheus.InitPrometheusServer(monitoringInfo.Prometheus.Address, prometheusPort, cfg.Vatz.ProtocolIdentifier)
135 | if err != nil {
136 | log.Error().Str("module", "prometheus").Msgf("Fail to init prometheus server: %s", err)
137 | }
138 | }
139 |
140 | log.Info().Str("module", "main").Msg("VATZ Manager Started")
141 | initHealthServer(s)
142 | if err := s.Serve(listener); err != nil {
143 | log.Panic().Str("module", "main").Msgf("Serve Error: %s", err)
144 | }
145 | return nil
146 | }
147 |
148 | func startExecutor(grpcClients []utils.GClientWithPlugin, quit <-chan os.Signal) {
149 | // TODO:: value in map would be overridden by different plugins flag value if function name is the same
150 | isOkayToSend := false
151 | if len(grpcClients) == 0 {
152 | log.Error().Str("module", "cmd:Start").Msg("No Plugins are set, Check your Configs.")
153 | os.Exit(1)
154 | }
155 | for _, client := range grpcClients {
156 | go multiPluginExecutor(client.PluginInfo, client.GRPCClient, isOkayToSend, quit)
157 | }
158 | }
159 |
160 | func multiPluginExecutor(plugin config.Plugin, singleClient pluginPb.PluginClient, okToSend bool, quit <-chan os.Signal) {
161 | verifyTicker := time.NewTicker(time.Duration(plugin.VerifyInterval) * time.Second)
162 | executeTicker := time.NewTicker(time.Duration(plugin.ExecuteInterval) * time.Second)
163 |
164 | ctx := context.Background()
165 |
166 | pluginDir, err := config.GetConfig().Vatz.AbsoluteHomePath()
167 | if err != nil {
168 | return
169 | }
170 |
171 | mgr := pl.NewManager(pluginDir)
172 | for {
173 | pluginState, pluginStateErr := mgr.Get(plugin.Name)
174 |
175 | select {
176 | case <-verifyTicker.C:
177 | if pluginState.IsEnabled {
178 | live, err := healthChecker.PluginHealthCheck(ctx, singleClient, plugin, dispatchers)
179 | if err != nil {
180 | if strings.Contains(err.Error(), "Failed to send all configured notifications") {
181 | executeTicker.Stop()
182 | os.Exit(1)
183 | }
184 | }
185 | if live == tp.AliveStatusUp {
186 | okToSend = true
187 | } else {
188 | okToSend = false
189 | }
190 | }
191 | case <-executeTicker.C:
192 | if pluginState.IsEnabled {
193 | if okToSend {
194 | if pluginStateErr != nil {
195 | log.Error().Str("module", "cmd > start").Msgf("Executor Error: %s", pluginStateErr)
196 | }
197 | err := executor.Execute(ctx, singleClient, plugin, dispatchers)
198 | if err != nil {
199 | log.Error().Str("module", "cmd > start").Msgf("Executor Error: %s", err)
200 | }
201 | }
202 | }
203 | case <-quit:
204 | osSig := <-quit
205 | executeTicker.Stop()
206 | log.Info().Str("module", "cmd > start").Msgf("Received signal: %s", osSig)
207 | log.Info().Msg("Terminating VATZ...")
208 | os.Exit(1)
209 | return
210 | }
211 | }
212 | }
213 |
214 | func initHealthServer(s *grpc.Server) {
215 | gRPCHealthServer := grpchealth.NewServer()
216 | gRPCHealthServer.SetServingStatus("vatz-health-status", healthpb.HealthCheckResponse_SERVING)
217 | healthpb.RegisterHealthServer(s, gRPCHealthServer)
218 | }
219 |
--------------------------------------------------------------------------------
/manager/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 | "sync"
11 |
12 | "github.com/rs/zerolog/log"
13 | "gopkg.in/yaml.v2"
14 | )
15 |
16 | const (
17 | // FlagConfig is name of CLI flags for config.
18 | FlagConfig = "config"
19 |
20 | // DefaultConfigFile is default file name of config.
21 | DefaultConfigFile = "default.yaml"
22 |
23 | // DefaultGRPCPort is default port number of grpc service.
24 | DefaultGRPCPort = 19090
25 |
26 | // DefaultHTTPPort is default port number of http service.
27 | DefaultHTTPPort = 19091
28 |
29 | // DefaultHomePath default home directory of VATZ.
30 | DefaultHomePath = "~/.vatz"
31 | )
32 |
33 | var (
34 | configOnce = &sync.Once{}
35 | vatzConfig *Config
36 | )
37 |
38 | // Config is Vatz config structure.
39 | type Config struct {
40 | Vatz VatzProtocolInfo `yaml:"vatz_protocol_info"`
41 | PluginInfos PluginInfo `yaml:"plugins_infos"`
42 | }
43 |
44 | // VatzProtocolInfo is VATZ information.
45 | type VatzProtocolInfo struct {
46 | ProtocolIdentifier string `yaml:"protocol_identifier"`
47 | Port int `yaml:"port"`
48 | NotificationInfo NotificationInfo `yaml:"notification_info"`
49 | HealthCheckerSchedule []string `yaml:"health_checker_schedule"`
50 | RPCInfo RPCInfo `yaml:"rpc_info"`
51 | MonitoringInfo MonitoringInfo `yaml:"monitoring_info"`
52 | HomePath string `yaml:"home_path"`
53 | }
54 |
55 | // AbsoluteHomePath is the default home path
56 | func (i VatzProtocolInfo) AbsoluteHomePath() (string, error) {
57 | if strings.HasPrefix(i.HomePath, "~") {
58 | homePath := os.Getenv("HOME")
59 | absPath := fmt.Sprintf("%s/%s", homePath, strings.Trim(i.HomePath, "~"))
60 |
61 | // Prevent double slash
62 | absPath, err := filepath.Abs(absPath)
63 | return absPath, err
64 | }
65 |
66 | absPath, err := filepath.Abs(i.HomePath)
67 | if err != nil {
68 | return "", err
69 | }
70 |
71 | return absPath, nil
72 | }
73 |
74 | // NotificationInfo is notification structure.
75 | type NotificationInfo struct {
76 | HostName string `yaml:"host_name"`
77 | DefaultReminderSchedule []string `yaml:"default_reminder_schedule"`
78 | DispatchChannels []struct {
79 | Channel string `yaml:"channel"`
80 | Secret string `yaml:"secret"`
81 | ChatID string `yaml:"chat_id"`
82 | Subscriptions []string `yaml:"subscriptions,omitempty"`
83 | ReminderSchedule []string `yaml:"reminder_schedule"`
84 | } `yaml:"dispatch_channels"`
85 | }
86 |
87 | // RPCInfo is structure for RPC service configuration.
88 | type RPCInfo struct {
89 | Enabled bool `yaml:"enabled"`
90 | Address string `yaml:"address"`
91 | GRPCPort int `yaml:"grpc_port"`
92 | HTTPPort int `yaml:"http_port"`
93 | }
94 |
95 | // MonitoringInfo is structure for RPC service configuration.
96 | type MonitoringInfo struct {
97 | GCP struct {
98 | GCPCloudLogging GCPCloudLoggingInfo `yaml:"gcp_cloud_logging_info"`
99 | } `yaml:"gcp"`
100 | Prometheus struct {
101 | Enabled bool `yaml:"enabled"`
102 | Address string `yaml:"address"`
103 | Port int `yaml:"port"`
104 | } `yaml:"prometheus"`
105 | }
106 |
107 | type GCPCloudLoggingInfo struct {
108 | Enabled bool `yaml:"enabled"`
109 | GCPCredentialInfo CloudLoggingCredentialInfo `yaml:"cloud_logging_credential_info"`
110 | }
111 |
112 | type CloudLoggingCredentialInfo struct {
113 | ProjectID string `yaml:"project_id"`
114 | CredentialsType string `yaml:"credentials_type"`
115 | Credentials string `yaml:"credentials"`
116 | CheckerSchedule []string `yaml:"checker_schedule"`
117 | }
118 |
119 | // PluginInfo contains general plugin info.
120 | type PluginInfo struct {
121 | DefaultVerifyInterval int `yaml:"default_verify_interval"`
122 | DefaultExecuteInterval int `yaml:"default_execute_interval"`
123 | DefaultPluginName string `yaml:"default_plugin_name"`
124 | Plugins []Plugin `yaml:"plugins"`
125 | }
126 |
127 | // Plugin contains specific plugin info.
128 | type Plugin struct {
129 | Name string `yaml:"plugin_name"`
130 | Address string `yaml:"plugin_address"`
131 | VerifyInterval int `yaml:"verify_interval"`
132 | ExecuteInterval int `yaml:"execute_interval"`
133 | Port int `yaml:"plugin_port"`
134 | ExecutableMethods []struct {
135 | Name string `yaml:"method_name"`
136 | } `yaml:"executable_methods"`
137 | }
138 |
139 | type parser struct {
140 | }
141 |
142 | func (p *parser) loadConfigFile(path string) ([]byte, error) {
143 | var (
144 | rawYAML []byte
145 | err error
146 | )
147 |
148 | if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
149 | resp, err := http.Get(path)
150 | if err != nil {
151 | return nil, err
152 | }
153 | if resp.StatusCode != http.StatusOK {
154 | return nil, fmt.Errorf("invalid response status %d", resp.StatusCode)
155 | }
156 |
157 | rawYAML, err = io.ReadAll(resp.Body)
158 | if err != nil {
159 | return nil, err
160 | }
161 | } else {
162 | rawYAML, err = os.ReadFile(path)
163 | if err != nil {
164 | return nil, err
165 | }
166 | }
167 |
168 | if err != nil {
169 | return nil, err
170 | }
171 |
172 | return rawYAML, err
173 | }
174 |
175 | func (p *parser) parseYAML(contents []byte) (*Config, error) {
176 | newConfig := Config{}
177 | err := yaml.Unmarshal(contents, &newConfig)
178 | if err != nil {
179 | return nil, err
180 | }
181 |
182 | p.overwrite(&newConfig)
183 |
184 | return &newConfig, nil
185 | }
186 |
187 | func (p *parser) overwrite(config *Config) {
188 | if config.Vatz.RPCInfo.GRPCPort == 0 {
189 | config.Vatz.RPCInfo.GRPCPort = DefaultGRPCPort
190 | }
191 |
192 | if config.Vatz.RPCInfo.HTTPPort == 0 {
193 | config.Vatz.RPCInfo.HTTPPort = DefaultHTTPPort
194 | }
195 |
196 | if config.Vatz.HomePath == "" {
197 | config.Vatz.HomePath = DefaultHomePath
198 | }
199 |
200 | for i, plugin := range config.PluginInfos.Plugins {
201 | if plugin.VerifyInterval == 0 {
202 | config.PluginInfos.Plugins[i].VerifyInterval = config.PluginInfos.DefaultVerifyInterval
203 | }
204 |
205 | if plugin.ExecuteInterval == 0 {
206 | config.PluginInfos.Plugins[i].ExecuteInterval = config.PluginInfos.DefaultExecuteInterval
207 | }
208 |
209 | if plugin.Name == "" {
210 | config.PluginInfos.Plugins[i].Name = config.PluginInfos.DefaultPluginName
211 | }
212 | }
213 | }
214 |
215 | func (p *parser) duplicatedPlugin(config *Config) {
216 | b := make(map[string][]int)
217 | for _, p := range config.PluginInfos.Plugins {
218 | b[p.Name] = append(b[p.Name], p.Port)
219 | }
220 |
221 | for pName, port := range b {
222 | if len(port) > 1 {
223 | log.Warn().Str("module", "config").
224 | Msgf(fmt.Sprintf("The plugin(%s) with the same name are currently up and running on %v ports.", pName, port))
225 | }
226 | }
227 | }
228 |
229 | // InitConfig - initializes VATZ config.
230 | func InitConfig(configFile string) (*Config, error) {
231 | if vatzConfig != nil {
232 | log.Info().Str("module", "config").Msgf("Config already loaded")
233 | return vatzConfig, nil
234 | }
235 |
236 | var configError error
237 |
238 | wg := sync.WaitGroup{}
239 | wg.Add(1)
240 |
241 | configOnce.Do(func() {
242 | // TODO: How do I add default values?
243 | log.Debug().Str("module", "config").Msgf("Load Config %s", configFile)
244 |
245 | defer wg.Done()
246 | var configData []byte
247 |
248 | p := parser{}
249 | configData, configError = p.loadConfigFile(configFile)
250 | if configError != nil {
251 | return
252 | }
253 |
254 | vatzConfig, configError = p.parseYAML(configData)
255 | if configError != nil {
256 | log.Error().Str("module", "config").Msgf("parseYAML Error: %s", configError)
257 | return
258 | }
259 |
260 | p.duplicatedPlugin(vatzConfig)
261 | })
262 |
263 | wg.Wait()
264 |
265 | return vatzConfig, configError
266 | }
267 |
268 | // GetConfig returns current Vatz config.
269 | func GetConfig() *Config {
270 | return vatzConfig
271 | }
272 |
--------------------------------------------------------------------------------
/LICENSE.LESSER:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/manager/plugin/plugin.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | tp "github.com/dsrvlabs/vatz/types"
8 | "os"
9 | "os/exec"
10 | "strings"
11 | "time"
12 |
13 | "github.com/rs/zerolog/log"
14 | "github.com/shirou/gopsutil/process"
15 |
16 | "github.com/dsrvlabs/vatz/manager/config"
17 | )
18 |
19 | const (
20 | // TODO: Should be configurable?
21 | pluginDBName = "vatz.db"
22 | )
23 |
24 | // VatzPlugin describes plugin information
25 | type VatzPlugin struct {
26 | Name string `json:"name"`
27 | IsEnabled bool `json:"is_enabled"`
28 | Location string `json:"location"`
29 | Repository string `json:"repository"`
30 | Version string `json:"version"`
31 | InstalledAt time.Time `json:"installed_at"`
32 | }
33 |
34 | // VatzPluginManager provides management functions for plugin.
35 | type VatzPluginManager interface {
36 | Init(runType tp.Initializer) error
37 | Get(name string) (VatzPlugin, error)
38 | Install(repo, name, version string) error
39 | Uninstall(name string) error
40 | List() ([]VatzPlugin, error)
41 | SetEnabled(pluginID string, isEnabled bool) error
42 | Start(name, args string, logfile *os.File) error
43 | Stop(name string) error
44 | }
45 |
46 | type vatzPluginManager struct {
47 | home string
48 | }
49 |
50 | func (m *vatzPluginManager) Init(runType tp.Initializer) error {
51 | dbName := pluginDBName
52 | if runType == tp.TEST {
53 | dbName = "vatz-test.db"
54 | }
55 |
56 | path, err := config.GetConfig().Vatz.AbsoluteHomePath()
57 | if err != nil {
58 | return err
59 | }
60 |
61 | return initDB(fmt.Sprintf("%s/%s", path, dbName))
62 | }
63 |
64 | func (m *vatzPluginManager) Install(repo, name, version string) error {
65 | var fixedRepo string
66 | strTokens := strings.Split(repo, "://")
67 | if len(strTokens) >= 2 {
68 | fixedRepo = strTokens[1]
69 | } else {
70 | fixedRepo = strTokens[0]
71 | }
72 |
73 | log.Debug().Str("module", "plugin").Msgf("Install new plugin %s", fixedRepo)
74 |
75 | var stdout, stderr bytes.Buffer
76 |
77 | os.Setenv("GOBIN", m.home)
78 |
79 | exeCmd := exec.Command("go", "install", fixedRepo+"@"+version)
80 | exeCmd.Stdout = &stdout
81 | exeCmd.Stderr = &stderr
82 |
83 | err := exeCmd.Run()
84 | if err != nil {
85 | log.Error().Str("module", "plugin").Msgf("Install > exeCmd.Run Error: %s", stderr.String())
86 | return err
87 | }
88 |
89 | dirTokens := strings.Split(repo, "/")
90 | binName := dirTokens[len(dirTokens)-1]
91 |
92 | origPath := fmt.Sprintf("%s/%s", m.home, binName)
93 | newPath := fmt.Sprintf("%s/%s", m.home, name)
94 |
95 | // Binary name should be changed.
96 | err = os.Rename(origPath, newPath)
97 | if err != nil {
98 | log.Error().Str("module", "plugin").Msgf("Install > os.Rename Error: %s", err)
99 | return err
100 | }
101 |
102 | dbWr, err := newWriter(fmt.Sprintf("%s/%s", m.home, pluginDBName))
103 | if err != nil {
104 | log.Error().Str("module", "plugin").Msgf("Install > newWriter Error: %s", err)
105 | return err
106 | }
107 |
108 | err = dbWr.AddPlugin(pluginEntry{
109 | Name: name,
110 | IsEnabled: 1,
111 | Repository: repo,
112 | BinaryLocation: newPath,
113 | Version: version,
114 | InstalledAt: time.Now(),
115 | })
116 |
117 | if err != nil {
118 | log.Error().Str("module", "plugin").Msgf("Install > dbWr.AddPlugin Error: %s", err)
119 | return err
120 | }
121 |
122 | log.Debug().Str("module", "plugin").Msgf("A new plugin %s from %s is installed at %s.", name, repo, newPath)
123 | return nil
124 | }
125 |
126 | func (m *vatzPluginManager) List() ([]VatzPlugin, error) {
127 | log.Debug().Str("module", "plugin").Msgf("List")
128 |
129 | dbRd, err := newReader(fmt.Sprintf("%s/%s", m.home, pluginDBName))
130 | if err != nil {
131 | log.Error().Str("module", "plugin").Msgf("Install > newReader Error: %s", err)
132 | return nil, err
133 | }
134 |
135 | dbPlugins, err := dbRd.List()
136 | if err != nil {
137 | log.Error().Str("module", "plugin").Msgf("Install > dbRd.List Error: %s", err)
138 | return nil, err
139 | }
140 |
141 | plugins := make([]VatzPlugin, len(dbPlugins))
142 |
143 | for i, p := range dbPlugins {
144 | isEnabled := false
145 | if p.IsEnabled == 1 {
146 | isEnabled = true
147 | }
148 | plugins[i].Name = p.Name
149 | plugins[i].IsEnabled = isEnabled
150 | plugins[i].Repository = p.Repository
151 | plugins[i].Location = p.BinaryLocation
152 | plugins[i].Version = p.Version
153 | plugins[i].InstalledAt = p.InstalledAt
154 | }
155 |
156 | return plugins, nil
157 | }
158 |
159 | func (m *vatzPluginManager) Uninstall(name string) error {
160 | log.Debug().Str("module", "plugin").Msgf("Uninstall")
161 | ps, err := m.findProcessByName(name)
162 | if err != nil {
163 | if !strings.Contains(err.Error(), "can't find the process") {
164 | return err
165 | }
166 | } else {
167 | running, err := ps.IsRunning()
168 | if err != nil {
169 | log.Error().Str("module", "plugin").Msgf("Uninstall > ps.IsRunning Error: %s", err)
170 | return err
171 | }
172 | if running {
173 | log.Error().Str("module", "plugin").Err(err).Msgf("Plugin %s is currently running, Please, stop plugin first.", name)
174 | return fmt.Errorf("Please, stop plugin: %s first.", name)
175 | }
176 | }
177 |
178 | pluginInfo, err := m.Get(name)
179 | if err != nil {
180 | return err
181 | }
182 |
183 | var stdout, stderr bytes.Buffer
184 |
185 | os.Setenv("GOBIN", m.home)
186 |
187 | exeCmd := exec.Command("rm", "-rf", pluginInfo.Location)
188 | exeCmd.Stdout = &stdout
189 | exeCmd.Stderr = &stderr
190 |
191 | err = exeCmd.Run()
192 | if err != nil {
193 | log.Error().Str("module", "plugin").Msgf("Uninstall > exeCmd.Run Error: %s", err)
194 | return err
195 | }
196 |
197 | dbWr, err := newWriter(fmt.Sprintf("%s/%s", m.home, pluginDBName))
198 | if err != nil {
199 | log.Error().Str("module", "plugin").Msgf("Uninstall > newWriter Error: %s", err)
200 | return err
201 | }
202 |
203 | err = dbWr.DeletePlugin(name)
204 | if err != nil {
205 | log.Error().Str("module", "plugin").Msgf("Uninstall > dbWr.DeletePlugin Error: %s", err)
206 | return err
207 | }
208 |
209 | return nil
210 | }
211 |
212 | func (m *vatzPluginManager) Get(name string) (VatzPlugin, error) {
213 | log.Debug().Str("module", "plugin").Msgf("Get %s", name)
214 |
215 | dbRd, err := newReader(fmt.Sprintf("%s/%s", m.home, pluginDBName))
216 | if err != nil {
217 | log.Error().Str("module", "plugin").Msgf("Get > newReader Error: %s", err)
218 | return VatzPlugin{}, err
219 | }
220 |
221 | dbPlugin, err := dbRd.Get(name)
222 | if err != nil {
223 | log.Error().Str("module", "plugin").Msgf("Get > dbRd.Get Error: %s", err)
224 | return VatzPlugin{}, err
225 | }
226 |
227 | isEnabled := false
228 | if dbPlugin.IsEnabled > 0 {
229 | isEnabled = true
230 | }
231 |
232 | return VatzPlugin{
233 | Name: dbPlugin.Name,
234 | IsEnabled: isEnabled,
235 | Repository: dbPlugin.Repository,
236 | Location: dbPlugin.BinaryLocation,
237 | Version: dbPlugin.Version,
238 | InstalledAt: dbPlugin.InstalledAt,
239 | }, nil
240 | }
241 |
242 | func (m *vatzPluginManager) SetEnabled(pluginID string, isEnabled bool) error {
243 | dbWr, err := newWriter(fmt.Sprintf("%s/%s", m.home, pluginDBName))
244 | if err != nil {
245 | log.Error().Str("module", "plugin").Msgf("Update > newWriter Error: %s", err)
246 | return err
247 | }
248 |
249 | err = dbWr.UpdatePluginEnabling(pluginID, isEnabled)
250 | if err != nil {
251 | log.Error().Str("module", "plugin").Msgf("Update > dbWr.UpdatePlugin Error: %s", err)
252 | return err
253 | }
254 | return nil
255 | }
256 |
257 | func (m *vatzPluginManager) Start(name, args string, logfile *os.File) error {
258 | log.Debug().Str("module", "plugin").Msgf("Start plugin %s", name)
259 |
260 | dbRd, err := newReader(fmt.Sprintf("%s/%s", m.home, pluginDBName))
261 | if err != nil {
262 | return err
263 | }
264 |
265 | e, err := dbRd.Get(name)
266 | if err != nil {
267 | return err
268 | }
269 |
270 | f := func(r rune) bool {
271 | return r == '=' || r == ' '
272 | }
273 |
274 | splits := strings.FieldsFunc(args, f)
275 | cmd := exec.Command(e.BinaryLocation, splits...)
276 | cmd.Stdout = logfile
277 | log.Info().Str("module", "plugin").Msgf("Plugin %s is successfully started.", name)
278 | return cmd.Start()
279 | }
280 |
281 | func (m *vatzPluginManager) Stop(name string) error {
282 | log.Debug().Str("module", "plugin").Msgf("Stop plugin %s", name)
283 |
284 | ps, err := m.findProcessByName(name)
285 | if err != nil {
286 | return err
287 | }
288 |
289 | err = ps.Kill()
290 | if err != nil {
291 | log.Error().Str("module", "plugin").Msgf("Stop > ps.Kill Error: %s", err)
292 | return err
293 | }
294 | log.Info().Str("module", "plugin").Msgf("Plugin %s is successfully stopped.", name)
295 | return nil
296 | }
297 |
298 | func (m *vatzPluginManager) findProcessByName(name string) (*process.Process, error) {
299 | log.Debug().Str("module", "plugin").Msgf("Find Process %s", name)
300 |
301 | processes, err := process.Processes()
302 | if err != nil {
303 | log.Error().Str("module", "plugin").Msgf("findProcessByName > process.Processes Error: %s", err)
304 | return nil, err
305 | }
306 |
307 | for _, p := range processes {
308 | pName, err := p.Name()
309 | if err != nil {
310 | log.Error().Str("module", "plugin").Msgf("findProcessByName > p.Name Error: %s", err)
311 | continue
312 | }
313 |
314 | if pName == name {
315 | return p, nil
316 | }
317 | }
318 | return nil, errors.New("can't find the process")
319 | }
320 |
321 | // NewManager creates new plugin manager.
322 | func NewManager(vatzHome string) VatzPluginManager {
323 | fullPath := fmt.Sprintf("%s/%s", vatzHome, pluginDBName)
324 | pManager := &vatzPluginManager{
325 | home: vatzHome,
326 | }
327 |
328 | if _, err := os.Stat(fullPath); err == nil {
329 | dbWr, err := newWriter(fullPath)
330 | if err != nil {
331 | log.Error().Str("module", "plugin").Msgf("NewManager > newWriter Error: %s", err)
332 | }
333 | err = dbWr.MigratePluginTable()
334 | if err != nil {
335 | log.Error().Str("module", "plugin").Msgf("NewManager > MigratePluginTable Error: %s", err)
336 | }
337 |
338 | }
339 | return pManager
340 | }
341 |
--------------------------------------------------------------------------------
/manager/plugin/db.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "database/sql"
5 | "os"
6 | "path/filepath"
7 | "sync"
8 | "time"
9 |
10 | _ "github.com/mattn/go-sqlite3"
11 | "github.com/rs/zerolog/log"
12 | "golang.org/x/net/context"
13 | )
14 |
15 | var (
16 | once = sync.Once{}
17 |
18 | db *pluginDB
19 | )
20 |
21 | type pluginEntry struct {
22 | Name string
23 | IsEnabled int
24 | Repository string
25 | BinaryLocation string
26 | Version string
27 | InstalledAt time.Time
28 | }
29 |
30 | type dbWriter interface {
31 | MigratePluginTable() error
32 | AddPlugin(e pluginEntry) error
33 | DeletePlugin(name string) error
34 | UpdatePluginEnabling(pluginID string, isEnabled bool) error
35 | }
36 |
37 | type dbReader interface {
38 | List() ([]pluginEntry, error)
39 | Get(identifier string) (*pluginEntry, error)
40 | }
41 |
42 | type pluginDB struct {
43 | conn *sql.Conn
44 | ctx context.Context
45 | }
46 |
47 | func (p *pluginDB) isFieldExist(tableName string, columnName string) (bool, error) {
48 | q := `PRAGMA table_info(plugin)`
49 | rows, err := p.conn.QueryContext(p.ctx, q)
50 | if err != nil {
51 | log.Error().Str("module", "db").Msgf("isFieldExist Error: %s", err)
52 | return false, err
53 | }
54 |
55 | defer rows.Close()
56 |
57 | // Check the columns of the result set
58 | found := false
59 | for rows.Next() {
60 | var cid int
61 | var name string
62 | var typename string
63 | var notnull bool
64 | var dfltvalue sql.NullString
65 | var pk bool
66 |
67 | err = rows.Scan(&cid, &name, &typename, ¬null, &dfltvalue, &pk)
68 | if err != nil {
69 | panic(err)
70 | }
71 |
72 | if name == "is_enabled" {
73 | found = true
74 | break
75 | }
76 | }
77 | return found, nil
78 | }
79 |
80 | func (p *pluginDB) MigratePluginTable() error {
81 | isExist, fieldError := p.isFieldExist("plugin", "is_enabled")
82 |
83 | if fieldError != nil {
84 | log.Error().Str("module", "db").Msgf("MigratePluginTable > isFieldExist Error: %s", fieldError)
85 | return fieldError
86 | }
87 |
88 | if !isExist {
89 | opts := &sql.TxOptions{Isolation: sql.LevelDefault}
90 | tx, err := p.conn.BeginTx(p.ctx, opts)
91 | if err != nil {
92 | return err
93 | }
94 | q := `ALTER TABLE plugin ADD COLUMN is_enabled INTEGER DEFAULT 1`
95 |
96 | _, err = tx.Exec(q)
97 | if err != nil {
98 | log.Error().Str("module", "db").Msgf("MigratePluginTable > tx.Exec Error: %s", err)
99 | return err
100 | }
101 |
102 | if err = tx.Commit(); err != nil {
103 | log.Error().Str("module", "db").Msgf("MigratePluginTable > tx.Commit Error: %s", err)
104 | return err
105 | }
106 | }
107 |
108 | return nil
109 | }
110 |
111 | func (p *pluginDB) AddPlugin(e pluginEntry) error {
112 | log.Debug().Str("module", "db").Msg("AddPlugin")
113 |
114 | opts := &sql.TxOptions{
115 | Isolation: sql.LevelDefault,
116 | }
117 | tx, err := p.conn.BeginTx(p.ctx, opts)
118 | if err != nil {
119 | log.Info().Str("module", "db").Err(err)
120 | return err
121 | }
122 |
123 | q := `INSERT INTO plugin(name, is_enabled, repository, binary_location, version, installed_at) VALUES(?, ?, ?, ?, ?, ?)
124 | `
125 |
126 | _, err = tx.Exec(q, e.Name, e.IsEnabled, e.Repository, e.BinaryLocation, e.Version, e.InstalledAt)
127 | if err != nil {
128 | log.Error().Str("module", "db").Msgf("AddPlugin > tx.Exec Error: %s", err)
129 | return err
130 | }
131 |
132 | if err = tx.Commit(); err != nil {
133 | log.Error().Str("module", "db").Msgf("AddPlugin > tx.Commit Error: %s", err)
134 | return err
135 | }
136 |
137 | return nil
138 | }
139 |
140 | func (p *pluginDB) createPluginTable() error {
141 | opts := &sql.TxOptions{Isolation: sql.LevelDefault}
142 | tx, err := p.conn.BeginTx(p.ctx, opts)
143 | if err != nil {
144 | return err
145 | }
146 |
147 | q := `
148 | CREATE TABLE IF NOT EXISTS plugin (
149 | name varchar(256) PRIMARY KEY,
150 | is_enabled int,
151 | repository varchar(256),
152 | binary_location varchar(256),
153 | version varchar(256),
154 | installed_at DATE)
155 | `
156 | _, err = tx.Exec(q)
157 | if err != nil {
158 | log.Error().Str("module", "db").Msgf("createPluginTable > tx.Exec Error: %s", err)
159 | return err
160 | }
161 |
162 | if err = tx.Commit(); err != nil {
163 | log.Error().Str("module", "db").Msgf("createPluginTable > tx.Commit Error: %s", err)
164 | return err
165 | }
166 |
167 | return nil
168 | }
169 |
170 | func (p *pluginDB) DeletePlugin(name string) error {
171 | log.Debug().Str("module", "db").Msgf("Uninstall Plugin %s", name)
172 |
173 | opts := &sql.TxOptions{Isolation: sql.LevelDefault}
174 | tx, err := p.conn.BeginTx(p.ctx, opts)
175 | if err != nil {
176 | log.Error().Str("module", "db").Msgf("DeletePlugin > conn.BeginTx Error: %s", err)
177 | return err
178 | }
179 |
180 | _, err = tx.Exec("DELETE FROM plugin WHERE name = ?", name)
181 | if err != nil {
182 | log.Error().Str("module", "db").Msgf("DeletePlugin > tx.Exec Error: %s", err)
183 | return err
184 | }
185 |
186 | if err = tx.Commit(); err != nil {
187 | log.Error().Str("module", "db").Msgf("DeletePlugin > tx.Commit Error: %s", err)
188 | return err
189 | }
190 |
191 | return nil
192 | }
193 |
194 | func (p *pluginDB) UpdatePluginEnabling(name string, isEnabled bool) error {
195 | // TODO: 1. Set best identifier for plugins either of Plugin_id or Name
196 | log.Debug().Str("module", "db").Msg("UpdatePlugin")
197 |
198 | response := "disabled"
199 | if isEnabled {
200 | response = "enabled"
201 | }
202 |
203 | opts := &sql.TxOptions{Isolation: sql.LevelDefault}
204 | tx, err := p.conn.BeginTx(p.ctx, opts)
205 | if err != nil {
206 | log.Info().Str("module", "db").Err(err)
207 | return err
208 | }
209 |
210 | q := `UPDATE plugin SET is_enabled = ? WHERE name = ?`
211 |
212 | isEnabledInt := 0
213 | if isEnabled {
214 | isEnabledInt = 1
215 | }
216 |
217 | _, err = tx.Exec(q, isEnabledInt, name)
218 | if err != nil {
219 | log.Error().Str("module", "db").Msgf("UpdatePlugin > tx.Exec Error: %s", err)
220 | return err
221 | }
222 |
223 | if err = tx.Commit(); err != nil {
224 | log.Error().Str("module", "db").Msgf("UpdatePlugin > tx.Commit Error: %s", err)
225 | return err
226 | }
227 |
228 | log.Info().Str("module", "db").Msgf("Plugin %s is %s.", name, response)
229 |
230 | return nil
231 | }
232 |
233 | func (p *pluginDB) List() ([]pluginEntry, error) {
234 | log.Debug().Str("module", "db").Msg("List")
235 |
236 | q := `SELECT name, is_enabled, repository, binary_location, version, installed_at FROM plugin`
237 | rows, err := p.conn.QueryContext(p.ctx, q)
238 | if err != nil {
239 | log.Info().Str("module", "db").Err(err)
240 | return nil, err
241 | }
242 |
243 | defer rows.Close()
244 | retPlugins := make([]pluginEntry, 0)
245 |
246 | for rows.Next() {
247 | e := pluginEntry{}
248 | err := rows.Scan(&e.Name, &e.IsEnabled, &e.Repository, &e.BinaryLocation, &e.Version, &e.InstalledAt)
249 | if err != nil {
250 | log.Error().Str("module", "db").Msgf("List > rows.Scan Error: %s", err)
251 | return nil, err
252 | }
253 | retPlugins = append(retPlugins, e)
254 | }
255 |
256 | return retPlugins, nil
257 | }
258 |
259 | func (p *pluginDB) Get(name string) (*pluginEntry, error) {
260 | log.Debug().Str("module", "db").Msgf("Get %s", name)
261 |
262 | q := `SELECT name, is_enabled, repository, binary_location, version, installed_at FROM plugin WHERE name=?`
263 | e := pluginEntry{}
264 | err := p.conn.
265 | QueryRowContext(p.ctx, q, name).
266 | Scan(&e.Name, &e.IsEnabled, &e.Repository, &e.BinaryLocation, &e.Version, &e.InstalledAt)
267 | if err != nil {
268 | log.Error().Str("module", "db").Msgf("Get > QueryRowContext.Scan Error: %s", err)
269 | return nil, err
270 | }
271 |
272 | return &e, err
273 | }
274 |
275 | func newWriter(dbfile string) (dbWriter, error) {
276 | // log.Debug().Str("module", "db").Msgf("newWriter %s", dbfile)
277 |
278 | chanErr := make(chan error, 1)
279 |
280 | once.Do(func() {
281 | log.Debug().Str("module", "db").Msg("Create DB Instance")
282 |
283 | ctx := context.Background()
284 | conn, err := getDBConnection(ctx, dbfile)
285 | if err != nil {
286 | log.Error().Str("module", "db").Msgf("newWriter > getDBConnection Error: %s", err)
287 | chanErr <- err
288 | return
289 | }
290 |
291 | db = &pluginDB{ctx: ctx, conn: conn}
292 | chanErr <- nil
293 | })
294 |
295 | var err error
296 | if db == nil {
297 | log.Info().Str("module", "db").Msg("Wait creation")
298 | err = <-chanErr
299 | }
300 |
301 | return db, err
302 | }
303 |
304 | func newReader(dbfile string) (dbReader, error) {
305 | // log.Debug().Str("module", "db").Msgf("newReader %s", dbfile)
306 |
307 | chanErr := make(chan error, 1)
308 |
309 | once.Do(func() {
310 | log.Info().Str("module", "db").Msg("Read DB Instance")
311 |
312 | ctx := context.Background()
313 | conn, err := getDBConnection(ctx, dbfile)
314 | if err != nil {
315 | log.Error().Str("module", "db").Msgf("newReader > getDBConnection Error: %s", err)
316 | chanErr <- err
317 | return
318 | }
319 |
320 | db = &pluginDB{ctx: ctx, conn: conn}
321 | chanErr <- nil
322 | })
323 |
324 | var err error
325 | if db == nil {
326 | log.Info().Str("module", "db").Msg("Wait creation")
327 | err = <-chanErr
328 | }
329 |
330 | return db, err
331 | }
332 |
333 | func getDBConnection(ctx context.Context, dbfile string) (*sql.Conn, error) {
334 | db, err := sql.Open("sqlite3", dbfile)
335 | if err != nil {
336 | return nil, err
337 | }
338 |
339 | conn, err := db.Conn(ctx)
340 | if err != nil {
341 | return nil, err
342 | }
343 |
344 | return conn, nil
345 | }
346 |
347 | func initDB(dbfile string) error {
348 | log.Info().Str("module", "db").Msgf("Initialize DB %s", dbfile)
349 |
350 | if db != nil {
351 | db.conn.Close()
352 | db = nil
353 |
354 | once = sync.Once{}
355 | }
356 |
357 | log.Debug().Str("module", "db").Msg("Remove old DB file")
358 |
359 | err := os.Remove(dbfile)
360 | if err != nil && !os.IsNotExist(err) {
361 | log.Error().Str("module", "---db").Err(err)
362 | return err
363 | }
364 |
365 | path := filepath.Dir(dbfile)
366 | if _, err := os.Stat(path); os.IsNotExist(err) {
367 | _ = os.Mkdir(path, 0755)
368 | }
369 |
370 | log.Debug().Str("module", "db").Msg("Create new DB file")
371 |
372 | f, err := os.Create(dbfile)
373 | if err != nil {
374 | log.Error().Str("module", "db").Err(err)
375 | return err
376 | }
377 |
378 | defer f.Close()
379 |
380 | _, err = newWriter(dbfile)
381 | if err != nil {
382 | log.Error().Str("module", "db").Err(err)
383 | return err
384 | }
385 |
386 | err = db.createPluginTable()
387 | if err != nil {
388 | log.Error().Str("module", "db").Err(err)
389 | return err
390 | }
391 |
392 | return nil
393 | }
394 |
--------------------------------------------------------------------------------