Skip to content

Commit

Permalink
getproviders: Initial real implementation of PackageOCIObject
Browse files Browse the repository at this point in the history
This now uses the included OCI distribution client to pull the object
by iterating over all of the directory entries in all of the layers that
were included in the manifest.

This is a minimal initial implementation for experimenting with, but it
has numerous TODOs and FIXMEs to attend to before we could use this in
a non-experimental capacity.

Co-authored-by: AbstractionFactory <[email protected]>
Signed-off-by: Martin Atkins <[email protected]>
  • Loading branch information
apparentlymart and abstractionfactory committed Nov 21, 2024
1 parent 1836494 commit 057345b
Showing 1 changed file with 101 additions and 8 deletions.
109 changes: 101 additions & 8 deletions internal/getproviders/package_location_oci_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ package getproviders
import (
"context"
"fmt"
"io"
"os"
"path/filepath"

"github.com/opentofu/libregistry/registryprotocols/ociclient"
)
Expand All @@ -20,10 +23,8 @@ import (
// manifest, because the decision about which platform to select should
// have already been made by whatever generates an object of this type.
type PackageOCIObject struct {
// imageMetadata describes both the address of the specific object that's
// being installed and the digests of all of the blobs the serve as its
// layers.
imageMetadata ociclient.OCIImageMetadata
repositoryAddr OCIRepository
imageManifestDigest ociclient.OCIDigest

// client is the OCI client that should be used to retrieve the
// object's layers.
Expand All @@ -33,10 +34,102 @@ type PackageOCIObject struct {
var _ PackageLocation = PackageOCIObject{}

func (p PackageOCIObject) String() string {
return p.imageMetadata.Addr.String()
return fmt.Sprintf("%s@%s", p.repositoryAddr, p.imageManifestDigest)
}

func (p PackageOCIObject) InstallProviderPackage(_ context.Context, _ PackageMeta, _ string, _ []Hash) (*PackageAuthenticationResult, error) {
// TODO: Implement
return nil, fmt.Errorf("installing OCI distribution objects as provider packages is not yet implemented")
func (p PackageOCIObject) InstallProviderPackage(ctx context.Context, meta PackageMeta, targetDir string, allowedHashes []Hash) (*PackageAuthenticationResult, error) {
// FIXME: This API cannot currently return warnings, so we just discard them.
files, _, err := p.client.PullImageWithImageDigest(ctx, ociclient.OCIAddrWithDigest{
OCIAddr: p.repositoryAddr.toClient(),
Digest: p.imageManifestDigest,
})
if err != nil {
return nil, fmt.Errorf("failed to pull OCI object %s: %w", p.String(), err)
}
defer files.Close()

// Since we'll be assembling the package directory gradually as we iterate over
// the directory entries in the layers, we'll need to make the containing directory
// separately first.
const modeUserWritableOtherReadExecutable = 0755
err = os.MkdirAll(targetDir, modeUserWritableOtherReadExecutable)
if err != nil {
return nil, fmt.Errorf("failed to create provider package cache directory %s: %w", targetDir, err)
}

for {
haveNext, err := files.Next()
if err != nil {
return nil, fmt.Errorf("failed to pull next file for OCI object %s: %w", p.String(), err)
}
if !haveNext {
break // no more layers
}

info := files.FileInfo()
targetFilename := filepath.Join(targetDir, info.Name())
if !filepath.IsLocal(targetFilename) {
return nil, fmt.Errorf("layer for %s contains invalid file path %q", p.String(), targetFilename)
}
// FIXME: Need to also check that we're not writing through a symlink, and
// possibly other hazards.
// Ideally we'd use the result of this proposal: https://github.com/golang/go/issues/67002
//
// FIXME: Must also check that info.Mode.Perm is something reasonable. We
// only really need to support the subset of modes that git supports:
// non-executable regular file, executable regular file, directory, symlink.

if info.IsDir() {
err = os.Mkdir(targetFilename, info.Mode().Perm())
if err != nil {
return nil, fmt.Errorf("while extracting OCI object %s: %w", p.String(), err)
}
} else {
// TODO: What if it's a symlink?

var f *os.File
f, err = os.OpenFile(targetFilename, os.O_CREATE|os.O_TRUNC|os.O_RDWR, info.Mode().Perm())
if err != nil {
return nil, fmt.Errorf("while extracting OCI object %s: %w", p.String(), err)
}
defer f.Close()

_, err = io.Copy(f, files)
if err != nil {
return nil, fmt.Errorf("while extracting OCI object %s: %w", p.String(), err)
}
}

// TODO: Is it important to retain any other metadata from the entries, such as
// their last modified times? Our checksum algorithm doesn't care about it
// but maybe something else will.

suitableHashCount := 0
for _, hash := range allowedHashes {
if !hash.HasScheme(HashSchemeZip) {
suitableHashCount++
}
}
if suitableHashCount > 0 {
localLoc := PackageLocalDir(targetDir)
var matches bool
if matches, err = PackageMatchesAnyHash(localLoc, allowedHashes); err != nil {
return nil, fmt.Errorf(
"failed to calculate checksum for %s %s package at %s: %w",
meta.Provider, meta.Version, meta.Location, err,
)
} else if !matches {
return nil, fmt.Errorf(
"the local package for %s %s doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://opentofu.org/docs/language/files/dependency-lock/#checksum-verification",
meta.Provider, meta.Version,
)
}
}
}

if meta.Authentication != nil {
return meta.Authentication.AuthenticatePackage(p)
}
//nolint:nilnil // this API predates our use of this linter and callers rely on this behavior
return nil, nil
}

0 comments on commit 057345b

Please sign in to comment.