diff --git a/cmd/codex/upload.go b/cmd/codex/upload.go index b532a92..14d3f5e 100644 --- a/cmd/codex/upload.go +++ b/cmd/codex/upload.go @@ -1,22 +1,26 @@ package codex import ( + "context" "fmt" "github.com/fatih/color" "github.com/pathbird/pbauthor/internal/api" "github.com/pathbird/pbauthor/internal/auth" "github.com/pathbird/pbauthor/internal/codex" - "github.com/pathbird/pbauthor/internal/config" + "github.com/pathbird/pbauthor/internal/graphql" "github.com/pathbird/pbauthor/internal/prompt" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "os" "path/filepath" + "time" ) // flag vars var ( skipConfirmation bool + noWait bool ) var codexUploadCmd = &cobra.Command{ @@ -44,7 +48,7 @@ var codexUploadCmd = &cobra.Command{ // the name of the codex and the course it's being uploaded to. if !skipConfirmation { if !prompt.Confirm("Upload codex?") { - _, _ = fmt.Fprintln(os.Stderr, red("Upload aborted.")) + _, _ = fmt.Fprintln(os.Stderr, failf("Upload aborted.")) os.Exit(1) } } @@ -57,9 +61,13 @@ var codexUploadCmd = &cobra.Command{ return err } if parseErr != nil { - _, _ = fmt.Fprintf(os.Stderr, "Failed to parse codex (%d issues):\n", len(parseErr.Errors)) + _, _ = fmt.Fprintf( + os.Stderr, + "Failed to parse codex (%d issues):\n", + len(parseErr.Errors), + ) for _, e := range parseErr.Errors { - _, _ = fmt.Fprintf(os.Stderr, "- %s\n (%s", red(e.Message), blue(e.Error)) + _, _ = fmt.Fprintf(os.Stderr, "- %s\n (%s", failf(e.Message), blue(e.Error)) if e.SourcePosition != "" { _, _ = fmt.Fprintf(os.Stderr, " at %s", cyan(e.SourcePosition)) } @@ -73,21 +81,69 @@ var codexUploadCmd = &cobra.Command{ os.Exit(1) } - fmt.Printf("codexId: %s\n", res.CodexId) - fmt.Printf("url: %s/codex/%s\n", config.PathbirdApiHost, res.CodexId) + detailsUrl := fmt.Sprintf("https://pathbird.com/codex/%s/details", res.CodexId) + + if !noWait { + start := time.Now() + timeoutCtx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) + defer cancel() + log.Info("waiting for kernel build to complete (this may take up to 20 minutes)...") + kernelStatus, err := codex.WaitForKernelBuildCompleted( + timeoutCtx, + graphql.NewClient(auth), + res.CodexId, + ) + if err != nil { + log.WithError( + err, + ).Error( + "Something went wrong while trying to check the status of the codex.", + ) + return err + } + log.Infof("waited %s for kernel build process", time.Since(start)) + if kernelStatus.BuildStatus != "built" { + _, _ = fmt.Fprint(os.Stderr, failf( + "Failed to build kernel (got status: %s): %s\n", + kernelStatus.BuildStatus, + detailsUrl, + )) + return errors.Errorf( + "failed to build kernel (got status: %s)", + kernelStatus.BuildStatus, + ) + } + } else { + log.Info("not waiting for kernel build to complete (--no-wait was set)") + } + + fmt.Printf(successf("Successfully uploaded codex: %s", detailsUrl)) return nil }, } func init() { - codexUploadCmd.Flags().BoolVarP(&skipConfirmation, "yes", "y", false, "don't ask for confirmation") + codexUploadCmd.Flags().BoolVarP( + &skipConfirmation, + "yes", + "y", + false, + "don't ask for confirmation", + ) + codexUploadCmd.Flags().BoolVar( + &noWait, + "no-wait", + false, + "don't wait for the kernel build process to complete", + ) Cmd.AddCommand(codexUploadCmd) } var ( - red = color.New(color.FgRed, color.Bold).SprintFunc() - cyan = color.New(color.FgCyan).SprintFunc() - faint = color.New(color.Faint).SprintFunc() - blue = color.New(color.FgBlue).SprintfFunc() + failf = color.New(color.FgRed, color.Bold).SprintfFunc() + successf = color.New(color.FgGreen, color.Bold).SprintfFunc() + cyan = color.New(color.FgCyan).SprintFunc() + faint = color.New(color.Faint).SprintFunc() + blue = color.New(color.FgBlue).SprintfFunc() ) diff --git a/internal/api/client.go b/internal/api/client.go index 27cf05e..de3b95a 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -29,6 +29,10 @@ func New(authToken string) *Client { } } +func (c *Client) Auth() string { + return c.authToken +} + type request struct { route string body interface{} @@ -117,7 +121,11 @@ func (r *response) unmarshalErrorBody() (*ErrorResponse, error) { Details: nil, }, nil } - return nil, errors.Errorf("unknown api error response (status: %s, content-type: %s)", r.httpResponse.Status, contentType) + return nil, errors.Errorf( + "unknown api error response (status: %s, content-type: %s)", + r.httpResponse.Status, + contentType, + ) } var errorResponse ErrorResponse diff --git a/internal/api/upload_codex.go b/internal/api/codex_upload.go similarity index 100% rename from internal/api/upload_codex.go rename to internal/api/codex_upload.go diff --git a/internal/codex/details.go b/internal/codex/details.go new file mode 100644 index 0000000..bd34c99 --- /dev/null +++ b/internal/codex/details.go @@ -0,0 +1,89 @@ +package codex + +import ( + "context" + "github.com/pathbird/pbauthor/internal/graphql" + "github.com/pathbird/pbauthor/internal/graphql/transport" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + time "time" +) + +type Details struct { + Name string + KernelSpec KernelSpec `json:"kernelSpec"` +} + +type KernelSpec struct { + ID string `json:"id"` + BuildStatus string `json:"buildStatus"` + Events []string `json:"events"` + BuildLog []string `json:"buildLog"` +} + +const waitForBuildStatusQuery = ` +query pbauthor_CodexBuildStatus($id: ID!) { + node(id: $id) { ... on CodexMetadata { + id + name + kernelSpec { + id + buildStatus + events + buildLog + } + }} +} +` + +// WaitForKernelBuildCompleted polls the API server until the kernel build is completed. +func WaitForKernelBuildCompleted( + ctx context.Context, + client *graphql.Client, + codexId string, +) (*KernelSpec, error) { + for { + log.WithField("codex_id", codexId).Debug("querying kernel build status") + status, err := queryKernelStatus(ctx, client, codexId) + if err != nil { + return nil, err + } + if status.BuildStatus != "pending" { + return status, nil + } + + // sleep for a few seconds, then try again + select { + case <-time.After(5 * time.Second): + // pass + case <-ctx.Done(): + return nil, ctx.Err() + } + } +} + +func queryKernelStatus( + ctx context.Context, + client *graphql.Client, + codexId string, +) (*KernelSpec, error) { + req := transport.NewRequest(waitForBuildStatusQuery) + req.Var("id", codexId) + var res struct { + Node struct { + ID string + Name string + KernelSpec KernelSpec + } + } + if err := client.Run(ctx, req, &res); err != nil { + return nil, err + } + if res.Node.ID == "" { + return nil, errors.Errorf("codex (id: %s) could not be found", codexId) + } + if res.Node.KernelSpec.ID == "" { + return nil, errors.Errorf("query didn't return KernelSpec data (for codex: %s)", codexId) + } + return &res.Node.KernelSpec, nil +}