Skip to content

Commit

Permalink
new functions: parse and get (#131)
Browse files Browse the repository at this point in the history
Provider-defined functions are new in Terraform 1.8+ (and opentofu 1.7+)

This is expected to be a replacement for the `oci_string` and `oci_ref`
datasources, which perform the same logic, but have their results
persisted in state, which adds to slowness.

Usage
```
output "parsed" {
  value = provider::oci::parse("cgr.dev/chainguard/static@sha256:abc...").digest  # sha256:abcdef...
}

locals {
  parsed = provider::oci::parse("cgr.dev/chainguard/static@sha256:abc...").digest  # sha256:abcdef...
  gotten = provider::oci::get("cgr.dev/chainguard/static").digest  # sha256:...
}
```

Docs


https://developer.hashicorp.com/terraform/plugin/framework/functions/concepts

https://developer.hashicorp.com/terraform/plugin/framework/functions/returns/object

https://developer.hashicorp.com/terraform/plugin/framework/functions/testing

---------

Signed-off-by: Jason Hall <jason@chainguard.dev>
  • Loading branch information
imjasonh authored May 7, 2024
1 parent 9cd02f4 commit 81b67cb
Show file tree
Hide file tree
Showing 12 changed files with 627 additions and 79 deletions.
9 changes: 4 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,10 @@ jobs:
matrix:
# list whatever Terraform versions here you would like to support
terraform:
- '1.0.*'
- '1.1.*'
- '1.2.*'
- '1.3.*'
- '1.4.*'
- '1.5.*'
- '1.6.*'
- '1.7.*'
- '1.8.*'
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
Expand Down
26 changes: 26 additions & 0 deletions docs/functions/get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "get function - terraform-provider-oci"
subcategory: ""
description: |-
Parses a pinned OCI string into its constituent parts.
---

# function: get





## Signature

<!-- signature generated by tfplugindocs -->
```text
get(input string) object
```

## Arguments

<!-- arguments generated by tfplugindocs -->
1. `input` (String) The OCI reference string to get.

26 changes: 26 additions & 0 deletions docs/functions/parse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "parse function - terraform-provider-oci"
subcategory: ""
description: |-
Parses a pinned OCI string into its constituent parts.
---

# function: parse





## Signature

<!-- signature generated by tfplugindocs -->
```text
parse(input string) object
```

## Arguments

<!-- arguments generated by tfplugindocs -->
1. `input` (String) The OCI reference string to parse.

141 changes: 141 additions & 0 deletions internal/provider/get_function.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package provider

import (
"context"
"fmt"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ function.Function = &GetFunction{}

func NewGetFunction() function.Function {
return &GetFunction{}
}

// GetFunction defines the function implementation.
type GetFunction struct{}

// Metadata should return the name of the function, such as parse_xyz.
func (s *GetFunction) Metadata(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "get"
}

// Definition should return the definition for the function.
func (s *GetFunction) Definition(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Parses a pinned OCI string into its constituent parts.",
Parameters: []function.Parameter{
function.StringParameter{
Name: "input",
Description: "The OCI reference string to get.",
},
},
Return: function.ObjectReturn{
AttributeTypes: map[string]attr.Type{
"full_ref": basetypes.StringType{},
"digest": basetypes.StringType{},
"tag": basetypes.StringType{},
"manifest": basetypes.ObjectType{AttrTypes: manifestAttribute.AttributeTypes},
"images": basetypes.MapType{ElemType: imageType},
"config": basetypes.ObjectType{AttrTypes: configAttribute.AttributeTypes},
},
},
}
}

// Run should return the result of the function logic. It is called when
// Terraform reaches a function call in the configuration. Argument data
// values should be read from the [RunRequest] and the result value set in
// the [RunResponse].
func (s *GetFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var input string
if ferr := req.Arguments.GetArgument(ctx, 0, &input); ferr != nil {
resp.Error = ferr
return
}

// Parse the input string into its constituent parts.
ref, err := name.ParseReference(input)
if err != nil {
resp.Error = function.NewFuncError(fmt.Sprintf("Failed to parse OCI reference: %v", err))
return
}

result := struct {
FullRef string `tfsdk:"full_ref"`
Digest string `tfsdk:"digest"`
Tag string `tfsdk:"tag"`
Manifest *Manifest `tfsdk:"manifest"`
Images map[string]Image `tfsdk:"images"`
Config *Config `tfsdk:"config"`
}{}

if t, ok := ref.(name.Tag); ok {
result.Tag = t.TagStr()
}

desc, err := remote.Get(ref,
remote.WithAuthFromKeychain(authn.DefaultKeychain),
remote.WithUserAgent("terraform-provider-oci"),
remote.WithContext(ctx))
if err != nil {
resp.Error = function.NewFuncError(fmt.Sprintf("Failed to get image: %v", err))
return
}

result.Digest = desc.Digest.String()
result.FullRef = ref.Context().Digest(desc.Digest.String()).String()

mf := &Manifest{}
if err := mf.FromDescriptor(desc); err != nil {
resp.Error = function.NewFuncError(fmt.Sprintf("Failed to parse manifest: %v", err))
return
}
result.Manifest = mf

if desc.MediaType.IsIndex() {
idx, err := desc.ImageIndex()
if err != nil {
resp.Error = function.NewFuncError(fmt.Sprintf("Failed to parse index: %v", err))
return
}
imf, err := idx.IndexManifest()
if err != nil {
resp.Error = function.NewFuncError(fmt.Sprintf("Failed to parse index manifest: %v", err))
return
}
result.Images = make(map[string]Image, len(imf.Manifests))
for _, m := range imf.Manifests {
if m.Platform == nil {
continue
}
result.Images[m.Platform.String()] = Image{
Digest: m.Digest.String(),
ImageRef: ref.Context().Digest(m.Digest.String()).String(),
}
}
} else if desc.MediaType.IsImage() {
img, err := desc.Image()
if err != nil {
resp.Error = function.NewFuncError(fmt.Sprintf("Failed to parse image: %v", err))
return
}
cf, err := img.ConfigFile()
if err != nil {
resp.Error = function.NewFuncError(fmt.Sprintf("Failed to parse config: %v", err))
return
}
cfg := &Config{}
cfg.FromConfigFile(cf)
result.Config = cfg
}

resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, &result))
}
Loading

0 comments on commit 81b67cb

Please sign in to comment.