summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/cleanup_and_retention_test.go228
-rw-r--r--tests/environment_test.go120
-rw-r--r--tests/go.mod50
-rw-r--r--tests/go.sum1085
-rw-r--r--tests/history_bench_test.go130
-rw-r--r--tests/history_test.go835
-rw-r--r--tests/history_test_zones.conf22
-rw-r--r--tests/instance_test.go8
-rw-r--r--tests/internal/utils/database.go28
-rw-r--r--tests/internal/utils/redis.go37
-rw-r--r--tests/internal/utils/slice.go37
-rw-r--r--tests/internal/utils/slice_test.go31
-rw-r--r--tests/internal/value/notification_states.go37
-rw-r--r--tests/internal/value/notification_types.go40
-rw-r--r--tests/internal/value/value.go103
-rw-r--r--tests/main_test.go29
-rw-r--r--tests/object_sync_test.conf54
-rw-r--r--tests/object_sync_test.go1415
-rw-r--r--tests/regression_394_test.go126
-rw-r--r--tests/sla_test.go385
-rw-r--r--tests/sql/main_test.go23
-rw-r--r--tests/sql/sla_test.go406
-rw-r--r--tests/state_sync_test.go10
23 files changed, 5239 insertions, 0 deletions
diff --git a/tests/cleanup_and_retention_test.go b/tests/cleanup_and_retention_test.go
new file mode 100644
index 0000000..5013516
--- /dev/null
+++ b/tests/cleanup_and_retention_test.go
@@ -0,0 +1,228 @@
+package icingadb_test
+
+import (
+ "encoding/binary"
+ "fmt"
+ "github.com/goccy/go-yaml"
+ "github.com/icinga/icinga-testing/services"
+ "github.com/icinga/icinga-testing/utils/eventually"
+ "github.com/icinga/icingadb/tests/internal/utils"
+ "github.com/jmoiron/sqlx"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestCleanupAndRetention(t *testing.T) {
+ r := it.RedisServerT(t)
+ i := it.Icinga2NodeT(t, "master")
+ i.EnableIcingaDb(r)
+ i.Reload()
+
+ rdb := getDatabase(t)
+ db, err := sqlx.Open(rdb.Driver(), rdb.DSN())
+ require.NoError(t, err, "connecting to SQL database shouldn't fail")
+ t.Cleanup(func() { _ = db.Close() })
+
+ reten := retention{
+ HistoryDays: 7,
+ SlaDays: 30,
+ Options: map[string]int{
+ "acknowledgement": 0, // No cleanup.
+ "comment": 1,
+ "downtime": 2,
+ // notification and state default to 7.
+ },
+ }
+
+ daysForCategory := func(category string) int {
+ if strings.HasPrefix(category, "sla_") {
+ return reten.SlaDays
+ } else if d, ok := reten.Options[category]; ok {
+ return d
+ } else {
+ return reten.HistoryDays
+ }
+ }
+
+ envId := utils.GetEnvironmentIdFromRedis(t, r)
+ otherEnvId := append([]byte(nil), envId...)
+ otherEnvId[0]++
+
+ rowsToDelete := 10000
+ rowsToSpare := 1000
+ rowsInOtherEnv := 1000
+
+ for category, stmt := range retentionStatements {
+ err := dropNotNullColumns(db, stmt)
+ assert.NoError(t, err)
+
+ retentionDays := daysForCategory(category)
+ start := time.Now().AddDate(0, 0, -retentionDays)
+ startMilli := start.UnixMilli()
+
+ type row struct {
+ Env []byte
+ Id []byte
+ Time int64
+ }
+
+ nextId := 1
+ getId := func() []byte {
+ id := make([]byte, 20)
+ binary.LittleEndian.PutUint64(id, uint64(nextId))
+ nextId++
+ return id
+ }
+ values := make([]row, 0, rowsToDelete+rowsToSpare+rowsInOtherEnv)
+
+ for j := 0; j < rowsToDelete; j++ {
+ values = append(values, row{envId, getId(), startMilli - int64(j)})
+ }
+ for j := 0; j < rowsToSpare; j++ {
+ values = append(values, row{envId, getId(), startMilli + (2 * time.Minute).Milliseconds() + int64(j)})
+ }
+ for j := 0; j < rowsInOtherEnv; j++ {
+ values = append(values, row{otherEnvId, getId(), startMilli - int64(j)})
+ }
+
+ _, err = db.NamedExec(fmt.Sprintf(`INSERT INTO %s (environment_id, %s, %s) VALUES (:env, :id, :time)`,
+ stmt.Table, stmt.PK, stmt.Column), values)
+ require.NoError(t, err)
+ }
+
+ waitForDumpDoneSignal(t, r, 20*time.Second, 100*time.Millisecond)
+ config, err := yaml.Marshal(struct {
+ Retention retention `yaml:"retention"`
+ }{reten})
+ assert.NoError(t, err)
+ it.IcingaDbInstanceT(t, r, rdb, services.WithIcingaDbConfig(string(config)))
+
+ eventually.Assert(t, func(t require.TestingT) {
+ for category, stmt := range retentionStatements {
+ retentionDays := daysForCategory(category)
+ threshold := time.Now().AddDate(0, 0, -retentionDays)
+ thresholdMilli := threshold.UnixMilli()
+
+ var rowsLeft int
+ err := db.QueryRow(
+ db.Rebind(fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE environment_id = ? AND %s < ?`, stmt.Table, stmt.Column)),
+ envId,
+ thresholdMilli,
+ ).Scan(&rowsLeft)
+ assert.NoError(t, err)
+
+ var rowsSpared int
+ err = db.QueryRow(
+ db.Rebind(fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE environment_id = ? AND %s >= ?`, stmt.Table, stmt.Column)),
+ envId,
+ thresholdMilli,
+ ).Scan(&rowsSpared)
+ assert.NoError(t, err)
+
+ if retentionDays == 0 {
+ // No cleanup.
+ assert.Equal(t, rowsToDelete+rowsToSpare, rowsLeft+rowsSpared, "all rows should still be there for %s", category)
+ } else {
+ assert.Equal(t, 0, rowsLeft, "rows left in retention period for %s", category)
+ assert.Equal(t, rowsToSpare, rowsSpared, "rows spared for %s", category)
+ }
+
+ var rowsSparedOtherEnv int
+ err = db.QueryRow(
+ db.Rebind(fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE environment_id <> ?`, stmt.Table)),
+ envId,
+ ).Scan(&rowsSparedOtherEnv)
+ assert.NoError(t, err)
+
+ assert.Equal(t, rowsInOtherEnv, rowsSparedOtherEnv, "should not delete rows in other environment for %s", category)
+ }
+ }, time.Minute, time.Second)
+}
+
+type cleanupStmt struct {
+ Table string
+ PK string
+ Column string
+}
+
+type retention struct {
+ HistoryDays int `yaml:"history-days"`
+ SlaDays int `yaml:"sla-days"`
+ Options map[string]int `yaml:"options"`
+}
+
+var retentionStatements = map[string]cleanupStmt{
+ "acknowledgement": {
+ Table: "acknowledgement_history",
+ PK: "id",
+ Column: "clear_time",
+ },
+ "comment": {
+ Table: "comment_history",
+ PK: "comment_id",
+ Column: "remove_time",
+ },
+ "downtime": {
+ Table: "downtime_history",
+ PK: "downtime_id",
+ Column: "end_time",
+ },
+ "flapping": {
+ Table: "flapping_history",
+ PK: "id",
+ Column: "end_time",
+ },
+ "notification": {
+ Table: "notification_history",
+ PK: "id",
+ Column: "send_time",
+ },
+ "state": {
+ Table: "state_history",
+ PK: "id",
+ Column: "event_time",
+ },
+ "sla_downtime": {
+ Table: "sla_history_downtime",
+ PK: "downtime_id",
+ Column: "downtime_end",
+ },
+ "sla_state": {
+ Table: "sla_history_state",
+ PK: "id",
+ Column: "event_time",
+ },
+}
+
+// dropNotNullColumns drops all columns with a NOT NULL constraint that are not
+// relevant to testing to simplify the insertion of test fixtures.
+func dropNotNullColumns(db *sqlx.DB, stmt cleanupStmt) error {
+ var schema string
+ switch db.DriverName() {
+ case "mysql":
+ schema = `SCHEMA()`
+ case "postgres":
+ schema = `CURRENT_SCHEMA()`
+ }
+
+ var cols []string
+ err := db.Select(&cols, db.Rebind(fmt.Sprintf(`
+SELECT column_name
+FROM information_schema.columns
+WHERE table_schema = %s AND table_name = ? AND column_name NOT IN (?, ?, ?) AND is_nullable = ?`,
+ schema)),
+ stmt.Table, "environment_id", stmt.PK, stmt.Column, "NO")
+ if err != nil {
+ return err
+ }
+ for i := range cols {
+ if _, err := db.Exec(fmt.Sprintf(`ALTER TABLE %s DROP COLUMN %s`, stmt.Table, cols[i])); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/tests/environment_test.go b/tests/environment_test.go
new file mode 100644
index 0000000..b4cedbc
--- /dev/null
+++ b/tests/environment_test.go
@@ -0,0 +1,120 @@
+package icingadb_test
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "github.com/icinga/icinga-testing/services"
+ "github.com/icinga/icinga-testing/utils/eventually"
+ "github.com/jmoiron/sqlx"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/sync/errgroup"
+ "sort"
+ "testing"
+ "time"
+)
+
+func TestMultipleEnvironments(t *testing.T) {
+ rdb := getDatabase(t)
+
+ numEnvs := 3
+ icinga2Instances := make([]services.Icinga2, numEnvs)
+
+ // Start numEnvs icinga2 instances with an icingadb instance each, all writing to the same SQL database.
+ var g errgroup.Group
+ for i := range icinga2Instances {
+ i := i
+
+ g.Go(func() error {
+ r := it.RedisServerT(t)
+ icinga2Instances[i] = it.Icinga2NodeT(t, "master")
+ icinga2Instances[i].EnableIcingaDb(r)
+ icinga2Instances[i].Reload()
+ it.IcingaDbInstanceT(t, r, rdb)
+ return nil
+ })
+ }
+ _ = g.Wait()
+
+ // Query the IcingaDB environment_id from each icinga2 instance.
+ var expectedEnvs []string
+ for _, instance := range icinga2Instances {
+ res, err := instance.ApiClient().GetJson("/v1/objects/icingadbs")
+ require.NoError(t, err, "requesting IcingaDB objects from API should succeed")
+ var objects ObjectsIcingaDBsResponse
+ err = json.NewDecoder(res.Body).Decode(&objects)
+ require.NoError(t, err, "requesting IcingaDB objects from API should succeed")
+ require.NotEmpty(t, objects.Results, "API response should return an IcingaDB object")
+ expectedEnvs = append(expectedEnvs, objects.Results[0].Attrs.EnvironmentId)
+ }
+
+ sort.Strings(expectedEnvs)
+ for i := 0; i < len(expectedEnvs)-1; i++ {
+ require.NotEqual(t, expectedEnvs[i], expectedEnvs[i+1], "all environment IDs should be distinct")
+ }
+
+ db, err := sqlx.Open(rdb.Driver(), rdb.DSN())
+ require.NoError(t, err, "SQL database open")
+ t.Cleanup(func() { _ = db.Close() })
+
+ t.Run("Table", func(t *testing.T) {
+ t.Parallel()
+
+ eventually.Assert(t, func(t require.TestingT) {
+ var query string
+ switch rdb.Driver() {
+ case "mysql":
+ query = "SELECT LOWER(HEX(id)), name FROM environment ORDER BY id"
+ case "postgres":
+ query = "SELECT LOWER(ENCODE(id, 'hex')), name FROM environment ORDER BY id"
+ default:
+ panic("unknown database driver")
+ }
+ rows, err := db.Query(query)
+ require.NoError(t, err, "SQL query")
+ defer rows.Close()
+
+ var gotEnvs []string
+ for rows.Next() {
+ var id, name string
+ err := rows.Scan(&id, &name)
+ require.NoError(t, err, "SQL scan")
+ require.Equal(t, id, name, "name should be initialized to the environment id")
+ gotEnvs = append(gotEnvs, id)
+ }
+
+ require.Equal(t, expectedEnvs, gotEnvs, "each environment should be present in the environments table")
+ }, 20*time.Second, 250*time.Millisecond)
+ })
+
+ t.Run("HA", func(t *testing.T) {
+ t.Parallel()
+
+ eventually.Assert(t, func(t require.TestingT) {
+ rows, err := db.Query("SELECT environment_id, COUNT(*) FROM icingadb_instance WHERE responsible = 'y' GROUP BY environment_id")
+ require.NoError(t, err, "SQL query")
+ defer rows.Close()
+
+ numRows := 0
+ for rows.Next() {
+ var env []byte
+ var count int
+ err := rows.Scan(&env, &count)
+ require.NoError(t, err, "SQL scan")
+
+ assert.LessOrEqualf(t, count, 1,
+ "environment %s must have at most one active instance", hex.EncodeToString(env))
+ numRows++
+ }
+ require.Equal(t, numEnvs, numRows, "each environment should have one active instance")
+ }, 20*time.Second, 250*time.Millisecond)
+ })
+}
+
+type ObjectsIcingaDBsResponse struct {
+ Results []struct {
+ Attrs struct {
+ EnvironmentId string `json:"environment_id"`
+ } `json:"attrs"`
+ } `json:"results"`
+}
diff --git a/tests/go.mod b/tests/go.mod
new file mode 100644
index 0000000..1e9f0e2
--- /dev/null
+++ b/tests/go.mod
@@ -0,0 +1,50 @@
+module github.com/icinga/icingadb/tests
+
+go 1.18
+
+require (
+ github.com/go-redis/redis/v8 v8.11.4
+ github.com/go-sql-driver/mysql v1.6.0
+ github.com/goccy/go-yaml v1.9.5
+ github.com/google/uuid v1.3.0
+ github.com/icinga/icinga-testing v0.0.0-20220516144008-9600081b7a69
+ github.com/jmoiron/sqlx v1.3.4
+ github.com/lib/pq v1.10.5
+ github.com/stretchr/testify v1.7.0
+ go.uber.org/zap v1.21.0
+ golang.org/x/exp v0.0.0-20221012112151-59b0eab1532e
+ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
+)
+
+require (
+ github.com/Icinga/go-libs v0.0.0-20220420130327-ef58ad52edd8 // indirect
+ github.com/Microsoft/go-winio v0.5.0 // indirect
+ github.com/benbjohnson/clock v1.1.0 // indirect
+ github.com/cespare/xxhash/v2 v2.1.2 // indirect
+ github.com/containerd/containerd v1.5.6 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/docker/distribution v2.7.1+incompatible // indirect
+ github.com/docker/docker v20.10.8+incompatible // indirect
+ github.com/docker/go-connections v0.4.0 // indirect
+ github.com/docker/go-units v0.4.0 // indirect
+ github.com/fatih/color v1.10.0 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.2 // indirect
+ github.com/mattn/go-colorable v0.1.8 // indirect
+ github.com/mattn/go-isatty v0.0.12 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.0.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/sirupsen/logrus v1.8.1 // indirect
+ go.uber.org/atomic v1.9.0 // indirect
+ go.uber.org/multierr v1.7.0 // indirect
+ golang.org/x/net v0.0.0-20211020060615-d418f374d309 // indirect
+ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
+ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+ google.golang.org/genproto v0.0.0-20211027162914-98a5263abeca // indirect
+ google.golang.org/grpc v1.41.0 // indirect
+ google.golang.org/protobuf v1.27.1 // indirect
+ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+)
diff --git a/tests/go.sum b/tests/go.sum
new file mode 100644
index 0000000..675837e
--- /dev/null
+++ b/tests/go.sum
@@ -0,0 +1,1085 @@
+bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
+github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
+github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
+github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
+github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
+github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
+github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Icinga/go-libs v0.0.0-20220420130327-ef58ad52edd8 h1:hG4Y/LPERK9i+P8/jnYlq9PeDd9deIkwEWOIimDU3uk=
+github.com/Icinga/go-libs v0.0.0-20220420130327-ef58ad52edd8/go.mod h1:xlgU55MKs/vIg1fMlAEBSrslahYayZNwjXvf3w1dvyA=
+github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
+github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU=
+github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
+github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
+github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ=
+github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
+github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
+github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00=
+github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600=
+github.com/Microsoft/hcsshim v0.8.18/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
+github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
+github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
+github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
+github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
+github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
+github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
+github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
+github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
+github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
+github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
+github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
+github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
+github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
+github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
+github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
+github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
+github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
+github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
+github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
+github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
+github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
+github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E=
+github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=
+github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=
+github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI=
+github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
+github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
+github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
+github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
+github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
+github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU=
+github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
+github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
+github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
+github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ=
+github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU=
+github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=
+github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
+github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=
+github.com/containerd/containerd v1.5.5/go.mod h1:oSTh0QpT1w6jYcGmbiSbxv9OSQYaa88mPyWIuU79zyo=
+github.com/containerd/containerd v1.5.6 h1:yi692sMr9kyyaps9dyodk3vVOTNM9fIPvlZp4UnyT4U=
+github.com/containerd/containerd v1.5.6/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
+github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
+github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=
+github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
+github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=
+github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
+github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
+github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
+github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
+github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=
+github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=
+github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU=
+github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk=
+github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
+github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
+github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=
+github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=
+github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=
+github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0=
+github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA=
+github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow=
+github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms=
+github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c=
+github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=
+github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=
+github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
+github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
+github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8=
+github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
+github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
+github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
+github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk=
+github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
+github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s=
+github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw=
+github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y=
+github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
+github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
+github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
+github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM=
+github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8=
+github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc=
+github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4=
+github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
+github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
+github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
+github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
+github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
+github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
+github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
+github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
+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/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
+github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
+github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
+github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v20.10.8+incompatible h1:RVqD337BgQicVCzYrrlhLDWhq6OAD2PJDUg2LsEUvKM=
+github.com/docker/docker v20.10.8+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
+github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
+github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
+github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
+github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
+github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
+github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
+github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
+github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
+github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/go-redis/redis/v8 v8.11.3/go.mod h1:xNJ9xDG09FsIPwh3bWdk+0oDWHbtF9rPN0F/oD9XeKc=
+github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
+github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/goccy/go-yaml v1.9.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0=
+github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
+github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
+github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
+github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
+github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
+github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
+github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/icinga/icinga-testing v0.0.0-20220516144008-9600081b7a69 h1:M5KN3s3TuHpGPnP78h5cFogtQrywapFIaYfvohQHc7I=
+github.com/icinga/icinga-testing v0.0.0-20220516144008-9600081b7a69/go.mod h1:ZP0pyqhmrRwwQ6FpAfz7UZMgmH7i3vOjEOm9JcFwOw0=
+github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
+github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
+github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+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/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
+github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
+github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
+github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
+github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
+github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
+github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
+github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
+github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=
+github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
+github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
+github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
+github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
+github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
+github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
+github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0=
+github.com/opencontainers/runc v1.0.1/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
+github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
+github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
+github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=
+github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
+github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+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/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
+github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
+github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/vbauerster/mpb/v6 v6.0.4 h1:h6J5zM/2wimP5Hj00unQuV8qbo5EPcj6wbkCqgj7KcY=
+github.com/vbauerster/mpb/v6 v6.0.4/go.mod h1:a/+JT57gqh6Du0Ay5jSR+uBMfXGdlR7VQlGP52fJxLM=
+github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
+github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
+github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
+github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
+github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
+github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
+github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
+github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
+github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
+github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
+go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
+go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+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/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
+go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
+go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
+go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
+go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
+go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+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/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20221012112151-59b0eab1532e h1:/SJUJZl3kz7J5GzAx5lgaKvqKGd4OfzshwDMr6YJCC4=
+golang.org/x/exp v0.0.0-20221012112151-59b0eab1532e/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
+golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211020060615-d418f374d309 h1:A0lJIi+hcTR6aajJH4YqKWwohY4aW9RO7oRMcdv+HKI=
+golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
+golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210824181836-a4879c3d0e89/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20211027162914-98a5263abeca h1:+e+aQDO4/c9KaG8PXWHTc6/+Du6kz+BKcXCSnV4SSTE=
+google.golang.org/genproto v0.0.0-20211027162914-98a5263abeca/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E=
+google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+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.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
+gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
+gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
+k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
+k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=
+k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=
+k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
+k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
+k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=
+k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=
+k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=
+k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0=
+k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
+k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
+k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=
+k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=
+k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
+k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
+k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc=
+k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
+k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
+k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/tests/history_bench_test.go b/tests/history_bench_test.go
new file mode 100644
index 0000000..e1ea0d2
--- /dev/null
+++ b/tests/history_bench_test.go
@@ -0,0 +1,130 @@
+package icingadb_test
+
+import (
+ "context"
+ "fmt"
+ "github.com/go-redis/redis/v8"
+ "github.com/google/uuid"
+ "github.com/icinga/icinga-testing/utils"
+ "github.com/jmoiron/sqlx"
+ "github.com/stretchr/testify/require"
+ "strconv"
+ "testing"
+ "time"
+)
+
+func BenchmarkHistory(b *testing.B) {
+ for _, numComments := range []int64{100_000, 200_000} {
+ b.Run(fmt.Sprintf("%d-Comments", numComments), func(b *testing.B) {
+ b.StopTimer()
+
+ for i := 0; i < b.N; i++ {
+ benchmarkHistory(b, numComments)
+ }
+ })
+ }
+
+}
+
+func benchmarkHistory(b *testing.B, numComments int64) {
+ m := it.MysqlDatabase()
+ defer m.Cleanup()
+ m.ImportIcingaDbSchema()
+
+ r := it.RedisServer()
+ defer r.Cleanup()
+ n := it.Icinga2Node("master")
+ defer n.Cleanup()
+ n.EnableIcingaDb(r)
+ err := n.Reload()
+ require.NoError(b, err, "icinga2 should reload without error")
+
+ db, err := sqlx.Connect("mysql", m.DSN())
+ require.NoError(b, err, "connecting to mysql")
+ defer func() { _ = db.Close() }()
+
+ redisClient := r.Open()
+ defer func() { _ = redisClient.Close() }()
+
+ client := n.ApiClient()
+
+ hostname := utils.UniqueName(b, "host")
+ client.CreateHost(b, hostname, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "enable_active_checks": false,
+ "enable_passive_checks": true,
+ "check_command": "dummy",
+ },
+ })
+
+ baseTime := time.Now().Add(time.Duration(-numComments) * time.Second)
+ for i := int64(0); i < numComments; i++ {
+ redisClient.XAdd(context.Background(), &redis.XAddArgs{
+ Stream: "icinga:history:stream:comment",
+ Values: map[string]string{
+ "comment_id": utils.RandomString(32),
+ "environment_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
+ "host_id": "05d7e9c12104a1e8851a871d2f78e25b8c3d9eae",
+ "entry_time": strconv.FormatInt(baseTime.Add(time.Duration(i)*time.Second).UnixMilli(), 10),
+ "author": utils.RandomString(8),
+ "comment": utils.RandomString(8),
+ "entry_type": "1",
+ "is_persistent": "0",
+ "is_sticky": "0",
+ "event_id": uuid.New().String(),
+ "event_type": "comment_add",
+ "object_type": "service",
+ "service_id": "98fe4a1696c4804c75ff5c0e76f1e79ef855c634",
+ "endpoint_id": "05d7e9c12104a1e8851a871d2f78e25b8c3d9eae",
+ },
+ })
+ }
+
+ pendingCount := func() int64 {
+ result, err := redisClient.XInfoStream(context.Background(), "icinga:history:stream:comment").Result()
+ require.NoError(b, err, "XINFO should not fail")
+ return result.Length
+ }
+
+ writtenCount := func() int64 {
+ var count int64
+ err := db.Get(&count, "SELECT COUNT(*) FROM comment_history")
+ require.NoError(b, err, "SELECT COUNT(*) should not fail")
+ return count
+ }
+
+ lastPending := pendingCount()
+ b.Logf("current stream length: %d", lastPending)
+
+ b.StartTimer()
+ idb := it.IcingaDbInstance(r, m)
+ defer idb.Cleanup()
+
+ ticker := time.NewTicker(5 * time.Millisecond)
+ defer ticker.Stop()
+ logTicker := time.NewTicker(1 * time.Second)
+ defer logTicker.Stop()
+ timeout := time.NewTicker(5 * time.Minute)
+ defer timeout.Stop()
+
+loop:
+ for {
+ select {
+ case <-ticker.C:
+ if pendingCount() == 0 && writtenCount() >= numComments {
+ break loop
+ }
+ case <-logTicker.C:
+ if p := pendingCount(); p > 0 {
+ b.Logf("last second: %d, pending: %d", lastPending-p, p)
+ lastPending = p
+ } else {
+ logTicker.Stop()
+ }
+ case <-timeout.C:
+ b.Fatal("did not drain stream in time")
+ }
+ }
+
+ b.StopTimer()
+}
diff --git a/tests/history_test.go b/tests/history_test.go
new file mode 100644
index 0000000..e720f02
--- /dev/null
+++ b/tests/history_test.go
@@ -0,0 +1,835 @@
+package icingadb_test
+
+import (
+ "bytes"
+ "context"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "github.com/go-redis/redis/v8"
+ "github.com/icinga/icinga-testing/services"
+ "github.com/icinga/icinga-testing/utils"
+ "github.com/icinga/icinga-testing/utils/eventually"
+ "github.com/icinga/icinga-testing/utils/pki"
+ "github.com/jmoiron/sqlx"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+ "io/ioutil"
+ "math"
+ "net/http"
+ "sort"
+ "strconv"
+ "testing"
+ "text/template"
+ "time"
+)
+
+//go:embed history_test_zones.conf
+var historyZonesConfRaw string
+var historyZonesConfTemplate = template.Must(template.New("zones.conf").Parse(historyZonesConfRaw))
+
+func TestHistory(t *testing.T) {
+ t.Run("SingleNode", func(t *testing.T) {
+ testHistory(t, 1)
+ })
+
+ t.Run("HA", func(t *testing.T) {
+ testHistory(t, 2)
+ })
+}
+
+func testHistory(t *testing.T, numNodes int) {
+ rdb := getDatabase(t)
+
+ ca, err := pki.NewCA()
+ require.NoError(t, err, "generating a CA should succeed")
+
+ type Node struct {
+ Name string
+ Icinga2 services.Icinga2
+ IcingaClient *utils.Icinga2Client
+
+ // Redis server and client for the instance used by the Icinga DB process.
+ Redis services.RedisServer
+ RedisClient *redis.Client
+
+ // Second Redis server and client to verify the consistency of the history streams in a HA setup.
+ // There is no Icinga DB process reading from this Redis so history events are not removed there.
+ ConsistencyRedis services.RedisServer
+ ConsistencyRedisClient *redis.Client
+ }
+
+ nodes := make([]*Node, numNodes)
+
+ for i := range nodes {
+ name := fmt.Sprintf("master-%d", i)
+ redisServer := it.RedisServerT(t)
+ consistencyRedisServer := it.RedisServerT(t)
+ icinga := it.Icinga2NodeT(t, name)
+
+ nodes[i] = &Node{
+ Name: name,
+ Icinga2: icinga,
+ IcingaClient: icinga.ApiClient(),
+ Redis: redisServer,
+ RedisClient: redisServer.Open(),
+ ConsistencyRedis: consistencyRedisServer,
+ ConsistencyRedisClient: consistencyRedisServer.Open(),
+ }
+ }
+
+ zonesConf := bytes.NewBuffer(nil)
+ err = historyZonesConfTemplate.Execute(zonesConf, nodes)
+ require.NoError(t, err, "failed to render zones.conf")
+
+ for _, n := range nodes {
+ cert, err := ca.NewCertificate(n.Name)
+ require.NoError(t, err, "generating cert for %q should succeed", n.Name)
+ n.Icinga2.WriteConfig("etc/icinga2/zones.conf", zonesConf.Bytes())
+ n.Icinga2.WriteConfig("etc/icinga2/features-available/api.conf", []byte(`
+ object ApiListener "api" {
+ accept_config = true
+ accept_commands = true
+ }
+ `))
+ n.Icinga2.WriteConfig("var/lib/icinga2/certs/ca.crt", ca.CertificateToPem())
+ n.Icinga2.WriteConfig("var/lib/icinga2/certs/"+n.Name+".crt", cert.CertificateToPem())
+ n.Icinga2.WriteConfig("var/lib/icinga2/certs/"+n.Name+".key", cert.KeyToPem())
+ n.Icinga2.EnableIcingaDb(n.Redis)
+ n.Icinga2.EnableIcingaDb(n.ConsistencyRedis)
+ err = n.Icinga2.Reload()
+ require.NoError(t, err, "icinga2 should reload without error")
+ it.IcingaDbInstanceT(t, n.Redis, rdb)
+
+ {
+ n := n
+ t.Cleanup(func() {
+ _ = n.RedisClient.Close()
+ _ = n.ConsistencyRedisClient.Close()
+ })
+ }
+ }
+
+ testConsistency := func(t *testing.T, stream string) {
+ if numNodes > 1 {
+ t.Run("Consistency", func(t *testing.T) {
+ var clients []*redis.Client
+ for _, node := range nodes {
+ clients = append(clients, node.ConsistencyRedisClient)
+ }
+ assertStreamConsistency(t, clients, stream)
+ })
+ }
+ }
+
+ eventually.Require(t, func(t require.TestingT) {
+ for i, ni := range nodes {
+ for j, nj := range nodes {
+ if i != j {
+ response, err := ni.IcingaClient.GetJson("/v1/objects/endpoints/" + nj.Name)
+ require.NoErrorf(t, err, "fetching endpoint %q from %q should not fail", nj.Name, ni.Name)
+ require.Equalf(t, 200, response.StatusCode, "fetching endpoint %q from %q should not fail", nj.Name, ni.Name)
+
+ var endpoints ObjectsEndpointsResponse
+ err = json.NewDecoder(response.Body).Decode(&endpoints)
+ require.NoErrorf(t, err, "parsing response from %q for endpoint %q should not fail", ni.Name, nj.Name)
+ require.NotEmptyf(t, endpoints.Results, "response from %q for endpoint %q should contain a result", ni.Name, nj.Name)
+
+ assert.Truef(t, endpoints.Results[0].Attrs.Connected, "endpoint %q should be connected to %q", nj.Name, ni.Name)
+ }
+ }
+ }
+ }, 15*time.Second, 200*time.Millisecond)
+
+ db, err := sqlx.Connect(rdb.Driver(), rdb.DSN())
+ require.NoError(t, err, "connecting to mysql")
+ t.Cleanup(func() { _ = db.Close() })
+
+ client := nodes[0].IcingaClient
+
+ t.Run("Acknowledgement", func(t *testing.T) {
+ const stream = "icinga:history:stream:acknowledgement"
+
+ hostname := utils.UniqueName(t, "host")
+ client.CreateHost(t, hostname, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "enable_active_checks": false,
+ "enable_passive_checks": true,
+ "check_command": "dummy",
+ "max_check_attempts": 1,
+ },
+ })
+
+ processCheckResult(t, client, hostname, 1)
+
+ author := utils.RandomString(8)
+ comment := utils.RandomString(8)
+ req, err := json.Marshal(ActionsAcknowledgeProblemRequest{
+ Type: "Host",
+ Filter: fmt.Sprintf(`host.name==%q`, hostname),
+ Author: author,
+ Comment: comment,
+ })
+ ackTime := time.Now()
+ require.NoError(t, err, "marshal request")
+ response, err := client.PostJson("/v1/actions/acknowledge-problem", bytes.NewBuffer(req))
+ require.NoError(t, err, "acknowledge-problem")
+ require.Equal(t, 200, response.StatusCode, "acknowledge-problem")
+
+ var ackResponse ActionsAcknowledgeProblemResponse
+ err = json.NewDecoder(response.Body).Decode(&ackResponse)
+ require.NoError(t, err, "decode acknowledge-problem response")
+ require.Equal(t, 1, len(ackResponse.Results), "acknowledge-problem should return 1 result")
+ require.Equal(t, http.StatusOK, ackResponse.Results[0].Code, "acknowledge-problem result should have OK status")
+
+ for _, n := range nodes {
+ assertEventuallyDrained(t, n.RedisClient, stream)
+ }
+
+ eventually.Assert(t, func(t require.TestingT) {
+ type Row struct {
+ Author string `db:"author"`
+ Comment string `db:"comment"`
+ }
+
+ var rows []Row
+ err = db.Select(&rows, db.Rebind("SELECT a.author, a.comment FROM history h"+
+ " JOIN host ON host.id = h.host_id"+
+ " JOIN acknowledgement_history a ON a.id = h.acknowledgement_history_id"+
+ " WHERE host.name = ? AND ? < h.event_time AND h.event_time < ?"),
+ hostname, ackTime.Add(-time.Second).UnixMilli(), ackTime.Add(time.Second).UnixMilli())
+ require.NoError(t, err, "select acknowledgement_history")
+
+ require.Equal(t, 1, len(rows), "there should be exactly one acknowledgement history entry")
+ assert.Equal(t, author, rows[0].Author, "acknowledgement author should match")
+ assert.Equal(t, comment, rows[0].Comment, "acknowledgement comment should match")
+ }, 5*time.Second, 200*time.Millisecond)
+
+ testConsistency(t, stream)
+ })
+
+ t.Run("Comment", func(t *testing.T) {
+ const stream = "icinga:history:stream:comment"
+
+ type HistoryEvent struct {
+ Type string `db:"event_type"`
+ Author string `db:"author"`
+ Comment string `db:"comment"`
+ RemovedBy *string `db:"removed_by"`
+ }
+
+ hostname := utils.UniqueName(t, "host")
+ client.CreateHost(t, hostname, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "enable_active_checks": false,
+ "enable_passive_checks": true,
+ "check_command": "dummy",
+ },
+ })
+
+ author := utils.RandomString(8)
+ comment := utils.RandomString(8)
+ req, err := json.Marshal(ActionsAddCommentRequest{
+ Type: "Host",
+ Filter: fmt.Sprintf(`host.name==%q`, hostname),
+ Author: author,
+ Comment: comment,
+ })
+ require.NoError(t, err, "marshal request")
+ response, err := client.PostJson("/v1/actions/add-comment", bytes.NewBuffer(req))
+ require.NoError(t, err, "add-comment")
+ require.Equal(t, 200, response.StatusCode, "add-comment")
+
+ var addResponse ActionsAddCommentResponse
+ err = json.NewDecoder(response.Body).Decode(&addResponse)
+ require.NoError(t, err, "decode add-comment response")
+ require.Equal(t, 1, len(addResponse.Results), "add-comment should return 1 result")
+ require.Equal(t, http.StatusOK, addResponse.Results[0].Code, "add-comment result should have OK status")
+ commentName := addResponse.Results[0].Name
+
+ // Ensure that downtime events have distinct timestamps in millisecond resolution.
+ time.Sleep(10 * time.Millisecond)
+
+ removedBy := utils.RandomString(8)
+ req, err = json.Marshal(ActionsRemoveCommentRequest{
+ Comment: commentName,
+ Author: removedBy,
+ })
+ require.NoError(t, err, "marshal remove-comment request")
+ response, err = client.PostJson("/v1/actions/remove-comment", bytes.NewBuffer(req))
+ require.NoError(t, err, "remove-comment")
+ require.Equal(t, 200, response.StatusCode, "remove-comment")
+
+ expected := []HistoryEvent{
+ {Type: "comment_add", Author: author, Comment: comment, RemovedBy: &removedBy},
+ {Type: "comment_remove", Author: author, Comment: comment, RemovedBy: &removedBy},
+ }
+
+ if !testing.Short() {
+ // Ensure that downtime events have distinct timestamps in millisecond resolution.
+ time.Sleep(10 * time.Millisecond)
+
+ expireAuthor := utils.RandomString(8)
+ expireComment := utils.RandomString(8)
+ expireDelay := time.Second
+ req, err = json.Marshal(ActionsAddCommentRequest{
+ Type: "Host",
+ Filter: fmt.Sprintf(`host.name==%q`, hostname),
+ Author: expireAuthor,
+ Comment: expireComment,
+ Expiry: float64(time.Now().Add(expireDelay).UnixMilli()) / 1000,
+ })
+ require.NoError(t, err, "marshal request")
+ response, err = client.PostJson("/v1/actions/add-comment", bytes.NewBuffer(req))
+ require.NoError(t, err, "add-comment")
+ require.Equal(t, 200, response.StatusCode, "add-comment")
+
+ // Icinga only expires comments every 60 seconds, so wait this long after the expiry time.
+ time.Sleep(expireDelay + 60*time.Second)
+
+ expected = append(expected,
+ HistoryEvent{Type: "comment_add", Author: expireAuthor, Comment: expireComment},
+ HistoryEvent{Type: "comment_remove", Author: expireAuthor, Comment: expireComment},
+ )
+ }
+
+ for _, n := range nodes {
+ assertEventuallyDrained(t, n.RedisClient, stream)
+ }
+
+ eventually.Assert(t, func(t require.TestingT) {
+ var rows []HistoryEvent
+ err = db.Select(&rows, db.Rebind("SELECT h.event_type, c.author, c.comment, c.removed_by"+
+ " FROM history h"+
+ " JOIN comment_history c ON c.comment_id = h.comment_history_id"+
+ " JOIN host ON host.id = c.host_id WHERE host.name = ?"+
+ " ORDER BY h.event_time"), hostname)
+ require.NoError(t, err, "select comment_history")
+
+ assert.Equal(t, expected, rows, "comment history should match")
+ }, 5*time.Second, 200*time.Millisecond)
+
+ testConsistency(t, stream)
+
+ if testing.Short() {
+ t.Skip("skipped comment expiry test")
+ }
+ })
+
+ t.Run("Downtime", func(t *testing.T) {
+ const stream = "icinga:history:stream:downtime"
+
+ type HistoryEvent struct {
+ Event string `db:"event_type"`
+ Author string `db:"author"`
+ Comment string `db:"comment"`
+ Cancelled string `db:"has_been_cancelled"`
+ }
+
+ hostname := utils.UniqueName(t, "host")
+ client.CreateHost(t, hostname, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "enable_active_checks": false,
+ "enable_flapping": true,
+ "enable_passive_checks": true,
+ "check_command": "dummy",
+ },
+ })
+
+ downtimeStart := time.Now()
+ author := utils.RandomString(8)
+ comment := utils.RandomString(8)
+ req, err := json.Marshal(ActionsScheduleDowntimeRequest{
+ Type: "Host",
+ Filter: fmt.Sprintf(`host.name==%q`, hostname),
+ StartTime: downtimeStart.Unix(),
+ EndTime: downtimeStart.Add(time.Hour).Unix(),
+ Fixed: true,
+ Author: author,
+ Comment: comment,
+ })
+ require.NoError(t, err, "marshal request")
+ response, err := client.PostJson("/v1/actions/schedule-downtime", bytes.NewBuffer(req))
+ require.NoError(t, err, "schedule-downtime")
+ require.Equal(t, 200, response.StatusCode, "schedule-downtime")
+
+ var scheduleResponse ActionsScheduleDowntimeResponse
+ err = json.NewDecoder(response.Body).Decode(&scheduleResponse)
+ require.NoError(t, err, "decode schedule-downtime response")
+ require.Equal(t, 1, len(scheduleResponse.Results), "schedule-downtime should return 1 result")
+ require.Equal(t, http.StatusOK, scheduleResponse.Results[0].Code, "schedule-downtime result should have OK status")
+ downtimeName := scheduleResponse.Results[0].Name
+
+ // Ensure that downtime events have distinct timestamps in millisecond resolution.
+ time.Sleep(10 * time.Millisecond)
+
+ req, err = json.Marshal(ActionsRemoveDowntimeRequest{
+ Downtime: downtimeName,
+ Author: utils.RandomString(8),
+ })
+ require.NoError(t, err, "marshal remove-downtime request")
+ response, err = client.PostJson("/v1/actions/remove-downtime", bytes.NewBuffer(req))
+ require.NoError(t, err, "remove-downtime")
+ require.Equal(t, 200, response.StatusCode, "remove-downtime")
+ downtimeEnd := time.Now()
+
+ expected := []HistoryEvent{
+ {Event: "downtime_start", Author: author, Comment: comment, Cancelled: "y"},
+ {Event: "downtime_end", Author: author, Comment: comment, Cancelled: "y"},
+ }
+
+ if !testing.Short() {
+ // Ensure that downtime events have distinct timestamps in second resolution (for start time).
+ time.Sleep(time.Second)
+
+ expireStart := time.Now()
+ expireAuthor := utils.RandomString(8)
+ expireComment := utils.RandomString(8)
+ req, err := json.Marshal(ActionsScheduleDowntimeRequest{
+ Type: "Host",
+ Filter: fmt.Sprintf(`host.name==%q`, hostname),
+ StartTime: expireStart.Unix(),
+ EndTime: expireStart.Add(time.Second).Unix(),
+ Fixed: true,
+ Author: expireAuthor,
+ Comment: expireComment,
+ })
+ require.NoError(t, err, "marshal request")
+ response, err := client.PostJson("/v1/actions/schedule-downtime", bytes.NewBuffer(req))
+ require.NoError(t, err, "schedule-downtime")
+ require.Equal(t, 200, response.StatusCode, "schedule-downtime")
+
+ var scheduleResponse ActionsScheduleDowntimeResponse
+ err = json.NewDecoder(response.Body).Decode(&scheduleResponse)
+ require.NoError(t, err, "decode schedule-downtime response")
+ require.Equal(t, 1, len(scheduleResponse.Results), "schedule-downtime should return 1 result")
+ require.Equal(t, http.StatusOK, scheduleResponse.Results[0].Code, "schedule-downtime result should have OK status")
+
+ // Icinga only expires downtimes every 60 seconds, so wait this long in addition to the downtime duration.
+ time.Sleep(60*time.Second + 1*time.Second)
+
+ expected = append(expected,
+ HistoryEvent{Event: "downtime_start", Author: expireAuthor, Comment: expireComment, Cancelled: "n"},
+ HistoryEvent{Event: "downtime_end", Author: expireAuthor, Comment: expireComment, Cancelled: "n"},
+ )
+
+ downtimeEnd = time.Now()
+ }
+
+ for _, n := range nodes {
+ assertEventuallyDrained(t, n.RedisClient, stream)
+ }
+
+ if !eventually.Assert(t, func(t require.TestingT) {
+ var got []HistoryEvent
+ err = db.Select(&got, db.Rebind("SELECT h.event_type, d.author, d.comment, d.has_been_cancelled"+
+ " FROM history h"+
+ " JOIN host ON host.id = h.host_id"+
+ // Joining downtime_history checks that events are written to it.
+ " JOIN downtime_history d ON d.downtime_id = h.downtime_history_id"+
+ " WHERE host.name = ? AND ? < h.event_time AND h.event_time < ?"+
+ " ORDER BY h.event_time"),
+ hostname, downtimeStart.Add(-time.Second).UnixMilli(), downtimeEnd.Add(time.Second).UnixMilli())
+ require.NoError(t, err, "select downtime_history")
+
+ assert.Equal(t, expected, got, "downtime history should match expected result")
+ }, 5*time.Second, 200*time.Millisecond) {
+ t.Logf("\n%s", utils.MustT(t).String(utils.PrettySelect(db,
+ "SELECT h.event_time, h.event_type FROM history h"+
+ " JOIN host ON host.id = h.host_id"+
+ " LEFT JOIN downtime_history d ON d.downtime_id = h.downtime_history_id"+
+ " WHERE host.name = ?"+
+ " ORDER BY h.event_time", hostname)))
+ }
+
+ testConsistency(t, stream)
+
+ if testing.Short() {
+ t.Skip("skipped expiring downtime")
+ }
+ })
+
+ t.Run("Flapping", func(t *testing.T) {
+ const stream = "icinga:history:stream:flapping"
+
+ hostname := utils.UniqueName(t, "host")
+ client.CreateHost(t, hostname, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "enable_active_checks": false,
+ "enable_flapping": true,
+ "enable_passive_checks": true,
+ "check_command": "dummy",
+ },
+ })
+
+ timeBefore := time.Now()
+ for i := 0; i < 10; i++ {
+ processCheckResult(t, client, hostname, 0)
+ processCheckResult(t, client, hostname, 1)
+ }
+ for i := 0; i < 20; i++ {
+ processCheckResult(t, client, hostname, 0)
+ }
+ timeAfter := time.Now()
+
+ for _, n := range nodes {
+ assertEventuallyDrained(t, n.RedisClient, stream)
+ }
+
+ eventually.Assert(t, func(t require.TestingT) {
+ var rows []string
+ err = db.Select(&rows, db.Rebind("SELECT h.event_type FROM history h"+
+ " JOIN host ON host.id = h.host_id"+
+ // Joining flapping_history checks that events are written to it.
+ " JOIN flapping_history f ON f.id = h.flapping_history_id"+
+ " WHERE host.name = ? AND ? < h.event_time AND h.event_time < ?"+
+ " ORDER BY h.event_time"),
+ hostname, timeBefore.Add(-time.Second).UnixMilli(), timeAfter.Add(time.Second).UnixMilli())
+ require.NoError(t, err, "select flapping_history")
+
+ require.Equal(t, []string{"flapping_start", "flapping_end"}, rows,
+ "flapping history should match expected result")
+ }, 5*time.Second, 200*time.Millisecond)
+
+ testConsistency(t, stream)
+ })
+
+ t.Run("Notification", func(t *testing.T) {
+ const stream = "icinga:history:stream:notification"
+
+ hostname := utils.UniqueName(t, "host")
+ client.CreateHost(t, hostname, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "enable_active_checks": false,
+ "enable_flapping": true,
+ "enable_passive_checks": true,
+ "check_command": "dummy",
+ "max_check_attempts": 1,
+ },
+ })
+
+ users := make([]string, 5)
+ for i := range users {
+ users[i] = utils.UniqueName(t, "user")
+ client.CreateObject(t, "users", users[i], nil)
+ }
+
+ // Sort users so that the SQL query can use ORDER BY and the resulting slices can just be compared for equality.
+ sort.Slice(users, func(i, j int) bool { return users[i] < users[j] })
+
+ command := utils.UniqueName(t, "notificationcommand")
+ client.CreateObject(t, "notificationcommands", command, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "command": []string{"true"},
+ },
+ })
+
+ notification := utils.UniqueName(t, "notification")
+ client.CreateObject(t, "notifications", hostname+"!"+notification, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "users": users,
+ "command": command,
+ },
+ })
+
+ type Notification struct {
+ Type string `db:"type"`
+ User string `db:"username"`
+ }
+
+ var expected []Notification
+
+ timeBefore := time.Now()
+ processCheckResult(t, client, hostname, 1)
+ for _, u := range users {
+ expected = append(expected, Notification{Type: "problem", User: u})
+ }
+ processCheckResult(t, client, hostname, 0)
+ for _, u := range users {
+ expected = append(expected, Notification{Type: "recovery", User: u})
+ }
+ timeAfter := time.Now()
+
+ for _, n := range nodes {
+ assertEventuallyDrained(t, n.RedisClient, stream)
+ }
+
+ eventually.Assert(t, func(t require.TestingT) {
+ var rows []Notification
+ err = db.Select(&rows, db.Rebind("SELECT n.type, COALESCE(u.name, '') AS username FROM history h"+
+ " JOIN host ON host.id = h.host_id"+
+ " JOIN notification_history n ON n.id = h.notification_history_id"+
+ " LEFT JOIN user_notification_history un ON un.notification_history_id = n.id"+
+ ` LEFT JOIN "user" u ON u.id = un.user_id`+
+ " WHERE host.name = ? AND ? < h.event_time AND h.event_time < ?"+
+ " ORDER BY h.event_time, username"),
+ hostname, timeBefore.Add(-time.Second).UnixMilli(), timeAfter.Add(time.Second).UnixMilli())
+ require.NoError(t, err, "select notification_history")
+
+ require.Equal(t, expected, rows, "notification history should match expected result")
+ }, 5*time.Second, 200*time.Millisecond)
+
+ testConsistency(t, stream)
+ })
+
+ t.Run("State", func(t *testing.T) {
+ const stream = "icinga:history:stream:state"
+
+ hostname := utils.UniqueName(t, "host")
+ client.CreateHost(t, hostname, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "enable_active_checks": false,
+ "enable_passive_checks": true,
+ "check_command": "dummy",
+ "max_check_attempts": 2,
+ },
+ })
+
+ type State struct {
+ Type string `db:"state_type"`
+ Soft int `db:"soft_state"`
+ Hard int `db:"hard_state"`
+ }
+
+ var expected []State
+
+ timeBefore := time.Now()
+ processCheckResult(t, client, hostname, 0) // UNKNOWN -> UP (hard)
+ expected = append(expected, State{Type: "hard", Soft: 0, Hard: 0})
+ processCheckResult(t, client, hostname, 1) // -> DOWN (soft)
+ expected = append(expected, State{Type: "soft", Soft: 1, Hard: 0})
+ processCheckResult(t, client, hostname, 1) // -> DOWN (hard)
+ expected = append(expected, State{Type: "hard", Soft: 1, Hard: 1})
+ processCheckResult(t, client, hostname, 1) // -> DOWN
+ processCheckResult(t, client, hostname, 0) // -> UP (hard)
+ expected = append(expected, State{Type: "hard", Soft: 0, Hard: 0})
+ processCheckResult(t, client, hostname, 1) // -> DOWN (soft)
+ expected = append(expected, State{Type: "soft", Soft: 1, Hard: 0})
+ processCheckResult(t, client, hostname, 0) // -> UP (hard)
+ expected = append(expected, State{Type: "hard", Soft: 0, Hard: 0})
+ processCheckResult(t, client, hostname, 0) // -> UP
+ processCheckResult(t, client, hostname, 1) // -> down (soft)
+ expected = append(expected, State{Type: "soft", Soft: 1, Hard: 0})
+ processCheckResult(t, client, hostname, 1) // -> DOWN (hard)
+ expected = append(expected, State{Type: "hard", Soft: 1, Hard: 1})
+ processCheckResult(t, client, hostname, 0) // -> UP (hard)
+ expected = append(expected, State{Type: "hard", Soft: 0, Hard: 0})
+ timeAfter := time.Now()
+
+ for _, n := range nodes {
+ assertEventuallyDrained(t, n.RedisClient, stream)
+ }
+
+ eventually.Assert(t, func(t require.TestingT) {
+
+ var rows []State
+ err = db.Select(&rows, db.Rebind("SELECT s.state_type, s.soft_state, s.hard_state FROM history h"+
+ " JOIN host ON host.id = h.host_id JOIN state_history s ON s.id = h.state_history_id"+
+ " WHERE host.name = ? AND ? < h.event_time AND h.event_time < ?"+
+ " ORDER BY h.event_time"),
+ hostname, timeBefore.Add(-time.Second).UnixMilli(), timeAfter.Add(time.Second).UnixMilli())
+ require.NoError(t, err, "select state_history")
+
+ require.Equal(t, expected, rows, "state history does not match expected result")
+ }, 5*time.Second, 200*time.Millisecond)
+
+ testConsistency(t, stream)
+ })
+}
+
+func assertEventuallyDrained(t testing.TB, redis *redis.Client, stream string) {
+ eventually.Assert(t, func(t require.TestingT) {
+ result, err := redis.XRange(context.Background(), stream, "-", "+").Result()
+ require.NoError(t, err, "reading %s should not fail", stream)
+ assert.Empty(t, result, "%s should eventually be drained", stream)
+ }, 5*time.Second, 10*time.Millisecond)
+}
+
+func assertStreamConsistency(t testing.TB, clients []*redis.Client, stream string) {
+ messages := make([][]map[string]interface{}, len(clients))
+
+ for i, c := range clients {
+ xmessages, err := c.XRange(context.Background(), stream, "-", "+").Result()
+ require.NoError(t, err, "reading %s should not fail", stream)
+ assert.NotEmpty(t, xmessages, "%s should not be empty on the Redis server %d", stream, i)
+
+ // Convert []XMessage into a slice of just the values. The IDs are generated by the Redis server based
+ // on the time the entry was written to the stream, so these IDs are expected to differ.
+ ms := make([]map[string]interface{}, 0, len(xmessages))
+ for _, xmessage := range xmessages {
+ values := xmessage.Values
+
+ // Delete endpoint_id as this is supposed to differ between both history streams as each endpoint
+ // writes its own ID into the stream.
+ delete(values, "endpoint_id")
+
+ // users_notified_ids represents a set, so order does not matter, sort for the comparison later.
+ if idsJson, ok := values["users_notified_ids"]; ok {
+ var ids []string
+ err := json.Unmarshal([]byte(idsJson.(string)), &ids)
+ require.NoError(t, err, "if users_notified_ids is present, it must be a JSON list of strings")
+ sort.Strings(ids)
+ values["users_notified_ids"] = ids
+ }
+
+ ms = append(ms, values)
+ }
+ sort.Slice(ms, func(i, j int) bool {
+ eventTime := func(v map[string]interface{}) uint64 {
+ s, ok := v["event_time"].(string)
+ if !ok {
+ return 0
+ }
+ u, err := strconv.ParseUint(s, 10, 64)
+ if err != nil {
+ return 0
+ }
+ return u
+ }
+
+ sortKey := func(v map[string]interface{}) string {
+ return fmt.Sprintf("%020d-%s", eventTime(v), v["event_id"])
+ }
+
+ return sortKey(ms[i]) < sortKey(ms[j])
+ })
+ messages[i] = ms
+ }
+
+ for i := 0; i < len(messages)-1; i++ {
+ assert.Equal(t, messages[i], messages[i+1], "%s should be equal on both Redis servers", stream)
+ }
+}
+
+func processCheckResult(t *testing.T, client *utils.Icinga2Client, hostname string, status int) time.Time {
+ // Ensure that check results have distinct timestamps in millisecond resolution.
+ time.Sleep(10 * time.Millisecond)
+
+ output := utils.RandomString(8)
+ reqBody, err := json.Marshal(ActionsProcessCheckResultRequest{
+ Type: "Host",
+ Filter: fmt.Sprintf(`host.name==%q`, hostname),
+ ExitStatus: status,
+ PluginOutput: output,
+ })
+ require.NoError(t, err, "marshal request")
+ response, err := client.PostJson("/v1/actions/process-check-result", bytes.NewBuffer(reqBody))
+ require.NoError(t, err, "process-check-result")
+ if !assert.Equal(t, 200, response.StatusCode, "process-check-result") {
+ body, err := ioutil.ReadAll(response.Body)
+ require.NoError(t, err, "reading process-check-result response")
+ it.Logger(t).Error("process-check-result", zap.ByteString("api-response", body))
+ t.FailNow()
+ }
+
+ response, err = client.GetJson("/v1/objects/hosts/" + hostname)
+ require.NoError(t, err, "get host: request")
+ require.Equal(t, 200, response.StatusCode, "get host: request")
+
+ var hosts ObjectsHostsResponse
+ err = json.NewDecoder(response.Body).Decode(&hosts)
+ require.NoError(t, err, "get host: parse response")
+
+ require.Equal(t, 1, len(hosts.Results), "there must be one host in the response")
+ host := hosts.Results[0]
+ require.Equal(t, output, host.Attrs.LastCheckResult.Output,
+ "last check result should be visible in host object")
+ require.Equal(t, status, host.Attrs.State, "state should match check result")
+
+ sec, nsec := math.Modf(host.Attrs.LastCheckResult.ExecutionEnd)
+ return time.Unix(int64(sec), int64(nsec*1e9))
+}
+
+type ActionsAcknowledgeProblemRequest struct {
+ Type string `json:"type"`
+ Filter string `json:"filter"`
+ Author string `json:"author"`
+ Comment string `json:"comment"`
+}
+
+type ActionsAcknowledgeProblemResponse struct {
+ Results []struct {
+ Code int `json:"code"`
+ Status string `json:"status"`
+ } `json:"results"`
+}
+
+type ActionsAddCommentRequest struct {
+ Type string `json:"type"`
+ Filter string `json:"filter"`
+ Author string `json:"author"`
+ Comment string `json:"comment"`
+ Expiry float64 `json:"expiry"`
+}
+
+type ActionsAddCommentResponse struct {
+ Results []struct {
+ Code int `json:"code"`
+ LegacyId int `json:"legacy_id"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ } `json:"results"`
+}
+
+type ActionsRemoveCommentRequest struct {
+ Comment string `json:"comment"`
+ Author string `json:"author"`
+}
+
+type ActionsProcessCheckResultRequest struct {
+ Type string `json:"type"`
+ Filter string `json:"filter"`
+ ExitStatus int `json:"exit_status"`
+ PluginOutput string `json:"plugin_output"`
+}
+
+type ActionsRemoveDowntimeRequest struct {
+ Downtime string `json:"downtime"`
+ Author string `json:"author"`
+}
+
+type ActionsScheduleDowntimeRequest struct {
+ Type string `json:"type"`
+ Filter string `json:"filter"`
+ StartTime int64 `json:"start_time"`
+ EndTime int64 `json:"end_time"`
+ Fixed bool `json:"fixed"`
+ Duration float64 `json:"duration"`
+ Author string `json:"author"`
+ Comment string `json:"comment"`
+}
+
+type ActionsScheduleDowntimeResponse struct {
+ Results []struct {
+ Code int `json:"code"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ } `json:"results"`
+}
+
+type ObjectsHostsResponse struct {
+ Results []struct {
+ Attrs struct {
+ State int `json:"state"`
+ StateType int `json:"state_type"`
+ LastCheckResult struct {
+ ExecutionEnd float64 `json:"execution_end"`
+ ExitStatus int `json:"exit_status"`
+ Output string `json:"output"`
+ } `json:"last_check_result"`
+ LastHardState int `json:"last_hard_state"`
+ LastHardStateChange float64 `json:"last_hard_state_change"`
+ LastState int `json:"last_state"`
+ } `json:"attrs"`
+ } `json:"results"`
+}
+
+type ObjectsEndpointsResponse struct {
+ Results []struct {
+ Name string `json:"name"`
+ Attrs struct {
+ Connected bool `json:"connected"`
+ } `json:"attrs"`
+ } `json:"results"`
+}
diff --git a/tests/history_test_zones.conf b/tests/history_test_zones.conf
new file mode 100644
index 0000000..9d645a2
--- /dev/null
+++ b/tests/history_test_zones.conf
@@ -0,0 +1,22 @@
+{{range .}}
+object Endpoint "{{.Name}}" {
+ host = "{{.Icinga2.Host}}"
+ port = "{{.Icinga2.Port}}"
+}
+{{end}}
+
+object Zone "master" {
+ endpoints = [
+{{range .}}
+ "{{.Name}}",
+{{end}}
+ ]
+}
+
+object Zone "global-templates" {
+ global = true
+}
+
+object Zone "director-global" {
+ global = true
+}
diff --git a/tests/instance_test.go b/tests/instance_test.go
new file mode 100644
index 0000000..0c75f3e
--- /dev/null
+++ b/tests/instance_test.go
@@ -0,0 +1,8 @@
+package icingadb
+
+import "testing"
+
+func TestInstance(t *testing.T) {
+ // TODO(jb): perform some checks on the icingadb_instance table
+ t.Skip()
+}
diff --git a/tests/internal/utils/database.go b/tests/internal/utils/database.go
new file mode 100644
index 0000000..c32566b
--- /dev/null
+++ b/tests/internal/utils/database.go
@@ -0,0 +1,28 @@
+package utils
+
+import (
+ "fmt"
+ "github.com/icinga/icinga-testing"
+ "github.com/icinga/icinga-testing/services"
+ "os"
+ "strings"
+ "testing"
+)
+
+func GetDatabase(it *icingatesting.IT, t testing.TB) services.RelationalDatabase {
+ k := "ICINGADB_TESTS_DATABASE_TYPE"
+ v := strings.ToLower(os.Getenv(k))
+
+ var rdb services.RelationalDatabase
+
+ switch v {
+ case "mysql":
+ rdb = it.MysqlDatabaseT(t)
+ case "pgsql":
+ rdb = it.PostgresqlDatabaseT(t)
+ default:
+ panic(fmt.Sprintf(`unknown database in %s environment variable: %q (must be "mysql" or "pgsql")`, k, v))
+ }
+
+ return rdb
+}
diff --git a/tests/internal/utils/redis.go b/tests/internal/utils/redis.go
new file mode 100644
index 0000000..da3615d
--- /dev/null
+++ b/tests/internal/utils/redis.go
@@ -0,0 +1,37 @@
+package utils
+
+import (
+ "context"
+ "encoding/hex"
+ "encoding/json"
+ "github.com/go-redis/redis/v8"
+ "github.com/icinga/icinga-testing/services"
+ "github.com/stretchr/testify/require"
+ "testing"
+ "time"
+)
+
+func GetEnvironmentIdFromRedis(t *testing.T, r services.RedisServer) []byte {
+ conn := r.Open()
+ defer conn.Close()
+
+ heartbeat, err := conn.XRead(context.Background(), &redis.XReadArgs{
+ Streams: []string{"icinga:stats", "0"},
+ Count: 1,
+ Block: 10 * time.Second,
+ }).Result()
+ require.NoError(t, err, "reading from icinga:stats failed")
+ require.NotEmpty(t, heartbeat, "response contains no streams")
+ require.NotEmpty(t, heartbeat[0].Messages, "response contains no messages")
+ require.Contains(t, heartbeat[0].Messages[0].Values, "icingadb_environment",
+ "icinga:stats message misses icingadb_environment")
+
+ var envIdHex string
+ err = json.Unmarshal([]byte(heartbeat[0].Messages[0].Values["icingadb_environment"].(string)), &envIdHex)
+ require.NoError(t, err, "cannot parse environment ID as a JSON string")
+
+ envId, err := hex.DecodeString(envIdHex)
+ require.NoError(t, err, "environment ID is not a hex string")
+
+ return envId
+}
diff --git a/tests/internal/utils/slice.go b/tests/internal/utils/slice.go
new file mode 100644
index 0000000..85821f5
--- /dev/null
+++ b/tests/internal/utils/slice.go
@@ -0,0 +1,37 @@
+package utils
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// AnySliceToInterfaceSlice takes a slice of type []T for any T and returns a slice of type []interface{} containing
+// the same elements, somewhat like casting []T to []interface{}.
+func AnySliceToInterfaceSlice(in interface{}) []interface{} {
+ v := reflect.ValueOf(in)
+ if v.Kind() != reflect.Slice {
+ panic(fmt.Errorf("AnySliceToInterfaceSlice() called on %T instead of a slice type", in))
+ }
+
+ out := make([]interface{}, v.Len())
+ for i := 0; i < v.Len(); i++ {
+ out[i] = v.Index(i).Interface()
+ }
+ return out
+}
+
+func SliceSubsets(in ...string) [][]string {
+ result := make([][]string, 0, 1<<len(in))
+
+ for bitset := 0; bitset < (1 << len(in)); bitset++ {
+ var subset []string
+ for i := 0; i < len(in); i++ {
+ if bitset&(1<<i) != 0 {
+ subset = append(subset, in[i])
+ }
+ }
+ result = append(result, subset)
+ }
+
+ return result
+}
diff --git a/tests/internal/utils/slice_test.go b/tests/internal/utils/slice_test.go
new file mode 100644
index 0000000..c878226
--- /dev/null
+++ b/tests/internal/utils/slice_test.go
@@ -0,0 +1,31 @@
+package utils
+
+import (
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestSliceSubsets(t *testing.T) {
+ data := []string{
+ "bla",
+ "blub",
+ "derp",
+ }
+
+ result := SliceSubsets(data...)
+
+ expected := [][]string{
+ nil,
+ {"bla"},
+ {"blub"},
+ {"bla", "blub"},
+ {"derp"},
+ {"bla", "derp"},
+ {"blub", "derp"},
+ {"bla", "blub", "derp"},
+ }
+
+ require.Equal(t, expected, result)
+
+ t.Logf("%#v", result)
+}
diff --git a/tests/internal/value/notification_states.go b/tests/internal/value/notification_states.go
new file mode 100644
index 0000000..3c5479e
--- /dev/null
+++ b/tests/internal/value/notification_states.go
@@ -0,0 +1,37 @@
+package value
+
+import "fmt"
+
+type NotificationStates []string
+
+func (s NotificationStates) IcingaDbValue() interface{} {
+ v := uint(0)
+
+ for _, s := range s {
+ if bit, ok := notificationStateMap[s]; ok {
+ v |= bit
+ } else {
+ panic(fmt.Errorf("unknown notification state %q", s))
+ }
+ }
+
+ return v
+}
+
+func (s NotificationStates) Icinga2ConfigValue() string {
+ return ToIcinga2Config([]string(s))
+}
+
+func (s NotificationStates) Icinga2ApiValue() interface{} {
+ return ToIcinga2Api([]string(s))
+}
+
+// https://github.com/Icinga/icinga2/blob/a8f98cf72115d50152137bc924277b426f483a3f/lib/icinga/notification.hpp#L20-L32
+var notificationStateMap = map[string]uint{
+ "OK": 1,
+ "Warning": 2,
+ "Critical": 4,
+ "Unknown": 8,
+ "Up": 16,
+ "Down": 32,
+}
diff --git a/tests/internal/value/notification_types.go b/tests/internal/value/notification_types.go
new file mode 100644
index 0000000..f368ed5
--- /dev/null
+++ b/tests/internal/value/notification_types.go
@@ -0,0 +1,40 @@
+package value
+
+import "fmt"
+
+type NotificationTypes []string
+
+func (t NotificationTypes) IcingaDbValue() interface{} {
+ v := uint(0)
+
+ for _, s := range t {
+ if bit, ok := notificationTypeMap[s]; ok {
+ v |= bit
+ } else {
+ panic(fmt.Errorf("unknown notification type %q", s))
+ }
+ }
+
+ return v
+}
+
+func (t NotificationTypes) Icinga2ConfigValue() string {
+ return ToIcinga2Config([]string(t))
+}
+
+func (t NotificationTypes) Icinga2ApiValue() interface{} {
+ return ToIcinga2Api([]string(t))
+}
+
+// https://github.com/Icinga/icinga2/blob/a8f98cf72115d50152137bc924277b426f483a3f/lib/icinga/notification.hpp#L34-L50
+var notificationTypeMap = map[string]uint{
+ "DowntimeStart": 1,
+ "DowntimeEnd": 2,
+ "DowntimeRemoved": 4,
+ "Custom": 8,
+ "Acknowledgement": 16,
+ "Problem": 32,
+ "Recovery": 64,
+ "FlappingStart": 128,
+ "FlappingEnd": 256,
+}
diff --git a/tests/internal/value/value.go b/tests/internal/value/value.go
new file mode 100644
index 0000000..87027dd
--- /dev/null
+++ b/tests/internal/value/value.go
@@ -0,0 +1,103 @@
+package value
+
+import (
+ "fmt"
+ "reflect"
+)
+
+func ToIcinga2Config(value interface{}) string {
+ if value == nil {
+ return "null"
+ }
+
+ refVal := reflect.ValueOf(value)
+ switch refVal.Kind() {
+ case reflect.Slice:
+ vs := ""
+ for i := 0; i < refVal.Len(); i++ {
+ vs += ToIcinga2Config(refVal.Index(i).Interface()) + ","
+ }
+ return "[" + vs + "]"
+ case reflect.Map:
+ kvs := ""
+ iter := refVal.MapRange()
+ for iter.Next() {
+ kvs += ToIcinga2Config(iter.Key().Interface()) + "=" + ToIcinga2Config(iter.Value().Interface()) + ","
+ }
+ return "{" + kvs + "}"
+ }
+
+ switch v := value.(type) {
+ case interface{ Icinga2ConfigValue() string }:
+ return v.Icinga2ConfigValue()
+ case string:
+ // TODO(jb): probably not perfect quoting, but good enough for now
+ return fmt.Sprintf("%q", v)
+ case *string:
+ if v != nil {
+ return ToIcinga2Config(*v)
+ } else {
+ return "null"
+ }
+ case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
+ return fmt.Sprintf("%v", v)
+ default:
+ panic(fmt.Errorf("ToIcinga2Config called on unknown type %T", value))
+ }
+}
+
+func ToIcinga2Api(value interface{}) interface{} {
+ if value == nil {
+ return nil
+ }
+
+ refVal := reflect.ValueOf(value)
+ switch refVal.Kind() {
+ case reflect.Slice:
+ r := make([]interface{}, refVal.Len())
+ for i := range r {
+ r[i] = ToIcinga2Api(refVal.Index(i).Interface())
+ }
+ return r
+ case reflect.Map:
+ r := make(map[string]interface{})
+ iter := refVal.MapRange()
+ for iter.Next() {
+ // TODO: perform a better check than the type assertion
+ r[ToIcinga2Api(iter.Key().Interface()).(string)] = ToIcinga2Api(iter.Value().Interface())
+ }
+ return r
+ }
+
+ switch v := value.(type) {
+ case interface{ Icinga2ApiValue() interface{} }:
+ return v.Icinga2ApiValue()
+ case string, []string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
+ return v
+ case *string:
+ if v != nil {
+ return ToIcinga2Api(*v)
+ } else {
+ return nil
+ }
+ default:
+ panic(fmt.Errorf("ToIcinga2Api called on unknown type %T", value))
+ }
+}
+
+func ToIcingaDb(value interface{}) interface{} {
+ switch v := value.(type) {
+ case interface{ IcingaDbValue() interface{} }:
+ return v.IcingaDbValue()
+ case bool:
+ if v {
+ return "y"
+ } else {
+ return "n"
+ }
+ case string, *string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
+ return v
+ default:
+ panic(fmt.Errorf("ToIcinga2Api called on unknown type %T", value))
+ }
+}
diff --git a/tests/main_test.go b/tests/main_test.go
new file mode 100644
index 0000000..bccd947
--- /dev/null
+++ b/tests/main_test.go
@@ -0,0 +1,29 @@
+package icingadb_test
+
+import (
+ "github.com/icinga/icinga-testing"
+ "github.com/icinga/icinga-testing/services"
+ "github.com/icinga/icingadb/tests/internal/utils"
+ "testing"
+)
+
+var it *icingatesting.IT
+
+func TestMain(m *testing.M) {
+ it = icingatesting.NewIT()
+ defer it.Cleanup()
+
+ m.Run()
+}
+
+func getDatabase(t testing.TB) services.RelationalDatabase {
+ rdb := getEmptyDatabase(t)
+
+ rdb.ImportIcingaDbSchema()
+
+ return rdb
+}
+
+func getEmptyDatabase(t testing.TB) services.RelationalDatabase {
+ return utils.GetDatabase(it, t)
+}
diff --git a/tests/object_sync_test.conf b/tests/object_sync_test.conf
new file mode 100644
index 0000000..e4c6da6
--- /dev/null
+++ b/tests/object_sync_test.conf
@@ -0,0 +1,54 @@
+{{range .GenericPrefixes}}
+object CheckCommand "{{.}}-checkcommand" {
+ command = ["true"]
+}
+
+object EventCommand "{{.}}-eventcommand" {
+ command = ["true"]
+}
+
+object NotificationCommand "{{.}}-notificationcommand" {
+ command = ["true"]
+}
+
+object Endpoint "{{.}}-endpoint" {}
+
+object Zone "{{.}}-zone" {
+ parent = "master"
+ endpoints = ["{{.}}-endpoint"]
+}
+
+object Host "{{.}}-host" {
+ check_command = "hostalive"
+}
+
+object HostGroup "{{.}}-hostgroup" {}
+
+object Service "{{.}}-service" {
+ host_name = "{{.}}-host"
+ check_command = "dummy"
+}
+
+object ServiceGroup "{{.}}-servicegroup" {}
+
+object TimePeriod "{{.}}-timeperiod" {}
+
+object User "{{.}}-user" {}
+
+object UserGroup "{{.}}-usergroup" {}
+{{end}}
+
+{{range .NotificationUserGroups}}
+object UserGroup "{{.}}" {
+}
+{{end}}
+
+{{range $user, $groups := .NotificationUsers}}
+object User "{{$user}}" {
+ groups = [
+ {{range $group, $_ := $groups}}
+ "{{$group}}",
+ {{end}}
+ ]
+}
+{{end}}
diff --git a/tests/object_sync_test.go b/tests/object_sync_test.go
new file mode 100644
index 0000000..27d3c95
--- /dev/null
+++ b/tests/object_sync_test.go
@@ -0,0 +1,1415 @@
+package icingadb_test
+
+import (
+ "bytes"
+ "context"
+ _ "embed"
+ "fmt"
+ "github.com/go-redis/redis/v8"
+ "github.com/icinga/icinga-testing/services"
+ "github.com/icinga/icinga-testing/utils"
+ "github.com/icinga/icinga-testing/utils/eventually"
+ localutils "github.com/icinga/icingadb/tests/internal/utils"
+ "github.com/icinga/icingadb/tests/internal/value"
+ "github.com/jmoiron/sqlx"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+ "golang.org/x/exp/slices"
+ "io"
+ "reflect"
+ "sort"
+ "strings"
+ "testing"
+ "text/template"
+ "time"
+)
+
+//go:embed object_sync_test.conf
+var testSyncConfRaw string
+var testSyncConfTemplate = template.Must(template.New("testdata.conf").Parse(testSyncConfRaw))
+
+var usergroups = []string{
+ "testusergroup1",
+ "testusergroup2",
+ "testusergroup3",
+}
+
+// Map of users to a set of their groups
+var users = map[string]map[string]struct{}{
+ "testuser1": {"testusergroup1": {}, "testusergroup3": {}},
+ "testuser2": {"testusergroup2": {}},
+ "testuser3": {"testusergroup3": {}, "testusergroup1": {}},
+}
+
+func TestObjectSync(t *testing.T) {
+ logger := it.Logger(t)
+
+ type Data struct {
+ GenericPrefixes []string
+ Hosts []Host
+ Services []Service
+ Users []User
+ Notifications []Notification
+ NotificationUsers map[string]map[string]struct{}
+ NotificationUserGroups []string
+ }
+ data := &Data{
+ // Some name prefixes to loop over in the template to generate multiple instances of objects,
+ // for example default-host, some-host, and other-host.
+ GenericPrefixes: []string{"default", "some", "other"},
+
+ Hosts: makeTestSyncHosts(t),
+ Services: makeTestSyncServices(t),
+ Users: makeTestUsers(t),
+ Notifications: makeTestNotifications(t),
+ NotificationUsers: users,
+ NotificationUserGroups: usergroups,
+ }
+
+ r := it.RedisServerT(t)
+ rdb := getDatabase(t)
+ i := it.Icinga2NodeT(t, "master")
+ conf := bytes.NewBuffer(nil)
+ err := testSyncConfTemplate.Execute(conf, data)
+ require.NoError(t, err, "render icinga2 config")
+ for _, host := range data.Hosts {
+ err := writeIcinga2ConfigObject(conf, host)
+ require.NoError(t, err, "generate icinga2 host config")
+ }
+ for _, service := range data.Services {
+ err := writeIcinga2ConfigObject(conf, service)
+ require.NoError(t, err, "generate icinga2 service config")
+ }
+ for _, user := range data.Users {
+ err := writeIcinga2ConfigObject(conf, user)
+ require.NoError(t, err, "generate icinga2 user config")
+ }
+ for _, notification := range data.Notifications {
+ err := writeIcinga2ConfigObject(conf, notification)
+ require.NoError(t, err, "generate icinga2 notification config")
+ }
+ //logger.Sugar().Infof("config:\n\n%s\n\n", conf.String())
+ i.WriteConfig("etc/icinga2/conf.d/testdata.conf", conf.Bytes())
+ i.EnableIcingaDb(r)
+ i.Reload()
+
+ // Wait for Icinga 2 to signal a successful dump before starting
+ // Icinga DB to ensure that we actually test the initial sync.
+ logger.Debug("waiting for icinga2 dump done signal")
+ waitForDumpDoneSignal(t, r, 20*time.Second, 100*time.Millisecond)
+
+ // Only after that, start Icinga DB.
+ logger.Debug("starting icingadb")
+ it.IcingaDbInstanceT(t, r, rdb)
+
+ db, err := sqlx.Open(rdb.Driver(), rdb.DSN())
+ require.NoError(t, err, "connecting to SQL database shouldn't fail")
+ t.Cleanup(func() { _ = db.Close() })
+
+ t.Run("Host", func(t *testing.T) {
+ t.Parallel()
+
+ for _, host := range data.Hosts {
+ host := host
+ t.Run("Verify-"+host.VariantInfoString(), func(t *testing.T) {
+ t.Parallel()
+
+ eventually.Assert(t, func(t require.TestingT) {
+ verifyIcingaDbRow(t, db, host)
+ }, 20*time.Second, 1*time.Second)
+
+ if host.Vars != nil {
+ t.Run("CustomVar", func(t *testing.T) {
+ logger := it.Logger(t)
+ eventually.Assert(t, func(t require.TestingT) {
+ host.Vars.VerifyCustomVar(t, logger, db, host)
+ }, 20*time.Second, 1*time.Second)
+ })
+
+ t.Run("CustomVarFlat", func(t *testing.T) {
+ logger := it.Logger(t)
+ eventually.Assert(t, func(t require.TestingT) {
+ host.Vars.VerifyCustomVarFlat(t, logger, db, host)
+ }, 20*time.Second, 1*time.Second)
+ })
+ }
+ })
+
+ }
+ })
+
+ t.Run("Service", func(t *testing.T) {
+ t.Parallel()
+
+ for _, service := range data.Services {
+ service := service
+ t.Run("Verify-"+service.VariantInfoString(), func(t *testing.T) {
+ t.Parallel()
+
+ eventually.Assert(t, func(t require.TestingT) {
+ verifyIcingaDbRow(t, db, service)
+ }, 20*time.Second, 1*time.Second)
+
+ if service.Vars != nil {
+ t.Run("CustomVar", func(t *testing.T) {
+ logger := it.Logger(t)
+ eventually.Assert(t, func(t require.TestingT) {
+ service.Vars.VerifyCustomVar(t, logger, db, service)
+ }, 20*time.Second, 1*time.Second)
+ })
+
+ t.Run("CustomVarFlat", func(t *testing.T) {
+ logger := it.Logger(t)
+ eventually.Assert(t, func(t require.TestingT) {
+ service.Vars.VerifyCustomVarFlat(t, logger, db, service)
+ }, 20*time.Second, 1*time.Second)
+ })
+ }
+ })
+ }
+ })
+
+ t.Run("HostGroup", func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Run("Member", func(t *testing.T) { t.Parallel(); t.Skip() })
+ t.Run("CustomVar", func(t *testing.T) { t.Parallel(); t.Skip() })
+ })
+
+ t.Run("ServiceGroup", func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Run("Member", func(t *testing.T) { t.Parallel(); t.Skip() })
+ t.Run("CustomVar", func(t *testing.T) { t.Parallel(); t.Skip() })
+ })
+
+ t.Run("Endpoint", func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Skip()
+ })
+
+ for _, commandType := range []string{"CheckCommend", "EventCommand", "NotificationCommand"} {
+ commandType := commandType
+ t.Run(commandType, func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Run("Argument", func(t *testing.T) { t.Parallel(); t.Skip() })
+ t.Run("EnvVar", func(t *testing.T) { t.Parallel(); t.Skip() })
+ t.Run("CustomVar", func(t *testing.T) { t.Parallel(); t.Skip() })
+ })
+ }
+
+ t.Run("Comment", func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Skip()
+ })
+
+ t.Run("Downtime", func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Skip()
+ })
+
+ t.Run("Notification", func(t *testing.T) {
+ t.Parallel()
+ for _, notification := range data.Notifications {
+ notification := notification
+ t.Run("Verify-"+notification.VariantInfoString(), func(t *testing.T) {
+ t.Parallel()
+
+ eventually.Assert(t, func(t require.TestingT) {
+ notification.verify(t, db)
+ }, 20*time.Second, 1*time.Second)
+
+ if notification.Vars != nil {
+ t.Run("CustomVar", func(t *testing.T) {
+ logger := it.Logger(t)
+ eventually.Assert(t, func(t require.TestingT) {
+ notification.Vars.VerifyCustomVar(t, logger, db, notification)
+ }, 20*time.Second, 1*time.Second)
+ })
+
+ t.Run("CustomVarFlat", func(t *testing.T) {
+ logger := it.Logger(t)
+ eventually.Assert(t, func(t require.TestingT) {
+ notification.Vars.VerifyCustomVarFlat(t, logger, db, notification)
+ }, 20*time.Second, 1*time.Second)
+ })
+ }
+ })
+ }
+ })
+
+ t.Run("TimePeriod", func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Run("Range", func(t *testing.T) { t.Parallel(); t.Skip() })
+ t.Run("OverrideInclude", func(t *testing.T) { t.Parallel(); t.Skip() })
+ t.Run("OverrideExclude", func(t *testing.T) { t.Parallel(); t.Skip() })
+ t.Run("CustomVar", func(t *testing.T) { t.Parallel(); t.Skip() })
+ })
+
+ t.Run("CustomVar", func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Skip()
+ })
+
+ t.Run("CustomVarFlat", func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Skip()
+ })
+
+ t.Run("User", func(t *testing.T) {
+ t.Parallel()
+
+ for _, user := range data.Users {
+ user := user
+ t.Run("Verify-"+user.VariantInfoString(), func(t *testing.T) {
+ t.Parallel()
+
+ eventually.Assert(t, func(t require.TestingT) {
+ verifyIcingaDbRow(t, db, user)
+ }, 20*time.Second, 1*time.Second)
+ })
+ }
+
+ t.Run("UserCustomVar", func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Skip()
+ })
+ })
+
+ t.Run("UserGroup", func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Run("Member", func(t *testing.T) { t.Parallel(); t.Skip() })
+ t.Run("CustomVar", func(t *testing.T) { t.Parallel(); t.Skip() })
+ })
+
+ t.Run("Zone", func(t *testing.T) {
+ t.Parallel()
+ // TODO(jb): add tests
+
+ t.Skip()
+ })
+
+ t.Run("RuntimeUpdates", func(t *testing.T) {
+ t.Parallel()
+
+ // Wait some time to give Icinga DB a chance to finish the initial sync.
+ // TODO(jb): properly wait for this? but I don't know of a good way to detect this at the moment
+ time.Sleep(20 * time.Second)
+
+ client := i.ApiClient()
+
+ t.Run("Service", func(t *testing.T) {
+ t.Parallel()
+
+ for _, service := range makeTestSyncServices(t) {
+ service := service
+
+ t.Run("CreateAndDelete-"+service.VariantInfoString(), func(t *testing.T) {
+ t.Parallel()
+
+ client.CreateObject(t, "services", *service.HostName+"!"+service.Name, map[string]interface{}{
+ "attrs": makeIcinga2ApiAttributes(service, false),
+ })
+
+ eventually.Assert(t, func(t require.TestingT) {
+ verifyIcingaDbRow(t, db, service)
+ }, 20*time.Second, 1*time.Second)
+
+ if service.Vars != nil {
+ t.Run("CustomVar", func(t *testing.T) {
+ logger := it.Logger(t)
+ eventually.Assert(t, func(t require.TestingT) {
+ service.Vars.VerifyCustomVar(t, logger, db, service)
+ }, 20*time.Second, 1*time.Second)
+ })
+
+ t.Run("CustomVarFlat", func(t *testing.T) {
+ logger := it.Logger(t)
+ eventually.Assert(t, func(t require.TestingT) {
+ service.Vars.VerifyCustomVarFlat(t, logger, db, service)
+ }, 20*time.Second, 1*time.Second)
+ })
+ }
+
+ client.DeleteObject(t, "services", *service.HostName+"!"+service.Name, false)
+
+ require.Eventuallyf(t, func() bool {
+ var count int
+ err := db.Get(&count, db.Rebind("SELECT COUNT(*) FROM service WHERE name = ?"), service.Name)
+ require.NoError(t, err, "querying service count should not fail")
+ return count == 0
+ }, 20*time.Second, 1*time.Second, "service with name=%q should be removed from database", service.Name)
+ })
+ }
+
+ t.Run("Update", func(t *testing.T) {
+ t.Parallel()
+
+ for _, service := range makeTestSyncServices(t) {
+ service := service
+
+ t.Run(service.VariantInfoString(), func(t *testing.T) {
+ t.Parallel()
+
+ // Start with the final host_name and zone. Finding out what happens when you change this on an
+ // existing object might be fun, but not at this time.
+ client.CreateObject(t, "services", *service.HostName+"!"+service.Name, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "check_command": "default-checkcommand",
+ "zone": service.ZoneName,
+ },
+ })
+ require.Eventuallyf(t, func() bool {
+ var count int
+ err := db.Get(&count, db.Rebind("SELECT COUNT(*) FROM service WHERE name = ?"), service.Name)
+ require.NoError(t, err, "querying service count should not fail")
+ return count == 1
+ }, 20*time.Second, 1*time.Second, "service with name=%q should exist in database", service.Name)
+
+ client.UpdateObject(t, "services", *service.HostName+"!"+service.Name, map[string]interface{}{
+ "attrs": makeIcinga2ApiAttributes(service, true),
+ })
+
+ eventually.Assert(t, func(t require.TestingT) {
+ verifyIcingaDbRow(t, db, service)
+ }, 20*time.Second, 1*time.Second)
+
+ if service.Vars != nil {
+ t.Run("CustomVar", func(t *testing.T) {
+ logger := it.Logger(t)
+ eventually.Assert(t, func(t require.TestingT) {
+ service.Vars.VerifyCustomVar(t, logger, db, service)
+ }, 20*time.Second, 1*time.Second)
+ })
+
+ t.Run("CustomVarFlat", func(t *testing.T) {
+ logger := it.Logger(t)
+ eventually.Assert(t, func(t require.TestingT) {
+ service.Vars.VerifyCustomVarFlat(t, logger, db, service)
+ }, 20*time.Second, 1*time.Second)
+ })
+ }
+
+ client.DeleteObject(t, "services", *service.HostName+"!"+service.Name, false)
+ })
+ }
+ })
+ })
+
+ t.Run("User", func(t *testing.T) {
+ t.Parallel()
+
+ for _, user := range makeTestUsers(t) {
+ user := user
+
+ t.Run("CreateAndDelete-"+user.VariantInfoString(), func(t *testing.T) {
+ t.Parallel()
+
+ client.CreateObject(t, "users", user.Name, map[string]interface{}{
+ "attrs": makeIcinga2ApiAttributes(user, false),
+ })
+
+ eventually.Assert(t, func(t require.TestingT) {
+ verifyIcingaDbRow(t, db, user)
+ }, 20*time.Second, 1*time.Second)
+
+ client.DeleteObject(t, "users", user.Name, false)
+
+ require.Eventuallyf(t, func() bool {
+ var count int
+ err := db.Get(&count, db.Rebind(`SELECT COUNT(*) FROM "user" WHERE name = ?`), user.Name)
+ require.NoError(t, err, "querying user count should not fail")
+ return count == 0
+ }, 20*time.Second, 1*time.Second, "user with name=%q should be removed from database", user.Name)
+ })
+ }
+
+ t.Run("Update", func(t *testing.T) {
+ t.Parallel()
+
+ userName := utils.UniqueName(t, "user")
+
+ client.CreateObject(t, "users", userName, map[string]interface{}{
+ "attrs": map[string]interface{}{},
+ })
+ require.Eventuallyf(t, func() bool {
+ var count int
+ err := db.Get(&count, db.Rebind(`SELECT COUNT(*) FROM "user" WHERE name = ?`), userName)
+ require.NoError(t, err, "querying user count should not fail")
+ return count == 1
+ }, 20*time.Second, 1*time.Second, "user with name=%q should exist in database", userName)
+
+ for _, user := range makeTestUsers(t) {
+ user := user
+ user.Name = userName
+
+ t.Run(user.VariantInfoString(), func(t *testing.T) {
+ client.UpdateObject(t, "users", userName, map[string]interface{}{
+ "attrs": makeIcinga2ApiAttributes(user, true),
+ })
+
+ eventually.Assert(t, func(t require.TestingT) {
+ verifyIcingaDbRow(t, db, user)
+ }, 20*time.Second, 1*time.Second)
+ })
+ }
+
+ client.DeleteObject(t, "users", userName, false)
+ })
+ })
+
+ t.Run("Notifications", func(t *testing.T) {
+ t.Parallel()
+
+ for _, notification := range makeTestNotifications(t) {
+ notification := notification
+
+ t.Run("CreateAndDelete-"+notification.VariantInfoString(), func(t *testing.T) {
+ t.Parallel()
+
+ client.CreateObject(t, "notifications", notification.fullName(), map[string]interface{}{
+ "attrs": makeIcinga2ApiAttributes(notification, false),
+ })
+
+ eventually.Assert(t, func(t require.TestingT) {
+ notification.verify(t, db)
+ }, 20*time.Second, 200*time.Millisecond)
+
+ client.DeleteObject(t, "notifications", notification.fullName(), false)
+
+ require.Eventuallyf(t, func() bool {
+ var count int
+ err := db.Get(&count, db.Rebind("SELECT COUNT(*) FROM notification WHERE name = ?"), notification.fullName())
+ require.NoError(t, err, "querying notification count should not fail")
+ return count == 0
+ }, 20*time.Second, 200*time.Millisecond, "notification with name=%q should be removed from database", notification.fullName())
+ })
+ }
+
+ t.Run("Update", func(t *testing.T) {
+ t.Parallel()
+
+ baseNotification := Notification{
+ Name: utils.UniqueName(t, "notification"),
+ HostName: newString("default-host"),
+ ServiceName: newString("default-service"),
+ Command: "default-notificationcommand",
+ Users: []string{"default-user"},
+ UserGroups: []string{"default-usergroup"},
+ Interval: 1800,
+ }
+
+ client.CreateObject(t, "notifications", baseNotification.fullName(), map[string]interface{}{
+ "attrs": makeIcinga2ApiAttributes(baseNotification, false),
+ })
+
+ require.Eventuallyf(t, func() bool {
+ var count int
+ err := db.Get(&count, db.Rebind("SELECT COUNT(*) FROM notification WHERE name = ?"), baseNotification.fullName())
+ require.NoError(t, err, "querying notification count should not fail")
+ return count == 1
+ }, 20*time.Second, 200*time.Millisecond, "notification with name=%q should exist in database", baseNotification.fullName())
+
+ // TODO: Currently broken, but has been tested manually multiple times. Gets more time after RC2
+ /*t.Run("CreateAndDeleteUser", func(t *testing.T) {
+ groupName := utils.UniqueName(t, "group")
+ userName := "testuser112312321"
+
+ // Create usergroup
+ client.CreateObject(t, "usergroups", groupName, nil)
+
+ baseNotification.UserGroups = []string{groupName}
+ client.UpdateObject(t, "notifications", baseNotification.fullName(), map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "user_groups": baseNotification.UserGroups,
+ },
+ })
+
+ eventually.Assert(t, func(t require.TestingT) {
+ baseNotification.verify(t, db)
+ }, 20*time.Second, 1*time.Second)
+
+ // Create user
+ users[userName] = map[string]struct{}{groupName: {}}
+ client.CreateObject(t, "users", userName, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "groups": baseNotification.UserGroups,
+ },
+ })
+
+ require.Eventuallyf(t, func() bool {
+ var count int
+ err := db.Get(&count, "SELECT COUNT(*) FROM user WHERE name = ?", userName)
+ require.NoError(t, err, "querying user count should not fail")
+ return count == 1
+ }, 20*time.Second, 200*time.Millisecond, "user with name=%q should exist in database", userName)
+
+ eventually.Assert(t, func(t require.TestingT) {
+ baseNotification.verify(t, db)
+ }, 20*time.Second, 1*time.Second)
+
+ // Delete user
+ delete(users, userName)
+ client.DeleteObject(t, "users", userName, false)
+
+ eventually.Assert(t, func(t require.TestingT) {
+ baseNotification.verify(t, db)
+ }, 20*time.Second, 1*time.Second)
+
+ // Remove group
+ baseNotification.UserGroups = []string{}
+ client.UpdateObject(t, "notifications", baseNotification.fullName(), map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "user_groups": baseNotification.UserGroups,
+ },
+ })
+
+ client.DeleteObject(t, "usergroups", groupName, false)
+
+ eventually.Assert(t, func(t require.TestingT) {
+ baseNotification.verify(t, db)
+ }, 20*time.Second, 1*time.Second)
+ })*/
+
+ for _, notification := range makeTestNotifications(t) {
+ notification := notification
+ notification.Name = baseNotification.Name
+
+ t.Run(notification.VariantInfoString(), func(t *testing.T) {
+ client.UpdateObject(t, "notifications", notification.fullName(), map[string]interface{}{
+ "attrs": makeIcinga2ApiAttributes(notification, true),
+ })
+
+ eventually.Assert(t, func(t require.TestingT) {
+ notification.verify(t, db)
+ }, 20*time.Second, 200*time.Millisecond)
+ })
+ }
+
+ client.DeleteObject(t, "notifications", baseNotification.fullName(), false)
+ })
+ })
+
+ // TODO(jb): add tests for remaining config object types
+ })
+}
+
+// waitForDumpDoneSignal reads from the icinga:dump Redis stream until there is a signal for key="*" state="done",
+// that is icinga2 signals that it has completed its initial config dump.
+func waitForDumpDoneSignal(t *testing.T, r services.RedisServer, wait time.Duration, interval time.Duration) {
+ rc := r.Open()
+ defer func() { _ = rc.Close() }()
+
+ require.Eventually(t, func() bool {
+ stream, err := rc.XRead(context.Background(), &redis.XReadArgs{
+ Streams: []string{"icinga:dump", "0-0"},
+ Block: -1,
+ }).Result()
+ if err == redis.Nil {
+ // empty stream
+ return false
+ }
+ require.NoError(t, err, "redis xread should succeed")
+
+ for _, message := range stream[0].Messages {
+ key, ok := message.Values["key"]
+ require.Truef(t, ok, "icinga:dump message should contain 'key': %+v", message)
+
+ state, ok := message.Values["state"]
+ require.Truef(t, ok, "icinga:dump message should contain 'state': %+v", message)
+
+ if key == "*" && state == "done" {
+ return true
+ }
+ }
+
+ return false
+ }, wait, interval, "icinga2 should signal key='*' state='done'")
+}
+
+type Host struct {
+ Name string ` icingadb:"name"`
+ DisplayName string `icinga2:"display_name" icingadb:"display_name"`
+ Address string `icinga2:"address" icingadb:"address"`
+ Address6 string `icinga2:"address6" icingadb:"address6"`
+ CheckCommandName string `icinga2:"check_command" icingadb:"checkcommand_name"`
+ MaxCheckAttempts float64 `icinga2:"max_check_attempts" icingadb:"max_check_attempts"`
+ CheckPeriodName string `icinga2:"check_period" icingadb:"check_timeperiod_name"`
+ CheckTimeout float64 `icinga2:"check_timeout" icingadb:"check_timeout"`
+ CheckInterval float64 `icinga2:"check_interval" icingadb:"check_interval"`
+ RetryInterval float64 `icinga2:"retry_interval" icingadb:"check_retry_interval"`
+ EnableNotifications bool `icinga2:"enable_notifications" icingadb:"notifications_enabled"`
+ EnableActiveChecks bool `icinga2:"enable_active_checks" icingadb:"active_checks_enabled"`
+ EnablePassiveChecks bool `icinga2:"enable_passive_checks" icingadb:"passive_checks_enabled"`
+ EnableEventHandler bool `icinga2:"enable_event_handler" icingadb:"event_handler_enabled"`
+ EnableFlapping bool `icinga2:"enable_flapping" icingadb:"flapping_enabled"`
+ FlappingThresholdHigh float64 `icinga2:"flapping_threshold_high" icingadb:"flapping_threshold_high"`
+ FlappingThresholdLow float64 `icinga2:"flapping_threshold_low" icingadb:"flapping_threshold_low"`
+ EnablePerfdata bool `icinga2:"enable_perfdata" icingadb:"perfdata_enabled"`
+ EventCommandName string `icinga2:"event_command" icingadb:"eventcommand_name"`
+ Volatile bool `icinga2:"volatile" icingadb:"is_volatile"`
+ ZoneName string `icinga2:"zone" icingadb:"zone_name"`
+ CommandEndpointName string `icinga2:"command_endpoint" icingadb:"command_endpoint_name"`
+ Notes string `icinga2:"notes" icingadb:"notes"`
+ NotesUrl *string `icinga2:"notes_url" icingadb:"notes_url.notes_url"`
+ ActionUrl *string `icinga2:"action_url" icingadb:"action_url.action_url"`
+ IconImage *string `icinga2:"icon_image" icingadb:"icon_image.icon_image"`
+ IconImageAlt string `icinga2:"icon_image_alt" icingadb:"icon_image_alt"`
+ Vars *CustomVarTestData `icinga2:"vars"`
+ // TODO(jb): groups
+
+ utils.VariantInfo
+}
+
+func makeTestSyncHosts(t *testing.T) []Host {
+ host := Host{
+ Address: "127.0.0.1",
+ Address6: "::1",
+ CheckCommandName: "hostalive",
+ MaxCheckAttempts: 3,
+ CheckTimeout: 60,
+ CheckInterval: 10,
+ RetryInterval: 5,
+ FlappingThresholdHigh: 80,
+ FlappingThresholdLow: 20,
+ }
+
+ hosts := utils.MakeVariants(host).
+ Vary("DisplayName", "Some Display Name", "Other Display Name").
+ Vary("Address", "192.0.2.23", "192.0.2.42").
+ Vary("Address6", "2001:db8::23", "2001:db8::42").
+ Vary("CheckCommandName", "some-checkcommand", "other-checkcommand").
+ Vary("MaxCheckAttempts", 5.0, 7.0).
+ Vary("CheckPeriodName", "some-timeperiod", "other-timeperiod").
+ Vary("CheckTimeout", 30. /* TODO(jb): 5 */, 120.0).
+ Vary("CheckInterval", 20. /* TODO(jb): 5 */, 30.0).
+ Vary("RetryInterval", 1. /* TODO(jb): 5 */, 2.0).
+ Vary("EnableNotifications", true, false).
+ Vary("EnableActiveChecks", true, false).
+ Vary("EnablePassiveChecks", true, false).
+ Vary("EnableEventHandler", true, false).
+ Vary("EnableFlapping", true, false).
+ Vary("FlappingThresholdHigh", 90.0, 95.5).
+ Vary("FlappingThresholdLow", 5.5, 10.0).
+ Vary("EnablePerfdata", true, false).
+ Vary("EventCommandName", "some-eventcommand", "other-eventcommand").
+ Vary("Volatile", true, false).
+ Vary("ZoneName", "some-zone", "other-zone").
+ Vary("CommandEndpointName", "some-endpoint", "other-endpoint").
+ Vary("Notes", "Some Notes", "Other Notes").
+ Vary("NotesUrl", newString("https://some.notes.invalid/host"), newString("http://other.notes.invalid/host")).
+ Vary("ActionUrl", newString("https://some.action.invalid/host"), newString("http://other.actions.invalid/host")).
+ Vary("IconImage", newString("https://some.icon.invalid/host.png"), newString("http://other.icon.invalid/host.jpg")).
+ Vary("IconImageAlt", "Some Icon Image Alt", "Other Icon Image Alt").
+ Vary("Vars", localutils.AnySliceToInterfaceSlice(makeCustomVarTestData(t))...).
+ ResultAsBaseTypeSlice().([]Host)
+
+ for i := range hosts {
+ hosts[i].Name = utils.UniqueName(t, "host")
+
+ if hosts[i].DisplayName == "" {
+ // if no display_name is set, it defaults to name
+ hosts[i].DisplayName = hosts[i].Name
+ }
+
+ if hosts[i].ZoneName == "" {
+ hosts[i].ZoneName = "master"
+ }
+ }
+
+ return hosts
+}
+
+type Service struct {
+ Name string ` icingadb:"name"`
+ DisplayName string `icinga2:"display_name" icingadb:"display_name"`
+ HostName *string `icinga2:"host_name,nomodify" icingadb:"host.name"`
+ CheckCommandName string `icinga2:"check_command" icingadb:"checkcommand_name"`
+ MaxCheckAttempts float64 `icinga2:"max_check_attempts" icingadb:"max_check_attempts"`
+ CheckPeriodName string `icinga2:"check_period" icingadb:"check_timeperiod_name"`
+ CheckTimeout float64 `icinga2:"check_timeout" icingadb:"check_timeout"`
+ CheckInterval float64 `icinga2:"check_interval" icingadb:"check_interval"`
+ RetryInterval float64 `icinga2:"retry_interval" icingadb:"check_retry_interval"`
+ EnableNotifications bool `icinga2:"enable_notifications" icingadb:"notifications_enabled"`
+ EnableActiveChecks bool `icinga2:"enable_active_checks" icingadb:"active_checks_enabled"`
+ EnablePassiveChecks bool `icinga2:"enable_passive_checks" icingadb:"passive_checks_enabled"`
+ EnableEventHandler bool `icinga2:"enable_event_handler" icingadb:"event_handler_enabled"`
+ EnableFlapping bool `icinga2:"enable_flapping" icingadb:"flapping_enabled"`
+ FlappingThresholdHigh float64 `icinga2:"flapping_threshold_high" icingadb:"flapping_threshold_high"`
+ FlappingThresholdLow float64 `icinga2:"flapping_threshold_low" icingadb:"flapping_threshold_low"`
+ EnablePerfdata bool `icinga2:"enable_perfdata" icingadb:"perfdata_enabled"`
+ EventCommandName string `icinga2:"event_command" icingadb:"eventcommand_name"`
+ Volatile bool `icinga2:"volatile" icingadb:"is_volatile"`
+ ZoneName string `icinga2:"zone" icingadb:"zone_name"`
+ CommandEndpointName string `icinga2:"command_endpoint" icingadb:"command_endpoint_name"`
+ Notes string `icinga2:"notes" icingadb:"notes"`
+ NotesUrl *string `icinga2:"notes_url" icingadb:"notes_url.notes_url"`
+ ActionUrl *string `icinga2:"action_url" icingadb:"action_url.action_url"`
+ IconImage *string `icinga2:"icon_image" icingadb:"icon_image.icon_image"`
+ IconImageAlt string `icinga2:"icon_image_alt" icingadb:"icon_image_alt"`
+ Vars *CustomVarTestData `icinga2:"vars"`
+ // TODO(jb): groups
+
+ utils.VariantInfo
+}
+
+func makeTestSyncServices(t *testing.T) []Service {
+ service := Service{
+ HostName: newString("default-host"),
+ CheckCommandName: "default-checkcommand",
+ MaxCheckAttempts: 3,
+ CheckTimeout: 60,
+ CheckInterval: 10,
+ RetryInterval: 5,
+ EnableNotifications: true,
+ EnableActiveChecks: true,
+ EnablePassiveChecks: true,
+ EnableEventHandler: true,
+ EnableFlapping: true,
+ FlappingThresholdHigh: 80,
+ FlappingThresholdLow: 20,
+ EnablePerfdata: true,
+ Volatile: false,
+ }
+
+ services := utils.MakeVariants(service).
+ Vary("HostName", newString("some-host"), newString("other-host")).
+ Vary("DisplayName", "Some Display Name", "Other Display Name").
+ Vary("CheckCommandName", "some-checkcommand", "other-checkcommand").
+ Vary("MaxCheckAttempts", 5.0, 7.0).
+ Vary("CheckPeriodName", "some-timeperiod", "other-timeperiod").
+ Vary("CheckTimeout", 23.0 /* TODO(jb): .42 */, 120.0).
+ Vary("CheckInterval", 20.0, 42.0 /* TODO(jb): .23 */).
+ Vary("RetryInterval", 1.0 /* TODO(jb): .5 */, 15.0).
+ Vary("EnableNotifications", true, false).
+ Vary("EnableActiveChecks", true, false).
+ Vary("EnablePassiveChecks", true, false).
+ Vary("EnableEventHandler", true, false).
+ Vary("EnableFlapping", true, false).
+ Vary("FlappingThresholdHigh", 95.0, 99.5).
+ Vary("FlappingThresholdLow", 0.5, 10.0).
+ Vary("EnablePerfdata", true, false).
+ Vary("EventCommandName", "some-eventcommand", "other-eventcommand").
+ Vary("Volatile", true, false).
+ Vary("ZoneName", "some-zone", "other-zone").
+ Vary("CommandEndpointName", "some-endpoint", "other-endpoint").
+ Vary("Notes", "Some Notes", "Other Notes").
+ Vary("NotesUrl", newString("https://some.notes.invalid/service"), newString("http://other.notes.invalid/service")).
+ Vary("ActionUrl", newString("https://some.action.invalid/service"), newString("http://other.actions.invalid/service")).
+ Vary("IconImage", newString("https://some.icon.invalid/service.png"), newString("http://other.icon.invalid/service.jpg")).
+ Vary("IconImageAlt", "Some Icon Image Alt", "Other Icon Image Alt").
+ Vary("Vars", localutils.AnySliceToInterfaceSlice(makeCustomVarTestData(t))...).
+ ResultAsBaseTypeSlice().([]Service)
+
+ for i := range services {
+ services[i].Name = utils.UniqueName(t, "service")
+
+ if services[i].DisplayName == "" {
+ // if no display_name is set, it defaults to name
+ services[i].DisplayName = services[i].Name
+ }
+
+ if services[i].ZoneName == "" {
+ services[i].ZoneName = "master"
+ }
+ }
+
+ return services
+}
+
+type User struct {
+ // TODO(jb): vars, groups
+ Name string ` icingadb:"name"`
+ DisplayName string `icinga2:"display_name" icingadb:"display_name"`
+ Email string `icinga2:"email" icingadb:"email"`
+ Pager string `icinga2:"pager" icingadb:"pager"`
+ EnableNotifications bool `icinga2:"enable_notifications" icingadb:"notifications_enabled"`
+ Period *string `icinga2:"period" icingadb:"timeperiod.name"`
+ Types value.NotificationTypes `icinga2:"types" icingadb:"types"`
+ States value.NotificationStates `icinga2:"states" icingadb:"states"`
+
+ utils.VariantInfo
+}
+
+func makeTestUsers(t *testing.T) []User {
+ users := utils.MakeVariants(User{EnableNotifications: true}).
+ Vary("DisplayName", "Some Display Name", "Other Display Name").
+ Vary("Email", "some@email.invalid", "other@email.invalid").
+ Vary("Pager", "some pager", "other pager").
+ Vary("EnableNotifications", true, false).
+ Vary("Period", newString("some-timeperiod"), newString("other-timeperiod")).
+ Vary("Types",
+ value.NotificationTypes{"DowntimeStart", "DowntimeEnd", "DowntimeRemoved"},
+ value.NotificationTypes{"Custom"},
+ value.NotificationTypes{"Acknowledgement"},
+ value.NotificationTypes{"Problem", "Recovery"},
+ value.NotificationTypes{"FlappingStart", "FlappingEnd"},
+ value.NotificationTypes{"DowntimeStart", "Problem", "FlappingStart"},
+ value.NotificationTypes{"DowntimeEnd", "DowntimeRemoved", "Recovery", "FlappingEnd"},
+ value.NotificationTypes{"DowntimeStart", "DowntimeEnd", "DowntimeRemoved", "Custom", "Acknowledgement", "Problem", "Recovery", "FlappingStart", "FlappingEnd"},
+ value.NotificationTypes{"Custom", "Acknowledgement"},
+ ).
+ Vary("States",
+ value.NotificationStates{},
+ value.NotificationStates{"Up", "Down"},
+ value.NotificationStates{"OK", "Warning", "Critical", "Unknown"},
+ value.NotificationStates{"Critical", "Down"},
+ value.NotificationStates{"OK", "Warning", "Critical", "Unknown", "Up", "Down"}).
+ ResultAsBaseTypeSlice().([]User)
+
+ for i := range users {
+ users[i].Name = utils.UniqueName(t, "user")
+ if users[i].DisplayName == "" {
+ users[i].DisplayName = users[i].Name
+ }
+ }
+
+ return users
+}
+
+type Notification struct {
+ Name string ` icingadb:"name"`
+ HostName *string `icinga2:"host_name,nomodify" icingadb:"host.name"`
+ ServiceName *string `icinga2:"service_name,nomodify" icingadb:"service.name"`
+ Command string `icinga2:"command" icingadb:"notificationcommand.name"`
+ Times map[string]string `icinga2:"times"`
+ Interval int `icinga2:"interval" icingadb:"notification_interval"`
+ Period *string `icinga2:"period" icingadb:"timeperiod.name"`
+ //Zone string `icinga2:"zone" icingadb:"zone.name"`
+ Types value.NotificationTypes `icinga2:"types" icingadb:"types"`
+ States value.NotificationStates `icinga2:"states" icingadb:"states"`
+ Users []string `icinga2:"users"`
+ UserGroups []string `icinga2:"user_groups"`
+ Vars *CustomVarTestData `icinga2:"vars"`
+
+ utils.VariantInfo
+}
+
+func (n Notification) fullName() string {
+ if n.ServiceName == nil {
+ return *n.HostName + "!" + n.Name
+ } else {
+ return *n.HostName + "!" + *n.ServiceName + "!" + n.Name
+ }
+}
+
+func (n Notification) verify(t require.TestingT, db *sqlx.DB) {
+ verifyIcingaDbRow(t, db, n)
+
+ // Check if the "notification_user" table has been populated correctly
+ {
+ query := `SELECT u.name FROM notification n JOIN notification_user nu ON n.id = nu.notification_id JOIN "user" u ON u.id = nu.user_id WHERE n.name = ? ORDER BY u.name`
+ var rows []string
+ err := db.Select(&rows, db.Rebind(query), n.fullName())
+ require.NoError(t, err, "SQL query")
+
+ expected := append([]string(nil), n.Users...)
+ sort.Strings(expected)
+
+ assert.Equal(t, expected, rows, "Users in database should be equal")
+ }
+
+ // Check if the "notification_groups" table has been populated correctly
+ {
+ query := "SELECT ug.name FROM notification n JOIN notification_usergroup ng ON n.id = ng.notification_id JOIN usergroup ug ON ug.id = ng.usergroup_id WHERE n.name = ? ORDER BY ug.name"
+ var rows []string
+ err := db.Select(&rows, db.Rebind(query), n.fullName())
+ require.NoError(t, err, "SQL query")
+
+ expected := append([]string(nil), n.UserGroups...)
+ sort.Strings(expected)
+ require.Equal(t, expected, rows, "Usergroups in database should be equal")
+ }
+
+ // Check if the "notification_recipients" table has been populated correctly
+ {
+ type Row struct {
+ User *string `db:"username"`
+ Group *string `db:"groupname"`
+ }
+
+ var expected []Row
+
+ for _, user := range n.Users {
+ expected = append(expected, Row{User: newString(user)})
+ }
+
+ for _, userGroup := range n.UserGroups {
+ expected = append(expected, Row{Group: newString(userGroup)})
+ for user, groups := range users {
+ if _, ok := groups[userGroup]; ok {
+ expected = append(expected, Row{User: newString(user), Group: newString(userGroup)})
+ }
+ }
+ }
+
+ sort.Slice(expected, func(i, j int) bool {
+ r1 := expected[i]
+ r2 := expected[j]
+
+ stringComparePtr := func(a, b *string) int {
+ if a == nil && b == nil {
+ return 0
+ } else if a == nil {
+ return -1
+ } else if b == nil {
+ return 1
+ }
+
+ return strings.Compare(*a, *b)
+ }
+
+ switch stringComparePtr(r1.User, r2.User) {
+ case -1:
+ return true
+ case 1:
+ return false
+ default:
+ return stringComparePtr(r1.Group, r2.Group) == -1
+ }
+ })
+
+ query := "SELECT u.name AS username, ug.name AS groupname FROM notification n " +
+ "JOIN notification_recipient nr ON n.id = nr.notification_id " +
+ `LEFT JOIN "user" u ON u.id = nr.user_id ` +
+ "LEFT JOIN usergroup ug ON ug.id = nr.usergroup_id " +
+ "WHERE n.name = ? " +
+ "ORDER BY u.name IS NOT NULL, u.name, ug.name IS NOT NULL, ug.name"
+
+ var rows []Row
+ err := db.Select(&rows, db.Rebind(query), n.fullName())
+
+ require.NoError(t, err, "SQL query")
+ require.Equal(t, expected, rows, "Recipients in database should be equal")
+ }
+}
+
+func makeTestNotifications(t *testing.T) []Notification {
+ notification := Notification{
+ HostName: newString("default-host"),
+ ServiceName: newString("default-service"),
+ Command: "default-notificationcommand",
+ Users: []string{"default-user", "testuser1", "testuser2", "testuser3"},
+ UserGroups: []string{"default-usergroup", "testusergroup1", "testusergroup2", "testusergroup3"},
+ Interval: 1800,
+ }
+
+ notifications := utils.MakeVariants(notification).
+ //Vary("TimesBegin", 5, 999, 23980, 525, 666, 0).
+ //Vary("TimesEnd", 0, 453, 74350, 423, 235, 63477).
+ Vary("Interval", 5, 453, 74350, 423, 235, 63477).
+ Vary("Period", newString("some-timeperiod"), newString("other-timeperiod")).
+ Vary("Types",
+ value.NotificationTypes{"DowntimeStart", "DowntimeEnd", "DowntimeRemoved"},
+ value.NotificationTypes{"Custom"},
+ value.NotificationTypes{"Acknowledgement"},
+ value.NotificationTypes{"Problem", "Recovery"},
+ value.NotificationTypes{"FlappingStart", "FlappingEnd"},
+ value.NotificationTypes{"DowntimeStart", "Problem", "FlappingStart"},
+ value.NotificationTypes{"DowntimeEnd", "DowntimeRemoved", "Recovery", "FlappingEnd"},
+ value.NotificationTypes{"DowntimeStart", "DowntimeEnd", "DowntimeRemoved", "Custom", "Acknowledgement", "Problem", "Recovery", "FlappingStart", "FlappingEnd"},
+ value.NotificationTypes{"Custom", "Acknowledgement"},
+ ).
+ Vary("States",
+ value.NotificationStates{},
+ value.NotificationStates{"OK", "Warning", "Critical", "Unknown"},
+ value.NotificationStates{"OK", "Unknown"}).
+ Vary("Users", localutils.AnySliceToInterfaceSlice(localutils.SliceSubsets(
+ "default-user", "some-user", "other-user", "testuser1", "testuser2", "testuser3"))...).
+ Vary("UserGroups", localutils.AnySliceToInterfaceSlice(localutils.SliceSubsets(
+ "default-usergroup", "some-usergroup", "other-usergroup", "testusergroup1", "testusergroup2", "testusergroup3"))...).
+ ResultAsBaseTypeSlice().([]Notification)
+
+ for i := range notifications {
+ notifications[i].Name = utils.UniqueName(t, "notification")
+ }
+
+ return notifications
+}
+
+// writeIcinga2ConfigObjects emits config objects as icinga2 DSL to a writer
+// based on the type of obj and its field having icinga2 struct tags.
+func writeIcinga2ConfigObject(w io.Writer, obj interface{}) error {
+ o := reflect.ValueOf(obj)
+ name := o.FieldByName("Name").Interface()
+ typ := o.Type()
+ typeName := typ.Name()
+
+ _, err := fmt.Fprintf(w, "object %s %s {\n", typeName, value.ToIcinga2Config(name))
+ if err != nil {
+ return err
+ }
+
+ for fieldIndex := 0; fieldIndex < typ.NumField(); fieldIndex++ {
+ tag := typ.Field(fieldIndex).Tag.Get("icinga2")
+ attr := strings.Split(tag, ",")[0]
+ if attr != "" {
+ if v := o.Field(fieldIndex).Interface(); v != nil {
+ _, err := fmt.Fprintf(w, "\t%s = %s\n", attr, value.ToIcinga2Config(v))
+ if err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ _, err = fmt.Fprintf(w, "}\n")
+ return err
+}
+
+// makeIcinga2ApiAttributes generates a map that can be JSON marshaled and passed to the icinga2 API
+// based on the type of obj and its field having icinga2 struct tags. Fields that are marked as "nomodify"
+// (for example `icinga2:"host_name,nomodify"`) are omitted if the modify parameter is set to true.
+func makeIcinga2ApiAttributes(obj interface{}, modify bool) map[string]interface{} {
+ attrs := make(map[string]interface{})
+
+ o := reflect.ValueOf(obj)
+ typ := o.Type()
+ for fieldIndex := 0; fieldIndex < typ.NumField(); fieldIndex++ {
+ tag := typ.Field(fieldIndex).Tag.Get("icinga2")
+ parts := strings.Split(tag, ",")
+ attr := parts[0]
+ flags := parts[1:]
+ if attr == "" || (modify && slices.Contains(flags, "nomodify")) {
+ continue
+ }
+ if val := o.Field(fieldIndex).Interface(); val != nil {
+ attrs[attr] = value.ToIcinga2Api(val)
+ }
+ }
+
+ return attrs
+}
+
+// verifyIcingaDbRow checks that the object given by obj is properly present in the SQL database. It checks compares all
+// struct fields that have an icingadb tag set to the column name. It automatically joins tables if required.
+func verifyIcingaDbRow(t require.TestingT, db *sqlx.DB, obj interface{}) {
+ o := reflect.ValueOf(obj)
+ name := o.FieldByName("Name").Interface()
+ typ := o.Type()
+ typeName := typ.Name()
+
+ if notification, ok := obj.(Notification); ok {
+ name = notification.fullName()
+ }
+
+ type ColumnValueExpected struct {
+ Column string
+ Value interface{}
+ Expected interface{}
+ }
+
+ joinColumns := func(cs []ColumnValueExpected) string {
+ var c []string
+ for i := range cs {
+ var quotedParts []string
+ for _, part := range strings.Split(cs[i].Column, ".") {
+ quotedParts = append(quotedParts, `"`+part+`"`)
+ }
+ c = append(c, strings.Join(quotedParts, "."))
+ }
+ return strings.Join(c, ", ")
+ }
+
+ scanSlice := func(cs []ColumnValueExpected) []interface{} {
+ var vs []interface{}
+ for i := range cs {
+ vs = append(vs, cs[i].Value)
+ }
+ return vs
+ }
+
+ table := strings.ToLower(typeName)
+ var columns []ColumnValueExpected
+ joins := make(map[string]struct{})
+
+ for fieldIndex := 0; fieldIndex < typ.NumField(); fieldIndex++ {
+ if col := typ.Field(fieldIndex).Tag.Get("icingadb"); col != "" && col != "name" {
+ if val := o.Field(fieldIndex).Interface(); val != nil {
+ dbVal := value.ToIcingaDb(val)
+ scanVal := reflect.New(reflect.TypeOf(dbVal)).Interface()
+ if strings.Contains(col, ".") {
+ parts := strings.SplitN(col, ".", 2)
+ joins[parts[0]] = struct{}{}
+ } else {
+ col = table + "." + col
+ }
+ columns = append(columns, ColumnValueExpected{
+ Column: col,
+ Value: scanVal,
+ Expected: dbVal,
+ })
+ }
+ }
+ }
+
+ joinsQuery := ""
+ for join := range joins {
+ joinsQuery += fmt.Sprintf(` LEFT JOIN "%s" ON "%s"."id" = "%s"."%s_id"`, join, join, table, join)
+ }
+
+ query := fmt.Sprintf(`SELECT %s FROM "%s" %s WHERE "%s"."name" = ?`,
+ joinColumns(columns), table, joinsQuery, table)
+ rows, err := db.Query(db.Rebind(query), name)
+ require.NoError(t, err, "SQL query: %s", query)
+ defer func() { _ = rows.Close() }()
+ require.True(t, rows.Next(), "SQL query should return a row: %s", query)
+
+ err = rows.Scan(scanSlice(columns)...)
+ require.NoError(t, err, "SQL scan: %s", query)
+
+ for _, col := range columns {
+ got := reflect.ValueOf(col.Value).Elem().Interface()
+ assert.Equalf(t, col.Expected, got, "%s should match", col.Column)
+ }
+
+ require.False(t, rows.Next(), "SQL query should return only one row: %s", query)
+}
+
+// newString allocates a new *string and initializes it. This helper function exists as
+// there seems to be no way to achieve this within a single statement.
+func newString(s string) *string {
+ return &s
+}
+
+type CustomVarTestData struct {
+ Value interface{} // Value to put into config or API
+ Vars map[string]string // Expected values in customvar table
+ VarsFlat map[string]string // Expected values in customvar_flat table
+}
+
+func (c *CustomVarTestData) Icinga2ConfigValue() string {
+ if c == nil {
+ return value.ToIcinga2Config(nil)
+ }
+ return value.ToIcinga2Config(c.Value)
+}
+
+func (c *CustomVarTestData) Icinga2ApiValue() interface{} {
+ if c == nil {
+ return value.ToIcinga2Api(nil)
+ }
+ return value.ToIcinga2Api(c.Value)
+}
+
+func (c *CustomVarTestData) VerifyCustomVar(t require.TestingT, logger *zap.Logger, db *sqlx.DB, obj interface{}) {
+ c.verify(t, logger, db, obj, false)
+}
+
+func (c *CustomVarTestData) VerifyCustomVarFlat(t require.TestingT, logger *zap.Logger, db *sqlx.DB, obj interface{}) {
+ c.verify(t, logger, db, obj, true)
+}
+
+func (c *CustomVarTestData) verify(t require.TestingT, logger *zap.Logger, db *sqlx.DB, obj interface{}, flat bool) {
+ o := reflect.ValueOf(obj)
+ name := o.FieldByName("Name").Interface()
+ typ := o.Type()
+ typeName := typ.Name()
+ table := strings.ToLower(typeName)
+
+ query := ""
+ if flat {
+ query += "SELECT customvar_flat.flatname, customvar_flat.flatvalue "
+ } else {
+ query += "SELECT customvar.name, customvar.value "
+ }
+ query += "FROM " + table + "_customvar " +
+ "JOIN " + table + " ON " + table + ".id = " + table + "_customvar." + table + "_id " +
+ "JOIN customvar ON customvar.id = " + table + "_customvar.customvar_id "
+ if flat {
+ query += "JOIN customvar_flat ON customvar_flat.customvar_id = customvar.id "
+ }
+ query += "WHERE " + table + ".name = ?"
+
+ rows, err := db.Query(db.Rebind(query), name)
+ require.NoError(t, err, "querying customvars")
+ defer func() { _ = rows.Close() }()
+
+ expectedSrc := c.Vars
+ if flat {
+ expectedSrc = c.VarsFlat
+ }
+
+ // copy map to remove items while reading from the database
+ expected := make(map[string]string)
+ for k, v := range expectedSrc {
+ expected[k] = v
+ }
+
+ for rows.Next() {
+ var cvName, cvValue string
+ err := rows.Scan(&cvName, &cvValue)
+ require.NoError(t, err, "scanning query row")
+
+ logger.Debug("custom var from database",
+ zap.Bool("flat", flat),
+ zap.String("object-type", typeName),
+ zap.Any("object-name", name),
+ zap.String("custom-var-name", cvName),
+ zap.String("custom-var-value", cvValue))
+
+ if cvExpected, ok := expected[cvName]; ok {
+ assert.Equalf(t, cvExpected, cvValue, "custom var %q", cvName)
+ delete(expected, cvName)
+ } else if !ok {
+ assert.Failf(t, "unexpected custom var", "%q: %q", cvName, cvValue)
+ }
+ }
+
+ for k, v := range expected {
+ assert.Failf(t, "missing custom var", "%q: %q", k, v)
+ }
+}
+
+func makeCustomVarTestData(t *testing.T) []*CustomVarTestData {
+ var data []*CustomVarTestData
+
+ // Icinga deduplicates identical custom variables between objects, therefore add a unique identifier to names and
+ // values to force it to actually sync new variables instead of just changing the mapping of objects to variables.
+ id := utils.UniqueName(t, "customvar")
+
+ // simple string values
+ data = append(data, &CustomVarTestData{
+ Value: map[string]interface{}{
+ id + "-hello": id + " world",
+ id + "-foo": id + " bar",
+ },
+ Vars: map[string]string{
+ id + "-hello": `"` + id + ` world"`,
+ id + "-foo": `"` + id + ` bar"`,
+ },
+ VarsFlat: map[string]string{
+ id + "-hello": id + " world",
+ id + "-foo": id + " bar",
+ },
+ })
+
+ // complex example
+ data = append(data, &CustomVarTestData{
+ Value: map[string]interface{}{
+ id + "-array": []interface{}{"foo", 23, "bar"},
+ id + "-dict": map[string]interface{}{
+ "some-key": "some-value",
+ "other-key": "other-value",
+ },
+ id + "-string": "hello icinga",
+ id + "-int": -1,
+ id + "-float": 13.37,
+ id + "-true": true,
+ id + "-false": false,
+ id + "-null": nil,
+ id + "-nested-dict": map[string]interface{}{
+ "top-level-entry": "good morning",
+ "array": []interface{}{"answer?", 42},
+ "dict": map[string]interface{}{"another-key": "another-value", "yet-another-key": 4711},
+ },
+ id + "-nested-array": []interface{}{
+ []interface{}{1, 2, 3},
+ map[string]interface{}{"contains-a-map": "yes", "really?": true},
+ -42,
+ },
+ },
+ Vars: map[string]string{
+ id + "-array": `["foo",23,"bar"]`,
+ id + "-dict": `{"other-key":"other-value","some-key":"some-value"}`,
+ id + "-string": `"hello icinga"`,
+ id + "-int": `-1`,
+ id + "-float": `13.37`,
+ id + "-true": `true`,
+ id + "-false": `false`,
+ id + "-null": `null`,
+ id + "-nested-dict": `{"array":["answer?",42],"dict":{"another-key":"another-value","yet-another-key":4711},"top-level-entry":"good morning"}`,
+ id + "-nested-array": `[[1,2,3],{"contains-a-map":"yes","really?":true},-42]`,
+ },
+ VarsFlat: map[string]string{
+ id + "-array[0]": `foo`,
+ id + "-array[1]": `23`,
+ id + "-array[2]": `bar`,
+ id + "-dict.some-key": `some-value`,
+ id + "-dict.other-key": `other-value`,
+ id + "-string": `hello icinga`,
+ id + "-int": `-1`,
+ id + "-float": `13.37`,
+ id + "-true": `true`,
+ id + "-false": `false`,
+ id + "-null": `null`,
+ id + "-nested-dict.dict.another-key": `another-value`,
+ id + "-nested-dict.dict.yet-another-key": `4711`,
+ id + "-nested-dict.array[0]": `answer?`,
+ id + "-nested-dict.array[1]": `42`,
+ id + "-nested-dict.top-level-entry": `good morning`,
+ id + "-nested-array[0][0]": `1`,
+ id + "-nested-array[0][1]": `2`,
+ id + "-nested-array[0][2]": `3`,
+ id + "-nested-array[1].contains-a-map": `yes`,
+ id + "-nested-array[1].really?": `true`,
+ id + "-nested-array[2]": `-42`,
+ },
+ })
+
+ // two sets of variables that share keys but have different values
+ data = append(data, &CustomVarTestData{
+ Value: map[string]interface{}{
+ "a": "foo",
+ "b": []interface{}{"bar", 42, -13.37},
+ "c": map[string]interface{}{"a": true, "b": false, "c": nil},
+ },
+ Vars: map[string]string{
+ "a": `"foo"`,
+ "b": `["bar",42,-13.37]`,
+ "c": `{"a":true,"b":false,"c":null}`,
+ },
+ VarsFlat: map[string]string{
+ "a": "foo",
+ "b[0]": `bar`,
+ "b[1]": `42`,
+ "b[2]": `-13.37`,
+ "c.a": `true`,
+ "c.b": `false`,
+ "c.c": `null`,
+ },
+ }, &CustomVarTestData{
+ Value: map[string]interface{}{
+ "a": -13.37,
+ "b": []interface{}{true, false, nil},
+ "c": map[string]interface{}{"a": "foo", "b": "bar", "c": 42},
+ },
+ Vars: map[string]string{
+ "a": "-13.37",
+ "b": `[true,false,null]`,
+ "c": `{"a":"foo","b":"bar","c":42}`,
+ },
+ VarsFlat: map[string]string{
+ "a": `-13.37`,
+ "b[0]": `true`,
+ "b[1]": `false`,
+ "b[2]": `null`,
+ "c.a": "foo",
+ "c.b": `bar`,
+ "c.c": `42`,
+ },
+ })
+
+ return data
+}
diff --git a/tests/regression_394_test.go b/tests/regression_394_test.go
new file mode 100644
index 0000000..7306293
--- /dev/null
+++ b/tests/regression_394_test.go
@@ -0,0 +1,126 @@
+package icingadb_test
+
+import (
+ "github.com/icinga/icinga-testing/utils"
+ "github.com/icinga/icinga-testing/utils/eventually"
+ "github.com/jmoiron/sqlx"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "strconv"
+ "testing"
+ "time"
+)
+
+// Regression test for https://github.com/Icinga/icingadb/pull/394
+//
+// Upsert and delete runtime objects for the same object must be executed in-order. Otherwise, an old update might
+// replace the object in the database with an older version or delete an object even if it still exists.
+func TestRegression394(t *testing.T) {
+ numObjects := 200
+
+ logger := it.Logger(t)
+
+ r := it.RedisServerT(t)
+ rdb := getDatabase(t)
+ i := it.Icinga2NodeT(t, "master")
+ i.EnableIcingaDb(r)
+ err := i.Reload()
+ require.NoError(t, err, "icinga2 reload")
+
+ // Wait for Icinga 2 to signal a successful dump before starting
+ // Icinga DB to ensure that we actually test the initial sync.
+ logger.Debug("waiting for icinga2 dump done signal")
+ waitForDumpDoneSignal(t, r, 20*time.Second, 100*time.Millisecond)
+
+ // Only after that, start Icinga DB.
+ logger.Debug("starting icingadb")
+ it.IcingaDbInstanceT(t, r, rdb)
+
+ client := i.ApiClient()
+
+ db, err := sqlx.Open(rdb.Driver(), rdb.DSN())
+ require.NoError(t, err, "connecting to SQL database shouldn't fail")
+ t.Cleanup(func() { _ = db.Close() })
+
+ waitForPendingRuntimeUpdates := func(t *testing.T) {
+ // To verify that all host runtime updates up to now have been processed, create a marker host, wait for it to
+ // appear in the database, delete it, and wait for it to disappear again.
+ markerName := utils.UniqueName(t, "marker")
+
+ client.CreateHost(t, markerName, nil)
+ eventually.Require(t, func(t require.TestingT) {
+ var count int
+ err = db.Get(&count, db.Rebind("SELECT COUNT(*) FROM host WHERE name = ?"), markerName)
+ require.NoError(t, err, "select host count from database")
+ assert.Equalf(t, 1, count, "marker host %q should appear in database", markerName)
+ }, 10*time.Second, 100*time.Millisecond)
+
+ client.DeleteHost(t, markerName, true)
+ eventually.Require(t, func(t require.TestingT) {
+ var count int
+ err = db.Get(&count, db.Rebind("SELECT COUNT(*) FROM host WHERE name = ?"), markerName)
+ require.NoError(t, err, "select host count from database")
+ assert.Zerof(t, count, "marker host %q should disappear from database", markerName)
+ }, 10*time.Second, 100*time.Millisecond)
+ }
+
+ t.Run("CreateAndDelete", func(t *testing.T) {
+ // This test creates a number of hosts and deletes them immediately afterwards. If the delete operation for a
+ // host is executed before the upsert operation, it would still exist in the database even though the object
+ // is gone from Icinga 2.
+
+ t.Parallel()
+
+ namePrefix := utils.UniqueName(t, "host") + "-"
+
+ for i := 0; i < numObjects; i++ {
+ name := namePrefix + strconv.Itoa(i)
+ client.CreateHost(t, name, nil)
+ client.DeleteHost(t, name, true)
+ }
+
+ waitForPendingRuntimeUpdates(t)
+
+ var countAfter int
+ err = db.Get(&countAfter, db.Rebind("SELECT COUNT(*) FROM host WHERE name LIKE ?"), namePrefix+"%")
+ require.NoError(t, err, "select host count from database")
+ assert.Zero(t, countAfter, "no hosts should be left in database")
+
+ })
+
+ t.Run("DeleteAndRecreate", func(t *testing.T) {
+ // This test performs an operation that is probably more useful: delete an existing object and recreate it
+ // immediately afterwards. If the delete operation is delayed, the host will be missing from the database.
+
+ t.Parallel()
+
+ namePrefix := utils.UniqueName(t, "host") + "-"
+ hosts := make([]string, numObjects)
+
+ for i := range hosts {
+ name := namePrefix + strconv.Itoa(i)
+ client.CreateHost(t, name, nil)
+ hosts[i] = name
+ }
+
+ waitForPendingRuntimeUpdates(t)
+
+ var countBefore int
+ err = db.Get(&countBefore, db.Rebind("SELECT COUNT(*) FROM host WHERE name LIKE ?"), namePrefix+"%")
+ require.NoError(t, err, "select host count from database")
+ assert.Equal(t, numObjects, countBefore, "all hosts should exist in database before recreation")
+
+ for _, name := range hosts {
+ client.DeleteHost(t, name, true)
+ client.CreateHost(t, name, nil)
+ }
+
+ waitForPendingRuntimeUpdates(t)
+
+ var countAfter int
+ err = db.Get(&countAfter, db.Rebind("SELECT COUNT(*) FROM host WHERE name LIKE ?"), namePrefix+"%")
+ require.NoError(t, err, "select host count from database")
+ assert.Equal(t, numObjects, countAfter, "all hosts should exist in database after recreation")
+ })
+
+}
diff --git a/tests/sla_test.go b/tests/sla_test.go
new file mode 100644
index 0000000..6fa3a0e
--- /dev/null
+++ b/tests/sla_test.go
@@ -0,0 +1,385 @@
+package icingadb_test
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/icinga/icinga-testing/utils"
+ "github.com/icinga/icinga-testing/utils/eventually"
+ "github.com/jmoiron/sqlx"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+ "math"
+ "net/http"
+ "testing"
+ "time"
+)
+
+func TestSla(t *testing.T) {
+ m := it.MysqlDatabaseT(t)
+ m.ImportIcingaDbSchema()
+
+ r := it.RedisServerT(t)
+ i := it.Icinga2NodeT(t, "master")
+ i.EnableIcingaDb(r)
+ err := i.Reload()
+ require.NoError(t, err, "icinga2 should reload without error")
+ it.IcingaDbInstanceT(t, r, m)
+
+ client := i.ApiClient()
+
+ t.Run("StateEvents", func(t *testing.T) {
+ t.Parallel()
+
+ hostname := utils.UniqueName(t, "host")
+ client.CreateHost(t, hostname, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "enable_active_checks": false,
+ "enable_passive_checks": true,
+ "check_command": "dummy",
+ "max_check_attempts": 3,
+ },
+ })
+
+ type StateChange struct {
+ Time float64
+ State int
+ }
+
+ var stateChanges []StateChange
+
+ processCheckResult := func(exitStatus int, isHard bool) *ObjectsHostsResponse {
+ time.Sleep(10 * time.Millisecond) // ensure there is a bit of difference in ms resolution
+
+ output := utils.UniqueName(t, "output")
+ data := ActionsProcessCheckResultRequest{
+ Type: "Host",
+ Filter: fmt.Sprintf(`host.name==%q`, hostname),
+ ExitStatus: exitStatus,
+ PluginOutput: output,
+ }
+ dataJson, err := json.Marshal(data)
+ require.NoError(t, err, "marshal request")
+ response, err := client.PostJson("/v1/actions/process-check-result", bytes.NewBuffer(dataJson))
+ require.NoError(t, err, "process-check-result")
+ require.Equal(t, 200, response.StatusCode, "process-check-result")
+
+ response, err = client.GetJson("/v1/objects/hosts/" + hostname)
+ require.NoError(t, err, "get host: request")
+ require.Equal(t, 200, response.StatusCode, "get host: request")
+
+ var hosts ObjectsHostsResponse
+ err = json.NewDecoder(response.Body).Decode(&hosts)
+ require.NoError(t, err, "get host: parse response")
+
+ require.Equal(t, 1, len(hosts.Results), "there must be one host in the response")
+ host := hosts.Results[0]
+ require.Equal(t, output, host.Attrs.LastCheckResult.Output,
+ "last check result should be visible in host object")
+ require.Equal(t, exitStatus, host.Attrs.State, "soft state should match check result")
+
+ if isHard {
+ require.Equal(t, exitStatus, host.Attrs.LastHardState, "hard state should match check result")
+ if len(stateChanges) > 0 {
+ require.Greater(t, host.Attrs.LastHardStateChange, stateChanges[len(stateChanges)-1].Time,
+ "last_hard_state_change_time of host should have changed")
+ }
+ stateChanges = append(stateChanges, StateChange{
+ Time: host.Attrs.LastHardStateChange,
+ State: exitStatus,
+ })
+ } else {
+ require.NotEmpty(t, stateChanges, "there should be a hard state change prior to a soft one")
+ require.Equal(t, stateChanges[len(stateChanges)-1].Time, host.Attrs.LastHardStateChange,
+ "check result should not lead to a hard state change, i.e. last_hard_state_change should not change")
+ }
+
+ return &hosts
+ }
+
+ processCheckResult(0, true) // hard (UNKNOWN -> UP)
+ processCheckResult(1, false) // soft
+ processCheckResult(1, false) // soft
+ processCheckResult(1, true) // hard (UP -> DOWN)
+ processCheckResult(1, false) // hard
+ processCheckResult(0, true) // hard (DOWN -> UP)
+ processCheckResult(0, false) // hard
+
+ assert.Equal(t, 3, len(stateChanges), "there should be three hard state changes")
+
+ db, err := sqlx.Connect("mysql", m.DSN())
+ require.NoError(t, err, "connecting to mysql")
+ defer func() { _ = db.Close() }()
+
+ type Row struct {
+ Time int64 `db:"event_time"`
+ State int `db:"hard_state"`
+ }
+
+ eventually.Assert(t, func(t require.TestingT) {
+ var rows []Row
+ err = db.Select(&rows, db.Rebind("SELECT s.event_time, s.hard_state FROM sla_history_state s "+
+ "JOIN host ON host.id = s.host_id WHERE host.name = ? ORDER BY event_time ASC"), hostname)
+ require.NoError(t, err, "select sla_history_state")
+
+ assert.Equal(t, len(stateChanges), len(rows), "number of sla_history_state entries")
+
+ for i := range rows {
+ assert.WithinDuration(t, time.UnixMilli(int64(stateChanges[i].Time*1000)), time.UnixMilli(rows[i].Time),
+ time.Millisecond, "event time should match state change time")
+ assert.Equal(t, stateChanges[i].State, rows[i].State, "hard state should match")
+ }
+ }, 5*time.Second, 200*time.Millisecond)
+
+ redis := r.Open()
+ defer func() { _ = redis.Close() }()
+
+ logger := it.Logger(t)
+
+ logger.Debug("redis state history", zap.Bool("before", true))
+ eventually.Assert(t, func(t require.TestingT) {
+ result, err := redis.XRange(context.Background(), "icinga:history:stream:state", "-", "+").Result()
+ require.NoError(t, err, "reading state history stream should not fail")
+ logger.Debug("redis state history", zap.Any("values", result))
+ assert.Empty(t, result, "redis state history stream should be drained")
+ }, 5*time.Second, 10*time.Millisecond)
+ logger.Debug("redis state history", zap.Bool("after", true))
+ })
+
+ t.Run("DowntimeEvents", func(t *testing.T) {
+ t.Parallel()
+
+ type Options struct {
+ Fixed bool // Whether to schedule a fixed or flexible downtime.
+ Cancel bool // Whether to cancel the downtime or let it expire.
+ }
+
+ downtimeTest := func(t *testing.T, o Options) {
+ hostname := utils.UniqueName(t, "host")
+ client.CreateHost(t, hostname, map[string]interface{}{
+ "attrs": map[string]interface{}{
+ "enable_active_checks": false,
+ "enable_passive_checks": true,
+ "check_command": "dummy",
+ "max_check_attempts": 1,
+ },
+ })
+
+ processCheckResult := func(status int) time.Time {
+ output := utils.RandomString(8)
+ reqBody, err := json.Marshal(ActionsProcessCheckResultRequest{
+ Type: "Host",
+ Filter: fmt.Sprintf(`host.name==%q`, hostname),
+ ExitStatus: status,
+ PluginOutput: output,
+ })
+ require.NoError(t, err, "marshal request")
+ response, err := client.PostJson("/v1/actions/process-check-result", bytes.NewBuffer(reqBody))
+ require.NoError(t, err, "process-check-result")
+ require.Equal(t, 200, response.StatusCode, "process-check-result")
+
+ response, err = client.GetJson("/v1/objects/hosts/" + hostname)
+ require.NoError(t, err, "get host: request")
+ require.Equal(t, 200, response.StatusCode, "get host: request")
+
+ var hosts ObjectsHostsResponse
+ err = json.NewDecoder(response.Body).Decode(&hosts)
+ require.NoError(t, err, "get host: parse response")
+
+ require.Equal(t, 1, len(hosts.Results), "there must be one host in the response")
+ host := hosts.Results[0]
+ require.Equal(t, output, host.Attrs.LastCheckResult.Output,
+ "last check result should be visible in host object")
+ require.Equal(t, 1, host.Attrs.StateType, "host should be in hard state")
+ require.Equal(t, status, host.Attrs.State, "state should match check result")
+
+ sec, nsec := math.Modf(host.Attrs.LastCheckResult.ExecutionEnd)
+ return time.Unix(int64(sec), int64(nsec*1e9))
+ }
+
+ // Ensure that host is in UP state.
+ processCheckResult(0)
+
+ refTime := time.Now().Truncate(time.Second)
+ // Schedule the downtime start in the past so that we would notice if Icinga 2/DB would
+ // use the current time somewhere where we expect the scheduled start time.
+ downtimeStart := refTime.Add(-1 * time.Hour)
+ var downtimeEnd time.Time
+ if o.Cancel || !o.Fixed {
+ // Downtimes we will cancel can expire long in the future as we don't have to wait for it.
+ // Same for flexible downtimes as for these, we don't have to wait until the scheduled end but only
+ // for their duration.
+ downtimeEnd = refTime.Add(1 * time.Hour)
+ } else {
+ // Let all other downtimes expire soon (fixed downtimes where we wait for expiry).
+ downtimeEnd = refTime.Add(5 * time.Second)
+ }
+
+ var duration time.Duration
+ if !o.Fixed {
+ duration = 10 * time.Second
+ }
+ req, err := json.Marshal(ActionsScheduleDowntimeRequest{
+ Type: "Host",
+ Filter: fmt.Sprintf(`host.name==%q`, hostname),
+ StartTime: downtimeStart.Unix(),
+ EndTime: downtimeEnd.Unix(),
+ Fixed: o.Fixed,
+ Duration: duration.Seconds(),
+ Author: utils.RandomString(8),
+ Comment: utils.RandomString(8),
+ })
+ require.NoError(t, err, "marshal request")
+ response, err := client.PostJson("/v1/actions/schedule-downtime", bytes.NewBuffer(req))
+ require.NoError(t, err, "schedule-downtime")
+ require.Equal(t, 200, response.StatusCode, "schedule-downtime")
+
+ var scheduleResponse ActionsScheduleDowntimeResponse
+ err = json.NewDecoder(response.Body).Decode(&scheduleResponse)
+ require.NoError(t, err, "decode schedule-downtime response")
+ require.Equal(t, 1, len(scheduleResponse.Results), "schedule-downtime should return 1 result")
+ require.Equal(t, http.StatusOK, scheduleResponse.Results[0].Code, "schedule-downtime should return 1 result")
+ downtimeName := scheduleResponse.Results[0].Name
+
+ type Row struct {
+ Start int64 `db:"downtime_start"`
+ End int64 `db:"downtime_end"`
+ }
+
+ db, err := sqlx.Connect("mysql", m.DSN())
+ require.NoError(t, err, "connecting to mysql")
+ defer func() { _ = db.Close() }()
+
+ if !o.Fixed {
+ // Give Icinga 2 and Icinga DB some time that if they would generate an SLA history event in error,
+ // they have a chance to do so before we check for its absence.
+ time.Sleep(10 * time.Second)
+
+ var count int
+ err = db.Get(&count, db.Rebind("SELECT COUNT(*) FROM sla_history_downtime s "+
+ "JOIN host ON host.id = s.host_id WHERE host.name = ?"), hostname)
+ require.NoError(t, err, "select sla_history_state")
+ assert.Zero(t, count, "there should be no event in sla_history_downtime when scheduling a flexible downtime on an UP host")
+ }
+
+ // Bring host into DOWN state.
+ criticalTime := processCheckResult(1)
+
+ eventually.Assert(t, func(t require.TestingT) {
+ var rows []Row
+ err = db.Select(&rows, db.Rebind("SELECT s.downtime_start, s.downtime_end FROM sla_history_downtime s "+
+ "JOIN host ON host.id = s.host_id WHERE host.name = ?"), hostname)
+ require.NoError(t, err, "select sla_history_state")
+
+ require.Equal(t, 1, len(rows), "there should be exactly one sla_history_downtime row")
+ if o.Fixed {
+ assert.Equal(t, downtimeStart, time.UnixMilli(rows[0].Start),
+ "downtime_start should match scheduled start time")
+ assert.Equal(t, downtimeEnd, time.UnixMilli(rows[0].End),
+ "downtime_end should match scheduled end time")
+ } else {
+ assert.WithinDuration(t, criticalTime, time.UnixMilli(rows[0].Start), time.Second,
+ "downtime_start should match time of host state change")
+ assert.Equal(t, duration, time.UnixMilli(rows[0].End).Sub(time.UnixMilli(rows[0].Start)),
+ "downtime_end - downtime_start duration should match scheduled duration")
+ }
+ }, 5*time.Second, 200*time.Millisecond)
+
+ redis := r.Open()
+ defer func() { _ = redis.Close() }()
+
+ eventually.Assert(t, func(t require.TestingT) {
+ result, err := redis.XRange(context.Background(), "icinga:history:stream:downtime", "-", "+").Result()
+ require.NoError(t, err, "reading downtime history stream should not fail")
+ assert.Empty(t, result, "redis downtime history stream should be drained")
+ }, 5*time.Second, 10*time.Millisecond)
+
+ if o.Cancel {
+ req, err = json.Marshal(ActionsRemoveDowntimeRequest{
+ Downtime: downtimeName,
+ })
+ require.NoError(t, err, "marshal remove-downtime request")
+ response, err = client.PostJson("/v1/actions/remove-downtime", bytes.NewBuffer(req))
+ require.NoError(t, err, "remove-downtime")
+ require.Equal(t, 200, response.StatusCode, "remove-downtime")
+ }
+
+ downtimeCancel := time.Now()
+
+ if !o.Cancel {
+ // Wait for downtime to expire + a few extra seconds. The row should not be updated, give
+ // enough time to have a chance catching if Icinga DB updates it nonetheless.
+ if !o.Fixed {
+ time.Sleep(duration + 5*time.Second)
+ } else {
+ d := time.Until(downtimeEnd) + 5*time.Second
+ require.Less(t, d, time.Minute, "bug in tests: don't wait too long")
+ time.Sleep(d)
+ }
+ }
+
+ eventually.Assert(t, func(t require.TestingT) {
+ var rows []Row
+ err = db.Select(&rows, db.Rebind("SELECT s.downtime_start, s.downtime_end FROM sla_history_downtime s "+
+ "JOIN host ON host.id = s.host_id WHERE host.name = ?"), hostname)
+ require.NoError(t, err, "select sla_history_state")
+
+ require.Equal(t, 1, len(rows), "there should be exactly one sla_history_downtime row")
+ if o.Fixed {
+ assert.Equal(t, downtimeStart, time.UnixMilli(rows[0].Start),
+ "downtime_start should match scheduled start")
+ } else {
+ assert.WithinDuration(t, criticalTime, time.UnixMilli(rows[0].Start), time.Second,
+ "downtime_start should match critical time")
+ }
+ if o.Cancel {
+ // Allow more delta for the end time after cancel as we did not choose the exact time.
+ assert.WithinDuration(t, downtimeCancel, time.UnixMilli(rows[0].End), time.Second,
+ "downtime_end should match cancel time")
+ } else if o.Fixed {
+ assert.Equal(t, downtimeEnd, time.UnixMilli(rows[0].End),
+ "downtime_start should match scheduled end")
+ } else {
+ assert.Equal(t, duration, time.UnixMilli(rows[0].End).Sub(time.UnixMilli(rows[0].Start)),
+ "downtime_end - downtime_start duration should match scheduled duration")
+ }
+ }, 5*time.Second, 200*time.Millisecond)
+
+ eventually.Assert(t, func(t require.TestingT) {
+ result, err := redis.XRange(context.Background(), "icinga:history:stream:downtime", "-", "+").Result()
+ require.NoError(t, err, "reading downtime history stream should not fail")
+ assert.Empty(t, result, "redis downtime history stream should be drained")
+ }, 5*time.Second, 10*time.Millisecond)
+ }
+
+ t.Run("Fixed", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Cancel", func(t *testing.T) {
+ t.Parallel()
+ downtimeTest(t, Options{Fixed: true, Cancel: true})
+ })
+
+ t.Run("Expire", func(t *testing.T) {
+ t.Parallel()
+ downtimeTest(t, Options{Fixed: true, Cancel: false})
+ })
+ })
+
+ t.Run("Flexible", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Cancel", func(t *testing.T) {
+ t.Parallel()
+ downtimeTest(t, Options{Fixed: false, Cancel: true})
+ })
+
+ t.Run("Expire", func(t *testing.T) {
+ t.Parallel()
+ downtimeTest(t, Options{Fixed: false, Cancel: false})
+ })
+ })
+ })
+}
diff --git a/tests/sql/main_test.go b/tests/sql/main_test.go
new file mode 100644
index 0000000..a295125
--- /dev/null
+++ b/tests/sql/main_test.go
@@ -0,0 +1,23 @@
+package sql_test
+
+import (
+ "github.com/icinga/icinga-testing"
+ "github.com/icinga/icinga-testing/services"
+ "github.com/icinga/icingadb/tests/internal/utils"
+ "testing"
+)
+
+var it *icingatesting.IT
+
+func TestMain(m *testing.M) {
+ it = icingatesting.NewIT()
+ defer it.Cleanup()
+
+ m.Run()
+}
+
+func getDatabase(t testing.TB) services.RelationalDatabase {
+ rdb := utils.GetDatabase(it, t)
+ rdb.ImportIcingaDbSchema()
+ return rdb
+}
diff --git a/tests/sql/sla_test.go b/tests/sql/sla_test.go
new file mode 100644
index 0000000..8a89850
--- /dev/null
+++ b/tests/sql/sla_test.go
@@ -0,0 +1,406 @@
+package sql_test
+
+import (
+ "crypto/rand"
+ "database/sql/driver"
+ "fmt"
+ "github.com/go-sql-driver/mysql"
+ "github.com/jmoiron/sqlx"
+ "github.com/lib/pq"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestSla(t *testing.T) {
+ rdb := getDatabase(t)
+ db, err := sqlx.Open(rdb.Driver(), rdb.DSN())
+ require.NoError(t, err, "connect to database")
+
+ type TestData struct {
+ Name string
+ Events []SlaHistoryEvent
+ Start uint64
+ End uint64
+ Expected float64
+ }
+
+ tests := []TestData{{
+ Name: "EmptyHistory",
+ // Empty history implies no previous problem state, therefore SLA should be 100%
+ Events: nil,
+ Start: 1000,
+ End: 2000,
+ Expected: 100.0,
+ }, {
+ Name: "MultipleStateChanges",
+ // Some flapping, test that all changes are considered.
+ Events: []SlaHistoryEvent{
+ &State{Time: 1000, State: 2, PreviousState: 99}, // -10%
+ &State{Time: 1100, State: 0, PreviousState: 2},
+ &State{Time: 1300, State: 2, PreviousState: 0}, // -10%
+ &State{Time: 1400, State: 0, PreviousState: 2},
+ &State{Time: 1600, State: 2, PreviousState: 0}, // -10%
+ &State{Time: 1700, State: 0, PreviousState: 2},
+ &State{Time: 1900, State: 2, PreviousState: 0}, // -10%
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 60.0,
+ }, {
+ Name: "OverlappingDowntimesAndProblems",
+ // SLA should be 90%:
+ // 1000..1100: OK, no downtime
+ // 1100..1200: OK, in downtime
+ // 1200..1300: CRITICAL, in downtime
+ // 1300..1400: CRITICAL, no downtime (only period counting for SLA, -10%)
+ // 1400..1500: CRITICAL, in downtime
+ // 1500..1600: OK, in downtime
+ // 1600..2000: OK, no downtime
+ Events: []SlaHistoryEvent{
+ &Downtime{Start: 1100, End: 1300},
+ &Downtime{Start: 1400, End: 1600},
+ &State{Time: 1200, State: 2, PreviousState: 0},
+ &State{Time: 1500, State: 0, PreviousState: 2},
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 90.0,
+ }, {
+ Name: "CriticalBeforeInterval",
+ // If there is no event within the SLA interval, the last state from before the interval should be used.
+ Events: []SlaHistoryEvent{
+ &State{Time: 0, State: 2, PreviousState: 99},
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 0.0,
+ }, {
+ Name: "CriticalBeforeIntervalWithDowntime",
+ // State change and downtime start from before the SLA interval should be considered if still relevant.
+ Events: []SlaHistoryEvent{
+ &State{Time: 800, State: 2, PreviousState: 99},
+ &Downtime{Start: 600, End: 1800},
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 80.0,
+ }, {
+ Name: "CriticalBeforeIntervalWithOverlappingDowntimes",
+ // Test that overlapping downtimes are properly accounted for.
+ Events: []SlaHistoryEvent{
+ &State{Time: 800, State: 2, PreviousState: 99},
+ &Downtime{Start: 600, End: 1000},
+ &Downtime{Start: 800, End: 1200},
+ &Downtime{Start: 1000, End: 1400},
+ // Everything except 1400-1600 is covered by downtimes, -20%
+ &Downtime{Start: 1600, End: 2000},
+ &Downtime{Start: 1800, End: 2200},
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 80.0,
+ }, {
+ Name: "FallbackToPreviousState",
+ // If there is no state event from before the SLA interval, the previous hard state from the first event
+ // after the beginning of the SLA interval should be used as the initial state.
+ Events: []SlaHistoryEvent{
+ &State{Time: 1200, State: 0, PreviousState: 2},
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 80.0,
+ }, {
+ Name: "FallbackToCurrentState",
+ // If there are no state history events, the current state of the checkable should be used.
+ Events: []SlaHistoryEvent{
+ &CurrentState{State: 2},
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 0.0,
+ }, {
+ Name: "PreferInitialStateFromBeforeOverLaterState",
+ // The previous_hard_state should only be used as a fallback when there is no event from before the
+ // SLA interval. Therefore, the latter should be preferred if there is conflicting information.
+ Events: []SlaHistoryEvent{
+ &State{Time: 800, State: 2, PreviousState: 99},
+ &State{Time: 1200, State: 0, PreviousState: 0},
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 80.0,
+ }, {
+ Name: "PreferInitialStateFromBeforeOverCurrentState",
+ // The current state should only be used as a fallback when there is no state history event.
+ // Therefore, the latter should be preferred if there is conflicting information.
+ Events: []SlaHistoryEvent{
+ &State{Time: 800, State: 2, PreviousState: 99},
+ &CurrentState{State: 0},
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 0.0,
+ }, {
+ Name: "PreferLaterStateOverCurrentState",
+ // The current state should only be used as a fallback when there is no state history event.
+ // Therefore, the latter should be preferred if there is conflicting information.
+ Events: []SlaHistoryEvent{
+ &State{Time: 1200, State: 0, PreviousState: 2},
+ &CurrentState{State: 2},
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 80.0,
+ }, {
+ Name: "InitialUnknownReducesTotalTime",
+ Events: []SlaHistoryEvent{
+ &State{Time: 1500, State: 2, PreviousState: 99},
+ &State{Time: 1700, State: 0, PreviousState: 2},
+ &CurrentState{State: 0},
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 60,
+ }, {
+ Name: "IntermediateUnknownReducesTotalTime",
+ Events: []SlaHistoryEvent{
+ &State{Time: 1000, State: 0, PreviousState: 2},
+ &State{Time: 1100, State: 2, PreviousState: 0},
+ &State{Time: 1600, State: 0, PreviousState: 99},
+ &State{Time: 1800, State: 2, PreviousState: 0},
+ &CurrentState{State: 0},
+ },
+ Start: 1000,
+ End: 2000,
+ Expected: 60,
+ }}
+
+ for _, test := range tests {
+ t.Run(test.Name, func(t *testing.T) {
+ testSla(t, db, test.Events, test.Start, test.End, test.Expected, "unexpected SLA value")
+ })
+ }
+
+ t.Run("Invalid", func(t *testing.T) {
+ m := SlaHistoryMeta{
+ EnvironmentId: make([]byte, 20),
+ EndpointId: make([]byte, 20),
+ ObjectType: "host",
+ HostId: make([]byte, 20),
+ }
+
+ checkErr := func(t *testing.T, err error) {
+ require.Error(t, err, "SLA function should return an error")
+
+ switch d := db.DriverName(); d {
+ case "mysql":
+ var mysqlErr *mysql.MySQLError
+ require.ErrorAs(t, err, &mysqlErr, "SLA function should return a MySQL error")
+ // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_signal_exception
+ assert.Equal(t, uint16(1644), mysqlErr.Number, "MySQL error should be ER_SIGNAL_EXCEPTION")
+ assert.Equal(t, "end time must be greater than start time", mysqlErr.Message,
+ "MySQL error should contain custom message")
+
+ case "postgres":
+ var pqErr *pq.Error
+ require.ErrorAs(t, err, &pqErr, "SLA function should return a PostgreSQL error")
+ assert.Equal(t, pq.ErrorCode("P0001"), pqErr.Code, "MySQL error should be ER_SIGNAL_EXCEPTION")
+ assert.Equal(t, "end time must be greater than start time", pqErr.Message,
+ "PostgreSQL error should contain custom message")
+
+ default:
+ panic(fmt.Sprintf("unknown database driver %q", d))
+ }
+ }
+
+ t.Run("ZeroDuration", func(t *testing.T) {
+ _, err := execSqlSlaFunc(db, &m, 1000, 1000)
+ checkErr(t, err)
+ })
+
+ t.Run("NegativeDuration", func(t *testing.T) {
+ _, err := execSqlSlaFunc(db, &m, 2000, 1000)
+ checkErr(t, err)
+ })
+ })
+}
+
+func execSqlSlaFunc(db *sqlx.DB, m *SlaHistoryMeta, start uint64, end uint64) (float64, error) {
+ var result float64
+ err := db.Get(&result, db.Rebind("SELECT get_sla_ok_percent(?, ?, ?, ?)"),
+ m.HostId, m.ServiceId, start, end)
+ return result, err
+}
+
+func testSla(t *testing.T, db *sqlx.DB, events []SlaHistoryEvent, start uint64, end uint64, expected float64, msg string) {
+ t.Run("Host", func(t *testing.T) {
+ testSlaWithObjectType(t, db, events, false, start, end, expected, msg)
+ })
+ t.Run("Service", func(t *testing.T) {
+ testSlaWithObjectType(t, db, events, true, start, end, expected, msg)
+ })
+}
+
+func testSlaWithObjectType(t *testing.T, db *sqlx.DB,
+ events []SlaHistoryEvent, service bool, start uint64, end uint64, expected float64, msg string,
+) {
+ makeId := func() []byte {
+ id := make([]byte, 20)
+ _, err := rand.Read(id)
+ require.NoError(t, err, "generating random id failed")
+ return id
+ }
+
+ meta := SlaHistoryMeta{
+ EnvironmentId: makeId(),
+ EndpointId: makeId(),
+ HostId: makeId(),
+ }
+ if service {
+ meta.ObjectType = "service"
+ meta.ServiceId = makeId()
+ } else {
+ meta.ObjectType = "host"
+ }
+
+ for _, event := range events {
+ err := event.WriteSlaEventToDatabase(db, &meta)
+ require.NoErrorf(t, err, "Inserting SLA history for %#v failed", event)
+ }
+
+ r, err := execSqlSlaFunc(db, &meta, start, end)
+ require.NoError(t, err, "SLA query should not fail")
+ assert.Equal(t, expected, r, msg)
+}
+
+type SlaHistoryMeta struct {
+ EnvironmentId NullableBytes `db:"environment_id"`
+ EndpointId NullableBytes `db:"endpoint_id"`
+ ObjectType string `db:"object_type"`
+ HostId NullableBytes `db:"host_id"`
+ ServiceId NullableBytes `db:"service_id"`
+}
+
+type SlaHistoryEvent interface {
+ WriteSlaEventToDatabase(db *sqlx.DB, m *SlaHistoryMeta) error
+}
+
+type State struct {
+ Time uint64
+ State uint8
+ PreviousState uint8
+}
+
+var _ SlaHistoryEvent = (*State)(nil)
+
+func (s *State) WriteSlaEventToDatabase(db *sqlx.DB, m *SlaHistoryMeta) error {
+ type values struct {
+ *SlaHistoryMeta
+ Id []byte `db:"id"`
+ EventTime uint64 `db:"event_time"`
+ HardState uint8 `db:"hard_state"`
+ PreviousHardState uint8 `db:"previous_hard_state"`
+ }
+
+ id := make([]byte, 20)
+ _, err := rand.Read(id)
+ if err != nil {
+ return err
+ }
+
+ _, err = db.NamedExec("INSERT INTO sla_history_state"+
+ " (id, environment_id, endpoint_id, object_type, host_id, service_id, event_time, hard_state, previous_hard_state)"+
+ " VALUES (:id, :environment_id, :endpoint_id, :object_type, :host_id, :service_id, :event_time, :hard_state, :previous_hard_state)",
+ &values{
+ SlaHistoryMeta: m,
+ Id: id[:],
+ EventTime: s.Time,
+ HardState: s.State,
+ PreviousHardState: s.PreviousState,
+ })
+ return err
+}
+
+type CurrentState struct {
+ State uint8
+}
+
+func (c *CurrentState) WriteSlaEventToDatabase(db *sqlx.DB, m *SlaHistoryMeta) error {
+ type values struct {
+ *SlaHistoryMeta
+ State uint8 `db:"state"`
+ PropertiesChecksum NullableBytes `db:"properties_checksum"`
+ }
+
+ v := values{
+ SlaHistoryMeta: m,
+ State: c.State,
+ PropertiesChecksum: make([]byte, 20),
+ }
+
+ if len(m.ServiceId) == 0 {
+ _, err := db.NamedExec("INSERT INTO host_state"+
+ " (id, host_id, environment_id, properties_checksum, soft_state, previous_soft_state,"+
+ " hard_state, previous_hard_state, check_attempt, severity, last_state_change, next_check, next_update)"+
+ " VALUES (:host_id, :host_id, :environment_id, :properties_checksum, :state, :state, :state, :state, 0, 0, 0, 0, 0)",
+ &v)
+ return err
+ } else {
+ _, err := db.NamedExec("INSERT INTO service_state"+
+ " (id, host_id, service_id, environment_id, properties_checksum, soft_state, previous_soft_state,"+
+ " hard_state, previous_hard_state, check_attempt, severity, last_state_change, next_check, next_update)"+
+ " VALUES (:service_id, :host_id, :service_id, :environment_id, :properties_checksum, :state, :state, :state, :state, 0, 0, 0, 0, 0)",
+ &v)
+ return err
+ }
+}
+
+var _ SlaHistoryEvent = (*CurrentState)(nil)
+
+type Downtime struct {
+ Start uint64
+ End uint64
+}
+
+var _ SlaHistoryEvent = (*Downtime)(nil)
+
+type slaHistoryDowntime struct {
+ *SlaHistoryMeta
+ DowntimeId []byte `db:"downtime_id"`
+ DowntimeStart uint64 `db:"downtime_start"`
+ DowntimeEnd uint64 `db:"downtime_end"`
+}
+
+func (d *Downtime) WriteSlaEventToDatabase(db *sqlx.DB, m *SlaHistoryMeta) error {
+ downtimeId := make([]byte, 20)
+ _, err := rand.Read(downtimeId)
+ if err != nil {
+ return err
+ }
+
+ _, err = db.NamedExec("INSERT INTO sla_history_downtime"+
+ " (environment_id, endpoint_id, object_type, host_id, service_id, downtime_id, downtime_start, downtime_end)"+
+ " VALUES (:environment_id, :endpoint_id, :object_type, :host_id,"+
+ " :service_id, :downtime_id, :downtime_start, :downtime_end)",
+ &slaHistoryDowntime{
+ SlaHistoryMeta: m,
+ DowntimeId: downtimeId[:],
+ DowntimeStart: d.Start,
+ DowntimeEnd: d.End,
+ })
+ return err
+}
+
+// NullableBytes allows writing to binary columns in a database with support for NULL.
+type NullableBytes []byte
+
+// Value implements the database/sql/driver.Valuer interface.
+func (b NullableBytes) Value() (driver.Value, error) {
+ if b != nil {
+ return []byte(b), nil
+ }
+
+ // any(nil) is treated as NULL in contrast to []byte(nil) which is a non-NULL byte sequence of length 0.
+ return nil, nil
+}
diff --git a/tests/state_sync_test.go b/tests/state_sync_test.go
new file mode 100644
index 0000000..a6b3010
--- /dev/null
+++ b/tests/state_sync_test.go
@@ -0,0 +1,10 @@
+package icingadb_test
+
+import "testing"
+
+func TestStateSync(t *testing.T) {
+ // TODO(jb): add tests
+
+ t.Run("Host", func(t *testing.T) { t.Parallel(); t.Skip() })
+ t.Run("Service", func(t *testing.T) { t.Parallel(); t.Skip() })
+}