11//
2- // Copyright 2023 The Chainloop Authors.
2+ // Copyright 2024 The Chainloop Authors.
33//
44// Licensed under the Apache License, Version 2.0 (the "License");
55// you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ import (
2222 "errors"
2323 "fmt"
2424 "io"
25+ "net/url"
2526 "strings"
2627
2728 "github.com/aws/aws-sdk-go/aws"
@@ -40,28 +41,16 @@ const (
4041)
4142
4243type Backend struct {
43- client * s3.S3
44- bucket string
44+ client * s3.S3
45+ bucket string
46+ customEndpoint string
4547}
4648
4749var _ backend.UploaderDownloader = (* Backend )(nil )
4850
49- type ConnOpt func ( * aws. Config )
51+ const defaultRegion = "us-east-1"
5052
51- // Optional endpoint configuration
52- func WithEndpoint (endpoint string ) ConnOpt {
53- return func (cfg * aws.Config ) {
54- cfg .Endpoint = aws .String (endpoint )
55- }
56- }
57-
58- func WithForcedS3PathStyle (force bool ) ConnOpt {
59- return func (cfg * aws.Config ) {
60- cfg .S3ForcePathStyle = aws .Bool (force )
61- }
62- }
63-
64- func NewBackend (creds * Credentials , connOpts ... ConnOpt ) (* Backend , error ) {
53+ func NewBackend (creds * Credentials ) (* Backend , error ) {
6554 if creds == nil {
6655 return nil , errors .New ("credentials cannot be nil" )
6756 }
@@ -70,11 +59,27 @@ func NewBackend(creds *Credentials, connOpts ...ConnOpt) (*Backend, error) {
7059 return nil , fmt .Errorf ("invalid credentials: %w" , err )
7160 }
7261
62+ // Set a default region if not provided
63+ var region = defaultRegion
64+ if creds .Region != "" {
65+ region = creds .Region
66+ }
67+
7368 c := credentials .NewStaticCredentials (creds .AccessKeyID , creds .SecretAccessKey , "" )
7469 // Configure AWS session
75- cfg := & aws.Config {Credentials : c , Region : aws .String (creds .Region )}
76- for _ , opt := range connOpts {
77- opt (cfg )
70+ cfg := & aws.Config {Credentials : c , Region : aws .String (region )}
71+
72+ // Bucket might contain the not only the bucket name but also the endpoint
73+ endpoint , bucket , err := extractLocationAndBucket (creds )
74+ if err != nil {
75+ return nil , fmt .Errorf ("failed to parse bucket name: %w" , err )
76+ }
77+
78+ // we have a custom endpoint
79+ // in some cases the server-side checksum verification is not supported like in the case of cloudflare r2
80+ if endpoint != "" {
81+ cfg .Endpoint = aws .String (endpoint )
82+ cfg .S3ForcePathStyle = aws .Bool (true )
7883 }
7984
8085 session , err := session .NewSession (cfg )
@@ -83,11 +88,55 @@ func NewBackend(creds *Credentials, connOpts ...ConnOpt) (*Backend, error) {
8388 }
8489
8590 return & Backend {
86- client : s3 .New (session ),
87- bucket : creds .BucketName ,
91+ client : s3 .New (session ),
92+ bucket : bucket ,
93+ customEndpoint : endpoint ,
8894 }, nil
8995}
9096
97+ // For now we are aware that the checksum verification is not supported by cloudflare r2
98+ // https://developers.cloudflare.com/r2/api/s3/api/
99+ func (b * Backend ) checksumVerificationEnabled () bool {
100+ var enabled = true
101+ if b .customEndpoint != "" && strings .Contains (b .customEndpoint , "r2.cloudflarestorage.com" ) {
102+ enabled = false
103+ }
104+
105+ return enabled
106+ }
107+
108+ // Extract the custom endpoint and the bucket name from the location string
109+ // The location string can be either a bucket name or a URL
110+ // i.e bucket-name or https://custom-domain/bucket-name
111+ func extractLocationAndBucket (creds * Credentials ) (string , string , error ) {
112+ // Older versions of the credentials didn't have the location field
113+ // and just the bucket name was stored in the bucket name field
114+ if creds .BucketName != "" {
115+ return "" , creds .BucketName , nil
116+ }
117+
118+ // Newer versions of the credentials have the location field which can contain the endpoint
119+ // so we override the bucket and set the endpoint if needed
120+ parsedLocation , err := url .Parse (creds .Location )
121+ if err != nil {
122+ return "" , "" , fmt .Errorf ("failed to parse location: %w" , err )
123+ }
124+
125+ host := parsedLocation .Host
126+ // It's a bucket name
127+ if host == "" {
128+ return "" , creds .Location , nil
129+ }
130+
131+ endpoint := fmt .Sprintf ("%s://%s" , parsedLocation .Scheme , host )
132+ // It's a URL, extract bucket name from the path
133+ if pathSegments := strings .Split (parsedLocation .Path , "/" ); len (pathSegments ) > 1 {
134+ return endpoint , pathSegments [1 ], nil
135+ }
136+
137+ return "" , "" , fmt .Errorf ("the location doesn't contain a bucket name" )
138+ }
139+
91140// Exists check that the artifact is already present in the repository
92141func (b * Backend ) Exists (ctx context.Context , digest string ) (bool , error ) {
93142 _ , err := b .Describe (ctx , digest )
@@ -100,29 +149,41 @@ func (b *Backend) Exists(ctx context.Context, digest string) (bool, error) {
100149
101150func (b * Backend ) Upload (ctx context.Context , r io.Reader , resource * pb.CASResource ) error {
102151 uploader := s3manager .NewUploaderWithClient (b .client )
103-
104- _ , err := uploader .UploadWithContext (ctx , & s3manager.UploadInput {
152+ input := & s3manager.UploadInput {
105153 Bucket : aws .String (b .bucket ),
106154 Key : aws .String (resourceName (resource .Digest )),
107155 Body : r ,
108- // Check that the object is uploaded correctly
109- ChecksumSHA256 : aws .String (hexSha256ToBinaryB64 (resource .Digest )),
110156 Metadata : map [string ]* string {
111157 annotationNameAuthor : aws .String (backend .AuthorAnnotation ),
112158 annotationNameFilename : aws .String (resource .FileName ),
113159 },
114- })
160+ }
115161
116- return err
162+ if b .checksumVerificationEnabled () {
163+ // Check that the object is uploaded correctly
164+ input .ChecksumSHA256 = aws .String (hexSha256ToBinaryB64 (resource .Digest ))
165+ }
166+
167+ if _ , err := uploader .UploadWithContext (ctx , input ); err != nil {
168+ return fmt .Errorf ("failed to upload to bucket: %w" , err )
169+ }
170+
171+ return nil
117172}
118173
119174func (b * Backend ) Describe (ctx context.Context , digest string ) (* pb.CASResource , error ) {
120- // and read the object back + validate integrity
121- resp , err := b .client .HeadObjectWithContext (ctx , & s3.HeadObjectInput {
122- Bucket : aws .String (b .bucket ),
123- Key : aws .String (resourceName (digest )),
124- ChecksumMode : aws .String ("ENABLED" ),
125- })
175+ input := & s3.HeadObjectInput {
176+ Bucket : aws .String (b .bucket ),
177+ Key : aws .String (resourceName (digest )),
178+ }
179+
180+ if b .checksumVerificationEnabled () {
181+ // Enable checksum verification
182+ input .ChecksumMode = aws .String ("ENABLED" )
183+ }
184+
185+ // and read the object back
186+ resp , err := b .client .HeadObjectWithContext (ctx , input )
126187
127188 // check error is aws error
128189 var awsErr awserr.Error
0 commit comments