diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b73280a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: go-co-op +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ca534ca --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + # Maintain Go dependencies + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..78e7ba0 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '34 7 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml new file mode 100644 index 0000000..2cd8195 --- /dev/null +++ b/.github/workflows/go_test.yml @@ -0,0 +1,33 @@ +on: [push] +name: golangci-lint +jobs: + golangci: + strategy: + matrix: + go-version: + - "1.20" + name: lint and test + runs-on: ubuntu-latest + services: + etcd: + image: bitnami/etcd:3.5.5 + env: + ALLOW_NONE_AUTHENTICATION: yes + ETCD_ADVERTISE_CLIENT_URLS: http://127.0.0.1:2379 + ETCDCTL_API: 3 + ports: + - 2379:2379 + - 2380:2380 + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3.7.0 + with: + version: v1.51.2 + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + - name: test + run: make test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b2d3be8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing to gocron + +Thank you for coming to contribute to gocron! We welcome new ideas, PRs and general feedback. + +## Reporting Bugs + +If you find a bug then please let the project know by opening an issue after doing the following: + +- Do a quick search of the existing issues to make sure the bug isn't already reported +- Try and make a minimal list of steps that can reliably reproduce the bug you are experiencing +- Collect as much information as you can to help identify what the issue is (project version, configuration files, etc) + +## Suggesting Enhancements + +If you have a use case that you don't see a way to support yet, we would welcome the feedback in an issue. Before opening the issue, please consider: + +- Is this a common use case? +- Is it simple to understand? + +You can help us out by doing the following before raising a new issue: + +- Check that the feature hasn't been requested already by searching existing issues +- Try and reduce your enhancement into a single, concise and deliverable request, rather than a general idea +- Explain your own use cases as the basis of the request + +## Adding Features + +Pull requests are always welcome. However, before going through the trouble of implementing a change it's worth creating a bug or feature request issue. +This allows us to discuss the changes and make sure they are a good fit for the project. + +Please always make sure a pull request has been: + +- Unit tested with `make test` +- Linted with `make lint` +- Vetted with `make vet` +- Formatted with `make fmt` or validated with `make check-fmt` + +## Writing Tests + +Tests should follow the [table driven test pattern](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go). See other tests in the code base for additional examples. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d6f18df --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: fmt check-fmt lint vet test + +GO_PKGS := $(shell go list -f {{.Dir}} ./...) + +fmt: + @go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {} + +lint: + @golangci-lint run + +test: + @go test -v $(GO_FLAGS) -count=1 $(GO_PKGS) diff --git a/README.md b/README.md index 0da7313..68f0505 100644 --- a/README.md +++ b/README.md @@ -1 +1,71 @@ -# gocron-etcd-elector \ No newline at end of file +# gocron-etcd-elector + +## install + +``` +go get github.com/go-co-op/gocron-etcd-elector +``` + +## usage + +Here is an example usage that would be deployed in multiple instances. + +```go +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/go-co-op/gocron" + elector "github.com/go-co-op/gocron-etcd-elector" +) + +func main() { + cfg := elector.Config{ + Endpoints: []string{"http://127.0.0.1:2379"}, + DialTimeout: 3 * time.Second, + } + + el, err := elector.NewElector(context.Background(), cfg, elector.WithTTL(10)) + if err != nil { + panic(err) + } + + go func() { + for { + err := el.Start("/gocron/elector") + if err == elector.ErrClosed { + return + } + + time.Sleep(1e9) + } + }() + + s := gocron.NewScheduler(time.UTC) + s.WithDistributedElector(el) + + s.Every("1s").Do(func() { + if el.IsLeader(context.TODO()) == nil { + fmt.Println("the current instance is leader") + } else { + fmt.Println("the current leader is", el.GetLeaderID()) + } + + fmt.Println("call 1s") + }) + + s.StartAsync() + + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) + <-c + + fmt.Println("exit") +} +``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6b98641 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Supported Versions + +The current plan is to maintain version 1 as long as possible incorporating any necessary security patches. + +| Version | Supported | +| ------- | ------------------ | +| 1.x.x | :white_check_mark: | + +## Reporting a Vulnerability + +Vulnerabilities can be reported by [opening an issue](https://github.com/go-co-op/gocron/issues/new/choose) or reaching out on Slack: [](https://gophers.slack.com/archives/CQ7T0T1FW) + +We will do our best to addrerss any vulnerabilities in an expeditious manner. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bf03bb0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3' + +services: + docker-etcd: + hostname: etcd + image: bitnami/etcd:3.5.5 + environment: + - ALLOW_NONE_AUTHENTICATION=yes + - ETCD_ADVERTISE_CLIENT_URLS=http://127.0.0.1:2379 + - ETCDCTL_API=3 + ports: + - "2379:2379" + - "2380:2380" diff --git a/elector.go b/elector.go new file mode 100644 index 0000000..79ee7e0 --- /dev/null +++ b/elector.go @@ -0,0 +1,211 @@ +package elector + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + mrand "math/rand" + "os" + "sync" + "time" + + "github.com/go-co-op/gocron" + clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/client/v3/concurrency" +) + +var ( + ErrNonLeader = errors.New("the elector is not leader") + ErrClosed = errors.New("the elector is already closed") +) + +var ( + // alias options + WithTTL = concurrency.WithTTL + WithContext = concurrency.WithContext + WithLease = concurrency.WithLease +) + +type ( + // alias clientv3.config + Config = clientv3.Config +) + +func nullLogger(msg ...interface{}) {} + +var _ gocron.Elector = (*Elector)(nil) + +type Elector struct { + ctx context.Context + cancel context.CancelFunc + + config clientv3.Config + options []concurrency.SessionOption + client *clientv3.Client + id string + + mu sync.RWMutex + closed bool + isLeader bool + leaderID string + + logger func(msg ...interface{}) +} + +func NewElector(ctx context.Context, cfg clientv3.Config, options ...concurrency.SessionOption) (*Elector, error) { + return newElector(ctx, nil, cfg, options...) +} + +func NewElectorWithClient(ctx context.Context, cli *clientv3.Client, options ...concurrency.SessionOption) (*Elector, error) { + return newElector(ctx, cli, Config{}, options...) +} + +func newElector(ctx context.Context, cli *clientv3.Client, cfg clientv3.Config, options ...concurrency.SessionOption) (*Elector, error) { + var err error + if cli == nil { + cli, err = clientv3.New(cfg) + if err != nil { + return nil, err + } + } + + cctx, cancel := context.WithCancel(ctx) + el := &Elector{ + ctx: cctx, + cancel: cancel, + config: cfg, + options: options, + id: getID(), + client: cli, + logger: nullLogger, + } + return el, nil +} + +func (e *Elector) SetLogger(fn func(msg ...interface{})) { + e.logger = fn +} + +func (e *Elector) GetID() string { + return e.id +} + +func (e *Elector) GetLeaderID() string { + e.mu.Lock() + defer e.mu.Unlock() + + return e.leaderID +} + +func (e *Elector) Stop() error { + e.mu.Lock() + defer e.mu.Unlock() + + if e.closed { + return nil + } + + e.cancel() + e.closed = true + e.client.Close() + return nil +} + +func (e *Elector) IsLeader(_ context.Context) error { + e.mu.RLock() + defer e.mu.RUnlock() + + if e.isLeader { + return nil + } + + return ErrNonLeader +} + +func (e *Elector) setLeader(id string) { + e.mu.Lock() + defer e.mu.Unlock() + + e.isLeader = true + e.leaderID = id +} + +func (e *Elector) unsetLeader(id string) { + e.mu.Lock() + defer e.mu.Unlock() + + e.isLeader = false + e.leaderID = id +} + +// Start Start the election. +// This method will keep trying the election. When the election is successful, set isleader to true. +// If it fails, the election directory will be monitored until the election is successful. +// The parameter electionPath is used to specify the etcd directory for the operation. +func (e *Elector) Start(electionPath string) error { + if e.closed { + return ErrClosed + } + + session, err := concurrency.NewSession(e.client, e.options...) + if err != nil { + return err + } + defer session.Close() + + electionHandler := concurrency.NewElection(session, electionPath) + go func() { + for e.ctx.Err() == nil { + // If the election cannot be obtained, it will be blocked until the election can be obtained. + if err := electionHandler.Campaign(e.ctx, e.id); err != nil { + e.logger(fmt.Errorf("election failed to campaign, err: %w", err)) + } + + time.Sleep(100 * time.Millisecond) + } + }() + + defer func() { + // unset leader + e.unsetLeader("") + }() + + for e.ctx.Err() == nil { + select { + case resp := <-electionHandler.Observe(e.ctx): + if len(resp.Kvs) == 0 { + continue + } + + for i := 0; i < len(resp.Kvs); i++ { + val := string(resp.Kvs[i].Value) + if val != e.id { + e.unsetLeader(val) + e.logger("switch to non-leader, the current leader is ", val) + continue + } + + if e.IsLeader(e.ctx) != nil { // is non-leader + e.setLeader(val) + e.logger("switch to leader, the current instance is leader") + } + } + + case <-e.ctx.Done(): + return nil + } + } + + return nil +} + +func getID() string { + hostname, _ := os.Hostname() + bs := make([]byte, 10) + _, err := rand.Read(bs) + if err != nil { + return fmt.Sprintf("%s-%d", hostname, mrand.Int63()) + } + return fmt.Sprintf("%s-%x", hostname, bs) +} diff --git a/elector_test.go b/elector_test.go new file mode 100644 index 0000000..f2c6b49 --- /dev/null +++ b/elector_test.go @@ -0,0 +1,232 @@ +package elector + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/go-co-op/gocron" + "github.com/stretchr/testify/assert" +) + +var ( + testConfig = Config{ + Endpoints: []string{"http://127.0.0.1:2379"}, + } + testElectionPath = "/gocron/elector/" +) + +func TestGocronWithElector(t *testing.T) { + el, err := NewElector(context.Background(), testConfig, WithTTL(1)) + assert.Equal(t, nil, err) + go func() { + err := el.Start(testElectionPath + "gocron_one") + if err != nil { + t.Error(err) + } + }() + + defer func() { + _ = el.Stop() + }() + + done := make(chan struct{}, 1) + counter := 0 + fn := func() { + counter++ + if counter == 10 { + close(done) + } + } + + sched := gocron.NewScheduler(time.UTC) + sched.WithDistributedElector(el) + sched.StartAsync() + _, err = sched.Every("50ms").Do(fn) + assert.Equal(t, nil, err) + + defer sched.Stop() + + select { + case <-done: + case <-time.After(1 * time.Second): + t.Error("done timeout") + } +} + +func TestGocronWithMultipleElectors(t *testing.T) { + elections := []*Elector{} + schedulers := []*gocron.Scheduler{} + workers := 3 + + resultChan := make(chan int, 100) + fn := func(leaderIdx int) { + resultChan <- leaderIdx + } + + for i := 0; i < workers; i++ { + el, err := NewElector(context.Background(), testConfig, WithTTL(1)) + assert.Equal(t, nil, err) + + go func() { + _ = el.Start(testElectionPath + "gocron_multi") + }() + + s := gocron.NewScheduler(time.UTC) + s.WithDistributedElector(el) + _, err = s.Every("50ms").Do(fn, i) + assert.Equal(t, nil, err) + s.StartAsync() + + elections = append(elections, el) + schedulers = append(schedulers, s) + } + + getLeader := func() int { + select { + case leader := <-resultChan: + return leader + case <-time.After(3 * time.Second): + t.Error("wait result timeout") + return -1 + } + } + + // all index of the leader are the same. + leader := getLeader() + for i := 0; i < 10; i++ { + cur := getLeader() + assert.Equal(t, leader, cur) + } + + for i := 0; i < workers; i++ { + _ = elections[i].Stop() + schedulers[i].Stop() + } +} + +func TestElectorSingleAcquire(t *testing.T) { + el, err := NewElector(context.Background(), testConfig, WithTTL(10)) + assert.Equal(t, nil, err) + sig := make(chan struct{}, 1) + go func() { + err := el.Start(testElectionPath + "single") + assert.Equal(t, nil, err) + close(sig) + }() + + time.Sleep(2 * time.Second) + assert.Equal(t, nil, el.IsLeader(context.Background())) + assert.Equal(t, el.GetLeaderID(), el.GetID()) + _ = el.Stop() + + select { + case <-sig: + case <-time.After(2 * time.Second): + t.Error("elector exit timeout") + } + + // after elector.stop, current instance is not leader + assert.Equal(t, ErrNonLeader, el.IsLeader(context.Background())) +} + +func TestElectorMultipleAcquire(t *testing.T) { + var elections = []*Elector{} + var workers = 3 + + // start all electors + for i := 0; i < workers; i++ { + el, err := NewElector(context.Background(), testConfig, WithTTL(10)) + assert.Equal(t, nil, err) + elections = append(elections, el) + + go func() { + err := el.Start(testElectionPath + "multi") + assert.Equal(t, nil, err) + }() + } + + time.Sleep(5 * time.Second) + + var leaderCounter int + for _, el := range elections { + if el.IsLeader(context.Background()) == nil { + leaderCounter++ + } + } + + // only one leader, other instance is non-leader. + assert.Equal(t, leaderCounter, 1) + + // stop all electors + for _, el := range elections { + _ = el.Stop() + } +} + +func TestElectorAcquireRace(t *testing.T) { + var elections = []*Elector{} + var workers = 3 + + // start all electors + for i := 0; i < workers; i++ { + el, err := NewElector(context.Background(), testConfig, WithTTL(1)) + assert.Equal(t, nil, err) + el.id = fmt.Sprintf("idx-%v", i) + + elections = append(elections, el) + + go func() { + err := el.Start(testElectionPath + "race") + assert.Equal(t, nil, err) + }() + + time.Sleep(100 * time.Millisecond) + } + + getCounter := func() int { + var counter int + for _, el := range elections { + if el.isLeader { + counter++ + } + } + return counter + } + + time.Sleep(2 * time.Second) + assert.Equal(t, 1, getCounter()) + + for idx, el := range elections { + last := len(elections) - 1 + + _ = el.Stop() + + time.Sleep(3 * time.Second) + if idx == last { + assert.Equal(t, 0, getCounter()) + break + } + assert.Equal(t, 1, getCounter()) + } +} + +func TestElectorStop(t *testing.T) { + el, err := NewElector(context.Background(), testConfig) + assert.Equal(t, nil, err) + _ = el.Stop() + err = el.Start(testElectionPath) + assert.Equal(t, err, ErrClosed) +} + +func TestGetSid(t *testing.T) { + count := 100000 + set := make(map[string]struct{}, count) + + for i := 0; i < count; i++ { + set[getID()] = struct{}{} + } + + assert.Equal(t, count, len(set)) +} diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..a71e37b --- /dev/null +++ b/example/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/go-co-op/gocron" + elector "github.com/go-co-op/gocron-etcd-elector" +) + +func main() { + cfg := elector.Config{ + Endpoints: []string{"http://127.0.0.1:2379"}, + DialTimeout: 3 * time.Second, + } + + el, err := elector.NewElector(context.Background(), cfg, elector.WithTTL(10)) + if err != nil { + panic(err) + } + + go func() { + for { + err := el.Start("/gocron/elector") + if err == elector.ErrClosed { + return + } + + time.Sleep(1e9) + } + }() + + s := gocron.NewScheduler(time.UTC) + s.WithDistributedElector(el) + + _, _ = s.Every("1s").Do(func() { + if el.IsLeader(context.Background()) == nil { + fmt.Println("the current instance is leader") + } else { + fmt.Println("the current leader is", el.GetLeaderID()) + } + + fmt.Println("call 1s") + }) + + s.StartAsync() + + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) + <-c + + fmt.Println("exit") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ce98cb0 --- /dev/null +++ b/go.mod @@ -0,0 +1,38 @@ +module github.com/go-co-op/gocron-etcd-elector + +go 1.19 + +require ( + github.com/go-co-op/gocron v1.33.2-0.20230907025933-6328e002f1f5 + github.com/stretchr/testify v1.8.2 + go.etcd.io/etcd/api/v3 v3.5.9 // indirect + go.etcd.io/etcd/client/v3 v3.5.9 +) + +require ( + github.com/coreos/go-semver v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + golang.org/x/net v0.9.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect + google.golang.org/grpc v1.57.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/coreos/go-systemd/v22 v22.3.2 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d361fb9 --- /dev/null +++ b/go.sum @@ -0,0 +1,119 @@ +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-co-op/gocron v1.33.2-0.20230907025933-6328e002f1f5 h1:uxP54RN2ZA53saCDkEPAsaKldYmYbz7dZBVOP0gUyJg= +github.com/go-co-op/gocron v1.33.2-0.20230907025933-6328e002f1f5/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/etcd/api/v3 v3.5.9 h1:4wSsluwyTbGGmyjJktOf3wFQoTBIURXHnq9n/G/JQHs= +go.etcd.io/etcd/api/v3 v3.5.9/go.mod h1:uyAal843mC8uUVSLWz6eHa/d971iDGnCRpmKd2Z+X8k= +go.etcd.io/etcd/client/pkg/v3 v3.5.9 h1:oidDC4+YEuSIQbsR94rY9gur91UPL6DnxDCIYd2IGsE= +go.etcd.io/etcd/client/pkg/v3 v3.5.9/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4pN8cGuJeL4= +go.etcd.io/etcd/client/v3 v3.5.9 h1:r5xghnU7CwbUxD/fbUtRyJGaYNfDun8sp/gTr1hew6E= +go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M= +google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=