opentofu/terraform/debug.go

524 lines
12 KiB
Go
Raw Normal View History

package terraform
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"sync"
"time"
)
// DebugInfo is the global handler for writing the debug archive. All methods
// are safe to call concurrently. Setting DebugInfo to nil will disable writing
// the debug archive. All methods are safe to call on the nil value.
var dbug *debugInfo
// SetDebugInfo initializes the debug handler with a backing file in the
// provided directory. This must be called before any other terraform package
// operations or not at all. Once his is called, CloseDebugInfo should be
// called before program exit.
func SetDebugInfo(path string) error {
if os.Getenv("TF_DEBUG") == "" {
return nil
}
di, err := newDebugInfoFile(path)
if err != nil {
return err
}
dbug = di
return nil
}
// CloseDebugInfo is the exported interface to Close the debug info handler.
// The debug handler needs to be closed before program exit, so we export this
// function to be deferred in the appropriate entrypoint for our executable.
func CloseDebugInfo() error {
return dbug.Close()
}
// newDebugInfoFile initializes the global debug handler with a backing file in
// the provided directory.
func newDebugInfoFile(dir string) (*debugInfo, error) {
err := os.MkdirAll(dir, 0755)
if err != nil {
return nil, err
}
// FIXME: not guaranteed unique, but good enough for now
name := fmt.Sprintf("debug-%s", time.Now().Format("2006-01-02-15-04-05.999999999"))
archivePath := filepath.Join(dir, name+".tar.gz")
f, err := os.OpenFile(archivePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
return nil, err
}
return newDebugInfo(name, f)
}
// newDebugInfo initializes the global debug handler.
func newDebugInfo(name string, w io.Writer) (*debugInfo, error) {
gz := gzip.NewWriter(w)
d := &debugInfo{
name: name,
w: w,
gz: gz,
tar: tar.NewWriter(gz),
}
// create the subdirs we need
topHdr := &tar.Header{
Name: name,
Typeflag: tar.TypeDir,
Mode: 0755,
}
graphsHdr := &tar.Header{
Name: name + "/graphs",
Typeflag: tar.TypeDir,
Mode: 0755,
}
err := d.tar.WriteHeader(topHdr)
// if the first errors, the second will too
err = d.tar.WriteHeader(graphsHdr)
if err != nil {
return nil, err
}
return d, nil
}
// debugInfo provides various methods for writing debug information to a
// central archive. The debugInfo struct should be initialized once before any
// output is written, and Close should be called before program exit. All
// exported methods on debugInfo will be safe for concurrent use. The exported
// methods are also all safe to call on a nil pointer, so that there is no need
// for conditional blocks before writing debug information.
//
// Each write operation done by the debugInfo will flush the gzip.Writer and
// tar.Writer, and call Sync() or Flush() on the output writer as needed. This
// ensures that as much data as possible is written to storage in the event of
// a crash. The append format of the tar file, and the stream format of the
// gzip writer allow easy recovery f the data in the event that the debugInfo
// is not closed before program exit.
type debugInfo struct {
sync.Mutex
// archive root directory name
name string
// current operation phase
phase string
// step is monotonic counter for for recording the order of operations
step int
// flag to protect Close()
closed bool
// the debug log output is in a tar.gz format, written to the io.Writer w
w io.Writer
gz *gzip.Writer
tar *tar.Writer
}
// Set the name of the current operational phase in the debug handler. Each file
// in the archive will contain the name of the phase in which it was created,
// i.e. "input", "apply", "plan", "refresh", "validate"
func (d *debugInfo) SetPhase(phase string) {
if d == nil {
return
}
d.Lock()
defer d.Unlock()
d.phase = phase
}
// Close the debugInfo, finalizing the data in storage. This closes the
// tar.Writer, the gzip.Wrtier, and if the output writer is an io.Closer, it is
// also closed.
func (d *debugInfo) Close() error {
if d == nil {
return nil
}
d.Lock()
defer d.Unlock()
if d.closed {
return nil
}
d.closed = true
d.tar.Close()
d.gz.Close()
if c, ok := d.w.(io.Closer); ok {
return c.Close()
}
return nil
}
// debug buffer is an io.WriteCloser that will write itself to the debug
// archive when closed.
type debugBuffer struct {
debugInfo *debugInfo
name string
buf bytes.Buffer
}
func (b *debugBuffer) Write(d []byte) (int, error) {
return b.buf.Write(d)
}
func (b *debugBuffer) Close() error {
return b.debugInfo.WriteFile(b.name, b.buf.Bytes())
}
// ioutils only has a noop ReadCloser
type nopWriteCloser struct{}
func (nopWriteCloser) Write([]byte) (int, error) { return 0, nil }
func (nopWriteCloser) Close() error { return nil }
// NewFileWriter returns an io.WriteClose that will be buffered and written to
// the debug archive when closed.
func (d *debugInfo) NewFileWriter(name string) io.WriteCloser {
if d == nil {
return nopWriteCloser{}
}
return &debugBuffer{
debugInfo: d,
name: name,
}
}
type syncer interface {
Sync() error
}
type flusher interface {
Flush() error
}
// Flush the tar.Writer and the gzip.Writer. Flush() or Sync() will be called
// on the output writer if they are available.
func (d *debugInfo) flush() {
d.tar.Flush()
d.gz.Flush()
if f, ok := d.w.(flusher); ok {
f.Flush()
}
if s, ok := d.w.(syncer); ok {
s.Sync()
}
}
// WriteFile writes data as a single file to the debug arhive.
func (d *debugInfo) WriteFile(name string, data []byte) error {
if d == nil {
return nil
}
d.Lock()
defer d.Unlock()
return d.writeFile(name, data)
}
func (d *debugInfo) writeFile(name string, data []byte) error {
defer d.flush()
path := fmt.Sprintf("%s/%d-%s-%s", d.name, d.step, d.phase, name)
d.step++
hdr := &tar.Header{
Name: path,
Mode: 0644,
Size: int64(len(data)),
}
err := d.tar.WriteHeader(hdr)
if err != nil {
return err
}
_, err = d.tar.Write(data)
return err
}
// DebugHook implements all methods of the terraform.Hook interface, and writes
// the arguments to a file in the archive. When a suitable format for the
// argument isn't available, the argument is encoded using json.Marshal. If the
// debug handler is nil, all DebugHook methods are noop, so no time is spent in
// marshaling the data structures.
type DebugHook struct{}
func (*DebugHook) PreApply(ii *InstanceInfo, is *InstanceState, id *InstanceDiff) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId() + "\n")
}
if is != nil {
buf.WriteString(is.String() + "\n")
}
idCopy, err := id.Copy()
if err != nil {
return HookActionContinue, err
}
js, err := json.MarshalIndent(idCopy, "", " ")
if err != nil {
return HookActionContinue, err
}
buf.Write(js)
dbug.WriteFile("hook-PreApply", buf.Bytes())
return HookActionContinue, nil
}
func (*DebugHook) PostApply(ii *InstanceInfo, is *InstanceState, err error) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId() + "\n")
}
if is != nil {
buf.WriteString(is.String() + "\n")
}
if err != nil {
buf.WriteString(err.Error())
}
dbug.WriteFile("hook-PostApply", buf.Bytes())
return HookActionContinue, nil
}
func (*DebugHook) PreDiff(ii *InstanceInfo, is *InstanceState) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId() + "\n")
}
if is != nil {
buf.WriteString(is.String())
buf.WriteString("\n")
}
dbug.WriteFile("hook-PreDiff", buf.Bytes())
return HookActionContinue, nil
}
func (*DebugHook) PostDiff(ii *InstanceInfo, id *InstanceDiff) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId() + "\n")
}
idCopy, err := id.Copy()
if err != nil {
return HookActionContinue, err
}
js, err := json.MarshalIndent(idCopy, "", " ")
if err != nil {
return HookActionContinue, err
}
buf.Write(js)
dbug.WriteFile("hook-PostDiff", buf.Bytes())
return HookActionContinue, nil
}
func (*DebugHook) PreProvisionResource(ii *InstanceInfo, is *InstanceState) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId() + "\n")
}
if is != nil {
buf.WriteString(is.String())
buf.WriteString("\n")
}
dbug.WriteFile("hook-PreProvisionResource", buf.Bytes())
return HookActionContinue, nil
}
func (*DebugHook) PostProvisionResource(ii *InstanceInfo, is *InstanceState) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId())
buf.WriteString("\n")
}
if is != nil {
buf.WriteString(is.String())
buf.WriteString("\n")
}
dbug.WriteFile("hook-PostProvisionResource", buf.Bytes())
return HookActionContinue, nil
}
func (*DebugHook) PreProvision(ii *InstanceInfo, s string) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId())
buf.WriteString("\n")
}
buf.WriteString(s + "\n")
dbug.WriteFile("hook-PreProvision", buf.Bytes())
return HookActionContinue, nil
}
func (*DebugHook) PostProvision(ii *InstanceInfo, s string, err error) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId() + "\n")
}
buf.WriteString(s + "\n")
dbug.WriteFile("hook-PostProvision", buf.Bytes())
return HookActionContinue, nil
}
func (*DebugHook) ProvisionOutput(ii *InstanceInfo, s1 string, s2 string) {
if dbug == nil {
return
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId())
buf.WriteString("\n")
}
buf.WriteString(s1 + "\n")
buf.WriteString(s2 + "\n")
dbug.WriteFile("hook-ProvisionOutput", buf.Bytes())
}
func (*DebugHook) PreRefresh(ii *InstanceInfo, is *InstanceState) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId() + "\n")
}
if is != nil {
buf.WriteString(is.String())
buf.WriteString("\n")
}
dbug.WriteFile("hook-PreRefresh", buf.Bytes())
return HookActionContinue, nil
}
func (*DebugHook) PostRefresh(ii *InstanceInfo, is *InstanceState) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId())
buf.WriteString("\n")
}
if is != nil {
buf.WriteString(is.String())
buf.WriteString("\n")
}
dbug.WriteFile("hook-PostRefresh", buf.Bytes())
return HookActionContinue, nil
}
func (*DebugHook) PreImportState(ii *InstanceInfo, s string) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId())
buf.WriteString("\n")
}
buf.WriteString(s + "\n")
dbug.WriteFile("hook-PreImportState", buf.Bytes())
return HookActionContinue, nil
}
func (*DebugHook) PostImportState(ii *InstanceInfo, iss []*InstanceState) (HookAction, error) {
if dbug == nil {
return HookActionContinue, nil
}
var buf bytes.Buffer
if ii != nil {
buf.WriteString(ii.HumanId() + "\n")
}
for _, is := range iss {
if is != nil {
buf.WriteString(is.String() + "\n")
}
}
dbug.WriteFile("hook-PostImportState", buf.Bytes())
return HookActionContinue, nil
}
// skip logging this for now, since it could be huge
func (*DebugHook) PostStateUpdate(*State) (HookAction, error) {
return HookActionContinue, nil
}