Skip to content

Commit 2d79db6

Browse files
authored
feat(cli): send CLI version on every request and forward it to policy providers (#3088)
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent baea2db commit 2d79db6

16 files changed

Lines changed: 301 additions & 49 deletions

File tree

app/cli/cmd/artifact.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func wrappedArtifactConn(cpConn *grpc.ClientConn, role pb.CASCredentialsServiceG
5353

5454
var opts = []grpcconn.Option{
5555
grpcconn.WithInsecure(apiInsecure()),
56+
grpcconn.WithCLIVersion(fullVersion()),
5657
}
5758

5859
if caValue := viper.GetString(confOptions.CASCA.viperKey); caValue != "" {

app/cli/cmd/root.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command {
134134

135135
var opts = []grpcconn.Option{
136136
grpcconn.WithInsecure(apiInsecure()),
137+
grpcconn.WithCLIVersion(fullVersion()),
137138
}
138139

139140
if caValue := viper.GetString(confOptions.controlplaneCA.viperKey); caValue != "" {
@@ -369,7 +370,7 @@ func initConfigFile() {
369370
}
370371

371372
func newActionOpts(logger zerolog.Logger, conn *grpc.ClientConn, token string) *action.ActionsOpts {
372-
return &action.ActionsOpts{CPConnection: conn, Logger: logger, AuthTokenRaw: token, OutputFormat: flagOutputFormat}
373+
return &action.ActionsOpts{CPConnection: conn, Logger: logger, AuthTokenRaw: token, OutputFormat: flagOutputFormat, CLIVersion: fullVersion()}
373374
}
374375

375376
func cleanup(conn *grpc.ClientConn) error {

app/cli/cmd/version.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ var (
3636
Edition = ossEdition
3737
)
3838

39+
// fullVersion returns the CLI version annotated with its edition flavor,
40+
// e.g. "v1.94.2-oss" or "dev-ee". Used as the value of the Chainloop-Cli-Version
41+
// header sent on every request to the Control Plane and CAS.
42+
func fullVersion() string {
43+
return Version + "-" + Edition
44+
}
45+
3946
type info struct {
4047
Version string
4148
Digest string

app/cli/pkg/action/action.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type ActionsOpts struct {
5959
Logger zerolog.Logger
6060
AuthTokenRaw string
6161
OutputFormat string
62+
CLIVersion string
6263
}
6364

6465
type OffsetPagination struct {
@@ -113,7 +114,7 @@ func newCrafter(stateOpts *newCrafterStateOpts, conn *grpc.ClientConn, opts ...c
113114
}
114115

115116
// getCASBackend tries to get CAS upload credentials and set up a CAS client
116-
func getCASBackend(ctx context.Context, client pb.AttestationServiceClient, workflowRunID, casCAPath, casURI string, casConnectionInsecure bool, logger zerolog.Logger, casBackend *casclient.CASBackend) (*clientAPI.Attestation_CASBackend, func() error, error) {
117+
func getCASBackend(ctx context.Context, client pb.AttestationServiceClient, workflowRunID, casCAPath, casURI string, casConnectionInsecure bool, logger zerolog.Logger, casBackend *casclient.CASBackend, cliVersion string) (*clientAPI.Attestation_CASBackend, func() error, error) {
117118
credsResp, err := client.GetUploadCreds(ctx, &pb.AttestationServiceGetUploadCredsRequest{
118119
WorkflowRunId: workflowRunID,
119120
})
@@ -151,7 +152,10 @@ func getCASBackend(ctx context.Context, client pb.AttestationServiceClient, work
151152
return casBackendInfo, nil, nil
152153
}
153154

154-
opts := []grpcconn.Option{grpcconn.WithInsecure(casConnectionInsecure)}
155+
opts := []grpcconn.Option{
156+
grpcconn.WithInsecure(casConnectionInsecure),
157+
grpcconn.WithCLIVersion(cliVersion),
158+
}
155159
if casCAPath != "" {
156160
// Check if it's a file path or content. If it's a file path, it should exist. If not, treat it as content.
157161
if _, err := os.Stat(casCAPath); err == nil {

app/cli/pkg/action/attestation_add.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (action *AttestationAdd) Run(ctx context.Context, attestationID, materialNa
104104
if !crafter.CraftingState.GetDryRun() {
105105
client := pb.NewAttestationServiceClient(action.CPConnection)
106106
workflowRunID := crafter.CraftingState.GetAttestation().GetWorkflow().GetWorkflowRunId()
107-
_, connectionCloserFn, getCASBackendErr := getCASBackend(ctx, client, workflowRunID, action.casCAPath, action.casURI, action.connectionInsecure, action.Logger, casBackend)
107+
_, connectionCloserFn, getCASBackendErr := getCASBackend(ctx, client, workflowRunID, action.casCAPath, action.casURI, action.connectionInsecure, action.Logger, casBackend, action.CLIVersion)
108108
if getCASBackendErr != nil {
109109
return nil, fmt.Errorf("failed to get CAS backend: %w", getCASBackendErr)
110110
}

app/cli/pkg/action/attestation_init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
250250
var casBackendInfo *clientAPI.Attestation_CASBackend
251251
if !action.dryRun && attestationID != "" {
252252
var connectionCloserFn func() error
253-
casBackendInfo, connectionCloserFn, err = getCASBackend(ctx, client, attestationID, action.casCAPath, action.casURI, action.connectionInsecure, action.Logger, casBackend)
253+
casBackendInfo, connectionCloserFn, err = getCASBackend(ctx, client, attestationID, action.casCAPath, action.casURI, action.connectionInsecure, action.Logger, casBackend, action.CLIVersion)
254254
if err != nil {
255255
// We don't want to fail the attestation initialization if CAS setup fails, it's a best-effort feature for PR/MR metadata
256256
action.Logger.Warn().Err(err).Msg("unexpected error getting CAS backend")

app/cli/pkg/action/attestation_push.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func (action *AttestationPush) Run(ctx context.Context, attestationID string, ru
222222
if evaluations := crafter.CraftingState.GetAttestation().GetPolicyEvaluations(); !crafter.CraftingState.DryRun && len(evaluations) > 0 {
223223
casBackend := &casclient.CASBackend{Name: "not-set"}
224224
workflowRunID := crafter.CraftingState.GetAttestation().GetWorkflow().GetWorkflowRunId()
225-
_, connectionCloserFn, getCASErr := getCASBackend(ctx, attClient, workflowRunID, action.casCAPath, action.casURI, action.connectionInsecure, action.Logger, casBackend)
225+
_, connectionCloserFn, getCASErr := getCASBackend(ctx, attClient, workflowRunID, action.casCAPath, action.casURI, action.connectionInsecure, action.Logger, casBackend, action.CLIVersion)
226226
if connectionCloserFn != nil {
227227
// nolint: errcheck
228228
defer connectionCloserFn()

app/controlplane/internal/service/attestation.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ func (s *AttestationService) GetPolicy(ctx context.Context, req *cpAPI.Attestati
492492
return nil, errors.Forbidden("forbidden", "organization not found")
493493
}
494494

495-
remotePolicy, err := s.workflowContractUseCase.GetPolicy(req.GetProvider(), req.GetPolicyName(), req.GetOrgName(), org.Name, token.Token)
495+
remotePolicy, err := s.workflowContractUseCase.GetPolicy(ctx, req.GetProvider(), req.GetPolicyName(), req.GetOrgName(), org.Name, token.Token)
496496
if err != nil {
497497
return nil, handleUseCaseErr(err, s.log)
498498
}
@@ -514,7 +514,7 @@ func (s *AttestationService) GetPolicyGroup(ctx context.Context, req *cpAPI.Atte
514514
return nil, errors.Forbidden("forbidden", "organization not found")
515515
}
516516

517-
remoteGroup, err := s.workflowContractUseCase.GetPolicyGroup(req.GetProvider(), req.GetGroupName(), req.GetOrgName(), org.Name, token.Token)
517+
remoteGroup, err := s.workflowContractUseCase.GetPolicyGroup(ctx, req.GetProvider(), req.GetGroupName(), req.GetOrgName(), org.Name, token.Token)
518518
if err != nil {
519519
return nil, handleUseCaseErr(err, s.log)
520520
}
@@ -732,7 +732,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP
732732

733733
// contract validation
734734
if req.GetContractBytes() != nil {
735-
if err = s.workflowContractUseCase.ValidateContractPolicies(req.GetContractBytes(), token); err != nil {
735+
if err = s.workflowContractUseCase.ValidateContractPolicies(ctx, req.GetContractBytes(), token); err != nil {
736736
return nil, handleUseCaseErr(err, s.log)
737737
}
738738
}

app/controlplane/internal/service/workflowcontract.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func (s *WorkflowContractService) Create(ctx context.Context, req *pb.WorkflowCo
146146
}
147147

148148
if len(req.RawContract) != 0 {
149-
if err = s.contractUseCase.ValidateContractPolicies(req.RawContract, token); err != nil {
149+
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token); err != nil {
150150
return nil, handleUseCaseErr(err, s.log)
151151
}
152152
}
@@ -212,7 +212,7 @@ func (s *WorkflowContractService) Update(ctx context.Context, req *pb.WorkflowCo
212212

213213
// Validate the contract policies if the raw contract is provided
214214
if len(req.RawContract) != 0 {
215-
if err = s.contractUseCase.ValidateContractPolicies(req.RawContract, token); err != nil {
215+
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token); err != nil {
216216
return nil, handleUseCaseErr(err, s.log)
217217
}
218218
}
@@ -251,7 +251,7 @@ func (s *WorkflowContractService) Apply(ctx context.Context, req *pb.WorkflowCon
251251
return nil, err
252252
}
253253

254-
if err = s.contractUseCase.ValidateContractPolicies(req.RawSchema, token); err != nil {
254+
if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawSchema, token); err != nil {
255255
return nil, handleUseCaseErr(err, s.log)
256256
}
257257

app/controlplane/internal/usercontext/entities/entitites.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2025 The Chainloop Authors.
2+
// Copyright 2025-2026 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.
@@ -19,6 +19,7 @@ import (
1919
"context"
2020
"strings"
2121

22+
"github.com/chainloop-dev/chainloop/pkg/grpcconn"
2223
"github.com/go-kratos/kratos/v2/middleware/auth/jwt"
2324
"github.com/go-kratos/kratos/v2/transport"
2425
)
@@ -46,3 +47,15 @@ func GetOrganizationNameFromHeader(ctx context.Context) (string, error) {
4647

4748
return "", nil
4849
}
50+
51+
// GetCLIVersionFromHeader returns the CLI version advertised by the caller in
52+
// the Chainloop-Cli-Version request header. The value format is
53+
// "<version>-<edition>", e.g. "v1.94.2-oss". Returns an empty string when the
54+
// header is absent or there is no transport in the context.
55+
func GetCLIVersionFromHeader(ctx context.Context) string {
56+
header, ok := transport.FromServerContext(ctx)
57+
if !ok {
58+
return ""
59+
}
60+
return header.RequestHeader().Get(grpcconn.CLIVersionHeader)
61+
}

0 commit comments

Comments
 (0)