Files
Packer-Cn/packer/telemetry.go
Wilken Rivera f8ebf69c0d telemetry: Add nil check in SetBundledUsage
Invoking Packer with the  CHECKPOINT_DISABLE env. variable the telemetry reporter is left uninitialized in order to disable telemetry reporting.
Any method calls on the nil reporter is expected to check if the reporter is active or in NOOP mode. This change fixes a crash when calling SetBundledUsage()
on a nil CheckpointTelemetry type that occurs when using a bundled plugin with CHECKPOINT_DISABLE=1.
2023-08-18 10:19:31 -04:00

258 lines
5.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package packer
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"time"
checkpoint "github.com/hashicorp/go-checkpoint"
"github.com/hashicorp/packer-plugin-sdk/pathing"
packerVersion "github.com/hashicorp/packer/version"
"github.com/zclconf/go-cty/cty"
)
type PackerTemplateType string
const (
UnknownTemplate PackerTemplateType = "Unknown"
HCL2Template PackerTemplateType = "HCL2"
JSONTemplate PackerTemplateType = "JSON"
)
const TelemetryVersion string = "beta/packer/7"
const TelemetryPanicVersion string = "beta/packer_panic/4"
var CheckpointReporter *CheckpointTelemetry
type PackerReport struct {
Spans []*TelemetrySpan `json:"spans"`
ExitCode int `json:"exit_code"`
Error string `json:"error"`
Command string `json:"command"`
TemplateType PackerTemplateType `json:"template_type"`
UseBundled bool `json:"use_bundled"`
}
type CheckpointTelemetry struct {
spans []*TelemetrySpan
signatureFile string
startTime time.Time
templateType PackerTemplateType
useBundled bool
}
func NewCheckpointReporter(disableSignature bool) *CheckpointTelemetry {
if disabled := os.Getenv("CHECKPOINT_DISABLE"); disabled != "" {
return nil
}
configDir, err := pathing.ConfigDir()
if err != nil {
log.Printf("[WARN] (telemetry) setup error: %s", err)
return nil
}
signatureFile := ""
if disableSignature {
log.Printf("[INFO] (telemetry) Checkpoint signature disabled")
} else {
signatureFile = filepath.Join(configDir, "checkpoint_signature")
}
return &CheckpointTelemetry{
signatureFile: signatureFile,
startTime: time.Now().UTC(),
templateType: UnknownTemplate,
}
}
func (c *CheckpointTelemetry) baseParams(prefix string) *checkpoint.ReportParams {
version := packerVersion.Version
if packerVersion.VersionPrerelease != "" {
version += "-" + packerVersion.VersionPrerelease
}
return &checkpoint.ReportParams{
Product: "packer",
SchemaVersion: prefix,
StartTime: c.startTime,
Version: version,
RunID: os.Getenv("PACKER_RUN_UUID"),
SignatureFile: c.signatureFile,
}
}
func (c *CheckpointTelemetry) ReportPanic(m string) error {
if c == nil {
return nil
}
panicParams := c.baseParams(TelemetryPanicVersion)
panicParams.Payload = m
panicParams.EndTime = time.Now().UTC()
// This timeout can be longer because it runs in the real main.
// We're also okay waiting a bit longer to collect panic information
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
return checkpoint.Report(ctx, panicParams)
}
func (c *CheckpointTelemetry) AddSpan(name, pluginType string, options interface{}) *TelemetrySpan {
if c == nil {
return nil
}
log.Printf("[INFO] (telemetry) Starting %s %s", pluginType, name)
ts := &TelemetrySpan{
Name: name,
Options: flattenConfigKeys(options),
StartTime: time.Now().UTC(),
Type: pluginType,
}
c.spans = append(c.spans, ts)
return ts
}
// SetTemplateType registers the template type being processed for a Packer command
func (c *CheckpointTelemetry) SetTemplateType(t PackerTemplateType) {
if c == nil {
return
}
c.templateType = t
}
// SetBundledUsage marks the template as using bundled plugins
func (c *CheckpointTelemetry) SetBundledUsage() {
if c == nil {
return
}
c.useBundled = true
}
func (c *CheckpointTelemetry) Finalize(command string, errCode int, err error) error {
if c == nil {
return nil
}
params := c.baseParams(TelemetryVersion)
params.EndTime = time.Now().UTC()
extra := &PackerReport{
Spans: c.spans,
ExitCode: errCode,
Command: command,
}
if err != nil {
extra.Error = err.Error()
}
extra.UseBundled = c.useBundled
extra.TemplateType = c.templateType
params.Payload = extra
// b, _ := json.MarshalIndent(params, "", " ")
// log.Println(string(b))
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
log.Printf("[INFO] (telemetry) Finalizing.")
return checkpoint.Report(ctx, params)
}
type TelemetrySpan struct {
EndTime time.Time `json:"end_time"`
Error string `json:"error"`
Name string `json:"name"`
Options []string `json:"options"`
StartTime time.Time `json:"start_time"`
Type string `json:"type"`
}
func (s *TelemetrySpan) End(err error) {
if s == nil {
return
}
s.EndTime = time.Now().UTC()
log.Printf("[INFO] (telemetry) ending %s", s.Name)
if err != nil {
s.Error = err.Error()
}
}
func flattenConfigKeys(options interface{}) []string {
var flatten func(string, interface{}) []string
flatten = func(prefix string, options interface{}) (strOpts []string) {
switch opt := options.(type) {
case map[string]interface{}:
return flattenJSON(prefix, options)
case cty.Value:
return flattenHCL(prefix, opt)
default:
return nil
}
}
flattened := flatten("", options)
sort.Strings(flattened)
return flattened
}
func flattenJSON(prefix string, options interface{}) (strOpts []string) {
if m, ok := options.(map[string]interface{}); ok {
for k, v := range m {
if prefix != "" {
k = prefix + "/" + k
}
if n, ok := v.(map[string]interface{}); ok {
strOpts = append(strOpts, flattenJSON(k, n)...)
} else {
strOpts = append(strOpts, k)
}
}
}
return
}
func flattenHCL(prefix string, v cty.Value) (args []string) {
if v.IsNull() {
return []string{}
}
t := v.Type()
switch {
case t.IsObjectType(), t.IsMapType():
if !v.IsKnown() {
return []string{}
}
it := v.ElementIterator()
for it.Next() {
key, val := it.Element()
keyStr := key.AsString()
if val.IsNull() {
continue
}
if prefix != "" {
keyStr = fmt.Sprintf("%s/%s", prefix, keyStr)
}
if val.Type().IsObjectType() || val.Type().IsMapType() {
args = append(args, flattenHCL(keyStr, val)...)
} else {
args = append(args, keyStr)
}
}
}
return args
}