-
Notifications
You must be signed in to change notification settings - Fork 907
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Experimental support for using an OCI repository as an alternative provider installation method #2170
base: main
Are you sure you want to change the base?
Conversation
d64d3a7
to
2e883cb
Compare
c08e220
to
ba2fce1
Compare
Just a note to self for later: Upstream in Go there's a recently-accepted proposal to add a safer filesystem abstraction called This likely won't be available soon enough for us to use it for our first round here, but we can try to design our extraction code to be compatible with the shape of that API so that we can adopt it later, so that we won't need to maintain a "safe file I/O" cross-platform abstraction forever ourselves. |
This extends our existing concept of "experimental features" to the CLI Configuration language, so that we can (in future commits) have configuration features that are not yet fully implemented or not yet ready to be constrained by our compatibility promises. As with all other uses of "experiments" in this codebase, these are available only if explicitly activated at build time and so experimental CLI config features would not be available in official release builds. Signed-off-by: Martin Atkins <[email protected]>
0e21c3c
to
b8a81df
Compare
This now has some basic real implementations of both the provider source and the "package location" type. As things currently stand there are two quirks:
I've run out of time for today, but I'll probably continue poking at this tomorrow. I'm currently testing using the following root module: terraform {
required_providers {
assume = {
source = "ghcr.io.opentofu-oci.example.com/apparentlymart/assume"
}
}
} ...and the following CLI Configuration to opt in to the ability to install a provider directly from an OCI repository without having to set up an provider_installation {
direct {
oci_registry_experiment = true
}
} |
@apparentlymart thanks, I'll try to debug why this is happening and I'll also add some content-type checks, maybe we need to ignore layers that are not tarballs. |
@apparentlymart found the bug, libregistry had a foreach-variable-reference issue when selecting the multi-arch manifest. Try running this:
|
9f93b4c
to
a7dcf9c
Compare
With the updated
Since we don't currently have any means to distribute the provider developer's signed checksums this currently falls into the same warning as for the other alternative installation methods where the dependency lock file ends up incomplete and needs to be explicitly completed using the Unfortunately because that command intentionally bypasses the CLI-configuration-level provider installation settings (because by default it assumes you want to pull the checksums from the origin registry) that command doesn't currently work, but if we decide to move forward with this design then I don't expect it will be too difficult to update that command to use the new (We still have the thing about returning the digest-based reference instead of the tag-based reference from the metadata request, but we're going to deal with that part soon with some further updates to libregistry.) |
This is a new provider installation method which is similar to network_mirror but uses a set of conventions for fetching a provider from an OCI distribution repository, instead of a wholly OpenTofu-specific protocol. This commit only introduces the CLI configuration block and the decoding and validation of its contents, and only for experimental builds. If someone tries to use this in an experimental build then it will return an error saying that the new method isn't supported yet. Full support for it will follow in later commits. Signed-off-by: Martin Atkins <[email protected]>
This is a stub for a future provider installation method that will use a user-provided template to translate a provider source address into an OCI distribution repository address and then use the OCI distribution registry protocol to obtain package metadata and, eventually, the packages themselves. Signed-off-by: Martin Atkins <[email protected]>
Previously we treated PackageLocation only as pure data, describing a location from which something could be retrieved. Unexported functions in package providercache then treated PackageLocation values as a closed union dispatched using a type switch. That strategy has been okay for our relatively-generic package locations so far, but in a future commit we intend to add a new kind of package location referring to a manifest in an OCI distribution repository, and installing from _that_ will require a nontrivial amount of OCI-distribution-specific logic that will be more tightly coupled to the getproviders.Source that will return such locations, and so we're switching to a model where _the location object itself_ is responsible for knowing how to install a provider package from its location, as a method of PackageLocation. The implementation of installing from each location type now moves from package providercache to package getproviders, which is arguably a better thematic home for that functionality anyway. For now these remain tested only indirectly through the tests in package providercache, since we didn't previously have any independent tests for these unexported functions. We might want to add more tightly-scoped unit tests for these to package getproviders in future, but for now this is not materially worse than it was before. Signed-off-by: Martin Atkins <[email protected]>
This package location type will be used only by our new "OCI mirror" provider installation method, to allow it to describe installation of a container image with a specific digest from a specific repository on a specific registry. As with all of the "OCI mirror" functionality for now, this is starting just as a stub with implementation to follow later once we have an OCI distribution client implementation available in this codebase. Signed-off-by: Martin Atkins <[email protected]>
We'll be mapping from provider source address to OCI distribution repository address using a user-provided template, so we need to make sure that the result is actually a valid address after evaluating it. One potential cause of an invalid result is from trying to install a provider whose namespace or type name contains non-ASCII characters, since OpenTofu allows any character that would be valid in an "internationalized domain name" while OCI distribution only allows ASCII letters and numerals. Therefore we have a special error message for that case, but also a more general error message for situations where the literal parts of the template are somehow wrong. It's not ideal to only be able to map a subset of valid provider addresses onto OCI repository addresses, but since non-ASCII characters in provider source addresses seem pretty rare in practice we'll accept this for now and see if we get feedback about it. If someone does need to use such a provider from an OCI mirror in the meantime, a workaround would be to use an oci_mirror installation method with the exact provider address in the "include" argument, and then use a totally-literal template that maps to an address that only includes ASCII characters. Signed-off-by: Martin Atkins <[email protected]>
This is a light implementation of the "Level 1" profile of URI Templates as described in RFC 6570. For automatic installation of providers from OCI registries we need to be able to generate an OCI distribution repository address based on a provider source address, since different OCI distribution registry implementations have different restrictions on which name shapes they will allow. For the "oci_mirror" provider installation method we used HCL templates for this, which made sense in that context because installation methods are specified in the HCL-based CLI configuration. However, for automatic installation using an OCI registry as the provider registry for a particular hostname the mapping from provider source string to OCI repository address will live in the host's service discovery document, which is retrieved over the network and so if we used HCL there then we'd be exposing the HCL parser to arbitrary input retrieved over the network. We ought to use something less heavy and more standardized for a wire protocol, and RFC 6570's URI template syntax provides a plausible compromise of something that is standardized outside of OpenTofu and is relatively minimal in what it provides, particularly at level 1. Technically an OCI distribution repository address (at least, the way we're defining them) is _not_ actually a URI, but it's similar enough in shape to a URI that we can use this spec as a starting point, and also set us up to use the URI template syntax for similar needs in the service discovery protocol in future, beyond the current OCI-specific goals. Signed-off-by: Martin Atkins <[email protected]>
…stry In future commits this experiment will enable support for an alternative provider registry protocol defined in terms of the OCI distribution specification, instead of OpenTofu's own provider registry protocol. This commit only includes some temporary syntax for opting in to the experiment, which for now will just cause an immediate error if enabled. The actual implementation of this new setting is still to come. Signed-off-by: Martin Atkins <[email protected]>
This turned out to make more sense architecturally to live in the svchost module where the service discovery protocol is implemented, since a service discovery document is where the URI templates would appear. Signed-off-by: Martin Atkins <[email protected]>
In experimental builds this allows using the CLI configuration to opt-in to a different treatment of the "direct" installation method where we support both "providers.v1" and "oci-providers.v1" services and, for the latter, treat the service discovery string as a template resolving to an OCI repository address whose images are to be used as provider packages. For now this uses a forked version of the upstream "svchost" module, with the URI template and OCI registry address support added. If we decide to move forward with this then we'll need to decide whether to make this fork permanent (presumably moving it into the opentofu organization) or to find some other approach to handle it outside of the svchost module. Signed-off-by: Martin Atkins <[email protected]>
Previously we were using a string consisting of a hostname and a name concatenated with a slash, but the OCI distribution spec doesn't define any such address syntax -- that's something we've essentially invented for OpenTofu, albeit with strong inspiration from Docker -- so we'll use a struct type to represent these addresses internally and then deal with parsing the slash-concatenated form at the same time as we're doing all of the other parsing, for consistency. This means that it's now the responsibility of whatever is dealing with the address template to also deal with the splitting of the hostname and the repository name. Signed-off-by: Martin Atkins <[email protected]>
In addition to the OCI mirror and the ability to use service discovery for a fully-custom mapping from OpenTofu provider address to OCI repository address on a particular hostname, this introduces a special hostname suffix that can be used after any existing OCI registry hostname to achieve an opinionated default mapping to that registry's namespace of OCI repositories. We don't actually have a real domain to use for this yet, so for now we're using opentofu-oci.example.com as a placeholder. If we decide to move forward with this approach then we'd switch this to using a domain we actually own. Signed-off-by: Martin Atkins <[email protected]>
a38298f
to
d61e0f2
Compare
We actually got the tag vs. digest thing sorted today after all, so that's now in and this is basically working for a first installation from nothing. The verification of packages against checksums already recorded in the dependency lock file on subsequent installation doesn't seem to be working quite right yet: it's reporting a checksum failure even though the checksum actually matches. I'll figure out what's causing that next. Edit: I fixed the checksum verification problem, so this now supports reinstallation of a provider version previously recorded in the lock file. With that done, I think this is now minimally functional, and so I'm going to start researching what our options might be for actually authenticating the packages (to achieve something similar to the GPG-based authentication OpenTofu does for providers installed using its own registry protocol, but in a more container-ecosystem-idiomatic way). |
This is an initial implementation of OCIMirrorSource in terms of libregistry's OCI client code. It has the minimum required to select a suitable image manifest from a multi-platform manifest and return it as a PackageOCIObject location. It does not yet support package authentication at all, and it also currently fails because libregistry's client is returning a tag-based image manifest address, rather than the digest for the specific image it returned. We'll deal with both of those concerns in future commits. Co-authored-by: AbstractionFactory <[email protected]> Signed-off-by: Martin Atkins <[email protected]>
d61e0f2
to
057345b
Compare
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]>
057345b
to
5fcfef9
Compare
Some early notes on package authentication. Nothing below is a decision; I'm just writing down what I've learned so far for later reference. The prevailing conventions for container authentication are those implemented by Cosign. Here's what I've understood so far about the shape of that:
As a starting point I'm going to experiment with this new checksum type for capturing image manifest digests directly into the dependency lock file, purely as an additional verification method. I need to study Cosign's details more before getting stuck into the actual signature verification part, and I expect we'll need to grow the libregistry OCI client API a little more to expose the additional operations needed to find and fetch the signature and other cosign metadata for a multi-platform image manifest. |
Cross-linking the cosign issue: #307 |
fd0d2c0
to
84e0151
Compare
I encountered an interesting new design challenge while working through implementing the new hash scheme today: Previously we had one hash scheme that supported everything (the The new scheme I've added today, which for now is called This has a few different interesting implications, but the most troublesome one for right now is that the current package authentication/verification model doesn't differentiate between pre-installation and post-installation authentication/verification: it expects to be able to perform all checks before installation (e.g. based on the Therefore I expect to need to make some tweaks to the authentication model so that we can more cleanly separate the pre-install and post-install checks. For OCI manifest locations in particular we can only check It's getting near the end of my work week now so I don't really have time to get into solving that properly today, but I'll think about it some more next week. |
An OCI registry talks about the content of objects using its own special digest scheme, and we can't derive a "h1:" checksum directly from those, so instead we'll follow a similar approach as with the legacy "zh:" scheme we use to work around some comparable limitations of the OpenTofu provider registry protocol where it only knows how to talk about whole-zip-file checksums, and not checksums of their contents. As of this commit nothing is actually using this scheme, but OCIMirrorSource and PackageOCIObject will make more use of it in future commits to allow us to pre-populate hashes for all platforms whenever a container image is validly signed. Signed-off-by: Martin Atkins <[email protected]>
Our PackageAuthentication model was designed for a world where at least some authentication tasks need to wait until after we've fetched a package from a remote location, but for OCIMirrorSource we have the advantage that the repository is content-addressable and so we can check ahead of time whether the platform-specific manifest is signed and then we only need to make sure during installation that the manifest and layers actually match their digests. Therefore this introduces NewPrecheckedAuthentication to allow for this special situation where all of the authentication happens inside a Source.PackageMeta, rather than some being delayed until the package has been fetched. This PackageAuthentication implementation just returns exactly what it was given at instantiation, and so OCIMirrorSource.PackageMeta can for now report just that the checksum was verified because fetching the package implicitly verifies its checksum. In future commits we'll also integrate Cosign signature checks into OCIMirrorSource.PackageMeta, and if that succeeds we'll be able to report that the package is to be treated as signed as long as its checksum is valid during installation. Signed-off-by: Martin Atkins <[email protected]>
84e0151
to
ca2e9f5
Compare
After some further consideration today I realized that the main thing that's special about installing from an OCI repository is that the repository is content-addressable and so installing from the "location" returned by Therefore I've added a new special Since we don't yet have enough Tomorrow I'm going to investigate exactly what additional information we'd need to determine whether a particular manifest has a signature and, if so, to fetch the information required to verify that signature. At RFC time we'll also need to ponder whether the environment variable That environment variable was introduced to make OpenTofu fail if any package from the main OpenTofu registry lacks a valid signature, and so I expect that anyone who has set it would prefer similar treatment for installation from OCI repositories too, but the environment variable name is a bit of a misnomer for that case since we won't actually be using GPG for OCI object signing. For the sake of this experimental implementation I'm going to ignore that question and just make it return signingSkipped when there doesn't seem to be any signing metadata available for a particular package, since that's a plausible place to start. Despite that, it won't be effective for an attacker to delete the signature metadata for a previously-signed manifest, because we'll use the manifest digests from the dependency lock file to ensure that the manifests all still match what we had previously verified as signed. |
@apparentlymart there is an older signature verification mechanism that isn't as complex as cosign (which won't be available in libregistry as long as the outstanding issues regarding stability and support are resolved in their go library). I never managed to get it to work on previous projects (mainly due to a lack of tooling a the time), but Docker Content Trust may be worth looking at. |
My intention with Cosign for the moment was just to understand how their verification method works as a protocol, rather than as a specific Go library implementation, since we might be able to reimplement just that part either directly in OpenTofu or in libregistry, without depending on the reference implementation at all. Of course, whether that is viable will depend on how complicated the protocol is and how many different variations it supports. If it seems like a separate verification-only implementation would either be too complex or too hard to maintain over time then of course I'd consider other alternatives instead. |
For now this is just a prototype of part of what we're discussing over in #2163.
It might eventually turn into a real implementation, but for now the goal is to just get the basic behavior in place so that we can try it out against some real OCI registry implementations, and we can get some experience with the new workflow for building and publishing providers into an OCI registry before we finalize the design in the RFC.
For now the new feature is guarded by the "experiments" mechanism, so it's only available when OpenTofu is built with experiments enabled. This is to reinforce that nothing here is final yet. It remains to be seen whether we'll merge with it still guarded in this way or if we'll wait until the design is finalized enough to enable it in release builds.
You can enable experiments when building the
tofu
executable, like this:go install -ldflags="-X 'main.experimentsAllowed=yes'" ./cmd/tofu