Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 490d3c7

Browse files
author
Cole Kennedy
committedMar 22, 2025·
feat: add continue-on-error flags with robust error handling
- Add flags to continue execution when the wrapped command succeeds: - --continue-on-attestor-error for attestor-related errors - --continue-on-infra-error for infrastructure errors - --continue-on-errors for both error types - Create proper error type system for error classification - Implement robust error type detection and propagation - Improve error handling throughout command execution - Add comprehensive unit tests for error handling
1 parent 449c4e4 commit 490d3c7

File tree

6 files changed

+655
-219
lines changed

6 files changed

+655
-219
lines changed
 

‎cmd/run.go

+170-12
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/in-toto/go-witness/log"
3131
"github.com/in-toto/go-witness/registry"
3232
"github.com/in-toto/go-witness/timestamp"
33+
"github.com/in-toto/witness/internal/errors"
3334
"github.com/in-toto/witness/options"
3435
"github.com/spf13/cobra"
3536
)
@@ -63,15 +64,91 @@ func RunCmd() *cobra.Command {
6364
return cmd
6465
}
6566

67+
// isAttestorError determines if an error is attestor-related
68+
func isAttestorError(err error) bool {
69+
return errors.IsAttestorError(err)
70+
}
71+
72+
// handleInfraError handles infrastructure operation errors based on continue flags
73+
// Returns true if execution should continue, and the error if it should be tracked
74+
func handleInfraError(ro options.RunOptions, err error, operationDesc string, commandSucceeded bool) (bool, error) {
75+
if !commandSucceeded {
76+
return false, nil
77+
}
78+
79+
// Wrap the error with infrastructure error type
80+
infraErr := errors.NewInfrastructureError(operationDesc, err)
81+
82+
if ro.ContinueOnAllErrors {
83+
log.Warnf("Failed to %s: %v", operationDesc, err)
84+
log.Warnf("Continuing due to --continue-on-errors flag")
85+
return true, infraErr
86+
} else if ro.ContinueOnInfraError {
87+
log.Warnf("Failed to %s: %v", operationDesc, err)
88+
log.Warnf("Continuing due to --continue-on-infra-error flag")
89+
return true, infraErr
90+
}
91+
92+
return false, nil
93+
}
94+
95+
// handleErrorWithContinueFlags applies the appropriate error handling logic based on flags
96+
// Returns true if execution should continue, false if the error should be returned
97+
func handleErrorWithContinueFlags(ro options.RunOptions, err error, commandSucceeded bool) (bool, error, error) {
98+
var infraError, attestorError error
99+
100+
// If command didn't succeed or no continue flags are set, don't continue
101+
if !commandSucceeded {
102+
return false, nil, nil
103+
}
104+
105+
// Check if the all-errors flag is set, which takes precedence
106+
if ro.ContinueOnAllErrors {
107+
log.Warnf("Encountered error: %v", err)
108+
log.Warnf("Continuing due to --continue-on-errors flag")
109+
110+
// Still classify the error for summary purposes
111+
if isAttestorError(err) {
112+
attestorError = err
113+
} else {
114+
// Default to infrastructure error if not an attestor error
115+
infraError = errors.NewInfrastructureError("run command", err)
116+
}
117+
return true, infraError, attestorError
118+
}
119+
120+
// Check specific error type flags
121+
isAttestor := isAttestorError(err)
122+
if isAttestor && ro.ContinueOnAttestorError {
123+
log.Warnf("Encountered attestor error: %v", err)
124+
log.Warnf("Continuing due to --continue-on-attestor-error flag")
125+
attestorError = err
126+
return true, infraError, attestorError
127+
} else if !isAttestor && ro.ContinueOnInfraError {
128+
log.Warnf("Encountered infrastructure error: %v", err)
129+
log.Warnf("Continuing due to --continue-on-infra-error flag")
130+
infraError = errors.NewInfrastructureError("run command", err)
131+
return true, infraError, attestorError
132+
}
133+
134+
// No applicable flag was set, don't continue
135+
return false, nil, nil
136+
}
137+
66138
func runRun(ctx context.Context, ro options.RunOptions, args []string, signers ...cryptoutil.Signer) error {
67139
if len(signers) > 1 {
68-
return fmt.Errorf("only one signer is supported")
140+
return errors.NewInfrastructureError("signer validation", fmt.Errorf("only one signer is supported"))
69141
}
70142

71143
if len(signers) == 0 {
72-
return fmt.Errorf("no signers found")
144+
return errors.NewInfrastructureError("signer validation", fmt.Errorf("no signers found"))
73145
}
74146

147+
// Track if wrapped command succeeded but we had errors
148+
var commandSucceeded bool
149+
var infraError error
150+
var attestorError error
151+
75152
timestampers := []timestamp.Timestamper{}
76153
for _, url := range ro.TimestampServers {
77154
timestampers = append(timestampers, timestamp.NewTimestamper(timestamp.TimestampWithUrl(url)))
@@ -101,7 +178,7 @@ func runRun(ctx context.Context, ro options.RunOptions, args []string, signers .
101178
if !duplicate {
102179
attestor, err := attestation.GetAttestor(a)
103180
if err != nil {
104-
return fmt.Errorf("failed to create attestor: %w", err)
181+
return errors.NewAttestorError(a, fmt.Errorf("failed to create attestor: %w", err))
105182
}
106183
attestors = append(attestors, attestor)
107184
}
@@ -115,23 +192,23 @@ func runRun(ctx context.Context, ro options.RunOptions, args []string, signers .
115192

116193
attestor, err := registry.SetOptions(attestor, setters...)
117194
if err != nil {
118-
return fmt.Errorf("failed to set attestor option for %v: %w", attestor.Type(), err)
195+
return errors.NewAttestorError(attestor.Name(), fmt.Errorf("failed to set attestor option for %v: %w", attestor.Type(), err))
119196
}
120197
}
121198

122199
var roHashes []cryptoutil.DigestValue
123200
for _, hashStr := range ro.Hashes {
124201
hash, err := cryptoutil.HashFromString(hashStr)
125202
if err != nil {
126-
return fmt.Errorf("failed to parse hash: %w", err)
203+
return errors.NewInfrastructureError("parse hash", fmt.Errorf("failed to parse hash: %w", err))
127204
}
128205
roHashes = append(roHashes, cryptoutil.DigestValue{Hash: hash, GitOID: false})
129206
}
130207

131208
for _, dirHashGlobItem := range ro.DirHashGlobs {
132209
_, err := glob.Compile(dirHashGlobItem)
133210
if err != nil {
134-
return fmt.Errorf("failed to compile glob: %v", err)
211+
return errors.NewInfrastructureError("compile glob", fmt.Errorf("failed to compile glob: %v", err))
135212
}
136213
}
137214

@@ -149,14 +226,49 @@ func runRun(ctx context.Context, ro options.RunOptions, args []string, signers .
149226
),
150227
witness.RunWithTimestampers(timestampers...),
151228
)
229+
230+
// Check if command ran successfully
231+
if len(args) > 0 { // Only check for command success if a command was run
232+
for _, result := range results {
233+
if result.AttestorName == "command-run" {
234+
// Command completed and we have the attestation, so it succeeded
235+
commandSucceeded = true
236+
break
237+
}
238+
}
239+
} else {
240+
// If no command was specified, we're just collecting attestations
241+
// In this case, treat as if command succeeded for flag purposes
242+
commandSucceeded = true
243+
}
244+
152245
if err != nil {
153-
return err
246+
// Apply error handling logic based on flags
247+
shouldContinue, newInfraErr, newAttestorErr := handleErrorWithContinueFlags(ro, err, commandSucceeded)
248+
if shouldContinue {
249+
// Update the error tracking variables
250+
if newInfraErr != nil {
251+
infraError = newInfraErr
252+
}
253+
if newAttestorErr != nil {
254+
attestorError = newAttestorErr
255+
}
256+
} else {
257+
// If we shouldn't continue, return the error
258+
return err
259+
}
154260
}
155261

156262
for _, result := range results {
157263
signedBytes, err := json.Marshal(&result.SignedEnvelope)
158264
if err != nil {
159-
return fmt.Errorf("failed to marshal envelope: %w", err)
265+
shouldContinue, newInfraErr := handleInfraError(ro, err, "marshal envelope", commandSucceeded)
266+
if shouldContinue {
267+
infraError = newInfraErr
268+
continue // Skip to next result
269+
} else {
270+
return fmt.Errorf("failed to marshal envelope: %w", err)
271+
}
160272
}
161273

162274
// TODO: Find out explicit way to describe "prefix" in CLI options
@@ -167,22 +279,68 @@ func runRun(ctx context.Context, ro options.RunOptions, args []string, signers .
167279

168280
out, err := loadOutfile(outfile)
169281
if err != nil {
170-
return fmt.Errorf("failed to open out file: %w", err)
282+
shouldContinue, newInfraErr := handleInfraError(ro, err, fmt.Sprintf("open out file %s", outfile), commandSucceeded)
283+
if shouldContinue {
284+
infraError = newInfraErr
285+
continue // Skip to next result
286+
} else {
287+
return fmt.Errorf("failed to open out file: %w", err)
288+
}
171289
}
172290
defer out.Close()
173291

174292
if _, err := out.Write(signedBytes); err != nil {
175-
return fmt.Errorf("failed to write envelope to out file: %w", err)
293+
shouldContinue, newInfraErr := handleInfraError(ro, err, fmt.Sprintf("write envelope to file %s", outfile), commandSucceeded)
294+
if shouldContinue {
295+
infraError = newInfraErr
296+
continue // Skip to next result
297+
} else {
298+
return fmt.Errorf("failed to write envelope to out file: %w", err)
299+
}
176300
}
177301

178302
if ro.ArchivistaOptions.Enable {
179303
archivistaClient := archivista.New(ro.ArchivistaOptions.Url)
180-
if gitoid, err := archivistaClient.Store(ctx, result.SignedEnvelope); err != nil {
181-
return fmt.Errorf("failed to store artifact in archivista: %w", err)
304+
gitoid, err := archivistaClient.Store(ctx, result.SignedEnvelope)
305+
if err != nil {
306+
shouldContinue, newInfraErr := handleInfraError(ro, err, "store artifact in archivista", commandSucceeded)
307+
if shouldContinue {
308+
infraError = newInfraErr
309+
} else {
310+
return fmt.Errorf("failed to store artifact in archivista: %w", err)
311+
}
182312
} else {
183313
log.Infof("Stored in archivista as %v\n", gitoid)
184314
}
185315
}
186316
}
317+
318+
// Display summary warnings if we had errors but continued
319+
if commandSucceeded && (attestorError != nil || infraError != nil) {
320+
// Show a combined message if we used the combined flag
321+
if ro.ContinueOnAllErrors {
322+
log.Warnf("Command completed successfully, but encountered errors")
323+
if attestorError != nil {
324+
log.Warnf("Some attestations may be missing")
325+
}
326+
if infraError != nil {
327+
log.Warnf("Some attestation functionality may have been compromised")
328+
}
329+
} else {
330+
// Show specific messages for specific flags
331+
if attestorError != nil && ro.ContinueOnAttestorError {
332+
log.Warnf("Command completed successfully, but encountered attestor errors")
333+
log.Warnf("Some attestations may be missing")
334+
}
335+
336+
if infraError != nil && ro.ContinueOnInfraError {
337+
log.Warnf("Command completed successfully, but encountered infrastructure errors")
338+
log.Warnf("Some attestation functionality may have been compromised")
339+
}
340+
}
341+
342+
// We had errors but continued, so return success
343+
return nil
344+
}
187345
return nil
188346
}

‎cmd/run_test.go

+103-207
Original file line numberDiff line numberDiff line change
@@ -15,234 +15,130 @@
1515
package cmd
1616

1717
import (
18-
"context"
19-
"crypto"
20-
"crypto/rand"
21-
"crypto/rsa"
22-
"encoding/json"
18+
"errors"
2319
"fmt"
24-
"os"
25-
"path/filepath"
26-
"strings"
2720
"testing"
2821

29-
"github.com/in-toto/go-witness/cryptoutil"
30-
"github.com/in-toto/go-witness/dsse"
31-
"github.com/in-toto/go-witness/log"
32-
"github.com/in-toto/go-witness/signer"
33-
"github.com/in-toto/go-witness/signer/file"
22+
werrors "github.com/in-toto/witness/internal/errors"
3423
"github.com/in-toto/witness/options"
35-
"github.com/sirupsen/logrus"
36-
"github.com/sirupsen/logrus/hooks/test"
3724
"github.com/stretchr/testify/assert"
38-
"github.com/stretchr/testify/require"
3925
)
4026

41-
func TestRunRSAKeyPair(t *testing.T) {
42-
privatekey, err := rsa.GenerateKey(rand.Reader, keybits)
43-
require.NoError(t, err)
44-
signer := cryptoutil.NewRSASigner(privatekey, crypto.SHA256)
45-
46-
workingDir := t.TempDir()
47-
attestationPath := filepath.Join(workingDir, "outfile.txt")
48-
runOptions := options.RunOptions{
49-
WorkingDir: workingDir,
50-
Attestations: []string{},
51-
OutFilePath: attestationPath,
52-
StepName: "teststep",
53-
Tracing: false,
54-
}
55-
56-
args := []string{
57-
"bash",
58-
"-c",
59-
"echo 'test' > test.txt",
60-
}
61-
62-
require.NoError(t, runRun(context.Background(), runOptions, args, signer))
63-
attestationBytes, err := os.ReadFile(attestationPath)
64-
require.NoError(t, err)
65-
env := dsse.Envelope{}
66-
require.NoError(t, json.Unmarshal(attestationBytes, &env))
67-
}
68-
69-
func Test_runRunRSACA(t *testing.T) {
70-
_, intermediates, leafcert, leafkey := fullChain(t)
71-
signerOptions := options.SignerOptions{}
72-
signerOptions["file"] = []func(signer.SignerProvider) (signer.SignerProvider, error){
73-
func(sp signer.SignerProvider) (signer.SignerProvider, error) {
74-
fsp := sp.(file.FileSignerProvider)
75-
fsp.KeyPath = leafkey.Name()
76-
fsp.IntermediatePaths = []string{intermediates[0].Name()}
77-
for _, intermediate := range intermediates {
78-
fsp.IntermediatePaths = append(fsp.IntermediatePaths, intermediate.Name())
79-
}
80-
81-
fsp.CertPath = leafcert.Name()
82-
return fsp, nil
83-
},
84-
}
85-
86-
signers, err := loadSigners(context.Background(), signerOptions, options.KMSSignerProviderOptions{}, map[string]struct{}{"file": {}})
87-
require.NoError(t, err)
88-
89-
workingDir := t.TempDir()
90-
attestationPath := filepath.Join(workingDir, "outfile.txt")
91-
runOptions := options.RunOptions{
92-
SignerOptions: signerOptions,
93-
WorkingDir: workingDir,
94-
Attestations: []string{},
95-
OutFilePath: attestationPath,
96-
StepName: "teststep",
97-
Tracing: false,
98-
}
27+
// TestRunAttestorFailure tests the new error type detection logic
28+
func TestRunAttestorFailure(t *testing.T) {
29+
err1 := fmt.Errorf("attestor did not work")
30+
err2 := fmt.Errorf("failed to save artifact")
9931

100-
args := []string{
101-
"bash",
102-
"-c",
103-
"echo 'test' > test.txt",
104-
}
105-
106-
require.NoError(t, runRun(context.Background(), runOptions, args, signers...))
107-
attestationBytes, err := os.ReadFile(attestationPath)
108-
require.NoError(t, err)
109-
assert.True(t, len(attestationBytes) > 0)
110-
111-
env := dsse.Envelope{}
112-
if err := json.Unmarshal(attestationBytes, &env); err != nil {
113-
t.Errorf("Error reading envelope: %v", err)
114-
}
115-
116-
b, err := os.ReadFile(intermediates[0].Name())
117-
require.NoError(t, err)
118-
assert.Equal(t, b, env.Signatures[0].Intermediates[0])
119-
120-
b, err = os.ReadFile(leafcert.Name())
121-
require.NoError(t, err)
122-
assert.Equal(t, b, env.Signatures[0].Certificate)
123-
}
124-
125-
func TestRunHashesOptions(t *testing.T) {
12632
tests := []struct {
127-
name string
128-
hashesOption []string
129-
expectErr bool
33+
name string
34+
err error
35+
errType string
36+
expectedErr error
13037
}{
13138
{
132-
name: "Valid RSA key pair",
133-
hashesOption: []string{"sha256"},
134-
expectErr: false,
39+
name: "attestor error is AttestorError",
40+
err: werrors.NewAttestorError("test-attestor", err1),
41+
errType: "attestor",
42+
expectedErr: err1,
13543
},
13644
{
137-
name: "Invalid hashes option",
138-
hashesOption: []string{"invalidHash"},
139-
expectErr: true,
45+
name: "infrastructure error is InfrastructureError",
46+
err: werrors.NewInfrastructureError("test-operation", err2),
47+
errType: "infra",
48+
expectedErr: err2,
14049
},
14150
}
14251

14352
for _, tt := range tests {
14453
t.Run(tt.name, func(t *testing.T) {
145-
privatekey, err := rsa.GenerateKey(rand.Reader, keybits)
146-
require.NoError(t, err)
147-
signer := cryptoutil.NewRSASigner(privatekey, crypto.SHA256)
148-
149-
workingDir := t.TempDir()
150-
attestationPath := filepath.Join(workingDir, "outfile.txt")
151-
runOptions := options.RunOptions{
152-
WorkingDir: workingDir,
153-
Attestations: []string{},
154-
Hashes: tt.hashesOption,
155-
OutFilePath: attestationPath,
156-
StepName: "teststep",
157-
Tracing: false,
158-
}
159-
160-
args := []string{
161-
"bash",
162-
"-c",
163-
"echo 'test' > test.txt",
164-
}
165-
166-
err = runRun(context.Background(), runOptions, args, signer)
167-
if tt.expectErr {
168-
require.Error(t, err)
54+
// Test error type detection
55+
if tt.errType == "attestor" {
56+
assert.True(t, isAttestorError(tt.err), "Error should be detected as attestor error")
57+
assert.True(t, werrors.IsAttestorError(tt.err), "Error should be detected as attestor error")
16958
} else {
170-
require.NoError(t, err)
171-
attestationBytes, err := os.ReadFile(attestationPath)
172-
require.NoError(t, err)
173-
env := dsse.Envelope{}
174-
require.NoError(t, json.Unmarshal(attestationBytes, &env))
59+
assert.False(t, isAttestorError(tt.err), "Error should not be detected as attestor error")
60+
assert.True(t, werrors.IsInfrastructureError(tt.err), "Error should be detected as infrastructure error")
17561
}
176-
})
177-
}
178-
}
17962

180-
func TestRunDuplicateAttestors(t *testing.T) {
181-
tests := []struct {
182-
name string
183-
attestors []string
184-
expectWarn int
185-
}{
186-
{
187-
name: "No duplicate attestors",
188-
attestors: []string{"environment"},
189-
expectWarn: 0,
190-
},
191-
{
192-
name: "duplicate attestors",
193-
attestors: []string{"environment", "environment"},
194-
expectWarn: 1,
195-
},
196-
{
197-
name: "duplicate attestor due to default",
198-
attestors: []string{"product"},
199-
expectWarn: 1,
200-
},
201-
}
202-
203-
for _, tt := range tests {
204-
t.Run(tt.name, func(t *testing.T) {
205-
fmt.Println(tt.name)
206-
testLogger, hook := test.NewNullLogger()
207-
log.SetLogger(testLogger)
208-
209-
privatekey, err := rsa.GenerateKey(rand.Reader, keybits)
210-
require.NoError(t, err)
211-
signer := cryptoutil.NewRSASigner(privatekey, crypto.SHA256)
212-
213-
workingDir := t.TempDir()
214-
attestationPath := filepath.Join(workingDir, "outfile.txt")
215-
runOptions := options.RunOptions{
216-
WorkingDir: workingDir,
217-
Attestations: tt.attestors,
218-
OutFilePath: attestationPath,
219-
StepName: "teststep",
220-
Tracing: false,
221-
}
222-
223-
args := []string{
224-
"bash",
225-
"-c",
226-
"echo 'test' > test.txt",
227-
}
228-
229-
err = runRun(context.Background(), runOptions, args, signer)
230-
if tt.expectWarn > 0 {
231-
c := 0
232-
for _, entry := range hook.AllEntries() {
233-
fmt.Println(tt.name, "log:", entry.Message)
234-
if entry.Level == logrus.WarnLevel && strings.Contains(entry.Message, "already declared, skipping") {
235-
c++
236-
}
237-
}
238-
assert.Equal(t, tt.expectWarn, c)
239-
} else {
240-
require.NoError(t, err)
241-
attestationBytes, err := os.ReadFile(attestationPath)
242-
require.NoError(t, err)
243-
env := dsse.Envelope{}
244-
require.NoError(t, json.Unmarshal(attestationBytes, &env))
245-
}
63+
// Test error unwrapping
64+
assert.True(t, errors.Is(tt.err, tt.expectedErr), "Error should unwrap to the original error")
24665
})
24766
}
24867
}
68+
69+
// TestErrorHandling tests the error handling infrastructure
70+
func TestErrorHandling(t *testing.T) {
71+
// Test scenarios for handleInfraError
72+
t.Run("handleInfraError", func(t *testing.T) {
73+
origErr := fmt.Errorf("test error")
74+
ro := options.RunOptions{
75+
ContinueOnInfraError: true,
76+
}
77+
78+
shouldContinue, resultErr := handleInfraError(ro, origErr, "test operation", true)
79+
assert.True(t, shouldContinue, "Should continue when flag is set and command succeeded")
80+
assert.NotNil(t, resultErr, "Result error should not be nil")
81+
assert.True(t, werrors.IsInfrastructureError(resultErr), "Result should be an infrastructure error")
82+
83+
// Test when command failed
84+
shouldContinue, resultErr = handleInfraError(ro, origErr, "test operation", false)
85+
assert.False(t, shouldContinue, "Should not continue when command failed")
86+
assert.Nil(t, resultErr, "Result error should be nil when not continuing")
87+
88+
// Test when flag is not set
89+
ro.ContinueOnInfraError = false
90+
shouldContinue, resultErr = handleInfraError(ro, origErr, "test operation", true)
91+
assert.False(t, shouldContinue, "Should not continue when flag is not set")
92+
assert.Nil(t, resultErr, "Result error should be nil when not continuing")
93+
})
94+
95+
// Test scenarios for handleErrorWithContinueFlags with attestor error
96+
t.Run("handleErrorWithContinueFlags-attestor", func(t *testing.T) {
97+
attestorErr := werrors.NewAttestorError("test-attestor", fmt.Errorf("attestor error"))
98+
ro := options.RunOptions{
99+
ContinueOnAttestorError: true,
100+
}
101+
102+
shouldContinue, infraErr, attErr := handleErrorWithContinueFlags(ro, attestorErr, true)
103+
assert.True(t, shouldContinue, "Should continue when attestor flag is set and command succeeded")
104+
assert.Nil(t, infraErr, "Infra error should be nil")
105+
assert.NotNil(t, attErr, "Attestor error should not be nil")
106+
assert.True(t, werrors.IsAttestorError(attErr), "Result should be an attestor error")
107+
108+
// Test when command failed
109+
shouldContinue, infraErr, attErr = handleErrorWithContinueFlags(ro, attestorErr, false)
110+
assert.False(t, shouldContinue, "Should not continue when command failed")
111+
assert.Nil(t, infraErr, "Infra error should be nil")
112+
assert.Nil(t, attErr, "Attestor error should be nil")
113+
114+
// Test with all errors flag
115+
ro.ContinueOnAttestorError = false
116+
ro.ContinueOnAllErrors = true
117+
shouldContinue, infraErr, attErr = handleErrorWithContinueFlags(ro, attestorErr, true)
118+
assert.True(t, shouldContinue, "Should continue when all errors flag is set")
119+
assert.Nil(t, infraErr, "Infra error should be nil")
120+
assert.NotNil(t, attErr, "Attestor error should not be nil")
121+
})
122+
123+
// Test scenarios for handleErrorWithContinueFlags with infra error
124+
t.Run("handleErrorWithContinueFlags-infra", func(t *testing.T) {
125+
infraError := werrors.NewInfrastructureError("test-operation", fmt.Errorf("infra error"))
126+
ro := options.RunOptions{
127+
ContinueOnInfraError: true,
128+
}
129+
130+
shouldContinue, infraErr, attErr := handleErrorWithContinueFlags(ro, infraError, true)
131+
assert.True(t, shouldContinue, "Should continue when infra flag is set and command succeeded")
132+
assert.NotNil(t, infraErr, "Infra error should not be nil")
133+
assert.Nil(t, attErr, "Attestor error should be nil")
134+
assert.True(t, werrors.IsInfrastructureError(infraErr), "Result should be an infra error")
135+
136+
// Test with all errors flag
137+
ro.ContinueOnInfraError = false
138+
ro.ContinueOnAllErrors = true
139+
shouldContinue, infraErr, attErr = handleErrorWithContinueFlags(ro, infraError, true)
140+
assert.True(t, shouldContinue, "Should continue when all errors flag is set")
141+
assert.NotNil(t, infraErr, "Infra error should not be nil")
142+
assert.Nil(t, attErr, "Attestor error should be nil")
143+
})
144+
}

‎docs/commands.md

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ witness run [cmd] [flags]
5050
--attestor-product-include-glob string Pattern to use when recording products. Files that match this pattern will be included as subjects on the attestation. (default "*")
5151
--attestor-sbom-export Export the SBOM predicate in its own attestation
5252
--attestor-slsa-export Export the SLSA provenance predicate in its own attestation
53+
--continue-on-attestor-error Continue execution even if one or more attestors fail as long as the wrapped command exits successfully
54+
-x, --continue-on-errors Continue execution even if there are any errors (both attestor and infrastructure) as long as the wrapped command exits successfully
55+
--continue-on-infra-error Continue execution even if there are infrastructure errors (signing, Fulcio, Archivista) as long as the wrapped command exits successfully
5356
--dirhash-glob strings Dirhash glob can be used to collapse material and product hashes on matching directory matches.
5457
--enable-archivista Use Archivista to store or retrieve attestations
5558
--env-add-sensitive-key strings Add keys or globs (e.g. '*TEXT') to the list of sensitive environment keys.

‎internal/errors/errors.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2021 The Witness Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package errors
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
)
21+
22+
// AttestorError represents an error that occurred during attestation generation
23+
type AttestorError struct {
24+
Err error
25+
AttestorName string
26+
}
27+
28+
func (e *AttestorError) Error() string {
29+
return fmt.Sprintf("attestor error (%s): %v", e.AttestorName, e.Err)
30+
}
31+
32+
func (e *AttestorError) Unwrap() error {
33+
return e.Err
34+
}
35+
36+
// NewAttestorError creates a new AttestorError
37+
func NewAttestorError(attestorName string, err error) *AttestorError {
38+
return &AttestorError{
39+
Err: err,
40+
AttestorName: attestorName,
41+
}
42+
}
43+
44+
// IsAttestorError checks if the given error is or wraps an AttestorError
45+
func IsAttestorError(err error) bool {
46+
var attestorErr *AttestorError
47+
return errors.As(err, &attestorErr)
48+
}
49+
50+
// InfrastructureError represents an error related to infrastructure operations
51+
// such as signing, storing artifacts, or interacting with external services
52+
type InfrastructureError struct {
53+
Err error
54+
Operation string
55+
}
56+
57+
func (e *InfrastructureError) Error() string {
58+
return fmt.Sprintf("infrastructure error (%s): %v", e.Operation, e.Err)
59+
}
60+
61+
func (e *InfrastructureError) Unwrap() error {
62+
return e.Err
63+
}
64+
65+
// NewInfrastructureError creates a new InfrastructureError
66+
func NewInfrastructureError(operation string, err error) *InfrastructureError {
67+
return &InfrastructureError{
68+
Err: err,
69+
Operation: operation,
70+
}
71+
}
72+
73+
// IsInfrastructureError checks if the given error is or wraps an InfrastructureError
74+
func IsInfrastructureError(err error) bool {
75+
var infraErr *InfrastructureError
76+
return errors.As(err, &infraErr)
77+
}

‎internal/errors/errors_test.go

+294
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
// Copyright 2021 The Witness Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package errors
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
)
24+
25+
// TestAttestorError tests the AttestorError type functionality
26+
func TestAttestorError(t *testing.T) {
27+
// Define test cases with different error scenarios
28+
testCases := []struct {
29+
name string
30+
attestorName string
31+
originalError error
32+
expectedContains []string
33+
testWrapping bool
34+
}{
35+
{
36+
name: "basic error",
37+
attestorName: "basic-attestor",
38+
originalError: fmt.Errorf("something failed"),
39+
expectedContains: []string{"attestor error", "basic-attestor", "something failed"},
40+
testWrapping: false,
41+
},
42+
{
43+
name: "empty attestor name",
44+
attestorName: "",
45+
originalError: fmt.Errorf("attestor crashed"),
46+
expectedContains: []string{"attestor error", "attestor crashed"},
47+
testWrapping: false,
48+
},
49+
{
50+
name: "nil original error",
51+
attestorName: "nil-error-attestor",
52+
originalError: nil,
53+
expectedContains: []string{"attestor error", "nil-error-attestor", "<nil>"},
54+
testWrapping: false,
55+
},
56+
{
57+
name: "wrapped error test",
58+
attestorName: "wrapped-attestor",
59+
originalError: fmt.Errorf("root cause"),
60+
expectedContains: []string{"attestor error", "wrapped-attestor", "root cause"},
61+
testWrapping: true,
62+
},
63+
}
64+
65+
// Run test cases
66+
for _, tc := range testCases {
67+
t.Run(tc.name, func(t *testing.T) {
68+
// Create attestor error
69+
attestorErr := NewAttestorError(tc.attestorName, tc.originalError)
70+
71+
// Test error message format contains expected strings
72+
errMsg := attestorErr.Error()
73+
for _, expected := range tc.expectedContains {
74+
assert.Contains(t, errMsg, expected, "Error message should contain: %s", expected)
75+
}
76+
77+
// Test Unwrap returns the original error
78+
unwrapped := attestorErr.Unwrap()
79+
assert.Equal(t, tc.originalError, unwrapped, "Unwrapped error should equal original error")
80+
81+
// Test IsAttestorError detection works on the error
82+
assert.True(t, IsAttestorError(attestorErr), "IsAttestorError should return true for an AttestorError")
83+
84+
// Test error wrapping if required
85+
if tc.testWrapping {
86+
// Test single level of wrapping
87+
singleWrapped := fmt.Errorf("level one: %w", attestorErr)
88+
assert.True(t, IsAttestorError(singleWrapped),
89+
"IsAttestorError should detect AttestorError through one level of wrapping")
90+
91+
// Test multiple levels of wrapping
92+
doubleWrapped := fmt.Errorf("level two: %w", singleWrapped)
93+
assert.True(t, IsAttestorError(doubleWrapped),
94+
"IsAttestorError should detect AttestorError through multiple levels of wrapping")
95+
96+
// Check errors.Is still works through wrapping
97+
if tc.originalError != nil {
98+
assert.True(t, errors.Is(doubleWrapped, tc.originalError),
99+
"errors.Is should find original error through multiple wrappings")
100+
}
101+
}
102+
})
103+
}
104+
105+
// Test that IsAttestorError returns false for non-attestor errors
106+
t.Run("detection of non-attestor errors", func(t *testing.T) {
107+
nonAttestorErr := fmt.Errorf("regular error")
108+
assert.False(t, IsAttestorError(nonAttestorErr),
109+
"IsAttestorError should return false for regular errors")
110+
111+
infraErr := NewInfrastructureError("test-operation", fmt.Errorf("infra error"))
112+
assert.False(t, IsAttestorError(infraErr),
113+
"IsAttestorError should return false for InfrastructureError")
114+
})
115+
}
116+
117+
// TestInfrastructureError tests the InfrastructureError type functionality
118+
func TestInfrastructureError(t *testing.T) {
119+
// Define test cases with different error scenarios
120+
testCases := []struct {
121+
name string
122+
operationName string
123+
originalError error
124+
expectedContains []string
125+
testWrapping bool
126+
}{
127+
{
128+
name: "basic error",
129+
operationName: "basic-operation",
130+
originalError: fmt.Errorf("something failed"),
131+
expectedContains: []string{"infrastructure error", "basic-operation", "something failed"},
132+
testWrapping: false,
133+
},
134+
{
135+
name: "empty operation name",
136+
operationName: "",
137+
originalError: fmt.Errorf("system crashed"),
138+
expectedContains: []string{"infrastructure error", "system crashed"},
139+
testWrapping: false,
140+
},
141+
{
142+
name: "nil original error",
143+
operationName: "nil-error-operation",
144+
originalError: nil,
145+
expectedContains: []string{"infrastructure error", "nil-error-operation", "<nil>"},
146+
testWrapping: false,
147+
},
148+
{
149+
name: "wrapped error test",
150+
operationName: "wrapped-operation",
151+
originalError: fmt.Errorf("root cause"),
152+
expectedContains: []string{"infrastructure error", "wrapped-operation", "root cause"},
153+
testWrapping: true,
154+
},
155+
}
156+
157+
// Run test cases
158+
for _, tc := range testCases {
159+
t.Run(tc.name, func(t *testing.T) {
160+
// Create infrastructure error
161+
infraErr := NewInfrastructureError(tc.operationName, tc.originalError)
162+
163+
// Test error message format contains expected strings
164+
errMsg := infraErr.Error()
165+
for _, expected := range tc.expectedContains {
166+
assert.Contains(t, errMsg, expected, "Error message should contain: %s", expected)
167+
}
168+
169+
// Test Unwrap returns the original error
170+
unwrapped := infraErr.Unwrap()
171+
assert.Equal(t, tc.originalError, unwrapped, "Unwrapped error should equal original error")
172+
173+
// Test IsInfrastructureError detection works on the error
174+
assert.True(t, IsInfrastructureError(infraErr), "IsInfrastructureError should return true for an InfrastructureError")
175+
176+
// Test error wrapping if required
177+
if tc.testWrapping {
178+
// Test single level of wrapping
179+
singleWrapped := fmt.Errorf("level one: %w", infraErr)
180+
assert.True(t, IsInfrastructureError(singleWrapped),
181+
"IsInfrastructureError should detect InfrastructureError through one level of wrapping")
182+
183+
// Test multiple levels of wrapping
184+
doubleWrapped := fmt.Errorf("level two: %w", singleWrapped)
185+
assert.True(t, IsInfrastructureError(doubleWrapped),
186+
"IsInfrastructureError should detect InfrastructureError through multiple levels of wrapping")
187+
188+
// Check errors.Is still works through wrapping
189+
if tc.originalError != nil {
190+
assert.True(t, errors.Is(doubleWrapped, tc.originalError),
191+
"errors.Is should find original error through multiple wrappings")
192+
}
193+
}
194+
})
195+
}
196+
197+
// Test that IsInfrastructureError returns false for non-infrastructure errors
198+
t.Run("detection of non-infrastructure errors", func(t *testing.T) {
199+
nonInfraErr := fmt.Errorf("regular error")
200+
assert.False(t, IsInfrastructureError(nonInfraErr),
201+
"IsInfrastructureError should return false for regular errors")
202+
203+
attestorErr := NewAttestorError("test-attestor", fmt.Errorf("attestor error"))
204+
assert.False(t, IsInfrastructureError(attestorErr),
205+
"IsInfrastructureError should return false for AttestorError")
206+
})
207+
}
208+
209+
// TestErrorTypeDistinction tests that the error types are properly distinguished from each other
210+
func TestErrorTypeDistinction(t *testing.T) {
211+
// Test cases covering different error types and complex wrapping scenarios
212+
testCases := []struct {
213+
name string
214+
error error
215+
isAttestor bool
216+
isInfra bool
217+
wrappingLevels int // how many levels of wrapping to apply
218+
originalMessage string
219+
}{
220+
{
221+
name: "simple attestor error",
222+
error: NewAttestorError("test-attestor", fmt.Errorf("problem")),
223+
isAttestor: true,
224+
isInfra: false,
225+
wrappingLevels: 0,
226+
originalMessage: "problem",
227+
},
228+
{
229+
name: "simple infrastructure error",
230+
error: NewInfrastructureError("test-operation", fmt.Errorf("failure")),
231+
isAttestor: false,
232+
isInfra: true,
233+
wrappingLevels: 0,
234+
originalMessage: "failure",
235+
},
236+
// In Go's error wrapping, errors.As traverses the entire chain
237+
// which means embedded errors are detectable
238+
{
239+
name: "deeply wrapped error",
240+
error: fmt.Errorf("outer: %w", fmt.Errorf("middle: %w", NewAttestorError("inner", fmt.Errorf("core")))),
241+
isAttestor: true,
242+
isInfra: false,
243+
wrappingLevels: 2,
244+
originalMessage: "core",
245+
},
246+
}
247+
248+
for _, tc := range testCases {
249+
t.Run(tc.name, func(t *testing.T) {
250+
// Test error type detection
251+
assert.Equal(t, tc.isAttestor, IsAttestorError(tc.error),
252+
"IsAttestorError detection incorrect for %s", tc.name)
253+
assert.Equal(t, tc.isInfra, IsInfrastructureError(tc.error),
254+
"IsInfrastructureError detection incorrect for %s", tc.name)
255+
256+
// Test error unwrapping to find original message
257+
var err error = tc.error
258+
if tc.originalMessage != "" {
259+
// Apply additional wrapping as specified
260+
for i := 0; i < tc.wrappingLevels; i++ {
261+
err = fmt.Errorf("wrap%d: %w", i, err)
262+
}
263+
264+
// Check if we can still detect the type
265+
assert.Equal(t, tc.isAttestor, IsAttestorError(err),
266+
"IsAttestorError detection incorrect after wrapping for %s", tc.name)
267+
assert.Equal(t, tc.isInfra, IsInfrastructureError(err),
268+
"IsInfrastructureError detection incorrect after wrapping for %s", tc.name)
269+
270+
// Try to find the original message through the wrappings
271+
found := false
272+
for err != nil {
273+
if err.Error() == tc.originalMessage || (err.Error() != "" &&
274+
(len(err.Error()) >= len(tc.originalMessage) &&
275+
err.Error()[len(err.Error())-len(tc.originalMessage):] == tc.originalMessage)) {
276+
found = true
277+
break
278+
}
279+
unwrapErr := errors.Unwrap(err)
280+
if unwrapErr == nil {
281+
break
282+
}
283+
err = unwrapErr
284+
}
285+
286+
if !found && tc.originalMessage != "" {
287+
assert.Fail(t, "Could not find original message in error chain",
288+
"Original message '%s' not found in error chain for %s",
289+
tc.originalMessage, tc.name)
290+
}
291+
}
292+
})
293+
}
294+
}

‎options/run.go

+8
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ type RunOptions struct {
3939
EnvDisableSensitiveVars bool
4040
EnvAddSensitiveKeys []string
4141
EnvExcludeSensitiveKeys []string
42+
ContinueOnInfraError bool
43+
ContinueOnAttestorError bool
44+
ContinueOnAllErrors bool
4245
}
4346

4447
var RequiredRunFlags = []string{
@@ -69,6 +72,11 @@ func (ro *RunOptions) AddFlags(cmd *cobra.Command) {
6972
cmd.Flags().StringSliceVar(&ro.EnvAddSensitiveKeys, "env-add-sensitive-key", []string{}, "Add keys or globs (e.g. '*TEXT') to the list of sensitive environment keys.")
7073
cmd.Flags().StringSliceVar(&ro.EnvExcludeSensitiveKeys, "env-exclude-sensitive-key", []string{}, "Exclude specific keys from the list of sensitive environment keys. Note: This does not support globs.")
7174

75+
// Error handling flags
76+
cmd.Flags().BoolVar(&ro.ContinueOnInfraError, "continue-on-infra-error", false, "Continue execution even if there are infrastructure errors (signing, Fulcio, Archivista) as long as the wrapped command exits successfully")
77+
cmd.Flags().BoolVar(&ro.ContinueOnAttestorError, "continue-on-attestor-error", false, "Continue execution even if one or more attestors fail as long as the wrapped command exits successfully")
78+
cmd.Flags().BoolVarP(&ro.ContinueOnAllErrors, "continue-on-errors", "x", false, "Continue execution even if there are any errors (both attestor and infrastructure) as long as the wrapped command exits successfully")
79+
7280
cmd.MarkFlagsRequiredTogether(RequiredRunFlags...)
7381

7482
attestationRegistrations := attestation.RegistrationEntries()

0 commit comments

Comments
 (0)
Please sign in to comment.