diff --git a/.gitignore b/.gitignore index dab8e7e..77e7bec 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *~ tofu tofu.exe +fake +fake.exe \ No newline at end of file diff --git a/downloader.go b/downloader.go index 27e8d6a..894a4af 100644 --- a/downloader.go +++ b/downloader.go @@ -46,7 +46,20 @@ func New(opts ...ConfigOpt) (Downloader, error) { } } - key, err := crypto.NewKeyFromArmored(cfg.GPGKey) + keyRing, err := createKeyRing(cfg.GPGKey) + if err != nil { + return nil, err + } + + return &downloader{ + cfg, + tpl, + keyRing, + }, nil +} + +func createKeyRing(armoredKey string) (*crypto.KeyRing, error) { + key, err := crypto.NewKeyFromArmored(armoredKey) if err != nil { return nil, &InvalidConfigurationError{ Message: "Failed to decode GPG key", @@ -61,12 +74,7 @@ func New(opts ...ConfigOpt) (Downloader, error) { if err != nil { return nil, &InvalidConfigurationError{Message: "Cannot create keyring", Cause: err} } - - return &downloader{ - cfg, - tpl, - keyRing, - }, nil + return keyRing, nil } type downloader struct { diff --git a/mockmirror/fake.go b/internal/helloworld/fake.go similarity index 57% rename from mockmirror/fake.go rename to internal/helloworld/fake.go index d79bdf9..7a26324 100644 --- a/mockmirror/fake.go +++ b/internal/helloworld/fake.go @@ -1,7 +1,7 @@ // Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 -package mockmirror +package helloworld import ( "errors" @@ -12,18 +12,14 @@ import ( "testing" ) -func buildFake(t *testing.T) []byte { - _, filename, _, _ := runtime.Caller(1) - fakeDir := path.Join(path.Dir(filename), "fake") - if err := os.MkdirAll(fakeDir, 0755); err != nil { - t.Fatalf("Failed to create fake dir (%v)", err) - } - binaryPath := path.Join(fakeDir, "fake") - if contents, err := os.ReadFile(binaryPath); err == nil { - return contents +// Build creates a hello-world binary for the current platform you can use to test. +func Build(t *testing.T) []byte { + fakeName := "fake" + if runtime.GOOS == "windows" { + fakeName += ".exe" } - dir := path.Join(os.TempDir(), "fake") + dir := path.Join(os.TempDir(), fakeName) if err := os.MkdirAll(dir, 0700); err != nil { t.Fatal(err) } @@ -37,7 +33,7 @@ func buildFake(t *testing.T) []byte { t.Fatal() } - cmd := exec.Command("go", "build", "-ldflags", "-s -w", "-o", "fake") + cmd := exec.Command("go", "build", "-ldflags", "-s -w", "-o", fakeName) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = dir @@ -50,14 +46,10 @@ func buildFake(t *testing.T) []byte { } } - contents, err := os.ReadFile(path.Join(dir, "fake")) + contents, err := os.ReadFile(path.Join(dir, fakeName)) if err != nil { t.Fatalf("Failed to read compiled fake (%v)", err) } - - if err := os.WriteFile(binaryPath, contents, 0700); err != nil { //nolint:gosec //This needs to be executable. - t.Fatalf("Failed to create fake binary at %s (%v)", binaryPath, err) - } return contents } diff --git a/internal/tools/mockmirror-build-fake/main.go b/internal/tools/mockmirror-build-fake/main.go deleted file mode 100644 index 9a12e43..0000000 --- a/internal/tools/mockmirror-build-fake/main.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) The OpenTofu Authors -// SPDX-License-Identifier: MPL-2.0 - -package main - -import ( - "bytes" - "errors" - "flag" - "fmt" - "log" - "os" - "os/exec" - "path" - "strconv" - "strings" - "text/template" - - "github.com/opentofu/tofudl/branding" -) - -func main() { - if err := runMain(); err != nil { - log.Fatal(err) - } -} - -func runMain() error { - file := "" - flag.StringVar(&file, "file", file, "File to write to.") - flag.Parse() - - dir := path.Join(os.TempDir(), "fake") - if err := os.MkdirAll(dir, 0755); err != nil { - return err - } - defer func() { - _ = os.RemoveAll(dir) - }() - if err := os.WriteFile(path.Join(dir, "go.mod"), []byte(gomod), 0644); err != nil { //nolint:gosec //This is not sensitive. - return err - } - if err := os.WriteFile(path.Join(dir, "main.go"), []byte(code), 0644); err != nil { //nolint:gosec //This is not sensitive. - return err - } - - cmd := exec.Command("go", "build", "-ldflags", "-s -w", "-o", "fake") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Dir = dir - if err := cmd.Run(); err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - return fmt.Errorf("build failed (exit code %d)", exitErr.ExitCode()) - } - return err - } - - contents, err := os.ReadFile(path.Join(dir, "fake")) - if err != nil { - return err - } - - tpl := template.New("") - if tpl, err = tpl.Parse(templateText); err != nil { - return err - } - - parts := make([]string, len(contents)) - for i, b := range contents { - if i%20 == 0 { - parts[i] = "\n" + strconv.Itoa(int(b)) - } else { - parts[i] = strconv.Itoa(int(b)) - } - } - contentsCode := "[]byte{" + strings.Join(parts, ", ") + "}" - - buf := &bytes.Buffer{} - if err := tpl.Execute(buf, struct { - Authors string - License string - Contents string - File string - }{ - branding.SPDXAuthorsName, - branding.SPDXLicense, - contentsCode, - file, - }); err != nil { - return err - } - - if err := os.WriteFile(file+"~", buf.Bytes(), 0644); err != nil { //nolint:gosec //This is not sensitive. - return err - } - if err := os.Rename(file+"~", file); err != nil { - return err - } - return nil -} - -var templateText = `// Copyright (c) {{ .Authors }} -// SPDX-License-Identifier: {{ .License }} - -// Code generated by internal/tools/mockmirror-build-fake. DO NOT EDIT. - -package mockmirror - -//go:` + `generate go run github.com/opentofu/tofudl/internal/tools/mockmirror-build-fake -file {{ .File }} - -const binaryContents = {{ .Contents }} -` - -var code = `package main - -func main() { - print("Hello world!") -} -` - -var gomod = `module fake - -go 1.21` diff --git a/mirror.go b/mirror.go index 2ed80e0..59300b5 100644 --- a/mirror.go +++ b/mirror.go @@ -8,6 +8,9 @@ import ( "fmt" "net/http" "time" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/opentofu/tofudl/branding" ) // NewMirror creates a new mirror, optionally acting as a pull-through cache when passing a pullThroughDownloader. @@ -17,10 +20,20 @@ func NewMirror(config MirrorConfig, storage MirrorStorage, pullThroughDownloader "no storage and no pull-through downloader passed to NewMirror, cannot create a working mirror", ) } + if config.GPGKey == "" { + config.GPGKey = branding.DefaultGPGKey + } + + keyRing, err := createKeyRing(config.GPGKey) + if err != nil { + return nil, err + } + return &mirror{ storage, pullThroughDownloader, config, + keyRing, }, nil } @@ -57,10 +70,14 @@ type MirrorConfig struct { // ArtifactCacheTimeout is the time the cached artifacts should be considered valid. A duration of 0 means that // artifacts should not be cached. A duration of -1 means that artifacts should be cached indefinitely. ArtifactCacheTimeout time.Duration `json:"artifact_cache_timeout"` + + // GPGKey is the ASCII-armored key to verify downloaded artifacts against. This is only needed in standalone mode. + GPGKey string `json:"gpg_key"` } type mirror struct { storage MirrorStorage pullThroughDownloader Downloader config MirrorConfig + keyRing *crypto.KeyRing } diff --git a/mirror_download_version.go b/mirror_download_version.go index dbe4f19..c401a0f 100644 --- a/mirror_download_version.go +++ b/mirror_download_version.go @@ -8,5 +8,5 @@ import ( ) func (m *mirror) DownloadVersion(ctx context.Context, version VersionWithArtifacts, platform Platform, architecture Architecture) ([]byte, error) { - return downloadVersion(ctx, version, platform, architecture, m.DownloadArtifact, m.pullThroughDownloader.VerifyArtifact) + return downloadVersion(ctx, version, platform, architecture, m.DownloadArtifact, m.VerifyArtifact) } diff --git a/mirror_test.go b/mirror_test.go index 1757e5f..aafec41 100644 --- a/mirror_test.go +++ b/mirror_test.go @@ -5,10 +5,14 @@ package tofudl_test import ( "context" + "runtime" "testing" "time" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/opentofu/tofudl" + "github.com/opentofu/tofudl/branding" + "github.com/opentofu/tofudl/internal/helloworld" "github.com/opentofu/tofudl/mockmirror" ) @@ -92,3 +96,50 @@ func TestMirroringE2E(t *testing.T) { t.Fatal("Empty artifact!") } } + +func TestMirrorStandalone(t *testing.T) { + binaryContents := helloworld.Build(t) + + ctx := context.Background() + key, err := crypto.GenerateKey(branding.ProductName+" Test", "noreply@example.org", "rsa", 2048) + if err != nil { + t.Fatal(err) + } + pubKey, err := key.GetArmoredPublicKey() + if err != nil { + t.Fatal(err) + } + builder, err := tofudl.NewReleaseBuilder(key) + if err != nil { + t.Fatal(err) + } + if err := builder.PackageBinary(tofudl.PlatformAuto, tofudl.ArchitectureAuto, binaryContents, nil); err != nil { + t.Fatalf("failed to package binary (%v)", err) + } + + mirrorStorage, err := tofudl.NewFilesystemStorage(t.TempDir()) + if err != nil { + t.Fatalf("failed to set up TofuDL mirror") + } + downloader, err := tofudl.NewMirror( + tofudl.MirrorConfig{ + GPGKey: pubKey, + }, + mirrorStorage, + nil, + ) + if err != nil { + t.Fatal(err) + } + if err := builder.Build(ctx, "1.9.0", downloader); err != nil { + t.Fatal(err) + } + _, err = downloader.Download(ctx) + if err != nil { + t.Fatal(err) + } + // Make sure all file handles are closed. + if runtime.GOOS == "windows" { + runtime.GC() + } +} diff --git a/mirror_verify_artifact.go b/mirror_verify_artifact.go index fcee847..82517af 100644 --- a/mirror_verify_artifact.go +++ b/mirror_verify_artifact.go @@ -4,5 +4,8 @@ package tofudl func (m *mirror) VerifyArtifact(artifactName string, artifactContents []byte, sumsFileContents []byte, signatureFileContent []byte) error { - return m.pullThroughDownloader.VerifyArtifact(artifactName, artifactContents, sumsFileContents, signatureFileContent) + if m.pullThroughDownloader != nil { + return m.pullThroughDownloader.VerifyArtifact(artifactName, artifactContents, sumsFileContents, signatureFileContent) + } + return verifyArtifact(m.keyRing, artifactName, artifactContents, sumsFileContents, signatureFileContent) } diff --git a/mockmirror/mockmirror.go b/mockmirror/mockmirror.go index 3f68a1d..965b8ef 100644 --- a/mockmirror/mockmirror.go +++ b/mockmirror/mockmirror.go @@ -14,13 +14,14 @@ import ( "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/opentofu/tofudl" "github.com/opentofu/tofudl/branding" + "github.com/opentofu/tofudl/internal/helloworld" ) // New returns a mirror serving a fake archive signed with a GPG key for testing purposes. func New( t *testing.T, ) Mirror { - return NewFromBinary(t, buildFake(t)) + return NewFromBinary(t, helloworld.Build(t)) } // NewFromBinary returns a mirror serving a binary passed and signed with a GPG key for testing purposes.