diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 51b9bc144..be6f89e79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,7 +39,6 @@ jobs: build-args: | BUILD=${{ steps.vars.outputs.build }} GIT_BUILD_HASH=${{ steps.vars.outputs.sha }} - CC_GH_COMMIT_SHA=${{ github.event.pull_request.head.sha }} build-cli: name: Build CLI runs-on: ubuntu-latest @@ -49,5 +48,22 @@ jobs: os: [darwin, linux] steps: - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true - run: | GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} make bin/gravity-cli + build-web: + name: Build Web + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: web/package.json + cache: "npm" + cache-dependency-path: web/package-lock.json + - run: make web-install gen-client-ts web-build + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 97924e61c..7e70ac78c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,9 +3,12 @@ "BOOTSTRAP_ROLES": "dns;dhcp;api;discovery;backup;monitoring;tsdb;tftp", "ETCD_ENDPOINT": "localhost:2385", "DEBUG": "true", - "INSTANCE_IP": "0.0.0.0" + "INSTANCE_IP": "0.0.0.0", + "CI": "true", }, "go.testFlags": ["-count=1", "-failfast", "-shuffle=on", "-v"], + "go.coverOnSingleTest": true, + "go.coverOnTestPackage": true, "gitlens.autolinks": [ { "alphanumeric": true, diff --git a/Dockerfile b/Dockerfile index 4a2519e17..1bca06b36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,6 @@ # Stage 1: Build web FROM --platform=${BUILDPLATFORM} docker.io/library/node:23.3 AS web-builder -ARG CC_GH_COMMIT_SHA - WORKDIR /work COPY ./Makefile /work/Makefile diff --git a/Makefile b/Makefile index 26c7b62cd..749ae2447 100644 --- a/Makefile +++ b/Makefile @@ -176,5 +176,6 @@ test: internal/resources/macoui internal/resources/blocky internal/resources/tft -covermode=atomic \ -count=${TEST_COUNT} \ ${TEST_FLAGS} \ - ./... 2>&1 | tee test-output + $(shell go list ./... | grep -v ./api) \ + 2>&1 | tee test-output go tool cover -html coverage.txt -o coverage.html diff --git a/go.mod b/go.mod index 248ce0236..3c1fc62a7 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.23.0 require ( github.com/0xERR0R/blocky v0.9.2-0.20241022123918-76aa6cc84cd1 + github.com/Masterminds/semver/v3 v3.2.1 github.com/Netflix/go-env v0.1.2 github.com/Ullaakut/nmap/v2 v2.2.2 github.com/aeden/traceroute v0.0.0-20210211061815-03f5f7cb7908 diff --git a/pkg/extconfig/config_test.go b/pkg/extconfig/config_test.go index 154b75525..6fca41973 100644 --- a/pkg/extconfig/config_test.go +++ b/pkg/extconfig/config_test.go @@ -25,9 +25,5 @@ func TestGetIP(t *testing.T) { } func TestVersion(t *testing.T) { - assert.Equal(t, Version+"-dev", FullVersion()) - BuildHash = "foo" - assert.Equal(t, Version+"-foo", FullVersion()) - BuildHash = "foobqerqewrqwer" - assert.Equal(t, Version+"-foobqerq", FullVersion()) + assert.Equal(t, Version+"+test", FullVersion()) } diff --git a/pkg/extconfig/log_iml/log_iml.go b/pkg/extconfig/log_iml/log_iml.go index 40ceea8a6..f2b5e37d3 100644 --- a/pkg/extconfig/log_iml/log_iml.go +++ b/pkg/extconfig/log_iml/log_iml.go @@ -7,13 +7,13 @@ import ( "go.uber.org/zap/zapcore" ) -type inMemoryLogger struct { +type InMemoryLogger struct { msgs []zapcore.Entry msgM sync.RWMutex max int } -func (iml *inMemoryLogger) Hook() zap.Option { +func (iml *InMemoryLogger) Hook() zap.Option { return zap.Hooks(func(e zapcore.Entry) error { iml.msgM.Lock() defer iml.msgM.Unlock() @@ -25,26 +25,31 @@ func (iml *inMemoryLogger) Hook() zap.Option { }) } -func (iml *inMemoryLogger) Messages() []zapcore.Entry { +func (iml *InMemoryLogger) MaxSize() int { + return iml.max +} + +func (iml *InMemoryLogger) Flush() { + iml.msgM.Lock() + iml.msgs = make([]zapcore.Entry, 0) + iml.msgM.Unlock() +} + +func (iml *InMemoryLogger) Messages() []zapcore.Entry { iml.msgM.RLock() defer iml.msgM.RUnlock() return iml.msgs } -var iml *inMemoryLogger +var iml *InMemoryLogger func init() { - iml = &inMemoryLogger{ + iml = &InMemoryLogger{ msgs: make([]zapcore.Entry, 0), max: 300, } } -type InMemoryLogger interface { - Messages() []zapcore.Entry - Hook() zap.Option -} - -func Get() InMemoryLogger { +func Get() *InMemoryLogger { return iml } diff --git a/pkg/extconfig/log_iml/log_iml_test.go b/pkg/extconfig/log_iml/log_iml_test.go new file mode 100644 index 000000000..c4de651ae --- /dev/null +++ b/pkg/extconfig/log_iml/log_iml_test.go @@ -0,0 +1,29 @@ +package log_iml_test + +import ( + "testing" + + "beryju.io/gravity/pkg/extconfig" + "beryju.io/gravity/pkg/extconfig/log_iml" + "github.com/stretchr/testify/assert" +) + +func TestInMemoryLogger(t *testing.T) { + iml := log_iml.Get() + iml.Flush() + extconfig.Get().Logger().Debug("test") + msgs := iml.Messages() + assert.Len(t, msgs, 1) +} + +func TestInMemoryLogger_Trunc(t *testing.T) { + iml := log_iml.Get() + iml.Flush() + for i := 0; i <= iml.MaxSize(); i++ { + extconfig.Get().Logger().Debug("test") + } + // Log one more message + extconfig.Get().Logger().Debug("test") + msgs := iml.Messages() + assert.Len(t, msgs, iml.MaxSize()) +} diff --git a/pkg/extconfig/version.go b/pkg/extconfig/version.go index 1badd25a2..d3696b158 100644 --- a/pkg/extconfig/version.go +++ b/pkg/extconfig/version.go @@ -1,6 +1,7 @@ package extconfig import ( + "os" "strings" ) @@ -11,10 +12,14 @@ var ( ) func FullVersion() string { + if os.Getenv("CI") == "true" { + Version = "99.99.99" + BuildHash = "test" + } version := strings.Builder{} version.WriteString(Version) if BuildHash != "" { - version.WriteRune('-') + version.WriteRune('+') if len(BuildHash) >= 8 { version.WriteString(BuildHash[:8]) } else { diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go index f805f922a..f3643da8f 100644 --- a/pkg/instance/instance.go +++ b/pkg/instance/instance.go @@ -336,7 +336,16 @@ func (i *Instance) startRole(ctx context.Context, id string, rawConfig []byte) b defer srs.Finish() defer i.putInstanceInfo(srs.Context()) instanceRoleStarted.WithLabelValues(id).SetToCurrentTime() - err := i.roles[id].Role.Start(srs.Context(), rawConfig) + // Run migrations + client, err := i.roles[id].RoleInstance.Migrator().Run(srs.Context()) + if err != nil { + i.log.Warn("failed to run migrations for role", zap.String("roleId", id)) + return false + } + // Overwrite role's KV client with the potentially hooked client for migrations + i.roles[id].RoleInstance.kv = client + // Start role + err = i.roles[id].Role.Start(srs.Context(), rawConfig) if err == roles.ErrRoleNotConfigured { i.log.Info("role not configured", zap.String("roleId", id)) } else if err != nil { diff --git a/pkg/instance/migrate/inline_migration.go b/pkg/instance/migrate/inline_migration.go new file mode 100644 index 000000000..9f6d2515b --- /dev/null +++ b/pkg/instance/migrate/inline_migration.go @@ -0,0 +1,43 @@ +package migrate + +import ( + "context" + + "beryju.io/gravity/pkg/storage" + "github.com/Masterminds/semver/v3" +) + +func MustParseConstraint(input string) *semver.Constraints { + c, err := semver.NewConstraint(input) + if err != nil { + panic(err) + } + return c +} + +type InlineMigration struct { + MigrationName string + ActivateOnVersion *semver.Constraints + HookFunc func(context.Context) (*storage.Client, error) + CleanupFunc func(context.Context) error +} + +func (im *InlineMigration) Name() string { + return im.MigrationName +} + +func (im *InlineMigration) Check(clusterVersion *semver.Version, ctx context.Context) (bool, error) { + check := im.ActivateOnVersion.Check(clusterVersion) + return check, nil +} + +func (im *InlineMigration) Hook(ctx context.Context) (*storage.Client, error) { + return im.HookFunc(ctx) +} + +func (im *InlineMigration) Cleanup(ctx context.Context) error { + if im.CleanupFunc != nil { + return im.CleanupFunc(ctx) + } + return nil +} diff --git a/pkg/instance/migrate/migrate.go b/pkg/instance/migrate/migrate.go new file mode 100644 index 000000000..653a275e3 --- /dev/null +++ b/pkg/instance/migrate/migrate.go @@ -0,0 +1,104 @@ +package migrate + +import ( + "context" + "encoding/json" + "sort" + + "beryju.io/gravity/pkg/extconfig" + "beryju.io/gravity/pkg/instance/types" + "beryju.io/gravity/pkg/roles" + "beryju.io/gravity/pkg/storage" + "github.com/Masterminds/semver/v3" + clientv3 "go.etcd.io/etcd/client/v3" + "go.uber.org/zap" +) + +type Migrator struct { + ri roles.Instance + log *zap.Logger + migrations []roles.Migration +} + +func New(ri roles.Instance) *Migrator { + return &Migrator{ + ri: ri, + log: ri.Log().Named("migrator"), + migrations: make([]roles.Migration, 0), + } +} + +func (mi *Migrator) GetClusterVersion() (*semver.Version, error) { + type partialInstanceInfo struct { + Version string `json:"version" required:"true"` + } + instances, err := mi.ri.KV().Get( + context.Background(), + mi.ri.KV().Key( + types.KeyInstance, + ).Prefix(true).String(), + clientv3.WithPrefix(), + ) + if err != nil { + return nil, err + } + // Gather all instances in the cluster and parse their versions + version := []*semver.Version{} + for _, inst := range instances.Kvs { + pi := partialInstanceInfo{} + err = json.Unmarshal(inst.Value, &pi) + if err != nil { + mi.log.Warn("failed to parse instance info", zap.Error(err)) + continue + } + v, err := semver.NewVersion(pi.Version) + if err != nil { + mi.log.Warn("failed to parse instance version", zap.Error(err)) + continue + } + version = append(version, v) + } + sort.Sort(semver.Collection(version)) + if len(version) < 1 { + return semver.MustParse(extconfig.FullVersion()), nil + } + return version[0], nil +} + +func (mi *Migrator) Run(ctx context.Context) (*storage.Client, error) { + cv, err := mi.GetClusterVersion() + if err != nil { + return nil, err + } + mi.log.Debug("Checking migrations to activate for cluster version", zap.String("clusterVersion", cv.String())) + cli := mi.ri.KV() + for _, m := range mi.migrations { + mi.log.Debug("Checking if migration needs to be run", zap.String("migration", m.Name())) + enabled, err := m.Check(cv, ctx) + if err != nil { + mi.log.Warn("failed to check if migration should be enabled", zap.String("migration", m.Name()), zap.Error(err)) + return nil, err + } + if enabled { + _cli, err := m.Hook(ctx) + if err != nil { + mi.log.Warn("failed to hook for migration", zap.String("migration", m.Name()), zap.Error(err)) + return nil, err + } + mi.log.Info("Enabling migration", zap.String("migration", m.Name())) + cli = _cli + } else { + mi.log.Info("Running cleanup for migration", zap.String("migration", m.Name())) + err := m.Cleanup(ctx) + if err != nil { + mi.log.Warn("failed to cleanup migration", zap.String("migration", m.Name()), zap.Error(err)) + continue + } + } + } + return cli, nil +} + +func (mi *Migrator) AddMigration(migration roles.Migration) { + mi.migrations = append(mi.migrations, migration) +} diff --git a/pkg/instance/migrate/migrate_test.go b/pkg/instance/migrate/migrate_test.go new file mode 100644 index 000000000..b5f6b3517 --- /dev/null +++ b/pkg/instance/migrate/migrate_test.go @@ -0,0 +1,143 @@ +package migrate_test + +import ( + "context" + "testing" + + "beryju.io/gravity/pkg/instance" + "beryju.io/gravity/pkg/instance/migrate" + "beryju.io/gravity/pkg/instance/types" + "beryju.io/gravity/pkg/storage" + "beryju.io/gravity/pkg/tests" + "github.com/stretchr/testify/assert" + clientv3 "go.etcd.io/etcd/client/v3" +) + +func TestMigrate_ClusterVersion(t *testing.T) { + defer tests.Setup(t)() + rootInst := instance.New() + ctx := tests.Context() + ri := rootInst.ForRole("migrate", ctx) + + _, err := ri.KV().Put( + ctx, + ri.KV().Key(types.KeyInstance, "foo").String(), + `{"version":"0.1.0"}`, + ) + assert.NoError(t, err) + _, err = ri.KV().Put( + ctx, + ri.KV().Key(types.KeyInstance, "bar").String(), + `{"version":"0.15.0+foo"}`, + ) + assert.NoError(t, err) + // Invalid JSON + _, err = ri.KV().Put( + ctx, + ri.KV().Key(types.KeyInstance, "baz").String(), + `{`, + ) + assert.NoError(t, err) + // Invalid Version + _, err = ri.KV().Put( + ctx, + ri.KV().Key(types.KeyInstance, "baz").String(), + `{"version":"0.15.0++foo"}`, + ) + assert.NoError(t, err) + + ct := 0 + ri.Migrator().AddMigration(&migrate.InlineMigration{ + MigrationName: "test", + ActivateOnVersion: migrate.MustParseConstraint("< 0.14.0"), + HookFunc: func(ctx context.Context) (*storage.Client, error) { + ct = 1 + return ri.KV(), nil + }, + }) + _, err = ri.Migrator().Run(ctx) + assert.NoError(t, err) + assert.Equal(t, 1, ct) +} + +func TestMigrate(t *testing.T) { + defer tests.Setup(t)() + rootInst := instance.New() + ctx := tests.Context() + ri := rootInst.ForRole("migrate", ctx) + ct := 0 + ri.Migrator().AddMigration(&migrate.InlineMigration{ + MigrationName: "test", + ActivateOnVersion: migrate.MustParseConstraint("> 0.0.0"), + HookFunc: func(ctx context.Context) (*storage.Client, error) { + ct = 1 + return ri.KV(), nil + }, + }) + _, err := ri.Migrator().Run(ctx) + assert.NoError(t, err) + _, err = ri.KV().Put( + ctx, + ri.KV().Key("foo").String(), + "bar", + ) + assert.NoError(t, err) + tests.AssertEtcd(t, ri.KV(), ri.KV().Key("foo"), "bar") + assert.Equal(t, 1, ct) +} + +func TestMigrate_Hook(t *testing.T) { + defer tests.Setup(t)() + rootInst := instance.New() + ctx := tests.Context() + ri := rootInst.ForRole("migrate", ctx) + ct := 0 + ri.Migrator().AddMigration(&migrate.InlineMigration{ + MigrationName: "test", + ActivateOnVersion: migrate.MustParseConstraint("> 0.0.0"), + HookFunc: func(ctx context.Context) (*storage.Client, error) { + return ri.KV().WithHooks(storage.StorageHook{ + Get: func(ctx context.Context, key string, opts ...clientv3.OpOption) error { + ct += 1 + return nil + }, + Put: func(ctx context.Context, key, val string, opts ...clientv3.OpOption) error { + ct += 1 + return nil + }, + }), nil + }, + }) + kv, err := ri.Migrator().Run(ctx) + assert.NoError(t, err) + _, err = kv.Put( + ctx, + kv.Key("foo").String(), + "bar", + ) + assert.NoError(t, err) + tests.AssertEtcd(t, kv, ri.KV().Key("foo"), "bar") + assert.Equal(t, 2, ct) +} + +func TestMigrate_Cleanup(t *testing.T) { + defer tests.Setup(t)() + rootInst := instance.New() + ctx := tests.Context() + ri := rootInst.ForRole("migrate", ctx) + ct := 0 + ri.Migrator().AddMigration(&migrate.InlineMigration{ + MigrationName: "test", + ActivateOnVersion: migrate.MustParseConstraint("< 0.1.0"), + HookFunc: func(ctx context.Context) (*storage.Client, error) { + return ri.KV(), nil + }, + CleanupFunc: func(ctx context.Context) error { + ct += 1 + return nil + }, + }) + _, err := ri.Migrator().Run(ctx) + assert.NoError(t, err) + assert.Equal(t, 1, ct) +} diff --git a/pkg/instance/role_instance.go b/pkg/instance/role_instance.go index df2760604..33d334697 100644 --- a/pkg/instance/role_instance.go +++ b/pkg/instance/role_instance.go @@ -4,30 +4,35 @@ import ( "context" "beryju.io/gravity/pkg/extconfig" + "beryju.io/gravity/pkg/instance/migrate" "beryju.io/gravity/pkg/roles" "beryju.io/gravity/pkg/storage" "go.uber.org/zap" ) type RoleInstance struct { - context context.Context - log *zap.Logger - parent *Instance - roleId string + kv *storage.Client + context context.Context + log *zap.Logger + parent *Instance + roleId string + migrator *migrate.Migrator } func (i *Instance) ForRole(roleId string, ctx context.Context) *RoleInstance { - in := &RoleInstance{ + ri := &RoleInstance{ log: extconfig.Get().Logger().Named("role." + roleId), roleId: roleId, parent: i, context: ctx, + kv: i.kv, } - return in + ri.migrator = migrate.New(ri) + return ri } func (ri *RoleInstance) KV() *storage.Client { - return ri.parent.kv + return ri.kv } func (ri *RoleInstance) Log() *zap.Logger { @@ -38,6 +43,10 @@ func (ri *RoleInstance) Context() context.Context { return ri.context } +func (ri *RoleInstance) Migrator() roles.RoleMigrator { + return ri.migrator +} + func (ri *RoleInstance) DispatchEvent(topic string, ev *roles.Event) { l := ri.log if extconfig.Get().Debug { diff --git a/pkg/instance/types/role.go b/pkg/instance/types/role.go index 4c6014587..a286b4857 100644 --- a/pkg/instance/types/role.go +++ b/pkg/instance/types/role.go @@ -1,8 +1,9 @@ package types const ( - KeyInstance = "instance" - KeyRole = "role" - KeyRoles = "roles" - KeyCluster = "cluster" + KeyInstance = "instance" + KeyRole = "role" + KeyRoles = "roles" + KeyCluster = "cluster" + KeyMigration = "migration" ) diff --git a/pkg/roles/role.go b/pkg/roles/role.go index 15e8c92ec..35ff58a1e 100644 --- a/pkg/roles/role.go +++ b/pkg/roles/role.go @@ -5,6 +5,7 @@ import ( "errors" "beryju.io/gravity/pkg/storage" + "github.com/Masterminds/semver/v3" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" ) @@ -55,6 +56,18 @@ type HookOptions struct { Env map[string]interface{} } +type Migration interface { + Check(clusterVersion *semver.Version, ctx context.Context) (bool, error) + Hook(context.Context) (*storage.Client, error) + Cleanup(context.Context) error + Name() string +} + +type RoleMigrator interface { + AddMigration(Migration) + Run(ctx context.Context) (*storage.Client, error) +} + type Instance interface { KV() *storage.Client Log() *zap.Logger @@ -62,4 +75,5 @@ type Instance interface { AddEventListener(topic string, handler EventHandler) Context() context.Context ExecuteHook(HookOptions, ...interface{}) + Migrator() RoleMigrator } diff --git a/pkg/storage/client.go b/pkg/storage/client.go index a5803d58a..73c967875 100644 --- a/pkg/storage/client.go +++ b/pkg/storage/client.go @@ -4,19 +4,27 @@ import ( "context" "time" - "github.com/getsentry/sentry-go" + "beryju.io/gravity/pkg/storage/trace" "go.uber.org/zap" clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/client/v3/namespace" ) +type StorageHook struct { + Get func(ctx context.Context, key string, opts ...clientv3.OpOption) error + Put func(ctx context.Context, key string, val string, opts ...clientv3.OpOption) error + Delete func(ctx context.Context, key string, opts ...clientv3.OpOption) error +} + type Client struct { *clientv3.Client log *zap.Logger config clientv3.Config prefix string debug bool + hooks []StorageHook + parent *Client } func NewClient(prefix string, logger *zap.Logger, debug bool, endpoints ...string) *Client { @@ -31,7 +39,11 @@ func NewClient(prefix string, logger *zap.Logger, debug bool, endpoints ...strin if err != nil { logger.Panic("failed to setup etcd client", zap.Error(err)) } - cli.KV = namespace.NewKV(cli.KV, prefix) + cli.KV = trace.NewKV(namespace.NewKV(cli.KV, prefix), func(op clientv3.Op) { + if debug { + logger.Warn("etcd op without transaction", zap.String("key", string(op.KeyBytes())), zap.String("op", trace.NameFromOp(op))) + } + }) cli.Watcher = namespace.NewWatcher(cli.Watcher, prefix) cli.Lease = namespace.NewLease(cli.Lease, prefix) @@ -41,6 +53,7 @@ func NewClient(prefix string, logger *zap.Logger, debug bool, endpoints ...strin prefix: prefix, config: config, debug: debug, + hooks: []StorageHook{}, } } @@ -48,33 +61,53 @@ func (c *Client) Config() clientv3.Config { return c.config } -func (c *Client) trace(ctx context.Context, op string, key string) func() { - tx := sentry.TransactionFromContext(ctx) - if tx == nil { - if c.debug { - c.log.Warn("etcd op without transaction", zap.String("key", key), zap.String("op", op)) - } - return func() {} - } - span := tx.StartChild(op) - span.Description = key - span.SetTag("etcd.key", key) - return func() { - span.Finish() +func (c *Client) WithHooks(hooks ...StorageHook) *Client { + return &Client{ + Client: c.Client, + log: c.log, + prefix: c.prefix, + config: c.config, + debug: c.debug, + hooks: hooks, + parent: c, } } func (c *Client) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) { - defer c.trace(ctx, "etcd.get", key)() + for _, h := range c.hooks { + if h.Get == nil { + continue + } + err := h.Get(ctx, key, opts...) + if err != nil { + return nil, err + } + } return c.Client.Get(ctx, key, opts...) } func (c *Client) Put(ctx context.Context, key string, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) { - defer c.trace(ctx, "etcd.put", key)() + for _, h := range c.hooks { + if h.Put == nil { + continue + } + err := h.Put(ctx, key, val, opts...) + if err != nil { + return nil, err + } + } return c.Client.Put(ctx, key, val, opts...) } func (c *Client) Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) { - defer c.trace(ctx, "etcd.delete", key)() + for _, h := range c.hooks { + if h.Delete == nil { + continue + } + err := h.Delete(ctx, key, opts...) + if err != nil { + return nil, err + } + } return c.Client.Delete(ctx, key, opts...) } diff --git a/pkg/storage/trace/kv.go b/pkg/storage/trace/kv.go new file mode 100644 index 000000000..5d8ef4d87 --- /dev/null +++ b/pkg/storage/trace/kv.go @@ -0,0 +1,60 @@ +package trace + +import ( + "context" + + "github.com/getsentry/sentry-go" + clientv3 "go.etcd.io/etcd/client/v3" +) + +type opWithoutSpan func(op clientv3.Op) + +type traceKV struct { + clientv3.KV + opWithoutSpan opWithoutSpan +} + +func NewKV(c clientv3.KV, opWithoutSpan opWithoutSpan) clientv3.KV { + return traceKV{c, opWithoutSpan} +} + +func NameFromOp(op clientv3.Op) string { + if op.IsGet() { + return "etcd.get" + } else if op.IsPut() { + return "etcd.put" + } else if op.IsDelete() { + return "etcd.delete" + } else { + return "etcd.unknown" + } +} + +func (kv traceKV) trace(ctx context.Context, op clientv3.Op) func() { + tx := sentry.TransactionFromContext(ctx) + if tx == nil { + kv.opWithoutSpan(op) + return func() {} + } + span := tx.StartChild(NameFromOp(op)) + span.Description = string(op.KeyBytes()) + span.SetTag("etcd.key", span.Description) + return func() { + span.Finish() + } +} + +func (kv traceKV) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) { + defer kv.trace(ctx, clientv3.OpGet(key, opts...))() + return kv.KV.Get(ctx, key, opts...) +} + +func (kv traceKV) Put(ctx context.Context, key string, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) { + defer kv.trace(ctx, clientv3.OpPut(key, val, opts...))() + return kv.KV.Put(ctx, key, val, opts...) +} + +func (kv traceKV) Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) { + defer kv.trace(ctx, clientv3.OpDelete(key, opts...))() + return kv.KV.Delete(ctx, key, opts...) +} diff --git a/web/rollup.config.mjs b/web/rollup.config.mjs index 7e0c65f68..c99bf41f1 100644 --- a/web/rollup.config.mjs +++ b/web/rollup.config.mjs @@ -2,7 +2,6 @@ import { codecovRollupPlugin } from "@codecov/rollup-plugin"; import commonjs from "@rollup/plugin-commonjs"; import json from "@rollup/plugin-json"; import { nodeResolve } from "@rollup/plugin-node-resolve"; -import { readFileSync } from "fs"; import copy from "rollup-plugin-copy"; import cssimport from "rollup-plugin-cssimport"; import esbuild from "rollup-plugin-esbuild"; @@ -35,11 +34,8 @@ export const resources = [ export const isProdBuild = process.env.NODE_ENV === "production"; const codecovToken = () => { - try { - return readFileSync("/run/secrets/CODECOV_TOKEN"); - } catch { - return undefined; - } + // eslint-disable-next-line no-undef + return process.env.CODECOV_TOKEN; }; export default { @@ -68,10 +64,7 @@ export default { enableBundleAnalysis: codecovToken() !== undefined, bundleName: "gravity-ui", uploadToken: codecovToken(), - uploadOverrides: { - // eslint-disable-next-line no-undef - sha: process.env.CC_GH_COMMIT_SHA, - }, + gitService: "github", }), ], watch: {