diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 72d52d91c..ed7d58929 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -89,6 +89,7 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l Opts: v2, } workflowRunService := service.NewWorkflowRunService(newWorkflowRunServiceOpts) + attestationUseCase := biz.NewAttestationUseCase(casClientUseCase, logger) fanOutDispatcher := dispatcher.New(integrationUseCase, workflowUseCase, workflowRunUseCase, readerWriter, casClientUseCase, availablePlugins, logger) newAttestationServiceOpts := &service.NewAttestationServiceOpts{ WorkflowRunUC: workflowRunUseCase, @@ -98,6 +99,7 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l CredsReader: readerWriter, IntegrationUseCase: integrationUseCase, CasCredsUseCase: casCredentialsUseCase, + AttestationUC: attestationUseCase, FanoutDispatcher: fanOutDispatcher, Opts: v2, } diff --git a/app/controlplane/internal/biz/attestation.go b/app/controlplane/internal/biz/attestation.go new file mode 100644 index 000000000..1f98b1f2b --- /dev/null +++ b/app/controlplane/internal/biz/attestation.go @@ -0,0 +1,65 @@ +// +// Copyright 2023 The Chainloop 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 biz + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + + "github.com/chainloop-dev/chainloop/internal/servicelogger" + "github.com/go-kratos/kratos/v2/log" + + cr_v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/secure-systems-lab/go-securesystemslib/dsse" +) + +type AttestationUseCase struct { + logger *log.Helper + CASClient +} + +func NewAttestationUseCase(client CASClient, logger log.Logger) *AttestationUseCase { + if logger == nil { + logger = log.NewStdLogger(io.Discard) + } + + return &AttestationUseCase{ + logger: servicelogger.ScopedHelper(logger, "biz/attestation"), + CASClient: client, + } +} + +func (uc *AttestationUseCase) UploadToCAS(ctx context.Context, envelope *dsse.Envelope, secretID, workflowRunID string) (*cr_v1.Hash, error) { + filename := fmt.Sprintf("attestation-%s.json", workflowRunID) + jsonContent, err := json.Marshal(envelope) + if err != nil { + return nil, fmt.Errorf("marshaling the envelope: %w", err) + } + + h, _, err := cr_v1.SHA256(bytes.NewBuffer(jsonContent)) + if err != nil { + return nil, fmt.Errorf("calculating the digest: %w", err) + } + + if err := uc.CASClient.Upload(ctx, secretID, bytes.NewBuffer(jsonContent), filename, h.String()); err != nil { + return nil, fmt.Errorf("uploading to CAS: %w", err) + } + + return &h, nil +} diff --git a/app/controlplane/internal/biz/biz.go b/app/controlplane/internal/biz/biz.go index 332481700..3914cf9e2 100644 --- a/app/controlplane/internal/biz/biz.go +++ b/app/controlplane/internal/biz/biz.go @@ -31,6 +31,7 @@ var ProviderSet = wire.NewSet( NewIntegrationUseCase, NewMembershipUseCase, NewCASClientUseCase, + NewAttestationUseCase, NewWorkflowRunExpirerUseCase, wire.Struct(new(NewIntegrationUseCaseOpts), "*"), wire.Struct(new(NewUserUseCaseParams), "*"), diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 9ff06f7a9..05faa26f4 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -20,7 +20,9 @@ import ( "encoding/json" "fmt" "sort" + "time" + "github.com/cenkalti/backoff/v4" cpAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/internal/biz" "github.com/chainloop-dev/chainloop/app/controlplane/internal/dispatcher" @@ -46,6 +48,7 @@ type AttestationService struct { integrationUseCase *biz.IntegrationUseCase integrationDispatcher *dispatcher.FanOutDispatcher casCredsUseCase *biz.CASCredentialsUseCase + attestationUseCase *biz.AttestationUseCase } type NewAttestationServiceOpts struct { @@ -56,6 +59,7 @@ type NewAttestationServiceOpts struct { CredsReader credentials.Reader IntegrationUseCase *biz.IntegrationUseCase CasCredsUseCase *biz.CASCredentialsUseCase + AttestationUC *biz.AttestationUseCase FanoutDispatcher *dispatcher.FanOutDispatcher Opts []NewOpt } @@ -71,6 +75,7 @@ func NewAttestationService(opts *NewAttestationServiceOpts) *AttestationService integrationUseCase: opts.IntegrationUseCase, casCredsUseCase: opts.CasCredsUseCase, integrationDispatcher: opts.FanoutDispatcher, + attestationUseCase: opts.AttestationUC, } } @@ -188,7 +193,31 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe } // We currently only support one backend per workflowRun - secretName := wRun.CASBackends[0].SecretName + casBackend := wRun.CASBackends[0] + + // If we have an external CAS backend, we will push there the attestation + if !casBackend.Inline { + b := backoff.NewExponentialBackOff() + b.MaxElapsedTime = 1 * time.Minute + err := backoff.Retry( + func() error { + // reset context + ctx := context.Background() + d, err := s.attestationUseCase.UploadToCAS(ctx, envelope, casBackend.SecretName, req.WorkflowRunId) + if err != nil { + return err + } + + s.log.Infow("msg", "attestation uploaded to CAS", "digest", d, "runID", req.WorkflowRunId) + return nil + }, b) + + if err != nil { + _ = sl.LogAndMaskErr(err, s.log) + } + } + + secretName := casBackend.SecretName // Run integrations dispatcher go func() {