Skip to content

OCI Support For Modules and Providers #1672

Open
@eunanio

Description

@eunanio

Summary

OCI support would allow users to package and distribute terraform modules and providers as oci-archives that can be tagged and distributed on any oci-compliant container registry such as AWS ECR and GitHub packages.

Problem Statement

Tofu currently relies on a limited number of registry implementations to host and manage modules and providers. This creates challenges when it comes to implementing a strategy for module version management across large projects and organizations

Supporting the oci archive format offers the user the ability to package, tag and distribute their modules via a wider variety of registries. Tagging should provide a simple and effective way of versioning modules and managing them across organizations

User-facing description

Provider setup

terraform {
	required_providers {
		hashicups = {
			source = "oci://ghcr.io/myorg/hashicups"
			version = "3.1.0"
		}
	}
}

Module setup

module "s3_bucket" {
	source = "oci://ghcr.io/myorg/aws-s3-bucket"
	version = "1.0.0"
	bucket_name = "test_bucket"
}

New oci commands would be added to support the feature

package

cmd:
tofu oci package --tag <uri>/<name>:<semver> --module ./modules/org_s3_bucket
Description:
To package the module the user must provide a tag and path to the file or directory. module and provider flags are mutually exclusive and are used to identify the type of package that is to be created. module expects a directory path while providers expects a path to the provider binary.
Flags:

  • --module: path to module directory
  • --provider: path to provider binary
  • --tag: uri for remote file repository

Pull

cmd:
tofu oci pull <tag>
Description:
Pull the image from a remote repository and and decompress the module into .terraform/modules manually

Login

cmd:
tofu oci login --username $USER --password $PASSWORD <uri>
Description:
Saves the credentials as base64 encoded string of $USER:$PASSWORD
Flags:

  • --username: username for remote repository
  • --password: password for remote repository
  • --password-stdin: password value piped in from standard in

Push

cmd:
tofu oci push <tag>
Description:
Pushes the tagged package to the remote repository.

Technical Description

Packaging

In order to package our module we must compress the module directory into a tarball that will represent a layer or blob in our image. When compressing your module directory, the parent directory of the module should be included in the archive and its name should conform to the name set when tagging the package.

This is to ensure that when we decompress the blob to .terraform/modules/ it should create a referable path such as .terraform/modules/aws-s3-bucket in the example given above.

All blobs should use the sha256 hash as their filename and be located within /blobs/sha256/

each package should be structured as seen below.

blobs/sha256/
	- 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
	- 2fa21be67085dac1155fd09abfbcce31c9923ba3d1273c8046f9e50cfa7707b4 # This is your module 
	- e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f # This is your manifest
index.json

packages must contain:
- index at the root of the package named index.json
- At least one manifest defined within index.json and stored as a blob
- config stored as a blob
- tarball with the contents of given module stored as a blob

Index

Media-Type: application/vnd.oci.image.index.v1+json

The index file allows you to declare multiple manifests and you can use architecture to identify the correct manifest. This can be used to differentiate between provider versions depending on architecture Further Info can be found HERE

Example Index

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "size": 7143,
      "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    }
  ],
}

Manifests

Media-Type: application/vnd.oci.image.manifest.v1+json

The manifest file is what's used to describe our image and is used to determine the location of blobs that the runtime must download. it should follow image-spec outlined HERE

Example Manifest

{
  "schemaVersion": "2",
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.empty.v1+json",
    "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
    "size": 2
  },
  "layers": [
    {
      "mediaType": "application/vnd.tofu.module.v1.tar",
      "digest": "sha256:2fa21be67085dac1155fd09abfbcce31c9923ba3d1273c8046f9e50cfa7707b4",
      "size": 2130
    }
  ]
}

Media Types
application/vnd.oci.image.* is reserved for the spec and should not be extended. Instead we will define our own media types to describe modules and provider layers

  • application/vnd.tofu.module.v1.tar
    • Primary module in package
  • application/vnd.tofu.provider.v1.tar
    • layer containing provider
  • application/vnd.tofu.config.v1+json
    • (Optional) describes metadata about the package

Config
Config file is a required part of the spec. In the example above we indicate that our blob is a file containing an empty JSON object {}. This could be utilized to facilitate more advanced features in future iterations

Registry Communication

All HTTP endpoints for registries can be found HERE and is essential reading if you are going to implement pull and push functionality

Authentication

Most registries use some form of basic auth for their authentication. when the user executes the login command. The login info should be encoded as a base64 string and stored as a token value within ~/.terraform.d/credentials.tfrc.json with a URI being used as the key. This token should then be used when making any of the repository operations.

Pulling

This functionality should be implemented as a new module getter. Call HEAD /v2/<name>/manifests/<tag> to check if the manifest for this tag exists. If so use a GET request to retrieve the file and iterate through the layers array to identify the digest of our module blob.

Using the digest we can call /v2/<name>/blobs/<digest> to download our compressed module. Its contents should be decompressed into ./.terraform/modules/<name>

Pushing

When pushing a new module we must always start by uploading all blob content. The monolithic approach to this is to iterate through each blob and POST the contents to POST /v2/<name>/blobs/uploads/?digest=<digest>. If successful it should return a 201 created status code

After uploading all the blobs, we must upload the manifest to PUT /v2/<name>/manifests/<ref> where ref represents the version tag we wish to assign.

Rationale and alternatives

There has been a number of alternative approaches suggested within #308 My aim with this feature is to maximize registry support with the least amount of divergence in tofu behaviour. I believe this gives us the best chance of success and the feature as a whole will drive tofu adoption.

Downsides

None identified.

Unresolved Questions

No response

Related Issues

Proof of Concept

TBC

Metadata

Metadata

Assignees

No one assigned

    Labels

    pending-decisionThis issue has not been accepted for implementation nor rejected. It's still open to discussion.rfc

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions