diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:40:59 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:40:59 +0000 |
commit | bc4e624732bd51c0dd1e9529cf228e8c23127732 (patch) | |
tree | d95dab8960e9d02d3b95f8653074ad2e54ca207c /tests | |
parent | Initial commit. (diff) | |
download | icingadb-bc4e624732bd51c0dd1e9529cf228e8c23127732.tar.xz icingadb-bc4e624732bd51c0dd1e9529cf228e8c23127732.zip |
Adding upstream version 1.1.1.upstream/1.1.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | tests/cleanup_and_retention_test.go | 228 | ||||
-rw-r--r-- | tests/environment_test.go | 120 | ||||
-rw-r--r-- | tests/go.mod | 44 | ||||
-rw-r--r-- | tests/go.sum | 1065 | ||||
-rw-r--r-- | tests/history_bench_test.go | 130 | ||||
-rw-r--r-- | tests/history_test.go | 835 | ||||
-rw-r--r-- | tests/history_test_zones.conf | 22 | ||||
-rw-r--r-- | tests/instance_test.go | 8 | ||||
-rw-r--r-- | tests/internal/utils/database.go | 28 | ||||
-rw-r--r-- | tests/internal/utils/redis.go | 37 | ||||
-rw-r--r-- | tests/internal/utils/slice.go | 37 | ||||
-rw-r--r-- | tests/internal/utils/slice_test.go | 31 | ||||
-rw-r--r-- | tests/internal/value/notification_states.go | 37 | ||||
-rw-r--r-- | tests/internal/value/notification_types.go | 40 | ||||
-rw-r--r-- | tests/internal/value/value.go | 103 | ||||
-rw-r--r-- | tests/main_test.go | 29 | ||||
-rw-r--r-- | tests/object_sync_test.conf | 54 | ||||
-rw-r--r-- | tests/object_sync_test.go | 1446 | ||||
-rw-r--r-- | tests/regression_394_test.go | 126 | ||||
-rw-r--r-- | tests/sla_test.go | 385 | ||||
-rw-r--r-- | tests/sql/main_test.go | 23 | ||||
-rw-r--r-- | tests/sql/sla_test.go | 406 | ||||
-rw-r--r-- | tests/state_sync_test.go | 10 |
23 files changed, 5244 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..aca258a --- /dev/null +++ b/tests/go.mod @@ -0,0 +1,44 @@ +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/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.5-0.20230721180626-a61e2b4c9c5f+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/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.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.7.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // 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..b283b96 --- /dev/null +++ b/tests/go.sum @@ -0,0 +1,1065 @@ +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/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/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/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/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/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.8+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.5-0.20230721180626-a61e2b4c9c5f+incompatible h1:vGLHGz5kxqgyHLchV0OAXgH6j/QjKcZfWeF+1XN2GCg= +github.com/docker/docker v24.0.5-0.20230721180626-a61e2b4c9c5f+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/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/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/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/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-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/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/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/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/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/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/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.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +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-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +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/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/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/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..e2455ec --- /dev/null +++ b/tests/object_sync_test.go @@ -0,0 +1,1446 @@ +package icingadb_test + +import ( + "bytes" + "context" + "database/sql" + _ "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]sql.NullString // 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() }() + + // copy map to remove items while reading from the database + expected := make(map[string]sql.NullString) + if flat { + for k, v := range c.VarsFlat { + expected[k] = v + } + } else { + for k, v := range c.Vars { + expected[k] = toDBString(v) + } + } + + for rows.Next() { + var cvName string + var cvValue sql.NullString + 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.String)) + + 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.String) + } + } + + 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]sql.NullString{ + id + "-hello": toDBString(id + " world"), + id + "-foo": toDBString(id + " bar"), + }, + }) + + // empty custom vars of type array and dictionaries + data = append(data, &CustomVarTestData{ + Value: map[string]interface{}{ + id + "-empty-list": []interface{}{}, + id + "-empty-nested-list": []interface{}{[]interface{}{}}, + id + "-empty-dict": map[string]interface{}{}, + id + "-empty-nested-dict": map[string]interface{}{ + "some-key": map[string]interface{}{}, + }, + }, + Vars: map[string]string{ + id + "-empty-list": `[]`, + id + "-empty-nested-list": `[[]]`, + id + "-empty-dict": `{}`, + id + "-empty-nested-dict": `{"some-key":{}}`, + }, + VarsFlat: map[string]sql.NullString{ + id + "-empty-list": {}, + id + "-empty-nested-list[0]": {}, + id + "-empty-dict": {}, + id + "-empty-nested-dict.some-key": {}, + }, + }) + + // 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]sql.NullString{ + id + "-array[0]": toDBString(`foo`), + id + "-array[1]": toDBString(`23`), + id + "-array[2]": toDBString(`bar`), + id + "-dict.some-key": toDBString(`some-value`), + id + "-dict.other-key": toDBString(`other-value`), + id + "-string": toDBString(`hello icinga`), + id + "-int": toDBString(`-1`), + id + "-float": toDBString(`13.37`), + id + "-true": toDBString(`true`), + id + "-false": toDBString(`false`), + id + "-null": toDBString(`null`), + id + "-nested-dict.dict.another-key": toDBString(`another-value`), + id + "-nested-dict.dict.yet-another-key": toDBString(`4711`), + id + "-nested-dict.array[0]": toDBString(`answer?`), + id + "-nested-dict.array[1]": toDBString(`42`), + id + "-nested-dict.top-level-entry": toDBString(`good morning`), + id + "-nested-array[0][0]": toDBString(`1`), + id + "-nested-array[0][1]": toDBString(`2`), + id + "-nested-array[0][2]": toDBString(`3`), + id + "-nested-array[1].contains-a-map": toDBString(`yes`), + id + "-nested-array[1].really?": toDBString(`true`), + id + "-nested-array[2]": toDBString(`-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]sql.NullString{ + "a": toDBString("foo"), + "b[0]": toDBString(`bar`), + "b[1]": toDBString(`42`), + "b[2]": toDBString(`-13.37`), + "c.a": toDBString(`true`), + "c.b": toDBString(`false`), + "c.c": toDBString(`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]sql.NullString{ + "a": toDBString(`-13.37`), + "b[0]": toDBString(`true`), + "b[1]": toDBString(`false`), + "b[2]": toDBString(`null`), + "c.a": toDBString("foo"), + "c.b": toDBString(`bar`), + "c.c": toDBString(`42`), + }, + }) + + return data +} + +func toDBString(str string) sql.NullString { + return sql.NullString{String: str, Valid: true} +} 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() }) +} |