From b90dfed5b36009296a56f642d0ffba878e40526d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Tue, 8 Sep 2026 15:48:00 +0700 Subject: [PATCH 1/5] [core/gitstore] refactor --- core/pkg/executor/action_composite.go | 6 +- core/pkg/executor/action_docker.go | 6 +- core/pkg/executor/action_node.go | 10 +-- core/pkg/executor/action_reference.go | 21 +++--- core/pkg/executor/step_executor.go | 6 +- core/pkg/executor/z_converters.go | 6 +- .../gitstore/store.go => git/manager.go} | 65 +++++++++---------- .../store/{repository => git}/repository.go | 16 ++--- .../{repository => git}/repository_test.go | 5 +- runner-gha/cmd/launch/launch.go | 4 +- runner-gha/pkg/messages/converter.go | 4 +- runner-gitea/cmd/launch/launch.go | 4 +- 12 files changed, 77 insertions(+), 76 deletions(-) rename core/pkg/store/{repository/gitstore/store.go => git/manager.go} (72%) rename core/pkg/store/{repository => git}/repository.go (90%) rename core/pkg/store/{repository => git}/repository_test.go (99%) diff --git a/core/pkg/executor/action_composite.go b/core/pkg/executor/action_composite.go index 311ccadcb..86a3f4f29 100644 --- a/core/pkg/executor/action_composite.go +++ b/core/pkg/executor/action_composite.go @@ -16,14 +16,14 @@ import ( "drassi.run/core/pkg/expression/libraries" "drassi.run/core/pkg/model/records" "drassi.run/core/pkg/model/workflows" - "drassi.run/core/pkg/store/repository" + "drassi.run/core/pkg/store/git" "drassi.run/core/util/dig" "drassi.run/core/util/otel" "go.uber.org/dig" ) type CompositeActionSpec struct { - Repo *repository.Repository + Repo *gitstore.RepoReference Inputs workflows.Evaluable[map[string]string] Outputs workflows.Evaluable[map[string]string] @@ -86,7 +86,7 @@ func (e *compositeActionExecutor) StepExecutor() StepExecutor { } func (e *compositeActionExecutor) Name() workflows.Evaluable[string] { - name := repository.Location(e.spec.Repo) + name := gitstore.Location(e.spec.Repo) return workflows.NewLiteralToken(name) } diff --git a/core/pkg/executor/action_docker.go b/core/pkg/executor/action_docker.go index aeab8a471..6a2e1a74b 100644 --- a/core/pkg/executor/action_docker.go +++ b/core/pkg/executor/action_docker.go @@ -16,7 +16,7 @@ import ( "drassi.run/core/pkg/model/workflows" "drassi.run/core/pkg/runtime" "drassi.run/core/pkg/scribe" - "drassi.run/core/pkg/store/repository" + "drassi.run/core/pkg/store/git" "drassi.run/core/util/dig" semconv "go.opentelemetry.io/otel/semconv/v1.27.0" "go.opentelemetry.io/otel/trace" @@ -24,7 +24,7 @@ import ( ) type DockerActionSpec struct { - Repo *repository.Repository + Repo *gitstore.RepoReference Inputs workflows.Evaluable[map[string]string] Outputs workflows.Evaluable[map[string]string] Env workflows.Evaluable[map[string]string] @@ -229,7 +229,7 @@ func (e *dockerActionExecutor) repr() string { str += fmt.Sprintf(" with Dockerfile=%q", e.spec.Image) } if repo := e.spec.Repo; repo != nil { - str += fmt.Sprintf(" from %q", repository.Location(repo)) + str += fmt.Sprintf(" from %q", gitstore.Location(repo)) } return str } diff --git a/core/pkg/executor/action_node.go b/core/pkg/executor/action_node.go index 73c00b691..6ae652207 100644 --- a/core/pkg/executor/action_node.go +++ b/core/pkg/executor/action_node.go @@ -15,14 +15,14 @@ import ( "drassi.run/core/pkg/model/workflows" "drassi.run/core/pkg/sandboxer" "drassi.run/core/pkg/scribe" - "drassi.run/core/pkg/store/repository" + "drassi.run/core/pkg/store/git" "drassi.run/core/util/otel" "go.opentelemetry.io/otel/trace" "go.uber.org/dig" ) type NodeActionSpec struct { - Repo *repository.Repository + Repo *gitstore.RepoReference Inputs workflows.Evaluable[map[string]string] Outputs workflows.Evaluable[map[string]string] @@ -57,7 +57,7 @@ func (e *nodeActionExecutor) StepExecutor() StepExecutor { } func (e *nodeActionExecutor) Name() workflows.Evaluable[string] { - name := repository.Location(e.spec.Repo) + name := gitstore.Location(e.spec.Repo) return workflows.NewLiteralToken(name) } @@ -139,7 +139,7 @@ func (e *nodeActionExecutor) computeScriptPath(layout *sandboxer.Layout, stage S script = e.spec.Main } - scriptPath := filepath.Join(layout.Actions, repository.Location(e.spec.Repo), script) + scriptPath := filepath.Join(layout.Actions, gitstore.Location(e.spec.Repo), script) return scriptPath } @@ -160,5 +160,5 @@ func (e *nodeActionExecutor) addSpanAttrs(ctx context.Context, stage Stage) { } func (e *nodeActionExecutor) repr() string { - return fmt.Sprintf("node action from %q", repository.Location(e.spec.Repo)) + return fmt.Sprintf("node action from %q", gitstore.Location(e.spec.Repo)) } diff --git a/core/pkg/executor/action_reference.go b/core/pkg/executor/action_reference.go index 53442efba..a17c655b1 100644 --- a/core/pkg/executor/action_reference.go +++ b/core/pkg/executor/action_reference.go @@ -21,8 +21,7 @@ import ( "drassi.run/core/pkg/model/workflows" "drassi.run/core/pkg/sandboxer" "drassi.run/core/pkg/scribe" - "drassi.run/core/pkg/store/repository" - "drassi.run/core/pkg/store/repository/gitstore" + "drassi.run/core/pkg/store/git" "drassi.run/core/util/dig" "drassi.run/core/util/otel" "github.com/go-git/go-git/v5/plumbing/object" @@ -36,13 +35,13 @@ type ReferenceActionSpec struct { // + Using a public action: `uses: actions/aws@v2.0.1` // + Using a public action in a subdirectory: `uses: actions/aws/ec2@main` // + Using a local action: `uses: ./.github/actions/hello-world-action` - Repo *repository.Repository + Repo *gitstore.RepoReference // cache resolved revision rev string } -func (spec *ReferenceActionSpec) Repository() *repository.Repository { +func (spec *ReferenceActionSpec) Repository() *gitstore.RepoReference { return spec.Repo } @@ -53,11 +52,11 @@ func (spec *ReferenceActionSpec) CreateExecutor( var ( forge *records.Forge - store gitstore.Store + store gitstore.Manager ) span := trace.SpanFromContext(ctx) - span.SetAttributes(xotel.ActionRepo(repository.Location(spec.Repo))) + span.SetAttributes(xotel.ActionRepo(gitstore.Location(spec.Repo))) if err := xdig.Populate(scope, &forge); err != nil { return nil, err @@ -72,14 +71,14 @@ func (spec *ReferenceActionSpec) CreateExecutor( token := forge.Token // If the action is located in different server than the job repo, // unset the token to prevent an unauthenticated error. - if repository.Endpoint(spec.Repo) != spec.serverDomain(forge.ServerUrl) { + if gitstore.Endpoint(spec.Repo) != spec.serverDomain(forge.ServerUrl) { token = "" } if rev, err := store.Fetch(ctx, spec.Repo, token); err != nil { return nil, err } else { - s.Writef("Download action repository %q (SHA:%s)", repository.Location(spec.Repo), rev) + s.Writef("Download action repository %q (SHA:%s)", gitstore.Location(spec.Repo), rev) spec.rev = rev } @@ -92,7 +91,7 @@ func (spec *ReferenceActionSpec) CreateExecutor( } } -func (spec *ReferenceActionSpec) loadAction(ctx context.Context, s *scribe.Scribe, store gitstore.Store) (ActionSpec, error) { +func (spec *ReferenceActionSpec) loadAction(ctx context.Context, s *scribe.Scribe, store gitstore.Manager) (ActionSpec, error) { span := trace.SpanFromContext(ctx) // 1. First, try reading "action.yml" or "action.yaml" file @@ -154,8 +153,8 @@ func (spec *ReferenceActionSpec) createDockerfileAction(dockerfile string) (Acti return action, nil } -func (spec *ReferenceActionSpec) transferAction(ctx context.Context, store gitstore.Store, sandbox sandboxer.Sandbox) error { - location := repository.FullName(spec.Repo) + "@" + spec.Repo.Ref +func (spec *ReferenceActionSpec) transferAction(ctx context.Context, store gitstore.Manager, sandbox sandboxer.Sandbox) error { + location := gitstore.FullName(spec.Repo) + "@" + spec.Repo.Ref r, err := store.Read(ctx, spec.Repo, spec.rev, location) if err != nil { return err diff --git a/core/pkg/executor/step_executor.go b/core/pkg/executor/step_executor.go index 15424b21b..d9cbdc8ab 100644 --- a/core/pkg/executor/step_executor.go +++ b/core/pkg/executor/step_executor.go @@ -21,7 +21,7 @@ import ( "drassi.run/core/pkg/model/workflows" "drassi.run/core/pkg/sandboxer" "drassi.run/core/pkg/scribe" - "drassi.run/core/pkg/store/repository" + "drassi.run/core/pkg/store/git" "drassi.run/core/pkg/stream" "drassi.run/core/util/context" "drassi.run/core/util/dig" @@ -200,7 +200,9 @@ func (e *stepExecutor) init(ctx context.Context, scope *dig.Scope) (ex error) { e.aExec = exec } - if r, ok := e.spec.Action.(interface{ Repository() *repository.Repository }); ok { + if r, ok := e.spec.Action.(interface { + Repository() *gitstore.RepoReference + }); ok { repo := r.Repository() e.forge.ActionRepository = repo.Name diff --git a/core/pkg/executor/z_converters.go b/core/pkg/executor/z_converters.go index 6542e7ad6..9fbc3db4a 100644 --- a/core/pkg/executor/z_converters.go +++ b/core/pkg/executor/z_converters.go @@ -14,7 +14,7 @@ import ( "drassi.run/core/pkg/model/actions" "drassi.run/core/pkg/model/records" "drassi.run/core/pkg/model/workflows" - "drassi.run/core/pkg/store/repository" + "drassi.run/core/pkg/store/git" "github.com/google/uuid" ) @@ -95,7 +95,7 @@ func ToStepSpec(step workflows.Step) *StepSpec { Image: s.Uses, } } else { - repo, _ := repository.Parse(s.Uses) + repo, _ := gitstore.Parse(s.Uses) spec.Action = &ReferenceActionSpec{ Repo: repo, } @@ -104,7 +104,7 @@ func ToStepSpec(step workflows.Step) *StepSpec { return spec } -func ToActionSpec(action *actions.Action, repo *repository.Repository) (ActionSpec, error) { +func ToActionSpec(action *actions.Action, repo *gitstore.RepoReference) (ActionSpec, error) { var spec ActionSpec switch r := action.Runs.(type) { case *actions.NodeRuns: diff --git a/core/pkg/store/repository/gitstore/store.go b/core/pkg/store/git/manager.go similarity index 72% rename from core/pkg/store/repository/gitstore/store.go rename to core/pkg/store/git/manager.go index 8fe701e5e..d13ffd2b2 100644 --- a/core/pkg/store/repository/gitstore/store.go +++ b/core/pkg/store/git/manager.go @@ -16,7 +16,6 @@ import ( "os" "path" - "drassi.run/core/pkg/store/repository" "drassi.run/core/util/fs" "drassi.run/core/util/path" "drassi.run/core/util/string" @@ -35,15 +34,15 @@ import ( "k8s.io/apimachinery/pkg/util/rand" ) -type Store interface { - Fetch(ctx context.Context, repo *repository.Repository, token string) (rev string, err error) - Read(ctx context.Context, repo *repository.Repository, rev, dir string) (io.ReadCloser, error) - File(ctx context.Context, repo *repository.Repository, rev, path string) (io.ReadCloser, error) +type Manager interface { + Fetch(ctx context.Context, repo *RepoReference, token string) (rev string, err error) + Read(ctx context.Context, repo *RepoReference, rev, dir string) (io.ReadCloser, error) + File(ctx context.Context, repo *RepoReference, rev, path string) (io.ReadCloser, error) } const remoteName string = "anonymous" -func New(rootDir string) (Store, error) { +func New(rootDir string) (Manager, error) { if d, err := xpath.ResolveDir(rootDir); err != nil { return nil, err } else { @@ -54,27 +53,27 @@ func New(rootDir string) (Store, error) { return nil, err } - rs := &store{ + m := &manager{ // Using `billy.Filesystem` instead of `rootDir` to // abstract from file system implementations and simplify testing. fsys: osfs.New(rootDir), repos: make(map[string]*git.Repository), } - return rs, nil + return m, nil } -type store struct { +type manager struct { fsys billy.Filesystem repos map[string]*git.Repository } -func (s *store) Fetch(ctx context.Context, repo *repository.Repository, token string) (string, error) { - path, err := s.ensureDir(repo) +func (m *manager) Fetch(ctx context.Context, repo *RepoReference, token string) (string, error) { + path, err := m.ensureDir(repo) if err != nil { return "", err } - gitRepo, err := s.ensureRepo(path, repo) + gitRepo, err := m.ensureRepo(path, repo) if err != nil { return "", err } @@ -82,7 +81,7 @@ func (s *store) Fetch(ctx context.Context, repo *repository.Repository, token st tmpBranch := rand.String(12) defer gitRepo.DeleteBranch(tmpBranch) - err = s.fetch(ctx, repo, token, tmpBranch) + err = m.fetch(ctx, repo, token, tmpBranch) if err != nil { return "", err } @@ -94,9 +93,9 @@ func (s *store) Fetch(ctx context.Context, repo *repository.Repository, token st return hash.String(), nil } -func (s *store) Read(ctx context.Context, repo *repository.Repository, rev string, dir string) (io.ReadCloser, error) { - id := repository.FullName(repo) - gitRepo, ok := s.repos[id] +func (m *manager) Read(ctx context.Context, repo *RepoReference, rev string, dir string) (io.ReadCloser, error) { + id := FullName(repo) + gitRepo, ok := m.repos[id] if !ok { return nil, fmt.Errorf("repo %q not found", id) } @@ -133,9 +132,9 @@ func (s *store) Read(ctx context.Context, repo *repository.Repository, rev strin return reader, nil } -func (s *store) File(ctx context.Context, repo *repository.Repository, rev, path string) (io.ReadCloser, error) { - id := repository.FullName(repo) - gitRepo, ok := s.repos[id] +func (m *manager) File(ctx context.Context, repo *RepoReference, rev, path string) (io.ReadCloser, error) { + id := FullName(repo) + gitRepo, ok := m.repos[id] if !ok { return nil, fmt.Errorf("repo %q not found", id) } @@ -161,8 +160,8 @@ func (s *store) File(ctx context.Context, repo *repository.Repository, rev, path return file.Reader() } -func (s *store) fetch(ctx context.Context, repo *repository.Repository, token, branch string) error { - gitRepo := s.repos[repository.FullName(repo)] +func (m *manager) fetch(ctx context.Context, repo *RepoReference, token, branch string) error { + gitRepo := m.repos[FullName(repo)] var auth transport.AuthMethod if token != "" { @@ -174,7 +173,7 @@ func (s *store) fetch(ctx context.Context, repo *repository.Repository, token, b remoteConfig := &config.RemoteConfig{ Name: remoteName, - URLs: []string{repository.Url(repo)}, + URLs: []string{Url(repo)}, } remote, err := gitRepo.CreateRemoteAnonymous(remoteConfig) if err != nil { @@ -197,36 +196,36 @@ func (s *store) fetch(ctx context.Context, repo *repository.Repository, token, b return remote.FetchContext(ctx, fetchOptions) } -func (s *store) ensureDir(repo *repository.Repository) (string, error) { - path := repository.FullName(repo) +func (m *manager) ensureDir(repo *RepoReference) (string, error) { + path := FullName(repo) path = xstring.EnsureSuffix(path, ".git") - fileInfo, err := s.fsys.Stat(path) + fileInfo, err := m.fsys.Stat(path) if err != nil { if os.IsNotExist(err) { - return path, s.fsys.MkdirAll(path, xfs.DirPerm) + return path, m.fsys.MkdirAll(path, xfs.DirPerm) } return "", err } if !fileInfo.IsDir() { - if err = util.RemoveAll(s.fsys, path); err != nil { + if err = util.RemoveAll(m.fsys, path); err != nil { return "", err } - return path, s.fsys.MkdirAll(path, xfs.DirPerm) + return path, m.fsys.MkdirAll(path, xfs.DirPerm) } return path, nil } -func (s *store) ensureRepo(path string, repo *repository.Repository) (*git.Repository, error) { - id := repository.FullName(repo) - if gitRepo, ok := s.repos[id]; ok { +func (m *manager) ensureRepo(path string, repo *RepoReference) (*git.Repository, error) { + id := FullName(repo) + if gitRepo, ok := m.repos[id]; ok { return gitRepo, nil } var storer storage.Storer - if dot, err := s.fsys.Chroot(path); err != nil { + if dot, err := m.fsys.Chroot(path); err != nil { return nil, err } else { storer = filesystem.NewStorage(dot, cache.NewObjectLRUDefault()) @@ -239,7 +238,7 @@ func (s *store) ensureRepo(path string, repo *repository.Repository) (*git.Repos gitRepo, err = git.Open(storer, nil) } if gitRepo != nil { - s.repos[id] = gitRepo + m.repos[id] = gitRepo } return gitRepo, err } diff --git a/core/pkg/store/repository/repository.go b/core/pkg/store/git/repository.go similarity index 90% rename from core/pkg/store/repository/repository.go rename to core/pkg/store/git/repository.go index 5a62d9bba..b53c8e2b7 100644 --- a/core/pkg/store/repository/repository.go +++ b/core/pkg/store/git/repository.go @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package repository +package gitstore import ( "fmt" @@ -19,7 +19,7 @@ var ( hostPortRegex = regexp.MustCompile(`^(?P[^:\s]+)(?::(?P\d{1,5}))?$`) ) -type Repository struct { +type RepoReference struct { Scheme string // e.g. git Transport string // e.g. http, https, ssh Endpoint string // e.g. github.com @@ -28,18 +28,18 @@ type Repository struct { Ref string // e.g. v3 } -func Endpoint(r *Repository) string { +func Endpoint(r *RepoReference) string { if r.Endpoint == "" { return "github.com" } return r.Endpoint } -func FullName(r *Repository) string { +func FullName(r *RepoReference) string { return fmt.Sprintf("%s/%s", Endpoint(r), r.Name) } -func Url(repo *Repository) string { +func Url(repo *RepoReference) string { r := FullName(repo) if repo.Transport == "" { return "https://" + r @@ -47,12 +47,12 @@ func Url(repo *Repository) string { return repo.Transport + "://" + r } -func Location(repo *Repository) string { +func Location(repo *RepoReference) string { r := FullName(repo) + "@" + repo.Ref return filepath.Join(r, repo.Path) } -func Parse(s string) (*Repository, error) { +func Parse(s string) (*RepoReference, error) { match := useRegex.FindStringSubmatch(s) if match == nil { return nil, fmt.Errorf("invalid action %q", s) @@ -100,7 +100,7 @@ func Parse(s string) (*Repository, error) { path = strings.Join(locParts[2:], "/") } - r := Repository{ + r := RepoReference{ Scheme: scheme, Transport: transport, Endpoint: endpoint, diff --git a/core/pkg/store/repository/repository_test.go b/core/pkg/store/git/repository_test.go similarity index 99% rename from core/pkg/store/repository/repository_test.go rename to core/pkg/store/git/repository_test.go index 9d49d9eca..2d67e6c14 100644 --- a/core/pkg/store/repository/repository_test.go +++ b/core/pkg/store/git/repository_test.go @@ -4,12 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -package repository +package gitstore import ( - "github.com/stretchr/testify/assert" "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestParse(t *testing.T) { diff --git a/runner-gha/cmd/launch/launch.go b/runner-gha/cmd/launch/launch.go index f590ba35a..c2bbc4457 100644 --- a/runner-gha/cmd/launch/launch.go +++ b/runner-gha/cmd/launch/launch.go @@ -19,7 +19,7 @@ import ( "drassi.run/core/pkg/model" "drassi.run/core/pkg/model/records" "drassi.run/core/pkg/sandboxer" - "drassi.run/core/pkg/store/repository/gitstore" + "drassi.run/core/pkg/store/git" "drassi.run/core/util/dig" "drassi.run/core/util/oauth2/clientcredentials" "drassi.run/core/wire" @@ -43,7 +43,7 @@ type launcher struct { Runner *ghaconfig.Runner Key *rsa.PrivateKey Sandboxer sandboxer.Engine - store gitstore.Store + store gitstore.Manager hc *http.Client wm *worker.Manager diff --git a/runner-gha/pkg/messages/converter.go b/runner-gha/pkg/messages/converter.go index bd208f81e..734a943d0 100644 --- a/runner-gha/pkg/messages/converter.go +++ b/runner-gha/pkg/messages/converter.go @@ -12,7 +12,7 @@ import ( "drassi.run/core/pkg/executor" "drassi.run/core/pkg/model/workflows" - "drassi.run/core/pkg/store/repository" + "drassi.run/core/pkg/store/git" ) func ToToken(token *TemplateToken) workflows.Token { @@ -105,7 +105,7 @@ func ToStepSpec(step *JobStep) (*executor.StepSpec, error) { return nil, fmt.Errorf("unsupported step %s with repo type %s", step.ContextName, ref.RepositoryType) } // TODO: support local action (ref.RepositoryType=self) - repo := &repository.Repository{ + repo := &gitstore.RepoReference{ Scheme: "https", Endpoint: "github.com", Name: ref.Name, diff --git a/runner-gitea/cmd/launch/launch.go b/runner-gitea/cmd/launch/launch.go index 579d262fb..b3203f841 100644 --- a/runner-gitea/cmd/launch/launch.go +++ b/runner-gitea/cmd/launch/launch.go @@ -18,7 +18,7 @@ import ( "drassi.run/core/pkg/model" "drassi.run/core/pkg/model/records" "drassi.run/core/pkg/sandboxer" - "drassi.run/core/pkg/store/repository/gitstore" + "drassi.run/core/pkg/store/git" "drassi.run/core/util/dig" "drassi.run/core/wire" giteaconfig "drassi.run/gitea-runner/config" @@ -36,7 +36,7 @@ type launcher struct { concurrency int client gitea.Client runtime sandboxer.Engine - store gitstore.Store + store gitstore.Manager // tasksVersion used to store the version of the last task fetched from the Gitea. tasksVersion atomic.Int64 From b8b6210def1ee17c3983a0ebd3a2810bd84c3787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Tue, 8 Sep 2026 16:15:12 +0700 Subject: [PATCH 2/5] [core/gitstore] add test case --- core/pkg/store/git/manager.go | 146 +++++--- core/pkg/store/git/manager_test.go | 512 +++++++++++++++++++++++++++++ core/pkg/store/git/repository.go | 2 +- 3 files changed, 617 insertions(+), 43 deletions(-) create mode 100644 core/pkg/store/git/manager_test.go diff --git a/core/pkg/store/git/manager.go b/core/pkg/store/git/manager.go index d13ffd2b2..a941530ff 100644 --- a/core/pkg/store/git/manager.go +++ b/core/pkg/store/git/manager.go @@ -8,6 +8,7 @@ package gitstore import ( "archive/tar" + "cmp" "context" "errors" "fmt" @@ -15,6 +16,8 @@ import ( "io/fs" "os" "path" + "strings" + "sync" "drassi.run/core/util/fs" "drassi.run/core/util/path" @@ -31,6 +34,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/storage" "github.com/go-git/go-git/v5/storage/filesystem" + "golang.org/x/sync/singleflight" "k8s.io/apimachinery/pkg/util/rand" ) @@ -64,40 +68,48 @@ func New(rootDir string) (Manager, error) { type manager struct { fsys billy.Filesystem + sf singleflight.Group + mu sync.RWMutex repos map[string]*git.Repository } func (m *manager) Fetch(ctx context.Context, repo *RepoReference, token string) (string, error) { - path, err := m.ensureDir(repo) - if err != nil { - return "", err - } + key := Location(repo) + v, err, _ := m.sf.Do(key, func() (any, error) { + path, err := m.ensureDir(repo) + if err != nil { + return "", err + } - gitRepo, err := m.ensureRepo(path, repo) - if err != nil { - return "", err - } + gitRepo, err := m.ensureRepo(path, repo) + if err != nil { + return "", err + } - tmpBranch := rand.String(12) - defer gitRepo.DeleteBranch(tmpBranch) + tmpBranch := rand.String(12) + defer gitRepo.DeleteBranch(tmpBranch) - err = m.fetch(ctx, repo, token, tmpBranch) - if err != nil { - return "", err - } + err = m.fetch(ctx, gitRepo, repo, token, tmpBranch) + if err != nil { + return "", err + } - hash, err := gitRepo.ResolveRevision(plumbing.Revision(tmpBranch)) + hash, err := gitRepo.ResolveRevision(plumbing.Revision(tmpBranch)) + if err != nil { + return "", err + } + return hash.String(), nil + }) if err != nil { return "", err } - return hash.String(), nil + return v.(string), nil } func (m *manager) Read(ctx context.Context, repo *RepoReference, rev string, dir string) (io.ReadCloser, error) { - id := FullName(repo) - gitRepo, ok := m.repos[id] - if !ok { - return nil, fmt.Errorf("repo %q not found", id) + gitRepo, err := m.getRepo(repo) + if err != nil { + return nil, err } commit, err := gitRepo.CommitObject(plumbing.NewHash(rev)) @@ -110,7 +122,7 @@ func (m *manager) Read(ctx context.Context, repo *RepoReference, rev string, dir } reader, writer := io.Pipe() - ch := make(chan int, 1) + ch := make(chan struct{}) go func() { select { case <-ctx.Done(): @@ -120,23 +132,22 @@ func (m *manager) Read(ctx context.Context, repo *RepoReference, rev string, dir }() go func() { defer files.Close() - defer writer.Close() defer close(ch) + tw := tar.NewWriter(writer) - defer tw.Close() handler := newTarHandler(tw, dir) err := files.ForEach(handler) + err = cmp.Or(err, tw.Close()) _ = writer.CloseWithError(err) }() return reader, nil } -func (m *manager) File(ctx context.Context, repo *RepoReference, rev, path string) (io.ReadCloser, error) { - id := FullName(repo) - gitRepo, ok := m.repos[id] - if !ok { - return nil, fmt.Errorf("repo %q not found", id) +func (m *manager) File(ctx context.Context, repo *RepoReference, rev, filePath string) (io.ReadCloser, error) { + gitRepo, err := m.getRepo(repo) + if err != nil { + return nil, err } commit, err := gitRepo.CommitObject(plumbing.NewHash(rev)) @@ -149,20 +160,57 @@ func (m *manager) File(ctx context.Context, repo *RepoReference, rev, path strin return nil, err } - file, err := tree.File(path) + cleanPath := strings.TrimPrefix(path.Clean(filePath), "/") + entry, err := tree.FindEntry(cleanPath) if err != nil { + if notFoundErr(err) { + return nil, fs.ErrNotExist + } return nil, err } - if !file.Mode.IsFile() { - return nil, fmt.Errorf("%q is not a (regular) file", path) + if !entry.Mode.IsFile() { + return nil, fmt.Errorf("%q is not a (regular) file", filePath) } - return file.Reader() + if file, err := tree.TreeEntryFile(entry); err != nil { + return nil, err + } else { + return file.Reader() + } } -func (m *manager) fetch(ctx context.Context, repo *RepoReference, token, branch string) error { - gitRepo := m.repos[FullName(repo)] +func (m *manager) getRepo(repo *RepoReference) (*git.Repository, error) { + id := FullName(repo) + m.mu.RLock() + gitRepo, ok := m.repos[id] + m.mu.RUnlock() + if ok { + return gitRepo, nil + } + + m.mu.Lock() + defer m.mu.Unlock() + if gitRepo, ok := m.repos[id]; ok { + return gitRepo, nil + } + + path := xstring.EnsureSuffix(id, ".git") + dot, err := m.fsys.Chroot(path) + if err != nil { + return nil, fmt.Errorf("repo %q not found: %w", id, err) + } + + storer := filesystem.NewStorage(dot, cache.NewObjectLRUDefault()) + gitRepo, err = git.Open(storer, nil) + if err != nil { + return nil, fmt.Errorf("repo %q not found: %w", id, err) + } + + m.repos[id] = gitRepo + return gitRepo, nil +} +func (m *manager) fetch(ctx context.Context, gitRepo *git.Repository, repo *RepoReference, token, branch string) error { var auth transport.AuthMethod if token != "" { auth = &http.BasicAuth{ @@ -184,7 +232,7 @@ func (m *manager) fetch(ctx context.Context, repo *RepoReference, token, branch // https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/ fetchOptions := &git.FetchOptions{ RefSpecs: []config.RefSpec{ - config.RefSpec(fmt.Sprintf("+%s:%s", repo.Ref, branch)), + config.RefSpec(fmt.Sprintf("+%s:refs/heads/%s", repo.Ref, branch)), }, Auth: auth, @@ -220,6 +268,9 @@ func (m *manager) ensureDir(repo *RepoReference) (string, error) { func (m *manager) ensureRepo(path string, repo *RepoReference) (*git.Repository, error) { id := FullName(repo) + m.mu.Lock() + defer m.mu.Unlock() + if gitRepo, ok := m.repos[id]; ok { return gitRepo, nil } @@ -231,12 +282,14 @@ func (m *manager) ensureRepo(path string, repo *RepoReference) (*git.Repository, storer = filesystem.NewStorage(dot, cache.NewObjectLRUDefault()) } - //gitRepo, err := git.PlainInit(path, true) gitRepo, err := git.Init(storer, nil) if errors.Is(err, git.ErrRepositoryAlreadyExists) { - //gitRepo, err = git.PlainOpen(path) gitRepo, err = git.Open(storer, nil) } + if err != nil { + return nil, err + } + if gitRepo != nil { m.repos[id] = gitRepo } @@ -259,6 +312,7 @@ func newTarHandler(tw *tar.Writer, dir string) tarHandler { } if mode&fs.ModeSymlink != 0 { + hdr.Typeflag = tar.TypeSymlink content, err := f.Contents() if err != nil { return err @@ -268,17 +322,25 @@ func newTarHandler(tw *tar.Writer, dir string) tarHandler { return tw.WriteHeader(hdr) } + hdr.Typeflag = tar.TypeReg hdr.Size = f.Size if err := tw.WriteHeader(hdr); err != nil { return err } - if reader, err := f.Reader(); err != nil { - return err - } else { - defer reader.Close() - _, err = io.Copy(tw, reader) + reader, err := f.Reader() + if err != nil { return err } + defer reader.Close() + + _, err = io.Copy(tw, reader) + return err } return h } + +func notFoundErr(err error) bool { + return errors.Is(err, object.ErrFileNotFound) || + errors.Is(err, object.ErrEntryNotFound) || + errors.Is(err, object.ErrDirectoryNotFound) +} diff --git a/core/pkg/store/git/manager_test.go b/core/pkg/store/git/manager_test.go new file mode 100644 index 000000000..c7cdbcf3b --- /dev/null +++ b/core/pkg/store/git/manager_test.go @@ -0,0 +1,512 @@ +/* + * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package gitstore + +import ( + "archive/tar" + "context" + "encoding/base64" + "errors" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --- Helper Functions --- + +func newTestManager(t *testing.T) (Manager, string) { + t.Helper() + rootDir := t.TempDir() + mgr, err := New(rootDir) + require.NoError(t, err) + return mgr, rootDir +} + +type testGitRepoInfo struct { + RepoDir string + MainCommitHash string + FeatureCommit string + TagName string + BranchName string +} + +func initTestGitRepo(t *testing.T, files, symlinks map[string]string) *testGitRepoInfo { + t.Helper() + repoDir := t.TempDir() + + gitRepo, err := git.PlainInit(repoDir, false) + require.NoError(t, err) + + wt, err := gitRepo.Worktree() + require.NoError(t, err) + + // Write files + for relPath, content := range files { + fullPath := filepath.Join(repoDir, relPath) + require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755)) + require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o644)) + _, err = wt.Add(relPath) + require.NoError(t, err) + } + + // Write symlinks + for linkName, target := range symlinks { + fullPath := filepath.Join(repoDir, linkName) + require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755)) + require.NoError(t, os.Symlink(target, fullPath)) + _, err = wt.Add(linkName) + require.NoError(t, err) + } + + // Commit on main/master + sig := &object.Signature{ + Name: "Drassi Tester", + Email: "tester@drassi.run", + When: time.Now(), + } + mainHash, err := wt.Commit("Initial commit", &git.CommitOptions{ + Author: sig, + Committer: sig, + }) + require.NoError(t, err) + + // Create a tag + tagName := "v1.0.0" + _, err = gitRepo.CreateTag(tagName, mainHash, &git.CreateTagOptions{ + Tagger: sig, + Message: "Release v1.0.0", + }) + require.NoError(t, err) + + // Create and checkout a feature branch + branchName := "feature-test" + headRef, err := gitRepo.Head() + require.NoError(t, err) + + err = wt.Checkout(&git.CheckoutOptions{ + Hash: headRef.Hash(), + Branch: plumbing.NewBranchReferenceName(branchName), + Create: true, + }) + require.NoError(t, err) + + // Add a file on the feature branch + featureFilePath := filepath.Join(repoDir, "feature.txt") + require.NoError(t, os.WriteFile(featureFilePath, []byte("feature branch content"), 0o644)) + _, err = wt.Add("feature.txt") + require.NoError(t, err) + + featHash, err := wt.Commit("Feature commit", &git.CommitOptions{ + Author: sig, + Committer: sig, + }) + require.NoError(t, err) + + // Checkout back to main + err = wt.Checkout(&git.CheckoutOptions{ + Branch: headRef.Name(), + }) + require.NoError(t, err) + + return &testGitRepoInfo{ + RepoDir: repoDir, + MainCommitHash: mainHash.String(), + FeatureCommit: featHash.String(), + TagName: tagName, + BranchName: branchName, + } +} + +func assertTarEntries(t *testing.T, r io.Reader, expectedEntries, expectedSymlinks map[string]string) { + t.Helper() + entries := make(map[string]string) + symlinks := make(map[string]string) + tr := tar.NewReader(r) + + for { + hdr, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + require.NoError(t, err) + + if hdr.Typeflag == tar.TypeSymlink { + symlinks[hdr.Name] = hdr.Linkname + } else if hdr.Typeflag == tar.TypeReg { + content, err := io.ReadAll(tr) + require.NoError(t, err) + entries[hdr.Name] = string(content) + } + } + + if expectedEntries != nil { + assert.Equal(t, expectedEntries, entries) + } else { + assert.Empty(t, entries) + } + + if expectedSymlinks != nil { + assert.Equal(t, expectedSymlinks, symlinks) + } else { + assert.Empty(t, symlinks) + } +} + +func makeLocalRepoRef(repoDir, ref string) *RepoReference { + // Trim leading slash to form a valid name + trimmed := strings.TrimPrefix(repoDir, "/") + return &RepoReference{ + Scheme: "git", + Transport: "file", + Endpoint: "localhost", + Name: trimmed, + Ref: ref, + } +} + +// --- Tests --- + +func TestNew(t *testing.T) { + t.Run("success", func(t *testing.T) { + tempDir := t.TempDir() + storeDir := filepath.Join(tempDir, "store") + mgr, err := New(storeDir) + require.NoError(t, err) + require.NotNil(t, mgr) + + info, err := os.Stat(storeDir) + require.NoError(t, err) + assert.True(t, info.IsDir()) + }) + + t.Run("invalid dir error", func(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "existing-file") + require.NoError(t, os.WriteFile(filePath, []byte("data"), 0o644)) + + // Attempting to create manager where root is a child of a file + invalidPath := filepath.Join(filePath, "child") + mgr, err := New(invalidPath) + assert.Error(t, err) + assert.Nil(t, mgr) + }) +} + +func TestFetch(t *testing.T) { + files := map[string]string{ + "action.yml": "name: test action", + "index.js": "console.log('hello')", + } + symlinks := map[string]string{ + "link.js": "index.js", + } + repoInfo := initTestGitRepo(t, files, symlinks) + + t.Run("fetch default branch (HEAD)", func(t *testing.T) { + mgr, _ := newTestManager(t) + ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + + rev, err := mgr.Fetch(context.Background(), ref, "") + require.NoError(t, err) + assert.Equal(t, repoInfo.MainCommitHash, rev) + }) + + t.Run("fetch feature branch", func(t *testing.T) { + mgr, _ := newTestManager(t) + ref := makeLocalRepoRef(repoInfo.RepoDir, repoInfo.BranchName) + + rev, err := mgr.Fetch(context.Background(), ref, "") + require.NoError(t, err) + assert.Equal(t, repoInfo.FeatureCommit, rev) + }) + + t.Run("fetch tag", func(t *testing.T) { + mgr, _ := newTestManager(t) + ref := makeLocalRepoRef(repoInfo.RepoDir, repoInfo.TagName) + + rev, err := mgr.Fetch(context.Background(), ref, "") + require.NoError(t, err) + assert.Equal(t, repoInfo.MainCommitHash, rev) + }) + + t.Run("fetch non-existent ref returns error", func(t *testing.T) { + mgr, _ := newTestManager(t) + ref := makeLocalRepoRef(repoInfo.RepoDir, "nonexistent-branch-404") + + rev, err := mgr.Fetch(context.Background(), ref, "") + assert.Error(t, err) + assert.Empty(t, rev) + }) + + t.Run("fetch with auth token propagates basic auth", func(t *testing.T) { + var receivedAuth string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedAuth = r.Header.Get("Authorization") + w.WriteHeader(http.StatusUnauthorized) + })) + defer server.Close() + + mgr, _ := newTestManager(t) + ref := &RepoReference{ + Transport: "http", + Endpoint: strings.TrimPrefix(server.URL, "http://"), + Name: "test/repo", + Ref: "HEAD", + } + + const testToken = "secret-token-123" + _, err := mgr.Fetch(context.Background(), ref, testToken) + assert.Error(t, err) // server returns 401 + expectedAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("token:"+testToken)) + assert.Equal(t, expectedAuth, receivedAuth) + }) + + t.Run("fetch non-existent repository returns error", func(t *testing.T) { + mgr, _ := newTestManager(t) + ref := makeLocalRepoRef(filepath.Join(t.TempDir(), "does-not-exist"), "HEAD") + + rev, err := mgr.Fetch(context.Background(), ref, "") + assert.Error(t, err) + assert.Empty(t, rev) + }) +} + +func TestRead(t *testing.T) { + files := map[string]string{ + "action.yml": "name: test-action\ndescription: test", + "src/index.js": "console.log('test');", + "nested/dir/file.md": "# Docs", + } + symlinks := map[string]string{ + "src/alias.js": "index.js", + } + repoInfo := initTestGitRepo(t, files, symlinks) + + mgr, _ := newTestManager(t) + ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + rev, err := mgr.Fetch(context.Background(), ref, "") + require.NoError(t, err) + + t.Run("read full archive with directory prefix", func(t *testing.T) { + prefix := "actions/my-action@v1" + rc, err := mgr.Read(context.Background(), ref, rev, prefix) + require.NoError(t, err) + defer rc.Close() + + expectedEntries := map[string]string{ + "actions/my-action@v1/action.yml": files["action.yml"], + "actions/my-action@v1/src/index.js": files["src/index.js"], + "actions/my-action@v1/nested/dir/file.md": files["nested/dir/file.md"], + } + expectedSymlinks := map[string]string{ + "actions/my-action@v1/src/alias.js": "index.js", + } + + assertTarEntries(t, rc, expectedEntries, expectedSymlinks) + }) + + t.Run("read full archive with empty prefix", func(t *testing.T) { + rc, err := mgr.Read(context.Background(), ref, rev, "") + require.NoError(t, err) + defer rc.Close() + + assertTarEntries(t, rc, files, symlinks) + }) + + t.Run("read non-existent revision error", func(t *testing.T) { + nonExistentRev := strings.Repeat("0", 40) + rc, err := mgr.Read(context.Background(), ref, nonExistentRev, "") + assert.Error(t, err) + assert.Nil(t, rc) + }) + + t.Run("read non-existent repo error", func(t *testing.T) { + unknownRef := &RepoReference{ + Endpoint: "unknown.com", + Name: "unknown/repo", + Ref: "main", + } + rc, err := mgr.Read(context.Background(), unknownRef, rev, "") + assert.Error(t, err) + assert.Nil(t, rc) + }) + + t.Run("context cancellation during read", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel immediately + + rc, err := mgr.Read(ctx, ref, rev, "") + require.NoError(t, err) + defer rc.Close() + + _, readErr := io.ReadAll(rc) + assert.Error(t, readErr) + assert.True(t, errors.Is(readErr, context.Canceled) || errors.Is(readErr, io.ErrClosedPipe)) + }) +} + +func TestFile(t *testing.T) { + files := map[string]string{ + "action.yml": "name: my-action", + "src/index.js": "console.log('ok')", + "nested/sub/data.json": `{"key": "value"}`, + } + repoInfo := initTestGitRepo(t, files, nil) + + mgr, _ := newTestManager(t) + ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + rev, err := mgr.Fetch(context.Background(), ref, "") + require.NoError(t, err) + + t.Run("read existing regular file", func(t *testing.T) { + rc, err := mgr.File(context.Background(), ref, rev, "action.yml") + require.NoError(t, err) + defer rc.Close() + + content, err := io.ReadAll(rc) + require.NoError(t, err) + assert.Equal(t, files["action.yml"], string(content)) + }) + + t.Run("read nested file with leading slash and relative dots", func(t *testing.T) { + testPaths := []string{ + "nested/sub/data.json", + "/nested/sub/data.json", + "./nested/sub/data.json", + "nested/../nested/sub/data.json", + } + for _, p := range testPaths { + rc, err := mgr.File(context.Background(), ref, rev, p) + require.NoError(t, err, "path: %s", p) + content, err := io.ReadAll(rc) + require.NoError(t, err) + assert.Equal(t, files["nested/sub/data.json"], string(content)) + _ = rc.Close() + } + }) + + t.Run("read non-existent file error", func(t *testing.T) { + rc, err := mgr.File(context.Background(), ref, rev, "does-not-exist.txt") + assert.Error(t, err) + assert.Nil(t, rc) + assert.True(t, errors.Is(err, object.ErrFileNotFound)) + }) + + t.Run("read directory as file error", func(t *testing.T) { + rc, err := mgr.File(context.Background(), ref, rev, "src") + assert.Error(t, err) + assert.Nil(t, rc) + assert.Contains(t, err.Error(), "not a (regular) file") + }) + + t.Run("read file with invalid revision error", func(t *testing.T) { + invalidRev := strings.Repeat("a", 40) + rc, err := mgr.File(context.Background(), ref, invalidRev, "action.yml") + assert.Error(t, err) + assert.Nil(t, rc) + }) + + t.Run("read file from non-existent repo error", func(t *testing.T) { + unknownRef := &RepoReference{ + Endpoint: "unknown.com", + Name: "unknown/repo", + Ref: "main", + } + rc, err := mgr.File(context.Background(), unknownRef, rev, "action.yml") + assert.Error(t, err) + assert.Nil(t, rc) + }) +} + +func TestPersistence(t *testing.T) { + files := map[string]string{ + "action.yml": "name: persistent-action", + } + repoInfo := initTestGitRepo(t, files, nil) + + rootDir := t.TempDir() + + // Manager 1: Fetches the repo + mgr1, err := New(rootDir) + require.NoError(t, err) + ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + rev, err := mgr1.Fetch(context.Background(), ref, "") + require.NoError(t, err) + + // Manager 2: Created with the same rootDir without calling Fetch + mgr2, err := New(rootDir) + require.NoError(t, err) + + // File lookup should succeed by reading from disk + rc, err := mgr2.File(context.Background(), ref, rev, "action.yml") + require.NoError(t, err) + defer rc.Close() + + content, err := io.ReadAll(rc) + require.NoError(t, err) + assert.Equal(t, files["action.yml"], string(content)) + + // Read archive should also succeed + tarRc, err := mgr2.Read(context.Background(), ref, rev, "") + require.NoError(t, err) + defer tarRc.Close() + assertTarEntries(t, tarRc, files, nil) +} + +func TestConcurrency(t *testing.T) { + files := map[string]string{ + "file1.txt": "content1", + "file2.txt": "content2", + } + repoInfo := initTestGitRepo(t, files, nil) + + mgr, _ := newTestManager(t) + ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + + const workers = 10 + var wg sync.WaitGroup + wg.Add(workers) + + for i := 0; i < workers; i++ { + go func(id int) { + defer wg.Done() + ctx := context.Background() + + // Concurrently fetch + rev, err := mgr.Fetch(ctx, ref, "") + require.NoError(t, err) + + // Concurrently read file + rc, err := mgr.File(ctx, ref, rev, "file1.txt") + require.NoError(t, err) + content, err := io.ReadAll(rc) + require.NoError(t, err) + assert.Equal(t, "content1", string(content)) + _ = rc.Close() + + // Concurrently read tar + tarRc, err := mgr.Read(ctx, ref, rev, "") + require.NoError(t, err) + assertTarEntries(t, tarRc, files, nil) + _ = tarRc.Close() + }(i) + } + + wg.Wait() +} diff --git a/core/pkg/store/git/repository.go b/core/pkg/store/git/repository.go index b53c8e2b7..95b8cc547 100644 --- a/core/pkg/store/git/repository.go +++ b/core/pkg/store/git/repository.go @@ -24,7 +24,7 @@ type RepoReference struct { Transport string // e.g. http, https, ssh Endpoint string // e.g. github.com Name string // e.g. actions/checkout - Path string // e.g .github/actions/hello-world-action + Path string // e.g. github/actions/checkout Ref string // e.g. v3 } From 88cd83a9df7507bdf1c8f1fb82e67ed0623f857a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Tue, 8 Sep 2026 16:31:15 +0700 Subject: [PATCH 3/5] [core/gitstore] add `manager.Close` --- core/pkg/store/git/manager.go | 43 +++++++++++++++++++++--------- core/pkg/store/git/manager_test.go | 22 +++++++++++++++ 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/core/pkg/store/git/manager.go b/core/pkg/store/git/manager.go index a941530ff..50fa480ce 100644 --- a/core/pkg/store/git/manager.go +++ b/core/pkg/store/git/manager.go @@ -14,6 +14,7 @@ import ( "fmt" "io" "io/fs" + "maps" "os" "path" "strings" @@ -42,6 +43,7 @@ type Manager interface { Fetch(ctx context.Context, repo *RepoReference, token string) (rev string, err error) Read(ctx context.Context, repo *RepoReference, rev, dir string) (io.ReadCloser, error) File(ctx context.Context, repo *RepoReference, rev, path string) (io.ReadCloser, error) + Close() error } const remoteName string = "anonymous" @@ -76,12 +78,12 @@ type manager struct { func (m *manager) Fetch(ctx context.Context, repo *RepoReference, token string) (string, error) { key := Location(repo) v, err, _ := m.sf.Do(key, func() (any, error) { - path, err := m.ensureDir(repo) + repoPath, err := m.ensureDir(repo) if err != nil { return "", err } - gitRepo, err := m.ensureRepo(path, repo) + gitRepo, err := m.ensureRepo(repoPath, repo) if err != nil { return "", err } @@ -194,8 +196,8 @@ func (m *manager) getRepo(repo *RepoReference) (*git.Repository, error) { return gitRepo, nil } - path := xstring.EnsureSuffix(id, ".git") - dot, err := m.fsys.Chroot(path) + repoPath := xstring.EnsureSuffix(id, ".git") + dot, err := m.fsys.Chroot(repoPath) if err != nil { return nil, fmt.Errorf("repo %q not found: %w", id, err) } @@ -245,25 +247,25 @@ func (m *manager) fetch(ctx context.Context, gitRepo *git.Repository, repo *Repo } func (m *manager) ensureDir(repo *RepoReference) (string, error) { - path := FullName(repo) - path = xstring.EnsureSuffix(path, ".git") - fileInfo, err := m.fsys.Stat(path) + id := FullName(repo) + repoPath := xstring.EnsureSuffix(id, ".git") + fileInfo, err := m.fsys.Stat(repoPath) if err != nil { - if os.IsNotExist(err) { - return path, m.fsys.MkdirAll(path, xfs.DirPerm) + if errors.Is(err, fs.ErrNotExist) { + return repoPath, m.fsys.MkdirAll(repoPath, xfs.DirPerm) } return "", err } if !fileInfo.IsDir() { - if err = util.RemoveAll(m.fsys, path); err != nil { + if err = util.RemoveAll(m.fsys, repoPath); err != nil { return "", err } - return path, m.fsys.MkdirAll(path, xfs.DirPerm) + return repoPath, m.fsys.MkdirAll(repoPath, xfs.DirPerm) } - return path, nil + return repoPath, nil } func (m *manager) ensureRepo(path string, repo *RepoReference) (*git.Repository, error) { @@ -339,6 +341,23 @@ func newTarHandler(tw *tar.Writer, dir string) tarHandler { return h } +func (m *manager) Close() error { + m.mu.Lock() + repos := maps.Clone(m.repos) + clear(m.repos) + m.mu.Unlock() + + var errs []error + for _, repo := range repos { + if c, ok := repo.Storer.(io.Closer); ok { + if err := c.Close(); err != nil { + errs = append(errs, err) + } + } + } + return errors.Join(errs...) +} + func notFoundErr(err error) bool { return errors.Is(err, object.ErrFileNotFound) || errors.Is(err, object.ErrEntryNotFound) || diff --git a/core/pkg/store/git/manager_test.go b/core/pkg/store/git/manager_test.go index c7cdbcf3b..837b5ad87 100644 --- a/core/pkg/store/git/manager_test.go +++ b/core/pkg/store/git/manager_test.go @@ -510,3 +510,25 @@ func TestConcurrency(t *testing.T) { wg.Wait() } + +func TestClose(t *testing.T) { + files := map[string]string{ + "action.yml": "name: close-test", + } + repoInfo := initTestGitRepo(t, files, nil) + + mgr, _ := newTestManager(t) + ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + + // Fetch repository to ensure it is opened and stored in manager + _, err := mgr.Fetch(context.Background(), ref, "") + require.NoError(t, err) + + // Close the manager + err = mgr.Close() + require.NoError(t, err) + + // Calling Close again should also succeed without error + err = mgr.Close() + require.NoError(t, err) +} From ebd23fbdea2b4b9ae0f5393f06338726c309f501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Tue, 8 Sep 2026 16:54:09 +0700 Subject: [PATCH 4/5] [core/gitstore] refactor tests to use TestSuite --- core/pkg/store/git/manager_test.go | 398 ++++++++++++++------------ core/pkg/store/git/repository_test.go | 67 +++-- 2 files changed, 254 insertions(+), 211 deletions(-) diff --git a/core/pkg/store/git/manager_test.go b/core/pkg/store/git/manager_test.go index 837b5ad87..0ccd2d60f 100644 --- a/core/pkg/store/git/manager_test.go +++ b/core/pkg/store/git/manager_test.go @@ -24,20 +24,34 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" ) -// --- Helper Functions --- +func TestManagerSuite(t *testing.T) { + suite.Run(t, new(ManagerTestSuite)) +} -func newTestManager(t *testing.T) (Manager, string) { - t.Helper() - rootDir := t.TempDir() - mgr, err := New(rootDir) - require.NoError(t, err) - return mgr, rootDir +type ManagerTestSuite struct { + suite.Suite + mgr Manager + rootDir string +} + +func (s *ManagerTestSuite) SetupTest() { + s.rootDir = s.T().TempDir() + mgr, err := New(s.rootDir) + s.Require().NoError(err) + s.mgr = mgr +} + +func (s *ManagerTestSuite) TearDownTest() { + if s.mgr != nil { + _ = s.mgr.Close() + } } +// --- Helper Methods --- + type testGitRepoInfo struct { RepoDir string MainCommitHash string @@ -46,35 +60,35 @@ type testGitRepoInfo struct { BranchName string } -func initTestGitRepo(t *testing.T, files, symlinks map[string]string) *testGitRepoInfo { - t.Helper() - repoDir := t.TempDir() +func (s *ManagerTestSuite) initTestGitRepo(files, symlinks map[string]string) *testGitRepoInfo { + s.T().Helper() + repoDir := s.T().TempDir() gitRepo, err := git.PlainInit(repoDir, false) - require.NoError(t, err) + s.Require().NoError(err) wt, err := gitRepo.Worktree() - require.NoError(t, err) + s.Require().NoError(err) - // Write files + // Write regular files for relPath, content := range files { fullPath := filepath.Join(repoDir, relPath) - require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755)) - require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o644)) + s.Require().NoError(os.MkdirAll(filepath.Dir(fullPath), 0o755)) + s.Require().NoError(os.WriteFile(fullPath, []byte(content), 0o644)) _, err = wt.Add(relPath) - require.NoError(t, err) + s.Require().NoError(err) } // Write symlinks for linkName, target := range symlinks { fullPath := filepath.Join(repoDir, linkName) - require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755)) - require.NoError(t, os.Symlink(target, fullPath)) + s.Require().NoError(os.MkdirAll(filepath.Dir(fullPath), 0o755)) + s.Require().NoError(os.Symlink(target, fullPath)) _, err = wt.Add(linkName) - require.NoError(t, err) + s.Require().NoError(err) } - // Commit on main/master + // Commit on default branch sig := &object.Signature{ Name: "Drassi Tester", Email: "tester@drassi.run", @@ -84,7 +98,7 @@ func initTestGitRepo(t *testing.T, files, symlinks map[string]string) *testGitRe Author: sig, Committer: sig, }) - require.NoError(t, err) + s.Require().NoError(err) // Create a tag tagName := "v1.0.0" @@ -92,37 +106,37 @@ func initTestGitRepo(t *testing.T, files, symlinks map[string]string) *testGitRe Tagger: sig, Message: "Release v1.0.0", }) - require.NoError(t, err) + s.Require().NoError(err) // Create and checkout a feature branch branchName := "feature-test" headRef, err := gitRepo.Head() - require.NoError(t, err) + s.Require().NoError(err) err = wt.Checkout(&git.CheckoutOptions{ Hash: headRef.Hash(), Branch: plumbing.NewBranchReferenceName(branchName), Create: true, }) - require.NoError(t, err) + s.Require().NoError(err) // Add a file on the feature branch featureFilePath := filepath.Join(repoDir, "feature.txt") - require.NoError(t, os.WriteFile(featureFilePath, []byte("feature branch content"), 0o644)) + s.Require().NoError(os.WriteFile(featureFilePath, []byte("feature branch content"), 0o644)) _, err = wt.Add("feature.txt") - require.NoError(t, err) + s.Require().NoError(err) featHash, err := wt.Commit("Feature commit", &git.CommitOptions{ Author: sig, Committer: sig, }) - require.NoError(t, err) + s.Require().NoError(err) // Checkout back to main err = wt.Checkout(&git.CheckoutOptions{ Branch: headRef.Name(), }) - require.NoError(t, err) + s.Require().NoError(err) return &testGitRepoInfo{ RepoDir: repoDir, @@ -133,8 +147,8 @@ func initTestGitRepo(t *testing.T, files, symlinks map[string]string) *testGitRe } } -func assertTarEntries(t *testing.T, r io.Reader, expectedEntries, expectedSymlinks map[string]string) { - t.Helper() +func (s *ManagerTestSuite) assertTarEntries(r io.Reader, expectedEntries, expectedSymlinks map[string]string) { + s.T().Helper() entries := make(map[string]string) symlinks := make(map[string]string) tr := tar.NewReader(r) @@ -144,32 +158,31 @@ func assertTarEntries(t *testing.T, r io.Reader, expectedEntries, expectedSymlin if errors.Is(err, io.EOF) { break } - require.NoError(t, err) + s.Require().NoError(err) if hdr.Typeflag == tar.TypeSymlink { symlinks[hdr.Name] = hdr.Linkname } else if hdr.Typeflag == tar.TypeReg { content, err := io.ReadAll(tr) - require.NoError(t, err) + s.Require().NoError(err) entries[hdr.Name] = string(content) } } if expectedEntries != nil { - assert.Equal(t, expectedEntries, entries) + s.Assert().Equal(expectedEntries, entries) } else { - assert.Empty(t, entries) + s.Assert().Empty(entries) } if expectedSymlinks != nil { - assert.Equal(t, expectedSymlinks, symlinks) + s.Assert().Equal(expectedSymlinks, symlinks) } else { - assert.Empty(t, symlinks) + s.Assert().Empty(symlinks) } } -func makeLocalRepoRef(repoDir, ref string) *RepoReference { - // Trim leading slash to form a valid name +func (s *ManagerTestSuite) makeLocalRepoRef(repoDir, ref string) *RepoReference { trimmed := strings.TrimPrefix(repoDir, "/") return &RepoReference{ Scheme: "git", @@ -180,35 +193,43 @@ func makeLocalRepoRef(repoDir, ref string) *RepoReference { } } +func (s *ManagerTestSuite) newTestManager() (Manager, string) { + s.T().Helper() + rootDir := s.T().TempDir() + mgr, err := New(rootDir) + s.Require().NoError(err) + return mgr, rootDir +} + // --- Tests --- -func TestNew(t *testing.T) { - t.Run("success", func(t *testing.T) { - tempDir := t.TempDir() +func (s *ManagerTestSuite) TestNew() { + s.Run("success", func() { + tempDir := s.T().TempDir() storeDir := filepath.Join(tempDir, "store") mgr, err := New(storeDir) - require.NoError(t, err) - require.NotNil(t, mgr) + s.Require().NoError(err) + s.Require().NotNil(mgr) info, err := os.Stat(storeDir) - require.NoError(t, err) - assert.True(t, info.IsDir()) + s.Require().NoError(err) + s.Assert().True(info.IsDir()) }) - t.Run("invalid dir error", func(t *testing.T) { - tempDir := t.TempDir() + s.Run("invalid dir error", func() { + tempDir := s.T().TempDir() filePath := filepath.Join(tempDir, "existing-file") - require.NoError(t, os.WriteFile(filePath, []byte("data"), 0o644)) + s.Require().NoError(os.WriteFile(filePath, []byte("data"), 0o644)) // Attempting to create manager where root is a child of a file invalidPath := filepath.Join(filePath, "child") mgr, err := New(invalidPath) - assert.Error(t, err) - assert.Nil(t, mgr) + s.Assert().Error(err) + s.Assert().Nil(mgr) }) } -func TestFetch(t *testing.T) { +func (s *ManagerTestSuite) TestFetch() { files := map[string]string{ "action.yml": "name: test action", "index.js": "console.log('hello')", @@ -216,45 +237,41 @@ func TestFetch(t *testing.T) { symlinks := map[string]string{ "link.js": "index.js", } - repoInfo := initTestGitRepo(t, files, symlinks) + repoInfo := s.initTestGitRepo(files, symlinks) - t.Run("fetch default branch (HEAD)", func(t *testing.T) { - mgr, _ := newTestManager(t) - ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + s.Run("fetch default branch (HEAD)", func() { + ref := s.makeLocalRepoRef(repoInfo.RepoDir, "HEAD") - rev, err := mgr.Fetch(context.Background(), ref, "") - require.NoError(t, err) - assert.Equal(t, repoInfo.MainCommitHash, rev) + rev, err := s.mgr.Fetch(s.T().Context(), ref, "") + s.Require().NoError(err) + s.Assert().Equal(repoInfo.MainCommitHash, rev) }) - t.Run("fetch feature branch", func(t *testing.T) { - mgr, _ := newTestManager(t) - ref := makeLocalRepoRef(repoInfo.RepoDir, repoInfo.BranchName) + s.Run("fetch feature branch", func() { + ref := s.makeLocalRepoRef(repoInfo.RepoDir, repoInfo.BranchName) - rev, err := mgr.Fetch(context.Background(), ref, "") - require.NoError(t, err) - assert.Equal(t, repoInfo.FeatureCommit, rev) + rev, err := s.mgr.Fetch(s.T().Context(), ref, "") + s.Require().NoError(err) + s.Assert().Equal(repoInfo.FeatureCommit, rev) }) - t.Run("fetch tag", func(t *testing.T) { - mgr, _ := newTestManager(t) - ref := makeLocalRepoRef(repoInfo.RepoDir, repoInfo.TagName) + s.Run("fetch tag", func() { + ref := s.makeLocalRepoRef(repoInfo.RepoDir, repoInfo.TagName) - rev, err := mgr.Fetch(context.Background(), ref, "") - require.NoError(t, err) - assert.Equal(t, repoInfo.MainCommitHash, rev) + rev, err := s.mgr.Fetch(s.T().Context(), ref, "") + s.Require().NoError(err) + s.Assert().Equal(repoInfo.MainCommitHash, rev) }) - t.Run("fetch non-existent ref returns error", func(t *testing.T) { - mgr, _ := newTestManager(t) - ref := makeLocalRepoRef(repoInfo.RepoDir, "nonexistent-branch-404") + s.Run("fetch non-existent ref returns error", func() { + ref := s.makeLocalRepoRef(repoInfo.RepoDir, "nonexistent-branch-404") - rev, err := mgr.Fetch(context.Background(), ref, "") - assert.Error(t, err) - assert.Empty(t, rev) + rev, err := s.mgr.Fetch(s.T().Context(), ref, "") + s.Assert().Error(err) + s.Assert().Empty(rev) }) - t.Run("fetch with auth token propagates basic auth", func(t *testing.T) { + s.Run("fetch with auth token propagates basic auth", func() { var receivedAuth string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { receivedAuth = r.Header.Get("Authorization") @@ -262,7 +279,6 @@ func TestFetch(t *testing.T) { })) defer server.Close() - mgr, _ := newTestManager(t) ref := &RepoReference{ Transport: "http", Endpoint: strings.TrimPrefix(server.URL, "http://"), @@ -271,23 +287,22 @@ func TestFetch(t *testing.T) { } const testToken = "secret-token-123" - _, err := mgr.Fetch(context.Background(), ref, testToken) - assert.Error(t, err) // server returns 401 + _, err := s.mgr.Fetch(s.T().Context(), ref, testToken) + s.Assert().Error(err) expectedAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("token:"+testToken)) - assert.Equal(t, expectedAuth, receivedAuth) + s.Assert().Equal(expectedAuth, receivedAuth) }) - t.Run("fetch non-existent repository returns error", func(t *testing.T) { - mgr, _ := newTestManager(t) - ref := makeLocalRepoRef(filepath.Join(t.TempDir(), "does-not-exist"), "HEAD") + s.Run("fetch non-existent repository returns error", func() { + ref := s.makeLocalRepoRef(filepath.Join(s.T().TempDir(), "does-not-exist"), "HEAD") - rev, err := mgr.Fetch(context.Background(), ref, "") - assert.Error(t, err) - assert.Empty(t, rev) + rev, err := s.mgr.Fetch(s.T().Context(), ref, "") + s.Assert().Error(err) + s.Assert().Empty(rev) }) } -func TestRead(t *testing.T) { +func (s *ManagerTestSuite) TestRead() { files := map[string]string{ "action.yml": "name: test-action\ndescription: test", "src/index.js": "console.log('test');", @@ -296,17 +311,16 @@ func TestRead(t *testing.T) { symlinks := map[string]string{ "src/alias.js": "index.js", } - repoInfo := initTestGitRepo(t, files, symlinks) + repoInfo := s.initTestGitRepo(files, symlinks) - mgr, _ := newTestManager(t) - ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") - rev, err := mgr.Fetch(context.Background(), ref, "") - require.NoError(t, err) + ref := s.makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + rev, err := s.mgr.Fetch(s.T().Context(), ref, "") + s.Require().NoError(err) - t.Run("read full archive with directory prefix", func(t *testing.T) { + s.Run("read full archive with directory prefix", func() { prefix := "actions/my-action@v1" - rc, err := mgr.Read(context.Background(), ref, rev, prefix) - require.NoError(t, err) + rc, err := s.mgr.Read(s.T().Context(), ref, rev, prefix) + s.Require().NoError(err) defer rc.Close() expectedEntries := map[string]string{ @@ -318,73 +332,77 @@ func TestRead(t *testing.T) { "actions/my-action@v1/src/alias.js": "index.js", } - assertTarEntries(t, rc, expectedEntries, expectedSymlinks) + s.assertTarEntries(rc, expectedEntries, expectedSymlinks) }) - t.Run("read full archive with empty prefix", func(t *testing.T) { - rc, err := mgr.Read(context.Background(), ref, rev, "") - require.NoError(t, err) + s.Run("read full archive with empty prefix", func() { + rc, err := s.mgr.Read(s.T().Context(), ref, rev, "") + s.Require().NoError(err) defer rc.Close() - assertTarEntries(t, rc, files, symlinks) + s.assertTarEntries(rc, files, symlinks) }) - t.Run("read non-existent revision error", func(t *testing.T) { + s.Run("read non-existent revision error", func() { nonExistentRev := strings.Repeat("0", 40) - rc, err := mgr.Read(context.Background(), ref, nonExistentRev, "") - assert.Error(t, err) - assert.Nil(t, rc) + rc, err := s.mgr.Read(s.T().Context(), ref, nonExistentRev, "") + s.Assert().Error(err) + s.Assert().Nil(rc) }) - t.Run("read non-existent repo error", func(t *testing.T) { + s.Run("read non-existent repo error", func() { unknownRef := &RepoReference{ Endpoint: "unknown.com", Name: "unknown/repo", Ref: "main", } - rc, err := mgr.Read(context.Background(), unknownRef, rev, "") - assert.Error(t, err) - assert.Nil(t, rc) + rc, err := s.mgr.Read(s.T().Context(), unknownRef, rev, "") + s.Assert().Error(err) + s.Assert().Nil(rc) }) - t.Run("context cancellation during read", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - cancel() // cancel immediately + s.Run("context cancellation during read", func() { + cancelMgr, _ := s.newTestManager() + + cancelRev, err := cancelMgr.Fetch(s.T().Context(), ref, "") + s.Require().NoError(err) + + ctx, cancel := context.WithCancel(s.T().Context()) + cancel() - rc, err := mgr.Read(ctx, ref, rev, "") - require.NoError(t, err) + rc, err := cancelMgr.Read(ctx, ref, cancelRev, "") + s.Require().NoError(err) defer rc.Close() _, readErr := io.ReadAll(rc) - assert.Error(t, readErr) - assert.True(t, errors.Is(readErr, context.Canceled) || errors.Is(readErr, io.ErrClosedPipe)) + s.Assert().Error(readErr) + s.Assert().True(errors.Is(readErr, context.Canceled) || errors.Is(readErr, io.ErrClosedPipe)) }) } -func TestFile(t *testing.T) { +func (s *ManagerTestSuite) TestFile() { files := map[string]string{ "action.yml": "name: my-action", "src/index.js": "console.log('ok')", "nested/sub/data.json": `{"key": "value"}`, } - repoInfo := initTestGitRepo(t, files, nil) + repoInfo := s.initTestGitRepo(files, nil) - mgr, _ := newTestManager(t) - ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") - rev, err := mgr.Fetch(context.Background(), ref, "") - require.NoError(t, err) + ref := s.makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + rev, err := s.mgr.Fetch(s.T().Context(), ref, "") + s.Require().NoError(err) - t.Run("read existing regular file", func(t *testing.T) { - rc, err := mgr.File(context.Background(), ref, rev, "action.yml") - require.NoError(t, err) + s.Run("read existing regular file", func() { + rc, err := s.mgr.File(s.T().Context(), ref, rev, "action.yml") + s.Require().NoError(err) defer rc.Close() content, err := io.ReadAll(rc) - require.NoError(t, err) - assert.Equal(t, files["action.yml"], string(content)) + s.Require().NoError(err) + s.Assert().Equal(files["action.yml"], string(content)) }) - t.Run("read nested file with leading slash and relative dots", func(t *testing.T) { + s.Run("read nested file with leading slash and relative dots", func() { testPaths := []string{ "nested/sub/data.json", "/nested/sub/data.json", @@ -392,92 +410,91 @@ func TestFile(t *testing.T) { "nested/../nested/sub/data.json", } for _, p := range testPaths { - rc, err := mgr.File(context.Background(), ref, rev, p) - require.NoError(t, err, "path: %s", p) + rc, err := s.mgr.File(s.T().Context(), ref, rev, p) + s.Require().NoError(err, "path: %s", p) content, err := io.ReadAll(rc) - require.NoError(t, err) - assert.Equal(t, files["nested/sub/data.json"], string(content)) + s.Require().NoError(err) + s.Assert().Equal(files["nested/sub/data.json"], string(content)) _ = rc.Close() } }) - t.Run("read non-existent file error", func(t *testing.T) { - rc, err := mgr.File(context.Background(), ref, rev, "does-not-exist.txt") - assert.Error(t, err) - assert.Nil(t, rc) - assert.True(t, errors.Is(err, object.ErrFileNotFound)) + s.Run("read non-existent file error", func() { + rc, err := s.mgr.File(s.T().Context(), ref, rev, "does-not-exist.txt") + s.Assert().Error(err) + s.Assert().Nil(rc) + s.Assert().True(os.IsNotExist(err) || notFoundErr(err)) }) - t.Run("read directory as file error", func(t *testing.T) { - rc, err := mgr.File(context.Background(), ref, rev, "src") - assert.Error(t, err) - assert.Nil(t, rc) - assert.Contains(t, err.Error(), "not a (regular) file") + s.Run("read directory as file error", func() { + rc, err := s.mgr.File(s.T().Context(), ref, rev, "src") + s.Assert().Error(err) + s.Assert().Nil(rc) + s.Assert().Contains(err.Error(), "not a (regular) file") }) - t.Run("read file with invalid revision error", func(t *testing.T) { + s.Run("read file with invalid revision error", func() { invalidRev := strings.Repeat("a", 40) - rc, err := mgr.File(context.Background(), ref, invalidRev, "action.yml") - assert.Error(t, err) - assert.Nil(t, rc) + rc, err := s.mgr.File(s.T().Context(), ref, invalidRev, "action.yml") + s.Assert().Error(err) + s.Assert().Nil(rc) }) - t.Run("read file from non-existent repo error", func(t *testing.T) { + s.Run("read file from non-existent repo error", func() { unknownRef := &RepoReference{ Endpoint: "unknown.com", Name: "unknown/repo", Ref: "main", } - rc, err := mgr.File(context.Background(), unknownRef, rev, "action.yml") - assert.Error(t, err) - assert.Nil(t, rc) + rc, err := s.mgr.File(s.T().Context(), unknownRef, rev, "action.yml") + s.Assert().Error(err) + s.Assert().Nil(rc) }) } -func TestPersistence(t *testing.T) { +func (s *ManagerTestSuite) TestPersistence() { files := map[string]string{ "action.yml": "name: persistent-action", } - repoInfo := initTestGitRepo(t, files, nil) + repoInfo := s.initTestGitRepo(files, nil) - rootDir := t.TempDir() + rootDir := s.T().TempDir() // Manager 1: Fetches the repo mgr1, err := New(rootDir) - require.NoError(t, err) - ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") - rev, err := mgr1.Fetch(context.Background(), ref, "") - require.NoError(t, err) + s.Require().NoError(err) + ref := s.makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + rev, err := mgr1.Fetch(s.T().Context(), ref, "") + s.Require().NoError(err) // Manager 2: Created with the same rootDir without calling Fetch mgr2, err := New(rootDir) - require.NoError(t, err) + s.Require().NoError(err) // File lookup should succeed by reading from disk - rc, err := mgr2.File(context.Background(), ref, rev, "action.yml") - require.NoError(t, err) + rc, err := mgr2.File(s.T().Context(), ref, rev, "action.yml") + s.Require().NoError(err) defer rc.Close() content, err := io.ReadAll(rc) - require.NoError(t, err) - assert.Equal(t, files["action.yml"], string(content)) + s.Require().NoError(err) + s.Assert().Equal(files["action.yml"], string(content)) // Read archive should also succeed - tarRc, err := mgr2.Read(context.Background(), ref, rev, "") - require.NoError(t, err) + tarRc, err := mgr2.Read(s.T().Context(), ref, rev, "") + s.Require().NoError(err) defer tarRc.Close() - assertTarEntries(t, tarRc, files, nil) + s.assertTarEntries(tarRc, files, nil) } -func TestConcurrency(t *testing.T) { +func (s *ManagerTestSuite) TestConcurrency() { files := map[string]string{ "file1.txt": "content1", "file2.txt": "content2", } - repoInfo := initTestGitRepo(t, files, nil) + repoInfo := s.initTestGitRepo(files, nil) - mgr, _ := newTestManager(t) - ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + ref := s.makeLocalRepoRef(repoInfo.RepoDir, "HEAD") const workers = 10 var wg sync.WaitGroup @@ -486,24 +503,24 @@ func TestConcurrency(t *testing.T) { for i := 0; i < workers; i++ { go func(id int) { defer wg.Done() - ctx := context.Background() + ctx := s.T().Context() // Concurrently fetch - rev, err := mgr.Fetch(ctx, ref, "") - require.NoError(t, err) + rev, err := s.mgr.Fetch(ctx, ref, "") + s.Require().NoError(err) // Concurrently read file - rc, err := mgr.File(ctx, ref, rev, "file1.txt") - require.NoError(t, err) + rc, err := s.mgr.File(ctx, ref, rev, "file1.txt") + s.Require().NoError(err) content, err := io.ReadAll(rc) - require.NoError(t, err) - assert.Equal(t, "content1", string(content)) + s.Require().NoError(err) + s.Assert().Equal("content1", string(content)) _ = rc.Close() // Concurrently read tar - tarRc, err := mgr.Read(ctx, ref, rev, "") - require.NoError(t, err) - assertTarEntries(t, tarRc, files, nil) + tarRc, err := s.mgr.Read(ctx, ref, rev, "") + s.Require().NoError(err) + s.assertTarEntries(tarRc, files, nil) _ = tarRc.Close() }(i) } @@ -511,24 +528,23 @@ func TestConcurrency(t *testing.T) { wg.Wait() } -func TestClose(t *testing.T) { +func (s *ManagerTestSuite) TestClose() { files := map[string]string{ "action.yml": "name: close-test", } - repoInfo := initTestGitRepo(t, files, nil) + repoInfo := s.initTestGitRepo(files, nil) - mgr, _ := newTestManager(t) - ref := makeLocalRepoRef(repoInfo.RepoDir, "HEAD") + ref := s.makeLocalRepoRef(repoInfo.RepoDir, "HEAD") // Fetch repository to ensure it is opened and stored in manager - _, err := mgr.Fetch(context.Background(), ref, "") - require.NoError(t, err) + _, err := s.mgr.Fetch(s.T().Context(), ref, "") + s.Require().NoError(err) // Close the manager - err = mgr.Close() - require.NoError(t, err) + err = s.mgr.Close() + s.Require().NoError(err) // Calling Close again should also succeed without error - err = mgr.Close() - require.NoError(t, err) + err = s.mgr.Close() + s.Require().NoError(err) } diff --git a/core/pkg/store/git/repository_test.go b/core/pkg/store/git/repository_test.go index 2d67e6c14..69ca87008 100644 --- a/core/pkg/store/git/repository_test.go +++ b/core/pkg/store/git/repository_test.go @@ -10,16 +10,19 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -func TestParse(t *testing.T) { - t.Run("scheme", testParseScheme) - t.Run("endpoint", testEndpoint) +func TestRepositorySuite(t *testing.T) { + suite.Run(t, new(RepositoryTestSuite)) } -func testParseScheme(t *testing.T) { - t.Run("success", func(tt *testing.T) { +type RepositoryTestSuite struct { + suite.Suite +} + +func (s *RepositoryTestSuite) TestParseScheme() { + s.Run("success", func() { tests := []struct { input string scheme string @@ -42,12 +45,13 @@ func testParseScheme(t *testing.T) { } for _, test := range tests { repo, err := Parse(test.input) - assert.NoError(tt, err, test.input) - assert.Equal(tt, test.scheme, repo.Scheme, test.input) - assert.Equal(tt, test.transport, repo.Transport, test.input) + s.Require().NoError(err, test.input) + s.Assert().Equal(test.scheme, repo.Scheme, test.input) + s.Assert().Equal(test.transport, repo.Transport, test.input) } }) - t.Run("failure", func(tt *testing.T) { + + s.Run("failure", func() { tests := []string{ "+://github.com/action/checkout@main", "git+://github.com/action/checkout@main", @@ -55,18 +59,17 @@ func testParseScheme(t *testing.T) { } for _, test := range tests { _, err := Parse(test) - assert.Error(tt, err, test) + s.Assert().Error(err, test) } }) } -func testEndpoint(t *testing.T) { - t.Run("success", func(tt *testing.T) { +func (s *RepositoryTestSuite) TestEndpoint() { + s.Run("success", func() { endpoints := []string{ "", "gitserver.com", "gitserver.com:8080", "1.2.3.4", "1.2.3.4:8080", - //"2002::1", "[2002::1]:8080", } paths := []string{"", "path/to/action"} for _, ep := range endpoints { @@ -74,14 +77,15 @@ func testEndpoint(t *testing.T) { input := ep + "/action/checkout/" + p input = strings.Trim(input, "/") + "@main" repo, err := Parse(input) - assert.NoError(tt, err, input) - assert.Equal(tt, ep, repo.Endpoint, input) - assert.Equal(tt, "action/checkout", repo.Name, input) - assert.Equal(tt, p, repo.Path, input) + s.Require().NoError(err, input) + s.Assert().Equal(ep, repo.Endpoint, input) + s.Assert().Equal("action/checkout", repo.Name, input) + s.Assert().Equal(p, repo.Path, input) } } }) - t.Run("failure", func(tt *testing.T) { + + s.Run("failure", func() { tests := []string{ "actions@main", "gitserver.com/actions@main", @@ -93,7 +97,30 @@ func testEndpoint(t *testing.T) { } for _, test := range tests { _, err := Parse(test) - assert.Error(tt, err, test) + s.Assert().Error(err, test) } }) } + +func (s *RepositoryTestSuite) TestHelpers() { + ref := &RepoReference{ + Endpoint: "custom.git.org", + Name: "actions/checkout", + Path: "action.yml", + Ref: "v3", + Transport: "https", + } + + s.Assert().Equal("custom.git.org", Endpoint(ref)) + s.Assert().Equal("custom.git.org/actions/checkout", FullName(ref)) + s.Assert().Equal("https://custom.git.org/actions/checkout", Url(ref)) + s.Assert().Equal("custom.git.org/actions/checkout@v3/action.yml", Location(ref)) + + defaultRef := &RepoReference{ + Name: "actions/checkout", + Ref: "main", + } + s.Assert().Equal("github.com", Endpoint(defaultRef)) + s.Assert().Equal("github.com/actions/checkout", FullName(defaultRef)) + s.Assert().Equal("https://github.com/actions/checkout", Url(defaultRef)) +} From 5c89703968c7ed6efab8a0aa92428480c4587116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BA=B7ng=20Minh=20D=C5=A9ng?= Date: Tue, 8 Sep 2026 17:33:02 +0700 Subject: [PATCH 5/5] [core/gitstore] using `expirable.LRU` for repos cache --- core/go.mod | 17 +++++++ core/go.sum | 88 +++++++++++++++++++++++++++++++++++ core/pkg/store/git/manager.go | 64 +++++++++++-------------- core/pkg/store/git/options.go | 32 +++++++++++++ go.work.sum | 53 ++++++++++----------- 5 files changed, 187 insertions(+), 67 deletions(-) create mode 100644 core/pkg/store/git/options.go diff --git a/core/go.mod b/core/go.mod index 203993dcc..c3349abda 100644 --- a/core/go.mod +++ b/core/go.mod @@ -17,6 +17,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/lxc/incus/v6 v6.23.0 github.com/moby/go-archive v0.3.3 github.com/moby/moby/api v1.55.0 @@ -50,19 +51,25 @@ require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.4.1 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/apex/log v1.9.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudflare/circl v1.6.5 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.3.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/cyphar/filepath-securejoin v0.7.0 // indirect github.com/distribution/reference v0.6.0 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.8 // indirect github.com/docker/go-connections v0.8.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect @@ -74,6 +81,7 @@ require ( github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-containerregistry v0.21.6 // indirect github.com/google/go-intervals v0.0.2 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -82,6 +90,9 @@ require ( github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/fs v0.1.0 // indirect + github.com/mattn/go-runewidth v0.0.27 // indirect + github.com/mattn/go-sqlite3 v1.14.48 // indirect + github.com/miekg/pkcs11 v1.1.2 // indirect github.com/mistifyio/go-zfs/v4 v4.0.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/capability v0.4.0 // indirect @@ -106,11 +117,15 @@ require ( github.com/sigstore/sigstore v1.10.8 // indirect github.com/sirupsen/logrus v1.10.2 // indirect github.com/skeema/knownhosts v1.3.3 // indirect + github.com/smallstep/pkcs7 v0.2.1 // indirect + github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect github.com/tchap/go-patricia/v2 v2.3.3 // indirect github.com/ulikunitz/xz v0.5.16 // indirect github.com/urfave/cli v1.22.17 // indirect github.com/vbatts/go-mtree v0.7.0 // indirect github.com/vbatts/tar-split v0.12.3 // indirect + github.com/vbauerster/cupwriter v0.0.4 // indirect + github.com/vbauerster/mpb/v8 v8.14.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect @@ -130,6 +145,8 @@ require ( golang.org/x/text v0.41.0 // indirect golang.org/x/tools v0.49.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.82.0 // indirect google.golang.org/protobuf v1.36.12 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/core/go.sum b/core/go.sum index e3de82945..12791d6ab 100644 --- a/core/go.sum +++ b/core/go.sum @@ -12,6 +12,10 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= @@ -33,6 +37,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chainguard-dev/clog v1.8.1 h1:Lab3GEDsVm1J9XGlpWEBuzXX7eETRmd0vN5PYoNyT+Y= github.com/chainguard-dev/clog v1.8.1/go.mod h1:5MQOZi+Iu7fV7GcJG8ag8rCB5elEOpqRMKEASgnGVdo= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -43,6 +49,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= github.com/containers/ocicrypt v1.3.2 h1:MuqHSfiPpGzoAQdgDSX85FgixSgIm9mSDXTLTgugY5E= @@ -104,7 +112,10 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUv 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.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.21.6 h1:T+yqQIlJXKrM98Om4DlW3GoWQAmhZuLMwoDOvVrtiUM= @@ -126,6 +137,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= @@ -160,7 +173,13 @@ github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcncea github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= +github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= +github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= +github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mistifyio/go-zfs/v4 v4.0.0 h1:sU0+5dX45tdDK5xNZ3HBi95nxUc48FS92qbIZEvpAg4= github.com/mistifyio/go-zfs/v4 v4.0.0/go.mod h1:weotFtXTHvBwhr9Mv96KYnDkTPBOHFUbm9cBmQpesL0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -244,11 +263,15 @@ github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBB github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= github.com/skeema/knownhosts v1.3.3 h1:tiFvsjIW+xqP5cqVsod7atn/xm57OY6v9KwxB1xWUyA= github.com/skeema/knownhosts v1.3.3/go.mod h1:2y0ntYxeqSNxOw5pfqSVDcRyBt2FqfxhzNDPoKz/ifQ= +github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= +github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -283,6 +306,10 @@ github.com/vbatts/go-mtree v0.7.0 h1:ytmOc3MTRidZiBi9VBCyZ2BHe4fZS47L5v7BVXDWW4E github.com/vbatts/go-mtree v0.7.0/go.mod h1:EjdpFC+LZy1TXbRGNa1MKKgjQ+7ew3foMFJK8o4/TdY= github.com/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw= github.com/vbatts/tar-split v0.12.3/go.mod h1:sQOc6OlqGCr7HkGx/IDBeKiTIvqhmj8KffNhEXG4Nq0= +github.com/vbauerster/cupwriter v0.0.4 h1:9sBPe0uXWLZuWQU5lqVbhyFlxX6c09asST/YfatFAys= +github.com/vbauerster/cupwriter v0.0.4/go.mod h1:IFyzS6Xis5dnBH/rdAhrnuzg3c+KkUqEN6yE8lhJlDw= +github.com/vbauerster/mpb/v8 v8.14.0 h1:55SR80dptMfASxIG/oCEkBXgBhxeSu4GrVsjl16oKmA= +github.com/vbauerster/mpb/v8 v8.14.0/go.mod h1:HgpQPKfcWe3kbuGGPmi+jatHreMase5C3Fp5dpdAy0Q= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -294,6 +321,7 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zitadel/oidc/v3 v3.49.4 h1:ROW+QObjcVMLK+z8he4psMMTm73pvycY2qFWQ+DTaLg= github.com/zitadel/oidc/v3 v3.49.4/go.mod h1:KQmYte+zOePAeVwR3LM153dxWBC9orWZAIa4w9r1ZhU= github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= @@ -328,22 +356,46 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -355,25 +407,61 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= diff --git a/core/pkg/store/git/manager.go b/core/pkg/store/git/manager.go index 50fa480ce..dfaa83eae 100644 --- a/core/pkg/store/git/manager.go +++ b/core/pkg/store/git/manager.go @@ -14,11 +14,9 @@ import ( "fmt" "io" "io/fs" - "maps" "os" "path" "strings" - "sync" "drassi.run/core/util/fs" "drassi.run/core/util/path" @@ -35,6 +33,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/storage" "github.com/go-git/go-git/v5/storage/filesystem" + "github.com/hashicorp/golang-lru/v2/expirable" "golang.org/x/sync/singleflight" "k8s.io/apimachinery/pkg/util/rand" ) @@ -48,7 +47,7 @@ type Manager interface { const remoteName string = "anonymous" -func New(rootDir string) (Manager, error) { +func New(rootDir string, opts ...Option) (Manager, error) { if d, err := xpath.ResolveDir(rootDir); err != nil { return nil, err } else { @@ -59,20 +58,33 @@ func New(rootDir string) (Manager, error) { return nil, err } + opt := &options{ + size: defaultSize, + ttl: defaultTTL, + } + for _, o := range opts { + o(opt) + } + m := &manager{ // Using `billy.Filesystem` instead of `rootDir` to // abstract from file system implementations and simplify testing. - fsys: osfs.New(rootDir), - repos: make(map[string]*git.Repository), + fsys: osfs.New(rootDir), } + m.repos = expirable.NewLRU[string, *git.Repository](opt.size, m.onEvict, opt.ttl) return m, nil } type manager struct { fsys billy.Filesystem sf singleflight.Group - mu sync.RWMutex - repos map[string]*git.Repository + repos *expirable.LRU[string, *git.Repository] +} + +func (m *manager) onEvict(_ string, repo *git.Repository) { + if closer, ok := repo.Storer.(io.Closer); ok { + _ = closer.Close() + } } func (m *manager) Fetch(ctx context.Context, repo *RepoReference, token string) (string, error) { @@ -183,16 +195,7 @@ func (m *manager) File(ctx context.Context, repo *RepoReference, rev, filePath s func (m *manager) getRepo(repo *RepoReference) (*git.Repository, error) { id := FullName(repo) - m.mu.RLock() - gitRepo, ok := m.repos[id] - m.mu.RUnlock() - if ok { - return gitRepo, nil - } - - m.mu.Lock() - defer m.mu.Unlock() - if gitRepo, ok := m.repos[id]; ok { + if gitRepo, ok := m.repos.Get(id); ok { return gitRepo, nil } @@ -203,12 +206,12 @@ func (m *manager) getRepo(repo *RepoReference) (*git.Repository, error) { } storer := filesystem.NewStorage(dot, cache.NewObjectLRUDefault()) - gitRepo, err = git.Open(storer, nil) + gitRepo, err := git.Open(storer, nil) if err != nil { return nil, fmt.Errorf("repo %q not found: %w", id, err) } - m.repos[id] = gitRepo + m.repos.Add(id, gitRepo) return gitRepo, nil } @@ -270,10 +273,7 @@ func (m *manager) ensureDir(repo *RepoReference) (string, error) { func (m *manager) ensureRepo(path string, repo *RepoReference) (*git.Repository, error) { id := FullName(repo) - m.mu.Lock() - defer m.mu.Unlock() - - if gitRepo, ok := m.repos[id]; ok { + if gitRepo, ok := m.repos.Get(id); ok { return gitRepo, nil } @@ -293,7 +293,7 @@ func (m *manager) ensureRepo(path string, repo *RepoReference) (*git.Repository, } if gitRepo != nil { - m.repos[id] = gitRepo + m.repos.Add(id, gitRepo) } return gitRepo, err } @@ -342,20 +342,8 @@ func newTarHandler(tw *tar.Writer, dir string) tarHandler { } func (m *manager) Close() error { - m.mu.Lock() - repos := maps.Clone(m.repos) - clear(m.repos) - m.mu.Unlock() - - var errs []error - for _, repo := range repos { - if c, ok := repo.Storer.(io.Closer); ok { - if err := c.Close(); err != nil { - errs = append(errs, err) - } - } - } - return errors.Join(errs...) + m.repos.Purge() + return nil } func notFoundErr(err error) bool { diff --git a/core/pkg/store/git/options.go b/core/pkg/store/git/options.go new file mode 100644 index 000000000..ba750f7ed --- /dev/null +++ b/core/pkg/store/git/options.go @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: (c) 2024 The Drassi Authors + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package gitstore + +import "time" + +const ( + defaultSize = 100 + defaultTTL = 24 * time.Hour +) + +type Option func(*options) +type options struct { + size int + ttl time.Duration +} + +func WithSize(size int) Option { + return func(o *options) { + o.size = size + } +} + +func WithTTL(ttl time.Duration) Option { + return func(o *options) { + o.ttl = ttl + } +} diff --git a/go.work.sum b/go.work.sum index db1ea69a8..2f9d745b4 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= chainguard.dev/go-grpc-kit v0.17.17 h1:Jwhc0zyUwQbC2hNcsi+YMeUX/JUnM+dXVCkTw6wtPzs= chainguard.dev/go-grpc-kit v0.17.17/go.mod h1:qn0meP6RtrbLicE1bgBZnnVU9dvX95eLs0x0T6kZ+b4= chainguard.dev/sdk v0.1.55 h1:QEGW0BVNcvbUkjBFKKCR/34in69PPoV4jZejkN45uu4= @@ -33,6 +35,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg6 github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/FuturFusion/vsock v0.0.0-20260219213046-d78a7104f821 h1:t2eOnMiztYbjWnbwQS5U9rvkzHAPaWelKkE+/FV1C18= github.com/FuturFusion/vsock v0.0.0-20260219213046-d78a7104f821/go.mod h1:0atKpUm0hXZMv6+9Kf5crGqV9KKtvkPAElG7/9CCFwU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= github.com/LINBIT/golinstor v0.60.0 h1:85QhDoLPQXMmqaZ9TKnGFdymu2FKb1Vj37ANVjBxHAQ= github.com/LINBIT/golinstor v0.60.0/go.mod h1:TXAMGiskT4fY/koTCOt6qqF60uWobDqUHoB8srNeCnY= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -49,10 +53,6 @@ github.com/Rican7/retry v0.3.1 h1:scY4IbO8swckzoA/11HgBwaZRJEyY9vaNJshcdhp1Mc= github.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0= github.com/ThalesGroup/crypto11 v1.6.0 h1:Og9EMn44fBS4GNnGnH1aqHnF2wL6F7IU/RhpJajWX/4= github.com/ThalesGroup/crypto11 v1.6.0/go.mod h1:H6LRjN5R5SHxTrLqGNteisLDI0/IC6+SGx1pHtbwizE= -github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= -github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= -github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= -github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g= github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY= github.com/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc= @@ -123,18 +123,20 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= -github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= -github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A= github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/cowsql/go-cowsql v1.22.0 h1:NOMuu3RWkkbKtQ3V+ny9ksR4q3a/h4jU54CbY1BEMBM= github.com/cowsql/go-cowsql v1.22.0/go.mod h1:+QzPcM7QRPIBI8XhsKJ47iUtxGY53lsYGX51G1WQ/4s= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= @@ -153,6 +155,14 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= @@ -214,17 +224,16 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 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 v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo= github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= @@ -267,8 +276,6 @@ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= @@ -298,6 +305,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= @@ -327,15 +336,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.14 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg= github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= -github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875 h1:ql8x//rJsHMjS+qqEag8n3i4azw1QneKh5PieH9UEbY= github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875/go.mod h1:kfOoFJuHWp76v1RgZCb9/gVUc7XdY877S2uVYbNliGc= github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 h1:2oDp6OOhLxQ9JBoUuysVz9UZ9uI6oLUbvAZu0x8o+vE= @@ -353,8 +359,6 @@ github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnE github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= -github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= -github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= @@ -414,6 +418,8 @@ github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG6 github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM= github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= @@ -459,8 +465,6 @@ github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.6 h1:C4rrjnIWR7i9GoIpI2 github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.6/go.mod h1:ejMD/17lMJ4HykQRPdj5NNr+OQYIEZto8HjDKghVMOA= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.6 h1:otSDRygk1TdzEiUBFyaGygT+qdwSnM9RT1i6BQMHmEY= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.6/go.mod h1:Ee/enmyxi/RFLVlajbnjgH2wOWQwlJ0wY8qZrk43hEw= -github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= -github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= github.com/smartystreets/gunit v1.0.0 h1:RyPDUFcJbvtXlhJPk7v+wnxZRY2EUokhEYl2EJOPToI= @@ -474,8 +478,6 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= -github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= -github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/sylabs/sif/v2 v2.24.1 h1:OhTOfTwBaGXfbWYWXK9C6Pojma3A1bTaNJ6CEhyuKok= @@ -508,10 +510,6 @@ github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8 github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/vbauerster/cupwriter v0.0.4 h1:9sBPe0uXWLZuWQU5lqVbhyFlxX6c09asST/YfatFAys= -github.com/vbauerster/cupwriter v0.0.4/go.mod h1:IFyzS6Xis5dnBH/rdAhrnuzg3c+KkUqEN6yE8lhJlDw= -github.com/vbauerster/mpb/v8 v8.14.0 h1:55SR80dptMfASxIG/oCEkBXgBhxeSu4GrVsjl16oKmA= -github.com/vbauerster/mpb/v8 v8.14.0/go.mod h1:HgpQPKfcWe3kbuGGPmi+jatHreMase5C3Fp5dpdAy0Q= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= @@ -532,13 +530,14 @@ github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3R github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zitadel/logging v0.7.0 h1:eugftwMM95Wgqwftsvj81isL0JK/hoScVqp/7iA2adQ= github.com/zitadel/logging v0.7.0/go.mod h1:9A6h9feBF/3u0IhA4uffdzSDY7mBaf7RE78H5sFMINQ= go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA= go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= @@ -581,12 +580,8 @@ google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgn google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= -google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=