diff --git a/cmd/gon/item.go b/cmd/gon/item.go index 989b9db..fda3da0 100644 --- a/cmd/gon/item.go +++ b/cmd/gon/item.go @@ -2,11 +2,13 @@ package main import ( "context" + "fmt" "os" "sync" "github.com/fatih/color" "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-multierror" "github.com/mitchellh/gon/internal/config" "github.com/mitchellh/gon/notarize" @@ -64,7 +66,7 @@ func (i *item) notarize(ctx context.Context, opts *processOptions) error { } // Start notarization - _, err := notarize.Notarize(ctx, ¬arize.Options{ + info, err := notarize.Notarize(ctx, ¬arize.Options{ File: i.Path, BundleId: bundleId, Username: opts.Config.AppleId.Username, @@ -75,18 +77,92 @@ func (i *item) notarize(ctx context.Context, opts *processOptions) error { UploadLock: opts.UploadLock, }) - // Save our state - i.State.Notarized = err == nil + // Save the error state. We don't save the notarization result yet + // because we don't know it for sure until we download the log file. i.State.NotarizeError = err - // After we're done we want to output information for this - // file right away. - lock.Lock() + // If we had an error, we mention immediate we have an error. if err != nil { + lock.Lock() color.New(color.FgRed).Fprintf(os.Stdout, " %sError notarizing\n", opts.Prefix) lock.Unlock() + } + + // If we have a log file, download it. We do this whether we have an error + // or not because the log file can contain more details about the error. + if info != nil && info.LogFileURL != "" { + opts.Logger.Info( + "downloading log file for notarization", + "request_uuid", info.RequestUUID, + "url", info.LogFileURL, + ) + + log, logerr := notarize.DownloadLog(info.LogFileURL) + opts.Logger.Debug("log file downloaded", "log", log, "err", logerr) + if logerr != nil { + opts.Logger.Warn( + "error downloading log file, this isn't a fatal error", + "err", err, + ) + + // If we already failed notarization, just return that error + if err := i.State.NotarizeError; err != nil { + return err + } + + // If it appears we succeeded notification, we make a new error. + // We can't say notarization is successful without downloading this + // file because warnings will cause notarization to not work + // when loaded. + lock.Lock() + color.New(color.FgRed).Fprintf(os.Stdout, + " %sError downloading log file to verify notarization.\n", + opts.Prefix, + ) + lock.Unlock() + + return fmt.Errorf( + "Error downloading log file to verify notarization success: %s\n\n"+ + "You can download the log file manually at: %s", + logerr, info.LogFileURL, + ) + } + + // If we have any issues then it is a failed notarization. Notarization + // can "succeed" with warnings, but when you attempt to use/open a file + // Gatekeeper rejects it. So we currently reject any and all issues. + if len(log.Issues) > 0 { + var err error + + lock.Lock() + color.New(color.FgRed).Fprintf(os.Stdout, + " %s%d issues during notarization:\n", + opts.Prefix, len(log.Issues)) + for idx, issue := range log.Issues { + color.New(color.FgRed).Fprintf(os.Stdout, + " %sIssue #%d (%s) for path %q: %s\n", + opts.Prefix, idx+1, issue.Severity, issue.Path, issue.Message) + + // Append the error so we can return it + err = multierror.Append(err, fmt.Errorf( + "%s for path %q: %s", + issue.Severity, issue.Path, issue.Message, + )) + } + lock.Unlock() + + return err + } + } + + // If we aren't notarized, then return + if err := i.State.NotarizeError; err != nil { return err } + + // Save our state + i.State.Notarized = true + lock.Lock() color.New(color.FgGreen).Fprintf(os.Stdout, " %sFile notarized!\n", opts.Prefix) lock.Unlock() diff --git a/cmd/gon/main.go b/cmd/gon/main.go index 6bfbf90..2fd91f0 100644 --- a/cmd/gon/main.go +++ b/cmd/gon/main.go @@ -262,7 +262,7 @@ func realMain() int { // If totalErr is not nil then we had one or more errors. if totalErr != nil { - fmt.Fprintf(os.Stdout, color.RedString("❗️ Error notarizing:\n\n%s\n", totalErr)) + fmt.Fprintf(os.Stdout, color.RedString("\n❗️ Error notarizing:\n\n%s\n", totalErr)) return 1 } diff --git a/go.mod b/go.mod index 79a5574..ee32cc5 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,10 @@ go 1.13 require ( github.com/davecgh/go-spew v1.1.1 github.com/fatih/color v1.7.0 + github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-hclog v0.9.3-0.20191025211905-234833755cb2 github.com/hashicorp/go-multierror v1.0.0 + github.com/hashicorp/go-retryablehttp v0.6.3 github.com/hashicorp/hcl/v2 v2.0.0 github.com/sebdah/goldie v1.0.0 github.com/stretchr/testify v1.3.0 diff --git a/go.sum b/go.sum index 22abe1c..48d8031 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FK github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.3-0.20191025211905-234833755cb2 h1:STV8OvzphW1vlhPFxcG8d6OIilzBSKRAoWFJt+Onu10= @@ -34,6 +36,8 @@ github.com/hashicorp/go-hclog v0.9.3-0.20191025211905-234833755cb2/go.mod h1:whp github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-retryablehttp v0.6.3 h1:tuulM+WnToeqa05z83YLmKabZxrySOmJAd4mJ+s2Nfg= +github.com/hashicorp/go-retryablehttp v0.6.3/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl/v2 v2.0.0 h1:efQznTz+ydmQXq3BOnRa3AXzvCeTq1P4dKj/z5GLlY8= github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= diff --git a/notarize/log.go b/notarize/log.go new file mode 100644 index 0000000..b5c1797 --- /dev/null +++ b/notarize/log.go @@ -0,0 +1,79 @@ +package notarize + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/go-retryablehttp" +) + +// Log is the structure that is available when downloading the log file +// that the notarization service creates. +// +// This may not be complete with all fields. I only included fields that +// I saw and even then only the more useful ones. +type Log struct { + JobId string `json:"jobId"` + Status string `json:"status"` + StatusSummary string `json:"statusSummary"` + StatusCode int `json:"statusCode"` + ArchiveFilename string `json:"archiveFilename"` + UploadDate string `json:"uploadDate"` + SHA256 string `json:"sha256"` + Issues []LogIssue `json:"issues"` + TicketContents []LogTicketContent `json:"ticketContents"` +} + +// LogIssue is a single issue that may have occurred during notarization. +type LogIssue struct { + Severity string `json:"severity"` + Path string `json:"path"` + Message string `json:"message"` +} + +// LogTicketContent is an entry that was noted as being within the archive. +type LogTicketContent struct { + Path string `json:"path"` + DigestAlgorithm string `json:"digestAlgorithm"` + CDHash string `json:"cdhash"` + Arch string `json:"arch"` +} + +// These are the log severities that may exist. +const ( + LogSeverityError = "error" + LogSeverityWarning = "warning" +) + +// ParseLog parses a log from the given reader, such as an HTTP response. +func ParseLog(r io.Reader) (*Log, error) { + // Protect against this since it is common with HTTP responses. + if r == nil { + return nil, fmt.Errorf("nil reader given to ParseLog") + } + + var result Log + return &result, json.NewDecoder(r).Decode(&result) +} + +// DownloadLog downloads a log file and parses it using a default HTTP client. +// If you want more fine-grained control over the download, download it +// using your own client and use ParseLog. +func DownloadLog(path string) (*Log, error) { + // Build our HTTP client + client := retryablehttp.NewClient() + client.HTTPClient = cleanhttp.DefaultClient() + + // Get it! + resp, err := client.Get(path) + if err != nil { + return nil, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + + return ParseLog(resp.Body) +}