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

Push rate limiting down into rekor client #60

Merged
merged 1 commit into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ replace github.com/theupdateframework/go-tuf => github.com/theupdateframework/go
require (
github.com/chainguard-dev/terraform-provider-oci v0.0.4
github.com/cyberphone/json-canonicalization v0.0.0-20230710064741-aa7fe85c7dbd
github.com/go-openapi/runtime v0.26.0
github.com/go-openapi/strfmt v0.21.7
github.com/go-openapi/swag v0.22.4
github.com/google/certificate-transparency-go v1.1.6
github.com/google/go-containerregistry v0.15.3-0.20230607134719-145eebe7465d
github.com/google/uuid v1.3.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-retryablehttp v0.7.4
github.com/hashicorp/terraform-plugin-docs v0.14.1
github.com/hashicorp/terraform-plugin-framework v1.3.3
github.com/hashicorp/terraform-plugin-framework-validators v0.10.0
Expand Down Expand Up @@ -133,7 +136,6 @@ require (
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/loads v0.21.2 // indirect
github.com/go-openapi/runtime v0.26.0 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/validate v0.22.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
Expand All @@ -158,12 +160,10 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.4.10 // indirect
github.com/hashicorp/go-retryablehttp v0.7.4 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
Expand Down
9 changes: 1 addition & 8 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"sync"

"github.com/chainguard-dev/terraform-provider-cosign/internal/secant/fulcio"
rclient "github.com/chainguard-dev/terraform-provider-cosign/internal/secant/rekor/client"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/v1/google"
"github.com/google/go-containerregistry/pkg/v1/remote"
Expand All @@ -14,9 +15,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/sigstore/fulcio/pkg/api"
rclient "github.com/sigstore/rekor/pkg/client"
"github.com/sigstore/rekor/pkg/generated/client"
"go.uber.org/ratelimit"
)

// Ensure Provider satisfies various provider interfaces.
Expand All @@ -43,9 +42,6 @@ type ProviderOpts struct {

// Keyed off rekor URL.
rekorClients map[string]*client.Rekor

// Client-side rate limiting to avoid rekor 429s.
limiter ratelimit.Limiter
}

func (p *ProviderOpts) rekorClient(rekorUrl string) (*client.Rekor, error) {
Expand Down Expand Up @@ -133,9 +129,6 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest,
oidc: &oidcProvider{},
signers: map[string]*fulcio.SignerVerifier{},
rekorClients: map[string]*client.Rekor{},
// A little bird told me that rekor allows 500 requests per minute.
// We want to stay well under that, so we'll round down to 5 QPS.
limiter: ratelimit.New(5, ratelimit.WithoutSlack),
}

// Make provider opts available to resources and data sources.
Expand Down
3 changes: 0 additions & 3 deletions internal/provider/resource_attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,6 @@ func (r *AttestResource) doAttest(ctx context.Context, data *AttestResourceModel
return "", nil, fmt.Errorf("creating rekor client: %w", err)
}

// Avoid hitting rekor rate limits.
r.popts.limiter.Take()

ctx, cancel := context.WithTimeout(ctx, options.DefaultTimeout)
defer cancel()

Expand Down
3 changes: 0 additions & 3 deletions internal/provider/resource_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,6 @@ func (r *SignResource) doSign(ctx context.Context, data *SignResourceModel) (str
return "", nil, fmt.Errorf("creating rekor client: %w", err)
}

// Avoid hitting rekor rate limits.
r.popts.limiter.Take()

ctx, cancel := context.WithTimeout(ctx, options.DefaultTimeout)
defer cancel()

Expand Down
108 changes: 108 additions & 0 deletions internal/secant/rekor/client/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2021 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

import (
"net/http"

"github.com/hashicorp/go-retryablehttp"
"go.uber.org/ratelimit"
)

// Option is a functional option for customizing static signatures.
type Option func(*options)

type options struct {
UserAgent string
RetryCount uint
Logger interface{}

// Client-side rate limiting to avoid rekor 429s.
// This is the only real difference from upstream.
// I'd rather just make the transport pluggable if we upstream this.
limiter ratelimit.Limiter
}

const (
// DefaultRetryCount is the default number of retries.
DefaultRetryCount = 3
)

func makeOptions(opts ...Option) *options {
o := &options{
UserAgent: "",
RetryCount: DefaultRetryCount,
// A little bird told me that rekor allows 500 requests per minute.
// We want to stay well under that, so we'll round down to 5 QPS.
limiter: ratelimit.New(5, ratelimit.WithoutSlack),
}

for _, opt := range opts {
opt(o)
}

return o
}

// WithUserAgent sets the media type of the signature.
func WithUserAgent(userAgent string) Option {
return func(o *options) {
o.UserAgent = userAgent
}
}

// WithRetryCount sets the number of retries.
func WithRetryCount(retryCount uint) Option {
return func(o *options) {
o.RetryCount = retryCount
}
}

// WithLogger sets the logger; it must implement either retryablehttp.Logger or retryablehttp.LeveledLogger; if not, this will not take effect.
func WithLogger(logger interface{}) Option {
return func(o *options) {
switch logger.(type) {
case retryablehttp.Logger, retryablehttp.LeveledLogger:
o.Logger = logger
}
}
}

type roundTripper struct {
http.RoundTripper
UserAgent string
limiter ratelimit.Limiter
}

// RoundTrip implements `http.RoundTripper`
func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", rt.UserAgent)

// Blocks to avoid hitting rate limits.
rt.limiter.Take()

return rt.RoundTripper.RoundTrip(req)
}

func createRoundTripper(inner http.RoundTripper, o *options) http.RoundTripper {
if inner == nil {
inner = http.DefaultTransport
}
return &roundTripper{
RoundTripper: inner,
UserAgent: o.UserAgent,
limiter: o.limiter,
}
}
63 changes: 63 additions & 0 deletions internal/secant/rekor/client/rekor_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2021 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

import (
"net/http"
"net/url"

"github.com/go-openapi/runtime"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/hashicorp/go-cleanhttp"
retryablehttp "github.com/hashicorp/go-retryablehttp"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/util"
)

func GetRekorClient(rekorServerURL string, opts ...Option) (*client.Rekor, error) {
url, err := url.Parse(rekorServerURL)
if err != nil {
return nil, err
}
o := makeOptions(opts...)

retryableClient := retryablehttp.NewClient()

// Another difference from upstream is that we want a DefaultPooledTransport
// because we have a single client per host.
defaultTransport := createRoundTripper(cleanhttp.DefaultPooledTransport(), o)
retryableClient.HTTPClient = &http.Client{
Transport: defaultTransport,
}
retryableClient.RetryMax = int(o.RetryCount)
retryableClient.Logger = o.Logger

httpClient := retryableClient.StandardClient()

// sanitize path
if url.Path == "" {
url.Path = client.DefaultBasePath
}

rt := httptransport.NewWithClient(url.Host, url.Path, []string{url.Scheme}, httpClient)
rt.Consumers["application/json"] = runtime.JSONConsumer()
rt.Consumers["application/x-pem-file"] = runtime.TextConsumer()
rt.Producers["application/json"] = runtime.JSONProducer()

registry := strfmt.Default
registry.Add("signedCheckpoint", &util.SignedNote{}, util.SignedCheckpointValidator)
return client.New(rt, registry), nil
}