Skip to content

Commit cad0215

Browse files
authored
feat(cli): remote attestation state support (#499)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent fe66360 commit cad0215

File tree

16 files changed

+201
-48
lines changed

16 files changed

+201
-48
lines changed

app/cli/cmd/attestation.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ import (
2424
)
2525

2626
var (
27-
robotAccount string
28-
GracefulExit bool
27+
robotAccount string
28+
useAttestationRemoteState bool
29+
GracefulExit bool
30+
// attestationID is the unique identifier of the in-progress attestation
31+
// this is required when use-attestation-remote-state is enabled
32+
attestationID string
2933
)
3034

3135
const robotAccountEnvVarName = "CHAINLOOP_ROBOT_ACCOUNT"
@@ -36,6 +40,22 @@ func newAttestationCmd() *cobra.Command {
3640
Aliases: []string{"att"},
3741
Short: "Craft Software Supply Chain Attestations",
3842
Example: "Refer to https://docs.chainloop.dev/getting-started/attestation-crafting",
43+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
44+
// run the initialization of the root command plus the new logic
45+
// specific to this attestation command
46+
rootCmd := cmd.Parent().Parent()
47+
if err := rootCmd.PersistentPreRunE(cmd, args); err != nil {
48+
return err
49+
}
50+
51+
// If the subcommand has the attestation-id flag,
52+
// we need to make sure that it's set if the remote-state flag is enabled
53+
if useAttestationRemoteState && cmd.Flags().Lookup("attestation-id") != nil {
54+
return cmd.MarkFlagRequired("attestation-id")
55+
}
56+
57+
return nil
58+
},
3959
}
4060

4161
cmd.PersistentFlags().StringVarP(&robotAccount, "token", "t", "", fmt.Sprintf("robot account token. NOTE: You can also use the env variable %s", robotAccountEnvVarName))
@@ -44,13 +64,19 @@ func newAttestationCmd() *cobra.Command {
4464
if robotAccount == "" {
4565
robotAccount = os.Getenv(robotAccountEnvVarName)
4666
}
67+
4768
cmd.PersistentFlags().BoolVar(&GracefulExit, "graceful-exit", false, "exit 0 in case of error. NOTE: this flag will be removed once Chainloop reaches 1.0")
69+
cmd.PersistentFlags().BoolVar(&useAttestationRemoteState, "remote-state", false, "Store the attestation state remotely (preview feature)")
4870

4971
cmd.AddCommand(newAttestationInitCmd(), newAttestationAddCmd(), newAttestationStatusCmd(), newAttestationPushCmd(), newAttestationResetCmd())
5072

5173
return cmd
5274
}
5375

76+
func flagAttestationID(cmd *cobra.Command) {
77+
cmd.Flags().StringVar(&attestationID, "attestation-id", "", "Unique identifier of the in-progress attestation")
78+
}
79+
5480
// extractAnnotations extracts the annotations from the flag and returns a map
5581
// the expected input format is key=value
5682
func extractAnnotations(annotationsFlag []string) (map[string]string, error) {

app/cli/cmd/attestation_add.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func newAttestationAddCmd() *cobra.Command {
5555
return err
5656
}
5757

58-
if err := a.Run(cmd.Context(), "", name, value, annotations); err != nil {
58+
if err := a.Run(cmd.Context(), attestationID, name, value, annotations); err != nil {
5959
if errors.Is(err, action.ErrAttestationNotInitialized) {
6060
return err
6161
}
@@ -83,6 +83,7 @@ func newAttestationAddCmd() *cobra.Command {
8383
err = cmd.MarkFlagRequired("value")
8484
cobra.CheckErr(err)
8585
cmd.Flags().StringSliceVar(&annotationsFlag, "annotation", nil, "additional annotation in the format of key=value")
86+
flagAttestationID(cmd)
8687

8788
return cmd
8889
}

app/cli/cmd/attestation_init.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func newAttestationInitCmd() *cobra.Command {
4848
}
4949

5050
// Initialize it
51-
err = a.Run(cmd.Context(), contractRevision)
51+
attestationID, err := a.Run(cmd.Context(), contractRevision)
5252
if err != nil {
5353
if errors.Is(err, action.ErrAttestationAlreadyExist) {
5454
return err
@@ -67,7 +67,7 @@ func newAttestationInitCmd() *cobra.Command {
6767
return newGracefulError(err)
6868
}
6969

70-
res, err := statusAction.Run(cmd.Context(), "")
70+
res, err := statusAction.Run(cmd.Context(), attestationID)
7171
if err != nil {
7272
return newGracefulError(err)
7373
}

app/cli/cmd/attestation_push.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func newAttestationPushCmd() *cobra.Command {
7373
return err
7474
}
7575

76-
res, err := a.Run(cmd.Context(), "", annotations)
76+
res, err := a.Run(cmd.Context(), attestationID, annotations)
7777
if err != nil {
7878
if errors.Is(err, action.ErrAttestationNotInitialized) {
7979
return err
@@ -96,6 +96,7 @@ func newAttestationPushCmd() *cobra.Command {
9696

9797
cmd.Flags().StringVarP(&pkPath, "key", "k", "", "reference (path or env variable name) to the cosign private key that will be used to sign the attestation")
9898
cmd.Flags().StringSliceVar(&annotationsFlag, "annotation", nil, "additional annotation in the format of key=value")
99+
flagAttestationID(cmd)
99100

100101
return cmd
101102
}

app/cli/cmd/attestation_reset.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func newAttestationResetCmd() *cobra.Command {
4646
return fmt.Errorf("failed to load action: %w", err)
4747
}
4848

49-
if err := a.Run(cmd.Context(), "", trigger, reason); err != nil {
49+
if err := a.Run(cmd.Context(), attestationID, trigger, reason); err != nil {
5050
return newGracefulError(err)
5151
}
5252

@@ -58,6 +58,7 @@ func newAttestationResetCmd() *cobra.Command {
5858

5959
cmd.Flags().StringVar(&trigger, "trigger", triggerFailed, fmt.Sprintf("trigger for the reset, valid options are %q and %q", triggerFailed, triggerCanceled))
6060
cmd.Flags().StringVar(&reason, "reason", "", "reset reason")
61+
flagAttestationID(cmd)
6162

6263
return cmd
6364
}

app/cli/cmd/attestation_status.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ func newAttestationStatusCmd() *cobra.Command {
3333
cmd := &cobra.Command{
3434
Use: "status",
3535
Short: "check the status of the current attestation process",
36+
Annotations: map[string]string{
37+
useWorkflowRobotAccount: "true",
38+
},
3639
RunE: func(cmd *cobra.Command, args []string) error {
3740
a, err := action.NewAttestationStatus(
3841
&action.AttestationStatusOpts{
@@ -43,7 +46,7 @@ func newAttestationStatusCmd() *cobra.Command {
4346
return fmt.Errorf("failed to load action: %w", err)
4447
}
4548

46-
res, err := a.Run(cmd.Context(), "")
49+
res, err := a.Run(cmd.Context(), attestationID)
4750
if err != nil {
4851
return err
4952
}
@@ -53,6 +56,7 @@ func newAttestationStatusCmd() *cobra.Command {
5356
}
5457

5558
cmd.Flags().BoolVar(&full, "full", false, "full report including current recorded values")
59+
flagAttestationID(cmd)
5660

5761
return cmd
5862
}
@@ -63,7 +67,7 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult) error
6367
gt.AppendRow(table.Row{"Initialized At", status.InitializedAt.Format(time.RFC822)})
6468
gt.AppendSeparator()
6569
meta := status.WorkflowMeta
66-
gt.AppendRow(table.Row{"Workflow", meta.WorkflowID})
70+
gt.AppendRow(table.Row{"Attestation ID", status.AttestationID})
6771
gt.AppendRow(table.Row{"Name", meta.Name})
6872
gt.AppendRow(table.Row{"Team", meta.Team})
6973
gt.AppendRow(table.Row{"Project", meta.Project})

app/cli/cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ func initConfigFile() {
175175
}
176176

177177
func newActionOpts(logger zerolog.Logger, conn *grpc.ClientConn) *action.ActionsOpts {
178-
return &action.ActionsOpts{CPConnection: conn, Logger: logger}
178+
return &action.ActionsOpts{CPConnection: conn, Logger: logger, UseAttestationRemoteState: useAttestationRemoteState}
179179
}
180180

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

app/cli/internal/action/action.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,39 @@ import (
2121
"path/filepath"
2222
"time"
2323

24+
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2425
"github.com/chainloop-dev/chainloop/internal/attestation/crafter"
2526
"github.com/chainloop-dev/chainloop/internal/attestation/crafter/statemanager/filesystem"
27+
"github.com/chainloop-dev/chainloop/internal/attestation/crafter/statemanager/remote"
2628
"github.com/rs/zerolog"
2729
"google.golang.org/grpc"
2830
)
2931

3032
type ActionsOpts struct {
31-
CPConnection *grpc.ClientConn
32-
Logger zerolog.Logger
33+
CPConnection *grpc.ClientConn
34+
Logger zerolog.Logger
35+
UseAttestationRemoteState bool
3336
}
3437

3538
func toTimePtr(t time.Time) *time.Time {
3639
return &t
3740
}
3841

39-
// load a crafter with local state manager
40-
// TODO: We'll enable the ability to load a crafter that relies on a remote state manager
41-
func newCrafter(_ *grpc.ClientConn, logger *zerolog.Logger) (*crafter.Crafter, error) {
42-
statePath := filepath.Join(os.TempDir(), "chainloop-attestation.tmp.json")
43-
localStateManager, err := filesystem.New(statePath)
42+
// load a crafter with either local or remote state
43+
func newCrafter(enableRemoteState bool, conn *grpc.ClientConn, logger *zerolog.Logger) (*crafter.Crafter, error) {
44+
var stateManager crafter.StateManager
45+
var err error
46+
47+
switch enableRemoteState {
48+
case true:
49+
stateManager, err = remote.New(pb.NewAttestationStateServiceClient(conn))
50+
case false:
51+
stateManager, err = filesystem.New(filepath.Join(os.TempDir(), "chainloop-attestation.tmp.json"))
52+
}
53+
4454
if err != nil {
45-
return nil, fmt.Errorf("failed to create local state manager: %w", err)
55+
return nil, fmt.Errorf("failed to create state manager: %w", err)
4656
}
4757

48-
return crafter.NewCrafter(localStateManager, crafter.WithLogger(logger))
58+
return crafter.NewCrafter(stateManager, crafter.WithLogger(logger))
4959
}

app/cli/internal/action/attestation_add.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type AttestationAdd struct {
4242
}
4343

4444
func NewAttestationAdd(cfg *AttestationAddOpts) (*AttestationAdd, error) {
45-
c, err := newCrafter(cfg.CPConnection, &cfg.Logger)
45+
c, err := newCrafter(cfg.UseAttestationRemoteState, cfg.CPConnection, &cfg.Logger)
4646
if err != nil {
4747
return nil, fmt.Errorf("failed to load crafter: %w", err)
4848
}
@@ -58,7 +58,9 @@ func NewAttestationAdd(cfg *AttestationAddOpts) (*AttestationAdd, error) {
5858
var ErrAttestationNotInitialized = errors.New("attestation not yet initialized")
5959

6060
func (action *AttestationAdd) Run(ctx context.Context, attestationID, materialName, materialValue string, annotations map[string]string) error {
61-
if initialized := action.c.AlreadyInitialized(ctx, attestationID); !initialized {
61+
if initialized, err := action.c.AlreadyInitialized(ctx, attestationID); err != nil {
62+
return fmt.Errorf("checking if attestation is already initialized: %w", err)
63+
} else if !initialized {
6264
return ErrAttestationNotInitialized
6365
}
6466

app/cli/internal/action/attestation_init.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (e ErrRunnerContextNotFound) Error() string {
4949
}
5050

5151
func NewAttestationInit(cfg *AttestationInitOpts) (*AttestationInit, error) {
52-
c, err := newCrafter(cfg.CPConnection, &cfg.Logger)
52+
c, err := newCrafter(cfg.UseAttestationRemoteState, cfg.CPConnection, &cfg.Logger)
5353
if err != nil {
5454
return nil, fmt.Errorf("failed to load crafter: %w", err)
5555
}
@@ -61,13 +61,18 @@ func NewAttestationInit(cfg *AttestationInitOpts) (*AttestationInit, error) {
6161
}, nil
6262
}
6363

64-
func (action *AttestationInit) Run(ctx context.Context, contractRevision int) error {
64+
// returns the attestation ID
65+
func (action *AttestationInit) Run(ctx context.Context, contractRevision int) (string, error) {
66+
if action.dryRun && action.UseAttestationRemoteState {
67+
return "", errors.New("remote state is not compatible with dry-run mode")
68+
}
69+
6570
action.Logger.Debug().Msg("Retrieving attestation definition")
6671
client := pb.NewAttestationServiceClient(action.ActionsOpts.CPConnection)
6772
// get information of the workflow
6873
resp, err := client.GetContract(ctx, &pb.AttestationServiceGetContractRequest{ContractRevision: int32(contractRevision)})
6974
if err != nil {
70-
return err
75+
return "", err
7176
}
7277

7378
workflow := resp.GetResult().GetWorkflow()
@@ -88,7 +93,7 @@ func (action *AttestationInit) Run(ctx context.Context, contractRevision int) er
8893
runnerType := resp.Result.Contract.GetV1().Runner.GetType()
8994
runnerContext := crafter.NewRunner(runnerType)
9095
if !action.dryRun && !runnerContext.CheckEnv() {
91-
return ErrRunnerContextNotFound{runnerContext.String()}
96+
return "", ErrRunnerContextNotFound{runnerContext.String()}
9297
}
9398

9499
// Identifier of this attestation instance
@@ -104,7 +109,7 @@ func (action *AttestationInit) Run(ctx context.Context, contractRevision int) er
104109
},
105110
)
106111
if err != nil {
107-
return err
112+
return "", err
108113
}
109114

110115
workflowRun := runResp.GetResult().GetWorkflowRun()
@@ -123,18 +128,18 @@ func (action *AttestationInit) Run(ctx context.Context, contractRevision int) er
123128
}
124129

125130
if err := action.c.Init(ctx, initOpts); err != nil {
126-
return err
131+
return "", err
127132
}
128133

129134
// Load the env variables both the system populated and the user predefined ones
130135
if err := action.c.ResolveEnvVars(ctx, attestationID); err != nil {
131136
if action.dryRun {
132-
return nil
137+
return "", nil
133138
}
134139

135140
_ = action.c.Reset(ctx, attestationID)
136-
return err
141+
return "", err
137142
}
138143

139-
return nil
144+
return attestationID, nil
140145
}

0 commit comments

Comments
 (0)