-
Notifications
You must be signed in to change notification settings - Fork 129
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
v1: Rewrite OCI storage to use new interface (#842)
* v1: Rewrite OCI storage to use new interface. * oci: Refactor storer structs to take in variadic options. Removes the need to take in a config object. * oci/options.go: add missing license header.
- Loading branch information
Showing
7 changed files
with
509 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
# v1 client changes proposal - storage refactoring | ||
|
||
With v1 approaching, I want to take a moment to look at changes we want to make | ||
to the existing client libraries to better set us up for long term maintenance. | ||
|
||
We already know that we want to reduce the external library surface of chains. | ||
But to do this, we need to define better interfaces between components that we | ||
expect external clients to use. | ||
|
||
Today, I think a lot of the codebase's complexity has come from a few places: | ||
|
||
1. Storage libraries | ||
|
||
Each storage type needs different pieces of data - i.e. Grafeas and OCI need | ||
to distinguish image signatures and attestation formats, and some clients | ||
need the original object to extract out information like GVKs, names, | ||
namespaces, etc. This has led to a organic growth of the chains libraries to | ||
pass the different types of data around, and a lot of typecasting and other | ||
generic object tricks. | ||
|
||
Looking at the storage interfaces, I think this data roughly boils down to: | ||
|
||
- The original Tekton object | ||
- The formatted data object | ||
- The signed payload + signature (with optional cert information) | ||
|
||
Unlike when chains first started, we now have another useful tool available | ||
to us: generics. I think we can use this to create clearer interfaces. | ||
|
||
2. Dependence on the config package | ||
|
||
tkn depends on the chains server config, but it probably shouldn't. We should | ||
aim to have better ways to initialize clients for others to use. | ||
|
||
Good news, I don't think we're far off, but we should make some changes | ||
|
||
## Signables | ||
|
||
At it's core, Chains is basically an ETL pipeline. We Extract artifacts from run | ||
objects, Transform and sign them, then Load them into storage. | ||
|
||
```go | ||
type Signable[T any] interface { | ||
Extract(ctx context.Context, obj objects.TektonObject) []T{} | ||
} | ||
``` | ||
|
||
## Payloaders | ||
|
||
I think payloaders are mostly in a good place, though we can introduce generics | ||
to start creating stricter type relationships between Signables and Payloaders. | ||
|
||
```go | ||
type Payloader[Input any, Output BinaryMarshaler] interface { | ||
CreatePayload(ctx context.Context, in Input) (Output, error) | ||
} | ||
``` | ||
|
||
tl;dr: Some type comes in, some type comes out. | ||
|
||
[BinaryMarshaler comes from the encoding package](https://pkg.go.dev/encoding#BinaryMarshaler), | ||
but basically all we're aiming for here is to make sure we can get a []byte for | ||
signing. For existing payload types, this may mean we need to wrap external | ||
types for this functionality. | ||
|
||
## Signers | ||
|
||
Signers are mostly in a good spot, though we should probably just embrace []byte | ||
instead of typecasting between string for cert details. | ||
|
||
```go | ||
type Signer interface { | ||
signature.SignerVerifier | ||
Cert() []byte | ||
Chain() []byte | ||
} | ||
``` | ||
|
||
## Storers | ||
|
||
Now that we have all the other pieces defined, we can now have stricter typing | ||
for storing: | ||
|
||
```go | ||
type Storer[Input any, Output any] interface { | ||
Store(ctx context.Context, req *StoreRequest) (*StoreResponse, error) | ||
} | ||
|
||
type StoreRequest[Input any, Output any] struct { | ||
Object objects.TektonObject | ||
Artifact Input | ||
Payload Output | ||
Bundle *signing.Bundle | ||
} | ||
|
||
type StoreResponse struct { | ||
// Some identifier for what we uploaded to reference later? | ||
ID string | ||
} | ||
|
||
type Bundle struct { | ||
Content []byte | ||
Signature []byte | ||
Cert []byte | ||
Chain []byte | ||
} | ||
``` | ||
|
||
While the StoreRequest struct may not be necessary, it has a nice RPC-like | ||
quality in that it will make it easier to add/remove fields in the future. | ||
|
||
## Attestors | ||
|
||
To put it all together, we can add a new type: Attestor. This is effectively | ||
just a wrapper type around all of the other interfaces that binds the generic | ||
types together. Because things are strictly typed, we'll know at compile | ||
what clients are compatible with each other. | ||
|
||
**TBD if we expose this at all** - it may remain an internal implementation | ||
detail of chains. This is what we will generate from the Chains server config. | ||
|
||
```go | ||
type Attestor[Input, Output] struct { | ||
payloader Payloader[Input, Output] | ||
signer Signer | ||
storer Storer[Input, Output] | ||
} | ||
``` | ||
|
||
What this looks like in practice: | ||
|
||
OCI Simple Signing: | ||
|
||
```go | ||
attestor := &Attestor[name.Digest, simple.SimpleContainerImage]{ | ||
payloader: NewSimpleSigningPayloader(), | ||
signer: x509Signer, | ||
storer: NewSimpleOCIStorage(), | ||
} | ||
``` | ||
|
||
SLSA: | ||
|
||
```go | ||
attestor := &Attestor[TektonObject, *intoto.Statement]{ | ||
payloader: NewSLSAPayloader(), | ||
signer: fulcio, | ||
storer: NewGCSStorage(), | ||
} | ||
``` | ||
|
||
Grafeas: | ||
|
||
```go | ||
attestor := &Attestor[TektonObject, *Occurrence]{ | ||
payloader: NewGrafeasPayloader(), | ||
signer: kmsSigner, | ||
storer: NewGrafeasClient(), | ||
} | ||
``` | ||
|
||
## Final thoughts | ||
|
||
If all goes well, this should have 0 impact on typical consumer usage of | ||
chains - these should all be internal refactors with no change in behavior. If | ||
our e2e start failing, we've done something wrong. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// Copyright 2023 The Tekton 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 api | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/tektoncd/chains/pkg/chains/objects" | ||
"github.com/tektoncd/chains/pkg/chains/signing" | ||
) | ||
|
||
// StoreRequest contains the information needed to store a signature/attestation object. | ||
type StoreRequest[Input any, Output any] struct { | ||
// Object is the original Tekton object as received by the controller. | ||
Object objects.TektonObject | ||
// Artifact is the artifact that was extracted from the Object (e.g. build config, image, etc.) | ||
Artifact Input | ||
// Payload is the formatted payload that was generated for the Artifact (e.g. simplesigning, in-toto attestation) | ||
Payload Output | ||
// Bundle contains the signing output details. | ||
Bundle *signing.Bundle | ||
} | ||
|
||
// StoreResponse contains metadata for the result of the store operation. | ||
type StoreResponse struct { | ||
// currently empty, but may contain data in the future. | ||
// present to allow for backwards compatible changes to the Storer interface in the future. | ||
} | ||
|
||
type Storer[Input, Output any] interface { | ||
Store(context.Context, *StoreRequest[Input, Output]) (*StoreResponse, error) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
// Copyright 2023 The Tekton 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 oci | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/google/go-containerregistry/pkg/name" | ||
"github.com/google/go-containerregistry/pkg/v1/remote" | ||
"github.com/in-toto/in-toto-golang/in_toto" | ||
"github.com/pkg/errors" | ||
"github.com/sigstore/cosign/v2/pkg/oci/mutate" | ||
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" | ||
"github.com/sigstore/cosign/v2/pkg/oci/static" | ||
"github.com/sigstore/cosign/v2/pkg/types" | ||
"github.com/tektoncd/chains/pkg/chains/storage/api" | ||
"knative.dev/pkg/logging" | ||
) | ||
|
||
var ( | ||
_ api.Storer[name.Digest, in_toto.Statement] = &AttestationStorer{} | ||
) | ||
|
||
// AttestationStorer stores in-toto Attestation payloads in OCI registries. | ||
type AttestationStorer struct { | ||
// repo configures the repo where data should be stored. | ||
// If empty, the repo is inferred from the Artifact. | ||
repo *name.Repository | ||
// remoteOpts are additional remote options (i.e. auth) to use for client operations. | ||
remoteOpts []remote.Option | ||
} | ||
|
||
func NewAttestationStorer(opts ...AttestationStorerOption) (*AttestationStorer, error) { | ||
s := &AttestationStorer{} | ||
for _, o := range opts { | ||
if err := o.applyAttestationStorer(s); err != nil { | ||
return nil, err | ||
} | ||
} | ||
return s, nil | ||
} | ||
|
||
func (s *AttestationStorer) Store(ctx context.Context, req *api.StoreRequest[name.Digest, in_toto.Statement]) (*api.StoreResponse, error) { | ||
logger := logging.FromContext(ctx) | ||
|
||
repo := req.Artifact.Repository | ||
if s.repo != nil { | ||
repo = *s.repo | ||
} | ||
se, err := ociremote.SignedEntity(req.Artifact, ociremote.WithRemoteOptions(s.remoteOpts...)) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "getting signed image") | ||
} | ||
|
||
// Create the new attestation for this entity. | ||
attOpts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)} | ||
if req.Bundle.Cert != nil { | ||
attOpts = append(attOpts, static.WithCertChain(req.Bundle.Cert, req.Bundle.Chain)) | ||
} | ||
att, err := static.NewAttestation(req.Bundle.Signature, attOpts...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
newImage, err := mutate.AttachAttestationToEntity(se, att) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Publish the signatures associated with this entity | ||
if err := ociremote.WriteAttestations(repo, newImage, ociremote.WithRemoteOptions(s.remoteOpts...)); err != nil { | ||
return nil, err | ||
} | ||
logger.Infof("Successfully uploaded attestation for %s", req.Artifact.String()) | ||
|
||
return &api.StoreResponse{}, nil | ||
} |
Oops, something went wrong.