Skip to content

Commit

Permalink
Working oci module support
Browse files Browse the repository at this point in the history
  • Loading branch information
eunanio committed Nov 17, 2024
1 parent 4288fdf commit 5fd9333
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 25 deletions.
10 changes: 2 additions & 8 deletions cmd/tofu/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,20 +356,14 @@ func initCommands(
// OCI
//-----------------------------------------------------------

// "oci package": func() (cli.Command, error) {
// return nil, nil
// },

// "oci pull": func() (cli.Command, error) {
// return nil, nil
// },

// Push a module direcotry to an OCI registry
"oci push": func() (cli.Command, error) {
return &command.OciPushCommand{
Meta: meta,
}, nil
},

// Pull a module from an OCI registry manually
"oci pull": func() (cli.Command, error) {
return &command.OciPullCommand{
Meta: meta,
Expand Down
7 changes: 6 additions & 1 deletion internal/command/oci_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package command

import (
"fmt"
"slices"

"github.com/opentofu/opentofu/internal/oci"
)
Expand All @@ -19,7 +20,7 @@ func (c *OciPushCommand) Run(args []string) int {
ref := args[0]
path := args[1]

if err := oci.PushPackagedModule(ref, path); err != nil {
if err := oci.PushPackagedModule(ref, path, isInsecure(args)); err != nil {
c.Ui.Error(err.Error())
return 1
}
Expand All @@ -43,3 +44,7 @@ func validateArgs(args []string) error {

return nil
}

func isInsecure(args []string) bool {
return slices.Contains(args, "--insecure")
}
19 changes: 9 additions & 10 deletions internal/oci/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import (
"github.com/opencontainers/image-spec/specs-go"
spec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opentofu/opentofu/internal/ociclient"
"oras.land/oras-go/v2/content"
)

func PushPackagedModule(ref string, src string) error {
func PushPackagedModule(ref string, src string, insecure bool) error {
client := ociclient.New()
err := client.GetCredentials(ref)
if err != nil {
Expand All @@ -20,13 +19,13 @@ func PushPackagedModule(ref string, src string) error {
if err != nil {
return err
}
dd, data_opts := createBlobPushOptions(ociclient.TOFU_LAYER_TYPE, ref, data)
dd, data_opts := createBlobPushOptions(ociclient.TOFU_LAYER_TYPE, ref, data, insecure)
if dd.Size <= 0 {
return fmt.Errorf("invalid digest")
}

// empty config, we can populate metadata if needed in the future.
cd, config_opts := createBlobPushOptions(spec.MediaTypeEmptyJSON, ref, []byte("{}"))
cd, config_opts := createBlobPushOptions(spec.MediaTypeEmptyJSON, ref, []byte("{}"), insecure)

manifest := spec.Manifest{
Versioned: specs.Versioned{
Expand All @@ -38,7 +37,7 @@ func PushPackagedModule(ref string, src string) error {
Layers: []spec.Descriptor{dd},
}

manifest_opts := createManifestPushOptions(manifest, ref)
manifest_opts := createManifestPushOptions(manifest, ref, insecure)

err = client.PushBlob(config_opts)
if err != nil {
Expand All @@ -58,22 +57,22 @@ func PushPackagedModule(ref string, src string) error {
return nil
}

func createBlobPushOptions(mediaType string, ref string, blob []byte) (digest spec.Descriptor, opts ociclient.PushBlobOptions) {
digest = content.NewDescriptorFromBytes(mediaType, blob)
func createBlobPushOptions(mediaType string, ref string, blob []byte, insecure bool) (digest spec.Descriptor, opts ociclient.PushBlobOptions) {
digest = ociclient.GetBlobDescriptor(mediaType, blob)
opts = ociclient.PushBlobOptions{
Ref: ref,
Blob: blob,
Insecure: false,
Insecure: insecure,
}

return digest, opts
}

func createManifestPushOptions(manifest spec.Manifest, ref string) ociclient.PushManifestOptions {
func createManifestPushOptions(manifest spec.Manifest, ref string, insecure bool) ociclient.PushManifestOptions {
return ociclient.PushManifestOptions{
Manifest: manifest,
Ref: ref,
Insecure: false,
Insecure: insecure,
}
}

Expand Down
23 changes: 17 additions & 6 deletions internal/ociclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import (
"oras.land/oras-go/v2/registry/remote/credentials"
)

// This is the oci client that will be used to interact with the OCI/Docker registries.
// fairly simple client implementation with auth support and basic blob and manifest operations.
type Client struct {
Credentials *Credentials
}
type PullBlobOptions struct {
Ref string
Descriptor spec.Descriptor
Expand All @@ -34,16 +39,13 @@ type PushManifestOptions struct {
Insecure bool
}

type Client struct {
Credentials *Credentials
}

type Credentials struct {
Username string
Password string
encoded string
}

// Custom layer types helps us to identify tofu modules from other artifacts
const (
TOFU_LAYER_TYPE = "application/vnd.tofu.module.v1.tar+gzip"
ARTIFACT_TYPE = "application/vnd.tofu.module.manifest.v1+json"
Expand All @@ -53,6 +55,7 @@ func New() *Client {
return &Client{}
}

// All OCI Registry API requests should support basic auth as part of the spec
func (c *Client) SetBasicAuth(username, password string) {
userpass := fmt.Sprintf("%s:%s", username, password)
encoded := base64.StdEncoding.EncodeToString([]byte(userpass))
Expand All @@ -64,6 +67,9 @@ func (c *Client) SetBasicAuth(username, password string) {
}
}

// GetCredentials retrieves the credentials for the given registry from the docker config
// and sets the basic auth header for the client. This should make integration into existing CI/CD workflows easier.
// We may need support for allowing the user to set the credentials manually in the future.
func (c *Client) GetCredentials(ref string) error {
ctx := context.Background()
reference, err := ParseRef(ref)
Expand All @@ -88,6 +94,7 @@ func (c *Client) GetCredentials(ref string) error {
return nil
}

// PullBlob retrieves the blob content from the given ref and descriptor.
func (c *Client) PullBlob(opts PullBlobOptions) ([]byte, error) {
ref, err := ParseRef(opts.Ref)
if err != nil {
Expand Down Expand Up @@ -126,6 +133,7 @@ func (c *Client) PullBlob(opts PullBlobOptions) ([]byte, error) {
return data, nil
}

// PullManifest retrieves the manifest for the given ref. This would need to be modified to support index manifests in the future.
func (c *Client) PullManifest(opts PullManifestOptions) (*spec.Manifest, error) {
ref, err := ParseRef(opts.Ref)
if err != nil {
Expand Down Expand Up @@ -154,7 +162,7 @@ func (c *Client) PullManifest(opts PullManifestOptions) (*spec.Manifest, error)

if resp.StatusCode != 200 {
if resp.StatusCode == http.StatusUnauthorized {
return nil, fmt.Errorf("unauthorized, please use nori login to authenticate")
return nil, fmt.Errorf("unauthorized, please use docker login to authenticate")
}

return nil, fmt.Errorf("cannot to pull manifest: %s", resp.Status)
Expand All @@ -174,6 +182,7 @@ func (c *Client) PullManifest(opts PullManifestOptions) (*spec.Manifest, error)
return manifest, nil
}

// PushBlob uploads the given blob to the registry.
func (c *Client) PushBlob(opts PushBlobOptions) error {
ref, err := ParseRef(opts.Ref)
if err != nil {
Expand Down Expand Up @@ -238,6 +247,8 @@ func (c *Client) PushBlob(opts PushBlobOptions) error {
return nil
}

// Uploads the manifest to the registry. This normally acts as the final action in the upload process. Depending on the registry
// they may validate that all the layers are uploaded before allowing the manifest to be uploaded.
func (c *Client) PushManifest(opts PushManifestOptions) error {
ref, err := ParseRef(opts.Ref)
if err != nil {
Expand Down Expand Up @@ -284,7 +295,7 @@ func (c *Client) PushManifest(opts PushManifestOptions) error {

if resp.StatusCode != 201 {
if resp.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("unauthorized, please use nori login to authenticate")
return fmt.Errorf("unauthorized, please use docker login to authenticate")
}
return fmt.Errorf("failed to push manifest: %s", resp.Status)
}
Expand Down

0 comments on commit 5fd9333

Please sign in to comment.