From 64e3aa5a4cccb73749776fc5139daefaa858525a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Wed, 9 Sep 2026 21:02:27 +0700 Subject: [PATCH 01/11] chores: add `.gitattributes` --- .gitattributes | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..67068010 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +go.work.sum -diff +**/go.sum -diff +core/mock/**/*.go -diff +runner-gha/mock/**/*.go -diff +runner-gitea/mock/**/*.go -diff From 70ebfc072fa9d903a890418f13dc53cbdd7b0ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Sat, 5 Sep 2026 07:28:11 +0700 Subject: [PATCH 02/11] core/runtime: implement `runtime.Provider` and `runtime.Runtime` --- core/pkg/runtime/provider.go | 62 ++++++++++++ core/pkg/runtime/provider_test.go | 154 ++++++++++++++++++++++++++++++ core/pkg/runtime/runtime.go | 72 ++++++++++++++ 3 files changed, 288 insertions(+) create mode 100644 core/pkg/runtime/provider.go create mode 100644 core/pkg/runtime/provider_test.go create mode 100644 core/pkg/runtime/runtime.go diff --git a/core/pkg/runtime/provider.go b/core/pkg/runtime/provider.go new file mode 100644 index 00000000..88ae7889 --- /dev/null +++ b/core/pkg/runtime/provider.go @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package runtime + +import ( + "fmt" + + "drassi.run/core/config" + "drassi.run/core/pkg/sandboxer" +) + +type Provider interface { + Get(nameOrAlias string) (Runtime, error) +} + +type provider struct { + runtimes map[string]Runtime + aliases map[string]string +} + +func NewProvider(sandbox sandboxer.Sandbox, runtimeConfigs map[string]*config.Runtime) (Provider, error) { + p := &provider{ + runtimes: make(map[string]Runtime, len(runtimeConfigs)), + aliases: make(map[string]string), + } + + for name, cfg := range runtimeConfigs { + if rt, err := NewRuntime(name, sandbox, cfg); err != nil { + return nil, err + } else { + p.runtimes[name] = rt + } + + for _, alias := range cfg.Alias { + if existing, ok := p.aliases[alias]; ok { + return nil, fmt.Errorf("duplicate runtime alias %q for %q (already defined in %q)", alias, name, existing) + } + if _, ok := runtimeConfigs[alias]; ok { + return nil, fmt.Errorf("runtime alias %q for %q conflicts with existing runtime name", alias, name) + } + p.aliases[alias] = name + } + } + + return p, nil +} + +func (p *provider) Get(nameOrAlias string) (Runtime, error) { + name := nameOrAlias + if n, ok := p.aliases[name]; ok { + name = n + } + if rt, ok := p.runtimes[name]; ok { + return rt, nil + } + + return nil, fmt.Errorf("unsupported runtime %q", nameOrAlias) +} diff --git a/core/pkg/runtime/provider_test.go b/core/pkg/runtime/provider_test.go new file mode 100644 index 00000000..e95bab61 --- /dev/null +++ b/core/pkg/runtime/provider_test.go @@ -0,0 +1,154 @@ +/* + * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package runtime + +import ( + "testing" + + "drassi.run/core/config" + mock_sandboxer "drassi.run/core/mock/sandboxer" + "drassi.run/core/pkg/sandboxer" + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" +) + +func TestProviderSuite(t *testing.T) { + suite.Run(t, new(ProviderTestSuite)) +} + +type ProviderTestSuite struct { + suite.Suite + ctrl *gomock.Controller + mockSb *mock_sandboxer.MockSandbox +} + +func (s *ProviderTestSuite) SetupTest() { + s.ctrl = gomock.NewController(s.T()) + s.mockSb = mock_sandboxer.NewMockSandbox(s.ctrl) + s.mockSb.EXPECT().Layout().Return(&sandboxer.Layout{ + Runtimes: "/opt/drassi/runtimes", + }).AnyTimes() +} + +func (s *ProviderTestSuite) TestGet() { + s.mockSb.EXPECT().Execute( + gomock.Any(), + []string{"/opt/drassi/runtimes/node/bin/node", "/workspace/app.js"}, + []string{"/extra/bin"}, + map[string]string{"NODE_ENV": "production"}, + "/workspace", + nil, + ).Return(nil) + + s.mockSb.EXPECT().Execute( + gomock.Any(), + []string{"/opt/drassi/runtimes/python/python", "/workspace/main.py"}, + nil, + nil, + "/workspace", + nil, + ).Return(nil) + + runtimes := map[string]*config.Runtime{ + "node": { + Image: "drassi/node:24", + Alias: []string{"node20", "node22"}, + Executable: "./bin/node", + Cmd: []string{"{0}"}, + Paths: []string{"bin"}, + }, + "python": { + Image: "drassi/python:3.12", + Alias: []string{"py3", "python3"}, + Executable: "python", + Cmd: nil, + Paths: nil, + }, + } + + p, err := NewProvider(s.mockSb, runtimes) + s.Require().NoError(err) + s.Require().NotNil(p) + + // Direct name + rt, err := p.Get("node") + s.Require().NoError(err) + s.Require().NotNil(rt) + + // Alias name returns same pre-initialized instance + rt20, err := p.Get("node20") + s.Require().NoError(err) + s.Require().Same(rt, rt20) + + // Unknown runtime + _, err = p.Get("unknown-rt") + s.Require().Error(err) + s.Assert().ErrorContains(err, "unsupported runtime") + + // Execute bound runtime + err = rt.Run(s.T().Context(), "/workspace/app.js", []string{"/extra/bin"}, map[string]string{"NODE_ENV": "production"}, "/workspace", nil) + s.Require().NoError(err) + + // Execute bound runtime with empty Cmd (fallback to scriptPath appended) + rtPy, err := p.Get("py3") + s.Require().NoError(err) + s.Require().NotNil(rtPy) + + err = rtPy.Run(s.T().Context(), "/workspace/main.py", nil, nil, "/workspace", nil) + s.Require().NoError(err) +} + +func (s *ProviderTestSuite) TestNew() { + tests := map[string]struct { + runtimes map[string]*config.Runtime + errMsg string + }{ + "duplicate alias": { + runtimes: map[string]*config.Runtime{ + "node": { + Image: "drassi/node:24", + Alias: []string{"node20"}, + }, + "node-alt": { + Image: "drassi/node:24-alt", + Alias: []string{"node20"}, + }, + }, + errMsg: "duplicate runtime alias", + }, + "alias collides with runtime name": { + runtimes: map[string]*config.Runtime{ + "node": { + Image: "drassi/node:24", + }, + "python": { + Image: "drassi/python:3.12", + Alias: []string{"node"}, + }, + }, + errMsg: "conflicts with existing runtime name", + }, + "cmd missing placeholder": { + runtimes: map[string]*config.Runtime{ + "node": { + Image: "drassi/node:24", + Executable: "node", + Cmd: []string{"--version"}, + }, + }, + errMsg: "must contain \"{0}\" placeholder", + }, + } + + for name, tt := range tests { + s.Run(name, func() { + _, err := NewProvider(s.mockSb, tt.runtimes) + s.Require().Error(err) + s.Assert().ErrorContains(err, tt.errMsg) + }) + } +} diff --git a/core/pkg/runtime/runtime.go b/core/pkg/runtime/runtime.go new file mode 100644 index 00000000..53d0b7ef --- /dev/null +++ b/core/pkg/runtime/runtime.go @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package runtime + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "drassi.run/core/config" + "drassi.run/core/pkg/sandboxer" + "drassi.run/core/pkg/stream" +) + +type Runtime interface { + Run(ctx context.Context, scriptPath string, paths []string, env map[string]string, workdir string, streams *stream.Streams) error +} + +type runtime struct { + sandbox sandboxer.Sandbox + cmd []string +} + +func NewRuntime(name string, sandbox sandboxer.Sandbox, cfg *config.Runtime) (Runtime, error) { + layout := sandbox.Layout() + binPath := filepath.Join(layout.Runtimes, name, cfg.Executable) + + cmd := make([]string, 0, len(cfg.Cmd)+1) + cmd = append(cmd, binPath) + if len(cfg.Cmd) > 0 { + var hasPlaceholder bool + for _, arg := range cfg.Cmd { + if strings.Contains(arg, "{0}") { + hasPlaceholder = true + break + } + } + if !hasPlaceholder { + return nil, fmt.Errorf("runtime %q cmd must contain \"{0}\" placeholder", name) + } + cmd = append(cmd, cfg.Cmd...) + } else { + cmd = append(cmd, "{0}") + } + + rt := &runtime{ + sandbox: sandbox, + cmd: cmd, + } + return rt, nil +} + +func (r *runtime) Run( + ctx context.Context, + scriptPath string, + paths []string, + env map[string]string, + workdir string, + streams *stream.Streams, +) error { + cmd := make([]string, len(r.cmd)) + for i, arg := range r.cmd { + cmd[i] = strings.ReplaceAll(arg, "{0}", scriptPath) + } + + return r.sandbox.Execute(ctx, cmd, paths, env, workdir, streams) +} From 9905a25cc6c3f7e213343b3d26971ca109c1e3eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Sat, 5 Sep 2026 07:28:11 +0700 Subject: [PATCH 03/11] core/executor: inject `runtime.Runtime` into node action --- core/pkg/executor/action_node.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/core/pkg/executor/action_node.go b/core/pkg/executor/action_node.go index 6ae65220..c3f0277f 100644 --- a/core/pkg/executor/action_node.go +++ b/core/pkg/executor/action_node.go @@ -13,9 +13,11 @@ import ( "strings" "drassi.run/core/pkg/model/workflows" + "drassi.run/core/pkg/runtime" "drassi.run/core/pkg/sandboxer" "drassi.run/core/pkg/scribe" "drassi.run/core/pkg/store/git" + "drassi.run/core/util/dig" "drassi.run/core/util/otel" "go.opentelemetry.io/otel/trace" "go.uber.org/dig" @@ -40,12 +42,32 @@ func (spec *NodeActionSpec) CreateExecutor( ctx context.Context, scope *dig.Scope, exec StepExecutor, ) (ActionExecutor, error) { e := &nodeActionExecutor{spec: spec, sExec: exec} + if err := e.init(ctx, scope); err != nil { + return nil, err + } return e, nil } type nodeActionExecutor struct { spec *NodeActionSpec sExec StepExecutor + + // injected values + runtime runtime.Runtime +} + +func (e *nodeActionExecutor) init(ctx context.Context, scope *dig.Scope) error { + var provider runtime.Provider + if err := xdig.Populate(scope, &provider); err != nil { + return err + } + + if rt, err := provider.Get(e.spec.Runtime); err != nil { + return err + } else { + e.runtime = rt + } + return nil } func (e *nodeActionExecutor) ActionSpec() ActionSpec { @@ -106,7 +128,6 @@ func (e *nodeActionExecutor) execute(stage Stage) ActionRun { sandbox := e.sExec.Sandbox() scriptPath := e.computeScriptPath(sandbox.Layout(), stage) - cmd := []string{"node", scriptPath} inputs := e.sExec.Inputs() scribe.GroupDetails(ctx, "Run "+e.repr(), @@ -123,7 +144,7 @@ func (e *nodeActionExecutor) execute(stage Stage) ActionRun { paths := e.sExec.JobExecutor().Path() streams := e.sExec.Streams(ctx, stage) defer streams.Close() - return sandbox.Execute(ctx, cmd, paths, env, "", streams) + return e.runtime.Run(ctx, scriptPath, paths, env, "", streams) } return runActionE(fn) } From f2bbe67311a7884c1416986eee5f7a3a9c5a4994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Sat, 5 Sep 2026 07:28:11 +0700 Subject: [PATCH 04/11] core/wire: add `runtime.Provider` to module --- core/wire/runtime/module.go | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/core/wire/runtime/module.go b/core/wire/runtime/module.go index 5aa407d0..25916d67 100644 --- a/core/wire/runtime/module.go +++ b/core/wire/runtime/module.go @@ -9,16 +9,49 @@ package wire_runtime import ( "fmt" + "drassi.run/core/config" + "drassi.run/core/pkg/runtime" + "drassi.run/core/pkg/sandboxer" + "drassi.run/core/util/dig" "drassi.run/core/wire" "go.uber.org/dig" ) -func Module() *wire.Module { +type Option func(o *options) + +type options struct { + runtimeConfigs map[string]*config.Runtime +} + +func WithRuntimes(cfg map[string]*config.Runtime) Option { + return func(o *options) { + o.runtimeConfigs = cfg + } +} + +func Module(opts ...Option) *wire.Module { + o := &options{} + for _, opt := range opts { + opt(o) + } + fn := func(scope *dig.Scope) error { + if o.runtimeConfigs != nil { + if err := xdig.Supply(scope, o.runtimeConfigs); err != nil { + return fmt.Errorf("supply runtimes: %w", err) + } + } if err := scope.Provide(NewContainerRuntime); err != nil { return fmt.Errorf("provide runtime.Container: %w", err) } + if err := scope.Provide(o.newProvider); err != nil { + return fmt.Errorf("provide runtime.Provider: %w", err) + } return nil } return wire.NewModule("core/runtime", fn) } + +func (o *options) newProvider(sb sandboxer.Sandbox) (runtime.Provider, error) { + return runtime.NewProvider(sb, o.runtimeConfigs) +} From ad8609b33391c4046dacc2dc412e32f557b226df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Wed, 9 Sep 2026 22:01:30 +0700 Subject: [PATCH 05/11] core/runtime: add `Name` method for display purpose --- core/pkg/executor/action_node.go | 2 +- core/pkg/runtime/runtime.go | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/pkg/executor/action_node.go b/core/pkg/executor/action_node.go index c3f0277f..b243d256 100644 --- a/core/pkg/executor/action_node.go +++ b/core/pkg/executor/action_node.go @@ -181,5 +181,5 @@ func (e *nodeActionExecutor) addSpanAttrs(ctx context.Context, stage Stage) { } func (e *nodeActionExecutor) repr() string { - return fmt.Sprintf("node action from %q", gitstore.Location(e.spec.Repo)) + return fmt.Sprintf("%s action from %q", e.runtime.Name(), gitstore.Location(e.spec.Repo)) } diff --git a/core/pkg/runtime/runtime.go b/core/pkg/runtime/runtime.go index 53d0b7ef..9fd88b71 100644 --- a/core/pkg/runtime/runtime.go +++ b/core/pkg/runtime/runtime.go @@ -18,12 +18,14 @@ import ( ) type Runtime interface { + Name() string Run(ctx context.Context, scriptPath string, paths []string, env map[string]string, workdir string, streams *stream.Streams) error } type runtime struct { - sandbox sandboxer.Sandbox + name string cmd []string + sandbox sandboxer.Sandbox } func NewRuntime(name string, sandbox sandboxer.Sandbox, cfg *config.Runtime) (Runtime, error) { @@ -49,12 +51,17 @@ func NewRuntime(name string, sandbox sandboxer.Sandbox, cfg *config.Runtime) (Ru } rt := &runtime{ - sandbox: sandbox, + name: name, cmd: cmd, + sandbox: sandbox, } return rt, nil } +func (r *runtime) Name() string { + return r.name +} + func (r *runtime) Run( ctx context.Context, scriptPath string, From d7a2902b6cd100228fbf807489ccab1dab464e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Wed, 9 Sep 2026 22:04:39 +0700 Subject: [PATCH 06/11] core/mock: add Runtime mocks --- core/mock/gen.go | 2 + core/mock/runtime/provider.go | 80 +++++++++++++++++++++++ core/mock/runtime/runtime.go | 118 ++++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 core/mock/runtime/provider.go create mode 100644 core/mock/runtime/runtime.go diff --git a/core/mock/gen.go b/core/mock/gen.go index 23882997..93ce3568 100644 --- a/core/mock/gen.go +++ b/core/mock/gen.go @@ -18,4 +18,6 @@ //go:generate mockgen -typed -destination=stream/sink.go -source=../pkg/stream/sink.go //go:generate mockgen -typed -destination=store/git/manager.go -source=../pkg/store/git/manager.go //go:generate mockgen -typed -destination=store/oci/manager.go -source=../pkg/store/oci/manager.go +//go:generate mockgen -typed -destination=runtime/provider.go -source=../pkg/runtime/provider.go +//go:generate mockgen -typed -destination=runtime/runtime.go -source=../pkg/runtime/runtime.go package mock diff --git a/core/mock/runtime/provider.go b/core/mock/runtime/provider.go new file mode 100644 index 00000000..ef1a6c06 --- /dev/null +++ b/core/mock/runtime/provider.go @@ -0,0 +1,80 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ../pkg/runtime/provider.go +// +// Generated by this command: +// +// mockgen -typed -destination=runtime/provider.go -source=../pkg/runtime/provider.go +// + +// Package mock_runtime is a generated GoMock package. +package mock_runtime + +import ( + reflect "reflect" + + runtime "drassi.run/core/pkg/runtime" + gomock "go.uber.org/mock/gomock" +) + +// MockProvider is a mock of Provider interface. +type MockProvider struct { + ctrl *gomock.Controller + recorder *MockProviderMockRecorder + isgomock struct{} +} + +// MockProviderMockRecorder is the mock recorder for MockProvider. +type MockProviderMockRecorder struct { + mock *MockProvider +} + +// NewMockProvider creates a new mock instance. +func NewMockProvider(ctrl *gomock.Controller) *MockProvider { + mock := &MockProvider{ctrl: ctrl} + mock.recorder = &MockProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProvider) EXPECT() *MockProviderMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockProvider) Get(nameOrAlias string) (runtime.Runtime, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", nameOrAlias) + ret0, _ := ret[0].(runtime.Runtime) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockProviderMockRecorder) Get(nameOrAlias any) *MockProviderGetCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockProvider)(nil).Get), nameOrAlias) + return &MockProviderGetCall{Call: call} +} + +// MockProviderGetCall wrap *gomock.Call +type MockProviderGetCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockProviderGetCall) Return(arg0 runtime.Runtime, arg1 error) *MockProviderGetCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockProviderGetCall) Do(f func(string) (runtime.Runtime, error)) *MockProviderGetCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockProviderGetCall) DoAndReturn(f func(string) (runtime.Runtime, error)) *MockProviderGetCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/core/mock/runtime/runtime.go b/core/mock/runtime/runtime.go new file mode 100644 index 00000000..8c927109 --- /dev/null +++ b/core/mock/runtime/runtime.go @@ -0,0 +1,118 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ../pkg/runtime/runtime.go +// +// Generated by this command: +// +// mockgen -typed -destination=runtime/runtime.go -source=../pkg/runtime/runtime.go +// + +// Package mock_runtime is a generated GoMock package. +package mock_runtime + +import ( + context "context" + reflect "reflect" + + stream "drassi.run/core/pkg/stream" + gomock "go.uber.org/mock/gomock" +) + +// MockRuntime is a mock of Runtime interface. +type MockRuntime struct { + ctrl *gomock.Controller + recorder *MockRuntimeMockRecorder + isgomock struct{} +} + +// MockRuntimeMockRecorder is the mock recorder for MockRuntime. +type MockRuntimeMockRecorder struct { + mock *MockRuntime +} + +// NewMockRuntime creates a new mock instance. +func NewMockRuntime(ctrl *gomock.Controller) *MockRuntime { + mock := &MockRuntime{ctrl: ctrl} + mock.recorder = &MockRuntimeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRuntime) EXPECT() *MockRuntimeMockRecorder { + return m.recorder +} + +// Name mocks base method. +func (m *MockRuntime) Name() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(string) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockRuntimeMockRecorder) Name() *MockRuntimeNameCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockRuntime)(nil).Name)) + return &MockRuntimeNameCall{Call: call} +} + +// MockRuntimeNameCall wrap *gomock.Call +type MockRuntimeNameCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockRuntimeNameCall) Return(arg0 string) *MockRuntimeNameCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockRuntimeNameCall) Do(f func() string) *MockRuntimeNameCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockRuntimeNameCall) DoAndReturn(f func() string) *MockRuntimeNameCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Run mocks base method. +func (m *MockRuntime) Run(ctx context.Context, scriptPath string, paths []string, env map[string]string, workdir string, streams *stream.Streams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Run", ctx, scriptPath, paths, env, workdir, streams) + ret0, _ := ret[0].(error) + return ret0 +} + +// Run indicates an expected call of Run. +func (mr *MockRuntimeMockRecorder) Run(ctx, scriptPath, paths, env, workdir, streams any) *MockRuntimeRunCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockRuntime)(nil).Run), ctx, scriptPath, paths, env, workdir, streams) + return &MockRuntimeRunCall{Call: call} +} + +// MockRuntimeRunCall wrap *gomock.Call +type MockRuntimeRunCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockRuntimeRunCall) Return(arg0 error) *MockRuntimeRunCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockRuntimeRunCall) Do(f func(context.Context, string, []string, map[string]string, string, *stream.Streams) error) *MockRuntimeRunCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockRuntimeRunCall) DoAndReturn(f func(context.Context, string, []string, map[string]string, string, *stream.Streams) error) *MockRuntimeRunCall { + c.Call = c.Call.DoAndReturn(f) + return c +} From d26c2328c1543d1d4d1a3ccb6a30679ee25e25d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Thu, 10 Sep 2026 00:53:57 +0700 Subject: [PATCH 07/11] gha: propagate config --- runner-gha/cmd/launch/launch.go | 2 +- runner-gha/pkg/worker/manager.go | 11 +++++++---- runner-gha/pkg/worker/worker.go | 8 +++++--- runner-gha/wire/synthetic.go | 12 ++++++++++-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/runner-gha/cmd/launch/launch.go b/runner-gha/cmd/launch/launch.go index 591dca02..50b35581 100644 --- a/runner-gha/cmd/launch/launch.go +++ b/runner-gha/cmd/launch/launch.go @@ -111,7 +111,7 @@ func (l *launcher) Init(ctx context.Context, opts *options) (err error) { } src := config.TokenSource(ctx) l.hc = oauth2.NewClient(ctx, src) - l.wm = worker.NewManager() + l.wm = worker.NewManager(cfg) if s, err := gitstore.New(".cache"); err != nil { return err diff --git a/runner-gha/pkg/worker/manager.go b/runner-gha/pkg/worker/manager.go index 175c58c6..d5ac8d9a 100644 --- a/runner-gha/pkg/worker/manager.go +++ b/runner-gha/pkg/worker/manager.go @@ -15,6 +15,7 @@ import ( "drassi.run/core/util/context" "drassi.run/core/wire" + ghaconfig "drassi.run/gha-runner/config" "drassi.run/gha-runner/pkg/messages" "github.com/chainguard-dev/clog" ) @@ -27,15 +28,17 @@ type flight struct { DoneCh chan struct{} } -func NewManager() *Manager { +func NewManager(cfg *ghaconfig.Config) *Manager { return &Manager{ + cfg: cfg, inflight: make(map[string]*flight), } } type Manager struct { - mu sync.Mutex - wg sync.WaitGroup + mu sync.Mutex + wg sync.WaitGroup + cfg *ghaconfig.Config inflight map[string]*flight } @@ -48,7 +51,7 @@ func (m *Manager) Submit(req *messages.PipelineAgentJobRequest, modules ...*wire ctx, cancel := context.WithCancelCause(ctx) f := &flight{ JobId: req.JobId, - Worker: NewWorker(req), + Worker: NewWorker(m.cfg, req), Cancel: cancel, Context: ctx, DoneCh: make(chan struct{}), diff --git a/runner-gha/pkg/worker/worker.go b/runner-gha/pkg/worker/worker.go index a185c273..d46cd02c 100644 --- a/runner-gha/pkg/worker/worker.go +++ b/runner-gha/pkg/worker/worker.go @@ -21,6 +21,7 @@ import ( "drassi.run/core/util/error" "drassi.run/core/util/otel" "drassi.run/core/wire" + ghaconfig "drassi.run/gha-runner/config" "drassi.run/gha-runner/pkg/lease" "drassi.run/gha-runner/pkg/log" "drassi.run/gha-runner/pkg/log/logtypes" @@ -31,12 +32,13 @@ import ( "go.uber.org/dig" ) -func NewWorker(msg *messages.PipelineAgentJobRequest) *Worker { - return &Worker{msg: msg} +func NewWorker(cfg *ghaconfig.Config, msg *messages.PipelineAgentJobRequest) *Worker { + return &Worker{msg: msg, cfg: cfg} } type Worker struct { msg *messages.PipelineAgentJobRequest + cfg *ghaconfig.Config lease lease.Lease timelineMgr *timeline.Manager @@ -51,7 +53,7 @@ func (w *Worker) Context() context.Context { func (w *Worker) Run(ctx context.Context, modules ...*wire.Module) (err error) { scope := dig.New().Scope("worker") - if err = gha_wire.Synthetic(scope, w.msg, modules...); err != nil { + if err = gha_wire.Synthetic(scope, w.cfg, w.msg, modules...); err != nil { return } diff --git a/runner-gha/wire/synthetic.go b/runner-gha/wire/synthetic.go index d9f8da2a..58df4475 100644 --- a/runner-gha/wire/synthetic.go +++ b/runner-gha/wire/synthetic.go @@ -18,6 +18,7 @@ import ( wire_scribe "drassi.run/core/wire/scribe" wire_secret "drassi.run/core/wire/secret" wire_stream "drassi.run/core/wire/stream" + ghaconfig "drassi.run/gha-runner/config" "drassi.run/gha-runner/pkg/messages" wire_core "drassi.run/gha-runner/wire/core" wire_lease "drassi.run/gha-runner/wire/lease" @@ -27,7 +28,12 @@ import ( "go.uber.org/dig" ) -func Synthetic(scope *dig.Scope, msg *messages.PipelineAgentJobRequest, extras ...*wire.Module) error { +func Synthetic( + scope *dig.Scope, + cfg *ghaconfig.Config, + msg *messages.PipelineAgentJobRequest, + extras ...*wire.Module, +) error { modules := make([]*wire.Module, 0, 4) // core modules @@ -39,7 +45,9 @@ func Synthetic(scope *dig.Scope, msg *messages.PipelineAgentJobRequest, extras . wire_command.UseDiscardIssueReporter(false), // use [command.IssueReporter] instead wire_command.UseBlackHoleAttachmentUploader(false), // use [command.xServiceAttacher] instead )) - modules = append(modules, wire_runtime.Module()) + modules = append(modules, wire_runtime.Module( + wire_runtime.WithRuntimes(cfg.Runtimes), + )) modules = append(modules, wire_scribe.Module()) modules = append(modules, wire_secret.Module()) modules = append(modules, wire_stream.Module()) From 4c78c77d51c1fb9135882d77c66eae73ac326fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Thu, 10 Sep 2026 00:56:06 +0700 Subject: [PATCH 08/11] gitea: propagate config --- runner-gitea/cmd/launch/launch.go | 4 +++- runner-gitea/pkg/worker/worker.go | 8 +++++--- runner-gitea/wire/synthetic.go | 12 ++++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/runner-gitea/cmd/launch/launch.go b/runner-gitea/cmd/launch/launch.go index b9343376..436f6316 100644 --- a/runner-gitea/cmd/launch/launch.go +++ b/runner-gitea/cmd/launch/launch.go @@ -31,6 +31,7 @@ import ( ) type launcher struct { + cfg *giteaconfig.Config runnerName string concurrency int client gitea.Client @@ -72,6 +73,7 @@ func (c *launcher) Init(ctx context.Context, o *options) error { if err != nil { return err } + c.cfg = config spec := config.Runner c.runnerName = spec.Name @@ -173,7 +175,7 @@ func (c *launcher) runTask(ctx context.Context, task *runnerv1.Task) { } func (c *launcher) runTaskE(ctx context.Context, task *runnerv1.Task) error { - w := worker.New(task) + w := worker.New(c.cfg, task) return w.Run(ctx, c.module()) } diff --git a/runner-gitea/pkg/worker/worker.go b/runner-gitea/pkg/worker/worker.go index 75006c55..0d342e6b 100644 --- a/runner-gitea/pkg/worker/worker.go +++ b/runner-gitea/pkg/worker/worker.go @@ -19,6 +19,7 @@ import ( "drassi.run/core/util/error" "drassi.run/core/util/otel" "drassi.run/core/wire" + giteaconfig "drassi.run/gitea-runner/config" "drassi.run/gitea-runner/pkg/reporter" gitea_wire "drassi.run/gitea-runner/wire" runnerv1 "gitea.dev/actionslib/runner/v1" @@ -27,13 +28,14 @@ import ( ) type Worker struct { + cfg *giteaconfig.Config task *runnerv1.Task ctx context.Context cancel context.CancelCauseFunc } -func New(task *runnerv1.Task) *Worker { - return &Worker{task: task} +func New(cfg *giteaconfig.Config, task *runnerv1.Task) *Worker { + return &Worker{cfg: cfg, task: task} } func (w *Worker) Context() context.Context { @@ -42,7 +44,7 @@ func (w *Worker) Context() context.Context { func (w *Worker) Run(ctx context.Context, modules ...*wire.Module) (err error) { scope := dig.New().Scope("worker") - if err = gitea_wire.Synthetic(scope, w.task, modules...); err != nil { + if err = gitea_wire.Synthetic(scope, w.cfg, w.task, modules...); err != nil { return } diff --git a/runner-gitea/wire/synthetic.go b/runner-gitea/wire/synthetic.go index 0c8063d6..29569b71 100644 --- a/runner-gitea/wire/synthetic.go +++ b/runner-gitea/wire/synthetic.go @@ -18,13 +18,19 @@ import ( wire_scribe "drassi.run/core/wire/scribe" wire_secret "drassi.run/core/wire/secret" wire_stream "drassi.run/core/wire/stream" + giteaconfig "drassi.run/gitea-runner/config" wire_core "drassi.run/gitea-runner/wire/core" wire_reporter "drassi.run/gitea-runner/wire/reporter" runnerv1 "gitea.dev/actionslib/runner/v1" "go.uber.org/dig" ) -func Synthetic(scope *dig.Scope, task *runnerv1.Task, extras ...*wire.Module) error { +func Synthetic( + scope *dig.Scope, + cfg *giteaconfig.Config, + task *runnerv1.Task, + extras ...*wire.Module, +) error { modules := make([]*wire.Module, 0, 4) // core modules @@ -34,7 +40,9 @@ func Synthetic(scope *dig.Scope, task *runnerv1.Task, extras ...*wire.Module) er ), )) modules = append(modules, wire_command.Module()) - modules = append(modules, wire_runtime.Module()) + modules = append(modules, wire_runtime.Module( + wire_runtime.WithRuntimes(cfg.Runtimes), + )) modules = append(modules, wire_scribe.Module()) modules = append(modules, wire_secret.Module()) modules = append(modules, wire_stream.Module( From 4fb26d27770bf21bea116135c157bdf615882377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Thu, 10 Sep 2026 01:11:52 +0700 Subject: [PATCH 09/11] core/sandboxer: revamp `sandboxer.Factory` --- core/pkg/sandboxer/container/engine.go | 55 +++++++++++++++-------- core/pkg/sandboxer/factory.go | 49 ++++++++++++++++++--- core/pkg/sandboxer/host/engine.go | 51 ++++++++++++++------- core/pkg/sandboxer/incus/engine.go | 61 +++++++++++++++++--------- runner-gha/cmd/launch/launch.go | 4 +- runner-gha/cmd/migrate/migrate_test.go | 4 +- runner-gitea/cmd/launch/launch.go | 6 ++- 7 files changed, 164 insertions(+), 66 deletions(-) diff --git a/core/pkg/sandboxer/container/engine.go b/core/pkg/sandboxer/container/engine.go index 9af45f2e..0f3eb77b 100644 --- a/core/pkg/sandboxer/container/engine.go +++ b/core/pkg/sandboxer/container/engine.go @@ -12,6 +12,7 @@ import ( "maps" "strconv" "strings" + "sync" "drassi.run/core/config" "drassi.run/core/pkg/container" @@ -21,24 +22,49 @@ import ( "drassi.run/core/pkg/model/records" "drassi.run/core/pkg/model/workflows" "drassi.run/core/pkg/sandboxer" + "drassi.run/core/pkg/store/oci" "drassi.run/core/pkg/stream" "drassi.run/core/util/string" dockerclient "github.com/moby/moby/client" - "github.com/pelletier/go-toml/v2" - "github.com/pelletier/go-toml/v2/unstable" "golang.org/x/sync/errgroup" ) func init() { - sandboxer.Register(config.ProviderContainer, func(raw unstable.RawMessage) (sandboxer.Engine, error) { - cfg := DefaultConfig() - if len(raw) > 0 { - if err := toml.Unmarshal(raw, cfg); err != nil { - return nil, err - } - } - return New(cfg) - }) + sandboxer.Register(config.ProviderContainer, DefaultConfig, NewFactory) +} + +func DefaultConfig() *Config { + return &Config{ + Implementation: "docker", + Image: "ghcr.io/drassi-run/ubuntu:26.04", + } +} + +func NewFactory(cfg *Config) sandboxer.Factory { + f := &factory{cfg: cfg} + f.create = sync.OnceValues(f.doCreate) + return f +} + +type factory struct { + create func() (sandboxer.Engine, error) + + cfg *Config + store ocistore.Manager + runtimes map[string]*config.Runtime +} + +func (f *factory) ProvisionRuntime(store ocistore.Manager, config map[string]*config.Runtime) { + f.store = store + f.runtimes = config +} + +func (f *factory) Create() (sandboxer.Engine, error) { + return f.create() +} + +func (f *factory) doCreate() (sandboxer.Engine, error) { + return New(f.cfg) } type Bootstrapper interface { @@ -51,13 +77,6 @@ type Config struct { Image string `toml:"image" json:"image,omitempty"` } -func DefaultConfig() *Config { - return &Config{ - Implementation: "docker", - Image: "ghcr.io/drassi-run/ubuntu:26.04", - } -} - type engine struct { client container.Engine defaultImage string diff --git a/core/pkg/sandboxer/factory.go b/core/pkg/sandboxer/factory.go index 6f1d4b0b..2c10ecd8 100644 --- a/core/pkg/sandboxer/factory.go +++ b/core/pkg/sandboxer/factory.go @@ -12,29 +12,64 @@ import ( "sync" "drassi.run/core/config" + "drassi.run/core/pkg/store/oci" + "github.com/pelletier/go-toml/v2" "github.com/pelletier/go-toml/v2/unstable" ) -type Factory func(cfg unstable.RawMessage) (Engine, error) +type Factory interface { + // SupportContainer(config) // TODO + + ProvisionRuntime(store ocistore.Manager, config map[string]*config.Runtime) + Create() (Engine, error) +} var ( mu sync.RWMutex - factories = make(map[string]Factory) + defaults = make(map[string]func() any) + factories = make(map[string]func(cfg unstable.RawMessage) (Factory, error)) ) -func Register(provider string, factory Factory) { +func Register[T any](provider string, d func() T, fn func(cfg T) Factory) { + provider = strings.ToLower(provider) mu.Lock() defer mu.Unlock() - factories[strings.ToLower(provider)] = factory + + defaults[provider] = func() any { + return d() + } + factories[provider] = func(raw unstable.RawMessage) (Factory, error) { + cfg := d() + if len(raw) > 0 { + if err := toml.Unmarshal(raw, cfg); err != nil { + return nil, fmt.Errorf("unmarshal provider %q config: %v", provider, err) + } + } + + return fn(cfg), nil + } } -func NewEngine(config *config.Sandboxer) (Engine, error) { +func NewFactory(config *config.Sandboxer) (Factory, error) { provider := strings.ToLower(config.Provider) mu.RLock() - factory, ok := factories[provider] + fn, ok := factories[provider] mu.RUnlock() + if !ok { return nil, fmt.Errorf("unsupported sandboxer provider %q", config.Provider) } - return factory(config.Config) + return fn(config.Config) +} + +func DefaultConfig(provider string) any { + provider = strings.ToLower(provider) + mu.RLock() + fn, ok := defaults[provider] + mu.RUnlock() + + if !ok { + return nil + } + return fn() } diff --git a/core/pkg/sandboxer/host/engine.go b/core/pkg/sandboxer/host/engine.go index e5525085..9f849cbc 100644 --- a/core/pkg/sandboxer/host/engine.go +++ b/core/pkg/sandboxer/host/engine.go @@ -12,34 +12,21 @@ import ( "os" "path/filepath" "strings" + "sync" "drassi.run/core/config" c "drassi.run/core/pkg/container" "drassi.run/core/pkg/container/docker" "drassi.run/core/pkg/sandboxer" "drassi.run/core/pkg/sandboxer/container" + "drassi.run/core/pkg/store/oci" "drassi.run/core/util/fs" "drassi.run/core/util/path" "drassi.run/core/util/string" - "github.com/pelletier/go-toml/v2" - "github.com/pelletier/go-toml/v2/unstable" ) func init() { - sandboxer.Register(config.ProviderHost, func(raw unstable.RawMessage) (sandboxer.Engine, error) { - cfg := DefaultConfig() - if len(raw) > 0 { - if err := toml.Unmarshal(raw, cfg); err != nil { - return nil, err - } - } - return New(cfg) - }) -} - -type Config struct { - RootDir string `toml:"root_dir" json:"rootDir"` - RuntimeDir string `toml:"runtime_dir,omitempty" json:"runtimeDir,omitempty"` + sandboxer.Register(config.ProviderHost, DefaultConfig, NewFactory) } func DefaultConfig() *Config { @@ -49,6 +36,38 @@ func DefaultConfig() *Config { } } +func NewFactory(cfg *Config) sandboxer.Factory { + f := &factory{cfg: cfg} + f.create = sync.OnceValues(f.doCreate) + return f +} + +type factory struct { + create func() (sandboxer.Engine, error) + + cfg *Config + store ocistore.Manager + runtimes map[string]*config.Runtime +} + +func (f *factory) ProvisionRuntime(store ocistore.Manager, config map[string]*config.Runtime) { + f.store = store + f.runtimes = config +} + +func (f *factory) Create() (sandboxer.Engine, error) { + return f.create() +} + +func (f *factory) doCreate() (sandboxer.Engine, error) { + return New(f.cfg) +} + +type Config struct { + RootDir string `toml:"root_dir" json:"rootDir"` + RuntimeDir string `toml:"runtime_dir,omitempty" json:"runtimeDir,omitempty"` +} + type engine struct { Config } diff --git a/core/pkg/sandboxer/incus/engine.go b/core/pkg/sandboxer/incus/engine.go index 2697994a..e4a61296 100644 --- a/core/pkg/sandboxer/incus/engine.go +++ b/core/pkg/sandboxer/incus/engine.go @@ -10,6 +10,7 @@ import ( "context" "path" "strings" + "sync" "drassi.run/core/config" c "drassi.run/core/pkg/container" @@ -17,24 +18,52 @@ import ( "drassi.run/core/pkg/model/records" "drassi.run/core/pkg/sandboxer" "drassi.run/core/pkg/sandboxer/container" + "drassi.run/core/pkg/store/oci" "drassi.run/core/util/string" incusclient "github.com/lxc/incus/v6/client" incusapi "github.com/lxc/incus/v6/shared/api" dockerclient "github.com/moby/moby/client" - "github.com/pelletier/go-toml/v2" - "github.com/pelletier/go-toml/v2/unstable" ) func init() { - sandboxer.Register(config.ProviderIncus, func(raw unstable.RawMessage) (sandboxer.Engine, error) { - cfg := DefaultConfig() - if len(raw) > 0 { - if err := toml.Unmarshal(raw, cfg); err != nil { - return nil, err - } - } - return New(cfg) - }) + sandboxer.Register(config.ProviderIncus, DefaultConfig, NewFactory) +} + +func DefaultConfig() *Config { + return &Config{ + Endpoint: "unix:///var/lib/incus/unix.socket", + Template: Template{ + Image: "ubuntu:latest", + Ephemeral: true, + }, + } +} + +func NewFactory(cfg *Config) sandboxer.Factory { + f := &factory{cfg: cfg} + f.create = sync.OnceValues(f.doCreate) + return f +} + +type factory struct { + create func() (sandboxer.Engine, error) + + cfg *Config + store ocistore.Manager + runtimes map[string]*config.Runtime +} + +func (f *factory) ProvisionRuntime(store ocistore.Manager, config map[string]*config.Runtime) { + f.store = store + f.runtimes = config +} + +func (f *factory) Create() (sandboxer.Engine, error) { + return f.create() +} + +func (f *factory) doCreate() (sandboxer.Engine, error) { + return New(f.cfg) } type Config struct { @@ -72,16 +101,6 @@ type Template struct { Ephemeral bool `toml:"ephemeral" json:"ephemeral,omitempty"` } -func DefaultConfig() *Config { - return &Config{ - Endpoint: "unix:///var/lib/incus/unix.socket", - Template: Template{ - Image: "ubuntu:latest", - Ephemeral: true, - }, - } -} - type engine struct { client incusclient.InstanceServer template *Template diff --git a/runner-gha/cmd/launch/launch.go b/runner-gha/cmd/launch/launch.go index 50b35581..3a8b7923 100644 --- a/runner-gha/cmd/launch/launch.go +++ b/runner-gha/cmd/launch/launch.go @@ -91,7 +91,9 @@ func (l *launcher) Init(ctx context.Context, opts *options) (err error) { if sbConfig, ok := cfg.Sandboxers[cfg.UseSandboxer]; !ok { return fmt.Errorf("sandboxer %q not configured", cfg.UseSandboxer) - } else if sb, err := sandboxer.NewEngine(sbConfig); err != nil { + } else if factory, err := sandboxer.NewFactory(sbConfig); err != nil { + return err + } else if sb, err := factory.Create(); err != nil { return err } else { l.Sandboxer = sb diff --git a/runner-gha/cmd/migrate/migrate_test.go b/runner-gha/cmd/migrate/migrate_test.go index 3e9d5d80..d9bf686c 100644 --- a/runner-gha/cmd/migrate/migrate_test.go +++ b/runner-gha/cmd/migrate/migrate_test.go @@ -99,7 +99,9 @@ func TestMigrateCommand(t *testing.T) { // Check sandboxer engine instantiation sb, ok := cfg.Sandboxers[cfg.UseSandboxer] require.True(t, ok) - engine, err := sandboxer.NewEngine(sb) + factory, err := sandboxer.NewFactory(sb) + require.NoError(t, err) + engine, err := factory.Create() require.NoError(t, err) require.NotNil(t, engine) } diff --git a/runner-gitea/cmd/launch/launch.go b/runner-gitea/cmd/launch/launch.go index 436f6316..f21120cf 100644 --- a/runner-gitea/cmd/launch/launch.go +++ b/runner-gitea/cmd/launch/launch.go @@ -191,10 +191,12 @@ func (c *launcher) loadGitStore() error { func (c *launcher) loadSandboxer(config *giteaconfig.Config, name string) error { if sbConfig, ok := config.Sandboxers[name]; !ok { return fmt.Errorf("sandboxer %q not configured", name) - } else if engine, err := sandboxer.NewEngine(sbConfig); err != nil { + } else if factory, err := sandboxer.NewFactory(sbConfig); err != nil { + return err + } else if sb, err := factory.Create(); err != nil { return err } else { - c.runtime = engine + c.runtime = sb return nil } } From 0ccc1841e552d1b1d8e0758aaf9a678e420fd49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Thu, 10 Sep 2026 09:06:18 +0700 Subject: [PATCH 10/11] core/ocistore: init Manager and pass to sandboxer.Factory --- core/pkg/store/oci/manager.go | 16 +++++++++++++ runner-gha/cmd/launch/launch.go | 33 +++++++++++++++++--------- runner-gha/cmd/migrate/migrate_test.go | 1 + runner-gitea/cmd/launch/launch.go | 31 ++++++++++++++++++------ 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/core/pkg/store/oci/manager.go b/core/pkg/store/oci/manager.go index 9ce91f4b..e9c5f9b2 100644 --- a/core/pkg/store/oci/manager.go +++ b/core/pkg/store/oci/manager.go @@ -42,6 +42,22 @@ type Manager interface { Close() error } +var defaultManager = sync.OnceValues(func() (Manager, error) { + opts, err := storage.DefaultStoreOptions() + if err != nil { + return nil, err + } + store, err := storage.GetStore(opts) + if err != nil { + return nil, err + } + return New(store), nil +}) + +func Default() (Manager, error) { + return defaultManager() +} + func New(store storage.Store) Manager { return &manager{store: store, pull: pull} } diff --git a/runner-gha/cmd/launch/launch.go b/runner-gha/cmd/launch/launch.go index 3a8b7923..f67885ea 100644 --- a/runner-gha/cmd/launch/launch.go +++ b/runner-gha/cmd/launch/launch.go @@ -19,6 +19,7 @@ import ( "drassi.run/core/pkg/model/records" "drassi.run/core/pkg/sandboxer" "drassi.run/core/pkg/store/git" + "drassi.run/core/pkg/store/oci" "drassi.run/core/util/dig" "drassi.run/core/util/oauth2/clientcredentials" "drassi.run/core/wire" @@ -42,7 +43,8 @@ type launcher struct { Runner *ghaconfig.Runner Key *rsa.PrivateKey Sandboxer sandboxer.Engine - store gitstore.Manager + gitStore gitstore.Manager + ociStore ocistore.Manager hc *http.Client wm *worker.Manager @@ -89,14 +91,29 @@ func (l *launcher) Init(ctx context.Context, opts *options) (err error) { l.Key = key } + if store, err := gitstore.New(".cache"); err != nil { + return err + } else { + l.gitStore = store + } + + if store, err := ocistore.Default(); err != nil { + return err + } else { + l.ociStore = store + } + if sbConfig, ok := cfg.Sandboxers[cfg.UseSandboxer]; !ok { return fmt.Errorf("sandboxer %q not configured", cfg.UseSandboxer) } else if factory, err := sandboxer.NewFactory(sbConfig); err != nil { return err - } else if sb, err := factory.Create(); err != nil { - return err } else { - l.Sandboxer = sb + factory.ProvisionRuntime(l.ociStore, cfg.Runtimes) + if sb, err := factory.Create(); err != nil { + return err + } else { + l.Sandboxer = sb + } } authz := cfg.Runner.Authorization @@ -115,12 +132,6 @@ func (l *launcher) Init(ctx context.Context, opts *options) (err error) { l.hc = oauth2.NewClient(ctx, src) l.wm = worker.NewManager(cfg) - if s, err := gitstore.New(".cache"); err != nil { - return err - } else { - l.store = s - } - return nil } @@ -306,7 +317,7 @@ func (l *launcher) module() *wire.Module { if err := xdig.Supply(scope, l.Sandboxer); err != nil { return fmt.Errorf("provide sandboxer.Engine: %w", err) } - if err := xdig.Supply(scope, l.store); err != nil { + if err := xdig.Supply(scope, l.gitStore); err != nil { return fmt.Errorf("provide gitstore.Store: %w", err) } if err := scope.Provide(l.runnerService); err != nil { diff --git a/runner-gha/cmd/migrate/migrate_test.go b/runner-gha/cmd/migrate/migrate_test.go index d9bf686c..6979405e 100644 --- a/runner-gha/cmd/migrate/migrate_test.go +++ b/runner-gha/cmd/migrate/migrate_test.go @@ -101,6 +101,7 @@ func TestMigrateCommand(t *testing.T) { require.True(t, ok) factory, err := sandboxer.NewFactory(sb) require.NoError(t, err) + factory.ProvisionRuntime(nil, cfg.Runtimes) engine, err := factory.Create() require.NoError(t, err) require.NotNil(t, engine) diff --git a/runner-gitea/cmd/launch/launch.go b/runner-gitea/cmd/launch/launch.go index f21120cf..566a528f 100644 --- a/runner-gitea/cmd/launch/launch.go +++ b/runner-gitea/cmd/launch/launch.go @@ -18,6 +18,7 @@ import ( "drassi.run/core/pkg/model/records" "drassi.run/core/pkg/sandboxer" "drassi.run/core/pkg/store/git" + "drassi.run/core/pkg/store/oci" "drassi.run/core/util/dig" "drassi.run/core/wire" giteaconfig "drassi.run/gitea-runner/config" @@ -36,7 +37,8 @@ type launcher struct { concurrency int client gitea.Client runtime sandboxer.Engine - store gitstore.Manager + gitStore gitstore.Manager + ociStore ocistore.Manager // tasksVersion used to store the version of the last task fetched from the Gitea. tasksVersion atomic.Int64 @@ -97,6 +99,9 @@ func (c *launcher) Init(ctx context.Context, o *options) error { if err = c.loadGitStore(); err != nil { return err } + if err = c.loadOciStore(); err != nil { + return err + } return c.loadSandboxer(config, config.UseSandboxer) } @@ -183,7 +188,16 @@ func (c *launcher) loadGitStore() error { if store, err := gitstore.New(".cache"); err != nil { return err } else { - c.store = store + c.gitStore = store + } + return nil +} + +func (c *launcher) loadOciStore() error { + if store, err := ocistore.Default(); err != nil { + return err + } else { + c.ociStore = store } return nil } @@ -193,11 +207,14 @@ func (c *launcher) loadSandboxer(config *giteaconfig.Config, name string) error return fmt.Errorf("sandboxer %q not configured", name) } else if factory, err := sandboxer.NewFactory(sbConfig); err != nil { return err - } else if sb, err := factory.Create(); err != nil { - return err } else { - c.runtime = sb - return nil + factory.ProvisionRuntime(c.ociStore, config.Runtimes) + if sb, err := factory.Create(); err != nil { + return err + } else { + c.runtime = sb + return nil + } } } @@ -215,7 +232,7 @@ func (c *launcher) module() *wire.Module { if err := xdig.Supply(scope, c.runtime); err != nil { return fmt.Errorf("provide sandboxer.Engine: %w", err) } - if err := xdig.Supply(scope, c.store); err != nil { + if err := xdig.Supply(scope, c.gitStore); err != nil { return fmt.Errorf("provide gitstore.Store: %w", err) } if err := xdig.Supply(scope, c.client); err != nil { From 6fa6dde8c2328995720e34ffbb110ea0b4068f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Thu, 10 Sep 2026 09:13:10 +0700 Subject: [PATCH 11/11] chores: Close resources in launcher --- runner-gha/cmd/launch/launch.go | 16 ++++++++++++++++ runner-gitea/cmd/launch/launch.go | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/runner-gha/cmd/launch/launch.go b/runner-gha/cmd/launch/launch.go index f67885ea..4e24a3d7 100644 --- a/runner-gha/cmd/launch/launch.go +++ b/runner-gha/cmd/launch/launch.go @@ -9,6 +9,7 @@ package launch import ( "context" "crypto/rsa" + "errors" "fmt" "log" "net/http" @@ -61,6 +62,7 @@ func New() *cobra.Command { ctx := cmd.Context() l := new(launcher) + defer l.Close() if err := l.Init(ctx, &opts); err != nil { return err } @@ -332,3 +334,17 @@ func (l *launcher) runnerService(hc *http.Client) (*lease.RunnerService, error) runner := l.Runner return lease.NewRunnerService(runner.ServerUrl, hc, runner.GroupId) } + +func (l *launcher) Close() error { + var errs []error + if l.Sandboxer != nil { + errs = append(errs, l.Sandboxer.Close()) + } + if l.gitStore != nil { + errs = append(errs, l.gitStore.Close()) + } + if l.ociStore != nil { + errs = append(errs, l.ociStore.Close()) + } + return errors.Join(errs...) +} diff --git a/runner-gitea/cmd/launch/launch.go b/runner-gitea/cmd/launch/launch.go index 566a528f..6f88f58b 100644 --- a/runner-gitea/cmd/launch/launch.go +++ b/runner-gitea/cmd/launch/launch.go @@ -55,6 +55,7 @@ func New() *cobra.Command { ctx := cmd.Context() l := new(launcher) + defer l.Close() if err := l.Init(ctx, &opts); err != nil { return err } @@ -242,3 +243,17 @@ func (c *launcher) module() *wire.Module { } return wire.NewModule("gitea/launch", fn) } + +func (c *launcher) Close() error { + var errs []error + if c.runtime != nil { + errs = append(errs, c.runtime.Close()) + } + if c.gitStore != nil { + errs = append(errs, c.gitStore.Close()) + } + if c.ociStore != nil { + errs = append(errs, c.ociStore.Close()) + } + return errors.Join(errs...) +}