forked from hashicorp/go-tfe
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request hashicorp#766 from hashicorp/TF-9075-provide-a-go-…
…tfe-example-showing-how-to-create-a-run-and-read-a-run-log-filtering-the-structured-log-for-error-severity-items Add example for parsing run errors
- Loading branch information
Showing
3 changed files
with
185 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
## Example: Parsing Run Errors | ||
|
||
In this example, you'll use terraform to create a run with errors on Terraform Cloud, then | ||
execute the command to read the plan log and filter it for errors. It's important to use | ||
Terraform to create the run, otherwise you will not get the structured log that this code | ||
example requires. | ||
|
||
#### Instructions | ||
|
||
1. Change to the terraform directory, and run terraform init using Terraform 1.3+ | ||
|
||
`cd terraform` | ||
`TF_CLOUD_ORGANIZATION="yourorg" terraform init` | ||
|
||
2. Apply the changes (You should see an error "Error making request" or similar) | ||
|
||
`TF_CLOUD_ORGANIZATION="yourorg" terraform apply` | ||
|
||
3. Notice the run ID in the URL (it begins with "run-") and execute the example with the run ID as a flag: | ||
|
||
`cd ../` | ||
`TFE_TOKEN="YOURTOKEN" go run main.go run-RUN_ID_FROM_URL_ABOVE` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package main | ||
|
||
import ( | ||
"bufio" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"log" | ||
"os" | ||
"time" | ||
|
||
tfe "github.com/hashicorp/go-tfe" | ||
) | ||
|
||
var ( | ||
pollInterval = 500 * time.Millisecond | ||
) | ||
|
||
// Diagnostic represents a diagnostic type message from Terraform, which is how errors | ||
// are usually represented. | ||
type Diagnostic struct { | ||
Severity string `json:"severity"` | ||
Summary string `json:"summary"` | ||
Detail string `json:"detail"` | ||
Address string `json:"address,omitempty"` | ||
Range *DiagnosticRange `json:"range,omitempty"` | ||
} | ||
|
||
// Pos represents a position in the source code. | ||
type Pos struct { | ||
// Line is a one-based count for the line in the indicated file. | ||
Line int `json:"line"` | ||
|
||
// Column is a one-based count of Unicode characters from the start of the line. | ||
Column int `json:"column"` | ||
|
||
// Byte is a zero-based offset into the indicated file. | ||
Byte int `json:"byte"` | ||
} | ||
|
||
// DiagnosticRange represents the filename and position of the diagnostic subject. | ||
type DiagnosticRange struct { | ||
Filename string `json:"filename"` | ||
Start Pos `json:"start"` | ||
End Pos `json:"end"` | ||
} | ||
|
||
// For full decoding, see https://github.com/hashicorp/terraform/blob/main/internal/command/jsonformat/renderer.go | ||
type JSONLog struct { | ||
Message string `json:"@message"` | ||
Level string `json:"@level"` | ||
Timestamp string `json:"@timestamp"` | ||
Type string `json:"type"` | ||
Diagnostic *Diagnostic `json:"diagnostic"` | ||
} | ||
|
||
// Given a | ||
func logErrorsOnly(reader io.Reader) { | ||
scanner := bufio.NewScanner(reader) | ||
for scanner.Scan() { | ||
var jsonLog JSONLog | ||
err := json.Unmarshal([]byte(scanner.Text()), &jsonLog) | ||
// It's possible this log is not encoded as JSON at all, so errors will be ignored. | ||
if err == nil && jsonLog.Level == "error" { | ||
fmt.Println() | ||
fmt.Println("--- Error Message") | ||
fmt.Println(jsonLog.Message) | ||
fmt.Println("---") | ||
fmt.Println() | ||
if jsonLog.Type == "diagnostic" { | ||
fmt.Println("--- Diagnostic Details") | ||
fmt.Println(jsonLog.Diagnostic.Detail) | ||
fmt.Println("---") | ||
fmt.Println() | ||
} | ||
} | ||
} | ||
} | ||
|
||
func logRunErrors(ctx context.Context, client *tfe.Client, run *tfe.Run) { | ||
var reader io.Reader | ||
var err error | ||
|
||
if run.Apply != nil && run.Apply.Status == tfe.ApplyErrored { | ||
log.Printf("Reading apply logs from %q", run.Apply.LogReadURL) | ||
reader, err = client.Applies.Logs(ctx, run.Apply.ID) | ||
} else if run.Plan != nil && run.Plan.Status == tfe.PlanErrored { | ||
log.Printf("Reading apply logs from %q", run.Plan.LogReadURL) | ||
reader, err = client.Plans.Logs(ctx, run.Plan.ID) | ||
} else { | ||
log.Fatal("Failed to find an errored plan or apply.") | ||
} | ||
|
||
if err != nil { | ||
log.Fatal("Failed to read error log: ", err) | ||
} | ||
|
||
logErrorsOnly(reader) | ||
} | ||
|
||
func readRun(ctx context.Context, client *tfe.Client, id string) *tfe.Run { | ||
r, err := client.Runs.ReadWithOptions(ctx, id, &tfe.RunReadOptions{ | ||
Include: []tfe.RunIncludeOpt{tfe.RunApply, tfe.RunPlan}, | ||
}) | ||
if err != nil { | ||
log.Fatal("Failed to read specified run: ", err) | ||
} | ||
return r | ||
} | ||
|
||
func main() { | ||
if len(os.Args) < 2 { | ||
fmt.Println("Usage:") | ||
fmt.Printf("\t%s <run ID>\n", os.Args[0]) | ||
os.Exit(1) | ||
} | ||
|
||
ctx := context.Background() | ||
client, err := tfe.NewClient(&tfe.Config{ | ||
Address: "https://app.terraform.io", | ||
RetryServerErrors: true, | ||
}) | ||
if err != nil { | ||
log.Fatal("Failed to initialize client: ", err) | ||
} | ||
|
||
r := readRun(ctx, client, os.Args[1]) | ||
|
||
poll: | ||
for { | ||
<-time.After(pollInterval) | ||
|
||
r := readRun(ctx, client, r.ID) | ||
|
||
switch r.Status { | ||
case tfe.RunApplied: | ||
fmt.Println("Run finished!") | ||
case tfe.RunErrored: | ||
fmt.Println("Run had errors!") | ||
logRunErrors(ctx, client, r) | ||
break poll | ||
default: | ||
fmt.Printf("Waiting for run to error... Run status was %q...\n", r.Status) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
terraform { | ||
cloud { | ||
workspaces { | ||
name = "go-tfe-examples-run_errors" | ||
} | ||
} | ||
} | ||
|
||
# The following example should return an error | ||
data "http" "example_head" { | ||
url = "https://this-shall-not-exist.hashicorp.com/example" | ||
method = "GET" | ||
} |