add retry logic for native notarization and codesigning (#7806)

Related to #7130, this adds logic to retry native notarization up to three times if it fails for some reason.

Since we're adding retries in various places, I added a new package under pkg for this purpose.
This commit is contained in:
Roberto Dip
2022-09-19 13:08:39 -03:00
committed by GitHub
parent d5a37dfd1a
commit 15c93f02ea
5 changed files with 146 additions and 36 deletions
@@ -0,0 +1 @@
* Added logic to retry notarization and codesigning when using `FLEETCTL_NATIVE_TOOLING`
+35 -29
View File
@@ -7,6 +7,7 @@ import (
"os/exec"
"path/filepath"
"github.com/fleetdm/fleet/v4/pkg/retry"
"github.com/fleetdm/fleet/v4/pkg/secure"
)
@@ -18,20 +19,22 @@ func rSign(pkgPath, cert string) error {
return fmt.Errorf("writing cert data: %e", err)
}
var outBuf bytes.Buffer
cmd := exec.Command(
"rcodesign",
"sign",
pkgPath,
"--pem-source", pemPath,
)
cmd.Stdout = &outBuf
cmd.Stderr = &outBuf
if err := cmd.Run(); err != nil {
fmt.Println(outBuf.String())
return fmt.Errorf("rcodesign: %w", err)
}
return nil
return retry.Do(func() error {
var outBuf bytes.Buffer
cmd := exec.Command(
"rcodesign",
"sign",
pkgPath,
"--pem-source", pemPath,
)
cmd.Stdout = &outBuf
cmd.Stderr = &outBuf
if err := cmd.Run(); err != nil {
fmt.Println(outBuf.String())
return fmt.Errorf("rcodesign: %w", err)
}
return nil
}, retry.WithMaxAttempts(3))
}
func rNotarizeStaple(pkg, apiKeyID, apiKeyIssuer, apiKeyContent string) error {
@@ -40,21 +43,24 @@ func rNotarizeStaple(pkg, apiKeyID, apiKeyIssuer, apiKeyContent string) error {
if err != nil {
return fmt.Errorf("writing API keys: %e", err)
}
var outBuf bytes.Buffer
cmd := exec.Command("rcodesign",
"notarize",
pkg,
"--api-issuer", apiKeyIssuer,
"--api-key", apiKeyID,
"--staple",
)
cmd.Stdout = &outBuf
cmd.Stderr = &outBuf
if err := cmd.Run(); err != nil {
fmt.Println(outBuf.String())
return fmt.Errorf("rcodesign notarize: %w", err)
}
return nil
return retry.Do(func() error {
var outBuf bytes.Buffer
cmd := exec.Command("rcodesign",
"notarize",
pkg,
"--api-issuer", apiKeyIssuer,
"--api-key", apiKeyID,
"--staple",
)
cmd.Stdout = &outBuf
cmd.Stderr = &outBuf
if err := cmd.Run(); err != nil {
fmt.Println(outBuf.String())
return fmt.Errorf("rcodesign notarize: %w", err)
}
return nil
}, retry.WithMaxAttempts(3))
}
func writeAPIKeys(issuer, id, content string) (string, error) {
+6 -7
View File
@@ -9,6 +9,8 @@ import (
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/pkg/retry"
)
func lock(lockFilePath string) {
@@ -74,13 +76,10 @@ func Retryable(err error) bool {
// RunWithNetRetry runs the given function and retries in case of network errors (see Retryable).
func RunWithNetRetry(t *testing.T, fn func() error) error {
for {
err := fn()
if err != nil && Retryable(err) {
time.Sleep(5 * time.Second)
return retry.Do(func() error {
if err := fn(); err != nil && Retryable(err) {
t.Logf("%s: retrying error: %s", t.Name(), err)
continue
}
return err
}
return nil
}, retry.WithInterval(5*time.Second))
}
+63
View File
@@ -0,0 +1,63 @@
// package retry has utilities to retry operations
package retry
import (
"time"
)
type config struct {
interval time.Duration
maxAttempts int
}
// Option allows to configure the behavior of retry.Do
type Option func(*config)
// WithRetryInterval allows to specify a custom duration to wait
// between retries.
func WithInterval(i time.Duration) Option {
return func(c *config) {
c.interval = i
}
}
// WithMaxAttempts allows to specify a maximum number of attempts
// before the doer gives up
func WithMaxAttempts(a int) Option {
return func(c *config) {
c.maxAttempts = a
}
}
// Do executes the provided function, if the function returns a
// non-nil error it performs a retry according to the options
// provided.
//
// By default operations are retried an unlimited number of times for 30
// seconds
func Do(fn func() error, opts ...Option) error {
cfg := &config{
interval: 30 * time.Second,
}
for _, opt := range opts {
opt(cfg)
}
attempts := 0
ticker := time.NewTicker(cfg.interval)
defer ticker.Stop()
for {
attempts++
err := fn()
if err == nil {
return nil
}
if cfg.maxAttempts != 0 && attempts >= cfg.maxAttempts {
return err
}
<-ticker.C
}
}
+41
View File
@@ -0,0 +1,41 @@
package retry
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
)
var errTest = errors.New("test error")
func TestRetryDo(t *testing.T) {
t.Run("WithMaxAttempts only performs the operation the configured number of times", func(t *testing.T) {
count := 0
max := 3
err := Do(func() error {
count++
return errTest
}, WithMaxAttempts(max), WithInterval(1*time.Millisecond))
require.ErrorIs(t, errTest, err)
require.Equal(t, max, count)
})
t.Run("operations are run an unlimited number of times by default", func(t *testing.T) {
count := 0
max := 10
err := Do(func() error {
if count++; count != max {
return errTest
}
return nil
}, WithInterval(1*time.Millisecond))
require.NoError(t, err)
require.Equal(t, max, count)
})
}