From b099ffa87c6efff9048c70e44e97f734ded299a2 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sat, 3 Aug 2024 15:40:36 -0500 Subject: [PATCH 01/17] Renames --- .github/workflows/main.yaml | 2 +- Makefile | 13 +++++++--- README.md | 20 ++++++++-------- cmd/root.go | 14 ++++++----- cmd/sync.go | 9 ++++--- cmd/sync_test.go | 25 +++++++++---------- internal/remotes/file.go | 30 +++++++++++------------ internal/remotes/git.go | 12 +++++----- internal/remotes/git_test.go | 12 +++++----- internal/vdmspec/spec.go | 27 ++++++++++++++------- internal/vdmspec/spec_test.go | 10 ++++---- internal/vdmspec/validate.go | 10 ++++---- internal/vdmspec/validate_test.go | 40 +++++++++++++++---------------- scripts/package.sh | 8 +++---- testdata/vdm.yaml | 20 ++++++++-------- 15 files changed, 135 insertions(+), 117 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 2b87cc4..5654442 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -44,6 +44,6 @@ jobs: - name: Package binaries run: 'make package' - name: Create GitHub Release - run: gh release create "v$(grep -v '#' ./VERSION)" --generate-notes --verify-tag --latest ./dist/zipped/* + run: gh release create "v$(grep -v '#' ./VERSION)" --generate-notes --verify-tag --latest ./dist/compressed/* env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index 37cc469..d1e137a 100644 --- a/Makefile +++ b/Makefile @@ -38,13 +38,14 @@ clean: *cache* \ .*cache* \ ./build/ \ - ./dist/zipped/*.tar.gz \ - ./dist/zipped/*.zip \ + ./dist/compressed/*.tar.gz \ + ./dist/compressed/*.zip \ ./dist/debian/vdm.deb \ *.out @sudo rm -rf ./dist/debian/vdm/usr # TODO: until I sort out the tests to write test data consistently, these deps/ -# directories etc. can kind of show up anywhere +# directories etc. can kind of show up anywhere, so we need to find & delete all +# of them @find . -type d -name '*deps*' -exec rm -rf {} + @find . -type f -name '*VDMMETA*' -delete @@ -63,3 +64,9 @@ add-local-symlinks: @mkdir -p "$${HOME}"/.local/bin @ln -fs $$(realpath build/$$(go env GOOS)-$$(go env GOARCH)/$(BINNAME)) "$${HOME}"/.local/bin/$(BINNAME) @printf 'Symlinked vdm to %s\n' "$${HOME}"/.local/bin/$(BINNAME) + +install: + @go install ./... + +docs: + @go run golang.org/x/pkgsite/cmd/pkgsite@latest -open . diff --git a/README.md b/README.md index a6b1ff6..37ae700 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ retrieve them whenever you need them. ### Installation `vdm` can be installed from [its GitHub Releases -page](https://github.com/opensourcecorp/vdm/releases). There is a zipped binary -for major platforms & architectures, and those are indicated in the Asset file -name. For example, if you have an M2 macOS laptop, you would download the +page](https://github.com/opensourcecorp/vdm/releases). There is a compressed +binary for major platforms & architectures, and those are indicated in the Asset +file name. For example, if you have an M2 macOS laptop, you would download the `vdm_darwin-arm64.tar.gz` file, and extract it to somewhere on your `$PATH`. If you have a recent version of the Go toolchain available, you can also install @@ -50,14 +50,14 @@ revisions & where you want them to live on your filesystem: ```yaml remotes: - - type: "git" # the default, and so can be omitted if desired - remote: "https://github.com/opensourcecorp/go-common" # can specify as 'git@...' to use SSH instead - local_path: "./deps/go-common" - version: "v0.2.0" # tag example; can also be a branch, commit hash, or the word 'latest' + - type: "git" # the default, and so can be omitted if desired + source: "https://github.com/opensourcecorp/go-common" # can specify as 'git@...' to use SSH instead + version: "v0.2.0" # tag example; can also be a branch, commit hash, or the word 'latest' + destination: "./deps/go-common" - - type: "file" # the 'file' type assumes the version is in the remote field itself somehow, so 'version' can be omitted - remote: "https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto" - local_path: "./deps/proto/http/http.proto" + - type: "file" # the 'file' type assumes the version is in the remote field itself somehow, so 'version' can be omitted + source: "https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto" + destination: "./deps/proto/http/http.proto" ``` You can have as many dependency specifications in that array as you want, and diff --git a/cmd/root.go b/cmd/root.go index baee032..ab2cb03 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,29 +15,31 @@ const vdmVersion string = "v0.2.1" var rootCmd = cobra.Command{ Use: "vdm", Short: "vdm -- a Versioned-Dependency Manager", - Long: "vdm is used to manage arbitrary remote dependencies", + Long: "vdm is used to manage retrieval of arbitrary remote dependencies", TraverseChildren: true, Version: vdmVersion, Run: func(cmd *cobra.Command, args []string) { MaybeSetDebug() if len(args) == 0 { + message.Errorf("You must provide a subcommand to vdm") err := cmd.Help() if err != nil { message.Fatalf("failed to print help message, somehow") } - os.Exit(0) + os.Exit(1) } }, } +// rootFlags defines the CLI flags for the root command. type rootFlags struct { SpecFilePath string Debug bool } -// RootFlagValues contains an initalized [rootFlags] struct with populated +// rootFlagValues contains an initalized [rootFlags] struct with populated // values. -var RootFlagValues rootFlags +var rootFlagValues rootFlags // Flag name keys const ( @@ -48,13 +50,13 @@ const ( func init() { var err error - rootCmd.PersistentFlags().StringVar(&RootFlagValues.SpecFilePath, specFilePathFlagKey, "./vdm.yaml", "Path to vdm specfile") + rootCmd.PersistentFlags().StringVar(&rootFlagValues.SpecFilePath, specFilePathFlagKey, "./vdm.yaml", "Path to vdm specfile") err = viper.BindPFlag(specFilePathFlagKey, rootCmd.PersistentFlags().Lookup(specFilePathFlagKey)) if err != nil { message.Fatalf("internal error: unable to bind state of flag --%s", specFilePathFlagKey) } - rootCmd.PersistentFlags().BoolVar(&RootFlagValues.Debug, debugFlagKey, false, "Show debug messages during runtime") + rootCmd.PersistentFlags().BoolVar(&rootFlagValues.Debug, debugFlagKey, false, "Show debug messages during runtime") err = viper.BindPFlag(debugFlagKey, rootCmd.PersistentFlags().Lookup(debugFlagKey)) if err != nil { message.Fatalf("internal error: unable to bind state of flag --%s", debugFlagKey) diff --git a/cmd/sync.go b/cmd/sync.go index cbf43ea..eb57cc9 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -26,7 +26,7 @@ func syncExecute(_ *cobra.Command, _ []string) error { // sync does the heavy lifting to ensure that the local directory tree(s) match // the desired state as defined in the specfile. func sync() error { - spec, err := vdmspec.GetSpecFromFile(RootFlagValues.SpecFilePath) + spec, err := vdmspec.GetSpecFromFile(rootFlagValues.SpecFilePath) if err != nil { return fmt.Errorf("getting specs from spec file: %w", err) } @@ -36,7 +36,6 @@ func sync() error { return fmt.Errorf("your vdm spec file is malformed: %w", err) } -SpecLoop: for _, remote := range spec.Remotes { // process stored vdm metafile so we know what operations to actually // perform for existing directories @@ -48,12 +47,12 @@ SpecLoop: if vdmMeta == (vdmspec.Remote{}) { message.Infof("%s: %s not found at local path, will be created", remote.OpMsg(), vdmspec.MetaFileName) } else { - if vdmMeta.Version != remote.Version && vdmMeta.Remote != remote.Remote { - message.Infof("%s: Will change '%s' from current local version spec '%s' to '%s'...", remote.OpMsg(), remote.Remote, vdmMeta.Version, remote.Version) + if vdmMeta.Version != remote.Version && vdmMeta.Source != remote.Source { + message.Infof("%s: Will change '%s' from current local version spec '%s' to '%s'...", remote.OpMsg(), remote.Source, vdmMeta.Version, remote.Version) panic("not implemented") } message.Infof("%s: version unchanged in spec file, skipping", remote.OpMsg()) - continue SpecLoop + continue } switch remote.Type { diff --git a/cmd/sync_test.go b/cmd/sync_test.go index 8708f01..858b865 100644 --- a/cmd/sync_test.go +++ b/cmd/sync_test.go @@ -1,6 +1,7 @@ package cmd import ( + "os" "path/filepath" "testing" @@ -20,37 +21,37 @@ func TestSync(t *testing.T) { require.NoError(t, err) // Need to override for test - RootFlagValues.SpecFilePath = testSpecFilePath + rootFlagValues.SpecFilePath = testSpecFilePath err = sync() require.NoError(t, err) - // defer t.Cleanup(func() { - // for _, remote := range spec.Remotes { - // err := os.RemoveAll(remote.LocalPath) - // require.NoError(t, err) - // } - // }) + defer t.Cleanup(func() { + for _, remote := range spec.Remotes { + err := os.RemoveAll(remote.Destination) + require.NoError(t, err) + } + }) t.Run("SyncGit", func(t *testing.T) { - t.Run("spec[0] used a tag", func(t *testing.T) { + t.Run("remotes[0] used a tag", func(t *testing.T) { vdmMeta, err := spec.Remotes[0].GetVDMMeta() require.NoError(t, err) assert.Equal(t, "v0.2.0", vdmMeta.Version) }) - t.Run("spec[1] used 'latest'", func(t *testing.T) { + t.Run("remotes[1] used 'latest'", func(t *testing.T) { vdmMeta, err := spec.Remotes[1].GetVDMMeta() require.NoError(t, err) assert.Equal(t, "latest", vdmMeta.Version) }) - t.Run("spec[2] used a branch", func(t *testing.T) { + t.Run("remotes[2] used a branch", func(t *testing.T) { vdmMeta, err := spec.Remotes[2].GetVDMMeta() require.NoError(t, err) assert.Equal(t, "main", vdmMeta.Version) }) - t.Run("spec[3] used a hash", func(t *testing.T) { + t.Run("remotes[3] used a hash", func(t *testing.T) { vdmMeta, err := spec.Remotes[3].GetVDMMeta() require.NoError(t, err) assert.Equal(t, "2e6657f5ac013296167c4dd92fbb46f0e3dbdc5f", vdmMeta.Version) @@ -58,7 +59,7 @@ func TestSync(t *testing.T) { }) t.Run("SyncFile", func(t *testing.T) { - t.Run("spec[4] had an implicit version", func(t *testing.T) { + t.Run("remotes[4] had an implicit version", func(t *testing.T) { vdmMeta, err := spec.Remotes[4].GetVDMMeta() require.NoError(t, err) assert.Equal(t, "", vdmMeta.Version) diff --git a/internal/remotes/file.go b/internal/remotes/file.go index ed9bd59..e86862c 100644 --- a/internal/remotes/file.go +++ b/internal/remotes/file.go @@ -20,50 +20,50 @@ func SyncFile(remote vdmspec.Remote) error { } if !fileExists { - message.Infof("File '%s' does not exist locally, retrieving", remote.LocalPath) + message.Infof("File '%s' does not exist locally, retrieving", remote.Destination) err = retrieveFile(remote) if err != nil { return fmt.Errorf("retrieving file: %w", err) } } else { - message.Infof("File '%s' already exists locally, skipping", remote.LocalPath) + message.Infof("File '%s' already exists locally, skipping", remote.Destination) } return nil } func checkFileExists(remote vdmspec.Remote) (bool, error) { - fullPath, err := filepath.Abs(remote.LocalPath) + fullPath, err := filepath.Abs(remote.Destination) if err != nil { - return false, fmt.Errorf("determining abspath for file '%s': %w", remote.LocalPath, err) + return false, fmt.Errorf("determining abspath for file '%s': %w", remote.Destination, err) } - _, err = os.Stat(remote.LocalPath) + _, err = os.Stat(remote.Destination) if errors.Is(err, os.ErrNotExist) { return false, nil } else if err != nil { - return false, fmt.Errorf("couldn't check if %s exists at '%s': %w", remote.LocalPath, fullPath, err) + return false, fmt.Errorf("couldn't check if %s exists at '%s': %w", remote.Destination, fullPath, err) } return true, nil } func retrieveFile(remote vdmspec.Remote) (err error) { - resp, err := http.Get(remote.Remote) + resp, err := http.Get(remote.Source) if err != nil { - return fmt.Errorf("retrieving remote file '%s': %w", remote.Remote, err) + return fmt.Errorf("retrieving remote file '%s': %w", remote.Source, err) } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - err = errors.Join(fmt.Errorf("closing response body after remote file '%s' retrieval: %w", remote.Remote, err)) + err = errors.Join(fmt.Errorf("closing response body after remote file '%s' retrieval: %w", remote.Source, err)) } }() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unsuccessful status code '%d' from server when retrieving remote file '%s'", resp.StatusCode, remote.Remote) + return fmt.Errorf("unsuccessful status code '%d' from server when retrieving remote file '%s'", resp.StatusCode, remote.Source) } - err = ensureParentDirs(remote.LocalPath) + err = ensureParentDirs(remote.Destination) if err != nil { return fmt.Errorf("creating parent directories for file: %w", err) } @@ -71,13 +71,13 @@ func retrieveFile(remote vdmspec.Remote) (err error) { // Note: I would normally use os.WriteFile() using the returned bytes // directly, but the internet says this os.Create()/io.Copy() approach // appears to be idiomatic - outFile, err := os.Create(remote.LocalPath) + outFile, err := os.Create(remote.Destination) if err != nil { - return fmt.Errorf("creating landing file '%s' for remote file: %w", remote.LocalPath, err) + return fmt.Errorf("creating landing file '%s' for remote file: %w", remote.Destination, err) } defer func() { if closeErr := outFile.Close(); closeErr != nil { - err = errors.Join(fmt.Errorf("closing local file '%s' after remote file '%s' retrieval: %w", remote.LocalPath, remote.Remote, err)) + err = errors.Join(fmt.Errorf("closing local file '%s' after remote file '%s' retrieval: %w", remote.Destination, remote.Source, err)) } }() @@ -85,7 +85,7 @@ func retrieveFile(remote vdmspec.Remote) (err error) { if err != nil { return fmt.Errorf("copying HTTP response to disk: ") } - message.Debugf("wrote %d bytes to '%s'", bytesWritten, remote.LocalPath) + message.Debugf("wrote %d bytes to '%s'", bytesWritten, remote.Destination) return nil } diff --git a/internal/remotes/git.go b/internal/remotes/git.go index 940a25c..f445c77 100644 --- a/internal/remotes/git.go +++ b/internal/remotes/git.go @@ -20,15 +20,15 @@ func SyncGit(remote vdmspec.Remote) error { if remote.Version != "latest" { message.Infof("%s: Setting specified version...", remote.OpMsg()) - checkoutCmd := exec.Command("git", "-C", remote.LocalPath, "checkout", remote.Version) + checkoutCmd := exec.Command("git", "-C", remote.Destination, "checkout", remote.Version) checkoutOutput, err := checkoutCmd.CombinedOutput() if err != nil { return fmt.Errorf("error checking out specified revision: exec error '%w', with output: %s", err, string(checkoutOutput)) } } - message.Debugf("removing .git dir for local path '%s'", remote.LocalPath) - dotGitPath := filepath.Join(remote.LocalPath, ".git") + message.Debugf("removing .git dir for local path '%s'", remote.Destination) + dotGitPath := filepath.Join(remote.Destination, ".git") err = os.RemoveAll(dotGitPath) if err != nil { return fmt.Errorf("removing directory %s: %w", dotGitPath, err) @@ -51,7 +51,7 @@ func checkGitAvailable() error { func gitClone(remote vdmspec.Remote) error { err := checkGitAvailable() if err != nil { - return fmt.Errorf("remote '%s' is a git type, but git may not installed/available on PATH: %w", remote.Remote, err) + return fmt.Errorf("remote '%s' is a git type, but git may not installed/available on PATH: %w", remote.Source, err) } // If users want "latest", then we can just do a depth-one clone and @@ -60,10 +60,10 @@ func gitClone(remote vdmspec.Remote) error { var cloneCmdArgs []string if remote.Version == "latest" { message.Debugf("%s: version specified as 'latest', so making shallow clone and skipping separate checkout operation", remote.OpMsg()) - cloneCmdArgs = []string{"clone", "--depth=1", remote.Remote, remote.LocalPath} + cloneCmdArgs = []string{"clone", "--depth=1", remote.Source, remote.Destination} } else { message.Debugf("%s: version specified as NOT latest, so making regular clone and will make separate checkout operation", remote.OpMsg()) - cloneCmdArgs = []string{"clone", remote.Remote, remote.LocalPath} + cloneCmdArgs = []string{"clone", remote.Source, remote.Destination} } message.Infof("%s: Retrieving...", remote.OpMsg()) diff --git a/internal/remotes/git_test.go b/internal/remotes/git_test.go index 6b2bde4..5801617 100644 --- a/internal/remotes/git_test.go +++ b/internal/remotes/git_test.go @@ -12,10 +12,10 @@ import ( func getTestGitSpec() vdmspec.Remote { specLocalPath := "./deps/go-common" return vdmspec.Remote{ - Type: "git", - Remote: "https://github.com/opensourcecorp/go-common", - Version: "v0.2.0", - LocalPath: specLocalPath, + Type: "git", + Source: "https://github.com/opensourcecorp/go-common", + Version: "v0.2.0", + Destination: specLocalPath, } } @@ -25,7 +25,7 @@ func TestSyncGit(t *testing.T) { require.NoError(t, err) defer t.Cleanup(func() { - if cleanupErr := os.RemoveAll(spec.LocalPath); cleanupErr != nil { + if cleanupErr := os.RemoveAll(spec.Destination); cleanupErr != nil { t.Fatalf("removing specLocalPath: %v", cleanupErr) } }) @@ -57,7 +57,7 @@ func TestGitClone(t *testing.T) { cloneErr := gitClone(spec) defer t.Cleanup(func() { - if cleanupErr := os.RemoveAll(spec.LocalPath); cleanupErr != nil { + if cleanupErr := os.RemoveAll(spec.Destination); cleanupErr != nil { t.Fatalf("removing specLocalPath: %v", cleanupErr) } }) diff --git a/internal/vdmspec/spec.go b/internal/vdmspec/spec.go index bfe2e3b..68e8cae 100644 --- a/internal/vdmspec/spec.go +++ b/internal/vdmspec/spec.go @@ -19,10 +19,19 @@ type Spec struct { // Remote defines the structure of each remote configuration in the vdm // specfile. type Remote struct { - Type string `json:"type,omitempty" yaml:"type,omitempty"` - Remote string `json:"remote" yaml:"remote"` - Version string `json:"version,omitempty" yaml:"version,omitempty"` - LocalPath string `json:"local_path" yaml:"local_path"` + // Type is the type of Source, e.g. git, archive, etc. + Type string `json:"type,omitempty" yaml:"type,omitempty"` + // Source is the fully-qualifed location from which the Remote is retrieved, + // e.g. "https://github.com/some-org/some-repo" + Source string `json:"source" yaml:"source"` + // Version states the version requested from Source, and is then later used + // for tracking purposes. Version can be anything supported by the Type + // field -- for example, for the "git" Type, this can be a tag, a branch + // name, or a commit hash. It can also be the word "latest". + Version string `json:"version,omitempty" yaml:"version,omitempty"` + // Destination is the relative or absolute path on disk that Source will be + // placed at + Destination string `json:"destination" yaml:"destination"` } const ( @@ -39,11 +48,11 @@ const ( // MakeMetaFilePath constructs the metafile path that vdm will use to track a // remote's state on disk. func (r Remote) MakeMetaFilePath() string { - metaFilePath := filepath.Join(r.LocalPath, MetaFileName) + metaFilePath := filepath.Join(r.Destination, MetaFileName) // TODO: this is brittle, but it's the best I can think of right now if r.Type == FileType { - fileDir := filepath.Dir(r.LocalPath) - fileName := filepath.Base(r.LocalPath) + fileDir := filepath.Dir(r.Destination) + fileName := filepath.Base(r.Destination) // converts to e.g. 'VDMMETA_http.proto' metaFilePath = filepath.Join(fileDir, fmt.Sprintf("%s_%s", MetaFileName, fileName)) } @@ -135,7 +144,7 @@ func GetSpecFromFile(specFilePath string) (Spec, error) { // performed at the moment func (r Remote) OpMsg() string { if r.Version != "" { - return fmt.Sprintf("%s@%s --> %s", r.Remote, r.Version, r.LocalPath) + return fmt.Sprintf("%s@%s --> %s", r.Source, r.Version, r.Destination) } - return fmt.Sprintf("%s --> %s", r.Remote, r.LocalPath) + return fmt.Sprintf("%s --> %s", r.Source, r.Destination) } diff --git a/internal/vdmspec/spec_test.go b/internal/vdmspec/spec_test.go index 70918cc..82999c5 100644 --- a/internal/vdmspec/spec_test.go +++ b/internal/vdmspec/spec_test.go @@ -16,15 +16,15 @@ var ( testVDMMetaFilePath = filepath.Join(testVDMRoot, MetaFileName) testRemote = Remote{ - Remote: "https://some-remote", - Version: "v1.0.0", - LocalPath: testVDMRoot, + Source: "https://some-remote", + Version: "v1.0.0", + Destination: testVDMRoot, } testSpecFilePath = filepath.Join(testVDMRoot, "vdm.yaml") testVDMMetaContents = fmt.Sprintf( - `{"remote": "https://some-remote", "version": "v1.0.0", "local_path": "%s"}`, + `{"source": "https://some-remote", "version": "v1.0.0", "destination": "%s"}`, testVDMRoot, ) ) @@ -51,7 +51,7 @@ func TestVDMMeta(t *testing.T) { }) // Needs to have parent dir(s) exist for write to work - err := os.MkdirAll(testRemote.LocalPath, 0644) + err := os.MkdirAll(testRemote.Destination, 0644) require.NoError(t, err) err = testRemote.WriteVDMMeta() diff --git a/internal/vdmspec/validate.go b/internal/vdmspec/validate.go index 2724244..2061ed8 100644 --- a/internal/vdmspec/validate.go +++ b/internal/vdmspec/validate.go @@ -16,14 +16,14 @@ func (spec Spec) Validate() error { for remoteIndex, remote := range spec.Remotes { // Remote field message.Debugf("Index #%d: validating field 'Remote' for %+v", remoteIndex, remote) - if len(remote.Remote) == 0 { + if len(remote.Source) == 0 { allErrors = append(allErrors, errors.New("all 'remote' fields must be non-zero length")) } protocolRegex := regexp.MustCompile(`(http(s?)://|git://|git@)`) - if !protocolRegex.MatchString(remote.Remote) { + if !protocolRegex.MatchString(remote.Source) { allErrors = append( allErrors, - fmt.Errorf("remote #%d provided as '%s', but all 'remote' fields must begin with a protocol specifier or other valid prefix (e.g. 'https://', '(user|git)@', etc.)", remoteIndex, remote.Remote), + fmt.Errorf("remote #%d provided as '%s', but all 'remote' fields must begin with a protocol specifier or other valid prefix (e.g. 'https://', '(user|git)@', etc.)", remoteIndex, remote.Source), ) } @@ -33,12 +33,12 @@ func (spec Spec) Validate() error { allErrors = append(allErrors, errors.New("all 'version' fields for the 'git' remote type must be non-zero length. If you don't care about the version (even though you probably should), then use 'latest'")) } if remote.Type == FileType && len(remote.Version) > 0 { - message.Warnf("NOTE: Remote #%d '%s' specified as type '%s', which does not take explicit version info (you provided '%s'); ignoring version field", remoteIndex, remote.Remote, remote.Type, remote.Version) + message.Warnf("NOTE: Remote #%d '%s' specified as type '%s', which does not take explicit version info (you provided '%s'); ignoring version field", remoteIndex, remote.Source, remote.Type, remote.Version) } // LocalPath field message.Debugf("Index #%d: validating field 'LocalPath' for %+v", remoteIndex, remote) - if len(remote.LocalPath) == 0 { + if len(remote.Destination) == 0 { allErrors = append(allErrors, errors.New("all 'local_path' fields must be non-zero length")) } diff --git a/internal/vdmspec/validate_test.go b/internal/vdmspec/validate_test.go index 7428cee..1c985fb 100644 --- a/internal/vdmspec/validate_test.go +++ b/internal/vdmspec/validate_test.go @@ -11,9 +11,9 @@ func TestValidate(t *testing.T) { t.Run("passes", func(t *testing.T) { spec := Spec{ Remotes: []Remote{{ - Remote: "https://some-remote", - Version: "v1.0.0", - LocalPath: "./deps/some-remote", + Source: "https://some-remote", + Version: "v1.0.0", + Destination: "./deps/some-remote", }}, } err := spec.Validate() @@ -23,9 +23,9 @@ func TestValidate(t *testing.T) { t.Run("fails on zero-length remote", func(t *testing.T) { spec := Spec{ Remotes: []Remote{{ - Remote: "", - Version: "v1.0.0", - LocalPath: "./deps/some-remote", + Source: "", + Version: "v1.0.0", + Destination: "./deps/some-remote", }}, } err := spec.Validate() @@ -35,9 +35,9 @@ func TestValidate(t *testing.T) { t.Run("fails on remote without valid protocol", func(t *testing.T) { spec := Spec{ Remotes: []Remote{{ - Remote: "some-remote", - Version: "v1.0.0", - LocalPath: "./deps/some-remote", + Source: "some-remote", + Version: "v1.0.0", + Destination: "./deps/some-remote", }}, } err := spec.Validate() @@ -47,10 +47,10 @@ func TestValidate(t *testing.T) { t.Run("fails on zero-length version for git remote type", func(t *testing.T) { spec := Spec{ Remotes: []Remote{{ - Remote: "https://some-remote", - Version: "", - LocalPath: "./deps/some-remote", - Type: GitType, + Source: "https://some-remote", + Version: "", + Destination: "./deps/some-remote", + Type: GitType, }}, } err := spec.Validate() @@ -60,10 +60,10 @@ func TestValidate(t *testing.T) { t.Run("fails on unrecognized remote type", func(t *testing.T) { spec := Spec{ Remotes: []Remote{{ - Remote: "https://some-remote", - Version: "", - LocalPath: "./deps/some-remote", - Type: "bad", + Source: "https://some-remote", + Version: "", + Destination: "./deps/some-remote", + Type: "bad", }}, } err := spec.Validate() @@ -73,9 +73,9 @@ func TestValidate(t *testing.T) { t.Run("fails on zero-length local path", func(t *testing.T) { spec := Spec{ Remotes: []Remote{{ - Remote: "https://some-remote", - Version: "v1.0.0", - LocalPath: "", + Source: "https://some-remote", + Version: "v1.0.0", + Destination: "", }}, } err := spec.Validate() diff --git a/scripts/package.sh b/scripts/package.sh index 13daeda..0df51cc 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -2,16 +2,16 @@ set -euo pipefail # cd-jumps because it makes logs cleaner, not sorry -mkdir -p dist/zipped +mkdir -p dist/compressed cd build || exit 1 for built in * ; do - printf 'Packaging for %s into dist/zipped/\n' "${built}" + printf 'Packaging for %s into dist/compressed/\n' "${built}" cd "${built}" || exit 1 # Windows might like .zips better, otherwise make .tar.gzs if [[ "${built}" =~ 'windows' ]] ; then - zip -r9 ../../dist/zipped/vdm_"${built}".zip ./* + zip -r9 ../../dist/compressed/vdm_"${built}".zip ./* else - tar -czf ../../dist/zipped/vdm_"${built}".tar.gz ./* + tar -czf ../../dist/compressed/vdm_"${built}".tar.gz ./* fi cd - > /dev/null done diff --git a/testdata/vdm.yaml b/testdata/vdm.yaml index 039d1e7..342719f 100644 --- a/testdata/vdm.yaml +++ b/testdata/vdm.yaml @@ -1,16 +1,16 @@ remotes: - - remote: "https://github.com/opensourcecorp/go-common" + - source: "https://github.com/opensourcecorp/go-common" version: "v0.2.0" - local_path: "./deps/go-common-tag" - - remote: "https://github.com/opensourcecorp/go-common" + destination: "./deps/go-common-tag" + - source: "https://github.com/opensourcecorp/go-common" version: "latest" - local_path: "./deps/go-common-latest" - - remote: "https://github.com/opensourcecorp/go-common" + destination: "./deps/go-common-latest" + - source: "https://github.com/opensourcecorp/go-common" version: "main" - local_path: "./deps/go-common-branch" - - remote: "https://github.com/opensourcecorp/go-common" + destination: "./deps/go-common-branch" + - source: "https://github.com/opensourcecorp/go-common" version: "2e6657f5ac013296167c4dd92fbb46f0e3dbdc5f" - local_path: "./deps/go-common-hash" + destination: "./deps/go-common-hash" - type: "file" - remote: "https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto" - local_path: "./deps/proto/http/http.proto" + source: "https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto" + destination: "./deps/proto/http/http.proto" From 46c4e96c93da4f00f14c26d4810088df81aff6b4 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sat, 3 Aug 2024 16:54:07 -0500 Subject: [PATCH 02/17] WIP --- VERSION | 2 +- cmd/flagsupport.go | 20 ++++++++++++++++---- cmd/flagvars/flagvars.go | 11 +++++++++++ cmd/root.go | 8 ++++---- cmd/sync.go | 28 +++++++++++++++++++++++++++- dist/debian/vdm/DEBIAN/control | 2 +- dist/man/man.1.md | 2 +- internal/message/message.go | 4 +++- internal/remotes/archive.go | 14 ++++++++++++++ internal/remotes/local.go | 14 ++++++++++++++ internal/vdmspec/spec.go | 8 ++++++++ 11 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 cmd/flagvars/flagvars.go create mode 100644 internal/remotes/archive.go create mode 100644 internal/remotes/local.go diff --git a/VERSION b/VERSION index e9ab468..a4fb4e6 100644 --- a/VERSION +++ b/VERSION @@ -1,3 +1,3 @@ # Version number used as an anchor for listed versions in various files in the # tree, orchestrated by scripts/bump-versions.sh -0.2.1 +0.3.0 diff --git a/cmd/flagsupport.go b/cmd/flagsupport.go index 31a122e..df57c92 100644 --- a/cmd/flagsupport.go +++ b/cmd/flagsupport.go @@ -3,17 +3,29 @@ package cmd import ( "os" + "github.com/opensourcecorp/vdm/cmd/flagvars" "github.com/opensourcecorp/vdm/internal/message" "github.com/spf13/viper" ) -// MaybeSetDebug sets the DEBUG environment variable if it was set as a flag by +// maybeSetDebug sets the DEBUG environment variable if it was set as a flag by // the caller. -func MaybeSetDebug() { +func maybeSetDebug() { if viper.GetBool(debugFlagKey) { - err := os.Setenv("DEBUG", "true") + err := os.Setenv(flagvars.Debug, "true") if err != nil { - message.Fatalf("internal error: unable to set environment variable DEBUG") + message.Fatalf("internal error: unable to set environment variable %s", flagvars.Debug) + } + } +} + +// maybeTryLocalSources sets the TRY_LOCAL_SOURCES environment variable if it +// was set as a flag by the caller. +func maybeTryLocalSources() { + if viper.GetBool(tryLocalSourcesFlagKey) { + err := os.Setenv(flagvars.TryLocalSources, "true") + if err != nil { + message.Fatalf("internal error: unable to set environment variable %s", flagvars.TryLocalSources) } } } diff --git a/cmd/flagvars/flagvars.go b/cmd/flagvars/flagvars.go new file mode 100644 index 0000000..7cd76ea --- /dev/null +++ b/cmd/flagvars/flagvars.go @@ -0,0 +1,11 @@ +// Package flagvars houses constants etc. for working with command-line flag +// values across packages. These helpers are pushed down to their own package in +// order to avoid import cycles. +package flagvars + +const ( + // Debug anchors to the DEBUG env var + Debug = "DEBUG" + // TryLocalSources anchors to the TRY_LOCAL_SOURCES env var + TryLocalSources = "TRY_LOCAL_SOURCES" +) diff --git a/cmd/root.go b/cmd/root.go index ab2cb03..35b7f4e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,7 +10,7 @@ import ( ) // !!! DO NOT TOUCH, the version-bumper script handles updating this !!! -const vdmVersion string = "v0.2.1" +const vdmVersion string = "v0.3.0" var rootCmd = cobra.Command{ Use: "vdm", @@ -19,7 +19,7 @@ var rootCmd = cobra.Command{ TraverseChildren: true, Version: vdmVersion, Run: func(cmd *cobra.Command, args []string) { - MaybeSetDebug() + maybeSetDebug() if len(args) == 0 { message.Errorf("You must provide a subcommand to vdm") err := cmd.Help() @@ -53,13 +53,13 @@ func init() { rootCmd.PersistentFlags().StringVar(&rootFlagValues.SpecFilePath, specFilePathFlagKey, "./vdm.yaml", "Path to vdm specfile") err = viper.BindPFlag(specFilePathFlagKey, rootCmd.PersistentFlags().Lookup(specFilePathFlagKey)) if err != nil { - message.Fatalf("internal error: unable to bind state of flag --%s", specFilePathFlagKey) + message.Fatalf("internal error: unable to bind state of flag --%s: %v", specFilePathFlagKey, err) } rootCmd.PersistentFlags().BoolVar(&rootFlagValues.Debug, debugFlagKey, false, "Show debug messages during runtime") err = viper.BindPFlag(debugFlagKey, rootCmd.PersistentFlags().Lookup(debugFlagKey)) if err != nil { - message.Fatalf("internal error: unable to bind state of flag --%s", debugFlagKey) + message.Fatalf("internal error: unable to bind state of flag --%s: %v", debugFlagKey, err) } rootCmd.AddCommand(syncCmd) diff --git a/cmd/sync.go b/cmd/sync.go index eb57cc9..fe554e3 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -7,6 +7,7 @@ import ( "github.com/opensourcecorp/vdm/internal/remotes" "github.com/opensourcecorp/vdm/internal/vdmspec" "github.com/spf13/cobra" + "github.com/spf13/viper" ) var syncCmd = &cobra.Command{ @@ -15,8 +16,33 @@ var syncCmd = &cobra.Command{ RunE: syncExecute, } +// syncFlags defines the CLI flags for the sync subcommand. +type syncFlags struct { + TryLocalSources bool +} + +// syncFlagValues contains an initalized [syncFlags] struct with populated +// values. +var syncFlagValues syncFlags + +// Flag name keys +const ( + tryLocalSourcesFlagKey string = "try-local-sources" +) + +func init() { + var err error + + syncCmd.Flags().BoolVar(&syncFlagValues.TryLocalSources, tryLocalSourcesFlagKey, false, "Whether to try & process local copies of sources before retrieving their remote copies") + err = viper.BindPFlag(tryLocalSourcesFlagKey, syncCmd.Flags().Lookup(tryLocalSourcesFlagKey)) + if err != nil { + message.Fatalf("internal error: unable to bind state of flag --%s: %v", tryLocalSourcesFlagKey, err) + } +} + func syncExecute(_ *cobra.Command, _ []string) error { - MaybeSetDebug() + maybeSetDebug() + maybeTryLocalSources() if err := sync(); err != nil { return fmt.Errorf("executing sync command: %w", err) } diff --git a/dist/debian/vdm/DEBIAN/control b/dist/debian/vdm/DEBIAN/control index ad01abe..9920cde 100644 --- a/dist/debian/vdm/DEBIAN/control +++ b/dist/debian/vdm/DEBIAN/control @@ -1,5 +1,5 @@ Package: vdm -Version: 0.2.1 +Version: 0.3.0 Architecture: amd64 Maintainer: Ryan Price Description: Versioned Dependency Manager diff --git a/dist/man/man.1.md b/dist/man/man.1.md index b24b915..62ef18a 100644 --- a/dist/man/man.1.md +++ b/dist/man/man.1.md @@ -1,4 +1,4 @@ -% VDM(1) vdm 0.2.1 +% VDM(1) vdm 0.3.0 % Ryan J. Price % 2023 diff --git a/internal/message/message.go b/internal/message/message.go index f0f4f9c..1b136c6 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -5,12 +5,14 @@ package message import ( "fmt" "os" + + "github.com/opensourcecorp/vdm/cmd/flagvars" ) // Debugf prints out debug-level information messages with a formatting // directive. func Debugf(format string, args ...any) { - if os.Getenv("DEBUG") != "" { + if os.Getenv(flagvars.Debug) != "" { fmt.Printf("DEBUG: "+format+"\n", args...) } } diff --git a/internal/remotes/archive.go b/internal/remotes/archive.go new file mode 100644 index 0000000..7e83ead --- /dev/null +++ b/internal/remotes/archive.go @@ -0,0 +1,14 @@ +package remotes + +import ( + "github.com/opensourcecorp/vdm/internal/vdmspec" +) + +// SyncArchive is the root of the sync operations for "archive" remote types. +func SyncArchive(remote vdmspec.Remote) error { + return nil +} + +func retrieveArchive(remote vdmspec.Remote) (err error) { + return nil +} diff --git a/internal/remotes/local.go b/internal/remotes/local.go new file mode 100644 index 0000000..a2b08ea --- /dev/null +++ b/internal/remotes/local.go @@ -0,0 +1,14 @@ +package remotes + +import ( + "github.com/opensourcecorp/vdm/internal/vdmspec" +) + +// SyncLocal is the root of the sync operations for "local" remote types. +func SyncLocal(remote vdmspec.Remote) error { + return nil +} + +func retrieveLocal(remote vdmspec.Remote) (err error) { + return nil +} diff --git a/internal/vdmspec/spec.go b/internal/vdmspec/spec.go index 68e8cae..7f2e236 100644 --- a/internal/vdmspec/spec.go +++ b/internal/vdmspec/spec.go @@ -32,6 +32,14 @@ type Remote struct { // Destination is the relative or absolute path on disk that Source will be // placed at Destination string `json:"destination" yaml:"destination"` + // TryLocalSource helps define behavior driven by the `try-local-sources` + // CLI flag, which allows checking for a local version of a Remote Source, + // and falling back to the other Source field if the local path does not + // exist. This is especially useful for when you might be developing one of + // your Remotes in a nearby directory, and want to copy over that version of + // the Remote and not keep pushing-and-pulling to a Git upstream just to + // test the changes. + TryLocalSource string `json:"try_local_source" yaml:"try_local_source"` } const ( From dcc7a0168b6c88b06045aa3e863264486728d411 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sat, 10 Aug 2024 23:41:49 -0500 Subject: [PATCH 03/17] WIP for archive & sumdb logic --- cmd/doc.go | 4 +- internal/archive/archive.go | 97 +++++++++++++++++++ internal/archive/archive_test.go | 7 ++ internal/filetree/filetree.go | 27 ++++++ internal/filetree/filetree_test.go | 30 ++++++ internal/remotes/doc.go | 4 +- internal/sumdb/doc.go | 3 + internal/sumdb/sumdb.go | 18 ++++ internal/sumdb/sumdb_test.go | 46 +++++++++ internal/vdmspec/doc.go | 6 +- scripts/ci.sh | 21 ++-- .../filetree/subdir/subdir-2/subdir-2-file | 0 testdata/filetree/subdir/subdir-file | 0 testdata/filetree/top-file | 0 testdata/sumdb/sha256test.txt | 1 + 15 files changed, 247 insertions(+), 17 deletions(-) create mode 100644 internal/archive/archive.go create mode 100644 internal/archive/archive_test.go create mode 100644 internal/filetree/filetree.go create mode 100644 internal/filetree/filetree_test.go create mode 100644 internal/sumdb/doc.go create mode 100644 internal/sumdb/sumdb.go create mode 100644 internal/sumdb/sumdb_test.go create mode 100644 testdata/filetree/subdir/subdir-2/subdir-2-file create mode 100644 testdata/filetree/subdir/subdir-file create mode 100644 testdata/filetree/top-file create mode 100644 testdata/sumdb/sha256test.txt diff --git a/cmd/doc.go b/cmd/doc.go index 669e3cf..2f75f76 100644 --- a/cmd/doc.go +++ b/cmd/doc.go @@ -1,4 +1,2 @@ -/* -Package cmd calls the implementation logic for vdm. -*/ +// Package cmd calls the implementation logic for vdm. package cmd diff --git a/internal/archive/archive.go b/internal/archive/archive.go new file mode 100644 index 0000000..bd15d0f --- /dev/null +++ b/internal/archive/archive.go @@ -0,0 +1,97 @@ +package archive + +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "io" + "os" + "strings" +) + +// Much of the following functions taken from: +// https://www.arthurkoziel.com/writing-tar-gz-files-in-go/ +func createArchive(files []string, archivePath string) (err error) { + if !strings.HasSuffix(archivePath, ".tar.gz") { + return errors.New("provided archive path must end in .tar.gz") + } + + buf, err := os.Create(archivePath) + if err != nil { + return fmt.Errorf("opening target archive path: %w", err) + } + defer func() { + if closeErr := buf.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing target archive file: %w", closeErr)) + } + }() + + // gzip and tar get their own writers, and note that they are chained -- tar + // writes to gzip, which writes to the buffer + gzipWriter := gzip.NewWriter(buf) + defer func() { + if closeErr := gzipWriter.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing gzip writer: %w", closeErr)) + } + }() + + tarWriter := tar.NewWriter(gzipWriter) + defer func() { + if closeErr := tarWriter.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing tar writer: %w", closeErr)) + } + }() + + for _, fileName := range files { + err := addToArchive(tarWriter, fileName) + if err != nil { + return fmt.Errorf("adding %s to archive: %w", fileName, err) + } + } + + return err +} + +func addToArchive(tarWriter *tar.Writer, fileName string) (err error) { + file, err := os.Open(fileName) + if err != nil { + return fmt.Errorf("opening file %s: %w", fileName, err) + } + defer func() { + if closeErr := file.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing file: %w", closeErr)) + } + }() + + info, err := file.Stat() + if err != nil { + return fmt.Errorf("getting file info for %s: %w", fileName, err) + } + + // Tar needs file headers, so create one from the file info + header, err := tar.FileInfoHeader(info, info.Name()) + if err != nil { + return fmt.Errorf("creating tar header for %s: %w", fileName, err) + } + + // Use full path as name (FileInfoHeader only takes the basename) + // If we don't do this the directory strucuture would + // not be preserved + // https://golang.org/src/archive/tar/common.go?#L626 + header.Name = fileName + + // Write file header to the tar archive + err = tarWriter.WriteHeader(header) + if err != nil { + return fmt.Errorf("writing tar header for %s: %w", fileName, err) + } + + // Copy file content to tar archive + _, err = io.Copy(tarWriter, file) + if err != nil { + return fmt.Errorf("adding file %s to tar archive: %w", fileName, err) + } + + return err +} diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go new file mode 100644 index 0000000..03d55fb --- /dev/null +++ b/internal/archive/archive_test.go @@ -0,0 +1,7 @@ +package archive + +import "testing" + +func TestCreateArchive(t *testing.T) { + t.Fatal("not implemented") +} diff --git a/internal/filetree/filetree.go b/internal/filetree/filetree.go new file mode 100644 index 0000000..01c4989 --- /dev/null +++ b/internal/filetree/filetree.go @@ -0,0 +1,27 @@ +package filetree + +import ( + "fmt" + "os" + "path/filepath" +) + +// GetFilePathsInDirectory traverse a directory tree from the provided root, and +// returns a slice of the files in the tree. +func GetFilePathsInDirectory(rootDir string) ([]string, error) { + var files []string + err := filepath.Walk(rootDir, func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + if !f.IsDir() { + files = append(files, path) + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("walking directory tree: %w", err) + } + + return files, nil +} diff --git a/internal/filetree/filetree_test.go b/internal/filetree/filetree_test.go new file mode 100644 index 0000000..aed22b8 --- /dev/null +++ b/internal/filetree/filetree_test.go @@ -0,0 +1,30 @@ +package filetree + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetFilePathsInDirectory(t *testing.T) { + t.Run("works", func(t *testing.T) { + root := "../../testdata/filetree" + want := []string{ + filepath.Join(root, "top-file"), + filepath.Join(root, "subdir", "subdir-file"), + filepath.Join(root, "subdir", "subdir-2", "subdir-2-file"), + } + got, err := GetFilePathsInDirectory(root) + require.NoError(t, err) + + assert.ElementsMatch(t, want, got) + }) + + t.Run("fails in some way, such as when given nonexistent root", func(t *testing.T) { + badRoot := "dir-that-doesnt-exist" + _, err := GetFilePathsInDirectory(badRoot) + assert.Error(t, err) + }) +} diff --git a/internal/remotes/doc.go b/internal/remotes/doc.go index cc7572b..3ffcd76 100644 --- a/internal/remotes/doc.go +++ b/internal/remotes/doc.go @@ -1,4 +1,2 @@ -/* -Package remotes defines logic for the various types of remotes that vdm supports. -*/ +// Package remotes defines logic for the various types of remotes that vdm supports. package remotes diff --git a/internal/sumdb/doc.go b/internal/sumdb/doc.go new file mode 100644 index 0000000..a87c1e9 --- /dev/null +++ b/internal/sumdb/doc.go @@ -0,0 +1,3 @@ +// Package sumdb provides tooling for calculating SHA sums for requested +// dependencies. +package sumdb diff --git a/internal/sumdb/sumdb.go b/internal/sumdb/sumdb.go new file mode 100644 index 0000000..d2fc761 --- /dev/null +++ b/internal/sumdb/sumdb.go @@ -0,0 +1,18 @@ +package sumdb + +import ( + "crypto/sha256" + "fmt" + "io" +) + +// CalculateSHASum takes an arbitrary [io.Reader] and calculates the SHA256 +// checksum for it. +func CalculateSHASum(reader io.Reader) (string, error) { + hasher := sha256.New() + if _, err := io.Copy(hasher, reader); err != nil { + return "", fmt.Errorf("writing reader to hasher: %w", err) + } + sum := fmt.Sprintf("%x", hasher.Sum(nil)) + return sum, nil +} diff --git a/internal/sumdb/sumdb_test.go b/internal/sumdb/sumdb_test.go new file mode 100644 index 0000000..b32ac7e --- /dev/null +++ b/internal/sumdb/sumdb_test.go @@ -0,0 +1,46 @@ +package sumdb + +import ( + "bytes" + "compress/gzip" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCalculateSHASum(t *testing.T) { + t.Run("works with an open file", func(t *testing.T) { + f, err := os.Open("../../testdata/sumdb/sha256test.txt") + require.NoError(t, err) + t.Cleanup(func() { + closeErr := f.Close() + require.NoError(t, closeErr) + }) + + want := `25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b` + got, err := CalculateSHASum(f) + + assert.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("works with an in-memory gzipped directory", func(t *testing.T) { + var b bytes.Buffer + zw := gzip.NewWriter(&b) + + f, err := os.Open("../../testdata/sumdb/sha256test.txt") + require.NoError(t, err) + t.Cleanup(func() { + closeErr := f.Close() + require.NoError(t, closeErr) + }) + + want := `25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b` + got, err := CalculateSHASum(f) + + assert.NoError(t, err) + assert.Equal(t, want, got) + }) +} diff --git a/internal/vdmspec/doc.go b/internal/vdmspec/doc.go index 0f9316c..3dd2906 100644 --- a/internal/vdmspec/doc.go +++ b/internal/vdmspec/doc.go @@ -1,5 +1,3 @@ -/* -Package vdmspec defines the [Spec] and [Remote] struct types, and their -associated methods. -*/ +// Package vdmspec defines the [Spec] and [Remote] struct types, and their +// associated methods. package vdmspec diff --git a/scripts/ci.sh b/scripts/ci.sh index 5bdab77..c888d19 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -11,16 +11,22 @@ if ! go vet ./... ; then failures+=('go-vet') fi -printf '>> Go linter\n' +printf '>> Go linter (staticcheck)\n' +if ! go run honnef.co/go/tools/cmd/staticcheck@latest ./... ; then + printf '>>> Failed go-lint-staticcheck\n' > /dev/stderr + failures+=('go-lint-staticcheck') +fi + +printf '>> Go linter (revive)\n' if ! go run github.com/mgechev/revive@latest --set_exit_status ./... ; then - printf '>>> Failed go-lint\n' > /dev/stderr - failures+=('go-lint') + printf '>>> Failed go-lint-revive\n' > /dev/stderr + failures+=('go-lint-revive') fi -printf '>> Go error checker\n' +printf '>> Go linter (errcheck)\n' if ! go run github.com/kisielk/errcheck@latest ./... ; then - printf '>>> Failed go-error-check\n' > /dev/stderr - failures+=('go-error-check') + printf '>>> Failed go-lint-errcheck\n' > /dev/stderr + failures+=('go-lint-errcheck') fi printf '>> Go test\n' @@ -36,7 +42,8 @@ if ! make -s package ; then fi if [[ "${#failures[@]}" -gt 0 ]] ; then - printf '> One or more checks failed, see output above\n' > /dev/stderr + printf '> The following checks failed -- logs for each are above\n' > /dev/stderr + printf '%s\n' "${failures[@]}" exit 1 fi diff --git a/testdata/filetree/subdir/subdir-2/subdir-2-file b/testdata/filetree/subdir/subdir-2/subdir-2-file new file mode 100644 index 0000000..e69de29 diff --git a/testdata/filetree/subdir/subdir-file b/testdata/filetree/subdir/subdir-file new file mode 100644 index 0000000..e69de29 diff --git a/testdata/filetree/top-file b/testdata/filetree/top-file new file mode 100644 index 0000000..e69de29 diff --git a/testdata/sumdb/sha256test.txt b/testdata/sumdb/sha256test.txt new file mode 100644 index 0000000..ecd37e2 --- /dev/null +++ b/testdata/sumdb/sha256test.txt @@ -0,0 +1 @@ +Contents here are irrelevant From 64ede251cb01b41a278f749b40a280bfeaf75b8a Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 11 Aug 2024 16:47:21 -0500 Subject: [PATCH 04/17] Got archiver working with the right dir structure, needs a more robust test though. TODO comment left. --- internal/archive/archive.go | 59 ++++++++++++++++++++++++--- internal/archive/archive_test.go | 43 ++++++++++++++++++- internal/{ => archive}/sumdb/doc.go | 0 internal/{ => archive}/sumdb/sumdb.go | 0 internal/archive/sumdb/sumdb_test.go | 45 ++++++++++++++++++++ internal/filetree/filetree.go | 7 +++- internal/filetree/filetree_test.go | 4 +- internal/sumdb/sumdb_test.go | 46 --------------------- 8 files changed, 148 insertions(+), 56 deletions(-) rename internal/{ => archive}/sumdb/doc.go (100%) rename internal/{ => archive}/sumdb/sumdb.go (100%) create mode 100644 internal/archive/sumdb/sumdb_test.go delete mode 100644 internal/sumdb/sumdb_test.go diff --git a/internal/archive/archive.go b/internal/archive/archive.go index bd15d0f..35059e1 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -7,17 +7,32 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" + + "github.com/opensourcecorp/vdm/internal/filetree" ) // Much of the following functions taken from: // https://www.arthurkoziel.com/writing-tar-gz-files-in-go/ -func createArchive(files []string, archivePath string) (err error) { + +// CreateArchive +func CreateArchive(rootDir string, archivePath string) (err error) { if !strings.HasSuffix(archivePath, ".tar.gz") { return errors.New("provided archive path must end in .tar.gz") } - buf, err := os.Create(archivePath) + rootDirAbs, err := filepath.Abs(rootDir) + if err != nil { + return fmt.Errorf("determining abspath of provided root dir %s: %w", rootDir, err) + } + + archivePathAbs, err := filepath.Abs(archivePath) + if err != nil { + return fmt.Errorf("determining abspath of provided archive path %s: %w", archivePath, err) + } + + buf, err := os.Create(archivePathAbs) if err != nil { return fmt.Errorf("opening target archive path: %w", err) } @@ -28,7 +43,8 @@ func createArchive(files []string, archivePath string) (err error) { }() // gzip and tar get their own writers, and note that they are chained -- tar - // writes to gzip, which writes to the buffer + // writes to gzip, which writes to the buffer. So, we only need to write to + // the tar writer gzipWriter := gzip.NewWriter(buf) defer func() { if closeErr := gzipWriter.Close(); closeErr != nil { @@ -43,8 +59,13 @@ func createArchive(files []string, archivePath string) (err error) { } }() + files, err := filetree.GetFilePathsInDirectory(rootDirAbs) + if err != nil { + return fmt.Errorf("populating list of files from %s: %w", rootDir, err) + } + for _, fileName := range files { - err := addToArchive(tarWriter, fileName) + err := addToArchive(tarWriter, rootDirAbs, fileName) if err != nil { return fmt.Errorf("adding %s to archive: %w", fileName, err) } @@ -53,7 +74,7 @@ func createArchive(files []string, archivePath string) (err error) { return err } -func addToArchive(tarWriter *tar.Writer, fileName string) (err error) { +func addToArchive(tarWriter *tar.Writer, rootDirAbs string, fileName string) (err error) { file, err := os.Open(fileName) if err != nil { return fmt.Errorf("opening file %s: %w", fileName, err) @@ -75,11 +96,20 @@ func addToArchive(tarWriter *tar.Writer, fileName string) (err error) { return fmt.Errorf("creating tar header for %s: %w", fileName, err) } + topLevelDir, err := maybeGetTopLevelDir(rootDirAbs) + if err != nil { + return fmt.Errorf("checking for top-level directory when adding to archive: %w", err) + } + fileNameClean := strings.ReplaceAll(fileName, rootDirAbs+string(filepath.Separator), "") + if topLevelDir != "" { + fileNameClean = filepath.Join(topLevelDir, fileNameClean) + } + // Use full path as name (FileInfoHeader only takes the basename) // If we don't do this the directory strucuture would // not be preserved // https://golang.org/src/archive/tar/common.go?#L626 - header.Name = fileName + header.Name = fileNameClean // Write file header to the tar archive err = tarWriter.WriteHeader(header) @@ -95,3 +125,20 @@ func addToArchive(tarWriter *tar.Writer, fileName string) (err error) { return err } + +func maybeGetTopLevelDir(rootDirAbs string) (topLevelDir string, err error) { + topLevelFileInfo, err := os.Stat(rootDirAbs) + if err != nil { + return "", fmt.Errorf("getting file info for %s: %w", rootDirAbs, err) + } + + // We only want to include the top-level directory for archive pathing if + // it's *actually* a directory, obviously + if !topLevelFileInfo.IsDir() { + topLevelDir = "" + } else { + topLevelDir = filepath.Base(rootDirAbs) + } + + return topLevelDir, nil +} diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 03d55fb..bfefd66 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -1,7 +1,46 @@ package archive -import "testing" +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) func TestCreateArchive(t *testing.T) { - t.Fatal("not implemented") + archiveRoot := "../../testdata/filetree" + archivePath := filepath.Join(os.TempDir(), "archive-test.tar.gz") + t.Cleanup(func() { + err := os.RemoveAll(archivePath) + require.NoError(t, err) + }) + + err := CreateArchive(archiveRoot, archivePath) + require.NoError(t, err) + + // TODO: add test for inspecting contents once we implement an archive + // extractor -- as of now, I'm just checking on the CLI if the expected tree + // matches +} + +func TestMaybeGetTopLevelDir(t *testing.T) { + t.Run("works when rootDir is a directory", func(t *testing.T) { + rootDir := "../../testdata/filetree" + wantTopLevelDir := "filetree" + gotTopLevelDir, err := maybeGetTopLevelDir(rootDir) + + assert.NoError(t, err) + assert.Equal(t, wantTopLevelDir, gotTopLevelDir) + }) + + t.Run("works when rootDir is a file", func(t *testing.T) { + rootDir := "../../testdata/filetree/top-file" + wantTopLevelDir := "" + gotTopLevelDir, err := maybeGetTopLevelDir(rootDir) + + assert.NoError(t, err) + assert.Equal(t, wantTopLevelDir, gotTopLevelDir) + }) } diff --git a/internal/sumdb/doc.go b/internal/archive/sumdb/doc.go similarity index 100% rename from internal/sumdb/doc.go rename to internal/archive/sumdb/doc.go diff --git a/internal/sumdb/sumdb.go b/internal/archive/sumdb/sumdb.go similarity index 100% rename from internal/sumdb/sumdb.go rename to internal/archive/sumdb/sumdb.go diff --git a/internal/archive/sumdb/sumdb_test.go b/internal/archive/sumdb/sumdb_test.go new file mode 100644 index 0000000..639bc24 --- /dev/null +++ b/internal/archive/sumdb/sumdb_test.go @@ -0,0 +1,45 @@ +package sumdb + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCalculateSHASum(t *testing.T) { + t.Run("works with an open file", func(t *testing.T) { + f, err := os.Open("../../../testdata/sumdb/sha256test.txt") + require.NoError(t, err) + t.Cleanup(func() { + closeErr := f.Close() + require.NoError(t, closeErr) + }) + + want := `25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b` + got, err := CalculateSHASum(f) + + assert.NoError(t, err) + assert.Equal(t, want, got) + }) + + // t.Run("works with an in-memory gzipped directory", func(t *testing.T) { + // rootDir := "../../testdata/filetree" + // err := archive.CreateArchive(rootDir, archivePath) + // require.NoError(t, err) + + // f, err := os.Open("../../testdata/sumdb/sha256test.txt") + // require.NoError(t, err) + // t.Cleanup(func() { + // closeErr := f.Close() + // require.NoError(t, closeErr) + // }) + + // want := `25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b` + // got, err := CalculateSHASum(f) + + // assert.NoError(t, err) + // assert.Equal(t, want, got) + // }) +} diff --git a/internal/filetree/filetree.go b/internal/filetree/filetree.go index 01c4989..6f4eaa9 100644 --- a/internal/filetree/filetree.go +++ b/internal/filetree/filetree.go @@ -9,8 +9,13 @@ import ( // GetFilePathsInDirectory traverse a directory tree from the provided root, and // returns a slice of the files in the tree. func GetFilePathsInDirectory(rootDir string) ([]string, error) { + rootDirAbs, err := filepath.Abs(rootDir) + if err != nil { + return nil, fmt.Errorf("determining abspath of rootDir %s: %w", rootDirAbs, err) + } + var files []string - err := filepath.Walk(rootDir, func(path string, f os.FileInfo, err error) error { + err = filepath.Walk(rootDirAbs, func(path string, f os.FileInfo, err error) error { if err != nil { return err } diff --git a/internal/filetree/filetree_test.go b/internal/filetree/filetree_test.go index aed22b8..eb5cd40 100644 --- a/internal/filetree/filetree_test.go +++ b/internal/filetree/filetree_test.go @@ -10,7 +10,9 @@ import ( func TestGetFilePathsInDirectory(t *testing.T) { t.Run("works", func(t *testing.T) { - root := "../../testdata/filetree" + root, err := filepath.Abs("../../testdata/filetree") + require.NoError(t, err) + want := []string{ filepath.Join(root, "top-file"), filepath.Join(root, "subdir", "subdir-file"), diff --git a/internal/sumdb/sumdb_test.go b/internal/sumdb/sumdb_test.go deleted file mode 100644 index b32ac7e..0000000 --- a/internal/sumdb/sumdb_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package sumdb - -import ( - "bytes" - "compress/gzip" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCalculateSHASum(t *testing.T) { - t.Run("works with an open file", func(t *testing.T) { - f, err := os.Open("../../testdata/sumdb/sha256test.txt") - require.NoError(t, err) - t.Cleanup(func() { - closeErr := f.Close() - require.NoError(t, closeErr) - }) - - want := `25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b` - got, err := CalculateSHASum(f) - - assert.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("works with an in-memory gzipped directory", func(t *testing.T) { - var b bytes.Buffer - zw := gzip.NewWriter(&b) - - f, err := os.Open("../../testdata/sumdb/sha256test.txt") - require.NoError(t, err) - t.Cleanup(func() { - closeErr := f.Close() - require.NoError(t, closeErr) - }) - - want := `25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b` - got, err := CalculateSHASum(f) - - assert.NoError(t, err) - assert.Equal(t, want, got) - }) -} From ba150198680c667d7c7e03234c7fdc572fb7a0ed Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 11 Aug 2024 18:29:39 -0500 Subject: [PATCH 05/17] Remote sum cache writer might be working now --- cmd/flagsupport.go | 10 ++--- cmd/flagvars/flagvars.go | 11 ----- cmd/vars/vars.go | 45 +++++++++++++++++++ internal/archive/sumdb/sumdb.go | 52 +++++++++++++++++++++- internal/archive/sumdb/sumdb_test.go | 66 +++++++++++++++++++++------- internal/message/message.go | 4 +- internal/vdmspec/spec.go | 5 +++ 7 files changed, 158 insertions(+), 35 deletions(-) delete mode 100644 cmd/flagvars/flagvars.go create mode 100644 cmd/vars/vars.go diff --git a/cmd/flagsupport.go b/cmd/flagsupport.go index df57c92..0f1e888 100644 --- a/cmd/flagsupport.go +++ b/cmd/flagsupport.go @@ -3,7 +3,7 @@ package cmd import ( "os" - "github.com/opensourcecorp/vdm/cmd/flagvars" + "github.com/opensourcecorp/vdm/cmd/vars" "github.com/opensourcecorp/vdm/internal/message" "github.com/spf13/viper" ) @@ -12,9 +12,9 @@ import ( // the caller. func maybeSetDebug() { if viper.GetBool(debugFlagKey) { - err := os.Setenv(flagvars.Debug, "true") + err := os.Setenv(vars.Debug, "true") if err != nil { - message.Fatalf("internal error: unable to set environment variable %s", flagvars.Debug) + message.Fatalf("internal error: unable to set environment variable %s", vars.Debug) } } } @@ -23,9 +23,9 @@ func maybeSetDebug() { // was set as a flag by the caller. func maybeTryLocalSources() { if viper.GetBool(tryLocalSourcesFlagKey) { - err := os.Setenv(flagvars.TryLocalSources, "true") + err := os.Setenv(vars.TryLocalSources, "true") if err != nil { - message.Fatalf("internal error: unable to set environment variable %s", flagvars.TryLocalSources) + message.Fatalf("internal error: unable to set environment variable %s", vars.TryLocalSources) } } } diff --git a/cmd/flagvars/flagvars.go b/cmd/flagvars/flagvars.go deleted file mode 100644 index 7cd76ea..0000000 --- a/cmd/flagvars/flagvars.go +++ /dev/null @@ -1,11 +0,0 @@ -// Package flagvars houses constants etc. for working with command-line flag -// values across packages. These helpers are pushed down to their own package in -// order to avoid import cycles. -package flagvars - -const ( - // Debug anchors to the DEBUG env var - Debug = "DEBUG" - // TryLocalSources anchors to the TRY_LOCAL_SOURCES env var - TryLocalSources = "TRY_LOCAL_SOURCES" -) diff --git a/cmd/vars/vars.go b/cmd/vars/vars.go new file mode 100644 index 0000000..1aa67ff --- /dev/null +++ b/cmd/vars/vars.go @@ -0,0 +1,45 @@ +// Package vars houses constants etc. for working with command-line flag values +// across packages. These helpers are pushed down to their own package in order +// to avoid import cycles. +package vars + +import ( + "os" + "path/filepath" +) + +const ( + // Debug anchors to the DEBUG env var + Debug = "DEBUG" + // TryLocalSources anchors to the TRY_LOCAL_SOURCES env var + TryLocalSources = "TRY_LOCAL_SOURCES" + // VDMHomeEnvVarName anchors to the VDM_HOME env var + VDMHomeEnvVarName = "VDM_HOME" +) + +var ( + // VDMHome stores the location of vdm's own home directory + VDMHome string +) + +func init() { + homedir, err := os.UserHomeDir() + if err != nil { + panic("unable to determine home directory") + } + + vdmHomeOverride, ok := os.LookupEnv(VDMHomeEnvVarName) + if ok { + VDMHome = filepath.Join(vdmHomeOverride, ".vdm") + } else { + VDMHome = filepath.Join(homedir, ".vdm") + } + err = os.Setenv(VDMHomeEnvVarName, VDMHome) + if err != nil { + panic("unable to set VDM home directory") + } +} + +func GetVDMCacheDir() string { + return filepath.Join(os.Getenv(VDMHomeEnvVarName), "cache") +} diff --git a/internal/archive/sumdb/sumdb.go b/internal/archive/sumdb/sumdb.go index d2fc761..1068611 100644 --- a/internal/archive/sumdb/sumdb.go +++ b/internal/archive/sumdb/sumdb.go @@ -2,12 +2,19 @@ package sumdb import ( "crypto/sha256" + "encoding/base64" + "errors" "fmt" "io" + "os" + "path/filepath" + + "github.com/opensourcecorp/vdm/cmd/vars" + "github.com/opensourcecorp/vdm/internal/vdmspec" ) -// CalculateSHASum takes an arbitrary [io.Reader] and calculates the SHA256 -// checksum for it. +// CalculateSHASum takes an arbitrary [io.Reader] (such as an open file handle) +// and calculates the SHA256 checksum for it. func CalculateSHASum(reader io.Reader) (string, error) { hasher := sha256.New() if _, err := io.Copy(hasher, reader); err != nil { @@ -16,3 +23,44 @@ func CalculateSHASum(reader io.Reader) (string, error) { sum := fmt.Sprintf("%x", hasher.Sum(nil)) return sum, nil } + +func CacheRemote(remote vdmspec.Remote, archiveFileHandle io.Reader) (err error) { + err = os.MkdirAll(vars.GetVDMCacheDir(), 0755) + if err != nil { + return fmt.Errorf("creating vdm cache directory %s: %w", vars.GetVDMCacheDir(), err) + } + + cacheFileName := StringAsBase64(remote.Source) + f, err := os.Create(filepath.Join(vars.GetVDMCacheDir(), cacheFileName)) + if err != nil { + return fmt.Errorf("creating file %s: %w", cacheFileName, err) + } + defer func() { + if closeErr := f.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing cache file %s: %w", cacheFileName, closeErr)) + } + }() + + sum, err := CalculateSHASum(archiveFileHandle) + if err != nil { + return fmt.Errorf("calculating sum when caching remote %s: %w", remote.Source, err) + } + + contents := fmt.Sprintf("%s %s %s", remote.Source, remote.Version, sum) + + err = os.WriteFile(f.Name(), []byte(contents), 0644) + if err != nil { + return fmt.Errorf("writing cache file %s: %w", f.Name(), err) + } + + return err +} + +func StringAsBase64(s string) string { + // We use base64-encoded names for caches, because remote sources have + // slashes and that can break FS pathing + data := []byte(s) + encodedBytes := make([]byte, base64.StdEncoding.EncodedLen(len(data))) + base64.StdEncoding.Encode(encodedBytes, data) + return string(encodedBytes) +} diff --git a/internal/archive/sumdb/sumdb_test.go b/internal/archive/sumdb/sumdb_test.go index 639bc24..bba2031 100644 --- a/internal/archive/sumdb/sumdb_test.go +++ b/internal/archive/sumdb/sumdb_test.go @@ -1,9 +1,14 @@ package sumdb import ( + "fmt" "os" + "path/filepath" "testing" + "github.com/opensourcecorp/vdm/cmd/vars" + "github.com/opensourcecorp/vdm/internal/archive" + "github.com/opensourcecorp/vdm/internal/vdmspec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -24,22 +29,53 @@ func TestCalculateSHASum(t *testing.T) { assert.Equal(t, want, got) }) - // t.Run("works with an in-memory gzipped directory", func(t *testing.T) { - // rootDir := "../../testdata/filetree" - // err := archive.CreateArchive(rootDir, archivePath) - // require.NoError(t, err) + t.Run("works on a created archive file", func(t *testing.T) { + rootDir := "../../../testdata/filetree" + archivePath := filepath.Join(os.TempDir(), "archive-to-hash.tar.gz") + err := archive.CreateArchive(rootDir, archivePath) + require.NoError(t, err) + + f, err := os.Open(archivePath) + require.NoError(t, err) + t.Cleanup(func() { + err := os.RemoveAll(archivePath) + require.NoError(t, err) + }) + + want := `c1c5e8e5cd54819ea0243db2b203e581aae7308937fb575204914fc2e41ef2a7` + got, err := CalculateSHASum(f) + + assert.NoError(t, err) + assert.Equal(t, want, got) + }) +} + +func TestStringAsBase64(t *testing.T) { + src := "https://github.com/org/user" + want := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2Vy" + got := StringAsBase64(src) + + assert.Equal(t, want, got) +} + +func TestCacheRemote(t *testing.T) { + t.Setenv(vars.VDMHomeEnvVarName, filepath.Join(os.TempDir(), "vdmhome")) + + f, err := os.Open("../../../testdata/sumdb/sha256test.txt") + require.NoError(t, err) + + remote := vdmspec.Remote{ + Source: "https://github.com/org/user", + Version: "v1.0.0", + } + cachedPath := filepath.Join(os.Getenv(vars.VDMHomeEnvVarName), "cache", StringAsBase64(remote.Source)) - // f, err := os.Open("../../testdata/sumdb/sha256test.txt") - // require.NoError(t, err) - // t.Cleanup(func() { - // closeErr := f.Close() - // require.NoError(t, closeErr) - // }) + err = CacheRemote(remote, f) + assert.NoError(t, err) - // want := `25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b` - // got, err := CalculateSHASum(f) + want := fmt.Sprintf("%s %s %s", remote.Source, remote.Version, "25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b") + got, err := os.ReadFile(cachedPath) + require.NoError(t, err) - // assert.NoError(t, err) - // assert.Equal(t, want, got) - // }) + assert.Equal(t, want, string(got)) } diff --git a/internal/message/message.go b/internal/message/message.go index 1b136c6..6201145 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -6,13 +6,13 @@ import ( "fmt" "os" - "github.com/opensourcecorp/vdm/cmd/flagvars" + "github.com/opensourcecorp/vdm/cmd/vars" ) // Debugf prints out debug-level information messages with a formatting // directive. func Debugf(format string, args ...any) { - if os.Getenv(flagvars.Debug) != "" { + if os.Getenv(vars.Debug) != "" { fmt.Printf("DEBUG: "+format+"\n", args...) } } diff --git a/internal/vdmspec/spec.go b/internal/vdmspec/spec.go index 7f2e236..82632ee 100644 --- a/internal/vdmspec/spec.go +++ b/internal/vdmspec/spec.go @@ -16,6 +16,11 @@ type Spec struct { Remotes []Remote `json:"remotes" yaml:"remotes"` } +type Remoter interface { + Sync() error + Cache() error +} + // Remote defines the structure of each remote configuration in the vdm // specfile. type Remote struct { From 9560835753e334c2fd4144ed5a86b918ad324d8a Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 11 Aug 2024 23:43:00 -0500 Subject: [PATCH 06/17] Start working on the Remoter interface, and change CLI commands to come from factories --- Makefile | 1 + cmd/root.go | 61 ++++++++++++++++------------ cmd/sync.go | 41 +++++++++++-------- cmd/vars/vars.go | 1 + internal/archive/archive.go | 3 +- internal/archive/doc.go | 3 ++ internal/archive/sumdb/sumdb.go | 11 +++-- internal/archive/sumdb/sumdb_test.go | 9 ++-- internal/filetree/doc.go | 3 ++ internal/remotes/archive.go | 13 ------ internal/remotes/file.go | 34 +++++++++++++--- internal/remotes/file_test.go | 6 +++ internal/remotes/git.go | 54 ++++++++++++++---------- internal/remotes/git_test.go | 26 ++++++------ internal/remotes/local.go | 13 ------ internal/vdmspec/spec.go | 34 +++++++++------- internal/vdmspec/validate.go | 12 +++--- 17 files changed, 187 insertions(+), 138 deletions(-) create mode 100644 internal/archive/doc.go create mode 100644 internal/filetree/doc.go diff --git a/Makefile b/Makefile index d1e137a..554d7d4 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ all: ci package package-debian ci: clean @bash ./scripts/ci.sh + @make -s clean # test is just an alias for ci test: ci diff --git a/cmd/root.go b/cmd/root.go index 35b7f4e..8836908 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,8 +1,8 @@ package cmd import ( + "errors" "fmt" - "os" "github.com/opensourcecorp/vdm/internal/message" "github.com/spf13/cobra" @@ -12,25 +12,6 @@ import ( // !!! DO NOT TOUCH, the version-bumper script handles updating this !!! const vdmVersion string = "v0.3.0" -var rootCmd = cobra.Command{ - Use: "vdm", - Short: "vdm -- a Versioned-Dependency Manager", - Long: "vdm is used to manage retrieval of arbitrary remote dependencies", - TraverseChildren: true, - Version: vdmVersion, - Run: func(cmd *cobra.Command, args []string) { - maybeSetDebug() - if len(args) == 0 { - message.Errorf("You must provide a subcommand to vdm") - err := cmd.Help() - if err != nil { - message.Fatalf("failed to print help message, somehow") - } - os.Exit(1) - } - }, -} - // rootFlags defines the CLI flags for the root command. type rootFlags struct { SpecFilePath string @@ -47,28 +28,54 @@ const ( debugFlagKey string = "debug" ) -func init() { +func newRootCommand() *cobra.Command { var err error + cmd := &cobra.Command{ + Use: "vdm", + Short: "vdm -- a Versioned-Dependency Manager", + Long: "vdm is used to manage retrieval of arbitrary remote dependencies", + TraverseChildren: true, + Version: vdmVersion, + SilenceUsage: true, + SilenceErrors: true, + RunE: executeRootCommand, + } - rootCmd.PersistentFlags().StringVar(&rootFlagValues.SpecFilePath, specFilePathFlagKey, "./vdm.yaml", "Path to vdm specfile") - err = viper.BindPFlag(specFilePathFlagKey, rootCmd.PersistentFlags().Lookup(specFilePathFlagKey)) + cmd.PersistentFlags().StringVar(&rootFlagValues.SpecFilePath, specFilePathFlagKey, "./vdm.yaml", "Path to vdm specfile") + err = viper.BindPFlag(specFilePathFlagKey, cmd.PersistentFlags().Lookup(specFilePathFlagKey)) if err != nil { message.Fatalf("internal error: unable to bind state of flag --%s: %v", specFilePathFlagKey, err) } - rootCmd.PersistentFlags().BoolVar(&rootFlagValues.Debug, debugFlagKey, false, "Show debug messages during runtime") - err = viper.BindPFlag(debugFlagKey, rootCmd.PersistentFlags().Lookup(debugFlagKey)) + cmd.PersistentFlags().BoolVar(&rootFlagValues.Debug, debugFlagKey, false, "Show debug messages during runtime") + err = viper.BindPFlag(debugFlagKey, cmd.PersistentFlags().Lookup(debugFlagKey)) if err != nil { message.Fatalf("internal error: unable to bind state of flag --%s: %v", debugFlagKey, err) } - rootCmd.AddCommand(syncCmd) + cmd.AddCommand(newSyncCommand()) + + return cmd +} + +func executeRootCommand(cmd *cobra.Command, args []string) error { + maybeSetDebug() + if len(args) == 0 { + message.Errorf("You must provide a subcommand to vdm") + err := cmd.Help() + if err != nil { + return errors.New("failed to print help message, somehow") + } + } + + return nil } // Execute wraps the primary execution logic for vdm's root command, and returns // any errors encountered to the caller. func Execute() error { - if err := rootCmd.Execute(); err != nil { + cmd := newRootCommand() + if err := cmd.Execute(); err != nil { return fmt.Errorf("executing root command: %w", err) } diff --git a/cmd/sync.go b/cmd/sync.go index fe554e3..cae3a93 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -10,12 +10,6 @@ import ( "github.com/spf13/viper" ) -var syncCmd = &cobra.Command{ - Use: "sync", - Short: "Sync remotes based on specfile", - RunE: syncExecute, -} - // syncFlags defines the CLI flags for the sync subcommand. type syncFlags struct { TryLocalSources bool @@ -30,17 +24,23 @@ const ( tryLocalSourcesFlagKey string = "try-local-sources" ) -func init() { - var err error +func newSyncCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "sync", + Short: "Sync remotes based on specfile", + RunE: executeSyncSubCommand, + } - syncCmd.Flags().BoolVar(&syncFlagValues.TryLocalSources, tryLocalSourcesFlagKey, false, "Whether to try & process local copies of sources before retrieving their remote copies") - err = viper.BindPFlag(tryLocalSourcesFlagKey, syncCmd.Flags().Lookup(tryLocalSourcesFlagKey)) + cmd.Flags().BoolVar(&syncFlagValues.TryLocalSources, tryLocalSourcesFlagKey, false, "Whether to try & process local copies of sources before retrieving their remote copies") + err := viper.BindPFlag(tryLocalSourcesFlagKey, cmd.Flags().Lookup(tryLocalSourcesFlagKey)) if err != nil { message.Fatalf("internal error: unable to bind state of flag --%s: %v", tryLocalSourcesFlagKey, err) } + + return cmd } -func syncExecute(_ *cobra.Command, _ []string) error { +func executeSyncSubCommand(_ *cobra.Command, _ []string) error { maybeSetDebug() maybeTryLocalSources() if err := sync(); err != nil { @@ -81,19 +81,26 @@ func sync() error { continue } + var determinedRemote vdmspec.Remoter switch remote.Type { case vdmspec.GitType, "": - if err := remotes.SyncGit(remote); err != nil { - return fmt.Errorf("syncing git remote: %w", err) - } + determinedRemote = remotes.Git{Remote: remote} case vdmspec.FileType: - if err := remotes.SyncFile(remote); err != nil { - return fmt.Errorf("syncing file remote: %w", err) - } + determinedRemote = remotes.File{Remote: remote} default: return fmt.Errorf("unrecognized remote type '%s'", remote.Type) } + err = determinedRemote.Cache() + if err != nil { + return fmt.Errorf("caching '%s' remote: %w", remote.Type, err) + } + + err = determinedRemote.Sync() + if err != nil { + return fmt.Errorf("syncing '%s' remote: %w", remote.Type, err) + } + err = remote.WriteVDMMeta() if err != nil { return fmt.Errorf("could not write %s file to disk: %w", vdmspec.MetaFileName, err) diff --git a/cmd/vars/vars.go b/cmd/vars/vars.go index 1aa67ff..d877dc8 100644 --- a/cmd/vars/vars.go +++ b/cmd/vars/vars.go @@ -40,6 +40,7 @@ func init() { } } +// GetVDMCacheDir returns the determined path to vdm's cache directory. func GetVDMCacheDir() string { return filepath.Join(os.Getenv(VDMHomeEnvVarName), "cache") } diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 35059e1..7964e00 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -16,7 +16,8 @@ import ( // Much of the following functions taken from: // https://www.arthurkoziel.com/writing-tar-gz-files-in-go/ -// CreateArchive +// CreateArchive writes a gzipped tarball based on the provided root directory +// from which to construct the archive, and its target file name. func CreateArchive(rootDir string, archivePath string) (err error) { if !strings.HasSuffix(archivePath, ".tar.gz") { return errors.New("provided archive path must end in .tar.gz") diff --git a/internal/archive/doc.go b/internal/archive/doc.go new file mode 100644 index 0000000..9e3c4d8 --- /dev/null +++ b/internal/archive/doc.go @@ -0,0 +1,3 @@ +// Package archive houses logic for interacting with archive file formats, and +// some of their specific uses within vdm. +package archive diff --git a/internal/archive/sumdb/sumdb.go b/internal/archive/sumdb/sumdb.go index 1068611..8affaae 100644 --- a/internal/archive/sumdb/sumdb.go +++ b/internal/archive/sumdb/sumdb.go @@ -24,13 +24,15 @@ func CalculateSHASum(reader io.Reader) (string, error) { return sum, nil } -func CacheRemote(remote vdmspec.Remote, archiveFileHandle io.Reader) (err error) { +// CacheRemote uses the provided [vdmspec.Remoter] information along with a file +// handle for a target archive to actually write the archive data. +func CacheRemote(remote vdmspec.Remoter, archiveFileHandle io.Reader) (err error) { err = os.MkdirAll(vars.GetVDMCacheDir(), 0755) if err != nil { return fmt.Errorf("creating vdm cache directory %s: %w", vars.GetVDMCacheDir(), err) } - cacheFileName := StringAsBase64(remote.Source) + cacheFileName := StringAsBase64(remote.GetSource()) f, err := os.Create(filepath.Join(vars.GetVDMCacheDir(), cacheFileName)) if err != nil { return fmt.Errorf("creating file %s: %w", cacheFileName, err) @@ -43,10 +45,10 @@ func CacheRemote(remote vdmspec.Remote, archiveFileHandle io.Reader) (err error) sum, err := CalculateSHASum(archiveFileHandle) if err != nil { - return fmt.Errorf("calculating sum when caching remote %s: %w", remote.Source, err) + return fmt.Errorf("calculating sum when caching remote %s: %w", remote.GetSource(), err) } - contents := fmt.Sprintf("%s %s %s", remote.Source, remote.Version, sum) + contents := fmt.Sprintf("%s %s %s", remote.GetSource(), remote.GetVersion(), sum) err = os.WriteFile(f.Name(), []byte(contents), 0644) if err != nil { @@ -56,6 +58,7 @@ func CacheRemote(remote vdmspec.Remote, archiveFileHandle io.Reader) (err error) return err } +// StringAsBase64 does what it says on the tin. func StringAsBase64(s string) string { // We use base64-encoded names for caches, because remote sources have // slashes and that can break FS pathing diff --git a/internal/archive/sumdb/sumdb_test.go b/internal/archive/sumdb/sumdb_test.go index bba2031..e874636 100644 --- a/internal/archive/sumdb/sumdb_test.go +++ b/internal/archive/sumdb/sumdb_test.go @@ -8,6 +8,7 @@ import ( "github.com/opensourcecorp/vdm/cmd/vars" "github.com/opensourcecorp/vdm/internal/archive" + "github.com/opensourcecorp/vdm/internal/remotes" "github.com/opensourcecorp/vdm/internal/vdmspec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -64,9 +65,11 @@ func TestCacheRemote(t *testing.T) { f, err := os.Open("../../../testdata/sumdb/sha256test.txt") require.NoError(t, err) - remote := vdmspec.Remote{ - Source: "https://github.com/org/user", - Version: "v1.0.0", + remote := remotes.Git{ + Remote: vdmspec.Remote{ + Source: "https://github.com/org/user", + Version: "v1.0.0", + }, } cachedPath := filepath.Join(os.Getenv(vars.VDMHomeEnvVarName), "cache", StringAsBase64(remote.Source)) diff --git a/internal/filetree/doc.go b/internal/filetree/doc.go new file mode 100644 index 0000000..01ae666 --- /dev/null +++ b/internal/filetree/doc.go @@ -0,0 +1,3 @@ +// Package filetree contains functionality for determining & operating on file +// paths in a tree. +package filetree diff --git a/internal/remotes/archive.go b/internal/remotes/archive.go index 7e83ead..a558bee 100644 --- a/internal/remotes/archive.go +++ b/internal/remotes/archive.go @@ -1,14 +1 @@ package remotes - -import ( - "github.com/opensourcecorp/vdm/internal/vdmspec" -) - -// SyncArchive is the root of the sync operations for "archive" remote types. -func SyncArchive(remote vdmspec.Remote) error { - return nil -} - -func retrieveArchive(remote vdmspec.Remote) (err error) { - return nil -} diff --git a/internal/remotes/file.go b/internal/remotes/file.go index e86862c..36b6d20 100644 --- a/internal/remotes/file.go +++ b/internal/remotes/file.go @@ -12,8 +12,18 @@ import ( "github.com/opensourcecorp/vdm/internal/vdmspec" ) -// SyncFile is the root of the sync operations for "file" remote types. -func SyncFile(remote vdmspec.Remote) error { +// File defines the file remote type +type File struct { + vdmspec.Remote +} + +// Cache provides the [vdmspec.Remoter.Cache] operations for "file" remote types. +func (remote File) Cache() error { + return errors.New("not implemented") +} + +// Sync provides the [vdmspec.Remoter.Sync] operations for "file" remote types. +func (remote File) Sync() error { fileExists, err := checkFileExists(remote) if err != nil { return fmt.Errorf("checking if file exists locally: %w", err) @@ -32,7 +42,17 @@ func SyncFile(remote vdmspec.Remote) error { return nil } -func checkFileExists(remote vdmspec.Remote) (bool, error) { +// GetSource returns the Source field. +func (remote File) GetSource() string { + return remote.Source +} + +// GetVersion returns the Version field. +func (remote File) GetVersion() string { + return remote.Version +} + +func checkFileExists(remote File) (bool, error) { fullPath, err := filepath.Abs(remote.Destination) if err != nil { return false, fmt.Errorf("determining abspath for file '%s': %w", remote.Destination, err) @@ -48,14 +68,14 @@ func checkFileExists(remote vdmspec.Remote) (bool, error) { return true, nil } -func retrieveFile(remote vdmspec.Remote) (err error) { +func retrieveFile(remote File) (err error) { resp, err := http.Get(remote.Source) if err != nil { return fmt.Errorf("retrieving remote file '%s': %w", remote.Source, err) } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - err = errors.Join(fmt.Errorf("closing response body after remote file '%s' retrieval: %w", remote.Source, err)) + err = errors.Join(err, fmt.Errorf("closing response body after remote file '%s' retrieval: %w", remote.Source, err)) } }() @@ -77,7 +97,7 @@ func retrieveFile(remote vdmspec.Remote) (err error) { } defer func() { if closeErr := outFile.Close(); closeErr != nil { - err = errors.Join(fmt.Errorf("closing local file '%s' after remote file '%s' retrieval: %w", remote.Destination, remote.Source, err)) + err = errors.Join(err, fmt.Errorf("closing local file '%s' after remote file '%s' retrieval: %w", remote.Destination, remote.Source, err)) } }() @@ -105,3 +125,5 @@ func ensureParentDirs(path string) error { return nil } + +var _ vdmspec.Remoter = File{} diff --git a/internal/remotes/file_test.go b/internal/remotes/file_test.go index a558bee..7169710 100644 --- a/internal/remotes/file_test.go +++ b/internal/remotes/file_test.go @@ -1 +1,7 @@ package remotes + +import "testing" + +func TestSync(t *testing.T) { + t.Fatal("not implemented") +} diff --git a/internal/remotes/git.go b/internal/remotes/git.go index f445c77..b09f0b8 100644 --- a/internal/remotes/git.go +++ b/internal/remotes/git.go @@ -11,20 +11,28 @@ import ( "github.com/opensourcecorp/vdm/internal/vdmspec" ) -// SyncGit is the root of the sync operations for "git" remote types. -func SyncGit(remote vdmspec.Remote) error { +// Git defines the git remote type +type Git struct { + vdmspec.Remote +} + +// Cache provides the [vdmspec.Remoter.Cache] operations for "git" remote types. +func (remote Git) Cache() error { + return errors.New("not implemented") +} + +// Sync provides the [vdmspec.Remoter.Sync] operations for "git" remote types. +func (remote Git) Sync() error { err := gitClone(remote) if err != nil { - return fmt.Errorf("cloing remote: %w", err) + return fmt.Errorf("cloning git repository: %w", err) } - if remote.Version != "latest" { - message.Infof("%s: Setting specified version...", remote.OpMsg()) - checkoutCmd := exec.Command("git", "-C", remote.Destination, "checkout", remote.Version) - checkoutOutput, err := checkoutCmd.CombinedOutput() - if err != nil { - return fmt.Errorf("error checking out specified revision: exec error '%w', with output: %s", err, string(checkoutOutput)) - } + message.Infof("%s: Setting specified version...", remote.OpMsg()) + checkoutCmd := exec.Command("git", "-C", remote.Destination, "checkout", remote.Version) + checkoutOutput, err := checkoutCmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error checking out specified revision: exec error '%w', with output: %s", err, string(checkoutOutput)) } message.Debugf("removing .git dir for local path '%s'", remote.Destination) @@ -37,6 +45,16 @@ func SyncGit(remote vdmspec.Remote) error { return nil } +// GetSource returns the Source field. +func (remote Git) GetSource() string { + return remote.Source +} + +// GetVersion returns the Version field. +func (remote Git) GetVersion() string { + return remote.Version +} + func checkGitAvailable() error { cmd := exec.Command("git", "--version") sysOutput, err := cmd.CombinedOutput() @@ -48,23 +66,13 @@ func checkGitAvailable() error { return nil } -func gitClone(remote vdmspec.Remote) error { +func gitClone(remote Git) error { err := checkGitAvailable() if err != nil { return fmt.Errorf("remote '%s' is a git type, but git may not installed/available on PATH: %w", remote.Source, err) } - // If users want "latest", then we can just do a depth-one clone and - // skip the checkout operation. But if they want non-latest, we need the - // full history to be able to find a specified revision - var cloneCmdArgs []string - if remote.Version == "latest" { - message.Debugf("%s: version specified as 'latest', so making shallow clone and skipping separate checkout operation", remote.OpMsg()) - cloneCmdArgs = []string{"clone", "--depth=1", remote.Source, remote.Destination} - } else { - message.Debugf("%s: version specified as NOT latest, so making regular clone and will make separate checkout operation", remote.OpMsg()) - cloneCmdArgs = []string{"clone", remote.Source, remote.Destination} - } + cloneCmdArgs := []string{"clone", remote.Source, remote.Destination} message.Infof("%s: Retrieving...", remote.OpMsg()) cloneCmd := exec.Command("git", cloneCmdArgs...) @@ -75,3 +83,5 @@ func gitClone(remote vdmspec.Remote) error { return nil } + +var _ vdmspec.Remoter = Git{} diff --git a/internal/remotes/git_test.go b/internal/remotes/git_test.go index 5801617..fd66d0e 100644 --- a/internal/remotes/git_test.go +++ b/internal/remotes/git_test.go @@ -9,23 +9,25 @@ import ( "github.com/stretchr/testify/require" ) -func getTestGitSpec() vdmspec.Remote { +func getTestGitRemote() Git { specLocalPath := "./deps/go-common" - return vdmspec.Remote{ - Type: "git", - Source: "https://github.com/opensourcecorp/go-common", - Version: "v0.2.0", - Destination: specLocalPath, + return Git{ + Remote: vdmspec.Remote{ + Type: "git", + Source: "https://github.com/opensourcecorp/go-common", + Version: "v0.2.0", + Destination: specLocalPath, + }, } } func TestSyncGit(t *testing.T) { - spec := getTestGitSpec() - err := SyncGit(spec) + remote := getTestGitRemote() + err := remote.Sync() require.NoError(t, err) defer t.Cleanup(func() { - if cleanupErr := os.RemoveAll(spec.Destination); cleanupErr != nil { + if cleanupErr := os.RemoveAll(remote.Destination); cleanupErr != nil { t.Fatalf("removing specLocalPath: %v", cleanupErr) } }) @@ -53,11 +55,11 @@ func TestCheckGitAvailable(t *testing.T) { } func TestGitClone(t *testing.T) { - spec := getTestGitSpec() - cloneErr := gitClone(spec) + remote := getTestGitRemote() + cloneErr := gitClone(remote) defer t.Cleanup(func() { - if cleanupErr := os.RemoveAll(spec.Destination); cleanupErr != nil { + if cleanupErr := os.RemoveAll(remote.Destination); cleanupErr != nil { t.Fatalf("removing specLocalPath: %v", cleanupErr) } }) diff --git a/internal/remotes/local.go b/internal/remotes/local.go index a2b08ea..a558bee 100644 --- a/internal/remotes/local.go +++ b/internal/remotes/local.go @@ -1,14 +1 @@ package remotes - -import ( - "github.com/opensourcecorp/vdm/internal/vdmspec" -) - -// SyncLocal is the root of the sync operations for "local" remote types. -func SyncLocal(remote vdmspec.Remote) error { - return nil -} - -func retrieveLocal(remote vdmspec.Remote) (err error) { - return nil -} diff --git a/internal/vdmspec/spec.go b/internal/vdmspec/spec.go index 82632ee..6aeaa2d 100644 --- a/internal/vdmspec/spec.go +++ b/internal/vdmspec/spec.go @@ -16,15 +16,24 @@ type Spec struct { Remotes []Remote `json:"remotes" yaml:"remotes"` } +// Remoter defines behavior that different remote types must exhibit type Remoter interface { - Sync() error + // Cache should retrieve the remote, and cache it as an archive in + // VDM_HOME's cache Cache() error + // Sync should unpack the archive from the cache in VDM_HOME to the + // specified destination + Sync() error + // GetRemote returns the [Remote.Source] value + GetSource() string + // GetRemote returns the [Remote.Version] value + GetVersion() string } // Remote defines the structure of each remote configuration in the vdm // specfile. type Remote struct { - // Type is the type of Source, e.g. git, archive, etc. + // Type is the type of Source, e.g. git, archive, file, etc. Type string `json:"type,omitempty" yaml:"type,omitempty"` // Source is the fully-qualifed location from which the Remote is retrieved, // e.g. "https://github.com/some-org/some-repo" @@ -33,12 +42,12 @@ type Remote struct { // for tracking purposes. Version can be anything supported by the Type // field -- for example, for the "git" Type, this can be a tag, a branch // name, or a commit hash. It can also be the word "latest". - Version string `json:"version,omitempty" yaml:"version,omitempty"` + Version string `json:"version" yaml:"version"` // Destination is the relative or absolute path on disk that Source will be // placed at Destination string `json:"destination" yaml:"destination"` // TryLocalSource helps define behavior driven by the `try-local-sources` - // CLI flag, which allows checking for a local version of a Remote Source, + // CLI flag, which allows checking for a local version of a [Remote.Source], // and falling back to the other Source field if the local path does not // exist. This is especially useful for when you might be developing one of // your Remotes in a nearby directory, and want to copy over that version of @@ -122,13 +131,13 @@ func (r Remote) GetVDMMeta() (Remote, error) { return vdmMeta, nil } -// GetSpecFromFile reads the specfile from disk (the path of which is determined -// by the user-supplied flag value), and returns it for further processing of -// remotes. +// GetSpecFromFile reads the specfile from disk (the path of which may be +// determined by the user-supplied flag value), and returns it for further +// processing of remotes. func GetSpecFromFile(specFilePath string) (Spec, error) { specFile, err := os.ReadFile(specFilePath) if err != nil { - message.Debugf("error reading specfile from disk: %w", err) + message.Debugf("error reading specfile from disk: %v", err) return Spec{}, fmt.Errorf( strings.Join([]string{ "there was a problem reading your vdm file from '%s' -- does it not exist?", @@ -153,11 +162,8 @@ func GetSpecFromFile(specFilePath string) (Spec, error) { return spec, nil } -// OpMsg constructs a loggable message outlining the specific operation being -// performed at the moment +// OpMsg constructs a loggable message outlining the specific remote details +// being performed at the moment func (r Remote) OpMsg() string { - if r.Version != "" { - return fmt.Sprintf("%s@%s --> %s", r.Source, r.Version, r.Destination) - } - return fmt.Sprintf("%s --> %s", r.Source, r.Destination) + return fmt.Sprintf("%s@%s --> %s", r.Source, r.Version, r.Destination) } diff --git a/internal/vdmspec/validate.go b/internal/vdmspec/validate.go index 2061ed8..d606981 100644 --- a/internal/vdmspec/validate.go +++ b/internal/vdmspec/validate.go @@ -19,7 +19,7 @@ func (spec Spec) Validate() error { if len(remote.Source) == 0 { allErrors = append(allErrors, errors.New("all 'remote' fields must be non-zero length")) } - protocolRegex := regexp.MustCompile(`(http(s?)://|git://|git@)`) + protocolRegex := regexp.MustCompile(`(http(s?)://|git://|git@|ftp(s?))`) if !protocolRegex.MatchString(remote.Source) { allErrors = append( allErrors, @@ -29,17 +29,17 @@ func (spec Spec) Validate() error { // Version field message.Debugf("Index #%d: validating field 'Version' for %+v", remoteIndex, remote) - if remote.Type == GitType && len(remote.Version) == 0 { - allErrors = append(allErrors, errors.New("all 'version' fields for the 'git' remote type must be non-zero length. If you don't care about the version (even though you probably should), then use 'latest'")) + if remote.Type == GitType && remote.Version == "" { + allErrors = append(allErrors, errors.New("all 'version' fields for the 'git' remote type must be non-zero length")) } - if remote.Type == FileType && len(remote.Version) > 0 { + if remote.Type == FileType && remote.Version != "" { message.Warnf("NOTE: Remote #%d '%s' specified as type '%s', which does not take explicit version info (you provided '%s'); ignoring version field", remoteIndex, remote.Source, remote.Type, remote.Version) } // LocalPath field - message.Debugf("Index #%d: validating field 'LocalPath' for %+v", remoteIndex, remote) + message.Debugf("Index #%d: validating field 'Destination' for %+v", remoteIndex, remote) if len(remote.Destination) == 0 { - allErrors = append(allErrors, errors.New("all 'local_path' fields must be non-zero length")) + allErrors = append(allErrors, errors.New("all 'destination' fields must be non-zero length")) } // Type field From c51a0280eb436fcf3c795cf06485c6076e732226 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Mon, 12 Aug 2024 00:20:29 -0500 Subject: [PATCH 07/17] WIP --- README.md | 4 +-- cmd/sync.go | 8 ++--- internal/archive/sumdb/sumdb_test.go | 2 +- internal/remotes/file.go | 2 +- internal/remotes/git.go | 52 ++++++++++++++++++++-------- internal/remotes/git_test.go | 2 +- internal/vdmspec/doc.go | 4 +-- internal/vdmspec/spec.go | 49 +++++++++++++------------- internal/vdmspec/spec_test.go | 2 +- internal/vdmspec/validate.go | 6 ++-- internal/vdmspec/validate_test.go | 12 +++---- testdata/vdm.yaml | 12 ++++--- 12 files changed, 93 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 37ae700..6c26f2a 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,9 @@ revisions & where you want them to live on your filesystem: ```yaml remotes: - - type: "git" # the default, and so can be omitted if desired + - type: "git" source: "https://github.com/opensourcecorp/go-common" # can specify as 'git@...' to use SSH instead - version: "v0.2.0" # tag example; can also be a branch, commit hash, or the word 'latest' + version: "v0.2.0" # tag example; can also be a branch, or a commit hash destination: "./deps/go-common" - type: "file" # the 'file' type assumes the version is in the remote field itself somehow, so 'version' can be omitted diff --git a/cmd/sync.go b/cmd/sync.go index cae3a93..2ae0fa5 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -70,12 +70,12 @@ func sync() error { return fmt.Errorf("getting vdm metadata file for sync: %w", err) } - if vdmMeta == (vdmspec.Remote{}) { + if vdmMeta == (vdmspec.RemoteTemplate{}) { message.Infof("%s: %s not found at local path, will be created", remote.OpMsg(), vdmspec.MetaFileName) } else { if vdmMeta.Version != remote.Version && vdmMeta.Source != remote.Source { message.Infof("%s: Will change '%s' from current local version spec '%s' to '%s'...", remote.OpMsg(), remote.Source, vdmMeta.Version, remote.Version) - panic("not implemented") + panic("jk not implemented") } message.Infof("%s: version unchanged in spec file, skipping", remote.OpMsg()) continue @@ -84,9 +84,9 @@ func sync() error { var determinedRemote vdmspec.Remoter switch remote.Type { case vdmspec.GitType, "": - determinedRemote = remotes.Git{Remote: remote} + determinedRemote = remotes.Git{RemoteTemplate: remote} case vdmspec.FileType: - determinedRemote = remotes.File{Remote: remote} + determinedRemote = remotes.File{RemoteTemplate: remote} default: return fmt.Errorf("unrecognized remote type '%s'", remote.Type) } diff --git a/internal/archive/sumdb/sumdb_test.go b/internal/archive/sumdb/sumdb_test.go index e874636..6544a97 100644 --- a/internal/archive/sumdb/sumdb_test.go +++ b/internal/archive/sumdb/sumdb_test.go @@ -66,7 +66,7 @@ func TestCacheRemote(t *testing.T) { require.NoError(t, err) remote := remotes.Git{ - Remote: vdmspec.Remote{ + RemoteTemplate: vdmspec.RemoteTemplate{ Source: "https://github.com/org/user", Version: "v1.0.0", }, diff --git a/internal/remotes/file.go b/internal/remotes/file.go index 36b6d20..875590e 100644 --- a/internal/remotes/file.go +++ b/internal/remotes/file.go @@ -14,7 +14,7 @@ import ( // File defines the file remote type type File struct { - vdmspec.Remote + vdmspec.RemoteTemplate } // Cache provides the [vdmspec.Remoter.Cache] operations for "file" remote types. diff --git a/internal/remotes/git.go b/internal/remotes/git.go index b09f0b8..02a9787 100644 --- a/internal/remotes/git.go +++ b/internal/remotes/git.go @@ -7,42 +7,64 @@ import ( "os/exec" "path/filepath" + "github.com/opensourcecorp/vdm/cmd/vars" + "github.com/opensourcecorp/vdm/internal/archive/sumdb" "github.com/opensourcecorp/vdm/internal/message" "github.com/opensourcecorp/vdm/internal/vdmspec" ) -// Git defines the git remote type +// Git defines the git remote type, and implements the [vdmspec.Remoter] +// interface. type Git struct { - vdmspec.Remote + vdmspec.RemoteTemplate } // Cache provides the [vdmspec.Remoter.Cache] operations for "git" remote types. -func (remote Git) Cache() error { - return errors.New("not implemented") -} +func (remote Git) Cache() (err error) { + tmpCachePath := filepath.Join(os.TempDir(), "vdm-tmp", filepath.Base(remote.Destination)) + message.Debugf("tmpCachePath: %s", tmpCachePath) -// Sync provides the [vdmspec.Remoter.Sync] operations for "git" remote types. -func (remote Git) Sync() error { - err := gitClone(remote) + if err := os.RemoveAll(tmpCachePath); err != nil { + return fmt.Errorf("trying to clean up possibly-duplicate old cache data at %s: %w", tmpCachePath, err) + } + + err = gitClone(remote, tmpCachePath) if err != nil { return fmt.Errorf("cloning git repository: %w", err) } + defer func() { + if rmErr := os.RemoveAll(tmpCachePath); rmErr != nil { + err = errors.Join(err, fmt.Errorf("removing temporary cache directory for '%s': %w", remote.GetSource(), rmErr)) + } + }() message.Infof("%s: Setting specified version...", remote.OpMsg()) - checkoutCmd := exec.Command("git", "-C", remote.Destination, "checkout", remote.Version) + checkoutCmd := exec.Command("git", "-C", tmpCachePath, "checkout", remote.Version) checkoutOutput, err := checkoutCmd.CombinedOutput() if err != nil { return fmt.Errorf("error checking out specified revision: exec error '%w', with output: %s", err, string(checkoutOutput)) } - message.Debugf("removing .git dir for local path '%s'", remote.Destination) - dotGitPath := filepath.Join(remote.Destination, ".git") + message.Debugf("removing .git dir for local path '%s'", tmpCachePath) + dotGitPath := filepath.Join(tmpCachePath, ".git") err = os.RemoveAll(dotGitPath) if err != nil { return fmt.Errorf("removing directory %s: %w", dotGitPath, err) } - return nil + // TODO-NOW: we need this to create two paths: one for the actual + // gzipped-tar cache, and one for the sumdb file + cachePath, err := os.Create(filepath.Join(vars.GetVDMCacheDir())) + sumdb.CacheRemote(remote) + + // return nil + + return errors.New("not implemented") +} + +// Sync provides the [vdmspec.Remoter.Sync] operations for "git" remote types. +func (remote Git) Sync() error { + return errors.New("not implemented") } // GetSource returns the Source field. @@ -66,17 +88,19 @@ func checkGitAvailable() error { return nil } -func gitClone(remote Git) error { +func gitClone(remote Git, dest string) error { err := checkGitAvailable() if err != nil { return fmt.Errorf("remote '%s' is a git type, but git may not installed/available on PATH: %w", remote.Source, err) } - cloneCmdArgs := []string{"clone", remote.Source, remote.Destination} + cloneCmdArgs := []string{"clone", remote.Source, dest} + message.Debugf("git args: %v", cloneCmdArgs) message.Infof("%s: Retrieving...", remote.OpMsg()) cloneCmd := exec.Command("git", cloneCmdArgs...) cloneOutput, err := cloneCmd.CombinedOutput() + message.Debugf("git clone command output: %s", string(cloneOutput)) if err != nil { return fmt.Errorf("cloning remote: exec error '%w', with output: %s", err, string(cloneOutput)) } diff --git a/internal/remotes/git_test.go b/internal/remotes/git_test.go index fd66d0e..cb22223 100644 --- a/internal/remotes/git_test.go +++ b/internal/remotes/git_test.go @@ -12,7 +12,7 @@ import ( func getTestGitRemote() Git { specLocalPath := "./deps/go-common" return Git{ - Remote: vdmspec.Remote{ + RemoteTemplate: vdmspec.RemoteTemplate{ Type: "git", Source: "https://github.com/opensourcecorp/go-common", Version: "v0.2.0", diff --git a/internal/vdmspec/doc.go b/internal/vdmspec/doc.go index 3dd2906..d793ad3 100644 --- a/internal/vdmspec/doc.go +++ b/internal/vdmspec/doc.go @@ -1,3 +1,3 @@ -// Package vdmspec defines the [Spec] and [Remote] struct types, and their -// associated methods. +// Package vdmspec defines the [Spec] and [RemoteTemplate] struct types, and +// their associated methods via the [Remoter] interface. package vdmspec diff --git a/internal/vdmspec/spec.go b/internal/vdmspec/spec.go index 6aeaa2d..1d6d4fb 100644 --- a/internal/vdmspec/spec.go +++ b/internal/vdmspec/spec.go @@ -13,7 +13,7 @@ import ( // Spec defines the overall structure of the vmd specfile. type Spec struct { - Remotes []Remote `json:"remotes" yaml:"remotes"` + Remotes []RemoteTemplate `json:"remotes" yaml:"remotes"` } // Remoter defines behavior that different remote types must exhibit @@ -24,35 +24,36 @@ type Remoter interface { // Sync should unpack the archive from the cache in VDM_HOME to the // specified destination Sync() error - // GetRemote returns the [Remote.Source] value + // GetRemote returns the [RemoteTemplate.Source] value GetSource() string - // GetRemote returns the [Remote.Version] value + // GetRemote returns the [RemoteTemplate.Version] value GetVersion() string } -// Remote defines the structure of each remote configuration in the vdm -// specfile. -type Remote struct { +// RemoteTemplate defines the template structure for each potential remote +// configuration in the vdm specfile. This struct can be embedded into other +// remote type structs to provide the common fields between them. +type RemoteTemplate struct { // Type is the type of Source, e.g. git, archive, file, etc. - Type string `json:"type,omitempty" yaml:"type,omitempty"` + Type string `json:"type" yaml:"type"` // Source is the fully-qualifed location from which the Remote is retrieved, // e.g. "https://github.com/some-org/some-repo" Source string `json:"source" yaml:"source"` // Version states the version requested from Source, and is then later used // for tracking purposes. Version can be anything supported by the Type // field -- for example, for the "git" Type, this can be a tag, a branch - // name, or a commit hash. It can also be the word "latest". + // name, or a commit hash. Version string `json:"version" yaml:"version"` // Destination is the relative or absolute path on disk that Source will be // placed at Destination string `json:"destination" yaml:"destination"` // TryLocalSource helps define behavior driven by the `try-local-sources` - // CLI flag, which allows checking for a local version of a [Remote.Source], - // and falling back to the other Source field if the local path does not - // exist. This is especially useful for when you might be developing one of - // your Remotes in a nearby directory, and want to copy over that version of - // the Remote and not keep pushing-and-pulling to a Git upstream just to - // test the changes. + // CLI flag, which allows checking for a local version of a + // [RemoteTemplate.Source], and falling back to the other Source field if + // the local path does not exist. This is especially useful for when you + // might be developing one of your Remotes in a nearby directory, and want + // to copy over that version of the Remote and not keep pushing-and-pulling + // to a Git upstream just to test the changes. TryLocalSource string `json:"try_local_source" yaml:"try_local_source"` } @@ -69,7 +70,7 @@ const ( // MakeMetaFilePath constructs the metafile path that vdm will use to track a // remote's state on disk. -func (r Remote) MakeMetaFilePath() string { +func (r RemoteTemplate) MakeMetaFilePath() string { metaFilePath := filepath.Join(r.Destination, MetaFileName) // TODO: this is brittle, but it's the best I can think of right now if r.Type == FileType { @@ -83,8 +84,8 @@ func (r Remote) MakeMetaFilePath() string { } // WriteVDMMeta writes the metafile contents to disk, the path of which is -// determined by [Remote.MakeMetaFilePath]. -func (r Remote) WriteVDMMeta() error { +// determined by [RemoteTemplate.MakeMetaFilePath]. +func (r RemoteTemplate) WriteVDMMeta() error { metaFilePath := r.MakeMetaFilePath() vdmMetaContent, err := yaml.Marshal(r) if err != nil { @@ -104,27 +105,27 @@ func (r Remote) WriteVDMMeta() error { // GetVDMMeta reads the metafile from disk, and returns it for further // processing. -func (r Remote) GetVDMMeta() (Remote, error) { +func (r RemoteTemplate) GetVDMMeta() (RemoteTemplate, error) { metaFilePath := r.MakeMetaFilePath() _, err := os.Stat(metaFilePath) if errors.Is(err, os.ErrNotExist) { - return Remote{}, nil // this is ok, because it might literally not exist yet + return RemoteTemplate{}, nil // this is ok, because it might literally not exist yet } else if err != nil { - return Remote{}, fmt.Errorf("couldn't check if %s exists at '%s': %w", MetaFileName, metaFilePath, err) + return RemoteTemplate{}, fmt.Errorf("couldn't check if %s exists at '%s': %w", MetaFileName, metaFilePath, err) } vdmMetaFile, err := os.ReadFile(metaFilePath) if err != nil { message.Debugf("error reading VMDMMETA from disk: %w", err) - return Remote{}, fmt.Errorf("there was a problem reading the %s file from '%s': %w", MetaFileName, metaFilePath, err) + return RemoteTemplate{}, fmt.Errorf("there was a problem reading the %s file from '%s': %w", MetaFileName, metaFilePath, err) } message.Debugf("%s contents read:\n%s", MetaFileName, string(vdmMetaFile)) - var vdmMeta Remote + var vdmMeta RemoteTemplate err = yaml.Unmarshal(vdmMetaFile, &vdmMeta) if err != nil { message.Debugf("error during %s unmarshal: w", MetaFileName, err) - return Remote{}, fmt.Errorf("there was a problem reading the contents of the %s file at '%s': %w", MetaFileName, metaFilePath, err) + return RemoteTemplate{}, fmt.Errorf("there was a problem reading the contents of the %s file at '%s': %w", MetaFileName, metaFilePath, err) } message.Debugf("file %s unmarshalled: %+v", MetaFileName, vdmMeta) @@ -164,6 +165,6 @@ func GetSpecFromFile(specFilePath string) (Spec, error) { // OpMsg constructs a loggable message outlining the specific remote details // being performed at the moment -func (r Remote) OpMsg() string { +func (r RemoteTemplate) OpMsg() string { return fmt.Sprintf("%s@%s --> %s", r.Source, r.Version, r.Destination) } diff --git a/internal/vdmspec/spec_test.go b/internal/vdmspec/spec_test.go index 82999c5..5c25ad6 100644 --- a/internal/vdmspec/spec_test.go +++ b/internal/vdmspec/spec_test.go @@ -15,7 +15,7 @@ const testVDMRoot = "../../testdata" var ( testVDMMetaFilePath = filepath.Join(testVDMRoot, MetaFileName) - testRemote = Remote{ + testRemote = RemoteTemplate{ Source: "https://some-remote", Version: "v1.0.0", Destination: testVDMRoot, diff --git a/internal/vdmspec/validate.go b/internal/vdmspec/validate.go index d606981..6b84f3a 100644 --- a/internal/vdmspec/validate.go +++ b/internal/vdmspec/validate.go @@ -44,10 +44,12 @@ func (spec Spec) Validate() error { // Type field message.Debugf("Index #%d: validating field 'Type' for %+v", remoteIndex, remote) + if remote.Type == "" { + allErrors = append(allErrors, errors.New("all remotes must specify a 'type' field")) + } typeMap := map[string]int{ GitType: 1, - "": 2, // also git - FileType: 3, + FileType: 2, } if _, ok := typeMap[remote.Type]; !ok { allErrors = append(allErrors, fmt.Errorf("unrecognized remote type '%s'", remote.Type)) diff --git a/internal/vdmspec/validate_test.go b/internal/vdmspec/validate_test.go index 1c985fb..17beef0 100644 --- a/internal/vdmspec/validate_test.go +++ b/internal/vdmspec/validate_test.go @@ -10,7 +10,7 @@ import ( func TestValidate(t *testing.T) { t.Run("passes", func(t *testing.T) { spec := Spec{ - Remotes: []Remote{{ + Remotes: []RemoteTemplate{{ Source: "https://some-remote", Version: "v1.0.0", Destination: "./deps/some-remote", @@ -22,7 +22,7 @@ func TestValidate(t *testing.T) { t.Run("fails on zero-length remote", func(t *testing.T) { spec := Spec{ - Remotes: []Remote{{ + Remotes: []RemoteTemplate{{ Source: "", Version: "v1.0.0", Destination: "./deps/some-remote", @@ -34,7 +34,7 @@ func TestValidate(t *testing.T) { t.Run("fails on remote without valid protocol", func(t *testing.T) { spec := Spec{ - Remotes: []Remote{{ + Remotes: []RemoteTemplate{{ Source: "some-remote", Version: "v1.0.0", Destination: "./deps/some-remote", @@ -46,7 +46,7 @@ func TestValidate(t *testing.T) { t.Run("fails on zero-length version for git remote type", func(t *testing.T) { spec := Spec{ - Remotes: []Remote{{ + Remotes: []RemoteTemplate{{ Source: "https://some-remote", Version: "", Destination: "./deps/some-remote", @@ -59,7 +59,7 @@ func TestValidate(t *testing.T) { t.Run("fails on unrecognized remote type", func(t *testing.T) { spec := Spec{ - Remotes: []Remote{{ + Remotes: []RemoteTemplate{{ Source: "https://some-remote", Version: "", Destination: "./deps/some-remote", @@ -72,7 +72,7 @@ func TestValidate(t *testing.T) { t.Run("fails on zero-length local path", func(t *testing.T) { spec := Spec{ - Remotes: []Remote{{ + Remotes: []RemoteTemplate{{ Source: "https://some-remote", Version: "v1.0.0", Destination: "", diff --git a/testdata/vdm.yaml b/testdata/vdm.yaml index 342719f..8863bbf 100644 --- a/testdata/vdm.yaml +++ b/testdata/vdm.yaml @@ -1,14 +1,18 @@ remotes: - - source: "https://github.com/opensourcecorp/go-common" + - type: "git" + source: "https://github.com/opensourcecorp/go-common" version: "v0.2.0" destination: "./deps/go-common-tag" - - source: "https://github.com/opensourcecorp/go-common" + - type: "git" + source: "https://github.com/opensourcecorp/go-common" version: "latest" destination: "./deps/go-common-latest" - - source: "https://github.com/opensourcecorp/go-common" + - type: "git" + source: "https://github.com/opensourcecorp/go-common" version: "main" destination: "./deps/go-common-branch" - - source: "https://github.com/opensourcecorp/go-common" + - type: "git" + source: "https://github.com/opensourcecorp/go-common" version: "2e6657f5ac013296167c4dd92fbb46f0e3dbdc5f" destination: "./deps/go-common-hash" - type: "file" From e35553b5c5df6c58b0e2b4de6e5e126169489d71 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Tue, 13 Aug 2024 00:10:29 -0500 Subject: [PATCH 08/17] WIP --- .gitignore | 2 + .../{sumdb/sumdb.go => cache/cache.go} | 31 ++-------- .../archive/cache/cachetest/cache_test.go | 48 +++++++++++++++ internal/archive/cache/cachetest/doc.go | 4 ++ internal/archive/cache/doc.go | 3 + internal/archive/cache/sumdb.go | 41 +++++++++++++ .../archive/{sumdb => cache}/sumdb_test.go | 58 +++++++++---------- internal/archive/sumdb/doc.go | 3 - internal/remotes/git.go | 45 +++++++++----- internal/remotes/git_test.go | 22 ++++--- internal/vdmspec/spec.go | 22 +++---- internal/vdmspec/validate.go | 12 ++-- internal/vdmspec/validate_test.go | 1 + 13 files changed, 193 insertions(+), 99 deletions(-) rename internal/archive/{sumdb/sumdb.go => cache/cache.go} (51%) create mode 100644 internal/archive/cache/cachetest/cache_test.go create mode 100644 internal/archive/cache/cachetest/doc.go create mode 100644 internal/archive/cache/doc.go create mode 100644 internal/archive/cache/sumdb.go rename internal/archive/{sumdb => cache}/sumdb_test.go (54%) delete mode 100644 internal/archive/sumdb/doc.go diff --git a/.gitignore b/.gitignore index 3d4ec61..87a4bcf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ *.log *.out *cache* +!internal/archive/cache +!internal/archive/cache/** build/ deps/ diff --git a/internal/archive/sumdb/sumdb.go b/internal/archive/cache/cache.go similarity index 51% rename from internal/archive/sumdb/sumdb.go rename to internal/archive/cache/cache.go index 8affaae..41ae798 100644 --- a/internal/archive/sumdb/sumdb.go +++ b/internal/archive/cache/cache.go @@ -1,8 +1,6 @@ -package sumdb +package cache import ( - "crypto/sha256" - "encoding/base64" "errors" "fmt" "io" @@ -13,26 +11,15 @@ import ( "github.com/opensourcecorp/vdm/internal/vdmspec" ) -// CalculateSHASum takes an arbitrary [io.Reader] (such as an open file handle) -// and calculates the SHA256 checksum for it. -func CalculateSHASum(reader io.Reader) (string, error) { - hasher := sha256.New() - if _, err := io.Copy(hasher, reader); err != nil { - return "", fmt.Errorf("writing reader to hasher: %w", err) - } - sum := fmt.Sprintf("%x", hasher.Sum(nil)) - return sum, nil -} - -// CacheRemote uses the provided [vdmspec.Remoter] information along with a file +// AddRemote uses the provided [vdmspec.Remoter] information along with a file // handle for a target archive to actually write the archive data. -func CacheRemote(remote vdmspec.Remoter, archiveFileHandle io.Reader) (err error) { +func AddRemote(remote vdmspec.Remoter, archiveFileHandle io.Reader) (err error) { err = os.MkdirAll(vars.GetVDMCacheDir(), 0755) if err != nil { return fmt.Errorf("creating vdm cache directory %s: %w", vars.GetVDMCacheDir(), err) } - cacheFileName := StringAsBase64(remote.GetSource()) + cacheFileName := StringToBase64(remote.GetSource()) f, err := os.Create(filepath.Join(vars.GetVDMCacheDir(), cacheFileName)) if err != nil { return fmt.Errorf("creating file %s: %w", cacheFileName, err) @@ -57,13 +44,3 @@ func CacheRemote(remote vdmspec.Remoter, archiveFileHandle io.Reader) (err error return err } - -// StringAsBase64 does what it says on the tin. -func StringAsBase64(s string) string { - // We use base64-encoded names for caches, because remote sources have - // slashes and that can break FS pathing - data := []byte(s) - encodedBytes := make([]byte, base64.StdEncoding.EncodedLen(len(data))) - base64.StdEncoding.Encode(encodedBytes, data) - return string(encodedBytes) -} diff --git a/internal/archive/cache/cachetest/cache_test.go b/internal/archive/cache/cachetest/cache_test.go new file mode 100644 index 0000000..bc7debd --- /dev/null +++ b/internal/archive/cache/cachetest/cache_test.go @@ -0,0 +1,48 @@ +package cachetest + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/opensourcecorp/vdm/cmd/vars" + "github.com/opensourcecorp/vdm/internal/archive/cache" + "github.com/opensourcecorp/vdm/internal/remotes" + "github.com/opensourcecorp/vdm/internal/vdmspec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCacheRemote(t *testing.T) { + vdmHomePath := filepath.Join(os.TempDir(), "vdmhome") + t.Setenv(vars.VDMHomeEnvVarName, vdmHomePath) + t.Cleanup(func() { + err := os.RemoveAll(vdmHomePath) + require.NoError(t, err) + }) + + f, err := os.Open("../../../../testdata/sumdb/sha256test.txt") + require.NoError(t, err) + t.Cleanup(func() { + err := f.Close() + require.NoError(t, err) + }) + + remote := remotes.Git{ + RemoteTemplate: vdmspec.RemoteTemplate{ + Source: "https://github.com/org/user", + Version: "v1.0.0", + }, + } + cachedPath := filepath.Join(os.Getenv(vars.VDMHomeEnvVarName), "cache", cache.StringToBase64(remote.Source)) + + err = cache.AddRemote(remote, f) + assert.NoError(t, err) + + want := fmt.Sprintf("%s %s %s", remote.Source, remote.Version, "25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b") + got, err := os.ReadFile(cachedPath) + require.NoError(t, err) + + assert.Equal(t, want, string(got)) +} diff --git a/internal/archive/cache/cachetest/doc.go b/internal/archive/cache/cachetest/doc.go new file mode 100644 index 0000000..b1c4acf --- /dev/null +++ b/internal/archive/cache/cachetest/doc.go @@ -0,0 +1,4 @@ +// Package cachetest exists because Go won't let me have a test for +// [cache.AddRemote] in the cache package because it imports [remotes.Git], +// which causes an import cycle. So, the tests for that are here instead. +package cachetest diff --git a/internal/archive/cache/doc.go b/internal/archive/cache/doc.go new file mode 100644 index 0000000..1dc7929 --- /dev/null +++ b/internal/archive/cache/doc.go @@ -0,0 +1,3 @@ +// Package cache provides tooling for handling the caching of data from Remotes, +// like writing the cache archives, calculating thier SHA sums, etc. +package cache diff --git a/internal/archive/cache/sumdb.go b/internal/archive/cache/sumdb.go new file mode 100644 index 0000000..f407a07 --- /dev/null +++ b/internal/archive/cache/sumdb.go @@ -0,0 +1,41 @@ +package cache + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "io" + "strings" +) + +// CalculateSHASum takes an arbitrary [io.Reader] (such as an open file handle) +// and calculates the SHA256 checksum for it. +func CalculateSHASum(reader io.Reader) (string, error) { + hasher := sha256.New() + if _, err := io.Copy(hasher, reader); err != nil { + return "", fmt.Errorf("writing reader to hasher: %w", err) + } + sum := fmt.Sprintf("%x", hasher.Sum(nil)) + return sum, nil +} + +// StringToBase64 does what it says on the tin. However, since this is intended +// for use as an archive file name, it replaces padding characters ('=') with +// '_PAD'. +func StringToBase64(s string) string { + // We use base64-encoded names for cache archives, because remote sources + // have slashes and that can break FS pathing + out := base64.StdEncoding.EncodeToString([]byte(s)) + out = strings.ReplaceAll(out, "=", "_PAD") + return out +} + +// StringFromBase64 does the inverse of [StringToBase64]. +func StringFromBase64(s string) (string, error) { + fixedString := strings.ReplaceAll(s, "_PAD", "=") + out, err := base64.StdEncoding.DecodeString(fixedString) + if err != nil { + return "", fmt.Errorf("decoding base64 string '%s': %w", s, err) + } + return string(out), nil +} diff --git a/internal/archive/sumdb/sumdb_test.go b/internal/archive/cache/sumdb_test.go similarity index 54% rename from internal/archive/sumdb/sumdb_test.go rename to internal/archive/cache/sumdb_test.go index 6544a97..682f48e 100644 --- a/internal/archive/sumdb/sumdb_test.go +++ b/internal/archive/cache/sumdb_test.go @@ -1,15 +1,11 @@ -package sumdb +package cache import ( - "fmt" "os" "path/filepath" "testing" - "github.com/opensourcecorp/vdm/cmd/vars" "github.com/opensourcecorp/vdm/internal/archive" - "github.com/opensourcecorp/vdm/internal/remotes" - "github.com/opensourcecorp/vdm/internal/vdmspec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -52,33 +48,35 @@ func TestCalculateSHASum(t *testing.T) { } func TestStringAsBase64(t *testing.T) { - src := "https://github.com/org/user" - want := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2Vy" - got := StringAsBase64(src) + t.Run("works on string with no resulting padding", func(t *testing.T) { + src := "https://github.com/org/user" + want := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2Vy" + got := StringToBase64(src) + assert.Equal(t, want, got) + }) - assert.Equal(t, want, got) + t.Run("works on string WITH resulting padding", func(t *testing.T) { + src := "https://github.com/org/user12" + want := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2VyMTI_PAD" + got := StringToBase64(src) + assert.Equal(t, want, got) + }) } -func TestCacheRemote(t *testing.T) { - t.Setenv(vars.VDMHomeEnvVarName, filepath.Join(os.TempDir(), "vdmhome")) - - f, err := os.Open("../../../testdata/sumdb/sha256test.txt") - require.NoError(t, err) - - remote := remotes.Git{ - RemoteTemplate: vdmspec.RemoteTemplate{ - Source: "https://github.com/org/user", - Version: "v1.0.0", - }, - } - cachedPath := filepath.Join(os.Getenv(vars.VDMHomeEnvVarName), "cache", StringAsBase64(remote.Source)) - - err = CacheRemote(remote, f) - assert.NoError(t, err) - - want := fmt.Sprintf("%s %s %s", remote.Source, remote.Version, "25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b") - got, err := os.ReadFile(cachedPath) - require.NoError(t, err) +func TestStringFromBase64(t *testing.T) { + t.Run("works on string with no resulting padding", func(t *testing.T) { + src := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2Vy" + want := "https://github.com/org/user" + got, err := StringFromBase64(src) + assert.NoError(t, err) + assert.Equal(t, want, got) + }) - assert.Equal(t, want, string(got)) + t.Run("works on string WITH resulting padding", func(t *testing.T) { + src := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2VyMTI_PAD" + want := "https://github.com/org/user12" + got, err := StringFromBase64(src) + assert.NoError(t, err) + assert.Equal(t, want, got) + }) } diff --git a/internal/archive/sumdb/doc.go b/internal/archive/sumdb/doc.go deleted file mode 100644 index a87c1e9..0000000 --- a/internal/archive/sumdb/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package sumdb provides tooling for calculating SHA sums for requested -// dependencies. -package sumdb diff --git a/internal/remotes/git.go b/internal/remotes/git.go index 02a9787..429da8b 100644 --- a/internal/remotes/git.go +++ b/internal/remotes/git.go @@ -8,7 +8,7 @@ import ( "path/filepath" "github.com/opensourcecorp/vdm/cmd/vars" - "github.com/opensourcecorp/vdm/internal/archive/sumdb" + "github.com/opensourcecorp/vdm/internal/archive/cache" "github.com/opensourcecorp/vdm/internal/message" "github.com/opensourcecorp/vdm/internal/vdmspec" ) @@ -21,14 +21,23 @@ type Git struct { // Cache provides the [vdmspec.Remoter.Cache] operations for "git" remote types. func (remote Git) Cache() (err error) { - tmpCachePath := filepath.Join(os.TempDir(), "vdm-tmp", filepath.Base(remote.Destination)) + // tmpCachePath is where the actual retrieval is targeted, which is then + // later archived to the persistent cache + tmpCacheRoot := filepath.Join(os.TempDir(), "vdm-tmp") + tmpCachePath := filepath.Join(tmpCacheRoot, filepath.Base(remote.Source)) message.Debugf("tmpCachePath: %s", tmpCachePath) + defer func() { + if rmErr := os.RemoveAll(tmpCacheRoot); rmErr != nil { + err = errors.Join(err, fmt.Errorf("removing temporary cache path '%s': %w", tmpCacheRoot, rmErr)) + } + }() - if err := os.RemoveAll(tmpCachePath); err != nil { - return fmt.Errorf("trying to clean up possibly-duplicate old cache data at %s: %w", tmpCachePath, err) + if err := os.RemoveAll(tmpCacheRoot); err != nil { + return fmt.Errorf("trying to clean up possibly-duplicate old temp cache data at '%s': %w", tmpCachePath, err) } - err = gitClone(remote, tmpCachePath) + message.Infof("%s: Retrieving...", remote.OpMsg()) + err = gitClone(remote.Source, tmpCachePath) if err != nil { return fmt.Errorf("cloning git repository: %w", err) } @@ -54,12 +63,23 @@ func (remote Git) Cache() (err error) { // TODO-NOW: we need this to create two paths: one for the actual // gzipped-tar cache, and one for the sumdb file - cachePath, err := os.Create(filepath.Join(vars.GetVDMCacheDir())) - sumdb.CacheRemote(remote) + cacheFilePath := filepath.Join(vars.GetVDMCacheDir(), cache.StringToBase64(remote.Source)) + cacheFile, err := os.Create(cacheFilePath) + if err != nil { + return fmt.Errorf("preparing file for cache of git remote %s: %w", remote.Source, err) + } + defer func() { + if closeErr := cacheFile.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing cache file %s: %w", cacheFilePath, closeErr)) + } + }() - // return nil + err = cache.AddRemote(remote, cacheFile) + if err != nil { + return fmt.Errorf("caching git remote %s: %w", remote.Source, err) + } - return errors.New("not implemented") + return err } // Sync provides the [vdmspec.Remoter.Sync] operations for "git" remote types. @@ -88,16 +108,15 @@ func checkGitAvailable() error { return nil } -func gitClone(remote Git, dest string) error { +func gitClone(src string, dest string) error { err := checkGitAvailable() if err != nil { - return fmt.Errorf("remote '%s' is a git type, but git may not installed/available on PATH: %w", remote.Source, err) + return fmt.Errorf("remote '%s' is a git type, but git may not installed/available on PATH: %w", src, err) } - cloneCmdArgs := []string{"clone", remote.Source, dest} + cloneCmdArgs := []string{"clone", src, dest} message.Debugf("git args: %v", cloneCmdArgs) - message.Infof("%s: Retrieving...", remote.OpMsg()) cloneCmd := exec.Command("git", cloneCmdArgs...) cloneOutput, err := cloneCmd.CombinedOutput() message.Debugf("git clone command output: %s", string(cloneOutput)) diff --git a/internal/remotes/git_test.go b/internal/remotes/git_test.go index cb22223..3d1cd31 100644 --- a/internal/remotes/git_test.go +++ b/internal/remotes/git_test.go @@ -2,6 +2,7 @@ package remotes import ( "os" + "path/filepath" "testing" "github.com/opensourcecorp/vdm/internal/vdmspec" @@ -9,9 +10,10 @@ import ( "github.com/stretchr/testify/require" ) -func getTestGitRemote() Git { +func getTestGitRemote(t *testing.T) (Git, string) { + t.Helper() specLocalPath := "./deps/go-common" - return Git{ + remote := Git{ RemoteTemplate: vdmspec.RemoteTemplate{ Type: "git", Source: "https://github.com/opensourcecorp/go-common", @@ -19,15 +21,17 @@ func getTestGitRemote() Git { Destination: specLocalPath, }, } + dest := filepath.Join(os.TempDir(), "vdm-test", filepath.Base(remote.Source)) + return remote, dest } func TestSyncGit(t *testing.T) { - remote := getTestGitRemote() + remote, dest := getTestGitRemote(t) err := remote.Sync() require.NoError(t, err) defer t.Cleanup(func() { - if cleanupErr := os.RemoveAll(remote.Destination); cleanupErr != nil { + if cleanupErr := os.RemoveAll(dest); cleanupErr != nil { t.Fatalf("removing specLocalPath: %v", cleanupErr) } }) @@ -55,11 +59,11 @@ func TestCheckGitAvailable(t *testing.T) { } func TestGitClone(t *testing.T) { - remote := getTestGitRemote() - cloneErr := gitClone(remote) + remote, dest := getTestGitRemote(t) + cloneErr := gitClone(remote.Source, dest) defer t.Cleanup(func() { - if cleanupErr := os.RemoveAll(remote.Destination); cleanupErr != nil { + if cleanupErr := os.RemoveAll(dest); cleanupErr != nil { t.Fatalf("removing specLocalPath: %v", cleanupErr) } }) @@ -69,13 +73,13 @@ func TestGitClone(t *testing.T) { }) t.Run("LocalPath is a directory, not a file", func(t *testing.T) { - outDir, err := os.Stat("./deps/go-common") + outDir, err := os.Stat(dest) require.NoError(t, err) assert.True(t, outDir.IsDir()) }) t.Run("a known file in the remote exists, and is a file", func(t *testing.T) { - sampleFile, err := os.Stat("./deps/go-common/go.mod") + sampleFile, err := os.Stat(filepath.Join(dest, "go.mod")) require.NoError(t, err) assert.False(t, sampleFile.IsDir()) }) diff --git a/internal/vdmspec/spec.go b/internal/vdmspec/spec.go index 1d6d4fb..1f5aea5 100644 --- a/internal/vdmspec/spec.go +++ b/internal/vdmspec/spec.go @@ -11,6 +11,17 @@ import ( "gopkg.in/yaml.v3" ) +const ( + // MetaFileName is the name of the tracking file that vdm uses to record & + // track remote statuses on disk. + MetaFileName string = "VDMMETA" + + // GitType represents the string to match against for git remote types. + GitType string = "git" + // FileType represents the string to match against for file remote types. + FileType string = "file" +) + // Spec defines the overall structure of the vmd specfile. type Spec struct { Remotes []RemoteTemplate `json:"remotes" yaml:"remotes"` @@ -57,17 +68,6 @@ type RemoteTemplate struct { TryLocalSource string `json:"try_local_source" yaml:"try_local_source"` } -const ( - // MetaFileName is the name of the tracking file that vdm uses to record & - // track remote statuses on disk. - MetaFileName string = "VDMMETA" - - // GitType represents the string to match against for git remote types. - GitType string = "git" - // FileType represents the string to match against for file remote types. - FileType string = "file" -) - // MakeMetaFilePath constructs the metafile path that vdm will use to track a // remote's state on disk. func (r RemoteTemplate) MakeMetaFilePath() string { diff --git a/internal/vdmspec/validate.go b/internal/vdmspec/validate.go index 6b84f3a..8b50e7b 100644 --- a/internal/vdmspec/validate.go +++ b/internal/vdmspec/validate.go @@ -13,17 +13,17 @@ import ( func (spec Spec) Validate() error { var allErrors []error + protocolRegex := regexp.MustCompile(`(http(s?)://|git://|git@|ftp(s?))`) for remoteIndex, remote := range spec.Remotes { - // Remote field - message.Debugf("Index #%d: validating field 'Remote' for %+v", remoteIndex, remote) + // Source field + message.Debugf("Index #%d: validating field 'Source' for %+v", remoteIndex, remote) if len(remote.Source) == 0 { - allErrors = append(allErrors, errors.New("all 'remote' fields must be non-zero length")) + allErrors = append(allErrors, errors.New("all 'source' fields must be non-zero length")) } - protocolRegex := regexp.MustCompile(`(http(s?)://|git://|git@|ftp(s?))`) if !protocolRegex.MatchString(remote.Source) { allErrors = append( allErrors, - fmt.Errorf("remote #%d provided as '%s', but all 'remote' fields must begin with a protocol specifier or other valid prefix (e.g. 'https://', '(user|git)@', etc.)", remoteIndex, remote.Source), + fmt.Errorf("remote #%d provided as '%s', but all 'source' fields must begin with a protocol specifier or other valid prefix (e.g. 'https://', '(user|git)@', etc.)", remoteIndex, remote.Source), ) } @@ -36,7 +36,7 @@ func (spec Spec) Validate() error { message.Warnf("NOTE: Remote #%d '%s' specified as type '%s', which does not take explicit version info (you provided '%s'); ignoring version field", remoteIndex, remote.Source, remote.Type, remote.Version) } - // LocalPath field + // Destination field message.Debugf("Index #%d: validating field 'Destination' for %+v", remoteIndex, remote) if len(remote.Destination) == 0 { allErrors = append(allErrors, errors.New("all 'destination' fields must be non-zero length")) diff --git a/internal/vdmspec/validate_test.go b/internal/vdmspec/validate_test.go index 17beef0..384536b 100644 --- a/internal/vdmspec/validate_test.go +++ b/internal/vdmspec/validate_test.go @@ -11,6 +11,7 @@ func TestValidate(t *testing.T) { t.Run("passes", func(t *testing.T) { spec := Spec{ Remotes: []RemoteTemplate{{ + Type: GitType, Source: "https://some-remote", Version: "v1.0.0", Destination: "./deps/some-remote", From 842bc864e5255fad076697d0bfd136be48eb633a Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Tue, 13 Aug 2024 13:18:32 -0500 Subject: [PATCH 09/17] Add a more robust base64 non-alpha converter --- internal/archive/cache/sumdb.go | 32 ++++++++++++++++++++++++++-- internal/archive/cache/sumdb_test.go | 18 ++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/internal/archive/cache/sumdb.go b/internal/archive/cache/sumdb.go index f407a07..918e5a5 100644 --- a/internal/archive/cache/sumdb.go +++ b/internal/archive/cache/sumdb.go @@ -26,16 +26,44 @@ func StringToBase64(s string) string { // We use base64-encoded names for cache archives, because remote sources // have slashes and that can break FS pathing out := base64.StdEncoding.EncodeToString([]byte(s)) - out = strings.ReplaceAll(out, "=", "_PAD") + out = replaceNonAlphaBase64Characters(out) return out } // StringFromBase64 does the inverse of [StringToBase64]. func StringFromBase64(s string) (string, error) { - fixedString := strings.ReplaceAll(s, "_PAD", "=") + fixedString := restoreNonAlphaBase64Characters(s) out, err := base64.StdEncoding.DecodeString(fixedString) if err != nil { return "", fmt.Errorf("decoding base64 string '%s': %w", s, err) } return string(out), nil } + +// base64NonAlphaMap maps non-alphanumeric base-64 characters to their arbitrary +// replacement values +var base64NonAlphaMap = map[string]string{ + "=": "_EQ", + "/": "_SLASH", + "+": "_PLUS", +} + +// replaceNonAlphaBase64Characters takes a valid base64 string and replaces +// non-alphanumeric characters +func replaceNonAlphaBase64Characters(s string) string { + out := s + for nonAlpha, alpha := range base64NonAlphaMap { + out = strings.ReplaceAll(out, nonAlpha, alpha) + } + return out +} + +// restoreNonAlphaBase64Characters takes an invalid base64 string and restores +// non-alphanumeric characters +func restoreNonAlphaBase64Characters(s string) string { + out := s + for nonAlpha, alpha := range base64NonAlphaMap { + out = strings.ReplaceAll(out, alpha, nonAlpha) + } + return out +} diff --git a/internal/archive/cache/sumdb_test.go b/internal/archive/cache/sumdb_test.go index 682f48e..87ae7a2 100644 --- a/internal/archive/cache/sumdb_test.go +++ b/internal/archive/cache/sumdb_test.go @@ -57,7 +57,7 @@ func TestStringAsBase64(t *testing.T) { t.Run("works on string WITH resulting padding", func(t *testing.T) { src := "https://github.com/org/user12" - want := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2VyMTI_PAD" + want := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2VyMTI_EQ" got := StringToBase64(src) assert.Equal(t, want, got) }) @@ -73,10 +73,24 @@ func TestStringFromBase64(t *testing.T) { }) t.Run("works on string WITH resulting padding", func(t *testing.T) { - src := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2VyMTI_PAD" + src := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2VyMTI_EQ" want := "https://github.com/org/user12" got, err := StringFromBase64(src) assert.NoError(t, err) assert.Equal(t, want, got) }) } + +func TestNonAlphaBase64(t *testing.T) { + t.Run("replacer works", func(t *testing.T) { + want := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2Vy_EQ" + got := replaceNonAlphaBase64Characters("aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2Vy=") + assert.Equal(t, want, got) + }) + + t.Run("restorer works", func(t *testing.T) { + want := "aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2Vy=" + got := restoreNonAlphaBase64Characters("aHR0cHM6Ly9naXRodWIuY29tL29yZy91c2Vy_EQ") + assert.Equal(t, want, got) + }) +} From abf5626360428ef095dc43ca3b7e462dfd177d29 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Tue, 13 Aug 2024 23:24:30 -0500 Subject: [PATCH 10/17] Got first pass at sumdb working --- internal/archive/archive.go | 25 +++++------ internal/archive/archive_test.go | 7 +++- internal/archive/cache/cache.go | 42 ++++++++++++------- .../archive/cache/cachetest/cache_test.go | 2 +- internal/archive/cache/sumdb.go | 22 +++++++++- internal/archive/cache/sumdb_test.go | 7 +++- internal/remotes/git.go | 16 +------ 7 files changed, 73 insertions(+), 48 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 7964e00..120ca88 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -17,31 +17,28 @@ import ( // https://www.arthurkoziel.com/writing-tar-gz-files-in-go/ // CreateArchive writes a gzipped tarball based on the provided root directory -// from which to construct the archive, and its target file name. -func CreateArchive(rootDir string, archivePath string) (err error) { +// from which to construct the archive, and its target file name. It returns an +// open file handle to the archive, which should be closed by the caller. +func CreateArchive(rootDir string, archivePath string) (f *os.File, err error) { if !strings.HasSuffix(archivePath, ".tar.gz") { - return errors.New("provided archive path must end in .tar.gz") + return nil, errors.New("provided archive path must end in .tar.gz") } rootDirAbs, err := filepath.Abs(rootDir) if err != nil { - return fmt.Errorf("determining abspath of provided root dir %s: %w", rootDir, err) + return nil, fmt.Errorf("determining abspath of provided root dir %s: %w", rootDir, err) } archivePathAbs, err := filepath.Abs(archivePath) if err != nil { - return fmt.Errorf("determining abspath of provided archive path %s: %w", archivePath, err) + return nil, fmt.Errorf("determining abspath of provided archive path %s: %w", archivePath, err) } buf, err := os.Create(archivePathAbs) if err != nil { - return fmt.Errorf("opening target archive path: %w", err) + return nil, fmt.Errorf("opening target archive path: %w", err) } - defer func() { - if closeErr := buf.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("closing target archive file: %w", closeErr)) - } - }() + // NOTE: file not closed because it's returned, open, to the caller // gzip and tar get their own writers, and note that they are chained -- tar // writes to gzip, which writes to the buffer. So, we only need to write to @@ -62,17 +59,17 @@ func CreateArchive(rootDir string, archivePath string) (err error) { files, err := filetree.GetFilePathsInDirectory(rootDirAbs) if err != nil { - return fmt.Errorf("populating list of files from %s: %w", rootDir, err) + return nil, fmt.Errorf("populating list of files from %s: %w", rootDir, err) } for _, fileName := range files { err := addToArchive(tarWriter, rootDirAbs, fileName) if err != nil { - return fmt.Errorf("adding %s to archive: %w", fileName, err) + return nil, fmt.Errorf("adding %s to archive: %w", fileName, err) } } - return err + return buf, err } func addToArchive(tarWriter *tar.Writer, rootDirAbs string, fileName string) (err error) { diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index bfefd66..92cde0d 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -17,8 +17,13 @@ func TestCreateArchive(t *testing.T) { require.NoError(t, err) }) - err := CreateArchive(archiveRoot, archivePath) + unneededFile, err := CreateArchive(archiveRoot, archivePath) require.NoError(t, err) + // Need to close the returned file because that's the caller's job + t.Cleanup(func() { + err = unneededFile.Close() + require.NoError(t, err) + }) // TODO: add test for inspecting contents once we implement an archive // extractor -- as of now, I'm just checking on the CLI if the expected tree diff --git a/internal/archive/cache/cache.go b/internal/archive/cache/cache.go index 41ae798..dc70e13 100644 --- a/internal/archive/cache/cache.go +++ b/internal/archive/cache/cache.go @@ -3,43 +3,57 @@ package cache import ( "errors" "fmt" - "io" "os" "path/filepath" "github.com/opensourcecorp/vdm/cmd/vars" + "github.com/opensourcecorp/vdm/internal/archive" + "github.com/opensourcecorp/vdm/internal/message" "github.com/opensourcecorp/vdm/internal/vdmspec" ) // AddRemote uses the provided [vdmspec.Remoter] information along with a file -// handle for a target archive to actually write the archive data. -func AddRemote(remote vdmspec.Remoter, archiveFileHandle io.Reader) (err error) { +// handle for a target archive to actually write the archive data. TODO fix this +func AddRemote(remote vdmspec.Remoter, cacheRoot string) (err error) { err = os.MkdirAll(vars.GetVDMCacheDir(), 0755) if err != nil { return fmt.Errorf("creating vdm cache directory %s: %w", vars.GetVDMCacheDir(), err) } - cacheFileName := StringToBase64(remote.GetSource()) - f, err := os.Create(filepath.Join(vars.GetVDMCacheDir(), cacheFileName)) + b64 := StringToBase64(remote.GetSource()) if err != nil { - return fmt.Errorf("creating file %s: %w", cacheFileName, err) + return fmt.Errorf("calculating sum when caching remote %s: %w", remote.GetSource(), err) + } + message.Debugf("remote '%s' base64'd to '%s'", remote.GetSource(), b64) + + cacheFileName := b64 + ".tar.gz" + cacheTargetPath := filepath.Join(vars.GetVDMCacheDir(), cacheFileName) + + cacheTarget, err := archive.CreateArchive(cacheRoot, cacheTargetPath) + if err != nil { + return fmt.Errorf("creating archive while adding remote '%s': %w", remote.GetSource(), err) + } + + sumDBFile, err := GetOrCreateSumDBFile() + if err != nil { + return fmt.Errorf("creating/opening sumdb file '%s': %w", sumDBFile.Name(), err) } defer func() { - if closeErr := f.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("closing cache file %s: %w", cacheFileName, closeErr)) + if closeErr := sumDBFile.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing sumdb file '%s': %w", sumDBFile.Name(), closeErr)) } }() - sum, err := CalculateSHASum(archiveFileHandle) + sum, err := CalculateSHASum(cacheTarget) if err != nil { - return fmt.Errorf("calculating sum when caching remote %s: %w", remote.GetSource(), err) + return fmt.Errorf("calculating checksum for writing: %w", err) } + message.Debugf("sum calculated for path '%s' was '%s'", cacheTargetPath) - contents := fmt.Sprintf("%s %s %s", remote.GetSource(), remote.GetVersion(), sum) - - err = os.WriteFile(f.Name(), []byte(contents), 0644) + sumDBContents := fmt.Sprintf("%s %s %s\n", remote.GetSource(), remote.GetVersion(), sum) + _, err = fmt.Fprint(sumDBFile, sumDBContents) if err != nil { - return fmt.Errorf("writing cache file %s: %w", f.Name(), err) + return fmt.Errorf("writing to cache file '%s': %w", cacheTargetPath, err) } return err diff --git a/internal/archive/cache/cachetest/cache_test.go b/internal/archive/cache/cachetest/cache_test.go index bc7debd..21b3009 100644 --- a/internal/archive/cache/cachetest/cache_test.go +++ b/internal/archive/cache/cachetest/cache_test.go @@ -37,7 +37,7 @@ func TestCacheRemote(t *testing.T) { } cachedPath := filepath.Join(os.Getenv(vars.VDMHomeEnvVarName), "cache", cache.StringToBase64(remote.Source)) - err = cache.AddRemote(remote, f) + err = cache.AddRemote(remote, "") assert.NoError(t, err) want := fmt.Sprintf("%s %s %s", remote.Source, remote.Version, "25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b") diff --git a/internal/archive/cache/sumdb.go b/internal/archive/cache/sumdb.go index 918e5a5..5e31a9a 100644 --- a/internal/archive/cache/sumdb.go +++ b/internal/archive/cache/sumdb.go @@ -5,11 +5,29 @@ import ( "encoding/base64" "fmt" "io" + "os" + "path/filepath" "strings" ) -// CalculateSHASum takes an arbitrary [io.Reader] (such as an open file handle) -// and calculates the SHA256 checksum for it. +// Caller's job to close file handle +func GetOrCreateSumDBFile() (*os.File, error) { + homedir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("determining user homedir: %w", err) + } + + sumDBPath := filepath.Join(homedir, ".vdm", "cache", "sumdb.json") + sumDBFile, err := os.OpenFile(sumDBPath, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return nil, fmt.Errorf("creating/opening sumdb file '%s': %w", sumDBPath, err) + } + + return sumDBFile, err +} + +// CalculateSHASum takes an arbitrary [io.Reader] (such as an open +// file handle) and calculates the SHA256 checksum for it. func CalculateSHASum(reader io.Reader) (string, error) { hasher := sha256.New() if _, err := io.Copy(hasher, reader); err != nil { diff --git a/internal/archive/cache/sumdb_test.go b/internal/archive/cache/sumdb_test.go index 87ae7a2..813aed8 100644 --- a/internal/archive/cache/sumdb_test.go +++ b/internal/archive/cache/sumdb_test.go @@ -29,8 +29,13 @@ func TestCalculateSHASum(t *testing.T) { t.Run("works on a created archive file", func(t *testing.T) { rootDir := "../../../testdata/filetree" archivePath := filepath.Join(os.TempDir(), "archive-to-hash.tar.gz") - err := archive.CreateArchive(rootDir, archivePath) + unneededFile, err := archive.CreateArchive(rootDir, archivePath) require.NoError(t, err) + // Need to close the returned file because that's the caller's job + t.Cleanup(func() { + err = unneededFile.Close() + require.NoError(t, err) + }) f, err := os.Open(archivePath) require.NoError(t, err) diff --git a/internal/remotes/git.go b/internal/remotes/git.go index 429da8b..0fa0163 100644 --- a/internal/remotes/git.go +++ b/internal/remotes/git.go @@ -7,7 +7,6 @@ import ( "os/exec" "path/filepath" - "github.com/opensourcecorp/vdm/cmd/vars" "github.com/opensourcecorp/vdm/internal/archive/cache" "github.com/opensourcecorp/vdm/internal/message" "github.com/opensourcecorp/vdm/internal/vdmspec" @@ -61,20 +60,7 @@ func (remote Git) Cache() (err error) { return fmt.Errorf("removing directory %s: %w", dotGitPath, err) } - // TODO-NOW: we need this to create two paths: one for the actual - // gzipped-tar cache, and one for the sumdb file - cacheFilePath := filepath.Join(vars.GetVDMCacheDir(), cache.StringToBase64(remote.Source)) - cacheFile, err := os.Create(cacheFilePath) - if err != nil { - return fmt.Errorf("preparing file for cache of git remote %s: %w", remote.Source, err) - } - defer func() { - if closeErr := cacheFile.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("closing cache file %s: %w", cacheFilePath, closeErr)) - } - }() - - err = cache.AddRemote(remote, cacheFile) + err = cache.AddRemote(remote, tmpCachePath) if err != nil { return fmt.Errorf("caching git remote %s: %w", remote.Source, err) } From b9fcfd8fc3d42581fc2dd379348e81a0d0b835ae Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sat, 17 Aug 2024 00:23:20 -0500 Subject: [PATCH 11/17] Temp-removed VDMMETA and file remote types, other changes --- README.md | 22 ++-- cmd/root.go | 3 +- cmd/sync.go | 61 +++++----- cmd/sync_test.go | 66 +++++------ internal/archive/archive.go | 104 +++++++++++++----- internal/archive/cache/cache.go | 36 ++++-- .../archive/cache/cachetest/cache_test.go | 2 +- internal/archive/cache/sumdb.go | 4 +- internal/filetree/filetree.go | 8 +- internal/message/message.go | 2 +- internal/remotes/file.go | 82 ++++++++------ internal/remotes/git.go | 48 ++++---- internal/remotes/git_test.go | 2 +- internal/vdmspec/spec.go | 22 ++-- internal/vdmspec/validate.go | 6 +- testdata/vdm.yaml | 21 ++-- 16 files changed, 287 insertions(+), 202 deletions(-) diff --git a/README.md b/README.md index 6c26f2a..30d7e85 100644 --- a/README.md +++ b/README.md @@ -51,20 +51,21 @@ revisions & where you want them to live on your filesystem: remotes: - type: "git" - source: "https://github.com/opensourcecorp/go-common" # can specify as 'git@...' to use SSH instead + source: "https://github.com/opensourcecorp/vdm" # can specify as 'git@...' to use SSH instead version: "v0.2.0" # tag example; can also be a branch, or a commit hash - destination: "./deps/go-common" + destination: "./deps/" # git types are themselves directories, so to prevent duplicating their top-level names just specify the root destination - - type: "file" # the 'file' type assumes the version is in the remote field itself somehow, so 'version' can be omitted - source: "https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto" - destination: "./deps/proto/http/http.proto" + - type: "git" + source: "https://github.com/opensourcecorp/osc-infra" + version: "main" + destination: "./deps/" ``` You can have as many dependency specifications in that array as you want, and they can be stored wherever you want. By default, this spec file is called `vdm.yaml` and lives at the calling location (which is probably your repo's root), but you can call it whatever you want and point to it using the -`--spec-file` flag to `vdm`. +`--specfile-path` flag to `vdm`. Once you have a spec file, just run: @@ -84,9 +85,10 @@ would look something like this: ```txt ./vdm.yaml ./deps/ - go-common/ + vdm/ + + osc-infra/ - http.proto ``` ## Dependencies @@ -113,4 +115,6 @@ running `vdm` commands. - Add `--keep-git-dir` flag so that `git` remote types don't wipe the `.git` directory at clone-time. -- Support more than just `git` and `file` types, and make `file` better +- Support more `remote` types, like `archive`. + +- Re-introduce the `file` remote type at some point. diff --git a/cmd/root.go b/cmd/root.go index 8836908..edc0abe 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -61,14 +61,13 @@ func newRootCommand() *cobra.Command { func executeRootCommand(cmd *cobra.Command, args []string) error { maybeSetDebug() if len(args) == 0 { - message.Errorf("You must provide a subcommand to vdm") err := cmd.Help() if err != nil { return errors.New("failed to print help message, somehow") } } - return nil + return errors.New("You must provide a subcommand to vdm") } // Execute wraps the primary execution logic for vdm's root command, and returns diff --git a/cmd/sync.go b/cmd/sync.go index 2ae0fa5..7486e18 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -1,7 +1,9 @@ package cmd import ( + "errors" "fmt" + "path/filepath" "github.com/opensourcecorp/vdm/internal/message" "github.com/opensourcecorp/vdm/internal/remotes" @@ -63,50 +65,57 @@ func sync() error { } for _, remote := range spec.Remotes { - // process stored vdm metafile so we know what operations to actually - // perform for existing directories - vdmMeta, err := remote.GetVDMMeta() - if err != nil { - return fmt.Errorf("getting vdm metadata file for sync: %w", err) - } - - if vdmMeta == (vdmspec.RemoteTemplate{}) { - message.Infof("%s: %s not found at local path, will be created", remote.OpMsg(), vdmspec.MetaFileName) - } else { - if vdmMeta.Version != remote.Version && vdmMeta.Source != remote.Source { - message.Infof("%s: Will change '%s' from current local version spec '%s' to '%s'...", remote.OpMsg(), remote.Source, vdmMeta.Version, remote.Version) - panic("jk not implemented") - } - message.Infof("%s: version unchanged in spec file, skipping", remote.OpMsg()) - continue - } + // TODO: add this back, but it's unused right now + // // process stored vdm metafile so we know what operations to actually + // // perform for existing directories + // vdmMeta, err := remote.GetVDMMeta() + // if err != nil { + // return fmt.Errorf("getting vdm metadata file for sync: %w", err) + // } + + // if vdmMeta == (vdmspec.RemoteTemplate{}) { + // message.Infof("%s: %s not found at local path, will be created", remote.OpMsg(), vdmspec.MetaFileName) + // } else { + // if vdmMeta.Version != remote.Version && vdmMeta.Source != remote.Source { + // message.Infof("%s: Will change %q from current local version spec %q to %q...", remote.OpMsg(), remote.Source, vdmMeta.Version, remote.Version) + // panic("jk not implemented") + // } + // message.Infof("%s: version unchanged in spec file, skipping", remote.OpMsg()) + // continue + // } var determinedRemote vdmspec.Remoter switch remote.Type { case vdmspec.GitType, "": determinedRemote = remotes.Git{RemoteTemplate: remote} case vdmspec.FileType: - determinedRemote = remotes.File{RemoteTemplate: remote} + return errors.New("cannot process 'file' remote types, as they are not yet fully implemented") default: - return fmt.Errorf("unrecognized remote type '%s'", remote.Type) + return fmt.Errorf("unrecognized remote type %q", remote.Type) } - err = determinedRemote.Cache() + cachePath, err := determinedRemote.Cache() if err != nil { - return fmt.Errorf("caching '%s' remote: %w", remote.Type, err) + return fmt.Errorf("caching %q remote: %w", remote.Type, err) } - err = determinedRemote.Sync() + absDestination, err := filepath.Abs(remote.Destination) if err != nil { - return fmt.Errorf("syncing '%s' remote: %w", remote.Type, err) + return fmt.Errorf("determining abspath of remote's destination %q: %w", remote.Destination, err) } - err = remote.WriteVDMMeta() + err = determinedRemote.Sync(cachePath, absDestination) if err != nil { - return fmt.Errorf("could not write %s file to disk: %w", vdmspec.MetaFileName, err) + return fmt.Errorf("syncing %q remote: %w", remote.Type, err) } - message.Infof("%s: Done.", remote.OpMsg()) + // TODO: add back, as above + // err = remote.WriteVDMMeta() + // if err != nil { + // return fmt.Errorf("could not write %s file to disk: %w", vdmspec.MetaFileName, err) + // } + + remote.OpMsg("Done.") } message.Infof("All done!") diff --git a/cmd/sync_test.go b/cmd/sync_test.go index 858b865..6fd25a3 100644 --- a/cmd/sync_test.go +++ b/cmd/sync_test.go @@ -23,46 +23,50 @@ func TestSync(t *testing.T) { // Need to override for test rootFlagValues.SpecFilePath = testSpecFilePath err = sync() - require.NoError(t, err) - defer t.Cleanup(func() { + assert.NoError(t, err) + + t.Cleanup(func() { for _, remote := range spec.Remotes { err := os.RemoveAll(remote.Destination) require.NoError(t, err) } }) - t.Run("SyncGit", func(t *testing.T) { - t.Run("remotes[0] used a tag", func(t *testing.T) { - vdmMeta, err := spec.Remotes[0].GetVDMMeta() - require.NoError(t, err) - assert.Equal(t, "v0.2.0", vdmMeta.Version) - }) + // TODO: the following tests relied on VDMMETA-checks, which are now + // unimplemented until I figure out how I want to manage those in the future - t.Run("remotes[1] used 'latest'", func(t *testing.T) { - vdmMeta, err := spec.Remotes[1].GetVDMMeta() - require.NoError(t, err) - assert.Equal(t, "latest", vdmMeta.Version) - }) + // t.Run("SyncGit", func(t *testing.T) { + // t.Run("remotes[0] used a tag", func(t *testing.T) { + // vdmMeta, err := spec.Remotes[0].GetVDMMeta() + // require.NoError(t, err) + // assert.Equal(t, "v0.2.0", vdmMeta.Version) + // }) - t.Run("remotes[2] used a branch", func(t *testing.T) { - vdmMeta, err := spec.Remotes[2].GetVDMMeta() - require.NoError(t, err) - assert.Equal(t, "main", vdmMeta.Version) - }) + // t.Run("remotes[1] used 'latest'", func(t *testing.T) { + // vdmMeta, err := spec.Remotes[1].GetVDMMeta() + // require.NoError(t, err) + // assert.Equal(t, "latest", vdmMeta.Version) + // }) - t.Run("remotes[3] used a hash", func(t *testing.T) { - vdmMeta, err := spec.Remotes[3].GetVDMMeta() - require.NoError(t, err) - assert.Equal(t, "2e6657f5ac013296167c4dd92fbb46f0e3dbdc5f", vdmMeta.Version) - }) - }) + // t.Run("remotes[2] used a branch", func(t *testing.T) { + // vdmMeta, err := spec.Remotes[2].GetVDMMeta() + // require.NoError(t, err) + // assert.Equal(t, "main", vdmMeta.Version) + // }) - t.Run("SyncFile", func(t *testing.T) { - t.Run("remotes[4] had an implicit version", func(t *testing.T) { - vdmMeta, err := spec.Remotes[4].GetVDMMeta() - require.NoError(t, err) - assert.Equal(t, "", vdmMeta.Version) - }) - }) + // t.Run("remotes[3] used a hash", func(t *testing.T) { + // vdmMeta, err := spec.Remotes[3].GetVDMMeta() + // require.NoError(t, err) + // assert.Equal(t, "2e6657f5ac013296167c4dd92fbb46f0e3dbdc5f", vdmMeta.Version) + // }) + // }) + + // t.Run("SyncFile", func(t *testing.T) { + // t.Run("remotes[4] had an implicit version", func(t *testing.T) { + // vdmMeta, err := spec.Remotes[4].GetVDMMeta() + // require.NoError(t, err) + // assert.Equal(t, "", vdmMeta.Version) + // }) + // }) } diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 120ca88..070b179 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -6,11 +6,13 @@ import ( "errors" "fmt" "io" + "log" "os" "path/filepath" "strings" "github.com/opensourcecorp/vdm/internal/filetree" + "github.com/opensourcecorp/vdm/internal/message" ) // Much of the following functions taken from: @@ -19,19 +21,20 @@ import ( // CreateArchive writes a gzipped tarball based on the provided root directory // from which to construct the archive, and its target file name. It returns an // open file handle to the archive, which should be closed by the caller. -func CreateArchive(rootDir string, archivePath string) (f *os.File, err error) { +func CreateArchive(root string, archivePath string) (f *os.File, err error) { if !strings.HasSuffix(archivePath, ".tar.gz") { return nil, errors.New("provided archive path must end in .tar.gz") } - rootDirAbs, err := filepath.Abs(rootDir) + rootAbs, err := filepath.Abs(root) if err != nil { - return nil, fmt.Errorf("determining abspath of provided root dir %s: %w", rootDir, err) + return nil, fmt.Errorf("determining abspath of provided root dir %q: %w", root, err) } + message.Debugf("root: %q, rootAbs: %q", root, rootAbs) archivePathAbs, err := filepath.Abs(archivePath) if err != nil { - return nil, fmt.Errorf("determining abspath of provided archive path %s: %w", archivePath, err) + return nil, fmt.Errorf("determining abspath of provided archive path %q: %w", archivePath, err) } buf, err := os.Create(archivePathAbs) @@ -57,25 +60,78 @@ func CreateArchive(rootDir string, archivePath string) (f *os.File, err error) { } }() - files, err := filetree.GetFilePathsInDirectory(rootDirAbs) + files, err := filetree.GetFilePathsInDirectory(rootAbs) if err != nil { - return nil, fmt.Errorf("populating list of files from %s: %w", rootDir, err) + return nil, fmt.Errorf("populating list of files from %q: %w", root, err) } for _, fileName := range files { - err := addToArchive(tarWriter, rootDirAbs, fileName) + err := addToArchive(tarWriter, rootAbs, fileName) if err != nil { - return nil, fmt.Errorf("adding %s to archive: %w", fileName, err) + return nil, fmt.Errorf("adding %q to archive: %w", fileName, err) } } return buf, err } -func addToArchive(tarWriter *tar.Writer, rootDirAbs string, fileName string) (err error) { +func ExtractArchiveToDestination(src, dest string) error { + gzipFile, err := os.Open(src) + if err != nil { + return fmt.Errorf("opening archive file: %w", err) + } + + gzipReader, err := gzip.NewReader(gzipFile) + if err != nil { + log.Fatal("ExtractTarGz: NewReader failed") + } + + tarReader := tar.NewReader(gzipReader) + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } else if err != nil { + return fmt.Errorf("Next() failed in tar reader: %w", err) + } + + filePath := filepath.Join(dest, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(filePath, 0755); err != nil { + return fmt.Errorf("making directory during tar extraction: %w", err) + } + case tar.TypeReg: + dirPath := filepath.Dir(filePath) + if err := os.MkdirAll(dirPath, 0755); err != nil { + return fmt.Errorf("making directory during tar extraction: %w", err) + } + + outFile, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("creating output file %q during tar extraction: %w", filePath, err) + } + if _, err := io.Copy(outFile, tarReader); err != nil { + return fmt.Errorf("writing to output file %q during tar extraction: %w", filePath, err) + } + closeErr := outFile.Close() + if closeErr != nil { + return fmt.Errorf("closing output file used during tar extraction: %w", closeErr) + } + default: + return fmt.Errorf("unknown tar type '%b' in %q", header.Typeflag, filePath) + } + } + + return nil +} + +func addToArchive(tarWriter *tar.Writer, rootAbs string, fileName string) (err error) { file, err := os.Open(fileName) if err != nil { - return fmt.Errorf("opening file %s: %w", fileName, err) + return fmt.Errorf("opening file %q: %w", fileName, err) } defer func() { if closeErr := file.Close(); closeErr != nil { @@ -85,20 +141,20 @@ func addToArchive(tarWriter *tar.Writer, rootDirAbs string, fileName string) (er info, err := file.Stat() if err != nil { - return fmt.Errorf("getting file info for %s: %w", fileName, err) + return fmt.Errorf("getting file info for %q: %w", fileName, err) } // Tar needs file headers, so create one from the file info header, err := tar.FileInfoHeader(info, info.Name()) if err != nil { - return fmt.Errorf("creating tar header for %s: %w", fileName, err) + return fmt.Errorf("creating tar header for %q: %w", fileName, err) } - topLevelDir, err := maybeGetTopLevelDir(rootDirAbs) + topLevelDir, err := maybeGetTopLevelDir(rootAbs) if err != nil { return fmt.Errorf("checking for top-level directory when adding to archive: %w", err) } - fileNameClean := strings.ReplaceAll(fileName, rootDirAbs+string(filepath.Separator), "") + fileNameClean := strings.ReplaceAll(fileName, rootAbs+string(filepath.Separator), "") if topLevelDir != "" { fileNameClean = filepath.Join(topLevelDir, fileNameClean) } @@ -112,31 +168,19 @@ func addToArchive(tarWriter *tar.Writer, rootDirAbs string, fileName string) (er // Write file header to the tar archive err = tarWriter.WriteHeader(header) if err != nil { - return fmt.Errorf("writing tar header for %s: %w", fileName, err) + return fmt.Errorf("writing tar header for %q: %w", fileName, err) } // Copy file content to tar archive _, err = io.Copy(tarWriter, file) if err != nil { - return fmt.Errorf("adding file %s to tar archive: %w", fileName, err) + return fmt.Errorf("adding file %q to tar archive: %w", fileName, err) } return err } -func maybeGetTopLevelDir(rootDirAbs string) (topLevelDir string, err error) { - topLevelFileInfo, err := os.Stat(rootDirAbs) - if err != nil { - return "", fmt.Errorf("getting file info for %s: %w", rootDirAbs, err) - } - - // We only want to include the top-level directory for archive pathing if - // it's *actually* a directory, obviously - if !topLevelFileInfo.IsDir() { - topLevelDir = "" - } else { - topLevelDir = filepath.Base(rootDirAbs) - } - +func maybeGetTopLevelDir(rootAbs string) (string, error) { + topLevelDir := filepath.Base(rootAbs) return topLevelDir, nil } diff --git a/internal/archive/cache/cache.go b/internal/archive/cache/cache.go index dc70e13..d2d62b0 100644 --- a/internal/archive/cache/cache.go +++ b/internal/archive/cache/cache.go @@ -3,6 +3,7 @@ package cache import ( "errors" "fmt" + "math/rand" "os" "path/filepath" @@ -14,47 +15,58 @@ import ( // AddRemote uses the provided [vdmspec.Remoter] information along with a file // handle for a target archive to actually write the archive data. TODO fix this -func AddRemote(remote vdmspec.Remoter, cacheRoot string) (err error) { +func AddRemote(remote vdmspec.Remoter, cacheRoot string) (cachePath string, err error) { err = os.MkdirAll(vars.GetVDMCacheDir(), 0755) if err != nil { - return fmt.Errorf("creating vdm cache directory %s: %w", vars.GetVDMCacheDir(), err) + return "", fmt.Errorf("creating vdm cache directory %q: %w", vars.GetVDMCacheDir(), err) } - b64 := StringToBase64(remote.GetSource()) + remoteWithVersion := fmt.Sprintf("%s@%s", remote.GetSource(), remote.GetVersion()) + b64 := StringToBase64(remoteWithVersion) if err != nil { - return fmt.Errorf("calculating sum when caching remote %s: %w", remote.GetSource(), err) + return "", fmt.Errorf("calculating sum when caching remote %q: %w", remoteWithVersion, err) } - message.Debugf("remote '%s' base64'd to '%s'", remote.GetSource(), b64) + message.Debugf("remote %q base64'd to %q", remoteWithVersion, b64) cacheFileName := b64 + ".tar.gz" cacheTargetPath := filepath.Join(vars.GetVDMCacheDir(), cacheFileName) cacheTarget, err := archive.CreateArchive(cacheRoot, cacheTargetPath) if err != nil { - return fmt.Errorf("creating archive while adding remote '%s': %w", remote.GetSource(), err) + return "", fmt.Errorf("creating archive while adding remote %q: %w", remoteWithVersion, err) } sumDBFile, err := GetOrCreateSumDBFile() if err != nil { - return fmt.Errorf("creating/opening sumdb file '%s': %w", sumDBFile.Name(), err) + return "", fmt.Errorf("creating/opening sumdb file %q: %w", sumDBFile.Name(), err) } defer func() { if closeErr := sumDBFile.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("closing sumdb file '%s': %w", sumDBFile.Name(), closeErr)) + err = errors.Join(err, fmt.Errorf("closing sumdb file %q: %w", sumDBFile.Name(), closeErr)) } }() sum, err := CalculateSHASum(cacheTarget) if err != nil { - return fmt.Errorf("calculating checksum for writing: %w", err) + return "", fmt.Errorf("calculating checksum for writing: %w", err) } - message.Debugf("sum calculated for path '%s' was '%s'", cacheTargetPath) + message.Debugf("sum calculated for path %q was %q", cacheTargetPath, sum) sumDBContents := fmt.Sprintf("%s %s %s\n", remote.GetSource(), remote.GetVersion(), sum) _, err = fmt.Fprint(sumDBFile, sumDBContents) if err != nil { - return fmt.Errorf("writing to cache file '%s': %w", cacheTargetPath, err) + return "", fmt.Errorf("writing to cache file %q: %w", cacheTargetPath, err) } - return err + return cacheTargetPath, err +} + +func GetTempCachePath(remote vdmspec.Remoter) string { + // tmpCachePath is where the actual retrieval is targeted, which should then + // be later archived to the persistent cache + randID := 100000000000 + rand.Intn(999999999999) + tmpCacheRoot := filepath.Join(os.TempDir(), fmt.Sprintf("vdm-tmp-%d", randID)) + tmpCachePath := filepath.Join(tmpCacheRoot, filepath.Base(remote.GetSource())) + + return tmpCachePath } diff --git a/internal/archive/cache/cachetest/cache_test.go b/internal/archive/cache/cachetest/cache_test.go index 21b3009..a14007e 100644 --- a/internal/archive/cache/cachetest/cache_test.go +++ b/internal/archive/cache/cachetest/cache_test.go @@ -37,7 +37,7 @@ func TestCacheRemote(t *testing.T) { } cachedPath := filepath.Join(os.Getenv(vars.VDMHomeEnvVarName), "cache", cache.StringToBase64(remote.Source)) - err = cache.AddRemote(remote, "") + _, err = cache.AddRemote(remote, "") assert.NoError(t, err) want := fmt.Sprintf("%s %s %s", remote.Source, remote.Version, "25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b") diff --git a/internal/archive/cache/sumdb.go b/internal/archive/cache/sumdb.go index 5e31a9a..3872c95 100644 --- a/internal/archive/cache/sumdb.go +++ b/internal/archive/cache/sumdb.go @@ -20,7 +20,7 @@ func GetOrCreateSumDBFile() (*os.File, error) { sumDBPath := filepath.Join(homedir, ".vdm", "cache", "sumdb.json") sumDBFile, err := os.OpenFile(sumDBPath, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0644) if err != nil { - return nil, fmt.Errorf("creating/opening sumdb file '%s': %w", sumDBPath, err) + return nil, fmt.Errorf("creating/opening sumdb file %q: %w", sumDBPath, err) } return sumDBFile, err @@ -53,7 +53,7 @@ func StringFromBase64(s string) (string, error) { fixedString := restoreNonAlphaBase64Characters(s) out, err := base64.StdEncoding.DecodeString(fixedString) if err != nil { - return "", fmt.Errorf("decoding base64 string '%s': %w", s, err) + return "", fmt.Errorf("decoding base64 string %q: %w", s, err) } return string(out), nil } diff --git a/internal/filetree/filetree.go b/internal/filetree/filetree.go index 6f4eaa9..09598f5 100644 --- a/internal/filetree/filetree.go +++ b/internal/filetree/filetree.go @@ -8,14 +8,14 @@ import ( // GetFilePathsInDirectory traverse a directory tree from the provided root, and // returns a slice of the files in the tree. -func GetFilePathsInDirectory(rootDir string) ([]string, error) { - rootDirAbs, err := filepath.Abs(rootDir) +func GetFilePathsInDirectory(root string) ([]string, error) { + rootAbs, err := filepath.Abs(root) if err != nil { - return nil, fmt.Errorf("determining abspath of rootDir %s: %w", rootDirAbs, err) + return nil, fmt.Errorf("determining abspath of root %q: %w", rootAbs, err) } var files []string - err = filepath.Walk(rootDirAbs, func(path string, f os.FileInfo, err error) error { + err = filepath.Walk(rootAbs, func(path string, f os.FileInfo, err error) error { if err != nil { return err } diff --git a/internal/message/message.go b/internal/message/message.go index 6201145..25a593b 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -20,7 +20,7 @@ func Debugf(format string, args ...any) { // Infof prints out debug-level information messages with a formatting // directive. func Infof(format string, args ...any) { - fmt.Printf(format+"\n", args...) + fmt.Printf("INFO: "+format+"\n", args...) } // Warnf prints out debug-level information messages with a formatting diff --git a/internal/remotes/file.go b/internal/remotes/file.go index 875590e..19ec219 100644 --- a/internal/remotes/file.go +++ b/internal/remotes/file.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" + "github.com/opensourcecorp/vdm/internal/archive" + "github.com/opensourcecorp/vdm/internal/archive/cache" "github.com/opensourcecorp/vdm/internal/message" "github.com/opensourcecorp/vdm/internal/vdmspec" ) @@ -18,27 +20,47 @@ type File struct { } // Cache provides the [vdmspec.Remoter.Cache] operations for "file" remote types. -func (remote File) Cache() error { - return errors.New("not implemented") +func (remote File) Cache() (cachePath string, err error) { + tmpCachePath := cache.GetTempCachePath(remote) + message.Debugf("tmpCachePath: %q", tmpCachePath) + defer func() { + if rmErr := os.RemoveAll(filepath.Dir(tmpCachePath)); rmErr != nil { + err = errors.Join(err, fmt.Errorf("removing temporary cache path %q: %w", tmpCachePath, rmErr)) + } + }() + + err = ensureParentDirs(tmpCachePath) + if err != nil { + return "", fmt.Errorf("creating parent temp cache directories for file remote %q: %w", tmpCachePath, err) + } + + remote.OpMsg("Retrieving...") + err = retrieveFile(remote, tmpCachePath) + if err != nil { + return "", fmt.Errorf("retrieving file: %w", err) + } + + cachePath, err = cache.AddRemote(remote, tmpCachePath) + if err != nil { + return "", fmt.Errorf("caching file remote %q: %w", remote.Source, err) + } + + remote.OpMsg("Done.") + return cachePath, err } // Sync provides the [vdmspec.Remoter.Sync] operations for "file" remote types. -func (remote File) Sync() error { - fileExists, err := checkFileExists(remote) +func (remote File) Sync(src, dest string) error { + // We want to make sure the parent directories exist for the real destination, not the temp cache + err := ensureParentDirs(remote.Destination) if err != nil { - return fmt.Errorf("checking if file exists locally: %w", err) + return fmt.Errorf("creating parent directories for file %q: %w", dest, err) } - if !fileExists { - message.Infof("File '%s' does not exist locally, retrieving", remote.Destination) - err = retrieveFile(remote) - if err != nil { - return fmt.Errorf("retrieving file: %w", err) - } - } else { - message.Infof("File '%s' already exists locally, skipping", remote.Destination) + err = archive.ExtractArchiveToDestination(src, dest) + if err != nil { + return fmt.Errorf("syncing file cache for remote %q: %w", remote.GetSource(), err) } - return nil } @@ -55,49 +77,45 @@ func (remote File) GetVersion() string { func checkFileExists(remote File) (bool, error) { fullPath, err := filepath.Abs(remote.Destination) if err != nil { - return false, fmt.Errorf("determining abspath for file '%s': %w", remote.Destination, err) + return false, fmt.Errorf("determining abspath for file %q: %w", remote.Destination, err) } _, err = os.Stat(remote.Destination) if errors.Is(err, os.ErrNotExist) { return false, nil } else if err != nil { - return false, fmt.Errorf("couldn't check if %s exists at '%s': %w", remote.Destination, fullPath, err) + return false, fmt.Errorf("couldn't check if %q exists at %q: %w", remote.Destination, fullPath, err) } return true, nil } -func retrieveFile(remote File) (err error) { +func retrieveFile(remote File, dest string) (err error) { resp, err := http.Get(remote.Source) if err != nil { - return fmt.Errorf("retrieving remote file '%s': %w", remote.Source, err) + return fmt.Errorf("retrieving remote file %q: %w", remote.Source, err) } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("closing response body after remote file '%s' retrieval: %w", remote.Source, err)) + err = errors.Join(err, fmt.Errorf("closing response body after remote file %q retrieval: %w", remote.Source, err)) } }() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unsuccessful status code '%d' from server when retrieving remote file '%s'", resp.StatusCode, remote.Source) - } - - err = ensureParentDirs(remote.Destination) - if err != nil { - return fmt.Errorf("creating parent directories for file: %w", err) + return fmt.Errorf("unsuccessful status code '%d' from server when retrieving remote file %q", resp.StatusCode, remote.Source) } // Note: I would normally use os.WriteFile() using the returned bytes // directly, but the internet says this os.Create()/io.Copy() approach // appears to be idiomatic - outFile, err := os.Create(remote.Destination) + message.Debugf("landing file to be created at %q", dest) + outFile, err := os.Create(dest) if err != nil { - return fmt.Errorf("creating landing file '%s' for remote file: %w", remote.Destination, err) + return fmt.Errorf("creating landing file %q for remote file: %w", dest, err) } defer func() { if closeErr := outFile.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("closing local file '%s' after remote file '%s' retrieval: %w", remote.Destination, remote.Source, err)) + err = errors.Join(err, fmt.Errorf("closing local file %q after remote file %q retrieval: %w", dest, remote.Source, err)) } }() @@ -105,7 +123,7 @@ func retrieveFile(remote File) (err error) { if err != nil { return fmt.Errorf("copying HTTP response to disk: ") } - message.Debugf("wrote %d bytes to '%s'", bytesWritten, remote.Destination) + message.Debugf("wrote %d bytes to %q", bytesWritten, dest) return nil } @@ -113,15 +131,15 @@ func retrieveFile(remote File) (err error) { func ensureParentDirs(path string) error { fullPath, err := filepath.Abs(path) if err != nil { - return fmt.Errorf("determining abspath for file '%s': %w", path, err) + return fmt.Errorf("determining abspath for file %q: %w", path, err) } - message.Debugf("absolute filepath for '%s' determined to be '%s'", path, fullPath) + message.Debugf("absolute filepath for %q determined to be %q", path, fullPath) dir := filepath.Dir(fullPath) err = os.MkdirAll(dir, os.ModePerm) if err != nil { return fmt.Errorf("making directories: %w", err) } - message.Debugf("created director(ies): %s", dir) + message.Debugf("created director(ies): %q", dir) return nil } diff --git a/internal/remotes/git.go b/internal/remotes/git.go index 0fa0163..fd1dc3b 100644 --- a/internal/remotes/git.go +++ b/internal/remotes/git.go @@ -7,6 +7,7 @@ import ( "os/exec" "path/filepath" + "github.com/opensourcecorp/vdm/internal/archive" "github.com/opensourcecorp/vdm/internal/archive/cache" "github.com/opensourcecorp/vdm/internal/message" "github.com/opensourcecorp/vdm/internal/vdmspec" @@ -19,58 +20,55 @@ type Git struct { } // Cache provides the [vdmspec.Remoter.Cache] operations for "git" remote types. -func (remote Git) Cache() (err error) { - // tmpCachePath is where the actual retrieval is targeted, which is then - // later archived to the persistent cache - tmpCacheRoot := filepath.Join(os.TempDir(), "vdm-tmp") - tmpCachePath := filepath.Join(tmpCacheRoot, filepath.Base(remote.Source)) - message.Debugf("tmpCachePath: %s", tmpCachePath) +func (remote Git) Cache() (cachePath string, err error) { + tmpCachePath := cache.GetTempCachePath(remote) + message.Debugf("tmpCachePath: %q", tmpCachePath) defer func() { - if rmErr := os.RemoveAll(tmpCacheRoot); rmErr != nil { - err = errors.Join(err, fmt.Errorf("removing temporary cache path '%s': %w", tmpCacheRoot, rmErr)) + if rmErr := os.RemoveAll(filepath.Dir(tmpCachePath)); rmErr != nil { + err = errors.Join(err, fmt.Errorf("removing temporary cache path %q: %w", tmpCachePath, rmErr)) } }() - if err := os.RemoveAll(tmpCacheRoot); err != nil { - return fmt.Errorf("trying to clean up possibly-duplicate old temp cache data at '%s': %w", tmpCachePath, err) - } - - message.Infof("%s: Retrieving...", remote.OpMsg()) + remote.OpMsg("Retrieving...") err = gitClone(remote.Source, tmpCachePath) if err != nil { - return fmt.Errorf("cloning git repository: %w", err) + return "", fmt.Errorf("cloning git repository: %w", err) } defer func() { if rmErr := os.RemoveAll(tmpCachePath); rmErr != nil { - err = errors.Join(err, fmt.Errorf("removing temporary cache directory for '%s': %w", remote.GetSource(), rmErr)) + err = errors.Join(err, fmt.Errorf("removing temporary cache directory for %q: %w", remote.GetSource(), rmErr)) } }() - message.Infof("%s: Setting specified version...", remote.OpMsg()) + remote.OpMsg("Setting specified version...") checkoutCmd := exec.Command("git", "-C", tmpCachePath, "checkout", remote.Version) checkoutOutput, err := checkoutCmd.CombinedOutput() if err != nil { - return fmt.Errorf("error checking out specified revision: exec error '%w', with output: %s", err, string(checkoutOutput)) + return "", fmt.Errorf("error checking out specified revision: exec error '%w', with output: %s", err, string(checkoutOutput)) } - message.Debugf("removing .git dir for local path '%s'", tmpCachePath) + message.Debugf("removing .git dir for local path %q", tmpCachePath) dotGitPath := filepath.Join(tmpCachePath, ".git") err = os.RemoveAll(dotGitPath) if err != nil { - return fmt.Errorf("removing directory %s: %w", dotGitPath, err) + return "", fmt.Errorf("removing directory %q: %w", dotGitPath, err) } - err = cache.AddRemote(remote, tmpCachePath) + cachePath, err = cache.AddRemote(remote, tmpCachePath) if err != nil { - return fmt.Errorf("caching git remote %s: %w", remote.Source, err) + return "", fmt.Errorf("caching git remote %q: %w", remote.Source, err) } - return err + return cachePath, err } // Sync provides the [vdmspec.Remoter.Sync] operations for "git" remote types. -func (remote Git) Sync() error { - return errors.New("not implemented") +func (remote Git) Sync(src, dest string) error { + err := archive.ExtractArchiveToDestination(src, dest) + if err != nil { + return fmt.Errorf("syncing git cache for remote %q: %w", remote.GetSource(), err) + } + return nil } // GetSource returns the Source field. @@ -97,7 +95,7 @@ func checkGitAvailable() error { func gitClone(src string, dest string) error { err := checkGitAvailable() if err != nil { - return fmt.Errorf("remote '%s' is a git type, but git may not installed/available on PATH: %w", src, err) + return fmt.Errorf("remote %q is a git type, but git may not installed/available on PATH: %w", src, err) } cloneCmdArgs := []string{"clone", src, dest} diff --git a/internal/remotes/git_test.go b/internal/remotes/git_test.go index 3d1cd31..d319886 100644 --- a/internal/remotes/git_test.go +++ b/internal/remotes/git_test.go @@ -27,7 +27,7 @@ func getTestGitRemote(t *testing.T) (Git, string) { func TestSyncGit(t *testing.T) { remote, dest := getTestGitRemote(t) - err := remote.Sync() + err := remote.Sync("", "") require.NoError(t, err) defer t.Cleanup(func() { diff --git a/internal/vdmspec/spec.go b/internal/vdmspec/spec.go index 1f5aea5..b77d0d8 100644 --- a/internal/vdmspec/spec.go +++ b/internal/vdmspec/spec.go @@ -31,10 +31,10 @@ type Spec struct { type Remoter interface { // Cache should retrieve the remote, and cache it as an archive in // VDM_HOME's cache - Cache() error + Cache() (string, error) // Sync should unpack the archive from the cache in VDM_HOME to the // specified destination - Sync() error + Sync(src, dest string) error // GetRemote returns the [RemoteTemplate.Source] value GetSource() string // GetRemote returns the [RemoteTemplate.Version] value @@ -89,12 +89,12 @@ func (r RemoteTemplate) WriteVDMMeta() error { metaFilePath := r.MakeMetaFilePath() vdmMetaContent, err := yaml.Marshal(r) if err != nil { - return fmt.Errorf("writing %s: %w", metaFilePath, err) + return fmt.Errorf("writing %q: %w", metaFilePath, err) } vdmMetaContent = append(vdmMetaContent, []byte("\n")...) - message.Debugf("writing metadata file to '%s'", metaFilePath) + message.Debugf("writing metadata file to %q", metaFilePath) err = os.WriteFile(metaFilePath, vdmMetaContent, 0644) if err != nil { return fmt.Errorf("writing metadata file: %w", err) @@ -111,13 +111,13 @@ func (r RemoteTemplate) GetVDMMeta() (RemoteTemplate, error) { if errors.Is(err, os.ErrNotExist) { return RemoteTemplate{}, nil // this is ok, because it might literally not exist yet } else if err != nil { - return RemoteTemplate{}, fmt.Errorf("couldn't check if %s exists at '%s': %w", MetaFileName, metaFilePath, err) + return RemoteTemplate{}, fmt.Errorf("couldn't check if %q exists at %q: %w", MetaFileName, metaFilePath, err) } vdmMetaFile, err := os.ReadFile(metaFilePath) if err != nil { message.Debugf("error reading VMDMMETA from disk: %w", err) - return RemoteTemplate{}, fmt.Errorf("there was a problem reading the %s file from '%s': %w", MetaFileName, metaFilePath, err) + return RemoteTemplate{}, fmt.Errorf("there was a problem reading the %s file from %q: %w", MetaFileName, metaFilePath, err) } message.Debugf("%s contents read:\n%s", MetaFileName, string(vdmMetaFile)) @@ -125,9 +125,9 @@ func (r RemoteTemplate) GetVDMMeta() (RemoteTemplate, error) { err = yaml.Unmarshal(vdmMetaFile, &vdmMeta) if err != nil { message.Debugf("error during %s unmarshal: w", MetaFileName, err) - return RemoteTemplate{}, fmt.Errorf("there was a problem reading the contents of the %s file at '%s': %w", MetaFileName, metaFilePath, err) + return RemoteTemplate{}, fmt.Errorf("there was a problem reading the contents of the %s file at %q: %w", MetaFileName, metaFilePath, err) } - message.Debugf("file %s unmarshalled: %+v", MetaFileName, vdmMeta) + message.Debugf("file %q unmarshalled: %+v", MetaFileName, vdmMeta) return vdmMeta, nil } @@ -141,7 +141,7 @@ func GetSpecFromFile(specFilePath string) (Spec, error) { message.Debugf("error reading specfile from disk: %v", err) return Spec{}, fmt.Errorf( strings.Join([]string{ - "there was a problem reading your vdm file from '%s' -- does it not exist?", + "there was a problem reading your vdm file from %q -- does it not exist?", "Either pass the --spec-file flag, or create one in the default location (details in the README).", "Error details: %w"}, " ", @@ -165,6 +165,6 @@ func GetSpecFromFile(specFilePath string) (Spec, error) { // OpMsg constructs a loggable message outlining the specific remote details // being performed at the moment -func (r RemoteTemplate) OpMsg() string { - return fmt.Sprintf("%s@%s --> %s", r.Source, r.Version, r.Destination) +func (r RemoteTemplate) OpMsg(msg string) { + message.Infof("%s@%s --> %s: %s", r.Source, r.Version, r.Destination, msg) } diff --git a/internal/vdmspec/validate.go b/internal/vdmspec/validate.go index 8b50e7b..4c8144b 100644 --- a/internal/vdmspec/validate.go +++ b/internal/vdmspec/validate.go @@ -23,7 +23,7 @@ func (spec Spec) Validate() error { if !protocolRegex.MatchString(remote.Source) { allErrors = append( allErrors, - fmt.Errorf("remote #%d provided as '%s', but all 'source' fields must begin with a protocol specifier or other valid prefix (e.g. 'https://', '(user|git)@', etc.)", remoteIndex, remote.Source), + fmt.Errorf("remote #%d provided as %q, but all 'source' fields must begin with a protocol specifier or other valid prefix (e.g. 'https://', '(user|git)@', etc.)", remoteIndex, remote.Source), ) } @@ -33,7 +33,7 @@ func (spec Spec) Validate() error { allErrors = append(allErrors, errors.New("all 'version' fields for the 'git' remote type must be non-zero length")) } if remote.Type == FileType && remote.Version != "" { - message.Warnf("NOTE: Remote #%d '%s' specified as type '%s', which does not take explicit version info (you provided '%s'); ignoring version field", remoteIndex, remote.Source, remote.Type, remote.Version) + message.Warnf("NOTE: Remote #%d %q specified as type %q, which does not take explicit version info (you provided %q); ignoring version field", remoteIndex, remote.Source, remote.Type, remote.Version) } // Destination field @@ -52,7 +52,7 @@ func (spec Spec) Validate() error { FileType: 2, } if _, ok := typeMap[remote.Type]; !ok { - allErrors = append(allErrors, fmt.Errorf("unrecognized remote type '%s'", remote.Type)) + allErrors = append(allErrors, fmt.Errorf("unrecognized remote type %q", remote.Type)) } } diff --git a/testdata/vdm.yaml b/testdata/vdm.yaml index 8863bbf..28aaba1 100644 --- a/testdata/vdm.yaml +++ b/testdata/vdm.yaml @@ -1,20 +1,17 @@ remotes: - type: "git" - source: "https://github.com/opensourcecorp/go-common" + source: "https://github.com/opensourcecorp/vdm" version: "v0.2.0" - destination: "./deps/go-common-tag" - - type: "git" - source: "https://github.com/opensourcecorp/go-common" - version: "latest" - destination: "./deps/go-common-latest" + destination: "./deps/git-tag" - type: "git" - source: "https://github.com/opensourcecorp/go-common" + source: "https://github.com/opensourcecorp/osc-infra" version: "main" - destination: "./deps/go-common-branch" + destination: "./deps/git-branch" - type: "git" source: "https://github.com/opensourcecorp/go-common" version: "2e6657f5ac013296167c4dd92fbb46f0e3dbdc5f" - destination: "./deps/go-common-hash" - - type: "file" - source: "https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto" - destination: "./deps/proto/http/http.proto" + destination: "./deps/git-hash" + # TODO: re-implement this in the future + # - type: "file" + # source: "https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto" + # destination: "./deps/proto/http/http.proto" From 84b5b8499fc9bf52a664b4e7ebe6a16754737063 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 18 Aug 2024 15:58:59 -0500 Subject: [PATCH 12/17] WIP for sumdb to SQLite DB --- go.mod | 17 ++++++- go.sum | 44 +++++++++++++++-- internal/archive/archive.go | 16 +++++-- internal/archive/cache/cache.go | 32 +++---------- internal/archive/cache/sumdb.go | 84 +++++++++++++++++++++++++++++---- internal/remotes/file.go | 13 ++++- internal/remotes/git.go | 13 ++++- internal/vdmspec/spec.go | 11 ++++- 8 files changed, 183 insertions(+), 47 deletions(-) diff --git a/go.mod b/go.mod index b1b5ed1..59046bc 100644 --- a/go.mod +++ b/go.mod @@ -7,17 +7,24 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.32.0 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -27,8 +34,14 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index 365ddea..0cfcb64 100644 --- a/go.sum +++ b/go.sum @@ -3,10 +3,17 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -15,13 +22,19 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= @@ -56,12 +69,15 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -69,3 +85,23 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= +modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 070b179..ddb8e8c 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -9,12 +9,18 @@ import ( "log" "os" "path/filepath" + "regexp" "strings" "github.com/opensourcecorp/vdm/internal/filetree" "github.com/opensourcecorp/vdm/internal/message" ) +var ( + tgzRegex = regexp.MustCompile(`(\.tar\.gz|\.tgz)$`) + zipRegex = regexp.MustCompile(`\.zip$`) +) + // Much of the following functions taken from: // https://www.arthurkoziel.com/writing-tar-gz-files-in-go/ @@ -22,8 +28,8 @@ import ( // from which to construct the archive, and its target file name. It returns an // open file handle to the archive, which should be closed by the caller. func CreateArchive(root string, archivePath string) (f *os.File, err error) { - if !strings.HasSuffix(archivePath, ".tar.gz") { - return nil, errors.New("provided archive path must end in .tar.gz") + if !tgzRegex.MatchString(archivePath) { + return nil, errors.New("provided archive path must have valid gzipped-tar extension") } rootAbs, err := filepath.Abs(root) @@ -75,7 +81,11 @@ func CreateArchive(root string, archivePath string) (f *os.File, err error) { return buf, err } -func ExtractArchiveToDestination(src, dest string) error { +func ExtractTGZArchive(src, dest string) error { + if !tgzRegex.MatchString(src) { + return errors.New("provided archive path must have valid gzipped-tar extension") + } + gzipFile, err := os.Open(src) if err != nil { return fmt.Errorf("opening archive file: %w", err) diff --git a/internal/archive/cache/cache.go b/internal/archive/cache/cache.go index d2d62b0..07ca507 100644 --- a/internal/archive/cache/cache.go +++ b/internal/archive/cache/cache.go @@ -1,7 +1,6 @@ package cache import ( - "errors" "fmt" "math/rand" "os" @@ -21,41 +20,24 @@ func AddRemote(remote vdmspec.Remoter, cacheRoot string) (cachePath string, err return "", fmt.Errorf("creating vdm cache directory %q: %w", vars.GetVDMCacheDir(), err) } - remoteWithVersion := fmt.Sprintf("%s@%s", remote.GetSource(), remote.GetVersion()) - b64 := StringToBase64(remoteWithVersion) + b64 := StringToBase64(remote.GetSourceVersion()) if err != nil { - return "", fmt.Errorf("calculating sum when caching remote %q: %w", remoteWithVersion, err) + return "", fmt.Errorf("calculating sum when caching remote %q: %w", remote.GetSourceVersion(), err) } - message.Debugf("remote %q base64'd to %q", remoteWithVersion, b64) + message.Debugf("remote %q base64'd to %q", remote.GetSourceVersion(), b64) cacheFileName := b64 + ".tar.gz" cacheTargetPath := filepath.Join(vars.GetVDMCacheDir(), cacheFileName) cacheTarget, err := archive.CreateArchive(cacheRoot, cacheTargetPath) if err != nil { - return "", fmt.Errorf("creating archive while adding remote %q: %w", remoteWithVersion, err) + return "", fmt.Errorf("creating archive while adding remote %q: %w", remote.GetSourceVersion(), err) } + defer cacheTarget.Close() - sumDBFile, err := GetOrCreateSumDBFile() + err = AddToSumDB(remote, cacheTarget) if err != nil { - return "", fmt.Errorf("creating/opening sumdb file %q: %w", sumDBFile.Name(), err) - } - defer func() { - if closeErr := sumDBFile.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("closing sumdb file %q: %w", sumDBFile.Name(), closeErr)) - } - }() - - sum, err := CalculateSHASum(cacheTarget) - if err != nil { - return "", fmt.Errorf("calculating checksum for writing: %w", err) - } - message.Debugf("sum calculated for path %q was %q", cacheTargetPath, sum) - - sumDBContents := fmt.Sprintf("%s %s %s\n", remote.GetSource(), remote.GetVersion(), sum) - _, err = fmt.Fprint(sumDBFile, sumDBContents) - if err != nil { - return "", fmt.Errorf("writing to cache file %q: %w", cacheTargetPath, err) + return "", fmt.Errorf("adding %q details to sumdb: %w", cacheTargetPath, err) } return cacheTargetPath, err diff --git a/internal/archive/cache/sumdb.go b/internal/archive/cache/sumdb.go index 3872c95..93be092 100644 --- a/internal/archive/cache/sumdb.go +++ b/internal/archive/cache/sumdb.go @@ -2,37 +2,94 @@ package cache import ( "crypto/sha256" + "database/sql" "encoding/base64" + "errors" "fmt" "io" "os" "path/filepath" "strings" + + "github.com/opensourcecorp/vdm/internal/message" + "github.com/opensourcecorp/vdm/internal/vdmspec" + _ "modernc.org/sqlite" ) -// Caller's job to close file handle -func GetOrCreateSumDBFile() (*os.File, error) { - homedir, err := os.UserHomeDir() +var ( + createStatement = "CREATE" + insertStatement = "INSERT" + dbStatements = map[string]string{ + createStatement: ` + CREATE TABLE IF NOT EXISTS sums ( + key TEXT PRIMARY KEY, + source TEXT, + version TEXT, + sum TEXT UNIQUE + ); + `, + insertStatement: ` + INSERT INTO sums ( + key, source, version, sum + ) VALUES ( + ?, ?, ?, ? + ); + `, + } +) + +func AddToSumDB(remote vdmspec.Remoter, reader io.Reader) (err error) { + sumDBPath, err := getSumDBPath() if err != nil { - return nil, fmt.Errorf("determining user homedir: %w", err) + return fmt.Errorf("getting sumdb path %q: %w", sumDBPath, err) } - sumDBPath := filepath.Join(homedir, ".vdm", "cache", "sumdb.json") - sumDBFile, err := os.OpenFile(sumDBPath, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0644) + db, err := sql.Open("sqlite", sumDBPath) if err != nil { - return nil, fmt.Errorf("creating/opening sumdb file %q: %w", sumDBPath, err) + return fmt.Errorf("opening sumdb path %q: %w", sumDBPath, err) } + defer func() { + if closeErr := db.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing sumdb file %q: %w", sumDBPath, closeErr)) + } + }() - return sumDBFile, err + _, err = db.Exec(dbStatements[createStatement]) + if err != nil { + return fmt.Errorf("creating sums table: %w", err) + } + + sum, err := CalculateSHASum(reader) + if err != nil { + return fmt.Errorf("calculating SHA sum for remote source %q: %w", remote.GetSource(), err) + } + message.Debugf("sum calculated for remote %q's archive file was %q", remote.GetSource(), sum) + + _, err = db.Exec( + dbStatements[insertStatement], + remote.GetSourceVersionSum(sum), + remote.GetSource(), + remote.GetVersion(), + sum, + ) + if err != nil { + return fmt.Errorf("inserting into sums table: %w", err) + } + + return err } // CalculateSHASum takes an arbitrary [io.Reader] (such as an open // file handle) and calculates the SHA256 checksum for it. func CalculateSHASum(reader io.Reader) (string, error) { + message.Debugf("reader address for calculating SHA sum: %v", reader) hasher := sha256.New() - if _, err := io.Copy(hasher, reader); err != nil { + var n int64 + var err error + if n, err = io.Copy(hasher, reader); err != nil { return "", fmt.Errorf("writing reader to hasher: %w", err) } + message.Debugf("number of bytes copied to hasher: %d", n) sum := fmt.Sprintf("%x", hasher.Sum(nil)) return sum, nil } @@ -58,6 +115,15 @@ func StringFromBase64(s string) (string, error) { return string(out), nil } +func getSumDBPath() (string, error) { + homedir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("determining user homedir: %w", err) + } + sumDBPath := filepath.Join(homedir, ".vdm", "cache", "sum.db") + return sumDBPath, nil +} + // base64NonAlphaMap maps non-alphanumeric base-64 characters to their arbitrary // replacement values var base64NonAlphaMap = map[string]string{ diff --git a/internal/remotes/file.go b/internal/remotes/file.go index 19ec219..31cdeff 100644 --- a/internal/remotes/file.go +++ b/internal/remotes/file.go @@ -57,7 +57,7 @@ func (remote File) Sync(src, dest string) error { return fmt.Errorf("creating parent directories for file %q: %w", dest, err) } - err = archive.ExtractArchiveToDestination(src, dest) + err = archive.ExtractTGZArchive(src, dest) if err != nil { return fmt.Errorf("syncing file cache for remote %q: %w", remote.GetSource(), err) } @@ -74,6 +74,17 @@ func (remote File) GetVersion() string { return remote.Version } +// GetVersion returns the Source & Version fields, concatenated with an '@'. +func (remote File) GetSourceVersion() string { + return fmt.Sprintf("%s@%s", remote.Source, remote.Version) +} + +// GetVersion returns the Source & Version fields, as well as the passed +// checksum, concatenated with '@'s. +func (remote File) GetSourceVersionSum(sum string) string { + return fmt.Sprintf("%s@%s@%s", remote.Source, remote.Version, sum) +} + func checkFileExists(remote File) (bool, error) { fullPath, err := filepath.Abs(remote.Destination) if err != nil { diff --git a/internal/remotes/git.go b/internal/remotes/git.go index fd1dc3b..9eebca9 100644 --- a/internal/remotes/git.go +++ b/internal/remotes/git.go @@ -64,7 +64,7 @@ func (remote Git) Cache() (cachePath string, err error) { // Sync provides the [vdmspec.Remoter.Sync] operations for "git" remote types. func (remote Git) Sync(src, dest string) error { - err := archive.ExtractArchiveToDestination(src, dest) + err := archive.ExtractTGZArchive(src, dest) if err != nil { return fmt.Errorf("syncing git cache for remote %q: %w", remote.GetSource(), err) } @@ -81,6 +81,17 @@ func (remote Git) GetVersion() string { return remote.Version } +// GetVersion returns the Source & Version fields, concatenated with an '@'. +func (remote Git) GetSourceVersion() string { + return fmt.Sprintf("%s@%s", remote.Source, remote.Version) +} + +// GetVersion returns the Source & Version fields, as well as the passed +// checksum, concatenated with '@'s. +func (remote Git) GetSourceVersionSum(sum string) string { + return fmt.Sprintf("%s@%s@%s", remote.Source, remote.Version, sum) +} + func checkGitAvailable() error { cmd := exec.Command("git", "--version") sysOutput, err := cmd.CombinedOutput() diff --git a/internal/vdmspec/spec.go b/internal/vdmspec/spec.go index b77d0d8..7477c6f 100644 --- a/internal/vdmspec/spec.go +++ b/internal/vdmspec/spec.go @@ -35,10 +35,17 @@ type Remoter interface { // Sync should unpack the archive from the cache in VDM_HOME to the // specified destination Sync(src, dest string) error - // GetRemote returns the [RemoteTemplate.Source] value + // GetRemote should return the [RemoteTemplate.Source] value GetSource() string - // GetRemote returns the [RemoteTemplate.Version] value + // GetRemote should return the [RemoteTemplate.Version] value GetVersion() string + // GetRemoteVersion should concatenate the [RemoteTemplate.Source] and + // [RemoteTemplate.Version] values, separated by an '@' symbol + GetSourceVersion() string + // GetRemoteSourceVersionSum should concatenate the [RemoteTemplate.Source], + // [RemoteTemplate.Version], and computed checksum values, separated by '@' + // symbols + GetSourceVersionSum(sum string) string } // RemoteTemplate defines the template structure for each potential remote From e75ed4167c7a902e24ed297096a4f905f9bdf3c8 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 18 Aug 2024 16:34:02 -0500 Subject: [PATCH 13/17] WIP as I move some things around for cache-checks --- cmd/root.go | 13 ++++ internal/archive/archive.go | 22 ++++--- internal/archive/archive_test.go | 7 +- internal/archive/cache/cache.go | 29 ++++----- internal/archive/cache/sumdb.go | 96 +++++++++++++++++++++++----- internal/archive/cache/sumdb_test.go | 11 +--- 6 files changed, 125 insertions(+), 53 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index edc0abe..17d6f52 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,7 +3,10 @@ package cmd import ( "errors" "fmt" + "os" + "github.com/opensourcecorp/vdm/cmd/vars" + "github.com/opensourcecorp/vdm/internal/archive/cache" "github.com/opensourcecorp/vdm/internal/message" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -67,6 +70,16 @@ func executeRootCommand(cmd *cobra.Command, args []string) error { } } + err := os.MkdirAll(vars.GetVDMCacheDir(), 0755) + if err != nil { + return fmt.Errorf("creating vdm cache directory %q: %w", vars.GetVDMCacheDir(), err) + } + + err = cache.CreateSumDB() + if err != nil { + return fmt.Errorf("creating sumdb before any vdm operations: %w", err) + } + return errors.New("You must provide a subcommand to vdm") } diff --git a/internal/archive/archive.go b/internal/archive/archive.go index ddb8e8c..b21f439 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -27,27 +27,31 @@ var ( // CreateArchive writes a gzipped tarball based on the provided root directory // from which to construct the archive, and its target file name. It returns an // open file handle to the archive, which should be closed by the caller. -func CreateArchive(root string, archivePath string) (f *os.File, err error) { +func CreateArchive(root string, archivePath string) (err error) { if !tgzRegex.MatchString(archivePath) { - return nil, errors.New("provided archive path must have valid gzipped-tar extension") + return errors.New("provided archive path must have valid gzipped-tar extension") } rootAbs, err := filepath.Abs(root) if err != nil { - return nil, fmt.Errorf("determining abspath of provided root dir %q: %w", root, err) + return fmt.Errorf("determining abspath of provided root dir %q: %w", root, err) } message.Debugf("root: %q, rootAbs: %q", root, rootAbs) archivePathAbs, err := filepath.Abs(archivePath) if err != nil { - return nil, fmt.Errorf("determining abspath of provided archive path %q: %w", archivePath, err) + return fmt.Errorf("determining abspath of provided archive path %q: %w", archivePath, err) } buf, err := os.Create(archivePathAbs) if err != nil { - return nil, fmt.Errorf("opening target archive path: %w", err) + return fmt.Errorf("opening target archive path %q: %w", archivePathAbs, err) } - // NOTE: file not closed because it's returned, open, to the caller + defer func() { + if closeErr := buf.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing archive file buffer from %q: %w", archivePathAbs, closeErr)) + } + }() // gzip and tar get their own writers, and note that they are chained -- tar // writes to gzip, which writes to the buffer. So, we only need to write to @@ -68,17 +72,17 @@ func CreateArchive(root string, archivePath string) (f *os.File, err error) { files, err := filetree.GetFilePathsInDirectory(rootAbs) if err != nil { - return nil, fmt.Errorf("populating list of files from %q: %w", root, err) + return fmt.Errorf("populating list of files from %q: %w", root, err) } for _, fileName := range files { err := addToArchive(tarWriter, rootAbs, fileName) if err != nil { - return nil, fmt.Errorf("adding %q to archive: %w", fileName, err) + return fmt.Errorf("adding %q to archive: %w", fileName, err) } } - return buf, err + return err } func ExtractTGZArchive(src, dest string) error { diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 92cde0d..bfefd66 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -17,13 +17,8 @@ func TestCreateArchive(t *testing.T) { require.NoError(t, err) }) - unneededFile, err := CreateArchive(archiveRoot, archivePath) + err := CreateArchive(archiveRoot, archivePath) require.NoError(t, err) - // Need to close the returned file because that's the caller's job - t.Cleanup(func() { - err = unneededFile.Close() - require.NoError(t, err) - }) // TODO: add test for inspecting contents once we implement an archive // extractor -- as of now, I'm just checking on the CLI if the expected tree diff --git a/internal/archive/cache/cache.go b/internal/archive/cache/cache.go index 07ca507..e2fcf6a 100644 --- a/internal/archive/cache/cache.go +++ b/internal/archive/cache/cache.go @@ -15,32 +15,31 @@ import ( // AddRemote uses the provided [vdmspec.Remoter] information along with a file // handle for a target archive to actually write the archive data. TODO fix this func AddRemote(remote vdmspec.Remoter, cacheRoot string) (cachePath string, err error) { - err = os.MkdirAll(vars.GetVDMCacheDir(), 0755) - if err != nil { - return "", fmt.Errorf("creating vdm cache directory %q: %w", vars.GetVDMCacheDir(), err) - } - b64 := StringToBase64(remote.GetSourceVersion()) - if err != nil { - return "", fmt.Errorf("calculating sum when caching remote %q: %w", remote.GetSourceVersion(), err) - } message.Debugf("remote %q base64'd to %q", remote.GetSourceVersion(), b64) cacheFileName := b64 + ".tar.gz" cacheTargetPath := filepath.Join(vars.GetVDMCacheDir(), cacheFileName) - cacheTarget, err := archive.CreateArchive(cacheRoot, cacheTargetPath) + remoteAlreadyInSumDB, err := CheckIfRemoteInSumDB(remote) if err != nil { - return "", fmt.Errorf("creating archive while adding remote %q: %w", remote.GetSourceVersion(), err) + return "", fmt.Errorf("checking if remote %q already in sumdb: %w", remote.GetSourceVersion(), err) } - defer cacheTarget.Close() - err = AddToSumDB(remote, cacheTarget) - if err != nil { - return "", fmt.Errorf("adding %q details to sumdb: %w", cacheTargetPath, err) + if !remoteAlreadyInSumDB { + message.Infof("Remote %q already found in local cache, will retrieve from there") + err = archive.CreateArchive(cacheRoot, cacheTargetPath) + if err != nil { + return "", fmt.Errorf("creating archive while adding remote %q: %w", remote.GetSourceVersion(), err) + } + + err = AddToSumDB(remote, cacheTargetPath) + if err != nil { + return "", fmt.Errorf("adding %q details to sumdb: %w", cacheTargetPath, err) + } } - return cacheTargetPath, err + return cacheTargetPath, nil } func GetTempCachePath(remote vdmspec.Remoter) string { diff --git a/internal/archive/cache/sumdb.go b/internal/archive/cache/sumdb.go index 93be092..5f04773 100644 --- a/internal/archive/cache/sumdb.go +++ b/internal/archive/cache/sumdb.go @@ -17,28 +17,33 @@ import ( ) var ( - createStatement = "CREATE" - insertStatement = "INSERT" - dbStatements = map[string]string{ - createStatement: ` - CREATE TABLE IF NOT EXISTS sums ( - key TEXT PRIMARY KEY, + tableName = "sums" + createStatement = "CREATE" + insertStatement = "INSERT" + checkKeyExistsStatement = "CHECK_EXISTS" + dbStatements = map[string]string{ + createStatement: fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS %s ( + key TEXT UNIQUE PRIMARY KEY, source TEXT, version TEXT, sum TEXT UNIQUE ); - `, - insertStatement: ` - INSERT INTO sums ( + `, tableName), + insertStatement: fmt.Sprintf(` + INSERT INTO %s ( key, source, version, sum ) VALUES ( ?, ?, ?, ? ); - `, + `, tableName), + checkKeyExistsStatement: fmt.Sprintf(` + SELECT COUNT(*) FROM %s + `, tableName), } ) -func AddToSumDB(remote vdmspec.Remoter, reader io.Reader) (err error) { +func CreateSumDB() (err error) { sumDBPath, err := getSumDBPath() if err != nil { return fmt.Errorf("getting sumdb path %q: %w", sumDBPath, err) @@ -59,7 +64,36 @@ func AddToSumDB(remote vdmspec.Remoter, reader io.Reader) (err error) { return fmt.Errorf("creating sums table: %w", err) } - sum, err := CalculateSHASum(reader) + return err +} + +func AddToSumDB(remote vdmspec.Remoter, cacheTargetPath string) (err error) { + sumDBPath, err := getSumDBPath() + if err != nil { + return fmt.Errorf("getting sumdb path %q: %w", sumDBPath, err) + } + + db, err := sql.Open("sqlite", sumDBPath) + if err != nil { + return fmt.Errorf("opening sumdb path %q: %w", sumDBPath, err) + } + defer func() { + if closeErr := db.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing sumdb file %q: %w", sumDBPath, closeErr)) + } + }() + + f, err := os.Open(cacheTargetPath) + if err != nil { + return fmt.Errorf("opening cache target path %q for hashing: %w", cacheTargetPath, err) + } + defer func() { + if closeErr := f.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing cache target file %q: %w", cacheTargetPath, closeErr)) + } + }() + + sum, err := calculateSHASum(f) if err != nil { return fmt.Errorf("calculating SHA sum for remote source %q: %w", remote.GetSource(), err) } @@ -67,7 +101,7 @@ func AddToSumDB(remote vdmspec.Remoter, reader io.Reader) (err error) { _, err = db.Exec( dbStatements[insertStatement], - remote.GetSourceVersionSum(sum), + remote.GetSourceVersion(), remote.GetSource(), remote.GetVersion(), sum, @@ -79,9 +113,41 @@ func AddToSumDB(remote vdmspec.Remoter, reader io.Reader) (err error) { return err } -// CalculateSHASum takes an arbitrary [io.Reader] (such as an open +func CheckIfRemoteInSumDB(remote vdmspec.Remoter) (hasKey bool, err error) { + sumDBPath, err := getSumDBPath() + if err != nil { + return false, fmt.Errorf("getting sumdb path %q: %w", sumDBPath, err) + } + + db, err := sql.Open("sqlite", sumDBPath) + if err != nil { + return false, fmt.Errorf("opening sumdb path %q: %w", sumDBPath, err) + } + defer func() { + if closeErr := db.Close(); closeErr != nil { + err = errors.Join(err, fmt.Errorf("closing sumdb file %q: %w", sumDBPath, closeErr)) + } + }() + + var numRows int + err = db.QueryRow(dbStatements[checkKeyExistsStatement]).Scan(&numRows) + if err != nil { + return false, fmt.Errorf("querying sumdb: %w", err) + } + message.Debugf("number of results from sumdb for remote key %q: %d", remote.GetSourceVersion(), numRows) + + message.Debugf("sumdb query result not yet checked for remote key %q, hasKey: %v", remote.GetSourceVersion(), hasKey) + if numRows > 0 { + hasKey = true + } + message.Debugf("sumdb query result now checked for remote key %q, hasKey: %v", remote.GetSourceVersion(), hasKey) + + return hasKey, err +} + +// calculateSHASum takes an arbitrary [io.Reader] (such as an open // file handle) and calculates the SHA256 checksum for it. -func CalculateSHASum(reader io.Reader) (string, error) { +func calculateSHASum(reader io.Reader) (string, error) { message.Debugf("reader address for calculating SHA sum: %v", reader) hasher := sha256.New() var n int64 diff --git a/internal/archive/cache/sumdb_test.go b/internal/archive/cache/sumdb_test.go index 813aed8..45a5d04 100644 --- a/internal/archive/cache/sumdb_test.go +++ b/internal/archive/cache/sumdb_test.go @@ -20,7 +20,7 @@ func TestCalculateSHASum(t *testing.T) { }) want := `25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b` - got, err := CalculateSHASum(f) + got, err := calculateSHASum(f) assert.NoError(t, err) assert.Equal(t, want, got) @@ -29,13 +29,8 @@ func TestCalculateSHASum(t *testing.T) { t.Run("works on a created archive file", func(t *testing.T) { rootDir := "../../../testdata/filetree" archivePath := filepath.Join(os.TempDir(), "archive-to-hash.tar.gz") - unneededFile, err := archive.CreateArchive(rootDir, archivePath) + err := archive.CreateArchive(rootDir, archivePath) require.NoError(t, err) - // Need to close the returned file because that's the caller's job - t.Cleanup(func() { - err = unneededFile.Close() - require.NoError(t, err) - }) f, err := os.Open(archivePath) require.NoError(t, err) @@ -45,7 +40,7 @@ func TestCalculateSHASum(t *testing.T) { }) want := `c1c5e8e5cd54819ea0243db2b203e581aae7308937fb575204914fc2e41ef2a7` - got, err := CalculateSHASum(f) + got, err := calculateSHASum(f) assert.NoError(t, err) assert.Equal(t, want, got) From c4310c275908610f08ff66553fd18a9bba64d566 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Mon, 19 Aug 2024 23:51:02 -0500 Subject: [PATCH 14/17] Cooking now but for some reason tests fail unless cached?? --- Makefile | 5 - cmd/flagsupport.go | 11 -- cmd/root.go | 15 +- cmd/sync.go | 52 ++---- cmd/sync_test.go | 85 +++++----- cmd/vars/vars.go | 30 ++-- internal/archive/archive_test.go | 9 - internal/archive/cache/cache.go | 46 ++--- .../archive/cache/cachetest/cache_test.go | 48 ------ internal/archive/cache/cachetest/doc.go | 4 - internal/archive/cache/sumdb.go | 26 ++- internal/remotes/file.go | 158 ------------------ internal/remotes/file_test.go | 7 - internal/remotes/git.go | 69 ++++---- internal/remotes/git_test.go | 62 ------- internal/vdminit/init.go | 28 ++++ internal/vdminit/init_test.go | 53 ++++++ internal/vdminit/testhelpers.go | 37 ++++ internal/vdmspec/spec.go | 99 +++-------- internal/vdmspec/spec_test.go | 74 -------- internal/vdmspec/validate.go | 7 - testdata/vdm.yaml | 4 - 22 files changed, 297 insertions(+), 632 deletions(-) delete mode 100644 internal/archive/cache/cachetest/cache_test.go delete mode 100644 internal/archive/cache/cachetest/doc.go delete mode 100644 internal/remotes/file.go delete mode 100644 internal/remotes/file_test.go create mode 100644 internal/vdminit/init.go create mode 100644 internal/vdminit/init_test.go create mode 100644 internal/vdminit/testhelpers.go diff --git a/Makefile b/Makefile index 554d7d4..c59f0a9 100644 --- a/Makefile +++ b/Makefile @@ -44,11 +44,6 @@ clean: ./dist/debian/vdm.deb \ *.out @sudo rm -rf ./dist/debian/vdm/usr -# TODO: until I sort out the tests to write test data consistently, these deps/ -# directories etc. can kind of show up anywhere, so we need to find & delete all -# of them - @find . -type d -name '*deps*' -exec rm -rf {} + - @find . -type f -name '*VDMMETA*' -delete bump-versions: clean @bash ./scripts/bump-versions.sh "$${old_version:-}" diff --git a/cmd/flagsupport.go b/cmd/flagsupport.go index 0f1e888..053a8cf 100644 --- a/cmd/flagsupport.go +++ b/cmd/flagsupport.go @@ -18,14 +18,3 @@ func maybeSetDebug() { } } } - -// maybeTryLocalSources sets the TRY_LOCAL_SOURCES environment variable if it -// was set as a flag by the caller. -func maybeTryLocalSources() { - if viper.GetBool(tryLocalSourcesFlagKey) { - err := os.Setenv(vars.TryLocalSources, "true") - if err != nil { - message.Fatalf("internal error: unable to set environment variable %s", vars.TryLocalSources) - } - } -} diff --git a/cmd/root.go b/cmd/root.go index 17d6f52..fb89f89 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,7 @@ import ( "github.com/opensourcecorp/vdm/cmd/vars" "github.com/opensourcecorp/vdm/internal/archive/cache" "github.com/opensourcecorp/vdm/internal/message" + "github.com/opensourcecorp/vdm/internal/vdminit" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -70,9 +71,19 @@ func executeRootCommand(cmd *cobra.Command, args []string) error { } } - err := os.MkdirAll(vars.GetVDMCacheDir(), 0755) + err := vdminit.Paths() if err != nil { - return fmt.Errorf("creating vdm cache directory %q: %w", vars.GetVDMCacheDir(), err) + return fmt.Errorf("initializing vdm: %w", err) + } + + vdmCacheDir, err := vars.GetVDMCacheDir() + if err != nil { + return fmt.Errorf("determining vdm cache directory while running root command: %w", err) + } + + err = os.MkdirAll(vdmCacheDir, 0755) + if err != nil { + return fmt.Errorf("creating vdm cache directory %q: %w", vdmCacheDir, err) } err = cache.CreateSumDB() diff --git a/cmd/sync.go b/cmd/sync.go index 7486e18..52f8a6c 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -7,9 +7,9 @@ import ( "github.com/opensourcecorp/vdm/internal/message" "github.com/opensourcecorp/vdm/internal/remotes" + "github.com/opensourcecorp/vdm/internal/vdminit" "github.com/opensourcecorp/vdm/internal/vdmspec" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // syncFlags defines the CLI flags for the sync subcommand. @@ -21,11 +21,6 @@ type syncFlags struct { // values. var syncFlagValues syncFlags -// Flag name keys -const ( - tryLocalSourcesFlagKey string = "try-local-sources" -) - func newSyncCommand() *cobra.Command { cmd := &cobra.Command{ Use: "sync", @@ -33,21 +28,21 @@ func newSyncCommand() *cobra.Command { RunE: executeSyncSubCommand, } - cmd.Flags().BoolVar(&syncFlagValues.TryLocalSources, tryLocalSourcesFlagKey, false, "Whether to try & process local copies of sources before retrieving their remote copies") - err := viper.BindPFlag(tryLocalSourcesFlagKey, cmd.Flags().Lookup(tryLocalSourcesFlagKey)) - if err != nil { - message.Fatalf("internal error: unable to bind state of flag --%s: %v", tryLocalSourcesFlagKey, err) - } - return cmd } func executeSyncSubCommand(_ *cobra.Command, _ []string) error { maybeSetDebug() - maybeTryLocalSources() + + err := vdminit.Paths() + if err != nil { + return fmt.Errorf("initializing vdm: %w", err) + } + if err := sync(); err != nil { return fmt.Errorf("executing sync command: %w", err) } + return nil } @@ -65,31 +60,14 @@ func sync() error { } for _, remote := range spec.Remotes { - // TODO: add this back, but it's unused right now - // // process stored vdm metafile so we know what operations to actually - // // perform for existing directories - // vdmMeta, err := remote.GetVDMMeta() - // if err != nil { - // return fmt.Errorf("getting vdm metadata file for sync: %w", err) - // } - - // if vdmMeta == (vdmspec.RemoteTemplate{}) { - // message.Infof("%s: %s not found at local path, will be created", remote.OpMsg(), vdmspec.MetaFileName) - // } else { - // if vdmMeta.Version != remote.Version && vdmMeta.Source != remote.Source { - // message.Infof("%s: Will change %q from current local version spec %q to %q...", remote.OpMsg(), remote.Source, vdmMeta.Version, remote.Version) - // panic("jk not implemented") - // } - // message.Infof("%s: version unchanged in spec file, skipping", remote.OpMsg()) - // continue - // } - var determinedRemote vdmspec.Remoter switch remote.Type { case vdmspec.GitType, "": determinedRemote = remotes.Git{RemoteTemplate: remote} - case vdmspec.FileType: - return errors.New("cannot process 'file' remote types, as they are not yet fully implemented") + case vdmspec.ArchiveType: + return errors.New("cannot process 'archive' remote types, as they are not yet fully implemented") + case vdmspec.LocalType: + return errors.New("cannot process 'local' remote types, as they are not yet fully implemented") default: return fmt.Errorf("unrecognized remote type %q", remote.Type) } @@ -109,12 +87,6 @@ func sync() error { return fmt.Errorf("syncing %q remote: %w", remote.Type, err) } - // TODO: add back, as above - // err = remote.WriteVDMMeta() - // if err != nil { - // return fmt.Errorf("could not write %s file to disk: %w", vdmspec.MetaFileName, err) - // } - remote.OpMsg("Done.") } diff --git a/cmd/sync_test.go b/cmd/sync_test.go index 6fd25a3..f65e203 100644 --- a/cmd/sync_test.go +++ b/cmd/sync_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/opensourcecorp/vdm/internal/vdmspec" + "github.com/opensourcecorp/vdm/internal/vdminit" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -17,56 +17,49 @@ var ( ) func TestSync(t *testing.T) { - spec, err := vdmspec.GetSpecFromFile(testSpecFilePath) - require.NoError(t, err) + _, cleanup := vdminit.SetupVDMForTest(t) - // Need to override for test - rootFlagValues.SpecFilePath = testSpecFilePath - err = sync() + // This runs as part of the outer test container, because we want to inspect + // the filesystem state as we go + t.Cleanup(cleanup) - assert.NoError(t, err) - - t.Cleanup(func() { - for _, remote := range spec.Remotes { - err := os.RemoveAll(remote.Destination) - require.NoError(t, err) - } + cmd := newRootCommand() + cmd.SetArgs([]string{ + "--debug", + "--specfile-path", testSpecFilePath, + "sync", }) - // TODO: the following tests relied on VDMMETA-checks, which are now - // unimplemented until I figure out how I want to manage those in the future - - // t.Run("SyncGit", func(t *testing.T) { - // t.Run("remotes[0] used a tag", func(t *testing.T) { - // vdmMeta, err := spec.Remotes[0].GetVDMMeta() - // require.NoError(t, err) - // assert.Equal(t, "v0.2.0", vdmMeta.Version) - // }) - - // t.Run("remotes[1] used 'latest'", func(t *testing.T) { - // vdmMeta, err := spec.Remotes[1].GetVDMMeta() - // require.NoError(t, err) - // assert.Equal(t, "latest", vdmMeta.Version) - // }) + t.Run("sync works without throwing any errors", func(t *testing.T) { + err := cmd.Execute() + assert.NoError(t, err) + }) - // t.Run("remotes[2] used a branch", func(t *testing.T) { - // vdmMeta, err := spec.Remotes[2].GetVDMMeta() - // require.NoError(t, err) - // assert.Equal(t, "main", vdmMeta.Version) - // }) + t.Run("filesytem state is as expected", func(t *testing.T) { + expectedGitDirs := map[string]string{ + "git-tag": "vdm", + "git-branch": "osc-infra", + "git-hash": "go-common", + } + for topDir, secondDir := range expectedGitDirs { + sourceRoot := filepath.Join("deps", topDir, secondDir) + t.Run("source directory exists at its destination", func(t *testing.T) { + gitTagSource, err := os.Stat(sourceRoot) + require.NoError(t, err) + assert.True(t, gitTagSource.IsDir()) + }) - // t.Run("remotes[3] used a hash", func(t *testing.T) { - // vdmMeta, err := spec.Remotes[3].GetVDMMeta() - // require.NoError(t, err) - // assert.Equal(t, "2e6657f5ac013296167c4dd92fbb46f0e3dbdc5f", vdmMeta.Version) - // }) - // }) + t.Run(".git directory was removed", func(t *testing.T) { + dotGitDir := filepath.Join(sourceRoot, ".git") + _, err := os.Stat(dotGitDir) + assert.ErrorIs(t, err, os.ErrNotExist) + }) - // t.Run("SyncFile", func(t *testing.T) { - // t.Run("remotes[4] had an implicit version", func(t *testing.T) { - // vdmMeta, err := spec.Remotes[4].GetVDMMeta() - // require.NoError(t, err) - // assert.Equal(t, "", vdmMeta.Version) - // }) - // }) + t.Run("a known file in the remote exists, and is a file", func(t *testing.T) { + readmePath := filepath.Join(sourceRoot, "README.md") + _, err := os.Stat(readmePath) + assert.NoError(t, err) + }) + } + }) } diff --git a/cmd/vars/vars.go b/cmd/vars/vars.go index d877dc8..85ea435 100644 --- a/cmd/vars/vars.go +++ b/cmd/vars/vars.go @@ -4,6 +4,7 @@ package vars import ( + "fmt" "os" "path/filepath" ) @@ -17,30 +18,29 @@ const ( VDMHomeEnvVarName = "VDM_HOME" ) -var ( - // VDMHome stores the location of vdm's own home directory - VDMHome string -) - -func init() { +func GetVDMHomeDir() (string, error) { homedir, err := os.UserHomeDir() if err != nil { - panic("unable to determine home directory") + return "", fmt.Errorf("determining home directory: %w", err) } + var vdmHome string vdmHomeOverride, ok := os.LookupEnv(VDMHomeEnvVarName) if ok { - VDMHome = filepath.Join(vdmHomeOverride, ".vdm") + vdmHome = filepath.Join(vdmHomeOverride, ".vdm") } else { - VDMHome = filepath.Join(homedir, ".vdm") - } - err = os.Setenv(VDMHomeEnvVarName, VDMHome) - if err != nil { - panic("unable to set VDM home directory") + vdmHome = filepath.Join(homedir, ".vdm") } + + return vdmHome, nil } // GetVDMCacheDir returns the determined path to vdm's cache directory. -func GetVDMCacheDir() string { - return filepath.Join(os.Getenv(VDMHomeEnvVarName), "cache") +func GetVDMCacheDir() (string, error) { + vdmHome, err := GetVDMHomeDir() + if err != nil { + return "", fmt.Errorf("getting vdm home directory: %w", err) + } + + return filepath.Join(vdmHome, "cache"), nil } diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index bfefd66..e5cc2bb 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -34,13 +34,4 @@ func TestMaybeGetTopLevelDir(t *testing.T) { assert.NoError(t, err) assert.Equal(t, wantTopLevelDir, gotTopLevelDir) }) - - t.Run("works when rootDir is a file", func(t *testing.T) { - rootDir := "../../testdata/filetree/top-file" - wantTopLevelDir := "" - gotTopLevelDir, err := maybeGetTopLevelDir(rootDir) - - assert.NoError(t, err) - assert.Equal(t, wantTopLevelDir, gotTopLevelDir) - }) } diff --git a/internal/archive/cache/cache.go b/internal/archive/cache/cache.go index e2fcf6a..e6e1ad7 100644 --- a/internal/archive/cache/cache.go +++ b/internal/archive/cache/cache.go @@ -13,33 +13,39 @@ import ( ) // AddRemote uses the provided [vdmspec.Remoter] information along with a file -// handle for a target archive to actually write the archive data. TODO fix this -func AddRemote(remote vdmspec.Remoter, cacheRoot string) (cachePath string, err error) { - b64 := StringToBase64(remote.GetSourceVersion()) - message.Debugf("remote %q base64'd to %q", remote.GetSourceVersion(), b64) +// handle for a target archive to actually write the archive data. +func AddRemote(remote vdmspec.Remoter, cacheRoot string) (string, error) { + cachePath, err := GetPersistentCacheFilePath(remote) + if err != nil { + return "", fmt.Errorf("getting cache location for remote %q: %w", remote.GetSumDBKey(), err) + } - cacheFileName := b64 + ".tar.gz" - cacheTargetPath := filepath.Join(vars.GetVDMCacheDir(), cacheFileName) + err = archive.CreateArchive(cacheRoot, cachePath) + if err != nil { + return "", fmt.Errorf("creating archive while adding remote %q: %w", remote.GetSumDBKey(), err) + } - remoteAlreadyInSumDB, err := CheckIfRemoteInSumDB(remote) + err = AddToSumDB(remote, cachePath) if err != nil { - return "", fmt.Errorf("checking if remote %q already in sumdb: %w", remote.GetSourceVersion(), err) + return "", fmt.Errorf("adding %q details to sumdb: %w", cachePath, err) } - if !remoteAlreadyInSumDB { - message.Infof("Remote %q already found in local cache, will retrieve from there") - err = archive.CreateArchive(cacheRoot, cacheTargetPath) - if err != nil { - return "", fmt.Errorf("creating archive while adding remote %q: %w", remote.GetSourceVersion(), err) - } - - err = AddToSumDB(remote, cacheTargetPath) - if err != nil { - return "", fmt.Errorf("adding %q details to sumdb: %w", cacheTargetPath, err) - } + return cachePath, nil +} + +func GetPersistentCacheFilePath(remote vdmspec.Remoter) (string, error) { + b64 := StringToBase64(remote.GetSumDBKey()) + message.Debugf("remote %q base64'd to %q", remote.GetSumDBKey(), b64) + + cacheFileName := b64 + ".tar.gz" + + vdmCacheDir, err := vars.GetVDMCacheDir() + if err != nil { + return "", fmt.Errorf("determining vdm cache directory while adding remote: %w", err) } - return cacheTargetPath, nil + cachePath := filepath.Join(vdmCacheDir, cacheFileName) + return cachePath, nil } func GetTempCachePath(remote vdmspec.Remoter) string { diff --git a/internal/archive/cache/cachetest/cache_test.go b/internal/archive/cache/cachetest/cache_test.go deleted file mode 100644 index a14007e..0000000 --- a/internal/archive/cache/cachetest/cache_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package cachetest - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/opensourcecorp/vdm/cmd/vars" - "github.com/opensourcecorp/vdm/internal/archive/cache" - "github.com/opensourcecorp/vdm/internal/remotes" - "github.com/opensourcecorp/vdm/internal/vdmspec" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCacheRemote(t *testing.T) { - vdmHomePath := filepath.Join(os.TempDir(), "vdmhome") - t.Setenv(vars.VDMHomeEnvVarName, vdmHomePath) - t.Cleanup(func() { - err := os.RemoveAll(vdmHomePath) - require.NoError(t, err) - }) - - f, err := os.Open("../../../../testdata/sumdb/sha256test.txt") - require.NoError(t, err) - t.Cleanup(func() { - err := f.Close() - require.NoError(t, err) - }) - - remote := remotes.Git{ - RemoteTemplate: vdmspec.RemoteTemplate{ - Source: "https://github.com/org/user", - Version: "v1.0.0", - }, - } - cachedPath := filepath.Join(os.Getenv(vars.VDMHomeEnvVarName), "cache", cache.StringToBase64(remote.Source)) - - _, err = cache.AddRemote(remote, "") - assert.NoError(t, err) - - want := fmt.Sprintf("%s %s %s", remote.Source, remote.Version, "25fce0ea957324f2fdab37fa2c35df8dc1c62703b1970a744a723603407b630b") - got, err := os.ReadFile(cachedPath) - require.NoError(t, err) - - assert.Equal(t, want, string(got)) -} diff --git a/internal/archive/cache/cachetest/doc.go b/internal/archive/cache/cachetest/doc.go deleted file mode 100644 index b1c4acf..0000000 --- a/internal/archive/cache/cachetest/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package cachetest exists because Go won't let me have a test for -// [cache.AddRemote] in the cache package because it imports [remotes.Git], -// which causes an import cycle. So, the tests for that are here instead. -package cachetest diff --git a/internal/archive/cache/sumdb.go b/internal/archive/cache/sumdb.go index 5f04773..ec83281 100644 --- a/internal/archive/cache/sumdb.go +++ b/internal/archive/cache/sumdb.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" + "github.com/opensourcecorp/vdm/cmd/vars" "github.com/opensourcecorp/vdm/internal/message" "github.com/opensourcecorp/vdm/internal/vdmspec" _ "modernc.org/sqlite" @@ -38,7 +39,10 @@ var ( ); `, tableName), checkKeyExistsStatement: fmt.Sprintf(` - SELECT COUNT(*) FROM %s + SELECT COUNT(*) + FROM %s + WHERE key = ? + ; `, tableName), } ) @@ -101,7 +105,7 @@ func AddToSumDB(remote vdmspec.Remoter, cacheTargetPath string) (err error) { _, err = db.Exec( dbStatements[insertStatement], - remote.GetSourceVersion(), + remote.GetSumDBKey(), remote.GetSource(), remote.GetVersion(), sum, @@ -130,17 +134,17 @@ func CheckIfRemoteInSumDB(remote vdmspec.Remoter) (hasKey bool, err error) { }() var numRows int - err = db.QueryRow(dbStatements[checkKeyExistsStatement]).Scan(&numRows) + err = db.QueryRow(dbStatements[checkKeyExistsStatement], remote.GetSumDBKey()).Scan(&numRows) if err != nil { return false, fmt.Errorf("querying sumdb: %w", err) } - message.Debugf("number of results from sumdb for remote key %q: %d", remote.GetSourceVersion(), numRows) + message.Debugf("number of results from sumdb for remote key %q: %d", remote.GetSumDBKey(), numRows) - message.Debugf("sumdb query result not yet checked for remote key %q, hasKey: %v", remote.GetSourceVersion(), hasKey) + message.Debugf("sumdb query result not yet checked for remote key %q, hasKey: %v", remote.GetSumDBKey(), hasKey) if numRows > 0 { hasKey = true } - message.Debugf("sumdb query result now checked for remote key %q, hasKey: %v", remote.GetSourceVersion(), hasKey) + message.Debugf("sumdb query result now checked for remote key %q, hasKey: %v", remote.GetSumDBKey(), hasKey) return hasKey, err } @@ -182,11 +186,15 @@ func StringFromBase64(s string) (string, error) { } func getSumDBPath() (string, error) { - homedir, err := os.UserHomeDir() + vdmCachePath, err := vars.GetVDMCacheDir() if err != nil { - return "", fmt.Errorf("determining user homedir: %w", err) + return "", fmt.Errorf("determining vdm cache path during sumdb creation: %w", err) } - sumDBPath := filepath.Join(homedir, ".vdm", "cache", "sum.db") + + message.Debugf("vdm cache directory during sumdb path determination was %q", vdmCachePath) + sumDBPath := filepath.Join(vdmCachePath, "sum.db") + message.Debugf("sumdb path to be used: %q", sumDBPath) + return sumDBPath, nil } diff --git a/internal/remotes/file.go b/internal/remotes/file.go deleted file mode 100644 index 31cdeff..0000000 --- a/internal/remotes/file.go +++ /dev/null @@ -1,158 +0,0 @@ -package remotes - -import ( - "errors" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - - "github.com/opensourcecorp/vdm/internal/archive" - "github.com/opensourcecorp/vdm/internal/archive/cache" - "github.com/opensourcecorp/vdm/internal/message" - "github.com/opensourcecorp/vdm/internal/vdmspec" -) - -// File defines the file remote type -type File struct { - vdmspec.RemoteTemplate -} - -// Cache provides the [vdmspec.Remoter.Cache] operations for "file" remote types. -func (remote File) Cache() (cachePath string, err error) { - tmpCachePath := cache.GetTempCachePath(remote) - message.Debugf("tmpCachePath: %q", tmpCachePath) - defer func() { - if rmErr := os.RemoveAll(filepath.Dir(tmpCachePath)); rmErr != nil { - err = errors.Join(err, fmt.Errorf("removing temporary cache path %q: %w", tmpCachePath, rmErr)) - } - }() - - err = ensureParentDirs(tmpCachePath) - if err != nil { - return "", fmt.Errorf("creating parent temp cache directories for file remote %q: %w", tmpCachePath, err) - } - - remote.OpMsg("Retrieving...") - err = retrieveFile(remote, tmpCachePath) - if err != nil { - return "", fmt.Errorf("retrieving file: %w", err) - } - - cachePath, err = cache.AddRemote(remote, tmpCachePath) - if err != nil { - return "", fmt.Errorf("caching file remote %q: %w", remote.Source, err) - } - - remote.OpMsg("Done.") - return cachePath, err -} - -// Sync provides the [vdmspec.Remoter.Sync] operations for "file" remote types. -func (remote File) Sync(src, dest string) error { - // We want to make sure the parent directories exist for the real destination, not the temp cache - err := ensureParentDirs(remote.Destination) - if err != nil { - return fmt.Errorf("creating parent directories for file %q: %w", dest, err) - } - - err = archive.ExtractTGZArchive(src, dest) - if err != nil { - return fmt.Errorf("syncing file cache for remote %q: %w", remote.GetSource(), err) - } - return nil -} - -// GetSource returns the Source field. -func (remote File) GetSource() string { - return remote.Source -} - -// GetVersion returns the Version field. -func (remote File) GetVersion() string { - return remote.Version -} - -// GetVersion returns the Source & Version fields, concatenated with an '@'. -func (remote File) GetSourceVersion() string { - return fmt.Sprintf("%s@%s", remote.Source, remote.Version) -} - -// GetVersion returns the Source & Version fields, as well as the passed -// checksum, concatenated with '@'s. -func (remote File) GetSourceVersionSum(sum string) string { - return fmt.Sprintf("%s@%s@%s", remote.Source, remote.Version, sum) -} - -func checkFileExists(remote File) (bool, error) { - fullPath, err := filepath.Abs(remote.Destination) - if err != nil { - return false, fmt.Errorf("determining abspath for file %q: %w", remote.Destination, err) - } - - _, err = os.Stat(remote.Destination) - if errors.Is(err, os.ErrNotExist) { - return false, nil - } else if err != nil { - return false, fmt.Errorf("couldn't check if %q exists at %q: %w", remote.Destination, fullPath, err) - } - - return true, nil -} - -func retrieveFile(remote File, dest string) (err error) { - resp, err := http.Get(remote.Source) - if err != nil { - return fmt.Errorf("retrieving remote file %q: %w", remote.Source, err) - } - defer func() { - if closeErr := resp.Body.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("closing response body after remote file %q retrieval: %w", remote.Source, err)) - } - }() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unsuccessful status code '%d' from server when retrieving remote file %q", resp.StatusCode, remote.Source) - } - - // Note: I would normally use os.WriteFile() using the returned bytes - // directly, but the internet says this os.Create()/io.Copy() approach - // appears to be idiomatic - message.Debugf("landing file to be created at %q", dest) - outFile, err := os.Create(dest) - if err != nil { - return fmt.Errorf("creating landing file %q for remote file: %w", dest, err) - } - defer func() { - if closeErr := outFile.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("closing local file %q after remote file %q retrieval: %w", dest, remote.Source, err)) - } - }() - - bytesWritten, err := io.Copy(outFile, resp.Body) - if err != nil { - return fmt.Errorf("copying HTTP response to disk: ") - } - message.Debugf("wrote %d bytes to %q", bytesWritten, dest) - - return nil -} - -func ensureParentDirs(path string) error { - fullPath, err := filepath.Abs(path) - if err != nil { - return fmt.Errorf("determining abspath for file %q: %w", path, err) - } - message.Debugf("absolute filepath for %q determined to be %q", path, fullPath) - dir := filepath.Dir(fullPath) - err = os.MkdirAll(dir, os.ModePerm) - if err != nil { - return fmt.Errorf("making directories: %w", err) - } - message.Debugf("created director(ies): %q", dir) - - return nil -} - -var _ vdmspec.Remoter = File{} diff --git a/internal/remotes/file_test.go b/internal/remotes/file_test.go deleted file mode 100644 index 7169710..0000000 --- a/internal/remotes/file_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package remotes - -import "testing" - -func TestSync(t *testing.T) { - t.Fatal("not implemented") -} diff --git a/internal/remotes/git.go b/internal/remotes/git.go index 9eebca9..1290ae8 100644 --- a/internal/remotes/git.go +++ b/internal/remotes/git.go @@ -29,34 +29,47 @@ func (remote Git) Cache() (cachePath string, err error) { } }() - remote.OpMsg("Retrieving...") - err = gitClone(remote.Source, tmpCachePath) + remoteAlreadyInSumDB, err := cache.CheckIfRemoteInSumDB(remote) if err != nil { - return "", fmt.Errorf("cloning git repository: %w", err) + return "", fmt.Errorf("checking if remote %q already in sumdb: %w", remote.GetSumDBKey(), err) } - defer func() { - if rmErr := os.RemoveAll(tmpCachePath); rmErr != nil { - err = errors.Join(err, fmt.Errorf("removing temporary cache directory for %q: %w", remote.GetSource(), rmErr)) - } - }() - remote.OpMsg("Setting specified version...") - checkoutCmd := exec.Command("git", "-C", tmpCachePath, "checkout", remote.Version) - checkoutOutput, err := checkoutCmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("error checking out specified revision: exec error '%w', with output: %s", err, string(checkoutOutput)) - } + if !remoteAlreadyInSumDB { + remote.OpMsg("Retrieving...") + err = gitClone(remote.Source, tmpCachePath) + if err != nil { + return "", fmt.Errorf("cloning git repository: %w", err) + } + defer func() { + if rmErr := os.RemoveAll(tmpCachePath); rmErr != nil { + err = errors.Join(err, fmt.Errorf("removing temporary cache directory for %q: %w", remote.GetSource(), rmErr)) + } + }() + + remote.OpMsg("Setting specified version...") + checkoutCmd := exec.Command("git", "-C", tmpCachePath, "checkout", remote.Version) + checkoutOutput, err := checkoutCmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("error checking out specified revision: exec error '%w', with output: %s", err, string(checkoutOutput)) + } - message.Debugf("removing .git dir for local path %q", tmpCachePath) - dotGitPath := filepath.Join(tmpCachePath, ".git") - err = os.RemoveAll(dotGitPath) - if err != nil { - return "", fmt.Errorf("removing directory %q: %w", dotGitPath, err) - } + message.Debugf("removing .git dir for local path %q", tmpCachePath) + dotGitPath := filepath.Join(tmpCachePath, ".git") + err = os.RemoveAll(dotGitPath) + if err != nil { + return "", fmt.Errorf("removing directory %q: %w", dotGitPath, err) + } - cachePath, err = cache.AddRemote(remote, tmpCachePath) - if err != nil { - return "", fmt.Errorf("caching git remote %q: %w", remote.Source, err) + cachePath, err = cache.AddRemote(remote, tmpCachePath) + if err != nil { + return "", fmt.Errorf("caching git remote %q: %w", remote.Source, err) + } + } else { + remote.OpMsg("Remote found in local cache; restoring...") + cachePath, err = cache.GetPersistentCacheFilePath(remote) + if err != nil { + return "", fmt.Errorf("getting existing cache path for remote %q: %w", remote.GetSumDBKey(), err) + } } return cachePath, err @@ -81,17 +94,11 @@ func (remote Git) GetVersion() string { return remote.Version } -// GetVersion returns the Source & Version fields, concatenated with an '@'. -func (remote Git) GetSourceVersion() string { +// GetSumDBKey returns the Source & Version fields, concatenated with an '@'. +func (remote Git) GetSumDBKey() string { return fmt.Sprintf("%s@%s", remote.Source, remote.Version) } -// GetVersion returns the Source & Version fields, as well as the passed -// checksum, concatenated with '@'s. -func (remote Git) GetSourceVersionSum(sum string) string { - return fmt.Sprintf("%s@%s@%s", remote.Source, remote.Version, sum) -} - func checkGitAvailable() error { cmd := exec.Command("git", "--version") sysOutput, err := cmd.CombinedOutput() diff --git a/internal/remotes/git_test.go b/internal/remotes/git_test.go index d319886..af2e256 100644 --- a/internal/remotes/git_test.go +++ b/internal/remotes/git_test.go @@ -1,47 +1,12 @@ package remotes import ( - "os" - "path/filepath" "testing" - "github.com/opensourcecorp/vdm/internal/vdmspec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func getTestGitRemote(t *testing.T) (Git, string) { - t.Helper() - specLocalPath := "./deps/go-common" - remote := Git{ - RemoteTemplate: vdmspec.RemoteTemplate{ - Type: "git", - Source: "https://github.com/opensourcecorp/go-common", - Version: "v0.2.0", - Destination: specLocalPath, - }, - } - dest := filepath.Join(os.TempDir(), "vdm-test", filepath.Base(remote.Source)) - return remote, dest -} - -func TestSyncGit(t *testing.T) { - remote, dest := getTestGitRemote(t) - err := remote.Sync("", "") - require.NoError(t, err) - - defer t.Cleanup(func() { - if cleanupErr := os.RemoveAll(dest); cleanupErr != nil { - t.Fatalf("removing specLocalPath: %v", cleanupErr) - } - }) - - t.Run(".git directory was removed", func(t *testing.T) { - _, err := os.Stat("./deps/go-common-tag/.git") - assert.ErrorIs(t, err, os.ErrNotExist) - }) -} - func TestCheckGitAvailable(t *testing.T) { t.Run("checkGitAvailable", func(t *testing.T) { t.Run("no error when git is available", func(t *testing.T) { @@ -57,30 +22,3 @@ func TestCheckGitAvailable(t *testing.T) { }) }) } - -func TestGitClone(t *testing.T) { - remote, dest := getTestGitRemote(t) - cloneErr := gitClone(remote.Source, dest) - - defer t.Cleanup(func() { - if cleanupErr := os.RemoveAll(dest); cleanupErr != nil { - t.Fatalf("removing specLocalPath: %v", cleanupErr) - } - }) - - t.Run("no error on success", func(t *testing.T) { - require.NoError(t, cloneErr) - }) - - t.Run("LocalPath is a directory, not a file", func(t *testing.T) { - outDir, err := os.Stat(dest) - require.NoError(t, err) - assert.True(t, outDir.IsDir()) - }) - - t.Run("a known file in the remote exists, and is a file", func(t *testing.T) { - sampleFile, err := os.Stat(filepath.Join(dest, "go.mod")) - require.NoError(t, err) - assert.False(t, sampleFile.IsDir()) - }) -} diff --git a/internal/vdminit/init.go b/internal/vdminit/init.go new file mode 100644 index 0000000..4a220c5 --- /dev/null +++ b/internal/vdminit/init.go @@ -0,0 +1,28 @@ +package vdminit + +import ( + "fmt" + "os" + + "github.com/opensourcecorp/vdm/cmd/vars" + "github.com/opensourcecorp/vdm/internal/archive/cache" +) + +func Paths() error { + vdmCacheDir, err := vars.GetVDMCacheDir() + if err != nil { + return fmt.Errorf("determining vdm cache directory while running init: %w", err) + } + + err = os.MkdirAll(vdmCacheDir, 0755) + if err != nil { + return fmt.Errorf("creating vdm cache directory %q during init: %w", vdmCacheDir, err) + } + + err = cache.CreateSumDB() + if err != nil { + return fmt.Errorf("creating sumdb during init: %w", err) + } + + return nil +} diff --git a/internal/vdminit/init_test.go b/internal/vdminit/init_test.go new file mode 100644 index 0000000..4c2f368 --- /dev/null +++ b/internal/vdminit/init_test.go @@ -0,0 +1,53 @@ +package vdminit + +import ( + "database/sql" + "os" + "path/filepath" + "testing" + + "github.com/opensourcecorp/vdm/cmd/vars" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPaths(t *testing.T) { + // SetupVDMForTest itself calls Paths(), handily + _, cleanup := SetupVDMForTest(t) + + t.Cleanup(cleanup) + + t.Run("env var is set right", func(t *testing.T) { + want := filepath.Join(os.TempDir(), "vdm-tmp") + got := os.Getenv(vars.VDMHomeEnvVarName) + assert.Equal(t, want, got) + }) + + t.Run("vdm cache is then returned as being under the test homedir", func(t *testing.T) { + vdmHome, err := vars.GetVDMHomeDir() + require.NoError(t, err) + want := filepath.Join(vdmHome, "cache") + + got, err := vars.GetVDMCacheDir() + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("sumdb exists and has sums table in it", func(t *testing.T) { + vdmCache, err := vars.GetVDMCacheDir() + require.NoError(t, err) + + sumDBPath := filepath.Join(vdmCache, "sum.db") + _, err = os.Stat(sumDBPath) + assert.NoError(t, err) + + db, err := sql.Open("sqlite", sumDBPath) + require.NoError(t, err) + + want := 1 + var got int + err = db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'sums';`).Scan(&got) + assert.NoError(t, err) + assert.Equal(t, want, got) + }) +} diff --git a/internal/vdminit/testhelpers.go b/internal/vdminit/testhelpers.go new file mode 100644 index 0000000..1c91778 --- /dev/null +++ b/internal/vdminit/testhelpers.go @@ -0,0 +1,37 @@ +package vdminit + +import ( + "os" + "path/filepath" + "testing" + + "github.com/opensourcecorp/vdm/cmd/vars" +) + +// SetupVDMForTest is a helper function for setting up vdm's tests. Notably, vdm +// requires itself to be initialized with its own home directory etc. before +// first use, and tests need a clean way to do that, and an equally-clean way to +// tear down their mess. This function returns both the string path to vdm's +// test home directory, as well as a cleanup function to be called by the test. +func SetupVDMForTest(t *testing.T) (vdmHome string, cleanup func()) { + t.Helper() + + vdmHome = filepath.Join(os.TempDir(), "vdm-tmp") + t.Setenv(vars.VDMHomeEnvVarName, vdmHome) + + err := Paths() + if err != nil { + t.Errorf("instantiating vdm paths for test: %v", err) + } + + // This runs as part of the outer test container, because we want to inspect + // the filesystem state as we go + cleanup = func() { + err := os.RemoveAll(vdmHome) + if err != nil { + t.Errorf("failed to clean up after test: %v", err) + } + } + + return vdmHome, cleanup +} diff --git a/internal/vdmspec/spec.go b/internal/vdmspec/spec.go index 7477c6f..6af888c 100644 --- a/internal/vdmspec/spec.go +++ b/internal/vdmspec/spec.go @@ -1,10 +1,8 @@ package vdmspec import ( - "errors" "fmt" "os" - "path/filepath" "strings" "github.com/opensourcecorp/vdm/internal/message" @@ -12,16 +10,24 @@ import ( ) const ( - // MetaFileName is the name of the tracking file that vdm uses to record & - // track remote statuses on disk. - MetaFileName string = "VDMMETA" - - // GitType represents the string to match against for git remote types. + // GitType represents the string to match against for "git" remote types. GitType string = "git" - // FileType represents the string to match against for file remote types. - FileType string = "file" + // ArchiveType represents the string to match against for "archive" remote + // types. + ArchiveType string = "archive" + // LocalType represents the string to match against for "local" "remote" + // types. + LocalType string = "local" ) +// typeMap is used to house more programmatic access to the various remote +// types, such as in tests or [Spec.Validate] +var typeMap = map[string]int{ + GitType: 0, + ArchiveType: 1, + LocalType: 2, +} + // Spec defines the overall structure of the vmd specfile. type Spec struct { Remotes []RemoteTemplate `json:"remotes" yaml:"remotes"` @@ -39,13 +45,10 @@ type Remoter interface { GetSource() string // GetRemote should return the [RemoteTemplate.Version] value GetVersion() string - // GetRemoteVersion should concatenate the [RemoteTemplate.Source] and - // [RemoteTemplate.Version] values, separated by an '@' symbol - GetSourceVersion() string - // GetRemoteSourceVersionSum should concatenate the [RemoteTemplate.Source], - // [RemoteTemplate.Version], and computed checksum values, separated by '@' - // symbols - GetSourceVersionSum(sum string) string + // GetSumDBKey should concatenate the relevant values from the + // [RemoteTemplate] to produce a string value satisfying the primary key + // constraint for the sumdb + GetSumDBKey() string } // RemoteTemplate defines the template structure for each potential remote @@ -75,70 +78,6 @@ type RemoteTemplate struct { TryLocalSource string `json:"try_local_source" yaml:"try_local_source"` } -// MakeMetaFilePath constructs the metafile path that vdm will use to track a -// remote's state on disk. -func (r RemoteTemplate) MakeMetaFilePath() string { - metaFilePath := filepath.Join(r.Destination, MetaFileName) - // TODO: this is brittle, but it's the best I can think of right now - if r.Type == FileType { - fileDir := filepath.Dir(r.Destination) - fileName := filepath.Base(r.Destination) - // converts to e.g. 'VDMMETA_http.proto' - metaFilePath = filepath.Join(fileDir, fmt.Sprintf("%s_%s", MetaFileName, fileName)) - } - - return metaFilePath -} - -// WriteVDMMeta writes the metafile contents to disk, the path of which is -// determined by [RemoteTemplate.MakeMetaFilePath]. -func (r RemoteTemplate) WriteVDMMeta() error { - metaFilePath := r.MakeMetaFilePath() - vdmMetaContent, err := yaml.Marshal(r) - if err != nil { - return fmt.Errorf("writing %q: %w", metaFilePath, err) - } - - vdmMetaContent = append(vdmMetaContent, []byte("\n")...) - - message.Debugf("writing metadata file to %q", metaFilePath) - err = os.WriteFile(metaFilePath, vdmMetaContent, 0644) - if err != nil { - return fmt.Errorf("writing metadata file: %w", err) - } - - return nil -} - -// GetVDMMeta reads the metafile from disk, and returns it for further -// processing. -func (r RemoteTemplate) GetVDMMeta() (RemoteTemplate, error) { - metaFilePath := r.MakeMetaFilePath() - _, err := os.Stat(metaFilePath) - if errors.Is(err, os.ErrNotExist) { - return RemoteTemplate{}, nil // this is ok, because it might literally not exist yet - } else if err != nil { - return RemoteTemplate{}, fmt.Errorf("couldn't check if %q exists at %q: %w", MetaFileName, metaFilePath, err) - } - - vdmMetaFile, err := os.ReadFile(metaFilePath) - if err != nil { - message.Debugf("error reading VMDMMETA from disk: %w", err) - return RemoteTemplate{}, fmt.Errorf("there was a problem reading the %s file from %q: %w", MetaFileName, metaFilePath, err) - } - message.Debugf("%s contents read:\n%s", MetaFileName, string(vdmMetaFile)) - - var vdmMeta RemoteTemplate - err = yaml.Unmarshal(vdmMetaFile, &vdmMeta) - if err != nil { - message.Debugf("error during %s unmarshal: w", MetaFileName, err) - return RemoteTemplate{}, fmt.Errorf("there was a problem reading the contents of the %s file at %q: %w", MetaFileName, metaFilePath, err) - } - message.Debugf("file %q unmarshalled: %+v", MetaFileName, vdmMeta) - - return vdmMeta, nil -} - // GetSpecFromFile reads the specfile from disk (the path of which may be // determined by the user-supplied flag value), and returns it for further // processing of remotes. diff --git a/internal/vdmspec/spec_test.go b/internal/vdmspec/spec_test.go index 5c25ad6..115ce45 100644 --- a/internal/vdmspec/spec_test.go +++ b/internal/vdmspec/spec_test.go @@ -1,75 +1 @@ package vdmspec - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const testVDMRoot = "../../testdata" - -var ( - testVDMMetaFilePath = filepath.Join(testVDMRoot, MetaFileName) - - testRemote = RemoteTemplate{ - Source: "https://some-remote", - Version: "v1.0.0", - Destination: testVDMRoot, - } - - testSpecFilePath = filepath.Join(testVDMRoot, "vdm.yaml") - - testVDMMetaContents = fmt.Sprintf( - `{"source": "https://some-remote", "version": "v1.0.0", "destination": "%s"}`, - testVDMRoot, - ) -) - -func TestVDMMeta(t *testing.T) { - t.Run("GetVDMMeta", func(t *testing.T) { - err := os.WriteFile(testVDMMetaFilePath, []byte(testVDMMetaContents), 0644) - require.NoError(t, err) - - defer t.Cleanup(func() { - err := os.RemoveAll(testVDMMetaFilePath) - require.NoError(t, err) - }) - - got, err := testRemote.GetVDMMeta() - require.NoError(t, err) - assert.Equal(t, testRemote, got) - }) - - t.Run("WriteVDMMeta", func(t *testing.T) { - defer t.Cleanup(func() { - err := os.RemoveAll(testVDMMetaFilePath) - require.NoError(t, err) - }) - - // Needs to have parent dir(s) exist for write to work - err := os.MkdirAll(testRemote.Destination, 0644) - require.NoError(t, err) - - err = testRemote.WriteVDMMeta() - require.NoError(t, err) - - got, err := testRemote.GetVDMMeta() - require.NoError(t, err) - assert.Equal(t, testRemote, got) - }) - - t.Run("GetSpecsFromFile", func(t *testing.T) { - defer t.Cleanup(func() { - err := os.RemoveAll(testVDMMetaFilePath) - require.NoError(t, err) - }) - - spec, err := GetSpecFromFile(testSpecFilePath) - require.NoError(t, err) - assert.Equal(t, 5, len(spec.Remotes)) - }) -} diff --git a/internal/vdmspec/validate.go b/internal/vdmspec/validate.go index 4c8144b..490d6a1 100644 --- a/internal/vdmspec/validate.go +++ b/internal/vdmspec/validate.go @@ -32,9 +32,6 @@ func (spec Spec) Validate() error { if remote.Type == GitType && remote.Version == "" { allErrors = append(allErrors, errors.New("all 'version' fields for the 'git' remote type must be non-zero length")) } - if remote.Type == FileType && remote.Version != "" { - message.Warnf("NOTE: Remote #%d %q specified as type %q, which does not take explicit version info (you provided %q); ignoring version field", remoteIndex, remote.Source, remote.Type, remote.Version) - } // Destination field message.Debugf("Index #%d: validating field 'Destination' for %+v", remoteIndex, remote) @@ -47,10 +44,6 @@ func (spec Spec) Validate() error { if remote.Type == "" { allErrors = append(allErrors, errors.New("all remotes must specify a 'type' field")) } - typeMap := map[string]int{ - GitType: 1, - FileType: 2, - } if _, ok := typeMap[remote.Type]; !ok { allErrors = append(allErrors, fmt.Errorf("unrecognized remote type %q", remote.Type)) } diff --git a/testdata/vdm.yaml b/testdata/vdm.yaml index 28aaba1..06a1e22 100644 --- a/testdata/vdm.yaml +++ b/testdata/vdm.yaml @@ -11,7 +11,3 @@ remotes: source: "https://github.com/opensourcecorp/go-common" version: "2e6657f5ac013296167c4dd92fbb46f0e3dbdc5f" destination: "./deps/git-hash" - # TODO: re-implement this in the future - # - type: "file" - # source: "https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto" - # destination: "./deps/proto/http/http.proto" From 734cdff562cfeb97cae860b8ad9cd8033946907e Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Sun, 25 Aug 2024 17:07:30 -0500 Subject: [PATCH 15/17] HOW DOES A GIT CLONE WIPE THE VDM HOMEDIR IN TESTS ONLY --- .github/workflows/main.yaml | 2 +- Makefile | 3 +++ README.md | 40 ++++++++++++--------------------- cmd/root.go | 24 +++----------------- cmd/sync.go | 15 +++---------- cmd/sync_test.go | 9 ++++---- internal/archive/cache/cache.go | 1 + internal/archive/cache/sumdb.go | 5 +++-- internal/message/message.go | 2 +- internal/remotes/git.go | 21 ++++++++++------- internal/vdminit/init_test.go | 1 - internal/vdminit/testhelpers.go | 2 -- internal/vdmspec/spec.go | 24 ++++++++++++++++---- internal/vdmspec/validate.go | 2 +- scripts/ci.sh | 1 + 15 files changed, 68 insertions(+), 84 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5654442..68b6bf5 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -44,6 +44,6 @@ jobs: - name: Package binaries run: 'make package' - name: Create GitHub Release - run: gh release create "v$(grep -v '#' ./VERSION)" --generate-notes --verify-tag --latest ./dist/compressed/* + run: gh release create "v$(grep -v '#' ./VERSION)" --draft --generate-notes --verify-tag --latest ./dist/compressed/* env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index c59f0a9..faa5e4c 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,9 @@ clean: ./dist/debian/vdm.deb \ *.out @sudo rm -rf ./dist/debian/vdm/usr +# TODO: until I (maybe) sort out the tests to write test data consistently, +# these deps/ directories etc. can kind of show up anywhere + @find . -type d -name '*deps*' -exec rm -rf {} + bump-versions: clean @bash ./scripts/bump-versions.sh "$${old_version:-}" diff --git a/README.md b/README.md index 30d7e85..852552c 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ go run github.com/opensourcecorp/vdm@ ... ### Usage -To get started, you'll need a `vdm` spec file, which is just a YAML (or JSON) +To get started, you'll need a `vdm` specfile, which is just a YAML (or JSON) file specifying all your external dependencies along with (usually) their revisions & where you want them to live on your filesystem: @@ -51,9 +51,9 @@ revisions & where you want them to live on your filesystem: remotes: - type: "git" - source: "https://github.com/opensourcecorp/vdm" # can specify as 'git@...' to use SSH instead - version: "v0.2.0" # tag example; can also be a branch, or a commit hash - destination: "./deps/" # git types are themselves directories, so to prevent duplicating their top-level names just specify the root destination + source: "https://github.com/opensourcecorp/vdm" # can specify the protocol in any way that you would usually run 'git clone' + version: "v0.2.0" # tag example; can also be a branch, a commit hash, or anything else supported by 'git checkout' + destination: "./deps/" # git types are themselves directories, so to prevent duplicating their top-level names you probably want to just specify the root destination - type: "git" source: "https://github.com/opensourcecorp/osc-infra" @@ -62,24 +62,24 @@ remotes: ``` You can have as many dependency specifications in that array as you want, and -they can be stored wherever you want. By default, this spec file is called +they can be stored wherever you want. By default, this specfile is called `vdm.yaml` and lives at the calling location (which is probably your repo's root), but you can call it whatever you want and point to it using the -`--specfile-path` flag to `vdm`. +`--specfile|-f` flag to `vdm`. -Once you have a spec file, just run: +Once you have a specfile, just run: ```sh vdm sync ``` -and `vdm` will process the spec file, retrieve your dependencies as specified, -and put them where you told them to go. By default, `vdm sync` also removes the -local `.git` directories for each `git` remote, so as to not upset your local -Git tree. If you want to change the version/revision of a remote, just update -your spec file and run `vdm sync` again. +and `vdm` will process the specfile, retrieve your dependencies as specified, +and put them where you told them to go. `vdm sync` also removes the local `.git` +directories for each `git` remote, so as to not upset your local Git tree. If +you want to change the version/revision of a remote, just update your specfile +and run `vdm sync` again. -After running `vdm sync` with the above example spec file, your directory tree +After running `vdm sync` with the above example specfile, your directory tree would look something like this: ```txt @@ -101,20 +101,8 @@ types. `vdm` will fail with an informative error if it can't find `git` on your ## A note about auth -`vdm` has zero goals to be an authN/authZ manager. If a remote in your spec file +`vdm` has zero goals to be an authN/authZ manager. If a remote in your specfile depends on a certain auth setup (an SSH key, something for HTTP basic auth like a `.netrc` file, an `.npmrc` config file, etc.), that setup is out of `vdm`'s scope. If required, you will need to ensure proper auth is configured before running `vdm` commands. - -## Future work - -- Make the sync mechanism more robust, such that if your spec file changes to - remove remotes, they'll get cleaned up automatically. - -- Add `--keep-git-dir` flag so that `git` remote types don't wipe the `.git` - directory at clone-time. - -- Support more `remote` types, like `archive`. - -- Re-introduce the `file` remote type at some point. diff --git a/cmd/root.go b/cmd/root.go index fb89f89..e8726b8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,10 +3,7 @@ package cmd import ( "errors" "fmt" - "os" - "github.com/opensourcecorp/vdm/cmd/vars" - "github.com/opensourcecorp/vdm/internal/archive/cache" "github.com/opensourcecorp/vdm/internal/message" "github.com/opensourcecorp/vdm/internal/vdminit" "github.com/spf13/cobra" @@ -28,7 +25,7 @@ var rootFlagValues rootFlags // Flag name keys const ( - specFilePathFlagKey string = "specfile-path" + specFilePathFlagKey string = "specfile" debugFlagKey string = "debug" ) @@ -45,7 +42,7 @@ func newRootCommand() *cobra.Command { RunE: executeRootCommand, } - cmd.PersistentFlags().StringVar(&rootFlagValues.SpecFilePath, specFilePathFlagKey, "./vdm.yaml", "Path to vdm specfile") + cmd.PersistentFlags().StringVarP(&rootFlagValues.SpecFilePath, specFilePathFlagKey, "f", "./vdm.yaml", "Path to vdm specfile") err = viper.BindPFlag(specFilePathFlagKey, cmd.PersistentFlags().Lookup(specFilePathFlagKey)) if err != nil { message.Fatalf("internal error: unable to bind state of flag --%s: %v", specFilePathFlagKey, err) @@ -76,22 +73,7 @@ func executeRootCommand(cmd *cobra.Command, args []string) error { return fmt.Errorf("initializing vdm: %w", err) } - vdmCacheDir, err := vars.GetVDMCacheDir() - if err != nil { - return fmt.Errorf("determining vdm cache directory while running root command: %w", err) - } - - err = os.MkdirAll(vdmCacheDir, 0755) - if err != nil { - return fmt.Errorf("creating vdm cache directory %q: %w", vdmCacheDir, err) - } - - err = cache.CreateSumDB() - if err != nil { - return fmt.Errorf("creating sumdb before any vdm operations: %w", err) - } - - return errors.New("You must provide a subcommand to vdm") + return errors.New("you must provide a subcommand to vdm") } // Execute wraps the primary execution logic for vdm's root command, and returns diff --git a/cmd/sync.go b/cmd/sync.go index 52f8a6c..27db9ec 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -12,15 +12,6 @@ import ( "github.com/spf13/cobra" ) -// syncFlags defines the CLI flags for the sync subcommand. -type syncFlags struct { - TryLocalSources bool -} - -// syncFlagValues contains an initalized [syncFlags] struct with populated -// values. -var syncFlagValues syncFlags - func newSyncCommand() *cobra.Command { cmd := &cobra.Command{ Use: "sync", @@ -51,18 +42,18 @@ func executeSyncSubCommand(_ *cobra.Command, _ []string) error { func sync() error { spec, err := vdmspec.GetSpecFromFile(rootFlagValues.SpecFilePath) if err != nil { - return fmt.Errorf("getting specs from spec file: %w", err) + return fmt.Errorf("getting specs from specfile: %w", err) } err = spec.Validate() if err != nil { - return fmt.Errorf("your vdm spec file is malformed: %w", err) + return fmt.Errorf("your vdm specfile is malformed: %w", err) } for _, remote := range spec.Remotes { var determinedRemote vdmspec.Remoter switch remote.Type { - case vdmspec.GitType, "": + case vdmspec.GitType: determinedRemote = remotes.Git{RemoteTemplate: remote} case vdmspec.ArchiveType: return errors.New("cannot process 'archive' remote types, as they are not yet fully implemented") diff --git a/cmd/sync_test.go b/cmd/sync_test.go index f65e203..05f0dc9 100644 --- a/cmd/sync_test.go +++ b/cmd/sync_test.go @@ -17,16 +17,15 @@ var ( ) func TestSync(t *testing.T) { - _, cleanup := vdminit.SetupVDMForTest(t) - // This runs as part of the outer test container, because we want to inspect // the filesystem state as we go + _, cleanup := vdminit.SetupVDMForTest(t) t.Cleanup(cleanup) cmd := newRootCommand() cmd.SetArgs([]string{ "--debug", - "--specfile-path", testSpecFilePath, + "--specfile", testSpecFilePath, "sync", }) @@ -44,9 +43,9 @@ func TestSync(t *testing.T) { for topDir, secondDir := range expectedGitDirs { sourceRoot := filepath.Join("deps", topDir, secondDir) t.Run("source directory exists at its destination", func(t *testing.T) { - gitTagSource, err := os.Stat(sourceRoot) + gitSource, err := os.Stat(sourceRoot) require.NoError(t, err) - assert.True(t, gitTagSource.IsDir()) + assert.True(t, gitSource.IsDir()) }) t.Run(".git directory was removed", func(t *testing.T) { diff --git a/internal/archive/cache/cache.go b/internal/archive/cache/cache.go index e6e1ad7..8f68dc9 100644 --- a/internal/archive/cache/cache.go +++ b/internal/archive/cache/cache.go @@ -19,6 +19,7 @@ func AddRemote(remote vdmspec.Remoter, cacheRoot string) (string, error) { if err != nil { return "", fmt.Errorf("getting cache location for remote %q: %w", remote.GetSumDBKey(), err) } + message.Debugf("persistent cache file path: %q", cachePath) err = archive.CreateArchive(cacheRoot, cachePath) if err != nil { diff --git a/internal/archive/cache/sumdb.go b/internal/archive/cache/sumdb.go index ec83281..c8a8824 100644 --- a/internal/archive/cache/sumdb.go +++ b/internal/archive/cache/sumdb.go @@ -14,6 +14,8 @@ import ( "github.com/opensourcecorp/vdm/cmd/vars" "github.com/opensourcecorp/vdm/internal/message" "github.com/opensourcecorp/vdm/internal/vdmspec" + + // Imports the SQLite driver _ "modernc.org/sqlite" ) @@ -140,11 +142,10 @@ func CheckIfRemoteInSumDB(remote vdmspec.Remoter) (hasKey bool, err error) { } message.Debugf("number of results from sumdb for remote key %q: %d", remote.GetSumDBKey(), numRows) - message.Debugf("sumdb query result not yet checked for remote key %q, hasKey: %v", remote.GetSumDBKey(), hasKey) if numRows > 0 { hasKey = true } - message.Debugf("sumdb query result now checked for remote key %q, hasKey: %v", remote.GetSumDBKey(), hasKey) + message.Debugf("sumdb query result checked for remote key %q, hasKey: %v", remote.GetSumDBKey(), hasKey) return hasKey, err } diff --git a/internal/message/message.go b/internal/message/message.go index 25a593b..f8d567f 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -1,4 +1,4 @@ -// Package message controls message printing. THis isn't a "logging" package per +// Package message controls message printing. This isn't a "logging" package per // se, but adds some niceties for log-like needs. package message diff --git a/internal/remotes/git.go b/internal/remotes/git.go index 1290ae8..f0b9712 100644 --- a/internal/remotes/git.go +++ b/internal/remotes/git.go @@ -19,6 +19,8 @@ type Git struct { vdmspec.RemoteTemplate } +var _ vdmspec.Remoter = Git{} + // Cache provides the [vdmspec.Remoter.Cache] operations for "git" remote types. func (remote Git) Cache() (cachePath string, err error) { tmpCachePath := cache.GetTempCachePath(remote) @@ -40,11 +42,6 @@ func (remote Git) Cache() (cachePath string, err error) { if err != nil { return "", fmt.Errorf("cloning git repository: %w", err) } - defer func() { - if rmErr := os.RemoveAll(tmpCachePath); rmErr != nil { - err = errors.Join(err, fmt.Errorf("removing temporary cache directory for %q: %w", remote.GetSource(), rmErr)) - } - }() remote.OpMsg("Setting specified version...") checkoutCmd := exec.Command("git", "-C", tmpCachePath, "checkout", remote.Version) @@ -113,12 +110,18 @@ func checkGitAvailable() error { func gitClone(src string, dest string) error { err := checkGitAvailable() if err != nil { - return fmt.Errorf("remote %q is a git type, but git may not installed/available on PATH: %w", src, err) + return fmt.Errorf("remote %q is a git type, but git may not be installed/available on PATH: %w", src, err) } + // TODO: remove + TEST := "/tmp/vdm-tmp" + _, statErr := os.Stat(TEST) + message.Debugf("home path %q exists? %v", TEST, statErr == nil) + cloneCmdArgs := []string{"clone", src, dest} message.Debugf("git args: %v", cloneCmdArgs) + // HOW TF IS THIS WHERE THE TEMP CACHE GOES MISSING cloneCmd := exec.Command("git", cloneCmdArgs...) cloneOutput, err := cloneCmd.CombinedOutput() message.Debugf("git clone command output: %s", string(cloneOutput)) @@ -126,7 +129,9 @@ func gitClone(src string, dest string) error { return fmt.Errorf("cloning remote: exec error '%w', with output: %s", err, string(cloneOutput)) } + // TODO: remove + _, statErr = os.Stat(TEST) + message.Debugf("home path %q exists? %v", TEST, statErr == nil) + return nil } - -var _ vdmspec.Remoter = Git{} diff --git a/internal/vdminit/init_test.go b/internal/vdminit/init_test.go index 4c2f368..e85146b 100644 --- a/internal/vdminit/init_test.go +++ b/internal/vdminit/init_test.go @@ -14,7 +14,6 @@ import ( func TestPaths(t *testing.T) { // SetupVDMForTest itself calls Paths(), handily _, cleanup := SetupVDMForTest(t) - t.Cleanup(cleanup) t.Run("env var is set right", func(t *testing.T) { diff --git a/internal/vdminit/testhelpers.go b/internal/vdminit/testhelpers.go index 1c91778..653702a 100644 --- a/internal/vdminit/testhelpers.go +++ b/internal/vdminit/testhelpers.go @@ -24,8 +24,6 @@ func SetupVDMForTest(t *testing.T) (vdmHome string, cleanup func()) { t.Errorf("instantiating vdm paths for test: %v", err) } - // This runs as part of the outer test container, because we want to inspect - // the filesystem state as we go cleanup = func() { err := os.RemoveAll(vdmHome) if err != nil { diff --git a/internal/vdmspec/spec.go b/internal/vdmspec/spec.go index 6af888c..70f4051 100644 --- a/internal/vdmspec/spec.go +++ b/internal/vdmspec/spec.go @@ -1,8 +1,11 @@ package vdmspec import ( + "encoding/json" + "errors" "fmt" "os" + "path/filepath" "strings" "github.com/opensourcecorp/vdm/internal/message" @@ -88,7 +91,7 @@ func GetSpecFromFile(specFilePath string) (Spec, error) { return Spec{}, fmt.Errorf( strings.Join([]string{ "there was a problem reading your vdm file from %q -- does it not exist?", - "Either pass the --spec-file flag, or create one in the default location (details in the README).", + "Either pass the --specfile flag, or create one in the default location (details in the README).", "Error details: %w"}, " ", ), @@ -99,10 +102,23 @@ func GetSpecFromFile(specFilePath string) (Spec, error) { message.Debugf("specfile contents read:\n%s", string(specFile)) var spec Spec - err = yaml.Unmarshal(specFile, &spec) + specfileExtension := filepath.Ext(specFilePath) + switch specfileExtension { + case ".yaml", ".yml": + message.Debugf("specfile format is YAML") + err = yaml.Unmarshal(specFile, &spec) + case ".json": + message.Debugf("specfile format is JSON") + err = json.Unmarshal(specFile, &spec) + case ".toml": + message.Debugf("specfile format is TOML") + err = errors.New("TOML format for vdm specfile is not yet supported") + default: + err = fmt.Errorf("unsupported specfile extension %q", specfileExtension) + } if err != nil { - message.Debugf("error during specfile unmarshal: w", err) - return Spec{}, fmt.Errorf("there was a problem reading the contents of your vdm spec file: %w", err) + message.Debugf("error during specfile %s unmarshal: %v", specfileExtension, err) + return Spec{}, fmt.Errorf("there was a problem reading the contents of your vdm specfile %q: %w", specFilePath, err) } message.Debugf("vdmSpecs unmarshalled: %+v", spec) diff --git a/internal/vdmspec/validate.go b/internal/vdmspec/validate.go index 490d6a1..1eba67b 100644 --- a/internal/vdmspec/validate.go +++ b/internal/vdmspec/validate.go @@ -53,7 +53,7 @@ func (spec Spec) Validate() error { for _, err := range allErrors { message.Errorf("validation failure: %s", err.Error()) } - return fmt.Errorf("%d validation failure(s) found in your vdm spec file", len(allErrors)) + return fmt.Errorf("%d validation failure(s) found in your vdm specfile", len(allErrors)) } return nil } diff --git a/scripts/ci.sh b/scripts/ci.sh index c888d19..f66f8ec 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -30,6 +30,7 @@ if ! go run github.com/kisielk/errcheck@latest ./... ; then fi printf '>> Go test\n' +go clean -testcache if ! go test -cover -coverprofile=./cover.out ./... ; then printf '>>> Failed go-test check\n' > /dev/stderr failures+=('go-test') From 123f527020eeb18bc7f72147199f1102de2e9946 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Tue, 27 Aug 2024 12:53:19 -0500 Subject: [PATCH 16/17] Make test vmd home have a random ID in the path --- internal/remotes/git.go | 10 ---------- internal/vdminit/init_test.go | 3 +-- internal/vdminit/testhelpers.go | 5 ++++- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/internal/remotes/git.go b/internal/remotes/git.go index f0b9712..b397633 100644 --- a/internal/remotes/git.go +++ b/internal/remotes/git.go @@ -113,15 +113,9 @@ func gitClone(src string, dest string) error { return fmt.Errorf("remote %q is a git type, but git may not be installed/available on PATH: %w", src, err) } - // TODO: remove - TEST := "/tmp/vdm-tmp" - _, statErr := os.Stat(TEST) - message.Debugf("home path %q exists? %v", TEST, statErr == nil) - cloneCmdArgs := []string{"clone", src, dest} message.Debugf("git args: %v", cloneCmdArgs) - // HOW TF IS THIS WHERE THE TEMP CACHE GOES MISSING cloneCmd := exec.Command("git", cloneCmdArgs...) cloneOutput, err := cloneCmd.CombinedOutput() message.Debugf("git clone command output: %s", string(cloneOutput)) @@ -129,9 +123,5 @@ func gitClone(src string, dest string) error { return fmt.Errorf("cloning remote: exec error '%w', with output: %s", err, string(cloneOutput)) } - // TODO: remove - _, statErr = os.Stat(TEST) - message.Debugf("home path %q exists? %v", TEST, statErr == nil) - return nil } diff --git a/internal/vdminit/init_test.go b/internal/vdminit/init_test.go index e85146b..05d3699 100644 --- a/internal/vdminit/init_test.go +++ b/internal/vdminit/init_test.go @@ -17,9 +17,8 @@ func TestPaths(t *testing.T) { t.Cleanup(cleanup) t.Run("env var is set right", func(t *testing.T) { - want := filepath.Join(os.TempDir(), "vdm-tmp") got := os.Getenv(vars.VDMHomeEnvVarName) - assert.Equal(t, want, got) + assert.Regexp(t, `vdm-tmp-\d+`, got) }) t.Run("vdm cache is then returned as being under the test homedir", func(t *testing.T) { diff --git a/internal/vdminit/testhelpers.go b/internal/vdminit/testhelpers.go index 653702a..89af3fa 100644 --- a/internal/vdminit/testhelpers.go +++ b/internal/vdminit/testhelpers.go @@ -1,6 +1,8 @@ package vdminit import ( + "fmt" + "math/rand" "os" "path/filepath" "testing" @@ -16,7 +18,8 @@ import ( func SetupVDMForTest(t *testing.T) (vdmHome string, cleanup func()) { t.Helper() - vdmHome = filepath.Join(os.TempDir(), "vdm-tmp") + randID := 100000000000 + rand.Intn(999999999999) + vdmHome = filepath.Join(os.TempDir(), fmt.Sprintf("vdm-tmp-%d", randID)) t.Setenv(vars.VDMHomeEnvVarName, vdmHome) err := Paths() From b1939e96db6e21bec22c6ad56311dafd9b321623 Mon Sep 17 00:00:00 2001 From: "Ryan J. Price" Date: Tue, 27 Aug 2024 13:03:43 -0500 Subject: [PATCH 17/17] Bump sqlite down --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 59046bc..ad12a32 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.32.0 + modernc.org/sqlite v1.31.1 ) require ( diff --git a/go.sum b/go.sum index 0cfcb64..b992684 100644 --- a/go.sum +++ b/go.sum @@ -99,8 +99,8 @@ modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= -modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= -modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= +modernc.org/sqlite v1.31.1 h1:XVU0VyzxrYHlBhIs1DiEgSl0ZtdnPtbLVy8hSkzxGrs= +modernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=