mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
As discussed in opentofu/opentofu#2656, this consolidates the two concerns of the PackageAuthentication interface into a single function that deals both with package authentication _and_ with reporting all of the package hashes that were used to make the authentication decision. This means that any .zip archive that OpenTofu directly verifies during installation can now have its hash recorded in the dependency lock file even if that package didn't come from the provider's origin registry, which is beneficial when the first installation of a provider comes from a secondary ("mirror") source because it creates an additional hook by which that dependency lock file entry can be "upgraded" to be complete in a future "tofu init" run against the origin registry, or by the "tofu providers lock" command. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
332 lines
12 KiB
Go
332 lines
12 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package getproviders
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/apparentlymart/go-versions/versions"
|
|
"github.com/google/go-cmp/cmp"
|
|
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
orasContent "oras.land/oras-go/v2/content"
|
|
orasMemoryStore "oras.land/oras-go/v2/content/memory"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/collections"
|
|
)
|
|
|
|
func TestPackageOCIBlobArchive(t *testing.T) {
|
|
// To avoid any external dependencies we'll implement this test in terms
|
|
// of an in-memory-only OCI repository. In real use we instead use the
|
|
// ORAS-Go remote registry client, which implements the same interfaces.
|
|
//
|
|
// [PackageOCIBlobArchive] focuses only on the leaf archive artifact,
|
|
// so we don't need to include any manifests here as they would normally
|
|
// be dealt with by the [Source] implementation instead.
|
|
store := orasMemoryStore.New()
|
|
|
|
// Unfortunately we need to first construct our blob in a separate RAM
|
|
// buffer here because we need to calculate a checksum for it in order
|
|
// to "push" it into the store.
|
|
blobBytes := makePlaceholderProviderPackageZip(t, "not a real executable; just a placeholder")
|
|
desc := pushOCIBlob(t, "archive/zip", "", blobBytes, store)
|
|
|
|
t.Run("happy path", func(t *testing.T) {
|
|
// The in-memory OCI repository contains just the blob represented
|
|
// by desc, which would not be enough to query the package metadata
|
|
// through the [Source] API, but is sufficient for the limited scope
|
|
// of [PackageOCIBlobArchive], since that is interested only in the
|
|
// archive blob and assumes that some other component already
|
|
// used tags and manifests to select a suitable blob.
|
|
loc := PackageOCIBlobArchive{
|
|
repoStore: store,
|
|
blobDescriptor: desc,
|
|
}
|
|
meta := PackageMeta{
|
|
Provider: addrs.NewBuiltInProvider("foo"),
|
|
Version: versions.MustParseVersion("1.0.0"),
|
|
TargetPlatform: CurrentPlatform,
|
|
Location: loc,
|
|
Authentication: &mockAuthentication{
|
|
hashes: HashDispositions{
|
|
Hash("test:placeholder"): {
|
|
SignedByGPGKeyIDs: collections.NewSet("abc123"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
targetDir := t.TempDir()
|
|
authResult, err := loc.InstallProviderPackage(t.Context(), meta, targetDir, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
// Installation was successful, so we should now have the two files guaranteed
|
|
// by the documentation of [makePlaceholderProviderPackageZip].
|
|
if diff := diffDirForPlaceholderProviderPackageZip(t, targetDir); diff != "" {
|
|
t.Error("wrong directory contents after successful installation\n" + diff)
|
|
}
|
|
if !authResult.Signed() {
|
|
// This fake package isn't _actually_ signed, but the mockAuthentication
|
|
// object we passed in meta reports that it was and so that information
|
|
// should pass through to the installation result if the installation
|
|
// process is correctly implemented.
|
|
t.Errorf("auth result does not indicate that the package is signed")
|
|
}
|
|
})
|
|
t.Run("checksum mismatch", func(t *testing.T) {
|
|
loc := PackageOCIBlobArchive{
|
|
repoStore: store,
|
|
blobDescriptor: desc,
|
|
}
|
|
meta := PackageMeta{
|
|
Provider: addrs.NewBuiltInProvider("foo"),
|
|
Version: versions.MustParseVersion("1.0.0"),
|
|
TargetPlatform: CurrentPlatform,
|
|
Location: loc,
|
|
}
|
|
targetDir := t.TempDir()
|
|
_, err := loc.InstallProviderPackage(t.Context(), meta, targetDir, []Hash{
|
|
HashSchemeZip.New("not-valid-never-matches"),
|
|
})
|
|
const wantErrSubstr = `doesn't match any of the checksums`
|
|
if err == nil {
|
|
t.Fatalf("unexpected success\nwant error containing: %s", wantErrSubstr)
|
|
}
|
|
if gotErr := err.Error(); !strings.Contains(gotErr, wantErrSubstr) {
|
|
t.Fatalf("wrong error\ngot: %s\nwant substring: %s", gotErr, wantErrSubstr)
|
|
}
|
|
// The checksum failure should've been detected before actually
|
|
// extracting the package.
|
|
if diff := diffDirEmpty(t, targetDir); diff != "" {
|
|
t.Error("unexpected content in target directory\n" + diff)
|
|
}
|
|
})
|
|
t.Run("unsupported archive type", func(t *testing.T) {
|
|
wrongDesc := desc // shallow copy
|
|
wrongDesc.MediaType = "application/x-lzh-compressed" // an unsupported archive type
|
|
loc := PackageOCIBlobArchive{
|
|
repoStore: store,
|
|
blobDescriptor: wrongDesc,
|
|
}
|
|
meta := PackageMeta{
|
|
Provider: addrs.NewBuiltInProvider("foo"),
|
|
Version: versions.MustParseVersion("1.0.0"),
|
|
TargetPlatform: CurrentPlatform,
|
|
Location: loc,
|
|
}
|
|
targetDir := t.TempDir()
|
|
_, err := loc.InstallProviderPackage(t.Context(), meta, targetDir, nil)
|
|
const wantErrSubstr = `selected OCI artifact manifest has unexpected media type "application/x-lzh-compressed"`
|
|
if err == nil {
|
|
t.Fatalf("unexpected success\nwant error containing: %s", wantErrSubstr)
|
|
}
|
|
if gotErr := err.Error(); !strings.Contains(gotErr, wantErrSubstr) {
|
|
t.Fatalf("wrong error\ngot: %s\nwant substring: %s", gotErr, wantErrSubstr)
|
|
}
|
|
// The unsupported format should've been detected before actually
|
|
// extracting the package. (The blob we provided was actually a
|
|
// zip archive despite the incorrect media type, so it could
|
|
// potentially still be extracted despite the media type problem.
|
|
if diff := diffDirEmpty(t, targetDir); diff != "" {
|
|
t.Error("unexpected content in target directory\n" + diff)
|
|
}
|
|
})
|
|
t.Run("descriptor has incorrect size", func(t *testing.T) {
|
|
wrongDesc := desc // shallow copy
|
|
wrongDesc.Size = 4 // not the actual size of the zip archive
|
|
loc := PackageOCIBlobArchive{
|
|
repoStore: store,
|
|
blobDescriptor: wrongDesc,
|
|
}
|
|
meta := PackageMeta{
|
|
Provider: addrs.NewBuiltInProvider("foo"),
|
|
Version: versions.MustParseVersion("1.0.0"),
|
|
TargetPlatform: CurrentPlatform,
|
|
Location: loc,
|
|
}
|
|
targetDir := t.TempDir()
|
|
_, err := loc.InstallProviderPackage(t.Context(), meta, targetDir, nil)
|
|
if err == nil {
|
|
// Unfortunately the shape of this error is not guaranteed since each
|
|
// implementation of orasContent.ReadOnlyStorage detects and reports
|
|
// this problem in a different way, so we can only test whether it failed.
|
|
t.Fatalf("unexpected success; want some sort of error about the descriptor being incorrect")
|
|
}
|
|
})
|
|
t.Run("descriptor has digest referring to missing blob", func(t *testing.T) {
|
|
wrongDesc := desc // shallow copy
|
|
wrongDesc.Digest = ociv1.DescriptorEmptyJSON.Digest // not actually present in our store
|
|
loc := PackageOCIBlobArchive{
|
|
repoStore: store,
|
|
blobDescriptor: wrongDesc,
|
|
}
|
|
meta := PackageMeta{
|
|
Provider: addrs.NewBuiltInProvider("foo"),
|
|
Version: versions.MustParseVersion("1.0.0"),
|
|
TargetPlatform: CurrentPlatform,
|
|
Location: loc,
|
|
}
|
|
targetDir := t.TempDir()
|
|
_, err := loc.InstallProviderPackage(t.Context(), meta, targetDir, nil)
|
|
if err == nil {
|
|
// Unfortunately the shape of this error is not guaranteed since each
|
|
// implementation of orasContent.ReadOnlyStorage detects and reports
|
|
// this problem in a different way, so we can only test whether it failed.
|
|
t.Fatalf("unexpected success; want some sort of error about the descriptor being incorrect")
|
|
}
|
|
})
|
|
t.Run("misbehaving store returns wrong content", func(t *testing.T) {
|
|
// In this case we use the correct descriptor, but use a special store
|
|
// implementation that intentionally returns incorrect results.
|
|
// The installation process should detect the problem by verifying
|
|
// what it received against the digest in the checksum.
|
|
loc := PackageOCIBlobArchive{
|
|
// The "lying storage" will return a blob that is correctly-sized
|
|
// but has the wrong content, to make sure that we're actually
|
|
// checking the content against the digest.
|
|
repoStore: LyingORASStorage{wantSize: desc.Size},
|
|
blobDescriptor: desc,
|
|
}
|
|
meta := PackageMeta{
|
|
Provider: addrs.NewBuiltInProvider("foo"),
|
|
Version: versions.MustParseVersion("1.0.0"),
|
|
TargetPlatform: CurrentPlatform,
|
|
Location: loc,
|
|
}
|
|
targetDir := t.TempDir()
|
|
_, err := loc.InstallProviderPackage(t.Context(), meta, targetDir, nil)
|
|
const wantErrSubstr = `provider package does not match digest`
|
|
if err == nil {
|
|
t.Fatalf("unexpected success\nwant error containing: %s", wantErrSubstr)
|
|
}
|
|
if gotErr := err.Error(); !strings.Contains(gotErr, wantErrSubstr) {
|
|
t.Fatalf("wrong error\ngot: %s\nwant substring: %s", gotErr, wantErrSubstr)
|
|
}
|
|
})
|
|
}
|
|
|
|
// makePlaceholderProviderPackageZip returns a byte array with data that
|
|
// would, if written to disk, represent a valid zip archive acting as
|
|
// a placeholder for a provider plugin package.
|
|
//
|
|
// The zip archive includes files named "terraform-provider-foo" and
|
|
// "README". The "terraform-provider-foo" file is generated with the
|
|
// content given in fakeExeContent, while the "README" content is
|
|
// unspecified since it's just here to act as additional baggage that
|
|
// a real provider could hypothetically make use of but OpenTofu itself
|
|
// doesn't care about.
|
|
func makePlaceholderProviderPackageZip(t *testing.T, fakeExeContent string) []byte {
|
|
t.Helper()
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
zipW := zip.NewWriter(buf)
|
|
exeW, err := zipW.Create("terraform-provider-foo")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = io.WriteString(exeW, fakeExeContent)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// additional file to make sure we get the entire package, and not just the executable
|
|
docW, err := zipW.Create("README")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = io.WriteString(docW, "not a real plugin; just a placeholder")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = zipW.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// diffDirForPlaceholderProviderPackageZip returns a string representation
|
|
// of a diff between the entries in the given directory and the files
|
|
// placed into an archive returned by [makePlaceholderProviderPackageZip].
|
|
//
|
|
// If the result is an empty string then there are no differences and so the
|
|
// directory does appear to have been created from the content of such a
|
|
// zip file.
|
|
func diffDirForPlaceholderProviderPackageZip(t *testing.T, dir string) string {
|
|
t.Helper()
|
|
|
|
type DiffableEntry struct {
|
|
Filename string
|
|
IsDir bool
|
|
}
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := make([]DiffableEntry, len(entries))
|
|
for i, entry := range entries {
|
|
got[i] = DiffableEntry{
|
|
Filename: entry.Name(),
|
|
IsDir: entry.IsDir(),
|
|
}
|
|
}
|
|
return cmp.Diff([]DiffableEntry{
|
|
{"README", false},
|
|
{"terraform-provider-foo", false},
|
|
}, got)
|
|
}
|
|
|
|
// diffDirForPlaceholderProviderPackageZip returns a string representation
|
|
// of a diff between the entries in the given directory and an empty
|
|
// directory.
|
|
//
|
|
// If the result is an empty string then there are no differences and so the
|
|
// directory is empty.
|
|
func diffDirEmpty(t *testing.T, dir string) string {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := make([]string, len(entries))
|
|
for i, entry := range entries {
|
|
got[i] = entry.Name()
|
|
}
|
|
return cmp.Diff([]string{}, got)
|
|
}
|
|
|
|
// LyingORASStorage is an implementation of [orasContent.ReadOnlyStorage]
|
|
// which always returns fixed content regardless of what descriptor it is
|
|
// given, which allows us to test that we're resilient against a remote
|
|
// OCI registry returning incorrect content for a blob.
|
|
type LyingORASStorage struct {
|
|
wantSize int64
|
|
}
|
|
|
|
var _ orasContent.ReadOnlyStorage = LyingORASStorage{}
|
|
|
|
// Exists implements [orasContent.ReadOnlyStorage].
|
|
func (s LyingORASStorage) Exists(ctx context.Context, target ociv1.Descriptor) (bool, error) {
|
|
// We pretend that everything exists
|
|
return true, nil
|
|
}
|
|
|
|
// Fetch implements [orasContent.ReadOnlyStorage].
|
|
func (s LyingORASStorage) Fetch(ctx context.Context, target ociv1.Descriptor) (io.ReadCloser, error) {
|
|
// We return a blob consisting of s.wantSize bytes of garbage, which
|
|
// the caller should therefore check and find that it mismatches
|
|
// the digest in the descriptor, unless the caller happens to ask
|
|
// for a digest that matches the garbage, which is vanishingly unlikely.
|
|
return io.NopCloser(io.LimitReader(rand.Reader, s.wantSize)), nil
|
|
}
|