diff --git a/.gon.hcl b/.gon.hcl index dfc1781..af92701 100644 --- a/.gon.hcl +++ b/.gon.hcl @@ -4,6 +4,7 @@ bundle_id = "com.mitchellh.gon" apple_id { username = "mitchell.hashimoto@gmail.com" password = "@env:AC_PASSWORD" + provider = "UL304B4VGY" } sign { diff --git a/README.md b/README.md index b2e2cec..47cab5a 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,7 @@ bundle_id = "com.mitchellh.example.terraform" apple_id { username = "mitchell@example.com" password = "@env:AC_PASSWORD" + provider = "UL304B4VGY" } sign { diff --git a/cmd/gon/item.go b/cmd/gon/item.go index fda3da0..9227a13 100644 --- a/cmd/gon/item.go +++ b/cmd/gon/item.go @@ -2,13 +2,11 @@ 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" @@ -66,19 +64,18 @@ func (i *item) notarize(ctx context.Context, opts *processOptions) error { } // Start notarization - info, err := notarize.Notarize(ctx, ¬arize.Options{ - File: i.Path, - BundleId: bundleId, - Username: opts.Config.AppleId.Username, - Password: opts.Config.AppleId.Password, - Provider: opts.Config.AppleId.Provider, - Logger: opts.Logger.Named("notarize"), - Status: &statusHuman{Prefix: opts.Prefix, Lock: lock}, - UploadLock: opts.UploadLock, + _, _, err := notarize.Notarize(ctx, ¬arize.Options{ + File: i.Path, + DeveloperId: opts.Config.AppleId.Username, + Password: opts.Config.AppleId.Password, + Provider: opts.Config.AppleId.Provider, + Logger: opts.Logger.Named("notarize"), + Status: &statusHuman{Prefix: opts.Prefix, Lock: lock}, + UploadLock: opts.UploadLock, }) // 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. + // because we don't know it for sure until we retrieve the log information. i.State.NotarizeError = err // If we had an error, we mention immediate we have an error. @@ -88,73 +85,6 @@ func (i *item) notarize(ctx context.Context, opts *processOptions) error { 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 diff --git a/cmd/gon/status_human.go b/cmd/gon/status_human.go index 8b484ee..09d0284 100644 --- a/cmd/gon/status_human.go +++ b/cmd/gon/status_human.go @@ -18,7 +18,8 @@ type statusHuman struct { Prefix string Lock *sync.Mutex - lastStatus string + lastInfoStatus string + lastLogStatus string } func (s *statusHuman) Submitting() { @@ -37,13 +38,23 @@ func (s *statusHuman) Submitted(uuid string) { os.Stdout, " %sWaiting for results from Apple. This can take minutes to hours.\n", s.Prefix) } -func (s *statusHuman) Status(info notarize.Info) { +func (s *statusHuman) InfoStatus(info notarize.Info) { s.Lock.Lock() defer s.Lock.Unlock() - if info.Status != s.lastStatus { - s.lastStatus = info.Status - color.New().Fprintf(os.Stdout, " %sStatus: %s\n", s.Prefix, info.Status) + if info.Status != s.lastInfoStatus { + s.lastInfoStatus = info.Status + color.New().Fprintf(os.Stdout, " %sInfoStatus: %s\n", s.Prefix, info.Status) + } +} + +func (s *statusHuman) LogStatus(log notarize.Log) { + s.Lock.Lock() + defer s.Lock.Unlock() + + if log.Status != s.lastLogStatus { + s.lastLogStatus = log.Status + color.New().Fprintf(os.Stdout, " %sLogStatus: %s\n", s.Prefix, log.Status) } } diff --git a/go.mod b/go.mod index ee32cc5..79a5574 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,8 @@ 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 48d8031..fe2093c 100644 --- a/go.sum +++ b/go.sum @@ -26,18 +26,11 @@ github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd/go.mod h1:qkLSc0A5EX github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 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= github.com/hashicorp/go-hclog v0.9.3-0.20191025211905-234833755cb2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= 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/info.go b/notarize/info.go index f23e32a..4c3f518 100644 --- a/notarize/info.go +++ b/notarize/info.go @@ -7,7 +7,6 @@ import ( "io" "os/exec" "path/filepath" - "time" "github.com/hashicorp/go-hclog" "howett.net/plist" @@ -23,33 +22,19 @@ type Info struct { // RequestUUID is the UUID provided by Apple after submitting the // notarization request. This can be used to look up notarization information // using the Apple tooling. - RequestUUID string `plist:"RequestUUID"` + RequestUUID string `plist:"id"` // Date is the date and time of submission - Date time.Time `plist:"Date"` + Date string `plist:"createdDate"` - // Hash is the encoded hash value for the submitted file. This is provided - // by Apple. This is not decoded into a richer type like hash/sha256 because - // it doesn't seem to be guaranteed by Apple anywhere what format this is in. - Hash string `plist:"Hash"` - - // LogFileURL is a URL to a log file for more details. - LogFileURL string `plist:"LogFileURL"` + // Name is th file uploaded for submission. + Name string `plist:"name"` // Status the status of the notarization. - // - // StatusMessage is a human-friendly message associated with a status. - Status string `plist:"Status"` - StatusMessage string `plist:"Status Message"` -} - -// infoResult is the structure of the plist emitted directly from -// --notarization-info -type infoResult struct { - Info *Info `plist:"notarization-info"` + Status string `plist:"status"` - // Errors is the list of errors that occurred while uploading - Errors Errors `plist:"product-errors"` + // StatusMessage is a human-friendly message associated with a status. + StatusMessage string `plist:"message"` } // info requests the information about a notarization and returns @@ -78,12 +63,13 @@ func info(ctx context.Context, uuid string, opts *Options) (*Info, error) { cmd.Args = []string{ filepath.Base(cmd.Path), - "altool", - "--notarization-info", + "notarytool", + "info", uuid, - "-u", opts.Username, - "-p", opts.Password, - "--output-format", "xml", + "--apple-id", opts.DeveloperId, + "--password", opts.Password, + "--team-id", opts.Provider, + "--output-format", "plist", } // We store all output in out for logging and in case there is an error @@ -109,23 +95,18 @@ func info(ctx context.Context, uuid string, opts *Options) (*Info, error) { // If we have any output, try to decode that since even in the case of // an error it will output some information. - var result infoResult + var result Info if out.Len() > 0 { if _, perr := plist.Unmarshal(out.Bytes(), &result); perr != nil { return nil, fmt.Errorf("failed to decode notarization submission output: %w", perr) } } - // If there are errors in the result, then show that error - if len(result.Errors) > 0 { - return nil, result.Errors - } - // Now we check the error for actually running the process if err != nil { return nil, fmt.Errorf("error checking on notarization status:\n\n%s", combined.String()) } - logger.Info("notarization info", "uuid", uuid, "info", result.Info) - return result.Info, nil + logger.Info("notarization info", "uuid", uuid, "info", result) + return &result, nil } diff --git a/notarize/info_test.go b/notarize/info_test.go index 37b040b..bf7b72b 100644 --- a/notarize/info_test.go +++ b/notarize/info_test.go @@ -11,56 +11,75 @@ import ( ) func init() { - childCommands["info-success"] = testCmdInfoSuccess + childCommands["info-accepted"] = testCmdInfoAcceptedSubmission + childCommands["info-invalid"] = testCmdInfoInvalidSubmission } -func TestInfo_success(t *testing.T) { +func TestInfo_accepted(t *testing.T) { info, err := info(context.Background(), "foo", &Options{ Logger: hclog.L(), - BaseCmd: childCmd(t, "info-success"), + BaseCmd: childCmd(t, "info-accepted"), }) require := require.New(t) require.NoError(err) - require.Equal(info.RequestUUID, "edc8e846-d6ce-444d-9eef-499aa444da1c") - require.Equal(info.Hash, "644d0af906ae26c87037cd6e9073382d5b0461b39e7f23c7bb69a35debacedd4") - require.Equal(info.LogFileURL, "https://osxapps-ssl.itunes.apple.com/itunes-assets/Enigma123/v4/29/f2/81/29f28128-e2be-158a-f421-1e19692dd935/developer_log.json?accessKey=1572864491_3132212434837665280_4XLMw7lZxMfKdHhgnlPkueVue9woI2MjQ6VEc8R0cxJrL9GGcTQSiE0C9Cu5o6o%2B3JtYGSqGWdvc3mJHbS0NBRZkHT%2BbwbdMGPT8poYk7TTkfHUIcW5aBz0aFO7RB6mSWVuZWOFT0dZ4VS%2Bep2LUP2KTDtDwiGQbTULu9VgZ1oY%3D") - require.Equal(info.Status, "success") - require.Equal(info.StatusMessage, "Package Approved") + require.Equal(info.RequestUUID, "32684f68-d63e-49ba-9234-25eeec84b369") + require.Equal(info.Status, "Accepted") + require.Equal(info.StatusMessage, "Successfully received submission info") } -// testCmdInfoSuccess mimicks a successful submission. -func testCmdInfoSuccess() int { +func TestInfo_invalid(t *testing.T) { + info, err := info(context.Background(), "foo", &Options{ + Logger: hclog.L(), + BaseCmd: childCmd(t, "info-invalid"), + }) + + require := require.New(t) + require.NoError(err) + require.Equal(info.RequestUUID, "cfd69166-8e2f-1397-8636-ec06f98e3597") + require.Equal(info.Status, "Invalid") +} + +// testCmdInfoAcceptedSubmission mimicks an accepted submission. +func testCmdInfoAcceptedSubmission() int { + fmt.Println(strings.TrimSpace(` + + + + + createdDate + 2023-08-01T08:22:19.939Z + id + 32684f68-d63e-49ba-9234-25eeec84b369 + message + Successfully received submission info + name + binary.zip + status + Accepted + + +`)) + return 0 +} + +// testCmdInfoInvalidSubmission mimicks an invalid submission. +func testCmdInfoInvalidSubmission() int { fmt.Println(strings.TrimSpace(` - notarization-info - - Date - 2019-11-02T02:17:12Z - Hash - 644d0af906ae26c87037cd6e9073382d5b0461b39e7f23c7bb69a35debacedd4 - LogFileURL - https://osxapps-ssl.itunes.apple.com/itunes-assets/Enigma123/v4/29/f2/81/29f28128-e2be-158a-f421-1e19692dd935/developer_log.json?accessKey=1572864491_3132212434837665280_4XLMw7lZxMfKdHhgnlPkueVue9woI2MjQ6VEc8R0cxJrL9GGcTQSiE0C9Cu5o6o%2B3JtYGSqGWdvc3mJHbS0NBRZkHT%2BbwbdMGPT8poYk7TTkfHUIcW5aBz0aFO7RB6mSWVuZWOFT0dZ4VS%2Bep2LUP2KTDtDwiGQbTULu9VgZ1oY%3D - RequestUUID - edc8e846-d6ce-444d-9eef-499aa444da1c - Status - success - Status Code - 0 - Status Message - Package Approved - - os-version - 10.15.1 - success-message - No errors getting notarization info. - tool-path - /Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/Frameworks/AppStoreService.framework - tool-version - 4.00.1181 + createdDate + 2023-08-01T08:12:11.193Z + id + cfd69166-8e2f-1397-8636-ec06f98e3597 + message + Successfully received submission info + name + binary.zip + status + Invalid `)) diff --git a/notarize/log.go b/notarize/log.go index c9325a1..c2ac6cc 100644 --- a/notarize/log.go +++ b/notarize/log.go @@ -1,20 +1,18 @@ package notarize import ( + "bytes" + "context" "encoding/json" "fmt" "io" + "os/exec" + "path/filepath" - "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-hclog" - "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. +// Log Retrieves notarization log for a single completed submission type Log struct { JobId string `json:"jobId"` Status string `json:"status"` @@ -42,40 +40,77 @@ type LogTicketContent struct { Arch string `json:"arch"` } -// These are the log severities that may exist. -const ( - LogSeverityError = "error" - LogSeverityWarning = "warning" -) +// log requests the information about a notarization and returns +// the updated information. +func log(ctx context.Context, uuid string, opts *Options) (*Log, error) { + logger := opts.Logger + if logger == nil { + logger = hclog.NewNullLogger() + } + + // Build our command + var cmd exec.Cmd + if opts.BaseCmd != nil { + cmd = *opts.BaseCmd + } -// 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") + // We only set the path if it isn't set. This lets the options set the + // path to the codesigning binary that we use. + if cmd.Path == "" { + path, err := exec.LookPath("xcrun") + if err != nil { + return nil, err + } + cmd.Path = path } + cmd.Args = []string{ + filepath.Base(cmd.Path), + "notarytool", + "log", + uuid, + "--apple-id", opts.DeveloperId, + "--password", opts.Password, + "--team-id", opts.Provider, + } + + // We store all output in out for logging and in case there is an error + var out, combined bytes.Buffer + cmd.Stdout = io.MultiWriter(&out, &combined) + cmd.Stderr = &combined + + // Log what we're going to execute + logger.Info("requesting notarization log", + "uuid", uuid, + "command_path", cmd.Path, + "command_args", cmd.Args, + ) + + // Execute + err := cmd.Run() + + // Log the result + logger.Info("notarization log command finished", + "output", out.String(), + "err", err, + ) + + // If we have any output, try to decode that since even in the case of + // an error it will output some information. var result Log - return &result, json.NewDecoder(r).Decode(&result) -} + // return &result, json.NewDecoder().Decode(&result) + if out.Len() > 0 { + if derr := json.Unmarshal(out.Bytes(), &result); derr != nil { + return nil, fmt.Errorf("failed to decode notarization submission output: %w", derr) -// 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() - client.Logger = hclog.NewNullLogger() - - // Get it! - resp, err := client.Get(path) - if err != nil { - return nil, err + } } - if resp.Body != nil { - defer resp.Body.Close() + + // Now we check the error for actually running the process + if err != nil { + return nil, fmt.Errorf("error checking on notarization status:\n\n%s", combined.String()) } - return ParseLog(resp.Body) + logger.Info("notarization log", "uuid", uuid, "info", result) + return &result, nil } diff --git a/notarize/log_test.go b/notarize/log_test.go index bc7c2bc..261b3bc 100644 --- a/notarize/log_test.go +++ b/notarize/log_test.go @@ -1,44 +1,116 @@ package notarize import ( - "os" - "path/filepath" + "context" + "fmt" + "strings" "testing" - "github.com/davecgh/go-spew/spew" - "github.com/sebdah/goldie" + "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" ) func init() { - goldie.FixtureDir = "testdata" - spew.Config.DisablePointerAddresses = true + childCommands["log-accepted"] = testCmdLogValidSubmission + childCommands["log-invalid"] = testCmdLogInvalidSubmission } -func TestParseFile(t *testing.T) { - f, err := os.Open("testdata") - require.NoError(t, err) - defer f.Close() +func TestLog_accepted(t *testing.T) { + log, err := log(context.Background(), "foo", &Options{ + Logger: hclog.L(), + BaseCmd: childCmd(t, "log-accepted"), + }) - fis, err := f.Readdir(-1) - require.NoError(t, err) - for _, fi := range fis { - if fi.IsDir() { - continue - } + require := require.New(t) + require.NoError(err) + require.Equal(log.JobId, "3382aa04-e417-46a0-b1b4-42eebf85906c") + require.Equal(log.Status, "Accepted") + require.Equal(log.StatusSummary, "Ready for distribution") + require.Equal(len(log.Issues), 0) + require.Equal(len(log.TicketContents), 1) +} - if filepath.Ext(fi.Name()) == ".golden" { - continue - } +func TestLog_invalid(t *testing.T) { + log, err := log(context.Background(), "foo", &Options{ + Logger: hclog.L(), + BaseCmd: childCmd(t, "log-invalid"), + }) + + require := require.New(t) + require.NoError(err) + require.Equal(log.JobId, "4ba7c420-7444-44bc-a190-1bd4bad97b13") + require.Equal(log.Status, "Invalid") + require.Equal(log.StatusSummary, "Archive contains critical validation errors") + require.Equal(len(log.TicketContents), 0) + require.Equal(len(log.Issues), 3) +} - t.Run(fi.Name(), func(t *testing.T) { - f, err := os.Open(filepath.Join("testdata", fi.Name())) - require.NoError(t, err) - defer f.Close() +// testCmdLogValidSubmission mimicks an accepted submission. +func testCmdLogValidSubmission() int { + fmt.Println(strings.TrimSpace(` +{ + "logFormatVersion": 1, + "jobId": "3382aa04-e417-46a0-b1b4-42eebf85906c", + "status": "Accepted", + "statusSummary": "Ready for distribution", + "statusCode": 0, + "archiveFilename": "gon.zip", + "uploadDate": "2019-11-06T00:51:10Z", + "sha256": "1070be725b5b0c89b8dad699a9080a3bf5809fe68bfe8f84d6ff4a282d661fd1", + "ticketContents": [ + { + "path": "gon.zip/foo", + "digestAlgorithm": "SHA-256", + "cdhash": "b7049085e21423f102d6119bca93d57ebd903289", + "arch": "x86_64" + } + ], + "issues": null +} +`)) + return 0 +} - log, err := ParseLog(f) - require.NoError(t, err) - goldie.Assert(t, fi.Name(), []byte(spew.Sdump(log))) - }) - } +// testCmdLogInvalidSubmission mimicks an invalid submission. +func testCmdLogInvalidSubmission() int { + fmt.Println(strings.TrimSpace(` +{ + "logFormatVersion": 1, + "jobId": "4ba7c420-7444-44bc-a190-1bd4bad97b13", + "status": "Invalid", + "statusSummary": "Archive contains critical validation errors", + "statusCode": 4000, + "archiveFilename": "gon.zip", + "uploadDate": "2019-11-06T00:54:22Z", + "sha256": "c109f26d378fbf1efadc8987fdab79d2ce63155e8941823d4d11a907152e11a5", + "ticketContents": null, + "issues": [ + { + "severity": "error", + "code": null, + "path": "gon.zip/foo", + "message": "The binary is not signed.", + "docUrl": null, + "architecture": "x86_64" + }, + { + "severity": "error", + "code": null, + "path": "gon.zip/foo", + "message": "The signature does not include a secure timestamp.", + "docUrl": null, + "architecture": "x86_64" + }, + { + "severity": "error", + "code": null, + "path": "gon.zip/foo", + "message": "The executable does not have the hardened runtime enabled.", + "docUrl": null, + "architecture": "x86_64" + } + ] +} +`)) + return 0 } diff --git a/notarize/notarize.go b/notarize/notarize.go index c28162a..c235917 100644 --- a/notarize/notarize.go +++ b/notarize/notarize.go @@ -16,11 +16,8 @@ type Options struct { // File is the file to notarize. This must be in zip, dmg, or pkg format. File string - // BundleId is the bundle ID for the package. Ex. "com.example.myapp" - BundleId string - - // Username is your Apple Connect username. - Username string + // DeveloperId is your Apple Developer Apple ID. + DeveloperId string // Password is your Apple Connect password. This must be specified. // This also supports `@keychain:` and `@env:` formats to @@ -47,7 +44,7 @@ type Options struct { // BaseCmd is the base command for executing app submission. This is // used for tests to overwrite where the codesign binary is. If this isn't - // specified then we use `xcrun altool` as the base. + // specified then we use `xcrun notarytool` as the base. BaseCmd *exec.Cmd } @@ -61,7 +58,7 @@ type Options struct { // // If error is nil, then Info is guaranteed to be non-nil. // If error is not nil, notarization failed and Info _may_ be non-nil. -func Notarize(ctx context.Context, opts *Options) (*Info, error) { +func Notarize(ctx context.Context, opts *Options) (*Info, *Log, error) { logger := opts.Logger if logger == nil { logger = hclog.NewNullLogger() @@ -83,7 +80,7 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) { uuid, err := upload(ctx, opts) lock.Unlock() if err != nil { - return nil, err + return nil, nil, err } status.Submitted(uuid) @@ -91,10 +88,10 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) { // _to even exist_. While we get an error requesting info with an error // code of 1519 (UUID not found), then we are stuck in a queue. Sometimes // this queue is hours long. We just have to wait. - result := &Info{RequestUUID: uuid} + infoResult := &Info{RequestUUID: uuid} for { time.Sleep(10 * time.Second) - _, err := info(ctx, result.RequestUUID, opts) + _, err := info(ctx, infoResult.RequestUUID, opts) if err == nil { break } @@ -106,7 +103,7 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) { } // A real error, just return that - return result, err + return infoResult, nil, err } // Now that the UUID result has been found, we poll more quickly @@ -115,9 +112,42 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) { for { // Update the info. It is possible for this to return a nil info // and we dont' ever want to set result to nil so we have a check. - newResult, err := info(ctx, result.RequestUUID, opts) - if newResult != nil { - result = newResult + newInfoResult, err := info(ctx, infoResult.RequestUUID, opts) + if newInfoResult != nil { + infoResult = newInfoResult + } + + if err != nil { + // This code is the network became unavailable error. If this + // happens then we just log and retry. + if e, ok := err.(Errors); ok && e.ContainsCode(-19000) { + logger.Warn("error that network became unavailable, will retry") + goto RETRYINFO + } + + return infoResult, nil, err + } + + status.InfoStatus(*infoResult) + + // If we reached a terminal state then exit + if infoResult.Status == "Accepted" || infoResult.Status == "Invalid" { + break + } + + RETRYINFO: + // Sleep, we just do a constant poll every 5 seconds. I haven't yet + // found any rate limits to the service so this seems okay. + time.Sleep(5 * time.Second) + } + + logResult := &Log{JobId: uuid} + for { + // Update the log. It is possible for this to return a nil log + // and we dont' ever want to set result to nil so we have a check. + newLogResult, err := log(ctx, logResult.JobId, opts) + if newLogResult != nil { + logResult = newLogResult } if err != nil { @@ -125,20 +155,20 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) { // happens then we just log and retry. if e, ok := err.(Errors); ok && e.ContainsCode(-19000) { logger.Warn("error that network became unavailable, will retry") - goto RETRY + goto RETRYLOG } - return result, err + return infoResult, logResult, err } - status.Status(*result) + status.LogStatus(*logResult) // If we reached a terminal state then exit - if result.Status == "success" || result.Status == "invalid" { + if logResult.Status == "Accepted" || logResult.Status == "Invalid" { break } - RETRY: + RETRYLOG: // Sleep, we just do a constant poll every 5 seconds. I haven't yet // found any rate limits to the service so this seems okay. time.Sleep(5 * time.Second) @@ -146,9 +176,9 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) { // If we're in an invalid status then return an error err = nil - if result.Status == "invalid" { - err = fmt.Errorf("package is invalid. To learn more download the logs at the URL: %s", result.LogFileURL) + if logResult.Status == "Invalid" && infoResult.Status == "Invalid" { + err = fmt.Errorf("package is invalid.") } - return result, err + return infoResult, logResult, err } diff --git a/notarize/status.go b/notarize/status.go index bdb3b4d..060dd00 100644 --- a/notarize/status.go +++ b/notarize/status.go @@ -12,11 +12,14 @@ type Status interface { // The arguments give you access to the requestUUID to query more information. Submitted(requestUUID string) - // Status is called as the status of the submitted package changes. + // InfoStatus is called as the status of the submitted package changes. // The info argument contains additional information about the status. // Note that some fields in the info argument may not be populated, please // refer to the docs. - Status(Info) + InfoStatus(Info) + + // LogStatus is called as the status of the submitted package changes. + LogStatus(Log) } // noopStatus implements Status and does nothing. @@ -24,7 +27,8 @@ type noopStatus struct{} func (noopStatus) Submitting() {} func (noopStatus) Submitted(string) {} -func (noopStatus) Status(Info) {} +func (noopStatus) InfoStatus(Info) {} +func (noopStatus) LogStatus(Log) {} // Assert that we always implement it var _ Status = noopStatus{} diff --git a/notarize/testdata/log_zip_failure.json b/notarize/testdata/log_zip_failure.json deleted file mode 100644 index de6d2d1..0000000 --- a/notarize/testdata/log_zip_failure.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "logFormatVersion": 1, - "jobId": "4ba7c420-7444-44bc-a190-1bd4bad97b13", - "status": "Invalid", - "statusSummary": "Archive contains critical validation errors", - "statusCode": 4000, - "archiveFilename": "gon.zip", - "uploadDate": "2019-11-06T00:54:22Z", - "sha256": "c109f26d378fbf1efadc8987fdab79d2ce63155e8941823d4d11a907152e11a5", - "ticketContents": null, - "issues": [ - { - "severity": "error", - "code": null, - "path": "gon.zip/foo", - "message": "The binary is not signed.", - "docUrl": null, - "architecture": "x86_64" - }, - { - "severity": "error", - "code": null, - "path": "gon.zip/foo", - "message": "The signature does not include a secure timestamp.", - "docUrl": null, - "architecture": "x86_64" - }, - { - "severity": "error", - "code": null, - "path": "gon.zip/foo", - "message": "The executable does not have the hardened runtime enabled.", - "docUrl": null, - "architecture": "x86_64" - } - ] -} diff --git a/notarize/testdata/log_zip_failure.json.golden b/notarize/testdata/log_zip_failure.json.golden deleted file mode 100644 index 4391593..0000000 --- a/notarize/testdata/log_zip_failure.json.golden +++ /dev/null @@ -1,27 +0,0 @@ -(*notarize.Log)({ - JobId: (string) (len=36) "4ba7c420-7444-44bc-a190-1bd4bad97b13", - Status: (string) (len=7) "Invalid", - StatusSummary: (string) (len=43) "Archive contains critical validation errors", - StatusCode: (int) 4000, - ArchiveFilename: (string) (len=7) "gon.zip", - UploadDate: (string) (len=20) "2019-11-06T00:54:22Z", - SHA256: (string) (len=64) "c109f26d378fbf1efadc8987fdab79d2ce63155e8941823d4d11a907152e11a5", - Issues: ([]notarize.LogIssue) (len=3 cap=4) { - (notarize.LogIssue) { - Severity: (string) (len=5) "error", - Path: (string) (len=11) "gon.zip/foo", - Message: (string) (len=25) "The binary is not signed." - }, - (notarize.LogIssue) { - Severity: (string) (len=5) "error", - Path: (string) (len=11) "gon.zip/foo", - Message: (string) (len=50) "The signature does not include a secure timestamp." - }, - (notarize.LogIssue) { - Severity: (string) (len=5) "error", - Path: (string) (len=11) "gon.zip/foo", - Message: (string) (len=58) "The executable does not have the hardened runtime enabled." - } - }, - TicketContents: ([]notarize.LogTicketContent) -}) diff --git a/notarize/testdata/log_zip_success.json b/notarize/testdata/log_zip_success.json deleted file mode 100644 index 79d0d30..0000000 --- a/notarize/testdata/log_zip_success.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "logFormatVersion": 1, - "jobId": "3382aa04-e417-46a0-b1b4-42eebf85906c", - "status": "Accepted", - "statusSummary": "Ready for distribution", - "statusCode": 0, - "archiveFilename": "gon.zip", - "uploadDate": "2019-11-06T00:51:10Z", - "sha256": "1070be725b5b0c89b8dad699a9080a3bf5809fe68bfe8f84d6ff4a282d661fd1", - "ticketContents": [ - { - "path": "gon.zip/foo", - "digestAlgorithm": "SHA-256", - "cdhash": "b7049085e21423f102d6119bca93d57ebd903289", - "arch": "x86_64" - } - ], - "issues": null -} diff --git a/notarize/testdata/log_zip_success.json.golden b/notarize/testdata/log_zip_success.json.golden deleted file mode 100644 index 60190c3..0000000 --- a/notarize/testdata/log_zip_success.json.golden +++ /dev/null @@ -1,18 +0,0 @@ -(*notarize.Log)({ - JobId: (string) (len=36) "3382aa04-e417-46a0-b1b4-42eebf85906c", - Status: (string) (len=8) "Accepted", - StatusSummary: (string) (len=22) "Ready for distribution", - StatusCode: (int) 0, - ArchiveFilename: (string) (len=7) "gon.zip", - UploadDate: (string) (len=20) "2019-11-06T00:51:10Z", - SHA256: (string) (len=64) "1070be725b5b0c89b8dad699a9080a3bf5809fe68bfe8f84d6ff4a282d661fd1", - Issues: ([]notarize.LogIssue) , - TicketContents: ([]notarize.LogTicketContent) (len=1 cap=4) { - (notarize.LogTicketContent) { - Path: (string) (len=11) "gon.zip/foo", - DigestAlgorithm: (string) (len=7) "SHA-256", - CDHash: (string) (len=40) "b7049085e21423f102d6119bca93d57ebd903289", - Arch: (string) (len=6) "x86_64" - } - } -}) diff --git a/notarize/upload.go b/notarize/upload.go index a755b2b..960a4cb 100644 --- a/notarize/upload.go +++ b/notarize/upload.go @@ -38,24 +38,14 @@ func upload(ctx context.Context, opts *Options) (string, error) { cmd.Args = []string{ filepath.Base(cmd.Path), - "altool", - "--notarize-app", - "--primary-bundle-id", opts.BundleId, - "-u", opts.Username, - "-p", opts.Password, + "notarytool", + "submit", opts.File, + "--apple-id", opts.DeveloperId, + "--password", opts.Password, + "--team-id", opts.Provider, + "--output-format", "plist", } - if opts.Provider != "" { - cmd.Args = append(cmd.Args, - "--asc-provider", opts.Provider, - ) - } - - cmd.Args = append(cmd.Args, - "-f", opts.File, - "--output-format", "xml", - ) - // We store all output in out for logging and in case there is an error var out, combined bytes.Buffer cmd.Stdout = io.MultiWriter(&out, &combined) @@ -86,36 +76,26 @@ func upload(ctx context.Context, opts *Options) (string, error) { } } - // If there are errors in the result, then show that error - if len(result.Errors) > 0 { - return "", result.Errors - } - // Now we check the error for actually running the process if err != nil { return "", fmt.Errorf("error submitting for notarization:\n\n%s", combined.String()) } // We should have a request UUID set at this point since we checked for errors - if result.Upload == nil || result.Upload.RequestUUID == "" { + if result.RequestUUID == "" { return "", fmt.Errorf( "notarization appeared to succeed, but we failed at parsing " + "the request UUID. Please enable logging, try again, and report " + "this as a bug.") } - logger.Info("notarization request submitted", "request_id", result.Upload.RequestUUID) - return result.Upload.RequestUUID, nil + logger.Info("notarization request submitted", "request_id", result.RequestUUID) + return result.RequestUUID, nil } // uploadResult is the plist structure when the upload succeeds type uploadResult struct { // Upload is non-nil if there is a successful upload - Upload *struct { - RequestUUID string `plist:"RequestUUID"` - } `plist:"notarization-upload"` - - // Errors is the list of errors that occurred while uploading - Errors Errors `plist:"product-errors"` + RequestUUID string `plist:"id"` } diff --git a/notarize/upload_test.go b/notarize/upload_test.go index 46d5716..28455fd 100644 --- a/notarize/upload_test.go +++ b/notarize/upload_test.go @@ -12,7 +12,6 @@ import ( func init() { childCommands["upload-success"] = testCmdUploadSuccess - childCommands["upload-errors"] = testCmdUploadErrors childCommands["upload-exit-status"] = testCmdUploadExitStatus } @@ -23,17 +22,7 @@ func TestUpload_success(t *testing.T) { }) require.NoError(t, err) - require.Equal(t, uuid, "edc8e846-d6ce-444d-9eef-499aa444da1c") -} - -func TestUpload_errors(t *testing.T) { - uuid, err := upload(context.Background(), &Options{ - Logger: hclog.L(), - BaseCmd: childCmd(t, "upload-errors"), - }) - - require.Error(t, err) - require.Empty(t, uuid) + require.Equal(t, uuid, "cfd69166-8e2f-1397-8636-ec06f98e3597") } func TestUpload_exitStatus(t *testing.T) { @@ -53,62 +42,15 @@ func testCmdUploadSuccess() int { - notarization-upload - - RequestUUID - edc8e846-d6ce-444d-9eef-499aa444da1c - - os-version - 10.15.1 - success-message - No errors uploading './terraform.zip'. - tool-path - /Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/Frameworks/AppStoreService.framework - tool-version - 4.00.1181 - - -`)) - return 0 -} - -// testCmdUploadErrors mimicks a successful submission. -func testCmdUploadErrors() int { - fmt.Println(strings.TrimSpace(` - - - - - os-version - 10.15.1 - product-errors - - - code - -18000 - message - ERROR ITMS-90732: "The software asset has already been uploaded. The upload ID is 671a0eb9-01b0-4966-b485-78b9f370abff" at SoftwareAssets/EnigmaSoftwareAsset - userInfo - - NSLocalizedDescription - ERROR ITMS-90732: "The software asset has already been uploaded. The upload ID is 671a0eb9-01b0-4966-b485-78b9f370abff" at SoftwareAssets/EnigmaSoftwareAsset - NSLocalizedFailureReason - ERROR ITMS-90732: "The software asset has already been uploaded. The upload ID is 671a0eb9-01b0-4966-b485-78b9f370abff" at SoftwareAssets/EnigmaSoftwareAsset - NSLocalizedRecoverySuggestion - ERROR ITMS-90732: "The software asset has already been uploaded. The upload ID is 671a0eb9-01b0-4966-b485-78b9f370abff" at SoftwareAssets/EnigmaSoftwareAsset - - - - tool-path - /Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/Frameworks/AppStoreService.framework - tool-version - 4.00.1181 + id + cfd69166-8e2f-1397-8636-ec06f98e3597 + message + Successfully uploaded file + path + /path/to/binary.zip `)) - - // Despite an error we return exit code 0 so we can test that we error - // in the presence of errors in the output. return 0 }