Skip to content

Commit 41cd51c

Browse files
author
Mikhail Swift
committed
Add Archivista Storage Backend
Adds the ability to store signed TaskRun and PipelineRun results in Archivista. Archivista currently only supports payloads that are wrapped in a DSSE envelope, so any signatures that are not DSSE envelopes will not be stored.
1 parent 7824cdc commit 41cd51c

File tree

40 files changed

+4297
-11
lines changed

40 files changed

+4297
-11
lines changed

docs/config.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Supported keys include:
2020
| Key | Description | Supported Values | Default |
2121
| :-------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :-------- |
2222
| `artifacts.taskrun.format` | The format to store `TaskRun` payloads in. | `in-toto`, `slsa/v1`, `slsa/v2alpha3`, `slsa/v2alpha4` | `in-toto` |
23-
| `artifacts.taskrun.storage` | The storage backend to store `TaskRun` signatures in. Multiple backends can be specified with comma-separated list ("tekton,oci"). To disable the `TaskRun` artifact input an empty string (""). | `tekton`, `oci`, `gcs`, `docdb`, `grafeas` | `tekton` |
23+
| `artifacts.taskrun.storage` | The storage backend to store `TaskRun` signatures in. Multiple backends can be specified with comma-separated list ("tekton,oci"). To disable the `TaskRun` artifact input an empty string (""). | `tekton`, `oci`, `gcs`, `docdb`, `grafeas`, `archivista` | `tekton` |
2424
| `artifacts.taskrun.signer` | The signature backend to sign `TaskRun` payloads with. | `x509`, `kms` | `x509` |
2525

2626
> NOTE:
@@ -34,7 +34,7 @@ Supported keys include:
3434
| Key | Description | Supported Values | Default |
3535
| :--------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------- | :-------- |
3636
| `artifacts.pipelinerun.format` | The format to store `PipelineRun` payloads in. | `in-toto`, `slsa/v1`, `slsa/v2alpha3`, `slsa/v2alpha4` | `in-toto` |
37-
| `artifacts.pipelinerun.storage` | The storage backend to store `PipelineRun` signatures in. Multiple backends can be specified with comma-separated list ("tekton,oci"). To disable the `PipelineRun` artifact input an empty string (""). | `tekton`, `oci`, `gcs`, `docdb`, `grafeas` | `tekton` |
37+
| `artifacts.pipelinerun.storage` | The storage backend to store `PipelineRun` signatures in. Multiple backends can be specified with comma-separated list ("tekton,oci"). To disable the `PipelineRun` artifact input an empty string (""). | `tekton`, `oci`, `gcs`, `docdb`, `grafeas`, `archivista` | `tekton` |
3838
| `artifacts.pipelinerun.signer` | The signature backend to sign `PipelineRun` payloads with. | `x509`, `kms` | `x509` |
3939
| `artifacts.pipelinerun.enable-deep-inspection` | This boolean option will configure whether Chains should inspect child taskruns in order to capture inputs/outputs within a pipelinerun. `"false"` means that Chains only checks pipeline level results, whereas `"true"` means Chains inspects both pipeline level and task level results. | `"true"`, `"false"` | `"false"` |
4040

@@ -73,6 +73,7 @@ Supported keys include:
7373
| `storage.grafeas.projectid` | The project of where grafeas server is located for storing occurrences | | |
7474
| `storage.grafeas.noteid` (optional) | This field will be used as the prefix part of the note name that will be created. The value of this field must be a string without spaces. (See more details [below](#grafeas).) | | |
7575
| `storage.grafeas.notehint` (optional) | This field is used to set the [human_readable_name](https://github.com/grafeas/grafeas/blob/cd23d4dc1bef740d6d6d90d5007db5c9a2431c41/proto/v1/attestation.proto#L49) field in the Grafeas ATTESTATION note. If it is not provided, the default `This attestation note was generated by Tekton Chains` will be used. | | |
76+
| `storage.archivista.url` | The URL endpoint for the Archivista service. | A valid HTTPS URL pointing to your Archivista instance (e.g. `https://archivista.testifysec.io`). | None |
7677

7778
#### docstore
7879

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ require (
1616
github.com/google/go-licenses v1.6.0
1717
github.com/grafeas/grafeas v0.2.3
1818
github.com/hashicorp/go-multierror v1.1.1
19+
github.com/in-toto/archivista v0.9.0
1920
github.com/in-toto/attestation v1.1.1
21+
github.com/in-toto/go-witness v0.7.0
2022
github.com/in-toto/in-toto-golang v0.9.1-0.20240317085821-8e2966059a09
2123
github.com/opencontainers/go-digest v1.0.0
2224
github.com/pkg/errors v0.9.1
@@ -183,6 +185,7 @@ require (
183185
github.com/eapache/go-resiliency v1.7.0 // indirect
184186
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
185187
github.com/eapache/queue v1.1.0 // indirect
188+
github.com/edwarnicke/gitoid v0.0.0-20220710194850-1be5bfda1f9d // indirect
186189
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
187190
github.com/emirpasic/gods v1.18.1 // indirect
188191
github.com/envoyproxy/go-control-plane v0.13.1 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4A
463463
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
464464
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
465465
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
466+
github.com/edwarnicke/gitoid v0.0.0-20220710194850-1be5bfda1f9d h1:4l+Uq5zFWSagXgGFaKRRVWJrnlzeathyagWgYUltCgY=
467+
github.com/edwarnicke/gitoid v0.0.0-20220710194850-1be5bfda1f9d/go.mod h1:WxWwA3EYuCQjlR5EBUX3uaTS8bh9BOa7BcqVREHQ0uQ=
466468
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
467469
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
468470
github.com/emicklei/proto v1.13.4 h1:myn1fyf8t7tAqIzV91Tj9qXpvyXXGXk8OS2H6IBSc9g=
@@ -863,8 +865,12 @@ github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJ
863865
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
864866
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
865867
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
868+
github.com/in-toto/archivista v0.9.0 h1:XlS+jkrcFjmwSMhp6BZbP5y8FOvFPXM1h23WvCDT8bQ=
869+
github.com/in-toto/archivista v0.9.0/go.mod h1:cLhrICj86j+8wJZmrUzDbNQdcwdc2lqX+v1SKV4tXpE=
866870
github.com/in-toto/attestation v1.1.1 h1:QD3d+oATQ0dFsWoNh5oT0udQ3tUrOsZZ0Fc3tSgWbzI=
867871
github.com/in-toto/attestation v1.1.1/go.mod h1:Dcq1zVwA2V7Qin8I7rgOi+i837wEf/mOZwRm047Sjys=
872+
github.com/in-toto/go-witness v0.7.0 h1:I48FUCLfyos0uCSlHJoqCJO6HjtxF2f/y65TQVpxd8k=
873+
github.com/in-toto/go-witness v0.7.0/go.mod h1:WZQY96yHqPPYkRcQU7dXl0d3saMKAg9DepWbUVL586E=
868874
github.com/in-toto/in-toto-golang v0.9.1-0.20240317085821-8e2966059a09 h1:cwCITdi9pF50CF8uh40qDbkJ/VrEVzx5AoaHP7OPdEo=
869875
github.com/in-toto/in-toto-golang v0.9.1-0.20240317085821-8e2966059a09/go.mod h1:yGCBn2JKF1m26FX8GmkcLSOFVjB6khWRxFsHwWIg7hw=
870876
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package archivista
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"strings"
10+
11+
archivistaClient "github.com/in-toto/archivista/pkg/http-client"
12+
"github.com/in-toto/go-witness/dsse"
13+
"github.com/tektoncd/chains/pkg/chains/objects"
14+
"github.com/tektoncd/chains/pkg/config"
15+
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
16+
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" // if needed
17+
tektonclient "github.com/tektoncd/pipeline/pkg/client/clientset/versioned"
18+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"k8s.io/apimachinery/pkg/types"
20+
"knative.dev/pkg/logging"
21+
)
22+
23+
const (
24+
StorageBackendArchivista = "archivista"
25+
)
26+
27+
// Backend is the interface that all storage backends must implement.
28+
type Backend interface {
29+
StorePayload(ctx context.Context, obj objects.TektonObject, rawPayload []byte, signature string, opts config.StorageOpts) error
30+
RetrievePayloads(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) (map[string]string, error)
31+
RetrieveSignatures(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) (map[string][]string, error)
32+
Type() string
33+
}
34+
35+
// ArchivistaStorage implements the Backend interface for Archivista.
36+
type ArchivistaStorage struct {
37+
client *archivistaClient.ArchivistaClient
38+
url string
39+
cfg config.ArchivistaStorageConfig
40+
tektonClient tektonclient.Interface // Injected Tekton client for patching objects
41+
}
42+
43+
// NewArchivistaStorage initializes a new ArchivistaStorage backend.
44+
func NewArchivistaStorage(cfg config.Config, tektonClient tektonclient.Interface) (*ArchivistaStorage, error) {
45+
archCfg := cfg.Storage.Archivista
46+
if strings.TrimSpace(archCfg.URL) == "" {
47+
return nil, fmt.Errorf("missing archivista URL in storage configuration")
48+
}
49+
50+
client, err := archivistaClient.CreateArchivistaClient(&http.Client{}, archCfg.URL)
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to create Archivista client: %w", err)
53+
}
54+
55+
return &ArchivistaStorage{
56+
client: client,
57+
url: archCfg.URL,
58+
cfg: archCfg,
59+
tektonClient: tektonClient,
60+
}, nil
61+
}
62+
63+
// patchTektonObjectAnnotations patches the Tekton object's annotations with the given key/value pairs
64+
// in one single patch call.
65+
func PatchTektonObjectAnnotations(ctx context.Context, obj objects.TektonObject, annotations map[string]string, tektonClient tektonclient.Interface) error {
66+
patchData := map[string]interface{}{
67+
"metadata": map[string]interface{}{
68+
"annotations": annotations,
69+
},
70+
}
71+
patchBytes, err := json.Marshal(patchData)
72+
if err != nil {
73+
return fmt.Errorf("failed to marshal patch data: %w", err)
74+
}
75+
76+
switch o := obj.GetObject().(type) {
77+
case *tektonv1.TaskRun:
78+
_, err = tektonClient.TektonV1().TaskRuns(o.Namespace).Patch(ctx, o.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{})
79+
return err
80+
case *tektonv1.PipelineRun:
81+
_, err = tektonClient.TektonV1().PipelineRuns(o.Namespace).Patch(ctx, o.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{})
82+
return err
83+
case *v1beta1.TaskRun:
84+
_, err = tektonClient.TektonV1beta1().TaskRuns(o.Namespace).Patch(ctx, o.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{})
85+
return err
86+
case *v1beta1.PipelineRun:
87+
_, err = tektonClient.TektonV1beta1().PipelineRuns(o.Namespace).Patch(ctx, o.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{})
88+
return err
89+
default:
90+
return fmt.Errorf("unsupported Tekton object type for patching")
91+
}
92+
}
93+
94+
// StorePayload builds a DSSE envelope from the raw payload and signature,
95+
// logs the envelope, uploads it via the Archivista client API, and patches the
96+
// Tekton object with the returned gitoid and Archivista URL.
97+
func (a *ArchivistaStorage) StorePayload(ctx context.Context, obj objects.TektonObject, rawPayload []byte, signature string, opts config.StorageOpts) error {
98+
logger := logging.FromContext(ctx)
99+
var env dsse.Envelope
100+
if err := json.Unmarshal([]byte(signature), &env); err != nil {
101+
logger.Errorf("Failed to parse DSSE envelope: %w", err)
102+
return errors.Join(errors.New("Failed to parse DSSE envelope"), err)
103+
}
104+
105+
// Upload the envelope using the Archivista client's Store method.
106+
uploadResp, err := a.client.Store(ctx, env)
107+
if err != nil {
108+
logger.Errorw("Failed to upload DSSE envelope to Archivista", "error", err)
109+
return err
110+
}
111+
logger.Infof("Successfully uploaded DSSE envelope to Archivista, response: %+v", uploadResp)
112+
113+
// Update the in-memory Tekton object with Archivista annotations.
114+
annotations := map[string]string{
115+
"chains.tekton.dev/archivista-gitoid": uploadResp.Gitoid,
116+
"chains.tekton.dev/archivista-url": a.url,
117+
}
118+
obj.SetAnnotations(annotations)
119+
120+
// Patch the live Tekton object in one call.
121+
if err := PatchTektonObjectAnnotations(ctx, obj, annotations, a.tektonClient); err != nil {
122+
logger.Errorw("Failed to patch Tekton object with Archivista annotations", "error", err)
123+
return fmt.Errorf("failed to patch Tekton object: %w", err)
124+
}
125+
126+
return nil
127+
}
128+
129+
// RetrievePayload is not implemented for Archivista.
130+
func (a *ArchivistaStorage) RetrievePayload(ctx context.Context, key string) ([]byte, []byte, error) {
131+
return nil, nil, fmt.Errorf("RetrievePayload not implemented for Archivista")
132+
}
133+
134+
// RetrievePayloads is not implemented for Archivista.
135+
func (a *ArchivistaStorage) RetrievePayloads(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) (map[string]string, error) {
136+
return nil, fmt.Errorf("RetrievePayloads not implemented for Archivista")
137+
}
138+
139+
// RetrieveSignatures is not implemented for Archivista.
140+
func (a *ArchivistaStorage) RetrieveSignatures(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) (map[string][]string, error) {
141+
return nil, fmt.Errorf("RetrieveSignatures not implemented for Archivista")
142+
}
143+
144+
// Type returns the storage backend type.
145+
func (a *ArchivistaStorage) Type() string {
146+
return StorageBackendArchivista
147+
}

0 commit comments

Comments
 (0)