Skip to content

Commit

Permalink
add data source for available keyless creds (#104)
Browse files Browse the repository at this point in the history
Fixes #103 

This adds a new data source that lists the OIDC providers available in
the current environment, and a new required attribute for `cosign_sign`
and `cosign_attest` for those providers.

The intention is that sign/attest resources should be created for each
available credential provider, instead of always creating them and
having them just warn and no-op if credentials aren't available. This
makes diffs less noisy when they're no-ops, since resources won't be
created at all.

Adding a new required attribute makes this a breaking API change; users
will have to add `oidc_provider = "github-actions"` to get the previous
behavior (or `"interactive"`, or `"filesystem"`, each of which had been
implicitly used before)

This also makes it easier to graft in new providers and multiple
providers in the future.

This change required some kind of gross hacks to test it, since it
depends so heavily on the environment it's running in, and some paths
are hard-coded in cosign, and since it should pass both locally and on
actual GitHub Actions.

In a future change we might be able to avoid some `t.Skip`s we've been
using to run only on GHA, since we'll have more control over which
providers are/aren't used, and how.

---------

Signed-off-by: Jason Hall <[email protected]>
  • Loading branch information
imjasonh authored Jan 9, 2024
1 parent 93a2169 commit a2b8345
Show file tree
Hide file tree
Showing 13 changed files with 319 additions and 48 deletions.
23 changes: 23 additions & 0 deletions docs/data-sources/available_credentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "cosign_available_credentials Data Source - terraform-provider-cosign"
subcategory: ""
description: |-
This produces a list of available keyless signing credentials.
---

# cosign_available_credentials (Data Source)

This produces a list of available keyless signing credentials.



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

### Read-Only

- `available` (Set of String) This contains the names of available keyless signing credentials.
- `id` (String) This contains the hash of available keyless signing credentials.


1 change: 1 addition & 0 deletions docs/resources/attest.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This attests the provided image digest with cosign.
### Required

- `image` (String) The digest of the container image to attest.
- `oidc_provider` (String) The OIDC provider to use for authentication.

### Optional

Expand Down
1 change: 1 addition & 0 deletions docs/resources/sign.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This signs the provided image digest with cosign.
### Required

- `image` (String) The digest of the container image to sign.
- `oidc_provider` (String) The OIDC provider to use for authentication.

### Optional

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module github.com/chainguard-dev/terraform-provider-cosign

go 1.21

toolchain go1.21.5

// https://github.com/theupdateframework/go-tuf/issues/527
replace github.com/theupdateframework/go-tuf => github.com/theupdateframework/go-tuf v0.5.2

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

import (
"context"
"crypto/sha256"
"fmt"
"os"
"sort"

_ "github.com/chainguard-dev/terraform-provider-cosign/internal/provider/interactive"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/sigstore/cosign/v2/pkg/cosign/env"
"github.com/sigstore/cosign/v2/pkg/providers/filesystem"
_ "github.com/sigstore/cosign/v2/pkg/providers/github"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ datasource.DataSource = &AvailableDataSource{}

func NewAvailableDataSource() datasource.DataSource {
return &AvailableDataSource{}
}

// ExampleDataSource defines the data source implementation.
type AvailableDataSource struct {
popts *ProviderOpts
}

// ExampleDataSourceModel describes the data source data model.
type AvailableDataSourceModel struct {
Id types.String `tfsdk:"id"`
Available types.Set `tfsdk:"available"`
}

func (d *AvailableDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_available_credentials"
}

func (d *AvailableDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "This produces a list of available keyless signing credentials.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "This contains the hash of available keyless signing credentials.",
Computed: true,
},
"available": schema.SetAttribute{
MarkdownDescription: "This contains the names of available keyless signing credentials.",
Computed: true,
ElementType: basetypes.StringType{},
},
},
}
}

func (d *AvailableDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

popts, ok := req.ProviderData.(*ProviderOpts)
if !ok || popts == nil {
resp.Diagnostics.AddError("Client Error", "invalid provider data")
return
}
d.popts = popts
}

// Copied from "github.com/sigstore/cosign/v2/pkg/providers/filesystem"
func gitHubAvailable() bool {
if env.Getenv(env.VariableGitHubRequestToken) == "" {
return false
}
if env.Getenv(env.VariableGitHubRequestURL) == "" {
return false
}
return true
}

// Allow this path to be overridden for testing.
var filesystemTokenPath = filesystem.FilesystemTokenPath

// Copied from "github.com/sigstore/cosign/v2/pkg/providers/filesystem"
func filesystemAvailable() bool {
// If we can stat the file without error then this is enabled.
_, err := os.Stat(filesystemTokenPath)
return err == nil
}

func interactiveAvailable() bool {
return os.Getenv("TF_COSIGN_LOCAL") != ""
}

func (d *AvailableDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data AvailableDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

var available []string
if interactiveAvailable() {
available = append(available, "interactive")
}
if filesystemAvailable() {
available = append(available, "filesystem")
}
if gitHubAvailable() {
available = append(available, "github-actions")
}
sort.Strings(available)

h := sha256.New()
for _, a := range available {
fmt.Fprintln(h, a)
}
digest := fmt.Sprintf("%x", h.Sum(nil))

var diag diag.Diagnostics
data.Available, diag = types.SetValueFrom(ctx, basetypes.StringType{}, available)
if diag.HasError() {
resp.Diagnostics.Append(diag...)
return
}

data.Id = types.StringValue(digest)

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
109 changes: 109 additions & 0 deletions internal/provider/data_source_available_credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package provider

import (
"os"
"path/filepath"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/sigstore/cosign/v2/pkg/cosign/env"
"github.com/sigstore/cosign/v2/pkg/providers/filesystem"
)

func TestAccAvailableCredentials(t *testing.T) {
dir := t.TempDir()
tmp, err := os.Create(filepath.Join(dir, "foo"))
if err != nil {
t.Fatal(err)
}
defer tmp.Close()

for _, c := range []struct {
desc string
pre, post func(t *testing.T) // pre- and post-test steps
env map[string]string
checks []resource.TestCheckFunc
}{{
desc: "no env",
env: nil,
pre: func(t *testing.T) {
// If this test is running on GHA, we will never have a scenario where we don't have some ambient credentials.
if os.Getenv(string(env.VariableGitHubRequestToken)) != "" {
t.Skip("Skipping no-env check since we're running on GitHub Actions")
}
},
checks: []resource.TestCheckFunc{
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.#", "0"),
},
}, {
desc: "github",
env: map[string]string{
string(env.VariableGitHubRequestToken): "foo",
string(env.VariableGitHubRequestURL): "bar",
},
checks: []resource.TestCheckFunc{
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.#", "1"),
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.0", "github-actions"),
},
}, {
desc: "filesystem and github",
env: map[string]string{
string(env.VariableGitHubRequestToken): "foo",
string(env.VariableGitHubRequestURL): "bar",
},
pre: func(*testing.T) { filesystemTokenPath = tmp.Name() },
post: func(*testing.T) { filesystemTokenPath = filesystem.FilesystemTokenPath },
checks: []resource.TestCheckFunc{
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.#", "2"),
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.0", "filesystem"),
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.1", "github-actions"),
},
}, {
desc: "interactive and github",
env: map[string]string{
string(env.VariableGitHubRequestToken): "foo",
string(env.VariableGitHubRequestURL): "bar",
"TF_COSIGN_LOCAL": "true",
},
checks: []resource.TestCheckFunc{
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.#", "2"),
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.0", "github-actions"),
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.1", "interactive"),
},
}, {
desc: "all together now",
env: map[string]string{
string(env.VariableGitHubRequestToken): "foo",
string(env.VariableGitHubRequestURL): "bar",
"TF_COSIGN_LOCAL": "true",
},
pre: func(*testing.T) { filesystemTokenPath = tmp.Name() },
post: func(*testing.T) { filesystemTokenPath = filesystem.FilesystemTokenPath },
checks: []resource.TestCheckFunc{
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.#", "3"),
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.0", "filesystem"),
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.1", "github-actions"),
resource.TestCheckResourceAttr("data.cosign_available_credentials.available", "available.2", "interactive"),
},
}} {
t.Run(c.desc, func(t *testing.T) {
if c.pre != nil {
c.pre(t)
}
if c.post != nil {
defer c.post(t)
}
for k, v := range c.env {
t.Setenv(k, v)
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{{
Config: `data "cosign_available_credentials" "available" {}`,
Check: resource.ComposeAggregateTestCheckFunc(c.checks...),
}},
})
})
}
}
23 changes: 0 additions & 23 deletions internal/provider/oidc.go

This file was deleted.

8 changes: 3 additions & 5 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ type ProviderOpts struct {
ropts []remote.Option
keychain authn.Keychain

oidc fulcio.OIDCProvider

sync.Mutex

// Keyed off fulcio URL.
Expand All @@ -61,7 +59,7 @@ func (p *ProviderOpts) rekorClient(rekorUrl string) (*client.Rekor, error) {
return rekorClient, nil
}

func (p *ProviderOpts) signerVerifier(fulcioUrl string) (*fulcio.SignerVerifier, error) {
func (p *ProviderOpts) signerVerifier(fulcioUrl string, provider fulcio.OIDCProvider) (*fulcio.SignerVerifier, error) {
p.Lock()
defer p.Unlock()

Expand All @@ -74,7 +72,7 @@ func (p *ProviderOpts) signerVerifier(fulcioUrl string) (*fulcio.SignerVerifier,
return nil, err
}
fulcioClient := api.NewClient(furl, api.WithUserAgent("terraform-provider-cosign"))
sv, err := fulcio.NewSigner(p.oidc, fulcioClient)
sv, err := fulcio.NewSigner(provider, fulcioClient)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -126,7 +124,6 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest,
opts := &ProviderOpts{
ropts: ropts,
keychain: kc,
oidc: &oidcProvider{},
signers: map[string]*fulcio.SignerVerifier{},
rekorClients: map[string]*client.Rekor{},
}
Expand All @@ -147,6 +144,7 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource {
func (p *Provider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewVerifyDataSource,
NewAvailableDataSource,
}
}

Expand Down
Loading

0 comments on commit a2b8345

Please sign in to comment.