524 lines
12 KiB
Go
524 lines
12 KiB
Go
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
|
|
}
|