Skip to content
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

add cosign_copy #28

Merged
merged 8 commits into from
Jun 3, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/resources/copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "cosign_copy Resource - terraform-provider-cosign"
subcategory: ""
description: |-
This copies the provided image digest cosign copy.
---

# cosign_copy (Resource)

This copies the provided image digest cosign copy.



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `destination` (String) The destination repository.
- `source` (String) The digest of the container image to copy.
imjasonh marked this conversation as resolved.
Show resolved Hide resolved

### Read-Only

- `copied_ref` (String) This always matches the input digest, but is a convenience for composition.
- `id` (String) The immutable digest this resource copies, along with its signatures, etc.


1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewAttestResource,
NewSignResource,
NewCopyResource,
}
}

Expand Down
179 changes: 179 additions & 0 deletions internal/provider/resource_copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package provider

import (
"context"
"errors"
"fmt"

"github.com/chainguard-dev/terraform-provider-oci/pkg/validators"
"github.com/google/go-containerregistry/pkg/name"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/copy"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
)

var (
_ resource.Resource = &CopyResource{}
_ resource.ResourceWithImportState = &CopyResource{}
)

func NewCopyResource() resource.Resource {
return &CopyResource{}
}

type CopyResource struct {
}

type CopyResourceModel struct {
Id types.String `tfsdk:"id"`
Source types.String `tfsdk:"source"`
Destination types.String `tfsdk:"destination"`

CopiedRef types.String `tfsdk:"copied_ref"`
}

func (r *CopyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_copy"
}

func (r *CopyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "This copies the provided image digest cosign copy.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The immutable digest this resource copies, along with its signatures, etc.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"source": schema.StringAttribute{
MarkdownDescription: "The digest of the container image to copy.",
Optional: false,
Required: true,
Validators: []validator.String{validators.DigestValidator{}},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"destination": schema.StringAttribute{
MarkdownDescription: "The destination repository.",
Optional: false,
Required: true,
Validators: []validator.String{validators.RepoValidator{}},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"copied_ref": schema.StringAttribute{
MarkdownDescription: "This always matches the input digest, but is a convenience for composition.",
Computed: true,
},
},
}
}

func (r *CopyResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
}

func doCopy(ctx context.Context, data *CopyResourceModel) (string, error) {
digest, err := name.NewDigest(data.Source.ValueString())
if err != nil {
return "", errors.New("Unable to parse image digest")
}

ropts := options.RegistryOptions{
KubernetesKeychain: true,
}
dst, err := name.NewRepository(data.Destination.ValueString())
if err != nil {
return "", errors.New("Unable to parse destination repository")
}

dstDig := dst.Digest(digest.DigestStr()).String()
if err := copy.CopyCmd(ctx, ropts, digest.String(), dstDig, false, false); err != nil {
return "", fmt.Errorf("Unable to copy image: %w", err)
}
return dstDig, nil
}

func (r *CopyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data *CopyResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

digest, err := doCopy(ctx, data)
if err != nil {
resp.Diagnostics.AddError("error while Copying", err.Error())
return
}

data.Id = types.StringValue(digest)
data.CopiedRef = types.StringValue(digest)

tflog.Trace(ctx, "created a resource")
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *CopyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *CopyResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

digest, err := name.NewDigest(data.Source.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse image digest: %v", err))
return
}
data.Id = types.StringValue(digest.String())
data.CopiedRef = types.StringValue(digest.String())

// TODO(mattmoor): should we check that the Copyature didn't disappear?

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *CopyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data *CopyResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

digest, err := doCopy(ctx, data)
if err != nil {
resp.Diagnostics.AddError("error while Copying", err.Error())
return
}

data.Id = types.StringValue(digest)
data.CopiedRef = types.StringValue(digest)

tflog.Trace(ctx, "updated a resource")
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *CopyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data *CopyResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

// TODO: If we ever want to delete the image from the registry, we can do it here.
}

func (r *CopyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
112 changes: 112 additions & 0 deletions internal/provider/resource_copy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package provider

import (
"fmt"
"os"
"testing"

ocitesting "github.com/chainguard-dev/terraform-provider-oci/testing"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestAccResourceCosignCopy(t *testing.T) {
if _, ok := os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_URL"); !ok {
t.Skip("Unable to keylessly sign without an actions token")
}

src, cleanup := ocitesting.SetupRepository(t, "src")
defer cleanup()

dst, cleanup := ocitesting.SetupRepository(t, "dst")
defer cleanup()

// Push an image by digest to the source repo.
img1, err := random.Image(1024, 1)
if err != nil {
t.Fatal(err)
}
dig1, err := img1.Digest()
if err != nil {
t.Fatal(err)
}
ref1 := src.Digest(dig1.String())
if err := remote.Write(ref1, img1); err != nil {
t.Fatal(err)
}

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{{
// Sign and copy the image, then verify the copy's signature.
Config: fmt.Sprintf(`
resource "cosign_sign" "foo" {
image = %q
}
imjasonh marked this conversation as resolved.
Show resolved Hide resolved

resource "cosign_attest" "foo" {
image = cosign_sign.foo.signed_ref
predicate_type = "https://predicate.type"
predicate = jsonencode({
foo = "bar"
})
}

resource "cosign_copy" "copy" {
source = cosign_sign.foo.attested_ref
mattmoor marked this conversation as resolved.
Show resolved Hide resolved
destination = %q
}

data "cosign_verify" "copy" {
image = cosign_copy.copy.copied_ref
policy = jsonencode({
apiVersion = "policy.sigstore.dev/v1beta1"
kind = "ClusterImagePolicy"
metadata = {
name = "attested-and-signed-it"
}
spec = {
images = [{
glob = cosign_copy.copy.copied_ref
}]
authorities = [{
keyless = {
url = "https://fulcio.sigstore.dev"
identities = [{
issuer = "https://token.actions.githubusercontent.com"
subject = "https://github.com/chainguard-dev/terraform-provider-cosign/.github/workflows/test.yml@refs/heads/main"
}]
}
attestations = [{
name = "must-have-attestation"
predicateType = "https://predicate.type"
policy = {
type = "cue"
// When we do things in this style, we can use file("foo.cue") too!
data = <<EOF
predicateType: "https://predicate.type"
predicate: {
foo: "bar"
}
EOF
}
}]
ctlog = {
url = "https://rekor.sigstore.dev"
}
}]
}
})
}
`, ref1, dst),
Check: resource.ComposeTestCheckFunc(
// Check that it got signed!
resource.TestCheckResourceAttr(
"data.cosign_verify.copy", "verified_ref", dst.Digest(dig1.String()).String(),
),
),
}},
})
}