Skip to content

Commit 4d6157b

Browse files
authored
feat(pr-detection): Autodetect PR information (#2617)
Signed-off-by: Javier Rodriguez <[email protected]>
1 parent 8dc3992 commit 4d6157b

39 files changed

+1517
-339
lines changed

app/cli/cmd/attestation_init.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/chainloop-dev/chainloop/app/cli/cmd/output"
2323
"github.com/chainloop-dev/chainloop/app/cli/pkg/action"
2424
"github.com/spf13/cobra"
25+
"github.com/spf13/viper"
2526
)
2627

2728
func newAttestationInitCmd() *cobra.Command {
@@ -77,11 +78,14 @@ func newAttestationInitCmd() *cobra.Command {
7778
RunE: func(cmd *cobra.Command, _ []string) error {
7879
a, err := action.NewAttestationInit(
7980
&action.AttestationInitOpts{
80-
ActionsOpts: ActionOpts,
81-
DryRun: attestationDryRun,
82-
Force: force,
83-
UseRemoteState: useAttestationRemoteState,
84-
LocalStatePath: attestationLocalStatePath,
81+
ActionsOpts: ActionOpts,
82+
DryRun: attestationDryRun,
83+
Force: force,
84+
UseRemoteState: useAttestationRemoteState,
85+
LocalStatePath: attestationLocalStatePath,
86+
CASURI: viper.GetString(confOptions.CASAPI.viperKey),
87+
CASCAPath: viper.GetString(confOptions.CASCA.viperKey),
88+
ConnectionInsecure: apiInsecure(),
8589
},
8690
)
8791
if err != nil {

app/cli/documentation/cli-reference.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ Options
200200
--annotation strings additional annotation in the format of key=value
201201
--attestation-id string Unique identifier of the in-progress attestation
202202
-h, --help help for add
203-
--kind string kind of the material to be recorded: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
203+
--kind string kind of the material to be recorded: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
204204
--name string name of the material as shown in the contract
205205
--registry-password string registry password, ($CHAINLOOP_REGISTRY_PASSWORD)
206206
--registry-server string OCI repository server, ($CHAINLOOP_REGISTRY_SERVER)
@@ -2871,7 +2871,7 @@ Options
28712871
--annotation strings Key-value pairs of material annotations (key=value)
28722872
-h, --help help for eval
28732873
--input stringArray Key-value pairs of policy inputs (key=value)
2874-
--kind string Kind of the material: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
2874+
--kind string Kind of the material: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
28752875
--material string Path to material or attestation file
28762876
-p, --policy string Policy reference (./my-policy.yaml, https://my-domain.com/my-policy.yaml, chainloop://my-stored-policy) (default "policy.yaml")
28772877
```

app/cli/pkg/action/action.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package action
1717

1818
import (
19+
"context"
1920
"fmt"
2021
"os"
2122
"path/filepath"
@@ -25,6 +26,9 @@ import (
2526
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
2627
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/statemanager/filesystem"
2728
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/statemanager/remote"
29+
"github.com/chainloop-dev/chainloop/pkg/casclient"
30+
"github.com/chainloop-dev/chainloop/pkg/grpcconn"
31+
2832
"github.com/rs/zerolog"
2933
"google.golang.org/grpc"
3034
)
@@ -94,3 +98,51 @@ func newCrafter(stateOpts *newCrafterStateOpts, conn *grpc.ClientConn, opts ...c
9498

9599
return crafter.NewCrafter(stateManager, attClient, opts...)
96100
}
101+
102+
// getCASBackend tries to get CAS upload credentials and set up a CAS client
103+
func getCASBackend(ctx context.Context, client pb.AttestationServiceClient, workflowRunID, casCAPath, casURI string, casConnectionInsecure bool, logger zerolog.Logger, casBackend *casclient.CASBackend) (func() error, error) {
104+
credsResp, err := client.GetUploadCreds(ctx, &pb.AttestationServiceGetUploadCredsRequest{
105+
WorkflowRunId: workflowRunID,
106+
})
107+
if err != nil {
108+
// Log warning but don't fail - will fall back to inline storage
109+
logger.Warn().Err(err).Msg("failed to get CAS credentials for PR metadata, will store inline")
110+
return nil, fmt.Errorf("getting upload creds: %w", err)
111+
}
112+
113+
if credsResp == nil || credsResp.GetResult() == nil {
114+
logger.Debug().Msg("no upload creds result, will store inline")
115+
return nil, fmt.Errorf("getting upload creds: %w", err)
116+
}
117+
118+
result := credsResp.GetResult()
119+
backend := result.GetBackend()
120+
if backend == nil {
121+
logger.Debug().Msg("no backend info in upload creds, will store inline")
122+
return nil, fmt.Errorf("no backend found in upload creds")
123+
}
124+
125+
casBackend.Name = backend.Provider
126+
if backend.GetLimits() != nil {
127+
casBackend.MaxSize = backend.GetLimits().MaxBytes
128+
}
129+
130+
// Only attempt to create a CAS connection when not inline and token is present
131+
if backend.IsInline || result.Token == "" {
132+
return nil, nil
133+
}
134+
135+
opts := []grpcconn.Option{grpcconn.WithInsecure(casConnectionInsecure)}
136+
if casCAPath != "" {
137+
opts = append(opts, grpcconn.WithCAFile(casCAPath))
138+
}
139+
140+
artifactCASConn, err := grpcconn.New(casURI, result.Token, opts...)
141+
if err != nil {
142+
logger.Warn().Err(err).Msg("failed to create CAS connection, will store inline")
143+
return nil, fmt.Errorf("creating CAS connection: %w", err)
144+
}
145+
146+
casBackend.Uploader = casclient.New(artifactCASConn, casclient.WithLogger(logger))
147+
return artifactCASConn.Close, nil
148+
}

app/cli/pkg/action/attestation_add.go

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
2525
api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
2626
"github.com/chainloop-dev/chainloop/pkg/casclient"
27-
"github.com/chainloop-dev/chainloop/pkg/grpcconn"
2827
"google.golang.org/grpc"
2928
)
3029

@@ -98,40 +97,15 @@ func (action *AttestationAdd) Run(ctx context.Context, attestationID, materialNa
9897

9998
// Define CASbackend information based on the API response
10099
if !crafter.CraftingState.GetDryRun() {
101-
// Get upload creds and CASbackend for the current attestation and set up CAS client
102100
client := pb.NewAttestationServiceClient(action.CPConnection)
103-
creds, err := client.GetUploadCreds(ctx,
104-
&pb.AttestationServiceGetUploadCredsRequest{
105-
WorkflowRunId: crafter.CraftingState.GetAttestation().GetWorkflow().GetWorkflowRunId(),
106-
},
107-
)
108-
if err != nil {
109-
return nil, fmt.Errorf("getting upload creds: %w", err)
110-
}
111-
b := creds.GetResult().GetBackend()
112-
if b == nil {
113-
return nil, fmt.Errorf("no backend found in upload creds")
101+
workflowRunID := crafter.CraftingState.GetAttestation().GetWorkflow().GetWorkflowRunId()
102+
connectionCloserFn, getCASBackendErr := getCASBackend(ctx, client, workflowRunID, action.casCAPath, action.casURI, action.connectionInsecure, action.Logger, casBackend)
103+
if getCASBackendErr != nil {
104+
return nil, fmt.Errorf("failed to get CAS backend: %w", getCASBackendErr)
114105
}
115-
casBackend.Name = b.Provider
116-
casBackend.MaxSize = b.GetLimits().MaxBytes
117-
// Some CASBackends will actually upload information to the CAS server
118-
// in such case we need to set up a connection
119-
if !b.IsInline && creds.Result.Token != "" {
120-
var opts = []grpcconn.Option{
121-
grpcconn.WithInsecure(action.connectionInsecure),
122-
}
123-
124-
if action.casCAPath != "" {
125-
opts = append(opts, grpcconn.WithCAFile(action.casCAPath))
126-
}
127-
128-
artifactCASConn, err := grpcconn.New(action.casURI, creds.Result.Token, opts...)
129-
if err != nil {
130-
return nil, fmt.Errorf("creating CAS connection: %w", err)
131-
}
132-
defer artifactCASConn.Close()
133-
134-
casBackend.Uploader = casclient.New(artifactCASConn, casclient.WithLogger(action.Logger))
106+
if connectionCloserFn != nil {
107+
// nolint: errcheck
108+
defer connectionCloserFn()
135109
}
136110
}
137111

app/cli/pkg/action/attestation_init.go

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal"
2828
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
2929
clientAPI "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
30+
"github.com/chainloop-dev/chainloop/pkg/casclient"
3031
"github.com/chainloop-dev/chainloop/pkg/policies"
3132
"github.com/rs/zerolog"
3233
)
@@ -37,16 +38,22 @@ type AttestationInitOpts struct {
3738
// Force the initialization and override any existing, in-progress ones.
3839
// Note that this is only useful when local-based attestation state is configured
3940
// since it's a protection to make sure you don't override the state by mistake
40-
Force bool
41-
UseRemoteState bool
42-
LocalStatePath string
41+
Force bool
42+
UseRemoteState bool
43+
LocalStatePath string
44+
CASURI string
45+
CASCAPath string // optional CA certificate for the CAS connection
46+
ConnectionInsecure bool
4347
}
4448

4549
type AttestationInit struct {
4650
*ActionsOpts
47-
dryRun, force bool
48-
c *crafter.Crafter
49-
useRemoteState bool
51+
dryRun, force bool
52+
c *crafter.Crafter
53+
useRemoteState bool
54+
casURI string
55+
casCAPath string
56+
connectionInsecure bool
5057
}
5158

5259
// ErrAttestationAlreadyExist means that there is an attestation in progress
@@ -67,11 +74,14 @@ func NewAttestationInit(cfg *AttestationInitOpts) (*AttestationInit, error) {
6774
}
6875

6976
return &AttestationInit{
70-
ActionsOpts: cfg.ActionsOpts,
71-
c: c,
72-
dryRun: cfg.DryRun,
73-
force: cfg.Force,
74-
useRemoteState: cfg.UseRemoteState,
77+
ActionsOpts: cfg.ActionsOpts,
78+
c: c,
79+
dryRun: cfg.DryRun,
80+
force: cfg.Force,
81+
useRemoteState: cfg.UseRemoteState,
82+
casURI: cfg.CASURI,
83+
casCAPath: cfg.CASCAPath,
84+
connectionInsecure: cfg.ConnectionInsecure,
7585
}, nil
7686
}
7787

@@ -219,6 +229,20 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
219229
attestationID = workflowRun.GetId()
220230
}
221231

232+
// Get CAS credentials for PR metadata upload
233+
var casBackend = &casclient.CASBackend{Name: "not-set"}
234+
if !action.dryRun && attestationID != "" {
235+
connectionCloserFn, err := getCASBackend(ctx, client, attestationID, action.casCAPath, action.casURI, action.connectionInsecure, action.Logger, casBackend)
236+
if err != nil {
237+
// We don't want to fail the attestation initialization if CAS setup fails, it's a best-effort feature for PR/MR metadata
238+
action.Logger.Warn().Err(err).Msg("unexpected error getting CAS backend")
239+
}
240+
if connectionCloserFn != nil {
241+
// nolint: errcheck
242+
defer connectionCloserFn()
243+
}
244+
}
245+
222246
var authInfo *clientAPI.Attestation_Auth
223247
if action.AuthTokenRaw != "" {
224248
authInfo, err = extractAuthInfo(action.AuthTokenRaw)
@@ -268,6 +292,12 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
268292
return "", err
269293
}
270294

295+
// Auto-collect PR/MR metadata if in PR/MR context
296+
if err := action.c.AutoCollectPRMetadata(ctx, attestationID, discoveredRunner, casBackend); err != nil {
297+
action.Logger.Warn().Err(err).Msg("failed to auto-collect PR/MR metadata")
298+
// Don't fail the init - this is best-effort
299+
}
300+
271301
return attestationID, nil
272302
}
273303

app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)