diff --git a/internal/assettype/assettype.go b/internal/assettype/assettype.go new file mode 100644 index 0000000..ffd87d0 --- /dev/null +++ b/internal/assettype/assettype.go @@ -0,0 +1,39 @@ +// Copyright 2023 Adevinta + +// Package assettype defines a set of asset types that are valid in +// the context of Lava. +package assettype + +import ( + "slices" + + types "github.com/adevinta/vulcan-types" +) + +// Lava asset types. +const ( + Path = types.AssetType("Path") +) + +// vulcanMap is the mapping between Lava and Vulcan asset types. +var vulcanMap = map[types.AssetType]types.AssetType{ + Path: types.GitRepository, +} + +// lavaTypes is the list of all Lava asset types. +var lavaTypes = []types.AssetType{Path} + +// IsValid reports whether the provided asset type is valid in the +// context of Lava. +func IsValid(at types.AssetType) bool { + return slices.Contains(lavaTypes, at) +} + +// ToVulcan maps a Lava asset type to a Vulcan asset type. If there is +// no such mapping, the provided asset type is returned. +func ToVulcan(at types.AssetType) types.AssetType { + if vt, ok := vulcanMap[at]; ok { + return vt + } + return at +} diff --git a/internal/assettype/assettype_test.go b/internal/assettype/assettype_test.go new file mode 100644 index 0000000..6e81c2a --- /dev/null +++ b/internal/assettype/assettype_test.go @@ -0,0 +1,75 @@ +// Copyright 2023 Adevinta + +package assettype + +import ( + "testing" + + types "github.com/adevinta/vulcan-types" +) + +func TestIsValid(t *testing.T) { + tests := []struct { + name string + at types.AssetType + want bool + }{ + { + name: "lava type", + at: Path, + want: true, + }, + { + name: "vulcan type", + at: types.Hostname, + want: false, + }, + { + name: "zero value", + at: types.AssetType(""), + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsValid(tt.at) + if got != tt.want { + t.Errorf("unexpected value: want: %v, got: %v", tt.want, got) + } + }) + } +} + +func TestToVulcan(t *testing.T) { + tests := []struct { + name string + at types.AssetType + want types.AssetType + }{ + { + name: "lava type", + at: Path, + want: types.GitRepository, + }, + { + name: "vulcan type", + at: types.Hostname, + want: types.Hostname, + }, + { + name: "zero value", + at: types.AssetType(""), + want: types.AssetType(""), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToVulcan(tt.at) + if got != tt.want { + t.Errorf("unexpected value: want: %v, got: %v", tt.want, got) + } + }) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 73f2b0d..599e560 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,6 +15,8 @@ import ( types "github.com/adevinta/vulcan-types" "golang.org/x/mod/semver" "gopkg.in/yaml.v3" + + "github.com/adevinta/lava/internal/assettype" ) var ( @@ -91,7 +93,7 @@ func ParseFile(path string) (Config, error) { } // validate validates the Lava configuration. -func (c *Config) validate() error { +func (c Config) validate() error { // Lava version validation. if !semver.IsValid(c.LavaVersion) { return ErrInvalidLavaVersion @@ -101,15 +103,9 @@ func (c *Config) validate() error { if len(c.Targets) == 0 { return ErrNoTargets } - for _, target := range c.Targets { - if target.Identifier == "" { - return ErrNoTargetIdentifier - } - if target.AssetType == "" { - return ErrNoTargetAssetType - } - if !target.AssetType.IsValid() { - return fmt.Errorf("%w: %v", ErrInvalidAssetType, target.AssetType) + for _, t := range c.Targets { + if err := t.validate(); err != nil { + return err } } return nil @@ -168,6 +164,20 @@ type Target struct { Options map[string]any `yaml:"options"` } +// validate reports whether the target is a valid configuration value. +func (t Target) validate() error { + if t.Identifier == "" { + return ErrNoTargetIdentifier + } + if t.AssetType == "" { + return ErrNoTargetAssetType + } + if !t.AssetType.IsValid() && !assettype.IsValid(t.AssetType) { + return fmt.Errorf("%w: %v", ErrInvalidAssetType, t.AssetType) + } + return nil +} + // RegistryAuth contains the credentials for a container registry. type RegistryAuth struct { // Server is the URL of the registry. diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 0762b82..5d50d01 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -144,14 +144,14 @@ func mkReport(rs *reportStore, srv *targetServer) (Report, error) { tmAddrs := tm.Addrs() - slog.Info("applying target map", "check", checkID, "map", tm, "tmAddr", tmAddrs) + slog.Info("applying target map", "check", checkID, "tm", tm, "tmAddr", tmAddrs) - r.Target = tm.Old + r.Target = tm.OldIdentifier var vulns []report.Vulnerability for _, vuln := range r.Vulnerabilities { - vuln = vulnReplaceAll(vuln, tm.New, tm.Old) - vuln = vulnReplaceAll(vuln, tmAddrs.New, tmAddrs.Old) + vuln = vulnReplaceAll(vuln, tm.NewIdentifier, tm.OldIdentifier) + vuln = vulnReplaceAll(vuln, tmAddrs.NewIdentifier, tmAddrs.OldIdentifier) vulns = append(vulns, vuln) } r.Vulnerabilities = vulns @@ -323,7 +323,8 @@ func beforeRun(params backend.RunParams, rc *docker.RunConfig, srv *targetServer } if tm, err := srv.Handle(params.CheckID, target); err == nil { if !tm.IsZero() { - rc.ContainerConfig.Env = setenv(rc.ContainerConfig.Env, "VULCAN_CHECK_TARGET", tm.New) + rc.ContainerConfig.Env = setenv(rc.ContainerConfig.Env, "VULCAN_CHECK_TARGET", tm.NewIdentifier) + rc.ContainerConfig.Env = setenv(rc.ContainerConfig.Env, "VULCAN_CHECK_ASSET_TYPE", string(tm.NewAssetType)) } } else { slog.Warn("could not handle target", "target", target, "err", err) diff --git a/internal/engine/jobs.go b/internal/engine/jobs.go index d3640f7..3e18b3e 100644 --- a/internal/engine/jobs.go +++ b/internal/engine/jobs.go @@ -14,19 +14,15 @@ import ( "github.com/adevinta/vulcan-agent/queue" "github.com/google/uuid" + "github.com/adevinta/lava/internal/assettype" "github.com/adevinta/lava/internal/checktype" "github.com/adevinta/lava/internal/config" ) // generateJobs generates the jobs to be sent to the agent. func generateJobs(checktypes checktype.Catalog, targets []config.Target) ([]jobrunner.Job, error) { - checks, err := generateChecks(checktypes, targets) - if err != nil { - return nil, fmt.Errorf("generate checks: %w", err) - } - var jobs []jobrunner.Job - for _, check := range checks { + for _, check := range generateChecks(checktypes, targets) { // Convert the options to a marshalled json string. jsonOpts, err := json.Marshal(check.options) if err != nil { @@ -74,17 +70,13 @@ type check struct { } // generateChecks generates a list of checks combining a map of -// checktypes and a list of targets. It returns an error if any of the -// targets has an empty or invalid asset type. -func generateChecks(checktypes checktype.Catalog, targets []config.Target) ([]check, error) { +// checktypes and a list of targets. +func generateChecks(checktypes checktype.Catalog, targets []config.Target) []check { var checks []check for _, t := range dedup(targets) { - if t.AssetType == "" || !t.AssetType.IsValid() { - return nil, fmt.Errorf("invalid target asset type: %v", t.AssetType) - } - for _, c := range checktypes { - if !c.Accepts(t.AssetType) { + at := assettype.ToVulcan(t.AssetType) + if !c.Accepts(at) { continue } @@ -102,7 +94,7 @@ func generateChecks(checktypes checktype.Catalog, targets []config.Target) ([]ch }) } } - return checks, nil + return checks } // dedup returns a deduplicated slice. diff --git a/internal/engine/jobs_test.go b/internal/engine/jobs_test.go index 91add09..70750ea 100644 --- a/internal/engine/jobs_test.go +++ b/internal/engine/jobs_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/adevinta/lava/internal/assettype" "github.com/adevinta/lava/internal/checktype" "github.com/adevinta/lava/internal/config" ) @@ -21,7 +22,6 @@ func TestGenerateChecks(t *testing.T) { checktypes checktype.Catalog targets []config.Target want []check - wantNilErr bool }{ { name: "one checktype and one target", @@ -58,7 +58,6 @@ func TestGenerateChecks(t *testing.T) { options: map[string]any{}, }, }, - wantNilErr: true, }, { name: "target overrides checktype options", @@ -115,7 +114,6 @@ func TestGenerateChecks(t *testing.T) { }, }, }, - wantNilErr: true, }, { name: "two checktypes and one target", @@ -175,7 +173,6 @@ func TestGenerateChecks(t *testing.T) { options: map[string]any{}, }, }, - wantNilErr: true, }, { name: "incompatible target", @@ -195,8 +192,7 @@ func TestGenerateChecks(t *testing.T) { AssetType: types.GitRepository, }, }, - want: nil, - wantNilErr: true, + want: nil, }, { name: "invalid target asset type", @@ -216,8 +212,7 @@ func TestGenerateChecks(t *testing.T) { AssetType: "InvalidAssetType", }, }, - want: nil, - wantNilErr: false, + want: nil, }, { name: "no checktypes", @@ -228,8 +223,7 @@ func TestGenerateChecks(t *testing.T) { AssetType: types.GitRepository, }, }, - want: nil, - wantNilErr: true, + want: nil, }, { name: "no targets", @@ -243,9 +237,8 @@ func TestGenerateChecks(t *testing.T) { }, }, }, - targets: nil, - want: nil, - wantNilErr: true, + targets: nil, + want: nil, }, { name: "target without asset type", @@ -264,8 +257,7 @@ func TestGenerateChecks(t *testing.T) { Identifier: "example.com", }, }, - want: nil, - wantNilErr: false, + want: nil, }, { name: "one checktype with two asset types and one target", @@ -304,7 +296,6 @@ func TestGenerateChecks(t *testing.T) { options: map[string]any{}, }, }, - wantNilErr: true, }, { name: "one checktype with two asset types and one target identifier with two asset types", @@ -363,7 +354,6 @@ func TestGenerateChecks(t *testing.T) { options: map[string]any{}, }, }, - wantNilErr: true, }, { name: "one target identifier with two asset types", @@ -404,7 +394,6 @@ func TestGenerateChecks(t *testing.T) { options: map[string]any{}, }, }, - wantNilErr: true, }, { name: "duplicated targets", @@ -445,16 +434,48 @@ func TestGenerateChecks(t *testing.T) { options: map[string]any{}, }, }, - wantNilErr: true, + }, + { + name: "lava asset type", + checktypes: checktype.Catalog{ + "checktype1": { + Name: "checktype1", + Description: "checktype1 description", + Image: "namespace/repository:tag", + Assets: []string{ + "GitRepository", + }, + }, + }, + targets: []config.Target{ + { + Identifier: ".", + AssetType: assettype.Path, + }, + }, + want: []check{ + { + checktype: checktype.Checktype{ + Name: "checktype1", + Description: "checktype1 description", + Image: "namespace/repository:tag", + Assets: []string{ + "GitRepository", + }, + }, + target: config.Target{ + Identifier: ".", + AssetType: assettype.Path, + }, + options: map[string]any{}, + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := generateChecks(tt.checktypes, tt.targets) - if (err == nil) != tt.wantNilErr { - t.Fatalf("unexpected error value: %v", err) - } + got := generateChecks(tt.checktypes, tt.targets) diffOpts := []cmp.Option{ cmp.AllowUnexported(check{}), cmpopts.SortSlices(checkLess), diff --git a/internal/engine/targetserver.go b/internal/engine/targetserver.go index efd4ef2..bfc7264 100644 --- a/internal/engine/targetserver.go +++ b/internal/engine/targetserver.go @@ -18,20 +18,24 @@ import ( types "github.com/adevinta/vulcan-types" "github.com/jroimartin/proxy" + "github.com/adevinta/lava/internal/assettype" "github.com/adevinta/lava/internal/config" "github.com/adevinta/lava/internal/gitserver" ) // targetMap maps a target identifier with its updated value. type targetMap struct { - // Old is the original target identifier. - Old string + // OldIdentifier is the original target identifier. + OldIdentifier string - // New is the updated target identifier. - New string + // OldAssetType is the original asset type of the target. + OldAssetType types.AssetType - // assetType is the asset type of the target. - assetType types.AssetType + // NewIdentifier is the updated target identifier. + NewIdentifier string + + // NewAssetType is the updated asset type of the target. + NewAssetType types.AssetType } // IsZero reports whether tm is the zero value. @@ -43,20 +47,21 @@ func (tm targetMap) IsZero() bool { // it is not possible to get the address of a target, then the target // is used. func (tm targetMap) Addrs() targetMap { - oldAddr, err := getTargetAddr(config.Target{Identifier: tm.Old, AssetType: tm.assetType}) + oldAddr, err := getTargetAddr(config.Target{Identifier: tm.OldIdentifier, AssetType: tm.OldAssetType}) if err != nil { - oldAddr = tm.Old + oldAddr = tm.OldIdentifier } - newAddr, err := getTargetAddr(config.Target{Identifier: tm.New, AssetType: tm.assetType}) + newAddr, err := getTargetAddr(config.Target{Identifier: tm.NewIdentifier, AssetType: tm.NewAssetType}) if err != nil { - newAddr = tm.New + newAddr = tm.NewIdentifier } tmAddrs := targetMap{ - Old: oldAddr, - New: newAddr, - assetType: tm.assetType, + OldIdentifier: oldAddr, + OldAssetType: tm.OldAssetType, + NewIdentifier: newAddr, + NewAssetType: tm.NewAssetType, } return tmAddrs } @@ -125,9 +130,12 @@ func (srv *targetServer) Handle(key string, target config.Target) (targetMap, er tm targetMap err error ) - if target.AssetType == types.GitRepository { + switch target.AssetType { + case types.GitRepository: tm, err = srv.handleGitRepo(target) - } else { + case assettype.Path: + tm, err = srv.handlePath(target) + default: tm, err = srv.handle(target) } if err != nil { @@ -192,9 +200,10 @@ loop: } tm := targetMap{ - Old: target.Identifier, - New: intIdentifier, - assetType: target.AssetType, + OldIdentifier: target.Identifier, + OldAssetType: target.AssetType, + NewIdentifier: intIdentifier, + NewAssetType: target.AssetType, } return tm, nil } @@ -224,9 +233,27 @@ func (srv *targetServer) handleGitRepo(target config.Target) (targetMap, error) } tm := targetMap{ - Old: target.Identifier, - New: fmt.Sprintf("http://%v/%v", srv.gitAddr, repo), - assetType: target.AssetType, + OldIdentifier: target.Identifier, + OldAssetType: target.AssetType, + NewIdentifier: fmt.Sprintf("http://%v/%v", srv.gitAddr, repo), + NewAssetType: target.AssetType, + } + return tm, nil +} + +// handlePath serves the provided path as a Git repository with a +// single commit. +func (srv *targetServer) handlePath(target config.Target) (targetMap, error) { + repo, err := srv.gs.AddPath(target.Identifier) + if err != nil { + return targetMap{}, fmt.Errorf("add path: %w", err) + } + + tm := targetMap{ + OldIdentifier: target.Identifier, + OldAssetType: target.AssetType, + NewIdentifier: fmt.Sprintf("http://%v/%v", srv.gitAddr, repo), + NewAssetType: assettype.ToVulcan(target.AssetType), } return tm, nil } diff --git a/internal/gitserver/gitserver.go b/internal/gitserver/gitserver.go index 3e2c0fc..a7c374a 100644 --- a/internal/gitserver/gitserver.go +++ b/internal/gitserver/gitserver.go @@ -8,6 +8,8 @@ import ( "context" "errors" "fmt" + "io" + "io/fs" "math/rand" "net" "net/http" @@ -29,6 +31,7 @@ type Server struct { mu sync.Mutex repos map[string]string + paths map[string]string } // New creates a git server, but doesn't start it. @@ -45,6 +48,7 @@ func New() (*Server, error) { srv := &Server{ basePath: tmpPath, repos: make(map[string]string), + paths: make(map[string]string), httpsrv: &http.Server{Handler: newSmartServer(tmpPath)}, } return srv, nil @@ -52,11 +56,11 @@ func New() (*Server, error) { // AddRepository adds a repository to the Git server. It returns the // name of the new served repository. -func (srv *Server) AddRepository(repoPath string) (string, error) { +func (srv *Server) AddRepository(path string) (string, error) { srv.mu.Lock() defer srv.mu.Unlock() - if repoName, ok := srv.repos[repoPath]; ok { + if repoName, ok := srv.repos[path]; ok { return repoName, nil } @@ -64,9 +68,6 @@ func (srv *Server) AddRepository(repoPath string) (string, error) { if err != nil { return "", fmt.Errorf("make temp dir: %w", err) } - repoName := filepath.Base(dstPath) - - srv.repos[repoPath] = repoName // --mirror implies --bare. Compared to --bare, --mirror not // only maps local branches of the source to local branches of @@ -74,11 +75,9 @@ func (srv *Server) AddRepository(repoPath string) (string, error) { // branches, notes etc.) and sets up a refspec configuration // such that all these refs are overwritten by a git remote // update in the target repository. - buf := &bytes.Buffer{} - cmd := exec.Command("git", "clone", "--mirror", repoPath, dstPath) - cmd.Stderr = buf + cmd := exec.Command("git", "clone", "--mirror", path, dstPath) if err = cmd.Run(); err != nil { - return "", fmt.Errorf("git clone %v: %w: %#q", repoPath, err, buf) + return "", fmt.Errorf("git clone: %w", err) } // Create a branch at HEAD. So, if HEAD is detached, the Git @@ -86,18 +85,127 @@ func (srv *Server) AddRepository(repoPath string) (string, error) { // pointing to. // // Reference: https://github.com/go-git/go-git/blob/f92cb0d49088af996433ebb106b9fc7c2adb8875/plumbing/protocol/packp/advrefs.go#L94-L104 - buf.Reset() branch := fmt.Sprintf("lava-%v", rand.Int63()) cmd = exec.Command("git", "branch", branch) cmd.Dir = dstPath - cmd.Stderr = buf if err = cmd.Run(); err != nil { - return "", fmt.Errorf("git update-ref %v: %w: %#q", repoPath, err, buf) + return "", fmt.Errorf("git branch: %w", err) } + repoName := filepath.Base(dstPath) + srv.repos[path] = repoName return repoName, nil } +// AddPath adds a file path to the Git server. The path is served as a +// Git repository with a single commit. It returns the name of the new +// served repository. +func (srv *Server) AddPath(path string) (string, error) { + srv.mu.Lock() + defer srv.mu.Unlock() + + if repoName, ok := srv.paths[path]; ok { + return repoName, nil + } + + dstPath, err := os.MkdirTemp(srv.basePath, "*.git") + if err != nil { + return "", fmt.Errorf("make temp dir: %w", err) + } + + if err := fscopy(dstPath, path); err != nil { + return "", fmt.Errorf("copy files: %w", err) + } + + cmd := exec.Command("git", "init") + cmd.Dir = dstPath + if err = cmd.Run(); err != nil { + return "", fmt.Errorf("git init: %w", err) + } + + cmd = exec.Command("git", "add", "-f", ".") + cmd.Dir = dstPath + if err = cmd.Run(); err != nil { + return "", fmt.Errorf("git add: %w", err) + } + + cmd = exec.Command( + "git", + "-c", "user.name=lava", + "-c", "user.email=lava@lava.local", + "commit", "-m", "[auto] lava", + ) + cmd.Dir = dstPath + if err = cmd.Run(); err != nil { + return "", fmt.Errorf("git commit: %w", err) + } + + repoName := filepath.Base(dstPath) + srv.paths[path] = repoName + return repoName, nil +} + +// fscopy copies src to dst recursively. It ignores all .git +// directories. +func fscopy(dst, src string) error { + err := filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + rel, err := filepath.Rel(src, path) + if err != nil { + return fmt.Errorf("rel: %w", err) + } + + switch typ := d.Type(); { + case typ.IsDir(): + if rel == "." { + // The source path is a directory. The + // destination directory already + // exists, so it is not necessary to + // create it. + return nil + } + if filepath.Base(rel) == ".git" { + // Ignore .git directory. + return filepath.SkipDir + } + if err := os.MkdirAll(filepath.Join(dst, rel), 0755); err != nil { + return fmt.Errorf("make dir: %w", err) + } + case typ.IsRegular(): + if rel == "." { + // The source path is a file. The + // destination file is the name of the + // source file. + rel = filepath.Base(path) + } + fsrc, err := os.Open(path) + if err != nil { + return fmt.Errorf("open source file: %w", err) + } + defer fsrc.Close() + fdst, err := os.Create(filepath.Join(dst, rel)) + if err != nil { + return fmt.Errorf("create destination file: %w", err) + } + defer fdst.Close() + if _, err := io.Copy(fdst, fsrc); err != nil { + return fmt.Errorf("copy file: %w", err) + } + default: + return fmt.Errorf("invalid file type: %v", path) + } + return nil + }) + + if err != nil { + return fmt.Errorf("walk dir: %w", err) + } + return nil +} + // ListenAndServe listens on the TCP network address addr and then // calls [*GitServer.Serve] to handle requests on incoming // connections. @@ -122,7 +230,6 @@ func (srv *Server) Serve(l net.Listener) error { if fn := testHookServerServe; fn != nil { fn(srv, l) } - return srv.httpsrv.Serve(l) } @@ -132,7 +239,6 @@ func (srv *Server) Close() error { if err := srv.httpsrv.Shutdown(context.Background()); err != nil { return fmt.Errorf("server shutdown: %w", err) } - if err := os.RemoveAll(srv.basePath); err != nil { return fmt.Errorf("remove temp dirs: %w", err) } diff --git a/internal/gitserver/gitserver_test.go b/internal/gitserver/gitserver_test.go index a871ece..57e135d 100644 --- a/internal/gitserver/gitserver_test.go +++ b/internal/gitserver/gitserver_test.go @@ -5,6 +5,7 @@ package gitserver import ( "fmt" "net" + "net/http" "os" "path/filepath" "testing" @@ -16,11 +17,11 @@ func TestServer_AddRepository(t *testing.T) { // Not parallel: uses global test hook. defer func() { testHookServerServe = nil }() - path, err := gittest.ExtractTemp(filepath.Join("testdata", "testrepo.tar")) + tmpPath, err := gittest.ExtractTemp(filepath.Join("testdata", "testrepo.tar")) if err != nil { t.Fatalf("unable to create a repository: %v", err) } - defer os.RemoveAll(path) + defer os.RemoveAll(tmpPath) gs, err := New() if err != nil { @@ -37,9 +38,9 @@ func TestServer_AddRepository(t *testing.T) { ln := <-lnc - repoName, err := gs.AddRepository(path) + repoName, err := gs.AddRepository(tmpPath) if err != nil { - t.Fatalf("unable to add a repository : %v", err) + t.Fatalf("unable to add a repository: %v", err) } repoPath, err := gittest.CloneTemp(fmt.Sprintf("http://%v/%s", ln.Addr(), repoName)) @@ -67,21 +68,19 @@ func TestServer_AddRepository_no_repo(t *testing.T) { defer gs.Close() //nolint:staticcheck if _, err = gs.AddRepository(tmpPath); err == nil { - t.Fatal("expected error adding a repository") + t.Fatal("expected error adding repository") } } func TestServer_AddRepository_invalid_dir(t *testing.T) { - tmpPath := "/fakedir" - gs, err := New() if err != nil { t.Fatalf("unable to create a server: %v", err) } defer gs.Close() //nolint:staticcheck - if _, err = gs.AddRepository(tmpPath); err == nil { - t.Fatal("expected error adding a repository") + if _, err = gs.AddRepository("/fakedir"); err == nil { + t.Fatal("expected error adding repository") } } @@ -92,38 +91,215 @@ func TestServer_AddRepository_invalid_dir_2(t *testing.T) { } defer os.RemoveAll(tmpPath) + gs := &Server{ + basePath: "testdata/fakedir", + repos: make(map[string]string), + httpsrv: &http.Server{Handler: newSmartServer(tmpPath)}, + } + defer gs.Close() //nolint:staticcheck + + if _, err = gs.AddRepository(tmpPath); err == nil { + t.Fatal("expected error adding repository") + } +} + +func TestServer_AddRepository_do_not_cache_error(t *testing.T) { gs, err := New() if err != nil { t.Fatalf("unable to create a server: %v", err) } defer gs.Close() //nolint:staticcheck - gs.basePath = "/fakedir" - if _, err = gs.AddRepository(tmpPath); err == nil { - t.Fatal("expected error adding a repository") + if _, err = gs.AddRepository("/fakedir"); err == nil { + t.Fatal("expected error adding repository") + } + + if _, err = gs.AddRepository("/fakedir"); err == nil { + t.Fatal("expected error adding repository") } } func TestServer_AddRepository_already_added(t *testing.T) { - path, err := gittest.ExtractTemp(filepath.Join("testdata", "testrepo.tar")) + tmpPath, err := gittest.ExtractTemp(filepath.Join("testdata", "testrepo.tar")) + if err != nil { + t.Fatalf("unable to create a repository: %v", err) + } + defer os.RemoveAll(tmpPath) + + gs, err := New() + if err != nil { + t.Fatalf("unable to create a server: %v", err) + } + defer gs.Close() + + repoName, err := gs.AddRepository(tmpPath) + if err != nil { + t.Fatalf("unable to add a repository: %v", err) + } + repoName2, err := gs.AddRepository(tmpPath) + if err != nil { + t.Fatalf("unable to add a repository: %v", err) + } + + if repoName != repoName2 { + t.Fatalf("%s should be the same as %s", repoName, repoName2) + } +} + +func TestServer_AddPath(t *testing.T) { + // Not parallel: uses global test hook. + defer func() { testHookServerServe = nil }() + + gs, err := New() + if err != nil { + t.Fatalf("unable to create a server: %v", err) + } + defer gs.Close() + + lnc := make(chan net.Listener) + testHookServerServe = func(gs *Server, ln net.Listener) { + lnc <- ln + } + + go gs.ListenAndServe("127.0.0.1:0") //nolint:errcheck + + ln := <-lnc + + repoName, err := gs.AddPath("testdata/dir") + if err != nil { + t.Fatalf("unable to add a repository: %v", err) + } + + repoPath, err := gittest.CloneTemp(fmt.Sprintf("http://%v/%s", ln.Addr(), repoName)) + if err != nil { + t.Fatalf("unable to clone the repo %s: %v", repoName, err) + } + defer os.RemoveAll(repoPath) + + if _, err := os.Stat(filepath.Join(repoPath, "foo.txt")); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestServer_AddPath_file(t *testing.T) { + // Not parallel: uses global test hook. + defer func() { testHookServerServe = nil }() + + gs, err := New() + if err != nil { + t.Fatalf("unable to create a server: %v", err) + } + defer gs.Close() + + lnc := make(chan net.Listener) + testHookServerServe = func(gs *Server, ln net.Listener) { + lnc <- ln + } + + go gs.ListenAndServe("127.0.0.1:0") //nolint:errcheck + + ln := <-lnc + + repoName, err := gs.AddPath("testdata/dir/foo.txt") + if err != nil { + t.Fatalf("unable to add a repository: %v", err) + } + + repoPath, err := gittest.CloneTemp(fmt.Sprintf("http://%v/%s", ln.Addr(), repoName)) + if err != nil { + t.Fatalf("unable to clone the repo %s: %v", repoName, err) + } + defer os.RemoveAll(repoPath) + + if _, err := os.Stat(filepath.Join(repoPath, "foo.txt")); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestServer_AddPath_repo(t *testing.T) { + // Not parallel: uses global test hook. + defer func() { testHookServerServe = nil }() + + tmpPath, err := gittest.ExtractTemp(filepath.Join("testdata", "testrepo.tar")) if err != nil { t.Fatalf("unable to create a repository: %v", err) } - defer os.RemoveAll(path) + defer os.RemoveAll(tmpPath) + + gs, err := New() + if err != nil { + t.Fatalf("unable to create a server: %v", err) + } + defer gs.Close() + + lnc := make(chan net.Listener) + testHookServerServe = func(gs *Server, ln net.Listener) { + lnc <- ln + } + + go gs.ListenAndServe("127.0.0.1:0") //nolint:errcheck + + ln := <-lnc + + repoName, err := gs.AddPath(tmpPath) + if err != nil { + t.Fatalf("unable to add a repository: %v", err) + } + + repoPath, err := gittest.CloneTemp(fmt.Sprintf("http://%v/%s", ln.Addr(), repoName)) + if err != nil { + t.Fatalf("unable to clone the repo %s: %v", repoName, err) + } + defer os.RemoveAll(repoPath) + + if _, err := os.Stat(filepath.Join(repoPath, "foo.txt")); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestServer_AddPath_invalid_path(t *testing.T) { + gs, err := New() + if err != nil { + t.Fatalf("unable to create a server: %v", err) + } + defer gs.Close() //nolint:staticcheck + + if _, err = gs.AddPath("/fakedir"); err == nil { + t.Fatal("expected error adding path") + } +} +func TestServer_AddPath_do_not_cache_error(t *testing.T) { + gs, err := New() + if err != nil { + t.Fatalf("unable to create a server: %v", err) + } + defer gs.Close() //nolint:staticcheck + + if _, err = gs.AddPath("/fakedir"); err == nil { + t.Fatal("expected error adding path") + } + + if _, err = gs.AddPath("/fakedir"); err == nil { + t.Fatal("expected error adding path") + } +} + +func TestServer_AddPath_already_added(t *testing.T) { gs, err := New() if err != nil { t.Fatalf("unable to create a server: %v", err) } defer gs.Close() - repoName, err := gs.AddRepository(path) + repoName, err := gs.AddPath("testdata/dir") if err != nil { - t.Fatalf("unable to add a repository : %v", err) + t.Fatalf("unable to add a repository: %v", err) } - repoName2, err := gs.AddRepository(path) + + repoName2, err := gs.AddPath("testdata/dir") if err != nil { - t.Fatalf("unable to add a repository : %v", err) + t.Fatalf("unable to add a repository: %v", err) } if repoName != repoName2 { diff --git a/internal/gitserver/testdata/dir/foo.txt b/internal/gitserver/testdata/dir/foo.txt new file mode 100644 index 0000000..257cc56 --- /dev/null +++ b/internal/gitserver/testdata/dir/foo.txt @@ -0,0 +1 @@ +foo