Skip to content

Commit 74da100

Browse files
authored
feat(cli): remote Helm Charts (#1484)
Signed-off-by: Miguel Martinez <miguel@chainloop.dev>
1 parent 7357935 commit 74da100

File tree

5 files changed

+55
-10
lines changed

5 files changed

+55
-10
lines changed

pkg/attestation/crafter/api/attestation/v1/crafting_state.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ func (m *Attestation_Material) CraftingStateToIntotoDescriptor(name string) (*in
176176
}
177177

178178
// Set the special annotations for container images
179-
if artifactType == v1.CraftingSchema_Material_CONTAINER_IMAGE {
179+
// NOTE: this is in fact an OCI artifact that can be a container image or any stored OCI artifact
180+
if m.GetContainerImage() != nil {
180181
if tag := m.GetContainerImage().GetTag(); tag != "" {
181182
annotationsM[AnnotationContainerTag] = tag
182183
}

pkg/attestation/crafter/materials/helmchart.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2828
"github.com/chainloop-dev/chainloop/internal/casclient"
2929
api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
30+
"github.com/google/go-containerregistry/pkg/authn"
3031
"github.com/rs/zerolog"
3132
"gopkg.in/yaml.v2"
3233
)
@@ -36,26 +37,51 @@ const (
3637
chartFileName = "Chart.yaml"
3738
// chartValuesYamlFileName is the name of the values.yaml file in the helm chart
3839
chartValuesYamlFileName = "values.yaml"
40+
// OCI artifact type mime type
41+
chartArtifactType = "application/vnd.cncf.helm.config.v1+json"
3942
)
4043

4144
type HelmChartCrafter struct {
4245
backend *casclient.CASBackend
4346
*crafterCommon
47+
// Helm Chart can be stored also as an OCI artifact
48+
ociCrafter *OCIImageCrafter
4449
}
4550

46-
func NewHelmChartCrafter(materialSchema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend,
51+
func NewHelmChartCrafter(materialSchema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, ociAuth authn.Keychain,
4752
l *zerolog.Logger) (*HelmChartCrafter, error) {
4853
if materialSchema.Type != schemaapi.CraftingSchema_Material_HELM_CHART {
4954
return nil, fmt.Errorf("material type is not HELM_CHART format")
5055
}
5156

57+
ociCrafter, err := NewOCIImageCrafter(materialSchema, ociAuth, l, WithArtifactTypeValidation(chartArtifactType))
58+
if err != nil {
59+
return nil, fmt.Errorf("failed to create OCI crafter: %w", err)
60+
}
61+
5262
return &HelmChartCrafter{
5363
backend: backend,
5464
crafterCommon: &crafterCommon{logger: l, input: materialSchema},
65+
ociCrafter: ociCrafter,
5566
}, nil
5667
}
5768

58-
func (c *HelmChartCrafter) Craft(ctx context.Context, filepath string) (*api.Attestation_Material, error) {
69+
func (c *HelmChartCrafter) Craft(ctx context.Context, helmChartRef string) (*api.Attestation_Material, error) {
70+
const ociProtocol = "oci://"
71+
72+
// if it starts with oci://, it's an OCI image
73+
if strings.HasPrefix(helmChartRef, ociProtocol) {
74+
c.logger.Debug().Str("name", helmChartRef).Msg("retrieving Helm Chart info from OCI registry")
75+
// craft without the prefix
76+
return c.ociCrafter.Craft(ctx, helmChartRef[len(ociProtocol):])
77+
}
78+
79+
c.logger.Debug().Str("name", helmChartRef).Msg("loading from local path")
80+
// otherwise, it's a local file
81+
return c.craftLocalHelmChart(ctx, helmChartRef)
82+
}
83+
84+
func (c *HelmChartCrafter) craftLocalHelmChart(ctx context.Context, filepath string) (*api.Attestation_Material, error) {
5985
// Open the helm chart tar file
6086
f, err := os.Open(filepath)
6187
if err != nil {

pkg/attestation/crafter/materials/helmchart_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func TestNewHelmChartCrafter(t *testing.T) {
5252

5353
for _, tc := range testCases {
5454
t.Run(tc.name, func(t *testing.T) {
55-
_, err := materials.NewHelmChartCrafter(tc.input, nil, nil)
55+
_, err := materials.NewHelmChartCrafter(tc.input, nil, nil, nil)
5656
if tc.wantErr {
5757
assert.Error(t, err)
5858
return
@@ -120,7 +120,7 @@ func TestHelmChartCraft(t *testing.T) {
120120
}
121121

122122
backend := &casclient.CASBackend{Uploader: uploader}
123-
crafter, err := materials.NewHelmChartCrafter(schema, backend, &l)
123+
crafter, err := materials.NewHelmChartCrafter(schema, backend, nil, &l)
124124
require.NoError(t, err)
125125

126126
got, err := crafter.Craft(context.TODO(), tc.filePath)

pkg/attestation/crafter/materials/materials.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia
184184
case schemaapi.CraftingSchema_Material_SARIF:
185185
crafter, err = NewSARIFCrafter(materialSchema, casBackend, logger)
186186
case schemaapi.CraftingSchema_Material_HELM_CHART:
187-
crafter, err = NewHelmChartCrafter(materialSchema, casBackend, logger)
187+
crafter, err = NewHelmChartCrafter(materialSchema, casBackend, ociAuth, logger)
188188
case schemaapi.CraftingSchema_Material_EVIDENCE:
189189
crafter, err = NewEvidenceCrafter(materialSchema, casBackend, logger)
190190
case schemaapi.CraftingSchema_Material_ATTESTATION:

pkg/attestation/crafter/materials/oci_image.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,26 @@ type containerSignatureInfo struct {
5757
type OCIImageCrafter struct {
5858
*crafterCommon
5959
keychain authn.Keychain
60+
// validate the artifact type (optional)
61+
artifactTypeValidation string
6062
}
63+
type OCICraftOpt func(*OCIImageCrafter)
6164

62-
func NewOCIImageCrafter(schema *schemaapi.CraftingSchema_Material, ociAuth authn.Keychain, l *zerolog.Logger) (*OCIImageCrafter, error) {
63-
if schema.Type != schemaapi.CraftingSchema_Material_CONTAINER_IMAGE {
64-
return nil, fmt.Errorf("material type is not container image")
65+
func WithArtifactTypeValidation(artifactTypeValidation string) OCICraftOpt {
66+
return func(opts *OCIImageCrafter) {
67+
opts.artifactTypeValidation = artifactTypeValidation
6568
}
69+
}
6670

71+
func NewOCIImageCrafter(schema *schemaapi.CraftingSchema_Material, ociAuth authn.Keychain, l *zerolog.Logger, opts ...OCICraftOpt) (*OCIImageCrafter, error) {
6772
craftCommon := &crafterCommon{logger: l, input: schema}
68-
return &OCIImageCrafter{craftCommon, ociAuth}, nil
73+
c := &OCIImageCrafter{crafterCommon: craftCommon, keychain: ociAuth}
74+
75+
for _, opt := range opts {
76+
opt(c)
77+
}
78+
79+
return c, nil
6980
}
7081

7182
func (i *OCIImageCrafter) Craft(_ context.Context, imageRef string) (*api.Attestation_Material, error) {
@@ -81,6 +92,13 @@ func (i *OCIImageCrafter) Craft(_ context.Context, imageRef string) (*api.Attest
8192
return nil, err
8293
}
8394

95+
if i.artifactTypeValidation != "" {
96+
i.logger.Debug().Str("name", imageRef).Str("want", i.artifactTypeValidation).Msg("validating artifact type")
97+
if descriptor.ArtifactType != i.artifactTypeValidation {
98+
return nil, fmt.Errorf("artifact type %s does not match expected type %s", descriptor.ArtifactType, i.artifactTypeValidation)
99+
}
100+
}
101+
84102
remoteRef := ref.Context().Digest(descriptor.Digest.String())
85103

86104
// FQDN of the repo, i.e bitnami/nginx => index.docker.io/bitnami/nginx

0 commit comments

Comments
 (0)